basic: WHILE start/end, fixed OPTION boolean, CONVERT, more type checking, set $loc offset, OPTION parsing

This commit is contained in:
Steven Hugg 2020-08-22 11:40:58 -05:00
parent b2df149fb3
commit 9e48b7f973
6 changed files with 172 additions and 107 deletions

View File

@ -15,6 +15,7 @@
} }
.gutter-info { .gutter-info {
width: 1em; width: 1em;
cursor: cell;
} }
.currentpc-span { .currentpc-span {
background-color: #7e2a70; background-color: #7e2a70;

View File

@ -22,7 +22,6 @@ export interface BASICOptions {
// VALUES AND OPERATORS // VALUES AND OPERATORS
defaultValues : boolean; // initialize unset variables to default value? (0 or "") defaultValues : boolean; // initialize unset variables to default value? (0 or "")
stringConcat : boolean; // can concat strings with "+" operator? stringConcat : boolean; // can concat strings with "+" operator?
typeConvert : boolean; // type convert strings <-> numbers? (NOT USED)
checkOverflow : boolean; // check for overflow of numerics? checkOverflow : boolean; // check for overflow of numerics?
bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops
maxStringLength : number; // maximum string length in chars maxStringLength : number; // maximum string length in chars
@ -52,7 +51,12 @@ export interface BASICOptions {
maxArrayElements? : number; // max array elements (all dimensions) maxArrayElements? : number; // max array elements (all dimensions)
} }
// objects that have source code position info
export interface SourceLocated { export interface SourceLocated {
$loc?: SourceLocation;
}
// statements also have the 'offset' (pc) field from SourceLine
export interface SourceLineLocated {
$loc?: SourceLine; $loc?: SourceLine;
} }
@ -115,7 +119,7 @@ export interface IndOp extends ExprBase {
args: Expr[]; args: Expr[];
} }
export interface Statement extends SourceLocated { export interface Statement extends SourceLineLocated {
command: string; command: string;
} }
@ -245,6 +249,12 @@ export interface CHANGE_Statement extends Statement {
dest: IndOp; dest: IndOp;
} }
export interface CONVERT_Statement extends Statement {
command: "CONVERT";
src: Expr;
dest: IndOp;
}
export interface NoArgStatement extends Statement { export interface NoArgStatement extends Statement {
command: string; command: string;
} }
@ -331,12 +341,22 @@ function isUnOp(arg: Expr): arg is UnOp {
return (arg as any).op != null && (arg as any).expr != null; return (arg as any).op != null && (arg as any).expr != null;
} }
function mergeLocs(a: SourceLocation, b: SourceLocation) : SourceLocation {
return {
line:Math.min(a.line, b.line),
start:Math.min(a.start, b.start),
end:Math.max(a.end, b.end),
label:a.label || b.label,
path:a.path || b.path,
}
}
///// BASIC PARSER ///// BASIC PARSER
export class BASICParser { export class BASICParser {
opts : BASICOptions = DIALECTS['DEFAULT']; opts : BASICOptions = DIALECTS['DEFAULT'];
optionCount : number; // how many OPTION stmts so far? optionCount : number; // how many OPTION stmts so far?
maxlinelen : number = 255; // maximum line length maxlinelen : number = 255; // maximum line length (some like HP use 72 chars)
stmts : Statement[]; stmts : Statement[];
errors: WorkerError[]; errors: WorkerError[];
listings: CodeListingMap; listings: CodeListingMap;
@ -454,9 +474,11 @@ export class BASICParser {
return this.stmts.length; return this.stmts.length;
} }
addStatement(stmt: Statement, cmdtok: Token, endtok?: Token) { addStatement(stmt: Statement, cmdtok: Token, endtok?: Token) {
// set location for statement // set location for statement, adding offset (PC) field
if (endtok == null) endtok = this.peekToken(); if (endtok == null) endtok = this.peekToken();
stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: endtok.$loc.start, label: this.curlabel, offset: null }; stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: endtok.$loc.start,
label: this.curlabel,
offset: this.stmts.length };
// check IF/THEN WHILE/WEND FOR/NEXT etc // check IF/THEN WHILE/WEND FOR/NEXT etc
this.modifyScope(stmt); this.modifyScope(stmt);
// add to list // add to list
@ -647,12 +669,12 @@ export class BASICParser {
var popidx = this.scopestack.pop(); var popidx = this.scopestack.pop();
var popstmt : ScopeStartStatement = popidx != null ? this.stmts[popidx] : null; var popstmt : ScopeStartStatement = popidx != null ? this.stmts[popidx] : null;
if (popstmt == null) if (popstmt == null)
this.compileError(`There's a ${close.command} without a matching ${open}.`); this.compileError(`There's a ${close.command} without a matching ${open}.`, close.$loc);
else if (popstmt.command != open) else if (popstmt.command != open)
this.compileError(`There's a ${close.command} paired with ${popstmt.command}, but it should be paired with ${open}.`); this.compileError(`There's a ${close.command} paired with ${popstmt.command}, but it should be paired with ${open}.`, close.$loc);
else if (close.command == 'NEXT' && !this.opts.optionalNextVar else if (close.command == 'NEXT' && !this.opts.optionalNextVar
&& close.lexpr.name != (popstmt as FOR_Statement).lexpr.name) && close.lexpr.name != (popstmt as FOR_Statement).lexpr.name)
this.compileError(`This NEXT statement is matched with the wrong FOR variable (${close.lexpr.name}).`); this.compileError(`This NEXT statement is matched with the wrong FOR variable (${close.lexpr.name}).`, close.$loc);
// set start + end locations // set start + end locations
close.startpc = popidx; close.startpc = popidx;
popstmt.endpc = this.getPC(); // has to be before adding statment to list popstmt.endpc = this.getPC(); // has to be before adding statment to list
@ -667,8 +689,9 @@ export class BASICParser {
args = this.parseExprList(); args = this.parseExprList();
this.expectToken(')', `There should be another expression or a ")" here.`); this.expectToken(')', `There should be another expression or a ")" here.`);
} }
var valtype = this.exprTypeForSubscript(tok.str, args); var loc = mergeLocs(tok.$loc, this.lasttoken.$loc);
return { valtype: valtype, name: tok.str, args: args }; var valtype = this.exprTypeForSubscript(tok.str, args, loc);
return { valtype: valtype, name: tok.str, args: args, $loc:loc };
default: default:
this.compileError(`There should be a variable name here.`); this.compileError(`There should be a variable name here.`);
break; break;
@ -683,7 +706,7 @@ export class BASICParser {
parseForNextLexpr() : IndOp { parseForNextLexpr() : IndOp {
var lexpr = this.parseLexpr(); var lexpr = this.parseLexpr();
if (lexpr.args || lexpr.name.endsWith('$')) if (lexpr.args || lexpr.name.endsWith('$'))
this.compileError(`A FOR ... NEXT loop can only use numeric variables.`); this.compileError(`A FOR ... NEXT loop can only use numeric variables.`, lexpr.$loc);
return lexpr; return lexpr;
} }
parseList<T>(parseFunc:()=>T, delim:string): T[] { parseList<T>(parseFunc:()=>T, delim:string): T[] {
@ -833,13 +856,17 @@ export class BASICParser {
// use logical operators instead of bitwise? // use logical operators instead of bitwise?
if (!this.opts.bitwiseLogic && op.str == 'AND') opfn = 'land'; if (!this.opts.bitwiseLogic && op.str == 'AND') opfn = 'land';
if (!this.opts.bitwiseLogic && op.str == 'OR') opfn = 'lor'; if (!this.opts.bitwiseLogic && op.str == 'OR') opfn = 'lor';
var valtype = this.exprTypeForOp(opfn, left, right); var valtype = this.exprTypeForOp(opfn, left, right, op);
left = { valtype:valtype, op:opfn, left: left, right: right }; left = { valtype:valtype, op:opfn, left: left, right: right };
} }
return left; return left;
} }
parseExpr(): Expr { parseExpr(): Expr {
return this.parseExpr1(this.parsePrimary(), 0); var startloc = this.peekToken().$loc;
var expr = this.parseExpr1(this.parsePrimary(), 0);
var endloc = this.lasttoken.$loc;
expr.$loc = mergeLocs(startloc, endloc);
return expr;
} }
parseExprWithType(expecttype: ValueType): Expr { parseExprWithType(expecttype: ValueType): Expr {
var expr = this.parseExpr(); var expr = this.parseExpr();
@ -886,30 +913,32 @@ export class BASICParser {
callback(expr); callback(expr);
} }
// type-checking // type-checking
exprTypeForOp(fnname: string, left: Expr, right: Expr) : ValueType { exprTypeForOp(fnname: string, left: Expr, right: Expr, optok: Token) : ValueType {
if (fnname == 'add' && (left.valtype == 'string' || right.valtype == 'string')) { if (left.valtype == 'string' || right.valtype == 'string') {
if (!this.opts.stringConcat) this.dialectErrorNoSupport(`the "+" operator to concatenate strings`); if (fnname == 'add') {
return 'string'; // string concatenation if (this.opts.stringConcat) return 'string' // concat strings
} else { else this.dialectErrorNoSupport(`the "+" operator to concatenate strings`, optok.$loc);
return 'number'; } else if (fnname.length != 2) // only relops are 2 chars long!
this.compileError(`You can't do math on strings until they're converted to numbers.`, optok.$loc);
} }
return 'number';
} }
exprTypeForSubscript(fnname: string, args: Expr[]) : ValueType { exprTypeForSubscript(fnname: string, args: Expr[], loc: SourceLocation) : ValueType {
args = args || []; args = args || [];
// first check the built-in functions // first check the built-in functions
var defs = BUILTIN_MAP[fnname]; var defs = BUILTIN_MAP[fnname];
if (defs != null) { if (defs != null) {
if (!this.validFunction(fnname)) this.dialectErrorNoSupport(`the ${fnname} function`); if (!this.validFunction(fnname)) this.dialectErrorNoSupport(`the ${fnname} function`, loc);
for (var def of defs) { for (var def of defs) {
if (args.length == def.args.length) if (args.length == def.args.length)
return def.result; // TODO: check arg types? return def.result; // TODO: check arg types
} }
// TODO: check func arg types // TODO: check func arg types
this.compileError(`The ${fnname} function takes ${def.args.length} arguments, but ${args.length} are given.`); this.compileError(`The ${fnname} function takes ${def.args.length} arguments, but ${args.length} are given.`, loc);
} }
// no function found, assume it's an array ref // no function found, assume it's an array ref
// TODO: validateVarName() later? // TODO: validateVarName() later?
this.varrefs[fnname] = this.lasttoken.$loc; // TODO? this.varrefs[fnname] = loc;
return fnname.endsWith('$') ? 'string' : 'number'; return fnname.endsWith('$') ? 'string' : 'number';
} }
@ -970,7 +999,7 @@ export class BASICParser {
} }
stmt__IF(): void { stmt__IF(): void {
var cmdtok = this.lasttoken; var cmdtok = this.lasttoken;
var cond = this.parseExpr(); var cond = this.parseExprWithType("number");
var ifstmt : IF_Statement = { command: "IF", cond: cond }; var ifstmt : IF_Statement = { command: "IF", cond: cond };
this.addStatement(ifstmt, cmdtok); this.addStatement(ifstmt, cmdtok);
// we accept GOTO or THEN if line number provided (DEC accepts GO TO) // we accept GOTO or THEN if line number provided (DEC accepts GO TO)
@ -1010,12 +1039,12 @@ export class BASICParser {
stmt__FOR() : FOR_Statement { stmt__FOR() : FOR_Statement {
var lexpr = this.parseForNextLexpr(); var lexpr = this.parseForNextLexpr();
this.expectToken('='); this.expectToken('=');
var init = this.parseExpr(); var init = this.parseExprWithType("number");
this.expectToken('TO'); this.expectToken('TO');
var targ = this.parseExpr(); var targ = this.parseExprWithType("number");
if (this.peekToken().str == 'STEP') { if (this.peekToken().str == 'STEP') {
this.consumeToken(); this.consumeToken();
var step = this.parseExpr(); var step = this.parseExprWithType("number");
} }
return { command:'FOR', lexpr:lexpr, initial:init, target:targ, step:step }; return { command:'FOR', lexpr:lexpr, initial:init, target:targ, step:step };
} }
@ -1034,7 +1063,7 @@ export class BASICParser {
return { command:'NEXT', lexpr:lexpr }; return { command:'NEXT', lexpr:lexpr };
} }
stmt__WHILE(): WHILE_Statement { stmt__WHILE(): WHILE_Statement {
var cond = this.parseExpr(); var cond = this.parseExprWithType("number");
return { command:'WHILE', cond:cond }; return { command:'WHILE', cond:cond };
} }
stmt__WEND(): WEND_Statement { stmt__WEND(): WEND_Statement {
@ -1048,8 +1077,10 @@ export class BASICParser {
else if (arr.args.length > this.opts.maxDimensions) else if (arr.args.length > this.opts.maxDimensions)
this.dialectErrorNoSupport(`arrays with more than ${this.opts.maxDimensions} dimensionals`); this.dialectErrorNoSupport(`arrays with more than ${this.opts.maxDimensions} dimensionals`);
for (var arrdim of arr.args) { for (var arrdim of arr.args) {
if (arrdim.valtype != 'number')
this.compileError(`Array dimensions must be numeric.`, arrdim.$loc);
if (isLiteral(arrdim) && arrdim.value < this.opts.defaultArrayBase) if (isLiteral(arrdim) && arrdim.value < this.opts.defaultArrayBase)
this.compileError(`An array dimension cannot be less than ${this.opts.defaultArrayBase}.`); this.compileError(`An array dimension cannot be less than ${this.opts.defaultArrayBase}.`, arrdim.$loc);
} }
}); });
return { command:'DIM', args:lexprs }; return { command:'DIM', args:lexprs };
@ -1097,7 +1128,7 @@ export class BASICParser {
return { command:'END' }; return { command:'END' };
} }
stmt__ON() : ONGO_Statement { stmt__ON() : ONGO_Statement {
var expr = this.parseExpr(); var expr = this.parseExprWithType("number");
var gotok = this.consumeToken(); var gotok = this.consumeToken();
var cmd = {GOTO:'ONGOTO', THEN:'ONGOTO', GOSUB:'ONGOSUB'}[gotok.str]; // THEN only for DEC basic? var cmd = {GOTO:'ONGOTO', THEN:'ONGOTO', GOSUB:'ONGOSUB'}[gotok.str]; // THEN only for DEC basic?
if (!cmd) this.compileError(`There should be a GOTO or GOSUB here.`); if (!cmd) this.compileError(`There should be a GOTO or GOSUB here.`);
@ -1107,8 +1138,8 @@ export class BASICParser {
stmt__DEF() : DEF_Statement { stmt__DEF() : DEF_Statement {
var lexpr = this.parseVarSubscriptOrFunc(); var lexpr = this.parseVarSubscriptOrFunc();
if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs) if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs)
this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`); this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`, lexpr.$loc);
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`) if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`, lexpr.$loc)
// local variables need to be marked as referenced (TODO: only for this scope) // local variables need to be marked as referenced (TODO: only for this scope)
this.vardefs[lexpr.name] = lexpr; this.vardefs[lexpr.name] = lexpr;
if (lexpr.args != null) if (lexpr.args != null)
@ -1154,35 +1185,34 @@ export class BASICParser {
var src = this.parseExpr(); var src = this.parseExpr();
this.expectToken('TO'); this.expectToken('TO');
var dest = this.parseLexpr(); var dest = this.parseLexpr();
if (dest.valtype == src.valtype)
this.compileError(`CHANGE can only convert strings to numeric arrays, or vice-versa.`, mergeLocs(src.$loc, dest.$loc));
return { command:'CHANGE', src:src, dest:dest }; return { command:'CHANGE', src:src, dest:dest };
} }
stmt__CONVERT() : CONVERT_Statement {
var src = this.parseExpr();
this.expectToken('TO');
var dest = this.parseLexpr();
if (dest.valtype == src.valtype)
this.compileError(`CONVERT can only convert strings to numbers, or vice-versa.`, mergeLocs(src.$loc, dest.$loc));
return { command:'CONVERT', src:src, dest:dest };
}
// TODO: CHANGE A TO A$ (4th edition, A(0) is len and A(1..) are chars) // TODO: CHANGE A TO A$ (4th edition, A(0) is len and A(1..) are chars)
stmt__OPTION() : OPTION_Statement { stmt__OPTION() : OPTION_Statement {
var tokname = this.consumeToken();
if (tokname.type != TokenType.Ident) this.compileError(`There must be a name after the OPTION statement.`)
var list : string[] = [];
var tok;
do {
tok = this.consumeToken();
if (isEOS(tok)) break;
list.push(tok.str);
} while (true);
this.pushbackToken(tok);
var stmt : OPTION_Statement = { command:'OPTION', optname:tokname.str, optargs:list };
this.parseOptions(stmt);
return stmt;
}
parseOptions(stmt: OPTION_Statement) {
var arg = stmt.optargs[0];
this.optionCount++; this.optionCount++;
switch (stmt.optname) { var tokname = this.consumeToken();
var optname = tokname.str.toUpperCase();
if (tokname.type != TokenType.Ident) this.compileError(`There must be a name after the OPTION statement.`)
var tokarg = this.consumeToken();
var arg = tokarg.str.toUpperCase();
switch (optname) {
case 'DIALECT': case 'DIALECT':
if (this.optionCount > 1) if (this.optionCount > 1) this.compileError(`OPTION DIALECT must be the first OPTION statement in the file.`, tokname.$loc);
this.compileError(`OPTION DIALECT must be the first OPTION statement in the file.`);
let dname = arg || ""; let dname = arg || "";
if (dname == "") this.compileError(`OPTION DIALECT requires a dialect name.`, tokname.$loc);
let dialect = DIALECTS[dname.toUpperCase()]; let dialect = DIALECTS[dname.toUpperCase()];
if (dialect) this.opts = dialect; if (dialect) this.opts = dialect;
else this.compileError(`OPTION DIALECT ${dname} is not supported by this compiler.`); else this.compileError(`${dname} is not a valid dialect.`);
break; break;
case 'BASE': case 'BASE':
let base = parseInt(arg); let base = parseInt(arg);
@ -1195,20 +1225,23 @@ export class BASICParser {
break; break;
default: default:
// maybe it's one of the options? // maybe it's one of the options?
var name = Object.getOwnPropertyNames(this.opts).find((n) => n.toUpperCase() == stmt.optname); let propname = Object.getOwnPropertyNames(this.opts).find((n) => n.toUpperCase() == optname);
if (name != null) switch (typeof this.opts[name]) { if (propname == null) this.compileError(`${optname} is not a valid option.`, tokname.$loc);
case 'boolean' : this.opts[name] = arg ? true : false; return; if (arg == null) this.compileError(`OPTION ${optname} requires a parameter.`);
case 'number' : this.opts[name] = parseFloat(arg); return; switch (typeof this.opts[propname]) {
case 'string' : this.opts[name] = arg; return; case 'boolean' : this.opts[propname] = arg.toUpperCase().startsWith("T") || (arg as any)>0; return;
case 'number' : this.opts[propname] = parseFloat(arg); return;
case 'string' : this.opts[propname] = arg; return;
case 'object' : case 'object' :
if (Array.isArray(this.opts[name]) && arg == 'ALL') { if (Array.isArray(this.opts[propname]) && arg == 'ALL') {
this.opts[name] = null; this.opts[propname] = null;
return; return;
} }
this.compileError(`OPTION ${optname} ALL is the only option supported.`);
} }
this.compileError(`OPTION ${stmt.optname} is not supported by this compiler.`);
break; break;
} }
return { command:'OPTION', optname:optname, optargs:[arg]}
} }
// for workermain // for workermain
@ -1217,9 +1250,7 @@ export class BASICParser {
var laststmt : Statement; var laststmt : Statement;
program.stmts.forEach((stmt, idx) => { program.stmts.forEach((stmt, idx) => {
laststmt = stmt; laststmt = stmt;
var sl = stmt.$loc; srclines.push(stmt.$loc);
sl.offset = idx;
srclines.push(sl);
}); });
if (this.opts.endStmtRequired && (laststmt == null || laststmt.command != 'END')) if (this.opts.endStmtRequired && (laststmt == null || laststmt.command != 'END'))
this.dialectError(`All programs must have a final END statement`); this.dialectError(`All programs must have a final END statement`);
@ -1276,7 +1307,6 @@ export const ECMA55_MINIMAL : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : false, defaultValues : false,
stringConcat : false, stringConcat : false,
typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255,
@ -1325,7 +1355,6 @@ export const DARTMOUTH_4TH_EDITION : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : false, defaultValues : false,
stringConcat : false, stringConcat : false,
typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255,
@ -1377,7 +1406,6 @@ export const TINY_BASIC : BASICOptions = {
defaultArraySize : 0, defaultArraySize : 0,
defaultValues : true, defaultValues : true,
stringConcat : false, stringConcat : false,
typeConvert : false,
maxDimensions : 0, maxDimensions : 0,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255,
@ -1413,7 +1441,7 @@ export const TINY_BASIC : BASICOptions = {
export const HP_TIMESHARED_BASIC : BASICOptions = { export const HP_TIMESHARED_BASIC : BASICOptions = {
dialectName: "HP2000", dialectName: "HP2000",
asciiOnly : true, asciiOnly : true,
uppercaseOnly : false, // the terminal is usually uppercase uppercaseOnly : true, // the terminal is usually uppercase
optionalLabels : false, optionalLabels : false,
optionalWhitespace : false, optionalWhitespace : false,
multipleStmtsPerLine : true, multipleStmtsPerLine : true,
@ -1424,10 +1452,9 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : false, defaultValues : false,
stringConcat : false, stringConcat : false,
typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255, // 72 for literals
tickComments : false, // TODO: HP BASIC has 'hh char constants tickComments : false, // TODO: HP BASIC has 'hh char constants
hexOctalConsts : false, hexOctalConsts : false,
validKeywords : [ validKeywords : [
@ -1461,7 +1488,8 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
chainAssignments : true, chainAssignments : true,
optionalLet : true, optionalLet : true,
compiledBlocks : true, compiledBlocks : true,
// TODO: max line number, array index 9999 maxArrayElements : 5000,
// TODO: max line number
} }
export const DEC_BASIC_11 : BASICOptions = { export const DEC_BASIC_11 : BASICOptions = {
@ -1478,7 +1506,6 @@ export const DEC_BASIC_11 : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : true, defaultValues : true,
stringConcat : true, // can also use & stringConcat : true, // can also use &
typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxDefArgs : 255, // ? maxDefArgs : 255, // ?
maxStringLength : 255, maxStringLength : 255,
@ -1533,7 +1560,6 @@ export const DEC_BASIC_PLUS : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : true, defaultValues : true,
stringConcat : true, // can also use "&" stringConcat : true, // can also use "&"
typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxDefArgs : 255, // ? maxDefArgs : 255, // ?
maxStringLength : 255, maxStringLength : 255,
@ -1595,7 +1621,6 @@ export const BASICODE : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : false, defaultValues : false,
stringConcat : true, stringConcat : true,
typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255,
@ -1646,7 +1671,6 @@ export const ALTAIR_BASIC41 : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : true, defaultValues : true,
stringConcat : true, stringConcat : true,
typeConvert : false,
maxDimensions : 128, // "as many as will fit on a single line" ... ? maxDimensions : 128, // "as many as will fit on a single line" ... ?
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255,
@ -1705,7 +1729,6 @@ export const APPLESOFT_BASIC : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : true, defaultValues : true,
stringConcat : true, stringConcat : true,
typeConvert : false,
maxDimensions : 88, maxDimensions : 88,
maxDefArgs : 1, // TODO: no string FNs maxDefArgs : 1, // TODO: no string FNs
maxStringLength : 255, maxStringLength : 255,
@ -1765,7 +1788,6 @@ export const BASIC80 : BASICOptions = {
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : true, defaultValues : true,
stringConcat : true, stringConcat : true,
typeConvert : false,
maxDimensions : 255, maxDimensions : 255,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 255, maxStringLength : 255,
@ -1826,7 +1848,6 @@ export const MODERN_BASIC : BASICOptions = {
defaultArraySize : 0, // DIM required defaultArraySize : 0, // DIM required
defaultValues : false, defaultValues : false,
stringConcat : true, stringConcat : true,
typeConvert : false,
maxDimensions : 255, maxDimensions : 255,
maxDefArgs : 255, maxDefArgs : 255,
maxStringLength : 2048, // TODO? maxStringLength : 2048, // TODO?

View File

@ -362,11 +362,11 @@ export class BASICRuntime {
this.returnStack.pop(); this.returnStack.pop();
} }
valueToString(obj) : string { valueToString(obj:basic.Value, padding:boolean) : string {
var str; var str;
if (typeof obj === 'number') { if (typeof obj === 'number') {
var numstr = this.float2str(obj, this.opts.printZoneLength - 4); var numstr = this.float2str(obj, this.opts.printZoneLength - 4);
if (!this.opts.numericPadding) if (!padding)
return numstr; return numstr;
else if (numstr.startsWith('-')) else if (numstr.startsWith('-'))
return `${numstr} `; return `${numstr} `;
@ -403,7 +403,7 @@ export class BASICRuntime {
} }
printExpr(obj) { printExpr(obj) {
var str = this.valueToString(obj); var str = this.valueToString(obj, this.opts.numericPadding);
this.column += str.length; this.column += str.length;
this.print(str); this.print(str);
} }
@ -674,16 +674,22 @@ export class BASICRuntime {
if (!end) end = start; if (!end) end = start;
start = this.ROUND(start); start = this.ROUND(start);
end = this.ROUND(end); end = this.ROUND(end);
if (start < 1 || end < 1) this.dialectError(`accept a string slice index less than 1`); if (start < 1) this.dialectError(`accept a string slice index less than 1`);
if (end < start) this.dialectError(`accept a string slice index less than the starting index`);
return (orig + ' '.repeat(start)).substr(0, start-1) + add.substr(0, end+1-start) + orig.substr(end); return (orig + ' '.repeat(start)).substr(0, start-1) + add.substr(0, end+1-start) + orig.substr(end);
} }
getStringSlice(s: string, start: number, end: number) { getStringSlice(s: string, start: number, end: number) {
s = this.checkString(s); s = this.checkString(s);
start = this.ROUND(start); start = this.ROUND(start);
end = this.ROUND(end); if (start < 1) this.dialectError(`accept a string slice index less than 1`);
if (start < 1 || end < 1) this.dialectError(`accept a string slice index less than 1`); if (end != null) {
return s.substr(start-1, end+1-start); end = this.ROUND(end);
if (end < start) this.dialectError(`accept a string slice index less than the starting index`);
return s.substr(start-1, end+1-start);
} else {
return s.substr(start-1);
}
} }
checkOnGoto(value: number, labels: string[]) { checkOnGoto(value: number, labels: string[]) {
@ -804,11 +810,17 @@ export class BASICRuntime {
do__WHILE(stmt : basic.WHILE_Statement) { do__WHILE(stmt : basic.WHILE_Statement) {
var cond = this.expr2js(stmt.cond); var cond = this.expr2js(stmt.cond);
return `this.whileLoop(${cond})`; if (stmt.endpc != null)
return `if (!(${cond})) { this.curpc = ${stmt.endpc+1}; }`;
else
return `this.whileLoop(${cond})`;
} }
do__WEND() { do__WEND(stmt : basic.WEND_Statement) {
return `this.nextWhileLoop()` if (stmt.startpc != null)
return `this.curpc = ${stmt.startpc}`;
else
return `this.nextWhileLoop()`
} }
do__DEF(stmt : basic.DEF_Statement) { do__DEF(stmt : basic.DEF_Statement) {
@ -826,12 +838,12 @@ export class BASICRuntime {
} }
_DIM(dim : basic.IndOp) { _DIM(dim : basic.IndOp) {
// HP BASIC doesn't really have string arrays // HP BASIC doesn't really have string arrays, just strings
if (this.opts.arraysContainChars && dim.name.endsWith('$')) if (this.opts.arraysContainChars && dim.name.endsWith('$'))
return; return '';
// dimension an array
var argsstr = ''; var argsstr = '';
for (var arg of dim.args) { for (var arg of dim.args) {
// TODO: check for float (or at compile time)
argsstr += ', ' + this.expr2js(arg, {isconst: this.opts.staticArrays}); argsstr += ', ' + this.expr2js(arg, {isconst: this.opts.staticArrays});
} }
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`; return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
@ -951,8 +963,18 @@ export class BASICRuntime {
} }
} }
do__CONVERT(stmt : basic.CONVERT_Statement) {
var num2str = stmt.dest.name.endsWith('$');
let src = this.expr2js(stmt.src);
let dest = this.assign2js(stmt.dest);
if (num2str) {
return `${dest} = this.valueToString(${src}, false)`;
} else {
return `${dest} = this.VAL(${src})`;
}
}
// TODO: ONERR, ON ERROR GOTO // TODO: ONERR, ON ERROR GOTO
// TODO: gosubs nested too deeply
// TODO: memory quota // TODO: memory quota
// TODO: useless loop (! 4th edition) // TODO: useless loop (! 4th edition)
// TODO: other 4th edition errors // TODO: other 4th edition errors
@ -1210,7 +1232,7 @@ export class BASICRuntime {
return this.checkNum(Math.sqrt(arg)); return this.checkNum(Math.sqrt(arg));
} }
STR$(arg : number) : string { STR$(arg : number) : string {
return this.valueToString(this.checkNum(arg)); return this.valueToString(this.checkNum(arg), false);
} }
STRING$(len : number, chr : number|string) : string { STRING$(len : number, chr : number|string) : string {
len = this.ROUND(len); len = this.ROUND(len);

View File

@ -1648,7 +1648,7 @@ function setupDebugControls() {
uitoolbar.newGroup(); uitoolbar.newGroup();
uitoolbar.grp.prop('id','debug_bar'); uitoolbar.grp.prop('id','debug_bar');
if (platform.runEval) { if (platform.runEval) {
uitoolbar.add('ctrl+alt+e', 'Restart Debugging', 'glyphicon-repeat', resetAndDebug).prop('id','dbg_restart'); uitoolbar.add('ctrl+alt+e', 'Reset and Debug', 'glyphicon-fast-backward', resetAndDebug).prop('id','dbg_restart');
} }
if (platform.stepBack) { if (platform.stepBack) {
uitoolbar.add('ctrl+alt+b', 'Step Backwards', 'glyphicon-step-backward', runStepBackwards).prop('id','dbg_stepback'); uitoolbar.add('ctrl+alt+b', 'Step Backwards', 'glyphicon-step-backward', runStepBackwards).prop('id','dbg_stepback');

View File

@ -6,6 +6,7 @@ import * as views from "../ide/views";
import { BASICRuntime } from "../common/basic/runtime"; import { BASICRuntime } from "../common/basic/runtime";
import { BASICProgram } from "../common/basic/compiler"; import { BASICProgram } from "../common/basic/compiler";
import { TeleTypeWithKeyboard } from "../common/teletype"; import { TeleTypeWithKeyboard } from "../common/teletype";
import { lpad } from "../common/util";
const BASIC_PRESETS = [ const BASIC_PRESETS = [
{ id: 'hello.bas', name: 'Tutorial' }, { id: 'hello.bas', name: 'Tutorial' },
@ -27,7 +28,7 @@ class BASICPlatform implements Platform {
clock: number = 0; clock: number = 0;
timer: AnimationTimer; timer: AnimationTimer;
tty: TeleTypeWithKeyboard; tty: TeleTypeWithKeyboard;
hotReload: boolean = false; hotReload: boolean = true;
animcount: number = 0; animcount: number = 0;
constructor(mainElement: HTMLElement) { constructor(mainElement: HTMLElement) {
@ -199,15 +200,35 @@ class BASICPlatform implements Platform {
} }
} }
inspect(sym: string) { inspect(sym: string) {
var o = this.runtime.vars[sym]; let o = this.runtime.vars[sym];
if (o != null) { if (o != null) return `${sym} = ${o}`;
return o.toString();
}
} }
showHelp(tool:string, ident:string) { showHelp(tool:string, ident:string) {
window.open("https://8bitworkshop.com/blog/platforms/basic/", "_help"); window.open("https://8bitworkshop.com/blog/platforms/basic/", "_help");
} }
/*
getDebugCategories() {
return ['Variables'];
}
getDebugInfo(category:string, state) : string {
switch (category) {
case 'Variables': return this.varsToLongString();
}
}
varsToLongString() : string {
var s = '';
var vars = Object.keys(this.runtime.vars);
vars.sort();
for (var name of vars) {
var value = this.runtime.vars[name];
var valstr = value.toString();
if (valstr.length > 24) valstr = `${valstr.substr(0,24)}...(${valstr.length})`;
s += lpad(name,3) + " = " + valstr + "\n";
}
return s;
}
*/
// TODO: debugging (get running state, etc) // TODO: debugging (get running state, etc)
onBreakpointHit : BreakpointCallback; onBreakpointHit : BreakpointCallback;

View File

@ -45,13 +45,13 @@ function doBuild(msgs, callback, outlen, nlines, nerrors, options) {
} }
assert.ok(err.msg); assert.ok(err.msg);
} }
assert.equal(nerrors, msg.errors.length, "errors"); assert.equal(nerrors, msg.errors.length);
} else { } else {
assert.equal(nerrors||0, 0, "errors"); assert.equal(nerrors||0, 0);
if (msg.output.lines) { // AST for BASIC if (msg.output.stmts) { // AST for BASIC
assert.equal(msg.output.lines.length, outlen, "output lines"); assert.equal(msg.output.stmts.length, outlen);
} else { } else {
assert.equal(msg.output.code?msg.output.code.length:msg.output.length, outlen, "output binary"); assert.equal(msg.output.code?msg.output.code.length:msg.output.length, outlen);
assert.ok(msg.output.code || msg.output instanceof Uint8Array); assert.ok(msg.output.code || msg.output instanceof Uint8Array);
} }
if (nlines) { if (nlines) {
@ -63,7 +63,7 @@ function doBuild(msgs, callback, outlen, nlines, nerrors, options) {
lstkeys.sort(); lstkeys.sort();
for (var key of lstkeys) { for (var key of lstkeys) {
var listing = msg.listings[key]; var listing = msg.listings[key];
assert.equal(listing.lines.length, nlines[i++], "listing lines"); assert.equal(listing.lines.length, nlines[i++]);
} }
} }
} }
@ -336,7 +336,7 @@ describe('Worker', function() {
assert.ok(ast); assert.ok(ast);
done(err, msg); done(err, msg);
}; };
doBuild(msgs, done2, 222, 0, 0); doBuild(msgs, done2, 205, 0, 0);
}); });
}); });