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-position: 50%;
pointer-events:auto;
z-index:2;
z-index:4;
}
.gutter.gutter-horizontal {
background-image: url('grips/vertical.png');
@ -424,7 +424,7 @@ div.markdown th {
user-select: auto;
}
.alert {
z-index:2;
z-index:8;
}
.segment {
border: 2px solid rgba(0,0,0,0.2);
@ -637,7 +637,20 @@ div.asset_toolbar {
bottom: 0;
height: 3em;
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;
}
.tree-header {

View File

@ -13,7 +13,7 @@ class CompileError extends Error {
// 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 {
EOL = 0,
@ -68,6 +68,11 @@ export interface LET_Statement {
right: Expr;
}
export interface DIM_Statement {
command: "DIM";
args: IndOp[];
}
export interface GOTO_Statement {
command: "GOTO";
label: Expr;
@ -82,11 +87,21 @@ export interface RETURN_Statement {
command: "RETURN";
}
export interface ONGOTO_Statement {
command: "ONGOTO";
expr: Expr;
labels: Expr[];
}
export interface IF_Statement {
command: "IF";
cond: Expr;
}
export interface ELSE_Statement {
command: "ELSE";
}
export interface FOR_Statement {
command: "FOR";
lexpr: IndOp;
@ -100,19 +115,19 @@ export interface NEXT_Statement {
lexpr?: IndOp;
}
export interface DIM_Statement {
command: "DIM";
args: IndOp[];
}
export interface INPUT_Statement {
command: "INPUT";
prompt: Expr;
args: IndOp[];
}
export interface DATA_Statement {
command: "DATA";
datums: Expr[];
}
export interface READ_Statement {
command: "INPUT";
command: "READ";
args: IndOp[];
}
@ -122,25 +137,25 @@ export interface DEF_Statement {
def: Expr;
}
export interface ONGOTO_Statement {
command: "ONGOTO";
expr: Expr;
labels: Expr[];
}
export interface DATA_Statement {
command: "DATA";
datums: Expr[];
}
export interface OPTION_Statement {
command: "OPTION";
optname: 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
| 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;
@ -195,7 +210,8 @@ function getPrecedence(tok: Token): number {
// is token an end of statement marker? (":" or end of line)
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) {
@ -205,32 +221,36 @@ function stripQuotes(s: string) {
// TODO: implement these
export interface BASICOptions {
dialectName : string; // use this to select the dialect
asciiOnly : boolean; // reject non-ASCII chars?
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
sharedArrayNamespace : boolean; // arrays and variables have same namespace? (conflict)
defaultArrayBase : number; // arrays start at this number (0 or 1)
defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0)
maxDimensions : number; // max number of dimensions for arrays
stringConcat : boolean; // can concat strings with "+" operator?
typeConvert : boolean; // type convert strings <-> numbers?
maxArguments : number; // maximum # of arguments for user-defined functions
sparseArrays : boolean; // true == don't require DIM for arrays
typeConvert : boolean; // type convert strings <-> numbers? (TODO)
maxDefArgs : number; // maximum # of arguments for user-defined functions
maxStringLength : number; // maximum string length in chars
sparseArrays : boolean; // true == don't require DIM for arrays (TODO)
tickComments : boolean; // support 'comments?
validKeywords : string[]; // valid keywords (or null for accept all)
validFunctions : string[]; // valid functions (or null for accept all)
validOperators : string[]; // valid operators (or null for accept all)
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?
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
export class BASICParser {
opts : BASICOptions = ALTAIR_BASIC40;
opts : BASICOptions = MAX8_BASIC;
errors: WorkerError[];
listings: CodeListingMap;
labels: { [label: string]: BASICLine };
@ -285,6 +305,16 @@ export class BASICParser {
parseOptLabel(line: BASICLine) {
let tok = this.consumeToken();
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:
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
this.labels[tok.str] = line;
@ -296,8 +326,8 @@ export class BASICParser {
this.compileError(`Line numbers must be positive integers.`);
break;
default:
if (this.opts.optionalLabels) this.pushbackToken(tok);
else this.dialectError(`optional line numbers`);
if (this.opts.optionalLabels) this.compileError(`A line must start with a line number, command, or label.`);
else this.compileError(`A line must start with a line number.`);
break;
}
}
@ -325,6 +355,9 @@ export class BASICParser {
for (var i = 1; i < TokenType._LAST; i++) {
let s : string = m[i];
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
if (i == TokenType.Ident || this.opts.uppercaseOnly)
s = s.toUpperCase();
@ -354,8 +387,16 @@ export class BASICParser {
}
parseCompoundStatement(): Statement[] {
var list = this.parseList(this.parseStatement, ':');
if (!isEOS(this.peekToken())) this.compileError(`Expected end of line or ':'`, this.peekToken().$loc);
return list;
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;
}
validKeyword(keyword: string) : string {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
}
parseStatement(): Statement | null {
var cmdtok = this.consumeToken();
@ -365,6 +406,8 @@ export class BASICParser {
case TokenType.Remark:
if (!this.opts.tickComments) this.dialectError(`tick remarks`);
return null;
case TokenType.Operator:
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
case TokenType.Ident:
// remark? ignore all tokens to eol
if (cmd == 'REM') {
@ -382,7 +425,7 @@ export class BASICParser {
// lookup JS function for command
var fn = this['stmt__' + cmd];
if (fn) {
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
if (this.validKeyword(cmd) == null)
this.dialectError(`the ${cmd} keyword`);
stmt = fn.bind(this)() as Statement;
break;
@ -409,8 +452,6 @@ export class BASICParser {
if (this.peekToken().str == '(') {
this.expectToken('(');
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(')');
}
return { name: tok.str, args: args, $loc: tok.$loc };
@ -449,13 +490,15 @@ export class BASICParser {
parseLabel() : Expr {
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
case TokenType.Int:
var label = parseInt(tok.str).toString();
var label = tok.str;
this.targets[label] = tok.$loc;
return {value:label};
default:
this.compileError(`There should be a line number here.`);
return;
if (this.opts.optionalLabels) this.compileError(`There should be a line number or label here.`);
else this.compileError(`There should be a line number here.`);
}
}
parsePrimary(): Expr {
@ -580,6 +623,17 @@ export class BASICParser {
this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
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 {
var lexpr = this.parseLexpr(); // TODO: parseNumVar()
this.expectToken('=');
@ -624,7 +678,7 @@ export class BASICParser {
stmt__DATA() : DATA_Statement {
return { command:'DATA', datums:this.parseExprList() };
}
stmt__READ() {
stmt__READ() : READ_Statement {
return { command:'READ', args:this.parseLexprList() };
}
stmt__RESTORE() {
@ -647,15 +701,26 @@ export class BASICParser {
}
stmt__DEF() : DEF_Statement {
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".`)
this.expectToken("=");
this.decls[lexpr.name] = this.lasttoken.$loc;
var func = this.parseExpr();
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 {
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 tok;
do {
@ -677,7 +742,7 @@ export class BASICParser {
break;
case 'DIALECT':
let dname = stmt.optargs[0] || "";
let dialect = DIALECTS[dname];
let dialect = DIALECTS[dname.toUpperCase()];
if (dialect) this.opts = dialect;
else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`);
break;
@ -726,6 +791,8 @@ export class BASICParser {
// TODO
export const ECMA55_MINIMAL : BASICOptions = {
dialectName: "ECMA55",
asciiOnly : true,
uppercaseOnly : true,
optionalLabels : false,
strictVarNames : true,
@ -736,7 +803,8 @@ export const ECMA55_MINIMAL : BASICOptions = {
stringConcat : false,
typeConvert : false,
maxDimensions : 2,
maxArguments : 255,
maxDefArgs : 255,
maxStringLength : 255,
sparseArrays : false,
tickComments : false,
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'],
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
printZoneLength : 15,
printPrecision : 6,
numericPadding : true,
checkOverflow : true,
multipleNextVars : false,
ifElse : false,
}
export const ALTAIR_BASIC40 : BASICOptions = {
dialectName: "ALTAIR40",
asciiOnly : true,
uppercaseOnly : true,
optionalLabels : false,
strictVarNames : true,
strictVarNames : false,
sharedArrayNamespace : true,
defaultArrayBase : 0,
defaultArraySize : 11,
defaultValues : false,
stringConcat : false,
defaultValues : true,
stringConcat : true,
typeConvert : false,
maxDimensions : 2,
maxArguments : 255,
maxDimensions : 128, // "as many as will fit on a single line" ... ?
maxDefArgs : 255,
maxStringLength : 255,
sparseArrays : false,
tickComments : false,
validKeywords : null, // all
validFunctions : null, // all
validOperators : null, // all ['\\','MOD','NOT','AND','OR','XOR','EQV','IMP'],
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,
multipleNextVars : true,
ifElse : true,
}
// TODO: integer vars
export const DIALECTS = {
"DEFAULT": ALTAIR_BASIC40,
"ALTAIR": ALTAIR_BASIC40,
"ALTAIR40": ALTAIR_BASIC40,
"ECMA55": ECMA55_MINIMAL,
"MINIMAL": ECMA55_MINIMAL,
"APPLESOFT": APPLESOFT_BASIC,
};

View File

@ -34,6 +34,7 @@ export class BASICRuntime {
label2lineidx : {[label : string] : number};
label2pc : {[label : string] : number};
datums : basic.Literal[];
builtins : {};
curpc : number;
dataptr : number;
@ -57,6 +58,7 @@ export class BASICRuntime {
this.line2pc = [];
this.pc2line = new Map();
this.datums = [];
this.builtins = this.getBuiltinFunctions();
// TODO: lines start @ 1?
program.lines.forEach((line, idx) => {
// make lookup tables
@ -76,7 +78,7 @@ export class BASICRuntime {
});
});
// 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
this.curpc = this.label2pc[prevlabel] || 0;
@ -88,7 +90,7 @@ export class BASICRuntime {
this.dataptr = 0;
this.vars = {};
this.arrays = {};
this.defs = this.getBuiltinFunctions();
this.defs = {};
this.forLoops = [];
this.returnStack = [];
this.column = 0;
@ -110,6 +112,9 @@ export class BASICRuntime {
// TODO: pass source location to error
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) {
var line;
@ -150,11 +155,16 @@ export class BASICRuntime {
compileStatement(stmt: basic.Statement & CompiledStatement) {
if (stmt.$run == null) {
var stmtfn = this['do__' + stmt.command];
if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`);
var functext = stmtfn.bind(this)(stmt);
if (this.trace) console.log(functext);
stmt.$run = new Function(functext).bind(this);
try {
var stmtfn = this['do__' + stmt.command];
if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`);
var functext = stmtfn.bind(this)(stmt);
if (this.trace) console.log(functext);
stmt.$run = new Function(functext).bind(this);
} catch (e) {
console.log(functext);
throw e;
}
}
}
executeStatement(stmt: basic.Statement & CompiledStatement) {
@ -168,6 +178,19 @@ export class BASICRuntime {
} 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() {
this.curpc = this.allstmts.length;
}
@ -192,30 +215,38 @@ export class BASICRuntime {
var pc = this.returnStack.pop();
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 {
var str;
if (typeof obj === 'number') {
var numstr = obj.toString().toUpperCase();
var prec = 11;
while (numstr.length > 11) {
var numlen = this.program.opts.printZoneLength - 4;
var prec = numlen;
while (numstr.length > numlen) {
numstr = obj.toPrecision(prec--);
}
if (numstr.startsWith('0.'))
numstr = numstr.substr(1);
else if (numstr.startsWith('-0.'))
numstr = '-'+numstr.substr(2);
if (numstr.startsWith('-')) {
if (!this.program.opts.numericPadding)
str = numstr;
else if (numstr.startsWith('-'))
str = `${numstr} `;
} else {
else
str = ` ${numstr} `;
}
} else if (obj == '\n') {
this.column = 0;
str = obj;
} else if (obj == '\t') {
var curgroup = Math.floor(this.column / 15);
var nextcol = (curgroup + 1) * 15;
var curgroup = Math.floor(this.column / this.program.opts.printZoneLength);
var nextcol = (curgroup + 1) * this.program.opts.printZoneLength;
str = this.TAB(nextcol);
} else {
str = `${obj}`;
@ -251,15 +282,16 @@ export class BASICRuntime {
} else {
if (opts.isconst) this.runtimeError(`I expected a constant value here`);
var s = '';
if (this.defs[expr.name]) { // is it a function?
s += `this.defs.${expr.name}(`;
if (expr.args) s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
s += ')';
if (expr.name.startsWith("FN")) { // is it a user-defined function?
let jsargs = expr.args && expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
s += `this.defs.${expr.name}(${jsargs})`; // TODO: what if no exist?
} 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?
s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join('');
} else {
// just a variable
} else { // just a variable
s = `this.vars.${expr.name}`;
}
if (opts.check)
@ -409,6 +441,7 @@ export class BASICRuntime {
}
do__LET(stmt : basic.LET_Statement) {
// TODO: range-checking for subscripts (get and set)
var lexpr = this.expr2js(stmt.lexpr, {check:false});
var right = this.expr2js(stmt.right, {check:true});
return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
@ -429,11 +462,14 @@ export class BASICRuntime {
do__IF(stmt : basic.IF_Statement) {
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) {
var lexpr = `this.defs.${stmt.lexpr.name}`;
var args = [];
for (var arg of stmt.lexpr.args || []) {
if (isLookup(arg)) {
@ -443,6 +479,8 @@ export class BASICRuntime {
}
}
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)`;
}
@ -509,9 +547,28 @@ export class BASICRuntime {
// 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: gosubs nested too deeply
// TODO: memory quota
// TODO: useless loop (! 4th edition)
// TODO: other 4th edition errors
// FUNCTIONS
@ -548,7 +605,10 @@ export class BASICRuntime {
}
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;
}
@ -556,8 +616,10 @@ export class BASICRuntime {
// TODO: if string-concat
if (typeof a === 'number' && typeof b === 'number')
return this.checkNum(a + b);
else if (this.program.opts.stringConcat)
return this.checkString(a + b);
else
return a + b;
this.dialectError(`use the "+" operator to concatenate strings`)
}
sub(a:number, b:number) : number {
return this.checkNum(a - b);
@ -597,23 +659,23 @@ export class BASICRuntime {
bor(a:number, b:number) : number {
return a | b;
}
eq(a:number, b:number) : boolean {
return a == b;
eq(a:number, b:number) : number {
return a == b ? 1 : 0;
}
ne(a:number, b:number) : boolean {
return a != b;
ne(a:number, b:number) : number {
return a != b ? 1 : 0;
}
lt(a:number, b:number) : boolean {
return a < b;
lt(a:number, b:number) : number {
return a < b ? 1 : 0;
}
gt(a:number, b:number) : boolean {
return a > b;
gt(a:number, b:number) : number {
return a > b ? 1 : 0;
}
le(a:number, b:number) : boolean {
return a <= b;
le(a:number, b:number) : number {
return a <= b ? 1 : 0;
}
ge(a:number, b:number) : boolean {
return a >= b;
ge(a:number, b:number) : number {
return a >= b ? 1 : 0;
}
// FUNCTIONS (uppercase)
@ -633,6 +695,9 @@ export class BASICRuntime {
COS(arg : number) : number {
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 {
return this.checkNum(Math.exp(arg));
}

View File

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