basic: fuzz test fixes, DEF cycle detector

This commit is contained in:
Steven Hugg 2020-08-15 15:03:56 -05:00
parent 295f1ef9de
commit 88fa924507
6 changed files with 1107 additions and 87 deletions

887
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@
"electron-packager": "^15.0.0",
"file-saver": "^2.0.2",
"jsdom": "^12.2.0",
"jsfuzz": "^1.0.14",
"jszip": "^3.5.0",
"localforage": "^1.9.0",
"lzg": "^1.0.x",

View File

@ -132,6 +132,8 @@ export interface Platform {
startProbing?() : ProbeRecorder;
stopProbing?() : void;
isBlocked?() : boolean; // is blocked, halted, or waiting for input?
}
export interface Preset {

View File

@ -6,8 +6,8 @@ export interface BASICOptions {
asciiOnly : boolean; // reject non-ASCII chars?
uppercaseOnly : boolean; // convert everything to uppercase?
optionalLabels : boolean; // can omit line numbers and use labels?
optionalWhitespace : boolean; // can "crunch" keywords?
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
optionalWhitespace : boolean; // can "crunch" keywords? also, eat extra ":" delims
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
tickComments : boolean; // support 'comments?
hexOctalConsts : boolean; // support &H and &O integer constants?
@ -43,6 +43,8 @@ export interface BASICOptions {
endStmtRequired : boolean; // need END at end?
// MISC
commandsPerSec? : number; // how many commands per second?
maxLinesPerFile? : number; // limit on # of lines
maxArrayElements? : number; // max array elements (all dimensions)
}
export interface SourceLocated {
@ -59,8 +61,8 @@ export class CompileError extends Error {
}
// Lexer regular expression -- each (capture group) handles a different token type
// FLOAT INT HEXOCTAL REMARK IDENT STRING RELOP EXP OPERATORS OTHER WS
const re_toks = /([0-9.]+[E][+-]?\d+|\d+[.][E0-9]*|[.][E0-9]+)|[0]*(\d+)|&([OH][0-9A-F]+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>#])|(\*\*)|([-+*/^,;:()\[\]\?\\])|(\S+)|(\s+)/gi;
// FLOAT INT HEXOCTAL REMARK IDENT STRING RELOP EXP OPERATORS OTHER WS
const re_toks = /([0-9.]+[E][+-]?\d+|\d+[.][E0-9]*|[.][E0-9]+)|[0]*(\d+)|&([OH][0-9A-F]+)|(['].*)|([A-Z_]\w*[$]?)|(".*?")|([<>]?[=<>#])|(\*\*)|([-+*/^,;:()\[\]\?\\])|(\S+)|(\s+)/gi;
export enum TokenType {
EOL = 0,
@ -302,6 +304,19 @@ function stripQuotes(s: string) {
return s.substr(1, s.length-2);
}
function isLiteral(arg: Expr): arg is Literal {
return (arg as any).value != null;
}
function isLookup(arg: Expr): arg is IndOp {
return (arg as any).name != null;
}
function isBinOp(arg: Expr): arg is BinOp {
return (arg as any).op != null && (arg as any).left != null && (arg as any).right != null;
}
function isUnOp(arg: Expr): arg is UnOp {
return (arg as any).op != null && (arg as any).expr != null;
}
///// BASIC PARSER
export class BASICParser {
@ -311,7 +326,9 @@ export class BASICParser {
listings: CodeListingMap;
labels: { [label: string]: BASICLine };
targets: { [targetlabel: string]: SourceLocation };
refs: { [name: string]: SourceLocation }; // references
varrefs: { [name: string]: SourceLocation }; // references
fnrefs: { [name: string]: string[] }; // DEF FN call graph
maxlinelen : number = 255; // maximum line length
path : string;
lineno : number;
@ -327,7 +344,8 @@ export class BASICParser {
this.lineno = 0;
this.curlabel = null;
this.listings = {};
this.refs = {};
this.varrefs = {};
this.fnrefs = {};
this.optionCount = 0;
}
addError(msg: string, loc?: SourceLocation) {
@ -382,10 +400,7 @@ export class BASICParser {
}
} else this.dialectError(`optional line numbers`);
case TokenType.Int:
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
this.labels[tok.str] = line;
line.label = tok.str;
this.curlabel = tok.str;
this.setCurrentLabel(line, tok.str);
break;
case TokenType.HexOctalInt:
case TokenType.Float:
@ -398,9 +413,16 @@ export class BASICParser {
break;
}
}
setCurrentLabel(line: BASICLine, str: string) {
if (this.labels[str] != null) this.compileError(`There's a duplicated label "${str}".`);
this.labels[str] = line;
line.label = str;
this.curlabel = str;
this.tokens.forEach((tok) => tok.$loc.label = str);
}
parseFile(file: string, path: string) : BASICProgram {
this.path = path;
var txtlines = file.split(/\n|\r\n/);
var txtlines = file.split(/\n|\r\n?/);
var pgmlines = txtlines.map((line) => this.parseLine(line));
var program = { opts: this.opts, lines: pgmlines };
this.checkAll(program);
@ -416,9 +438,7 @@ export class BASICParser {
return {label:null, stmts:[]};
}
}
tokenize(line: string) : void {
this.lineno++;
this.tokens = [];
_tokenize(line: string) : void {
// 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
@ -428,7 +448,7 @@ export class BASICParser {
for (var i = 1; i <= lastTokType; i++) {
let s : string = m[i];
if (s != null) {
let loc = { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length, label: this.curlabel };
let loc = { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length };
// maybe we don't support unicode in 1975?
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
this.dialectError(`non-ASCII characters`);
@ -437,6 +457,9 @@ export class BASICParser {
s = s.toUpperCase();
// DATA statement captures whitespace too
if (s == 'DATA') lastTokType = TokenType.Whitespace;
// certain keywords shouldn't split for rest of line
if (s == 'DATA') splitre = null;
if (s == 'OPTION') splitre = null;
// REM means ignore rest of statement
if (lastTokType == TokenType.CatchAll && s.startsWith('REM')) {
s = 'REM';
@ -451,26 +474,31 @@ export class BASICParser {
}
// un-crunch tokens?
if (splitre && i == TokenType.Ident) {
var splittoks = s.split(splitre);
splittoks.forEach((ss) => {
if (ss != '') {
// leftover might be integer
i = /^[0-9]+$/.test(ss) ? TokenType.Int : TokenType.Ident;
// disable crunching after this token?
if (ss == 'DATA' || ss == 'OPTION')
splitre = null;
var splittoks = s.split(splitre).filter((s) => s != ''); // only non-empties
if (splittoks.length > 1) {
splittoks.forEach((ss) => {
// check to see if leftover might be integer, or identifier
if (/^[0-9]+$/.test(ss)) i = TokenType.Int;
else if (/^[A-Z_]\w*[$]?$/.test(ss)) i = TokenType.Ident;
else this.compileError(`Try adding whitespace before "${ss}".`);
this.tokens.push({str: ss, type: i, $loc:loc});
}
});
} else {
// add token to list
this.tokens.push({str: s, type: i, $loc:loc});
});
s = null;
}
}
// add token to list
if (s) this.tokens.push({str: s, type: i, $loc:loc});
break;
}
}
}
this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length, label: this.curlabel } };
}
tokenize(line: string) : void {
this.lineno++;
this.tokens = []; // can't have errors until this is set
this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length } };
if (line.length > this.maxlinelen) this.compileError(`A line should be no more than ${this.maxlinelen} characters long.`);
this._tokenize(line);
}
parse() : BASICLine {
var line = {label: null, stmts: []};
@ -502,6 +530,9 @@ export class BASICParser {
return false;
}
parseStatement(): Statement | null {
// eat extra ":" (should have separate property for this)
if (this.opts.optionalWhitespace && this.peekToken().str == ':') return null;
// get the command word
var cmdtok = this.consumeToken();
var cmd = cmdtok.str;
var stmt;
@ -510,6 +541,7 @@ export class BASICParser {
if (cmdtok.str.startsWith("'") && !this.opts.tickComments) this.dialectError(`tick remarks`);
return null;
case TokenType.Operator:
// "?" is alias for "PRINT" on some platforms
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
case TokenType.Ident:
// ignore remarks
@ -534,8 +566,11 @@ export class BASICParser {
this.pushbackToken(cmdtok);
stmt = this.stmt__LET();
break;
} else {
this.compileError(`I don't understand the command "${cmd}".`);
}
case TokenType.EOL:
if (this.opts.optionalWhitespace) return null;
default:
this.compileError(`There should be a command here.`);
return null;
@ -547,7 +582,7 @@ export class BASICParser {
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
this.refs[tok.str] = tok.$loc;
this.varrefs[tok.str] = tok.$loc;
let args = null;
if (this.peekToken().str == '(') {
this.expectToken('(');
@ -745,6 +780,20 @@ export class BASICParser {
break;
}
}
visitExpr(expr: Expr, callback: (expr:Expr) => void) {
if (isBinOp(expr)) {
this.visitExpr(expr.left, callback);
this.visitExpr(expr.right, callback);
}
if (isUnOp(expr)) {
this.visitExpr(expr.expr, callback);
}
if (isLookup(expr) && expr.args != null) {
for (var arg of expr.args)
this.visitExpr(arg, callback);
}
callback(expr);
}
//// STATEMENTS
@ -865,6 +914,10 @@ export class BASICParser {
this.compileError(`An array defined by DIM must have at least one dimension.`)
else if (arr.args.length > this.opts.maxDimensions)
this.dialectError(`more than ${this.opts.maxDimensions} dimensional arrays`);
for (var arrdim of arr.args) {
if (isLiteral(arrdim) && arrdim.value < this.opts.defaultArrayBase)
this.compileError(`An array dimension cannot be less than ${this.opts.defaultArrayBase}.`);
}
});
return { command:'DIM', args:lexprs };
}
@ -925,8 +978,26 @@ export class BASICParser {
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
this.expectToken("=");
var func = this.parseExpr();
// build call graph to detect cycles
this.visitExpr(func, (expr:Expr) => {
if (isLookup(expr) && expr.name.startsWith('FN')) {
if (!this.fnrefs[lexpr.name])
this.fnrefs[lexpr.name] = [];
this.fnrefs[lexpr.name].push(expr.name);
}
});
this.checkCallGraph(lexpr.name, new Set());
return { command:'DEF', lexpr:lexpr, def:func };
}
// detect cycles in call graph starting at function 'name'
checkCallGraph(name: string, visited: Set<string>) {
if (visited.has(name)) this.compileError(`There was a cycle in the function definition graph for ${name}.`);
visited.add(name);
var refs = this.fnrefs[name] || [];
for (var ref of refs)
this.checkCallGraph(ref, visited); // recurse
visited.delete(name);
}
stmt__POP() : NoArgStatement {
return { command:'POP' };
}
@ -1550,9 +1621,7 @@ export const MODERN_BASIC : BASICOptions = {
// TODO: integer vars
// TODO: DEFINT/DEFSTR
// TODO: superfluous ":" ignored on MS basics only?
// TODO: excess INPUT ignored, error msg
// TODO: max line len?
export const DIALECTS = {
"DEFAULT": ALTAIR_BASIC41,

View File

@ -3,43 +3,6 @@ import { BASICParser, DIALECTS, BASICOptions } from "./compiler";
import { BASICRuntime } from "./runtime";
import { lpad, rpad } from "../util";
function dumpDialectInfo() {
var dialects = new Set<BASICOptions>();
var array = {};
var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN'];
SELECTED_DIALECTS.forEach((dkey) => {
dialects.add(DIALECTS[dkey]);
});
var ALL_KEYWORDS = new Set<string>();
dialects.forEach((dialect) => {
Object.entries(dialect).forEach(([key, value]) => {
if (value === null) value = "all";
else if (value === true) value = "Y";
else if (value === false) value = "-";
else if (Array.isArray(value))
value = value.length;
if (!array[key]) array[key] = [];
array[key].push(value);
if (dialect.validKeywords) dialect.validKeywords.map(ALL_KEYWORDS.add.bind(ALL_KEYWORDS));
});
});
dialects.forEach((dialect) => {
ALL_KEYWORDS.forEach((keyword) => {
if (parser.supportsKeyword(keyword)) {
var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0;
if (!array[keyword]) array[keyword] = [];
array[keyword].push(has ? "Y" : "-");
}
});
});
Object.entries(array).forEach(([key, arr]) => {
var s = rpad(key, 30) + "|";
s += (arr as []).map((val) => rpad(val, 9)).join('|');
console.log(s);
});
process.exit(0);
}
var readline = require('readline');
var rl = readline.createInterface({
input: process.stdin,
@ -77,16 +40,20 @@ var data = fs.readFileSync(filename, 'utf-8');
try {
var pgm = parser.parseFile(data, filename);
} catch (e) {
console.log(e);
if (parser.errors.length == 0)
console.log(`@@@ ${e}`);
else
console.log(e);
}
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
if (parser.errors.length) process.exit(2);
// run program
runtime.load(pgm);
try {
runtime.load(pgm);
} catch (e) {
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
process.exit(1);
}
runtime.reset();
runtime.print = (s:string) => {
fs.writeSync(1, s+"");
@ -125,3 +92,63 @@ runtime.resume = function() {
});
}
runtime.resume();
/////
function dumpDialectInfo() {
var dialects = new Set<BASICOptions>();
var array = {};
var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN'];
SELECTED_DIALECTS.forEach((dkey) => {
dialects.add(DIALECTS[dkey]);
});
var ALL_KEYWORDS = new Set<string>();
var ALL_FUNCTIONS = new Set<string>();
var ALL_OPERATORS = new Set<string>();
dialects.forEach((dialect) => {
Object.entries(dialect).forEach(([key, value]) => {
if (value === null) value = "all";
else if (value === true) value = "Y";
else if (value === false) value = "-";
else if (Array.isArray(value))
value = value.length;
if (!array[key]) array[key] = [];
array[key].push(value);
if (dialect.validKeywords) dialect.validKeywords.map(ALL_KEYWORDS.add.bind(ALL_KEYWORDS));
if (dialect.validFunctions) dialect.validFunctions.map(ALL_FUNCTIONS.add.bind(ALL_FUNCTIONS));
if (dialect.validOperators) dialect.validOperators.map(ALL_OPERATORS.add.bind(ALL_OPERATORS));
});
});
dialects.forEach((dialect) => {
ALL_KEYWORDS.forEach((keyword) => {
if (parser.supportsKeyword(keyword)) {
var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0;
keyword = '`'+keyword+'`'
if (!array[keyword]) array[keyword] = [];
array[keyword].push(has ? "Y" : "-");
}
});
ALL_OPERATORS.forEach((keyword) => {
var has = dialect.validOperators == null || dialect.validOperators.indexOf(keyword) >= 0;
if (keyword == '#') keyword = '*#*';
keyword = "*a* " + keyword + " *b*";
if (!array[keyword]) array[keyword] = [];
array[keyword].push(has ? "Y" : "-");
});
ALL_FUNCTIONS.forEach((keyword) => {
if (runtime.supportsFunction(keyword)) {
var has = dialect.validFunctions == null || dialect.validFunctions.indexOf(keyword) >= 0;
keyword = '`'+keyword+'()`'
if (!array[keyword]) array[keyword] = [];
array[keyword].push(has ? "Y" : "-");
}
});
});
Object.entries(array).forEach(([key, arr]) => {
var s = rpad(key, 30) + "|";
s += (arr as []).map((val) => rpad(val, 9)).join('|');
console.log(s);
});
process.exit(0);
}

View File

@ -66,6 +66,8 @@ class RNG {
}
};
const DEFAULT_MAX_ARRAY_ELEMENTS = 1024*1024;
export class BASICRuntime {
program : basic.BASICProgram;
@ -111,6 +113,7 @@ export class BASICRuntime {
// initialize program
this.program = program;
this.opts = program.opts;
if (!this.opts.maxArrayElements) this.opts.maxArrayElements = DEFAULT_MAX_ARRAY_ELEMENTS;
this.label2pc = {};
this.label2dataptr = {};
this.allstmts = [];
@ -194,9 +197,12 @@ export class BASICRuntime {
// if no valid function list, look for ABC...() functions in prototype
if (!fnames) fnames = Object.keys(BASICRuntime.prototype).filter((name) => /^[A-Z]{3,}[$]?$/.test(name));
var dict = {};
for (var fn of fnames) if (this[fn]) dict[fn] = this[fn].bind(this);
for (var fn of fnames) if (this.supportsFunction(fn)) dict[fn] = this[fn].bind(this);
return dict;
}
supportsFunction(fnname: string) {
return typeof this[fnname] === 'function';
}
runtimeError(msg : string) {
this.curpc--; // we did curpc++ before executing statement
@ -257,7 +263,7 @@ export class BASICRuntime {
if (this.trace) console.log(functext);
stmt.$run = this.compileJS(functext);
} catch (e) {
console.log(functext);
if (functext) console.log(functext);
throw e;
}
}
@ -279,14 +285,16 @@ export class BASICRuntime {
}
skipToElse() {
do {
while (this.curpc < this.allstmts.length) {
// in Altair BASIC, ELSE is bound to the right-most IF
// TODO: this is complicated, we should just have nested expressions
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));
if (this.pc2line.get(this.curpc))
break;
}
}
skipToEOF() {
@ -314,7 +322,7 @@ export class BASICRuntime {
var nesting = 0;
while (pc < this.allstmts.length) {
var stmt = this.allstmts[pc];
console.log(nesting, pc, stmt);
//console.log(nesting, pc, stmt);
if (stmt.command == 'WHILE') {
nesting++;
} else if (stmt.command == 'WEND') {
@ -461,7 +469,8 @@ export class BASICRuntime {
if (!opts) opts = {};
var s = '';
// is it a function? not allowed
if (expr.name.startsWith("FN") || this.builtins[expr.name]) this.runtimeError(`I can't call a function here.`);
if (expr.name.startsWith("FN") || this.builtins[expr.name])
this.runtimeError(`I can't call a function here.`);
// is it a subscript?
if (expr.args) {
// set array slice (HP BASIC)
@ -590,10 +599,14 @@ export class BASICRuntime {
// dimension array
dimArray(name: string, ...dims:number[]) {
// TODO: maybe do this check at compile-time?
dims = dims.map(Math.round);
if (this.arrays[name] != null) {
if (this.opts.staticArrays) return;
else this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
}
if (this.getTotalArrayLength(dims) > this.opts.maxArrayElements) {
this.runtimeError(`I can't create an array with this many elements.`);
}
var isstring = name.endsWith('$');
// if numeric value, we use Float64Array which inits to 0
var arrcons = isstring ? Array : Float64Array;
@ -609,6 +622,16 @@ export class BASICRuntime {
}
}
getTotalArrayLength(dims:number[]) {
var n = 1;
for (var i=0; i<dims.length; i++) {
if (dims[i] < this.opts.defaultArrayBase)
this.runtimeError(`I can't create an array with a dimension less than ${this.opts.defaultArrayBase}.`);
n *= dims[i];
}
return n;
}
getArray(name: string, order: number) : [] {
if (!this.arrays[name]) {
if (this.opts.defaultArraySize == 0)
@ -648,6 +671,7 @@ export class BASICRuntime {
return (orig + ' '.repeat(start)).substr(0, start-1) + add + orig.substr(end);
}
getStringSlice(s: string, start: number, end: number) {
s = this.checkString(s);
return s.substr(start-1, end+1-start);
}
@ -947,11 +971,13 @@ export class BASICRuntime {
return fn;
}
checkNum(n:number) : number {
this.checkValue(n, 'this');
if (n === Infinity) this.runtimeError(`I computed a number too big to store.`);
if (isNaN(n)) this.runtimeError(`I computed an invalid number.`);
return n;
}
checkString(s:string) : string {
this.checkValue(s, 'this');
if (typeof s !== 'string')
this.runtimeError(`I expected a string here.`);
else if (s.length > this.opts.maxStringLength)
@ -1044,11 +1070,14 @@ export class BASICRuntime {
}
// FUNCTIONS (uppercase)
// TODO: swizzle names for type-checking
ABS(arg : number) : number {
return this.checkNum(Math.abs(arg));
}
ASC(arg : string) : number {
arg = this.checkString(arg);
if (arg == '') this.runtimeError(`I tried to call ASC() on an empty string.`);
return arg.charCodeAt(0);
}
ATN(arg : number) : number {
@ -1089,6 +1118,8 @@ export class BASICRuntime {
return this.checkNum(Math.floor(arg));
}
LEFT$(arg : string, count : number) : string {
arg = this.checkString(arg);
count = this.ROUND(count);
return arg.substr(0, count);
}
LEN(arg : string) : number {
@ -1108,6 +1139,9 @@ export class BASICRuntime {
return this.checkNum(Math.log10(arg));
}
MID$(arg : string, start : number, count : number) : string {
arg = this.checkString(arg);
start = this.ROUND(start);
count = this.ROUND(count);
if (start < 1) this.runtimeError(`I can't compute MID$ if the starting index is less than 1.`)
if (count == 0) count = arg.length;
return arg.substr(start-1, count);
@ -1123,6 +1157,8 @@ export class BASICRuntime {
return this.column + 1;
}
RIGHT$(arg : string, count : number) : string {
arg = this.checkString(arg);
count = this.ROUND(count);
return arg.substr(arg.length - count, count);
}
RND(arg : number) : number {
@ -1134,14 +1170,14 @@ export class BASICRuntime {
return this.checkNum(Math.round(arg));
}
SGN(arg : number) : number {
this.checkNum(arg);
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
}
SIN(arg : number) : number {
return this.checkNum(Math.sin(arg));
}
SPACE$(arg : number) : string {
arg = this.ROUND(arg);
return (arg > 0) ? ' '.repeat(arg) : '';
return this.STRING$(arg, ' ');
}
SPC(arg : number) : string {
return this.SPACE$(arg);
@ -1156,13 +1192,17 @@ export class BASICRuntime {
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);
if (len > this.opts.maxStringLength)
this.runtimeError(`I can't create a string longer than ${this.opts.maxStringLength} characters.`);
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;
return (spaces > 0) ? ' '.repeat(spaces) : '';
return this.SPACE$(spaces);
}
TAN(arg : number) : number {
return this.checkNum(Math.tan(arg));
@ -1196,10 +1236,12 @@ export class BASICRuntime {
return isNaN(n) ? 0 : n; // TODO? altair works this way
}
LPAD$(arg : string, len : number) : string {
arg = this.checkString(arg);
while (arg.length < len) arg = " " + arg;
return arg;
}
RPAD$(arg : string, len : number) : string {
arg = this.checkString(arg);
while (arg.length < len) arg = arg + " ";
return arg;
}