basic: RANDOMIZE, WHILE/WEND, RESTORE line, BASIC80 (17, 56, 67)

This commit is contained in:
Steven Hugg 2020-08-12 11:57:54 -05:00
parent 0fd04658cb
commit 45ab88611e
4 changed files with 287 additions and 56 deletions

View File

@ -33,7 +33,9 @@ export interface BASICOptions {
// CONTROL FLOW
testInitialFor : boolean; // can we skip a NEXT statement? (can't interleave tho)
optionalNextVar : boolean; // can do NEXT without variable
ifElse : boolean; // IF...ELSE construct
multipleNextVars : boolean; // NEXT J,I
checkOnGotoIndex : boolean; // fatal error when ON..GOTO index out of bounds
restoreWithLabel : boolean; // RESTORE <label>
// MISC
commandsPerSec? : number; // how many commands per second?
}
@ -43,9 +45,11 @@ export interface SourceLocated {
}
export class CompileError extends Error {
constructor(msg: string) {
$loc : SourceLocation;
constructor(msg: string, loc: SourceLocation) {
super(msg);
Object.setPrototypeOf(this, CompileError.prototype);
this.$loc = loc;
}
}
@ -154,6 +158,15 @@ export interface NEXT_Statement {
lexpr?: IndOp;
}
export interface WHILE_Statement {
command: "WHILE";
cond: Expr;
}
export interface WEND_Statement {
command: "WEND";
}
export interface INPUT_Statement {
command: "INPUT";
prompt: Expr;
@ -170,6 +183,11 @@ export interface READ_Statement {
args: IndOp[];
}
export interface RESTORE_Statement {
command: "RESTORE";
label: Expr;
}
export interface DEF_Statement {
command: "DEF";
lexpr: IndOp;
@ -182,7 +200,7 @@ export interface OPTION_Statement {
optargs: string[];
}
export interface GET_Statement {
export interface GET_Statement { // applesoft only?
command: "GET";
lexpr: IndOp;
}
@ -194,7 +212,8 @@ export interface NoArgStatement {
export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement
| IF_Statement | FOR_Statement | NEXT_Statement | DIM_Statement
| INPUT_Statement | READ_Statement | DEF_Statement | ONGOTO_Statement
| DATA_Statement | OPTION_Statement | NoArgStatement;
| DATA_Statement | OPTION_Statement | GET_Statement | RESTORE_Statement
| NoArgStatement;
export type Statement = StatementTypes & SourceLocated;
@ -294,7 +313,7 @@ export class BASICParser {
compileError(msg: string, loc?: SourceLocation) {
if (!loc) loc = this.peekToken().$loc;
this.errors.push({path:loc.path, line:loc.line, label:this.curlabel, start:loc.start, end:loc.end, msg:msg});
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
throw new CompileError(msg, loc);
}
dialectError(what: string, loc?: SourceLocation) {
this.compileError(`The selected BASIC dialect (${this.opts.dialectName}) doesn't support ${what}.`, loc); // TODO
@ -376,7 +395,9 @@ export class BASICParser {
tokenize(line: string) : void {
this.lineno++;
this.tokens = [];
// split identifier regex (if token-crunching enabled)
let splitre = this.opts.optionalWhitespace && new RegExp(this.opts.validKeywords.map(s => `^${s}`).join('|'));
// iterate over each token via re_toks regex
var m : RegExpMatchArray;
while (m = re_toks.exec(line)) {
for (var i = 1; i < TokenType._LAST; i++) {
@ -389,7 +410,7 @@ export class BASICParser {
// uppercase all identifiers, and maybe more
if (i == TokenType.Ident || i == TokenType.HexOctalInt || this.opts.uppercaseOnly)
s = s.toUpperCase();
// un-crunch tokens?
// un-crunch tokens? (TODO: still doesn't handle reserved words inside of variables)
if (splitre) {
while (i == TokenType.Ident) {
let m2 = splitre.exec(s);
@ -673,7 +694,7 @@ export class BASICParser {
var thengoto = this.expectTokens(['THEN','GOTO']);
var lineno = this.peekToken();
// assume GOTO if number given after THEN
if (lineno.type == TokenType.Int && thengoto.str == 'THEN') {
if (lineno.type == TokenType.Int) {
this.pushbackToken({type:TokenType.Ident, str:'GOTO', $loc:lineno.$loc});
}
// add fake ":"
@ -681,7 +702,6 @@ export class BASICParser {
return { command: "IF", cond: cond };
}
stmt__ELSE(): ELSE_Statement {
if (!this.opts.ifElse) this.dialectError(`IF...ELSE statements`);
var lineno = this.peekToken();
// assume GOTO if number given after ELSE
if (lineno.type == TokenType.Int) {
@ -705,11 +725,25 @@ export class BASICParser {
}
stmt__NEXT() : NEXT_Statement {
var lexpr = null;
if (!isEOS(this.peekToken())) {
// NEXT var might be optional
if (!this.opts.optionalNextVar || !isEOS(this.peekToken())) {
lexpr = this.parseForNextLexpr();
// convert ',' to ':' 'NEXT'
if (this.opts.multipleNextVars && this.peekToken().str == ',') {
this.consumeToken(); // consume ','
this.tokens.unshift({type:TokenType.Ident, str:'NEXT', $loc:this.peekToken().$loc});
this.tokens.unshift({type:TokenType.Operator, str:':', $loc:this.peekToken().$loc});
}
}
return { command:'NEXT', lexpr:lexpr };
}
stmt__WHILE(): WHILE_Statement {
var cond = this.parseExpr();
return { command:'WHILE', cond:cond };
}
stmt__WEND(): WEND_Statement {
return { command:'WEND' };
}
stmt__DIM() : DIM_Statement {
var lexprs = this.parseLexprList();
lexprs.forEach((arr) => {
@ -738,8 +772,11 @@ export class BASICParser {
stmt__READ() : READ_Statement {
return { command:'READ', args:this.parseLexprList() };
}
stmt__RESTORE() {
return { command:'RESTORE' };
stmt__RESTORE() : RESTORE_Statement {
var label = null;
if (this.opts.restoreWithLabel && !isEOS(this.peekToken()))
label = this.parseLabel();
return { command:'RESTORE', label:label };
}
stmt__RETURN() {
return { command:'RETURN' };
@ -779,6 +816,9 @@ export class BASICParser {
stmt__CLEAR() : NoArgStatement {
return { command:'CLEAR' };
}
stmt__RANDOMIZE() : NoArgStatement {
return { command:'RANDOMIZE' };
}
// TODO: CHANGE A TO A$ (4th edition, A(0) is len and A(1..) are chars)
stmt__OPTION() : OPTION_Statement {
var tokname = this.consumeToken();
@ -890,19 +930,26 @@ export const ECMA55_MINIMAL : BASICOptions = {
maxStringLength : 255,
tickComments : false,
hexOctalConsts : false,
validKeywords : ['BASE','DATA','DEF','DIM','END',
validKeywords : [
'BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO'
],
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'],
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
validFunctions : [
'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'
],
validOperators : [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'
],
printZoneLength : 15,
numericPadding : true,
checkOverflow : true,
testInitialFor : true,
optionalNextVar : false,
ifElse : false,
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : true,
restoreWithLabel : false,
}
export const BASICODE : BASICOptions = {
@ -924,20 +971,27 @@ export const BASICODE : BASICOptions = {
maxStringLength : 255,
tickComments : false,
hexOctalConsts : false,
validKeywords : ['BASE','DATA','DEF','DIM','END',
validKeywords : [
'BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO'
],
validFunctions : ['ABS','ASC','ATN','CHR$','COS','EXP','INT','LEFT$','LEN','LOG',
'MID$','RIGHT$','SGN','SIN','SQR','TAB','TAN','VAL'],
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'],
validFunctions : [
'ABS','ASC','ATN','CHR$','COS','EXP','INT','LEFT$','LEN','LOG',
'MID$','RIGHT$','SGN','SIN','SQR','TAB','TAN','VAL'
],
validOperators : [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'
],
printZoneLength : 15,
numericPadding : true,
checkOverflow : true,
testInitialFor : true,
optionalNextVar : false,
ifElse : false,
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : true,
restoreWithLabel : false,
}
export const ALTAIR_BASIC41 : BASICOptions = {
@ -965,7 +1019,9 @@ export const ALTAIR_BASIC41 : BASICOptions = {
'FOR','GOTO','GOSUB','IF','THEN','ELSE','INPUT','LET','LINE',
'PRINT','LPRINT','USING','NEXT','ON','OUT','POKE',
'READ','REM','RESTORE','RESUME','RETURN','STOP','SWAP',
'TROFF','TRON','WAIT'],
'TROFF','TRON','WAIT',
'TO','STEP',
],
validFunctions : null, // all
validOperators : null, // all
printZoneLength : 15,
@ -973,9 +1029,10 @@ export const ALTAIR_BASIC41 : BASICOptions = {
checkOverflow : true,
testInitialFor : false,
optionalNextVar : true,
//multipleNextVars : true, // TODO: not supported
ifElse : true,
multipleNextVars : true,
bitwiseLogic : true,
checkOnGotoIndex : false,
restoreWithLabel : false,
}
export const APPLESOFT_BASIC : BASICOptions = {
@ -1000,25 +1057,81 @@ export const APPLESOFT_BASIC : BASICOptions = {
validKeywords : [
'OPTION',
'CLEAR','LET','DIM','DEF','GOTO','GOSUB','RETURN','ON','POP',
'FOR','TO','NEXT','IF','THEN','END','STOP','ONERR','RESUME',
'FOR','NEXT','IF','THEN','END','STOP','ONERR','RESUME',
'PRINT','INPUT','GET','HOME','HTAB','VTAB',
'INVERSE','FLASH','NORMAL','TEXT',
'GR','COLOR','PLOT','HLIN','VLIN',
'HGR','HGR2','HPLOT','HCOLOR','AT',
'DATA','READ','RESTORE',
'REM','TRACE','NOTRACE'],
'REM','TRACE','NOTRACE',
'TO','STEP',
],
validFunctions : [
'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN',
'LEN','LEFT$','MID$','RIGHT$','STR$','VAL','CHR$','ASC',
'FRE','SCRN','PDL','PEEK','POS'],
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'],
'FRE','SCRN','PDL','PEEK','POS'
],
validOperators : [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'
],
printZoneLength : 16,
numericPadding : false,
checkOverflow : true,
testInitialFor : false,
optionalNextVar : true,
ifElse : false,
multipleNextVars : true,
bitwiseLogic : false,
checkOnGotoIndex : false,
restoreWithLabel : false,
}
export const BASIC80 : BASICOptions = {
dialectName: "BASIC80",
asciiOnly : true,
uppercaseOnly : false,
optionalLabels : false,
optionalWhitespace : true,
varNaming : "*",
staticArrays : false,
sharedArrayNamespace : true,
defaultArrayBase : 0,
defaultArraySize : 11,
defaultValues : true,
stringConcat : true,
typeConvert : false,
maxDimensions : 255,
maxDefArgs : 255,
maxStringLength : 255,
//maxElements : 32767, // TODO
tickComments : true,
hexOctalConsts : true,
validKeywords : [
'OPTION',
'CONSOLE','DATA','DEF','DEFUSR','DIM','END','ERASE','ERROR',
'FOR','GOTO','GOSUB','IF','THEN','ELSE','INPUT','LET','LINE',
'PRINT','LPRINT','USING','NEXT','ON','OUT','POKE',
'READ','REM','RESTORE','RESUME','RETURN','STOP','SWAP',
'TROFF','TRON','WAIT',
'CALL','CHAIN','COMMON','WHILE','WEND','WRITE','RANDOMIZE',
'TO','STEP',
],
validFunctions : [
'ABS','ASC','ATN','CDBL','CHR$','CINT','COS','CSNG','CVI','CVS','CVD',
'EOF','EXP','FIX','FRE','HEX$','INP','INPUT$','INSTR','INT',
'LEFT$','LEN','LOC','LOG','LPOS','MID$','MKI$','MKS$','MKD$',
'OCT$','PEEK','POS','RIGHT$','RND','SGN','SIN','SPACE$','SPC',
'SQR','STR$','STRING$','TAB','TAN','USR','VAL','VARPTR'
],
validOperators : null, // all
printZoneLength : 14,
numericPadding : true,
checkOverflow : false, // TODO: message displayed when overflow, division by zero = ok
testInitialFor : true,
optionalNextVar : true,
multipleNextVars : true,
bitwiseLogic : true,
checkOnGotoIndex : false,
restoreWithLabel : true,
}
export const MODERN_BASIC : BASICOptions = {
@ -1048,8 +1161,10 @@ export const MODERN_BASIC : BASICOptions = {
checkOverflow : true,
testInitialFor : true,
optionalNextVar : true,
ifElse : true,
multipleNextVars : true,
bitwiseLogic : true,
checkOnGotoIndex : false,
restoreWithLabel : true,
}
// TODO: integer vars
@ -1063,5 +1178,6 @@ export const DIALECTS = {
"MINIMAL": ECMA55_MINIMAL,
"BASICODE": BASICODE,
"APPLESOFT": APPLESOFT_BASIC,
"BASIC80": BASIC80,
"MODERN": MODERN_BASIC,
};

View File

@ -38,11 +38,11 @@ try {
var pgm = parser.parseFile(data, filename);
} catch (e) {
if (parser.errors.length == 0)
console.log("@@@ " + e.msg);
console.log(`@@@ ${e}`);
else
console.log(e);
}
parser.errors.forEach((err) => console.log("@@@ " + err.msg));
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
if (parser.errors.length) process.exit(2);
// run program
@ -55,13 +55,16 @@ runtime.input = async (prompt:string) => {
return new Promise( (resolve, reject) => {
function answered(answer) {
var vals = answer.toUpperCase().split(',');
console.log(">>>",vals);
//console.log(">>>",vals);
resolve(vals);
}
fs.writeSync(1, prompt+"?");
prompt += ' ?';
if (inputlines.length) {
fs.writeSync(1, prompt);
fs.writeSync(1, '\n');
answered(inputlines.shift());
} else rl.question(prompt, (answer) => {
fs.writeSync(1, '\n');
answered(answer);
});
});
@ -72,7 +75,7 @@ runtime.resume = function() {
if (runtime.step()) {
if (runtime.running) runtime.resume();
} else if (runtime.exited) {
console.log("*** PROGRAM EXITED ***");
//console.log("*** PROGRAM EXITED ***");
process.exit(0);
}
} catch (e) {

View File

@ -31,6 +31,38 @@ function isArray(obj) {
return obj != null && (Array.isArray(obj) || obj.BYTES_PER_ELEMENT);
}
class RNG {
next : () => number;
seed : (aa,bb,cc,dd) => void;
randomize() {
this.seed(Math.random()*0x7fffffff, Math.random()*0x7fffffff, Math.random()*0x7fffffff, Math.random()*0x7fffffff);
}
constructor() {
let f = () => {
var a, b, c, d : number;
this.seed = function(aa,bb,cc,dd) {
a = aa; b = bb; c = cc; d = dd;
}
this.next = function() {
// sfc32
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
var t = (a + b) | 0;
a = b ^ b >>> 9;
b = c + (c << 3) | 0;
c = (c << 21 | c >>> 11);
d = d + 1 | 0;
t = t + d | 0;
c = c + t | 0;
return (t >>> 0) / 4294967296;
}
};
f();
this.seed(0x12345678, 0xdeadbeef, 0xf0d3984e, 0xfeed3660); //default seed
this.next();
this.next();
}
};
export class BASICRuntime {
program : basic.BASICProgram;
@ -49,9 +81,11 @@ export class BASICRuntime {
arrays : {};
defs : {};
forLoops : { [varname:string] : { $next:(name:string) => void, inner:string } };
whileLoops : number[];
topForLoopName : string;
returnStack : number[];
column : number;
rng : RNG;
running : boolean = false;
exited : boolean = true;
@ -112,6 +146,8 @@ export class BASICRuntime {
this.defs = {}; // TODO? only in interpreters
this.forLoops = {};
this.topForLoopName = null;
this.whileLoops = [];
this.rng = new RNG();
// initialize arrays?
if (this.opts && this.opts.staticArrays) {
this.allstmts.filter((stmt) => stmt.command == 'DIM').forEach((dimstmt: basic.DIM_Statement) => {
@ -120,6 +156,8 @@ export class BASICRuntime {
}
}
// TODO: saveState(), loadState()
getBuiltinFunctions() {
var fnames = this.program && this.opts.validFunctions;
// if no valid function list, look for ABC...() functions in prototype
@ -213,11 +251,9 @@ export class BASICRuntime {
do {
// in Altair BASIC, ELSE is bound to the right-most IF
// TODO: this is complicated, we should just have nested expressions
if (this.opts.ifElse) {
var cmd = this.allstmts[this.curpc].command;
if (cmd == 'ELSE') { this.curpc++; break; }
else if (cmd == 'IF') return this.skipToEOL();
}
var cmd = this.allstmts[this.curpc].command;
if (cmd == 'ELSE') { this.curpc++; break; }
else if (cmd == 'IF') return this.skipToEOL();
this.curpc++;
} while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
}
@ -242,6 +278,26 @@ export class BASICRuntime {
this.runtimeError(`I couldn't find a matching NEXT ${forname} to skip this for loop.`);
}
skipToAfterWend() {
var pc = this.curpc - 1;
var nesting = 0;
while (pc < this.allstmts.length) {
var stmt = this.allstmts[pc];
console.log(nesting, pc, stmt);
if (stmt.command == 'WHILE') {
nesting++;
} else if (stmt.command == 'WEND') {
nesting--;
if (nesting == 0) {
this.curpc = pc + 1;
return;
}
}
pc++;
}
this.runtimeError(`I couldn't find a matching WEND for this WHILE.`);
}
gotoLabel(label) {
var pc = this.label2pc[label];
if (pc >= 0) {
@ -252,7 +308,7 @@ export class BASICRuntime {
}
gosubLabel(label) {
if (this.returnStack.length > 65535)
if (this.returnStack.length > 32767) // TODO: const?
this.runtimeError(`I did too many GOSUBs without a RETURN.`)
this.returnStack.push(this.curpc);
this.gotoLabel(label);
@ -424,10 +480,24 @@ export class BASICRuntime {
nextForLoop(name) {
var fl = this.forLoops[name || (this.opts.optionalNextVar && this.topForLoopName)];
if (!fl) this.runtimeError(`I couldn't find a FOR for this NEXT.`)
if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`)
fl.$next(name);
}
whileLoop(cond) {
if (cond) {
this.whileLoops.push(this.curpc-1);
} else {
this.skipToAfterWend();
}
}
nextWhileLoop() {
var pc = this.whileLoops.pop();
if (pc == null) this.runtimeError(`I couldn't find a matching WHILE for this WEND.`);
else this.curpc = pc;
}
// converts a variable to string/number based on var name
assign(name: string, right: number|string) : number|string {
if (this.opts.typeConvert)
@ -515,17 +585,24 @@ export class BASICRuntime {
return (v as any) as basic.Value;
}
onGotoLabel(value: number, ...labels: string[]) {
checkOnGoto(value: number, labels: string[]) {
value = this.ROUND(value);
if (value < 1 || value > labels.length)
if (value < 0) // > 255 ?
this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
if (this.opts.checkOnGotoIndex && (value < 1 || value > labels.length))
this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
else
value = Math.min(Math.max(1, value), labels.length);
return value;
}
onGotoLabel(value: number, ...labels: string[]) {
value = this.checkOnGoto(value, labels);
this.gotoLabel(labels[value-1]);
}
onGosubLabel(value: number, ...labels: string[]) {
value = this.ROUND(value);
if (value < 1 || value > labels.length)
this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
value = this.checkOnGoto(value, labels);
this.gosubLabel(labels[value-1]);
}
@ -568,14 +645,13 @@ export class BASICRuntime {
}
do__LET(stmt : basic.LET_Statement) {
// TODO: range-checking for subscripts (get and set)
var lexpr = this.assign2js(stmt.lexpr);
var right = this.expr2js(stmt.right);
return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
}
do__FOR(stmt : basic.FOR_Statement) {
var name = JSON.stringify(stmt.lexpr.name); // TODO: args?
var name = JSON.stringify(stmt.lexpr.name);
var init = this.expr2js(stmt.initial);
var targ = this.expr2js(stmt.target);
var step = stmt.step ? this.expr2js(stmt.step) : 'null';
@ -583,7 +659,7 @@ export class BASICRuntime {
}
do__NEXT(stmt : basic.NEXT_Statement) {
var name = stmt.lexpr && JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
var name = stmt.lexpr && JSON.stringify(stmt.lexpr.name);
return `this.nextForLoop(${name})`;
}
@ -596,6 +672,15 @@ export class BASICRuntime {
return `this.skipToEOL()`
}
do__WHILE(stmt : basic.WHILE_Statement) {
var cond = this.expr2js(stmt.cond);
return `this.whileLoop(${cond})`;
}
do__WEND() {
return `this.nextWhileLoop()`
}
do__DEF(stmt : basic.DEF_Statement) {
var args = [];
for (var arg of stmt.lexpr.args || []) {
@ -661,8 +746,11 @@ export class BASICRuntime {
return s;
}
do__RESTORE() {
return `this.dataptr = 0`;
do__RESTORE(stmt : basic.RESTORE_Statement) {
if (stmt.label != null)
return `this.dataptr = this.label2pc[${this.expr2js(stmt.label, {isconst:true})}] || 0`;
else
return `this.dataptr = 0`;
}
do__END() {
@ -696,8 +784,11 @@ export class BASICRuntime {
return 'this.clearVars()';
}
do__RANDOMIZE() {
return `this.rng.randomize()`;
}
// TODO: ONERR, ON ERROR GOTO
// TODO: "SUBSCRIPT ERROR" (range check)
// TODO: gosubs nested too deeply
// TODO: memory quota
// TODO: useless loop (! 4th edition)
@ -707,7 +798,7 @@ export class BASICRuntime {
// FUNCTIONS
isValid(obj:number|string) : boolean {
if (typeof obj === 'number' && !isNaN(obj) && isFinite(obj))
if (typeof obj === 'number' && !isNaN(obj) && (!this.opts.checkOverflow || isFinite(obj)))
return true;
else if (typeof obj === 'string')
return true;
@ -841,6 +932,9 @@ export class BASICRuntime {
CHR$(arg : number) : string {
return String.fromCharCode(this.checkNum(arg));
}
CINT(arg : number) : number {
return this.ROUND(arg);
}
COS(arg : number) : number {
return this.checkNum(Math.cos(arg));
}
@ -854,7 +948,7 @@ export class BASICRuntime {
return this.checkNum(arg < 0 ? Math.ceil(arg) : Math.floor(arg));
}
HEX$(arg : number) : string {
return arg.toString(16);
return this.ROUND(arg).toString(16);
}
INSTR(a, b, c) : number {
if (c != null) {
@ -882,11 +976,18 @@ export class BASICRuntime {
if (count == 0) count = arg.length;
return arg.substr(start-1, count);
}
OCT$(arg : number) : string {
return this.ROUND(arg).toString(8);
}
POS(arg : number) : number { // arg ignored
return this.column + 1;
}
RIGHT$(arg : string, count : number) : string {
return arg.substr(arg.length - count, count);
}
RND(arg : number) : number {
return Math.random(); // argument ignored
// TODO: X<0 restart w/ seed, X=0 repeats
return this.rng.next();
}
ROUND(arg : number) : number {
return this.checkNum(Math.round(arg));
@ -898,7 +999,11 @@ export class BASICRuntime {
return this.checkNum(Math.sin(arg));
}
SPACE$(arg : number) : string {
return (arg > 0) ? ' '.repeat(this.checkNum(arg)) : '';
arg = this.ROUND(arg);
return (arg > 0) ? ' '.repeat(arg) : '';
}
SPC(arg : number) : string {
return this.SPACE$(arg);
}
SQR(arg : number) : number {
if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`)
@ -907,6 +1012,12 @@ export class BASICRuntime {
STR$(arg : number) : string {
return this.valueToString(this.checkNum(arg));
}
STRING$(len : number, chr : number|string) : string {
len = this.ROUND(len);
if (len <= 0) return '';
if (typeof chr === 'string') return chr.substr(0,1).repeat(len);
else return String.fromCharCode(chr).repeat(len);
}
TAB(arg : number) : string {
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
var spaces = this.ROUND(arg) - 1 - this.column;

View File

@ -177,6 +177,7 @@ class BASICPlatform implements Platform {
Arrays: this.runtime.arrays,
Functions: this.runtime.defs,
ForLoops: this.runtime.forLoops,
WhileLoops: this.runtime.whileLoops,
ReturnStack: this.runtime.returnStack,
NextDatum: this.runtime.datums[this.runtime.dataptr],
Options: this.runtime.opts,