1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-11-26 10:49:17 +00:00

basic: GET, POP, fixed func/builtins, dialect stuff, print head, ELSE (44, 44, 68)

This commit is contained in:
Steven Hugg 2020-08-08 21:36:21 -05:00
parent a1efa8eebd
commit bef0c6e7e3
4 changed files with 336 additions and 97 deletions

View File

@ -353,7 +353,7 @@ div.replaydiv {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50%; background-position: 50%;
pointer-events:auto; pointer-events:auto;
z-index:2; z-index:4;
} }
.gutter.gutter-horizontal { .gutter.gutter-horizontal {
background-image: url('grips/vertical.png'); background-image: url('grips/vertical.png');
@ -424,7 +424,7 @@ div.markdown th {
user-select: auto; user-select: auto;
} }
.alert { .alert {
z-index:2; z-index:8;
} }
.segment { .segment {
border: 2px solid rgba(0,0,0,0.2); border: 2px solid rgba(0,0,0,0.2);
@ -637,7 +637,20 @@ div.asset_toolbar {
bottom: 0; bottom: 0;
height: 3em; height: 3em;
width: 100%; width: 100%;
z-index: 1; z-index: 6;
pointer-events: none;
}
.transcript-print-head.printing {
height: 5em;
}
.transcript-print-shield {
background: '#ffffff';
background: linear-gradient(0deg, rgba(0,0,0,0.5) 0%, rgba(255,255,255,0) 56%, rgba(0,0,0,0) 100%);
position: absolute;
bottom: 0;
height: 3em;
width: 100%;
z-index: 5;
pointer-events: none; pointer-events: none;
} }
.tree-header { .tree-header {

View File

@ -13,7 +13,7 @@ class CompileError extends Error {
// Lexer regular expression -- each (capture group) handles a different token type // Lexer regular expression -- each (capture group) handles a different token type
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi; const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|[0]*(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()?])|(\S+)/gi;
export enum TokenType { export enum TokenType {
EOL = 0, EOL = 0,
@ -68,6 +68,11 @@ export interface LET_Statement {
right: Expr; right: Expr;
} }
export interface DIM_Statement {
command: "DIM";
args: IndOp[];
}
export interface GOTO_Statement { export interface GOTO_Statement {
command: "GOTO"; command: "GOTO";
label: Expr; label: Expr;
@ -82,11 +87,21 @@ export interface RETURN_Statement {
command: "RETURN"; command: "RETURN";
} }
export interface ONGOTO_Statement {
command: "ONGOTO";
expr: Expr;
labels: Expr[];
}
export interface IF_Statement { export interface IF_Statement {
command: "IF"; command: "IF";
cond: Expr; cond: Expr;
} }
export interface ELSE_Statement {
command: "ELSE";
}
export interface FOR_Statement { export interface FOR_Statement {
command: "FOR"; command: "FOR";
lexpr: IndOp; lexpr: IndOp;
@ -100,19 +115,19 @@ export interface NEXT_Statement {
lexpr?: IndOp; lexpr?: IndOp;
} }
export interface DIM_Statement {
command: "DIM";
args: IndOp[];
}
export interface INPUT_Statement { export interface INPUT_Statement {
command: "INPUT"; command: "INPUT";
prompt: Expr; prompt: Expr;
args: IndOp[]; args: IndOp[];
} }
export interface DATA_Statement {
command: "DATA";
datums: Expr[];
}
export interface READ_Statement { export interface READ_Statement {
command: "INPUT"; command: "READ";
args: IndOp[]; args: IndOp[];
} }
@ -122,25 +137,25 @@ export interface DEF_Statement {
def: Expr; def: Expr;
} }
export interface ONGOTO_Statement {
command: "ONGOTO";
expr: Expr;
labels: Expr[];
}
export interface DATA_Statement {
command: "DATA";
datums: Expr[];
}
export interface OPTION_Statement { export interface OPTION_Statement {
command: "OPTION"; command: "OPTION";
optname: string; optname: string;
optargs: string[]; optargs: string[];
} }
export interface GET_Statement {
command: "GET";
lexpr: IndOp;
}
export interface NoArgStatement {
command: string;
}
export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement
| IF_Statement | FOR_Statement | DATA_Statement; | IF_Statement | FOR_Statement | NEXT_Statement | DIM_Statement
| INPUT_Statement | READ_Statement | DEF_Statement | ONGOTO_Statement
| DATA_Statement | OPTION_Statement | NoArgStatement;
export type Statement = StatementTypes & SourceLocated; export type Statement = StatementTypes & SourceLocated;
@ -195,7 +210,8 @@ function getPrecedence(tok: Token): number {
// is token an end of statement marker? (":" or end of line) // is token an end of statement marker? (":" or end of line)
function isEOS(tok: Token) { function isEOS(tok: Token) {
return (tok.type == TokenType.EOL) || (tok.type == TokenType.Operator && tok.str == ':'); return tok.type == TokenType.EOL || tok.type == TokenType.Remark
|| tok.str == ':' || tok.str == 'ELSE'; // TODO: only ELSE if ifElse==true
} }
function stripQuotes(s: string) { function stripQuotes(s: string) {
@ -205,6 +221,8 @@ function stripQuotes(s: string) {
// TODO: implement these // TODO: implement these
export interface BASICOptions { export interface BASICOptions {
dialectName : string; // use this to select the dialect
asciiOnly : boolean; // reject non-ASCII chars?
uppercaseOnly : boolean; // convert everything to uppercase? uppercaseOnly : boolean; // convert everything to uppercase?
optionalLabels : boolean; // can omit line numbers and use labels? optionalLabels : boolean; // can omit line numbers and use labels?
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
@ -213,24 +231,26 @@ export interface BASICOptions {
defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0) defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0)
maxDimensions : number; // max number of dimensions for arrays maxDimensions : number; // max number of dimensions for arrays
stringConcat : boolean; // can concat strings with "+" operator? stringConcat : boolean; // can concat strings with "+" operator?
typeConvert : boolean; // type convert strings <-> numbers? typeConvert : boolean; // type convert strings <-> numbers? (TODO)
maxArguments : number; // maximum # of arguments for user-defined functions maxDefArgs : number; // maximum # of arguments for user-defined functions
sparseArrays : boolean; // true == don't require DIM for arrays maxStringLength : number; // maximum string length in chars
sparseArrays : boolean; // true == don't require DIM for arrays (TODO)
tickComments : boolean; // support 'comments? tickComments : boolean; // support 'comments?
validKeywords : string[]; // valid keywords (or null for accept all) validKeywords : string[]; // valid keywords (or null for accept all)
validFunctions : string[]; // valid functions (or null for accept all) validFunctions : string[]; // valid functions (or null for accept all)
validOperators : string[]; // valid operators (or null for accept all) validOperators : string[]; // valid operators (or null for accept all)
printZoneLength : number; // print zone length printZoneLength : number; // print zone length
printPrecision : number; // print precision # of digits numericPadding : boolean; // " " or "-" before and " " after numbers?
checkOverflow : boolean; // check for overflow of numerics? checkOverflow : boolean; // check for overflow of numerics?
defaultValues : boolean; // initialize unset variables to default value? (0 or "") defaultValues : boolean; // initialize unset variables to default value? (0 or "")
multipleNextVars : boolean; // NEXT Y,X multipleNextVars : boolean; // NEXT Y,X (TODO)
ifElse : boolean; // IF...ELSE construct
} }
///// BASIC PARSER ///// BASIC PARSER
export class BASICParser { export class BASICParser {
opts : BASICOptions = ALTAIR_BASIC40; opts : BASICOptions = MAX8_BASIC;
errors: WorkerError[]; errors: WorkerError[];
listings: CodeListingMap; listings: CodeListingMap;
labels: { [label: string]: BASICLine }; labels: { [label: string]: BASICLine };
@ -285,6 +305,16 @@ export class BASICParser {
parseOptLabel(line: BASICLine) { parseOptLabel(line: BASICLine) {
let tok = this.consumeToken(); let tok = this.consumeToken();
switch (tok.type) { switch (tok.type) {
case TokenType.Ident:
if (this.opts.optionalLabels) {
if (this.peekToken().str == ':') { // is it a label:
this.consumeToken(); // eat the ":"
// fall through to the next case
} else {
this.pushbackToken(tok); // nope
break;
}
} else this.dialectError(`optional line numbers`);
case TokenType.Int: case TokenType.Int:
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`); if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
this.labels[tok.str] = line; this.labels[tok.str] = line;
@ -296,8 +326,8 @@ export class BASICParser {
this.compileError(`Line numbers must be positive integers.`); this.compileError(`Line numbers must be positive integers.`);
break; break;
default: default:
if (this.opts.optionalLabels) this.pushbackToken(tok); if (this.opts.optionalLabels) this.compileError(`A line must start with a line number, command, or label.`);
else this.dialectError(`optional line numbers`); else this.compileError(`A line must start with a line number.`);
break; break;
} }
} }
@ -325,6 +355,9 @@ export class BASICParser {
for (var i = 1; i < TokenType._LAST; i++) { for (var i = 1; i < TokenType._LAST; i++) {
let s : string = m[i]; let s : string = m[i];
if (s != null) { if (s != null) {
// maybe we don't support unicode in 1975?
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
this.dialectError(`non-ASCII characters`);
// uppercase all identifiers, and maybe more // uppercase all identifiers, and maybe more
if (i == TokenType.Ident || this.opts.uppercaseOnly) if (i == TokenType.Ident || this.opts.uppercaseOnly)
s = s.toUpperCase(); s = s.toUpperCase();
@ -354,9 +387,17 @@ export class BASICParser {
} }
parseCompoundStatement(): Statement[] { parseCompoundStatement(): Statement[] {
var list = this.parseList(this.parseStatement, ':'); var list = this.parseList(this.parseStatement, ':');
if (!isEOS(this.peekToken())) this.compileError(`Expected end of line or ':'`, this.peekToken().$loc); var next = this.peekToken();
if (!isEOS(next))
this.compileError(`Expected end of line or ':'`, next.$loc);
if (next.str == 'ELSE')
return list.concat(this.parseCompoundStatement());
else
return list; return list;
} }
validKeyword(keyword: string) : string {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
}
parseStatement(): Statement | null { parseStatement(): Statement | null {
var cmdtok = this.consumeToken(); var cmdtok = this.consumeToken();
var cmd = cmdtok.str; var cmd = cmdtok.str;
@ -365,6 +406,8 @@ export class BASICParser {
case TokenType.Remark: case TokenType.Remark:
if (!this.opts.tickComments) this.dialectError(`tick remarks`); if (!this.opts.tickComments) this.dialectError(`tick remarks`);
return null; return null;
case TokenType.Operator:
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
case TokenType.Ident: case TokenType.Ident:
// remark? ignore all tokens to eol // remark? ignore all tokens to eol
if (cmd == 'REM') { if (cmd == 'REM') {
@ -382,7 +425,7 @@ export class BASICParser {
// lookup JS function for command // lookup JS function for command
var fn = this['stmt__' + cmd]; var fn = this['stmt__' + cmd];
if (fn) { if (fn) {
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0) if (this.validKeyword(cmd) == null)
this.dialectError(`the ${cmd} keyword`); this.dialectError(`the ${cmd} keyword`);
stmt = fn.bind(this)() as Statement; stmt = fn.bind(this)() as Statement;
break; break;
@ -409,8 +452,6 @@ export class BASICParser {
if (this.peekToken().str == '(') { if (this.peekToken().str == '(') {
this.expectToken('('); this.expectToken('(');
args = this.parseExprList(); args = this.parseExprList();
if (args && args.length > this.opts.maxArguments)
this.compileError(`There can be no more than ${this.opts.maxArguments} arguments to a function or subscript.`);
this.expectToken(')'); this.expectToken(')');
} }
return { name: tok.str, args: args, $loc: tok.$loc }; return { name: tok.str, args: args, $loc: tok.$loc };
@ -449,13 +490,15 @@ export class BASICParser {
parseLabel() : Expr { parseLabel() : Expr {
var tok = this.consumeToken(); var tok = this.consumeToken();
switch (tok.type) { switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
case TokenType.Int: case TokenType.Int:
var label = parseInt(tok.str).toString(); var label = tok.str;
this.targets[label] = tok.$loc; this.targets[label] = tok.$loc;
return {value:label}; return {value:label};
default: default:
this.compileError(`There should be a line number here.`); if (this.opts.optionalLabels) this.compileError(`There should be a line number or label here.`);
return; else this.compileError(`There should be a line number here.`);
} }
} }
parsePrimary(): Expr { parsePrimary(): Expr {
@ -580,6 +623,17 @@ export class BASICParser {
this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc}); this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
return { command: "IF", cond: cond }; 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) {
this.pushbackToken({type:TokenType.Ident, str:'GOTO', $loc:lineno.$loc});
}
// add fake ":"
this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
return { command: "ELSE" };
}
stmt__FOR() : FOR_Statement { stmt__FOR() : FOR_Statement {
var lexpr = this.parseLexpr(); // TODO: parseNumVar() var lexpr = this.parseLexpr(); // TODO: parseNumVar()
this.expectToken('='); this.expectToken('=');
@ -624,7 +678,7 @@ export class BASICParser {
stmt__DATA() : DATA_Statement { stmt__DATA() : DATA_Statement {
return { command:'DATA', datums:this.parseExprList() }; return { command:'DATA', datums:this.parseExprList() };
} }
stmt__READ() { stmt__READ() : READ_Statement {
return { command:'READ', args:this.parseLexprList() }; return { command:'READ', args:this.parseLexprList() };
} }
stmt__RESTORE() { stmt__RESTORE() {
@ -647,15 +701,26 @@ 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)
this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`);
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".`)
this.expectToken("="); this.expectToken("=");
this.decls[lexpr.name] = this.lasttoken.$loc; this.decls[lexpr.name] = this.lasttoken.$loc;
var func = this.parseExpr(); var func = this.parseExpr();
return { command:'DEF', lexpr:lexpr, def:func }; return { command:'DEF', lexpr:lexpr, def:func };
} }
stmt__POP() : NoArgStatement {
return { command:'POP' };
}
stmt__GET() : GET_Statement {
var lexpr = this.parseLexpr();
this.decls[lexpr.name] = this.lasttoken.$loc;
return { command:'GET', lexpr:lexpr };
}
// 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(); var tokname = this.consumeToken();
if (tokname.type != TokenType.Ident) this.compileError(`There should be a name after the OPTION statement.`) if (tokname.type != TokenType.Ident) this.compileError(`There must be a name after the OPTION statement.`)
var list : string[] = []; var list : string[] = [];
var tok; var tok;
do { do {
@ -677,7 +742,7 @@ export class BASICParser {
break; break;
case 'DIALECT': case 'DIALECT':
let dname = stmt.optargs[0] || ""; let dname = stmt.optargs[0] || "";
let dialect = DIALECTS[dname]; let dialect = DIALECTS[dname.toUpperCase()];
if (dialect) this.opts = dialect; if (dialect) this.opts = dialect;
else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`); else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`);
break; break;
@ -726,6 +791,8 @@ export class BASICParser {
// TODO // TODO
export const ECMA55_MINIMAL : BASICOptions = { export const ECMA55_MINIMAL : BASICOptions = {
dialectName: "ECMA55",
asciiOnly : true,
uppercaseOnly : true, uppercaseOnly : true,
optionalLabels : false, optionalLabels : false,
strictVarNames : true, strictVarNames : true,
@ -736,7 +803,8 @@ export const ECMA55_MINIMAL : BASICOptions = {
stringConcat : false, stringConcat : false,
typeConvert : false, typeConvert : false,
maxDimensions : 2, maxDimensions : 2,
maxArguments : 255, maxDefArgs : 255,
maxStringLength : 255,
sparseArrays : false, sparseArrays : false,
tickComments : false, tickComments : false,
validKeywords : ['BASE','DATA','DEF','DIM','END', validKeywords : ['BASE','DATA','DEF','DIM','END',
@ -746,38 +814,102 @@ export const ECMA55_MINIMAL : BASICOptions = {
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'], validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'],
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'], validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
printZoneLength : 15, printZoneLength : 15,
printPrecision : 6, numericPadding : true,
checkOverflow : true, checkOverflow : true,
multipleNextVars : false, multipleNextVars : false,
ifElse : false,
} }
export const ALTAIR_BASIC40 : BASICOptions = { export const ALTAIR_BASIC40 : BASICOptions = {
dialectName: "ALTAIR40",
asciiOnly : true,
uppercaseOnly : true, uppercaseOnly : true,
optionalLabels : false, optionalLabels : false,
strictVarNames : true, strictVarNames : false,
sharedArrayNamespace : true, sharedArrayNamespace : true,
defaultArrayBase : 0, defaultArrayBase : 0,
defaultArraySize : 11, defaultArraySize : 11,
defaultValues : false, defaultValues : true,
stringConcat : false, stringConcat : true,
typeConvert : false, typeConvert : false,
maxDimensions : 2, maxDimensions : 128, // "as many as will fit on a single line" ... ?
maxArguments : 255, maxDefArgs : 255,
maxStringLength : 255,
sparseArrays : false, sparseArrays : false,
tickComments : false, tickComments : false,
validKeywords : null, // all validKeywords : null, // all
validFunctions : null, // all validFunctions : null, // all
validOperators : null, // all ['\\','MOD','NOT','AND','OR','XOR','EQV','IMP'], validOperators : null, // all ['\\','MOD','NOT','AND','OR','XOR','EQV','IMP'],
printZoneLength : 15, printZoneLength : 15,
printPrecision : 6, numericPadding : true,
checkOverflow : true,
multipleNextVars : true, // TODO: not supported
ifElse : true,
}
export const APPLESOFT_BASIC : BASICOptions = {
dialectName: "APPLESOFT",
asciiOnly : true,
uppercaseOnly : false,
optionalLabels : false,
strictVarNames : false, // TODO: first two alphanum chars
sharedArrayNamespace : false,
defaultArrayBase : 0,
defaultArraySize : 9, // A(0) to A(8)
defaultValues : true,
stringConcat : true,
typeConvert : false,
maxDimensions : 88,
maxDefArgs : 1, // TODO: no string FNs
maxStringLength : 255,
sparseArrays : false,
tickComments : false,
validKeywords : null, // all
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN',
'LEN','LEFT$','MID$','RIGHT$','STR$','VAL','CHR$','ASC',
'FRE','SCRN','PDL','PEEK'], // TODO
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'],
printZoneLength : 16,
numericPadding : false,
checkOverflow : true,
multipleNextVars : false,
ifElse : false,
}
export const MAX8_BASIC : BASICOptions = {
dialectName: "MAX8",
asciiOnly : false,
uppercaseOnly : false,
optionalLabels : true,
strictVarNames : false, // TODO: first two alphanum chars
sharedArrayNamespace : false,
defaultArrayBase : 0,
defaultArraySize : 11,
defaultValues : true,
stringConcat : true,
typeConvert : true,
maxDimensions : 255,
maxDefArgs : 255, // TODO: no string FNs
maxStringLength : 1024*1024,
sparseArrays : false,
tickComments : true,
validKeywords : null, // all
validFunctions : null,
validOperators : null,
printZoneLength : 15,
numericPadding : false,
checkOverflow : true, checkOverflow : true,
multipleNextVars : true, multipleNextVars : true,
ifElse : true,
} }
// TODO: integer vars
export const DIALECTS = { export const DIALECTS = {
"DEFAULT": ALTAIR_BASIC40, "DEFAULT": ALTAIR_BASIC40,
"ALTAIR": ALTAIR_BASIC40, "ALTAIR": ALTAIR_BASIC40,
"ALTAIR40": ALTAIR_BASIC40, "ALTAIR40": ALTAIR_BASIC40,
"ECMA55": ECMA55_MINIMAL, "ECMA55": ECMA55_MINIMAL,
"MINIMAL": ECMA55_MINIMAL, "MINIMAL": ECMA55_MINIMAL,
"APPLESOFT": APPLESOFT_BASIC,
}; };

View File

@ -34,6 +34,7 @@ export class BASICRuntime {
label2lineidx : {[label : string] : number}; label2lineidx : {[label : string] : number};
label2pc : {[label : string] : number}; label2pc : {[label : string] : number};
datums : basic.Literal[]; datums : basic.Literal[];
builtins : {};
curpc : number; curpc : number;
dataptr : number; dataptr : number;
@ -57,6 +58,7 @@ export class BASICRuntime {
this.line2pc = []; this.line2pc = [];
this.pc2line = new Map(); this.pc2line = new Map();
this.datums = []; this.datums = [];
this.builtins = this.getBuiltinFunctions();
// TODO: lines start @ 1? // TODO: lines start @ 1?
program.lines.forEach((line, idx) => { program.lines.forEach((line, idx) => {
// make lookup tables // make lookup tables
@ -76,7 +78,7 @@ export class BASICRuntime {
}); });
}); });
// TODO: compile statements? // TODO: compile statements?
//line.stmts.forEach((stmt) => this.compileStatement(stmt)); line.stmts.forEach((stmt) => this.compileStatement(stmt));
}); });
// try to resume where we left off after loading // try to resume where we left off after loading
this.curpc = this.label2pc[prevlabel] || 0; this.curpc = this.label2pc[prevlabel] || 0;
@ -88,7 +90,7 @@ export class BASICRuntime {
this.dataptr = 0; this.dataptr = 0;
this.vars = {}; this.vars = {};
this.arrays = {}; this.arrays = {};
this.defs = this.getBuiltinFunctions(); this.defs = {};
this.forLoops = []; this.forLoops = [];
this.returnStack = []; this.returnStack = [];
this.column = 0; this.column = 0;
@ -110,6 +112,9 @@ export class BASICRuntime {
// TODO: pass source location to error // TODO: pass source location to error
throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`); throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`);
} }
dialectError(what : string) {
this.runtimeError(`I can't ${what} in this dialect of BASIC.`);
}
getLineForPC(pc:number) { getLineForPC(pc:number) {
var line; var line;
@ -150,11 +155,16 @@ export class BASICRuntime {
compileStatement(stmt: basic.Statement & CompiledStatement) { compileStatement(stmt: basic.Statement & CompiledStatement) {
if (stmt.$run == null) { if (stmt.$run == null) {
try {
var stmtfn = this['do__' + stmt.command]; var stmtfn = this['do__' + stmt.command];
if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`); if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`);
var functext = stmtfn.bind(this)(stmt); var functext = stmtfn.bind(this)(stmt);
if (this.trace) console.log(functext); if (this.trace) console.log(functext);
stmt.$run = new Function(functext).bind(this); stmt.$run = new Function(functext).bind(this);
} catch (e) {
console.log(functext);
throw e;
}
} }
} }
executeStatement(stmt: basic.Statement & CompiledStatement) { executeStatement(stmt: basic.Statement & CompiledStatement) {
@ -168,6 +178,19 @@ export class BASICRuntime {
} while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc)); } while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
} }
skipToElse() {
do {
// in Altair BASIC, ELSE is bound to the right-most IF
// TODO: this is complicated, we should just have nested expressions
if (this.program.opts.ifElse) {
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));
}
skipToEOF() { skipToEOF() {
this.curpc = this.allstmts.length; this.curpc = this.allstmts.length;
} }
@ -193,29 +216,37 @@ export class BASICRuntime {
this.curpc = pc; this.curpc = pc;
} }
popReturnStack() {
if (this.returnStack.length == 0)
this.runtimeError("I tried to POP, but there wasn't a corresponding GOSUB.");
this.returnStack.pop();
}
valueToString(obj) : string { valueToString(obj) : string {
var str; var str;
if (typeof obj === 'number') { if (typeof obj === 'number') {
var numstr = obj.toString().toUpperCase(); var numstr = obj.toString().toUpperCase();
var prec = 11; var numlen = this.program.opts.printZoneLength - 4;
while (numstr.length > 11) { var prec = numlen;
while (numstr.length > numlen) {
numstr = obj.toPrecision(prec--); numstr = obj.toPrecision(prec--);
} }
if (numstr.startsWith('0.')) if (numstr.startsWith('0.'))
numstr = numstr.substr(1); numstr = numstr.substr(1);
else if (numstr.startsWith('-0.')) else if (numstr.startsWith('-0.'))
numstr = '-'+numstr.substr(2); numstr = '-'+numstr.substr(2);
if (numstr.startsWith('-')) { if (!this.program.opts.numericPadding)
str = numstr;
else if (numstr.startsWith('-'))
str = `${numstr} `; str = `${numstr} `;
} else { else
str = ` ${numstr} `; str = ` ${numstr} `;
}
} else if (obj == '\n') { } else if (obj == '\n') {
this.column = 0; this.column = 0;
str = obj; str = obj;
} else if (obj == '\t') { } else if (obj == '\t') {
var curgroup = Math.floor(this.column / 15); var curgroup = Math.floor(this.column / this.program.opts.printZoneLength);
var nextcol = (curgroup + 1) * 15; var nextcol = (curgroup + 1) * this.program.opts.printZoneLength;
str = this.TAB(nextcol); str = this.TAB(nextcol);
} else { } else {
str = `${obj}`; str = `${obj}`;
@ -251,15 +282,16 @@ export class BASICRuntime {
} else { } else {
if (opts.isconst) this.runtimeError(`I expected a constant value here`); if (opts.isconst) this.runtimeError(`I expected a constant value here`);
var s = ''; var s = '';
if (this.defs[expr.name]) { // is it a function? if (expr.name.startsWith("FN")) { // is it a user-defined function?
s += `this.defs.${expr.name}(`; let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
if (expr.args) s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', '); s += `this.defs.${expr.name}(${jsargs})`; // TODO: what if no exist?
s += ')'; } else if (this.builtins[expr.name]) { // is it a built-in function?
let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
s += `this.builtins.${expr.name}(${jsargs})`;
} else if (expr.args) { // is it a subscript? } else if (expr.args) { // is it a subscript?
s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`; s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join(''); s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join('');
} else { } else { // just a variable
// just a variable
s = `this.vars.${expr.name}`; s = `this.vars.${expr.name}`;
} }
if (opts.check) if (opts.check)
@ -409,6 +441,7 @@ export class BASICRuntime {
} }
do__LET(stmt : basic.LET_Statement) { do__LET(stmt : basic.LET_Statement) {
// TODO: range-checking for subscripts (get and set)
var lexpr = this.expr2js(stmt.lexpr, {check:false}); var lexpr = this.expr2js(stmt.lexpr, {check:false});
var right = this.expr2js(stmt.right, {check:true}); var right = this.expr2js(stmt.right, {check:true});
return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`; return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
@ -429,11 +462,14 @@ export class BASICRuntime {
do__IF(stmt : basic.IF_Statement) { do__IF(stmt : basic.IF_Statement) {
var cond = this.expr2js(stmt.cond, {check:true}); var cond = this.expr2js(stmt.cond, {check:true});
return `if (!(${cond})) { this.skipToEOL(); }` return `if (!(${cond})) { this.skipToElse(); }`
}
do__ELSE() {
return `this.skipToEOL()`
} }
do__DEF(stmt : basic.DEF_Statement) { do__DEF(stmt : basic.DEF_Statement) {
var lexpr = `this.defs.${stmt.lexpr.name}`;
var args = []; var args = [];
for (var arg of stmt.lexpr.args || []) { for (var arg of stmt.lexpr.args || []) {
if (isLookup(arg)) { if (isLookup(arg)) {
@ -443,6 +479,8 @@ export class BASICRuntime {
} }
} }
var functext = this.expr2js(stmt.def, {check:true, locals:args}); var functext = this.expr2js(stmt.def, {check:true, locals:args});
//this.defs[stmt.lexpr.name] = new Function(args.join(','), functext).bind(this);
var lexpr = `this.defs.${stmt.lexpr.name}`;
return `${lexpr} = function(${args.join(',')}) { return ${functext}; }.bind(this)`; return `${lexpr} = function(${args.join(',')}) { return ${functext}; }.bind(this)`;
} }
@ -509,9 +547,28 @@ export class BASICRuntime {
// already parsed in compiler // already parsed in compiler
} }
do__POP() {
return `this.popReturnStack()`;
}
do__GET(stmt : basic.GET_Statement) {
var lexpr = this.expr2js(stmt.lexpr, {check:false});
// TODO: single key input
return `this.running=false;
this.input().then((vals) => {
${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, vals[0]);
this.running=true;
this.resume();
})`;
}
// TODO: ONERR, ON ERROR GOTO
// TODO: "SUBSCRIPT ERROR" (range check) // TODO: "SUBSCRIPT ERROR" (range check)
// TODO: gosubs nested too deeply // TODO: gosubs nested too deeply
// TODO: memory quota // TODO: memory quota
// TODO: useless loop (! 4th edition)
// TODO: other 4th edition errors
// FUNCTIONS // FUNCTIONS
@ -548,7 +605,10 @@ export class BASICRuntime {
} }
checkString(s:string) : string { checkString(s:string) : string {
if (typeof s !== 'string') this.runtimeError(`I expected a string here.`); if (typeof s !== 'string')
this.runtimeError(`I expected a string here.`);
else if (s.length > this.program.opts.maxStringLength)
this.dialectError(`create strings longer than ${this.program.opts.maxStringLength} characters`);
return s; return s;
} }
@ -556,8 +616,10 @@ export class BASICRuntime {
// TODO: if string-concat // TODO: if string-concat
if (typeof a === 'number' && typeof b === 'number') if (typeof a === 'number' && typeof b === 'number')
return this.checkNum(a + b); return this.checkNum(a + b);
else if (this.program.opts.stringConcat)
return this.checkString(a + b);
else else
return a + b; this.dialectError(`use the "+" operator to concatenate strings`)
} }
sub(a:number, b:number) : number { sub(a:number, b:number) : number {
return this.checkNum(a - b); return this.checkNum(a - b);
@ -597,23 +659,23 @@ export class BASICRuntime {
bor(a:number, b:number) : number { bor(a:number, b:number) : number {
return a | b; return a | b;
} }
eq(a:number, b:number) : boolean { eq(a:number, b:number) : number {
return a == b; return a == b ? 1 : 0;
} }
ne(a:number, b:number) : boolean { ne(a:number, b:number) : number {
return a != b; return a != b ? 1 : 0;
} }
lt(a:number, b:number) : boolean { lt(a:number, b:number) : number {
return a < b; return a < b ? 1 : 0;
} }
gt(a:number, b:number) : boolean { gt(a:number, b:number) : number {
return a > b; return a > b ? 1 : 0;
} }
le(a:number, b:number) : boolean { le(a:number, b:number) : number {
return a <= b; return a <= b ? 1 : 0;
} }
ge(a:number, b:number) : boolean { ge(a:number, b:number) : number {
return a >= b; return a >= b ? 1 : 0;
} }
// FUNCTIONS (uppercase) // FUNCTIONS (uppercase)
@ -633,6 +695,9 @@ export class BASICRuntime {
COS(arg : number) : number { COS(arg : number) : number {
return this.checkNum(Math.cos(arg)); return this.checkNum(Math.cos(arg));
} }
COT(arg : number) : number {
return this.checkNum(1.0 / Math.tan(arg)); // 4th edition only
}
EXP(arg : number) : number { EXP(arg : number) : number {
return this.checkNum(Math.exp(arg)); return this.checkNum(Math.exp(arg));
} }

View File

@ -36,6 +36,7 @@ class TeleType {
this.lines = []; this.lines = [];
this.ncharsout = 0; this.ncharsout = 0;
$(this.page).empty(); $(this.page).empty();
this.showPrintHead(true);
} }
ensureline() { ensureline() {
if (this.curline == null) { if (this.curline == null) {
@ -75,13 +76,15 @@ class TeleType {
span.appendTo(this.curline); span.appendTo(this.curline);
} }
this.col += line.length; this.col += line.length;
// TODO: wrap @ 80 columns
this.ncharsout += line.length; this.ncharsout += line.length;
//this.movePrintHead(); this.movePrintHead(true);
} }
} }
newline() { newline() {
this.flushline(); this.flushline();
this.col = 0; this.col = 0;
this.movePrintHead(false);
} }
// TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines? // TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines?
print(val: string) { print(val: string) {
@ -129,9 +132,21 @@ class TeleType {
scrollToBottom() { scrollToBottom() {
this.curline.scrollIntoView(); this.curline.scrollIntoView();
} }
movePrintHead() { movePrintHead(printing: boolean) {
var x = $(this.page).position().left + this.col * ($(this.page).width() / 80); /*
$("#printhead").offset({left: x}); var ph = $("#printhead"); // TODO: speed?
var x = $(this.page).position().left + this.col * ($(this.page).width() / 80) - 200;
ph.stop().animate({left: x}, {duration:20});
//ph.offset({left: x});
if (printing) ph.addClass("printing");
else ph.removeClass("printing");
*/
}
showPrintHead(show: boolean) {
/*
var ph = $("#printhead"); // TODO: speed?
if (show) ph.show(); else ph.hide();
*/
} }
} }
@ -151,11 +166,15 @@ class TeleTypeWithKeyboard extends TeleType {
this.input = input; this.input = input;
this.platform = platform; this.platform = platform;
this.runtime = platform.runtime; this.runtime = platform.runtime;
this.runtime.input = async (prompt:string) => { this.runtime.input = async (prompt:string, nargs:number) => {
return new Promise( (resolve, reject) => { return new Promise( (resolve, reject) => {
if (prompt != null) {
this.addtext(prompt, 0); this.addtext(prompt, 0);
this.addtext('? ', 0); this.addtext('? ', 0);
this.waitingfor = 'line'; this.waitingfor = 'line';
} else {
this.waitingfor = 'char';
}
this.focusinput(); this.focusinput();
this.resolveInput = resolve; this.resolveInput = resolve;
}); });
@ -176,8 +195,13 @@ class TeleTypeWithKeyboard extends TeleType {
}; };
this.hideinput(); this.hideinput();
} }
clear() {
super.clear();
this.hideinput();
}
focusinput() { focusinput() {
this.ensureline(); this.ensureline();
this.showPrintHead(false);
// don't steal focus while editing // don't steal focus while editing
if (this.keepinput) if (this.keepinput)
$(this.input).css('visibility', 'visible'); $(this.input).css('visibility', 'visible');
@ -194,6 +218,7 @@ class TeleTypeWithKeyboard extends TeleType {
$(this.input).removeClass('transcript-input-char') $(this.input).removeClass('transcript-input-char')
} }
hideinput() { hideinput() {
this.showPrintHead(true);
if (this.keepinput) if (this.keepinput)
$(this.input).css('visibility','hidden'); $(this.input).css('visibility','hidden');
else else
@ -215,16 +240,18 @@ class TeleTypeWithKeyboard extends TeleType {
} }
sendinput(s: string) { sendinput(s: string) {
if (this.resolveInput) { if (this.resolveInput) {
if (this.platform.program.opts.uppercaseOnly)
s = s.toUpperCase(); s = s.toUpperCase();
this.addtext(s, 4); this.addtext(s, 4);
this.flushline(); this.flushline();
this.resolveInput(s.split(',')); this.resolveInput(s.split(',')); // TODO: should parse quotes, etc
this.resolveInput = null; this.resolveInput = null;
} }
this.clearinput(); this.clearinput();
this.hideinput(); // keep from losing input handlers this.hideinput(); // keep from losing input handlers
} }
sendchar(code: number) { sendchar(code: number) {
this.sendinput(String.fromCharCode(code));
} }
ensureline() { ensureline() {
if (!this.keepinput) $(this.input).hide(); if (!this.keepinput) $(this.input).hide();
@ -277,6 +304,7 @@ class BASICPlatform implements Platform {
var windowport = $('<div id="windowport" class="transcript transcript-style-2"/>').appendTo(gameport); var windowport = $('<div id="windowport" class="transcript transcript-style-2"/>').appendTo(gameport);
var inputline = $('<input class="transcript-input transcript-style-2" type="text" style="max-width:95%"/>').appendTo(parent); var inputline = $('<input class="transcript-input transcript-style-2" type="text" style="max-width:95%"/>').appendTo(parent);
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent); //var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
//var printshield = $('<div id="printhead" class="transcript-print-shield"/>').appendTo(parent);
this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this); this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this);
this.tty.scrolldiv = parent; this.tty.scrolldiv = parent;
this.timer = new AnimationTimer(60, this.animate.bind(this)); this.timer = new AnimationTimer(60, this.animate.bind(this));
@ -323,6 +351,7 @@ class BASICPlatform implements Platform {
exitmsg() { exitmsg() {
this.tty.print("\n\n"); this.tty.print("\n\n");
this.tty.addtext("*** END OF PROGRAM ***", 1); this.tty.addtext("*** END OF PROGRAM ***", 1);
this.tty.showPrintHead(false);
} }
resize: () => void; resize: () => void;