mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-11-25 18:33:11 +00:00
basic: GET, POP, fixed func/builtins, dialect stuff, print head, ELSE (44, 44, 68)
This commit is contained in:
parent
a1efa8eebd
commit
bef0c6e7e3
19
css/ui.css
19
css/ui.css
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user