1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-11-30 21:52:07 +00:00

fixed null err msg during exception, basic: multiline if/else/then, MOD, SUB, CALL, newLocalScope()

This commit is contained in:
Steven Hugg 2020-08-24 11:51:47 -05:00
parent edb8a58aab
commit 13a4570745
9 changed files with 265 additions and 67 deletions

View File

@ -444,6 +444,12 @@ div.markdown th {
-webkit-background-clip: text; -webkit-background-clip: text;
color:rgba(0,0,0,0); color:rgba(0,0,0,0);
} }
.logo-gradient:hover {
text-shadow: 0px 0px 0.5em rgba(255,255,255,1);
text-decoration: underline;
text-decoration-color: #ddd;
text-decoration-thickness: 1px;
}
.disable-select { .disable-select {
user-select: none; user-select: none;
} }

View File

@ -635,12 +635,5 @@ $( ".dropdown-submenu" ).click(function(event) {
startUI(); startUI();
</script> </script>
<script>
/*
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (!isFirefox && platform_id != 'vcs') { $("#best_in_firefox").show(); }
*/
</script>
</body> </body>
</html> </html>

View File

@ -15,8 +15,9 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
var ERRORCLASS = 'error'; var ERRORCLASS = 'error';
function wordRegexp(words, crunch) { function wordRegexp(words, crunch) {
return new RegExp("^((" + words.join(")|(") + "))", "i"); // for token crunching
//return new RegExp("^((" + words.join(")|(") + "))\\b", "i"); //return new RegExp("^((" + words.join(")|(") + "))", "i");
return new RegExp("^((" + words.join(")|(") + "))\\b", "i");
} }
var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]"); var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]");
@ -26,16 +27,17 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"); var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
var openingKeywords = ['if','for']; var openingKeywords = ['if','for','while'];
var middleKeywords = ['to','then']; var middleKeywords = ['to','then','else'];
var endKeywords = ['next','end']; var endKeywords = ['next','end','wend'];
var operatorKeywords = ['and', 'or', 'not', 'xor', 'eqv', 'imp']; var operatorKeywords = ['and', 'or', 'not', 'xor', 'eqv', 'imp', 'mod'];
var wordOperators = wordRegexp(operatorKeywords); var wordOperators = wordRegexp(operatorKeywords);
var commonKeywords = [ var commonKeywords = [
'BASE','DATA','DEF','DIM', 'BASE','DATA','DEF','DIM',
'GO','GOSUB','GOTO','INPUT','LET','ON','OPTION','PRINT', 'GO','GOSUB','GOTO','INPUT','LET','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB' 'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB',
'CALL','CHANGE','CONVERT','CLEAR','DIALECT','SELECT','CASE'
]; ];
var commontypes = ['xxxxbyte','xxxxword']; var commontypes = ['xxxxbyte','xxxxword'];
@ -278,6 +280,6 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
return external; return external;
}); });
CodeMirror.defineMIME("text/x-vb", "vb"); CodeMirror.defineMIME("text/x-basic", "basic");
}); });

View File

@ -172,7 +172,9 @@ export class BreakpointList {
} }
} }
} }
export type Breakpoint = {cond:DebugCondition}; export interface Breakpoint {
cond: DebugCondition;
};
export interface EmuRecorder { export interface EmuRecorder {
frameRequested() : boolean; frameRequested() : boolean;

View File

@ -0,0 +1,80 @@
# BASIC Compiler Internals
If you want to know more about the internals of a BASIC compiler written in TypeScript, then read on.
## Tokenizer
The tokenizer is powered by one huge gnarly regular expression.
Each token type is a separate capture group, and we just look for the first one that matched.
Here's a sample of the regex:
~~~
comment identifier string
... (['].*) | ([A-Z_]\w*[$]?) | (".*?") ...
~~~
In some tokenizers, like Microsoft BASIC, each keyword supported by the language is matched individually,
so whitespace is not required around keywords.
For example, `FORI=ATOB` would be matched `FOR I = A TO B`.
This was sometimes called "crunching."
We have a special case in the tokenizer to enable this for these dialects.
The tokenizer also has special cases for `REM`, `DATA`, and `OPTION` which require tokens be untouched
-- and in the case of `DATA`, whitespace preserved.
Since BASIC is a line-oriented language, the tokenizer operates on one line at a time,
and each line is then fed to the parser.
For block-oriented languages, we'd probably want to tokenize the entire file before the parsing stage.
## Parser
The parser is a hand-coded recursive descent parser.
Why?
There was no `yacc` nor `bison` when BASIC was invented, so the language was not designed for these tools.
In fact, BASIC is a little gnarly when you get into the details, so having a bit of control is nice,
and error messages can be more informative.
Both clang and gcc use recursive descent parsers, so it can't be that bad, right?
The program is parsed one line at a time.
After line tokenization, the tokens go into an array.
We can consume tokens (remove from the list), peek at tokens (check the next token without removing), and pushback (return them to the list).
We don't have to check for `null`; we will always get the EOL (end-of-line) empty-string token if we run out.
Expressions are parsed with an [operator-precedence parser](https://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudocode), which isn't really that complicated.
We also infer types at this type (number or string).
We have a list of function signatures, and we know that "$" means a string variable, so we can check types.
The tricky part is that `FNA(X)` is a user-defined function, while `INT(X)` is a function, and `I1(X)` could be a dimensioned array.
Tokens carry their source code location with them, so we can assign a precise source code location to each statement.
This is used for error messages and for debugging.
The compiler generates an AST (Abstract Syntax Tree) and not any kind of VM bytecode.
The top-level of the AST is a list of statements, and an associated mapping of labels (line numbers) to statements.
AST nodes must refer to other nodes by index, not by reference, as the worker transfers it to the main thread using `JSON.stringify()`.
## Runtime
The runtime interprets the AST generated by the compiler.
It compiles each statement (PRINT, LET, etc.) into JavaScript.
The methods `expr2js()` converts expression trees to JavaScript, and `assign2js()` handles assignments like `LET`, `READ` and `INPUT`.
One statement is executed every time step.
There's a "program counter", which is the index of the next-to-run Statement node in the list.
Early BASICs were compiled languages, but the most popular BASICs for microcomputers were tokenized and interpreted.
There are subtle differences between the two.
For example, interpreted BASIC supports NEXT statements without a variable,
which always jump back to the most recent FOR even if you GOTO a different NEXT.
This requires the runtime maintain a stack of FOR loops.
Compiled BASIC dialects will verify loop structures at compile time.
For INPUT commands, the runtime calls the `input()` method, which returns a Promise.
The IDE overriddes this method to show a text field to the user, and resolve the Promise when data is entered.
The runtime might call multiple times until valid data is entered.
The compiler and runtime are each about [1300 lines of TypeScript](https://github.com/sehugg/8bitworkshop/tree/master/src/common/basic),
excluding the definitions of the BASIC dialects.
It's tested with a [test suite](https://github.com/sehugg/nbs-ecma55-test)
and with a [coverage-guided fuzzer](https://github.com/fuzzitdev/jsfuzz).

View File

@ -44,6 +44,7 @@ export interface BASICOptions {
restoreWithLabel : boolean; // RESTORE <label> restoreWithLabel : boolean; // RESTORE <label>
endStmtRequired : boolean; // need END at end? endStmtRequired : boolean; // need END at end?
// MISC // MISC
multilineIfThen? : boolean; // multi-line IF .. ELSE .. END IF?
commandsPerSec? : number; // how many commands per second? commandsPerSec? : number; // how many commands per second?
maxLinesPerFile? : number; // limit on # of lines maxLinesPerFile? : number; // limit on # of lines
maxArrayElements? : number; // max array elements (all dimensions) maxArrayElements? : number; // max array elements (all dimensions)
@ -196,6 +197,10 @@ export interface WEND_Statement extends ScopeEndStatement {
command: "WEND"; command: "WEND";
} }
export interface END_Statement extends ScopeEndStatement {
command: "END";
}
export interface INPUT_Statement extends Statement { export interface INPUT_Statement extends Statement {
command: "INPUT"; command: "INPUT";
prompt: Expr; prompt: Expr;
@ -230,6 +235,16 @@ export interface DEF_Statement extends Statement {
def: Expr; def: Expr;
} }
export interface SUB_Statement extends ScopeStartStatement {
command: "SUB";
lexpr: IndOp;
}
export interface CALL_Statement {
command: "CALL";
call: IndOp;
}
export interface OPTION_Statement extends Statement { export interface OPTION_Statement extends Statement {
command: "OPTION"; command: "OPTION";
optname: string; optname: string;
@ -263,7 +278,7 @@ export interface BASICProgram {
labels: { [label: string]: number }; // label -> PC labels: { [label: string]: number }; // label -> PC
} }
class Token { class Token implements SourceLocated {
str: string; str: string;
type: TokenType; type: TokenType;
$loc: SourceLocation; $loc: SourceLocation;
@ -292,6 +307,7 @@ const OPERATORS = {
'+': {f:'add',p:100}, '+': {f:'add',p:100},
'-': {f:'sub',p:100}, '-': {f:'sub',p:100},
'%': {f:'mod',p:140}, '%': {f:'mod',p:140},
'MOD': {f:'mod',p:140},
'\\': {f:'idiv',p:150}, '\\': {f:'idiv',p:150},
'*': {f:'mul',p:200}, '*': {f:'mul',p:200},
'/': {f:'div',p:200}, '/': {f:'div',p:200},
@ -364,6 +380,7 @@ export class BASICParser {
varrefs: { [name: string]: SourceLocation }; // variable references varrefs: { [name: string]: SourceLocation }; // variable references
fnrefs: { [name: string]: string[] }; // DEF FN call graph fnrefs: { [name: string]: string[] }; // DEF FN call graph
scopestack: number[]; scopestack: number[];
elseifcount: number;
path : string; path : string;
lineno : number; lineno : number;
@ -385,14 +402,16 @@ export class BASICParser {
this.varrefs = {}; this.varrefs = {};
this.fnrefs = {}; this.fnrefs = {};
this.scopestack = []; this.scopestack = [];
this.elseifcount = 0;
} }
addError(msg: string, loc?: SourceLocation) { addError(msg: string, loc?: SourceLocation) {
var tok = this.lasttoken || this.peekToken(); var tok = this.lasttoken || this.peekToken();
if (!loc) loc = tok.$loc; if (!loc) loc = tok.$loc;
this.errors.push({path:loc.path, line:loc.line, label:this.curlabel, start:loc.start, end:loc.end, msg:msg}); this.errors.push({path:loc.path, line:loc.line, label:this.curlabel, start:loc.start, end:loc.end, msg:msg});
} }
compileError(msg: string, loc?: SourceLocation) { compileError(msg: string, loc?: SourceLocation, loc2?: SourceLocation) {
this.addError(msg, loc); this.addError(msg, loc);
//if (loc2 != null) this.addError(`...`, loc2);
throw new CompileError(msg, loc); throw new CompileError(msg, loc);
} }
dialectError(what: string, loc?: SourceLocation) { dialectError(what: string, loc?: SourceLocation) {
@ -482,9 +501,9 @@ export class BASICParser {
// add to list // add to list
this.stmts.push(stmt); this.stmts.push(stmt);
} }
addLabel(str: string) { addLabel(str: string, offset?: number) {
if (this.labels[str] != null) this.compileError(`There's a duplicated label named "${str}".`); if (this.labels[str] != null) this.compileError(`There's a duplicated label named "${str}".`);
this.labels[str] = this.getPC(); this.labels[str] = this.getPC() + (offset || 0);
this.curlabel = str; this.curlabel = str;
this.tokens.forEach((tok) => tok.$loc.label = str); this.tokens.forEach((tok) => tok.$loc.label = str);
} }
@ -654,7 +673,7 @@ export class BASICParser {
modifyScope(stmt: Statement) { modifyScope(stmt: Statement) {
if (this.opts.compiledBlocks) { if (this.opts.compiledBlocks) {
var cmd = stmt.command; var cmd = stmt.command;
if (cmd == 'FOR' || cmd == 'WHILE') { if (cmd == 'FOR' || cmd == 'WHILE' || cmd == 'SUB') {
this.scopestack.push(this.getPC()); // has to be before adding statment to list this.scopestack.push(this.getPC()); // has to be before adding statment to list
} else if (cmd == 'NEXT') { } else if (cmd == 'NEXT') {
this.popScope(stmt as NEXT_Statement, 'FOR'); this.popScope(stmt as NEXT_Statement, 'FOR');
@ -663,20 +682,34 @@ export class BASICParser {
} }
} }
} }
popScope(close: WEND_Statement|NEXT_Statement, open: string) { popScope(close: WEND_Statement|NEXT_Statement|END_Statement, open: string) {
var popidx = this.scopestack.pop(); var popidx = this.scopestack.pop();
var popstmt : ScopeStartStatement = popidx != null ? this.stmts[popidx] : null; var popstmt : ScopeStartStatement = popidx != null ? this.stmts[popidx] : null;
if (popstmt == null) if (popstmt == null)
this.compileError(`There's a ${close.command} without a matching ${open}.`, close.$loc); this.compileError(`There's a ${close.command} without a matching ${open}.`, close.$loc);
else if (popstmt.command != open) else if (popstmt.command != open)
this.compileError(`There's a ${close.command} paired with ${popstmt.command}, but it should be paired with ${open}.`, close.$loc); this.compileError(`There's a ${close.command} paired with ${popstmt.command}, but it should be paired with ${open}.`, close.$loc, popstmt.$loc);
else if (close.command == 'NEXT' && !this.opts.optionalNextVar else if (close.command == 'NEXT' && !this.opts.optionalNextVar
&& close.lexpr.name != (popstmt as FOR_Statement).lexpr.name) && close.lexpr.name != (popstmt as FOR_Statement).lexpr.name)
this.compileError(`This NEXT statement is matched with the wrong FOR variable (${close.lexpr.name}).`, close.$loc); this.compileError(`This NEXT statement is matched with the wrong FOR variable (${close.lexpr.name}).`, close.$loc, popstmt.$loc);
// set start + end locations // set start + end locations
close.startpc = popidx; close.startpc = popidx;
popstmt.endpc = this.getPC(); // has to be before adding statment to list popstmt.endpc = this.getPC(); // has to be before adding statment to list
} }
popIfThenScope(nextpc?: number) {
var popidx = this.scopestack.pop();
var popstmt : ScopeStartStatement = popidx != null ? this.stmts[popidx] : null;
if (popstmt == null)
this.compileError(`There's an END IF without a matching IF or ELSE.`);
if (popstmt.command == 'ELSE') {
popstmt.endpc = this.getPC();
this.popIfThenScope(popidx + 1); // IF goes to ELSE+1
} else if (popstmt.command == 'IF') {
popstmt.endpc = nextpc != null ? nextpc : this.getPC();
} else {
this.compileError(`There's an END IF paired with a ${popstmt.command}, not IF or ELSE.`, this.lasttoken.$loc, popstmt.$loc);
}
}
parseVarSubscriptOrFunc(): IndOp { parseVarSubscriptOrFunc(): IndOp {
var tok = this.consumeToken(); var tok = this.consumeToken();
switch (tok.type) { switch (tok.type) {
@ -1003,6 +1036,10 @@ export class BASICParser {
// we accept GOTO or THEN if line number provided (DEC accepts GO TO) // we accept GOTO or THEN if line number provided (DEC accepts GO TO)
var thengoto = this.expectTokens(['THEN','GOTO','GO']); var thengoto = this.expectTokens(['THEN','GOTO','GO']);
if (thengoto.str == 'GO') this.expectToken('TO'); if (thengoto.str == 'GO') this.expectToken('TO');
// multiline IF .. THEN? push it to scope stack
if (this.opts.multilineIfThen && isEOS(this.peekToken())) {
this.scopestack.push(this.getPC() - 1); // we already added stmt to list, so - 1
} else {
// parse line number or statement clause // parse line number or statement clause
this.parseGotoOrStatements(); this.parseGotoOrStatements();
// is the next statement an ELSE? // is the next statement an ELSE?
@ -1015,13 +1052,24 @@ export class BASICParser {
ifstmt.endpc = this.getPC(); ifstmt.endpc = this.getPC();
} }
} }
}
stmt__ELSE(): void { stmt__ELSE(): void {
var elsestmt : ELSE_Statement = { command: "ELSE" }; var elsestmt : ELSE_Statement = { command: "ELSE" };
this.addStatement(elsestmt, this.lasttoken); this.addStatement(elsestmt, this.lasttoken);
// multiline ELSE? or ELSE IF?
var nexttok = this.peekToken();
if (this.opts.multilineIfThen && isEOS(nexttok)) {
this.scopestack.push(this.getPC() - 1); // we already added stmt to list, so - 1
} else if (this.opts.multilineIfThen && nexttok.str == 'IF') {
this.scopestack.push(this.getPC() - 1); // we already added stmt to list, so - 1
this.parseGotoOrStatements();
this.elseifcount++;
} else {
// parse line number or statement clause // parse line number or statement clause
this.parseGotoOrStatements(); this.parseGotoOrStatements();
elsestmt.endpc = this.getPC(); elsestmt.endpc = this.getPC();
} }
}
parseGotoOrStatements() { parseGotoOrStatements() {
var lineno = this.peekToken(); var lineno = this.peekToken();
// assume GOTO if number given after THEN // assume GOTO if number given after THEN
@ -1123,8 +1171,20 @@ export class BASICParser {
return { command:'STOP' }; return { command:'STOP' };
} }
stmt__END() { stmt__END() {
if (this.opts.multilineIfThen && this.scopestack.length) {
let endtok = this.expectTokens(['IF','SUB']);
if (endtok.str == 'IF') {
this.popIfThenScope();
while (this.elseifcount--) this.popIfThenScope(); // pop additional ELSE IF blocks?
this.elseifcount = 0;
} else if (endtok.str == 'SUB') {
this.addStatement( { command: 'RETURN' }, endtok );
this.popScope( { command: 'END' }, 'SUB'); // fake command to avoid null
}
} else {
return { command:'END' }; return { command:'END' };
} }
}
stmt__ON() : ONGO_Statement { stmt__ON() : ONGO_Statement {
var expr = this.parseExprWithType("number"); var expr = this.parseExprWithType("number");
var gotok = this.consumeToken(); var gotok = this.consumeToken();
@ -1134,16 +1194,11 @@ export class BASICParser {
return { command:cmd, expr:expr, labels:labels }; return { command:cmd, expr:expr, labels:labels };
} }
stmt__DEF() : DEF_Statement { stmt__DEF() : DEF_Statement {
var lexpr = this.parseVarSubscriptOrFunc(); var lexpr = this.parseVarSubscriptOrFunc(); // TODO: only allow parameter names, not exprs
if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs) if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs)
this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`, lexpr.$loc); this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`, lexpr.$loc);
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`, lexpr.$loc) if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`, lexpr.$loc)
// local variables need to be marked as referenced (TODO: only for this scope) this.markVarDefs(lexpr); // local variables need to be marked as referenced (TODO: only for this scope)
this.vardefs[lexpr.name] = lexpr;
if (lexpr.args != null)
for (let arg of lexpr.args)
if (isLookup(arg))
this.vardefs[arg.name] = arg;
this.expectToken("="); this.expectToken("=");
var func = this.parseExpr(); var func = this.parseExpr();
// build call graph to detect cycles // build call graph to detect cycles
@ -1157,6 +1212,25 @@ export class BASICParser {
this.checkCallGraph(lexpr.name, new Set()); this.checkCallGraph(lexpr.name, new Set());
return { command:'DEF', lexpr:lexpr, def:func }; return { command:'DEF', lexpr:lexpr, def:func };
} }
stmt__SUB() : SUB_Statement {
var lexpr = this.parseVarSubscriptOrFunc(); // TODO: only allow parameter names, not exprs
this.markVarDefs(lexpr); // local variables need to be marked as referenced (TODO: only for this scope)
this.addLabel(lexpr.name, 1); // offset +1 to skip SUB command
return { command:'SUB', lexpr:lexpr };
}
stmt__CALL() : CALL_Statement {
return { command:'CALL', call:this.parseVarSubscriptOrFunc() };
}
markVarDefs(lexpr: IndOp) {
this.vardefs[lexpr.name] = lexpr;
if (lexpr.args != null)
for (let arg of lexpr.args) {
if (isLookup(arg) && arg.args == null)
this.vardefs[arg.name] = arg;
else
this.compileError(`A definition can only define symbols, not expressions.`);
}
}
// detect cycles in call graph starting at function 'name' // detect cycles in call graph starting at function 'name'
checkCallGraph(name: string, visited: Set<string>) { checkCallGraph(name: string, visited: Set<string>) {
if (visited.has(name)) this.compileError(`There was a cycle in the function definition graph for ${name}.`); if (visited.has(name)) this.compileError(`There was a cycle in the function definition graph for ${name}.`);
@ -1275,7 +1349,7 @@ export class BASICParser {
checkScopes() { checkScopes() {
if (this.opts.compiledBlocks && this.scopestack.length) { if (this.opts.compiledBlocks && this.scopestack.length) {
var open = this.stmts[this.scopestack.pop()]; var open = this.stmts[this.scopestack.pop()];
var close = {FOR:"NEXT", WHILE:"WEND", IF:"ENDIF"}; var close = {FOR:"NEXT", WHILE:"WEND", IF:"END IF", SUB:"END SUB"};
this.compileError(`Don't forget to add a matching ${close[open.command]} statement.`, open.$loc); this.compileError(`Don't forget to add a matching ${close[open.command]} statement.`, open.$loc);
} }
} }
@ -1313,7 +1387,7 @@ export const ECMA55_MINIMAL : BASICOptions = {
validKeywords : [ validKeywords : [
'BASE','DATA','DEF','DIM','END', 'BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT', 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO' 'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','THEN','TO' // 'SUB'
], ],
validFunctions : [ validFunctions : [
'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN' 'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'
@ -1361,7 +1435,7 @@ export const DARTMOUTH_4TH_EDITION : BASICOptions = {
validKeywords : [ validKeywords : [
'BASE','DATA','DEF','DIM','END', 'BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT', 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO', 'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','THEN','TO', //'SUB',
'CHANGE','MAT','RANDOM','RESTORE$','RESTORE*', 'CHANGE','MAT','RANDOM','RESTORE$','RESTORE*',
], ],
validFunctions : [ validFunctions : [
@ -1458,7 +1532,7 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
validKeywords : [ validKeywords : [
'BASE','DATA','DEF','DIM','END', 'BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','OPTION','PRINT', 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','OPTION','PRINT',
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO', 'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','THEN','TO', //'SUB',
'ENTER','MAT','CONVERT','OF','IMAGE','USING' 'ENTER','MAT','CONVERT','OF','IMAGE','USING'
], ],
validFunctions : [ validFunctions : [
@ -1627,7 +1701,7 @@ export const BASICODE : BASICOptions = {
validKeywords : [ validKeywords : [
'BASE','DATA','DEF','DIM','END', 'BASE','DATA','DEF','DIM','END',
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT', 'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
'READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO', 'READ','REM','RESTORE','RETURN','STEP','STOP','THEN','TO', // 'SUB',
'AND', 'NOT', 'OR' 'AND', 'NOT', 'OR'
], ],
validFunctions : [ validFunctions : [
@ -1682,7 +1756,8 @@ export const ALTAIR_BASIC41 : BASICOptions = {
'READ','REM','RESTORE','RESUME','RETURN','STOP','SWAP', 'READ','REM','RESTORE','RESUME','RETURN','STOP','SWAP',
'TROFF','TRON','WAIT', 'TROFF','TRON','WAIT',
'TO','STEP', 'TO','STEP',
'AND', 'NOT', 'OR', 'XOR', 'IMP', 'EQV', 'MOD' 'AND', 'NOT', 'OR', 'XOR', 'IMP', 'EQV', 'MOD',
'RANDOMIZE' // not in Altair BASIC, but we add it anyway
], ],
validFunctions : [ validFunctions : [
'ABS','ASC','ATN','CDBL','CHR$','CINT','COS','ERL','ERR', 'ABS','ASC','ATN','CDBL','CHR$','CINT','COS','ERL','ERR',
@ -1870,6 +1945,7 @@ export const MODERN_BASIC : BASICOptions = {
chainAssignments : true, chainAssignments : true,
optionalLet : true, optionalLet : true,
compiledBlocks : true, compiledBlocks : true,
multilineIfThen : true,
} }
// TODO: integer vars // TODO: integer vars

View File

@ -88,9 +88,11 @@ export class BASICRuntime {
curpc : number; curpc : number;
dataptr : number; dataptr : number;
vars : {}; vars : {[name:string] : any}; // actually Value, but += doesn't work
globals : {[name:string] : any};
arrays : {}; arrays : {};
defs : {}; defs : {};
subroutines : {};
forLoops : { [varname:string] : { $next:(name:string) => void } }; forLoops : { [varname:string] : { $next:(name:string) => void } };
forLoopStack: string[]; forLoopStack: string[];
whileLoops : number[]; whileLoops : number[];
@ -123,6 +125,7 @@ export class BASICRuntime {
this.label2dataptr = {}; this.label2dataptr = {};
this.pc2label = new Map(); this.pc2label = new Map();
this.datums = []; this.datums = [];
this.subroutines = {};
this.builtins = this.getBuiltinFunctions(); this.builtins = this.getBuiltinFunctions();
// TODO: detect undeclared vars // TODO: detect undeclared vars
// build PC -> label lookup // build PC -> label lookup
@ -130,18 +133,19 @@ export class BASICRuntime {
var targetpc = program.labels[label]; var targetpc = program.labels[label];
this.pc2label.set(targetpc, label); this.pc2label.set(targetpc, label);
} }
// compile statements ahead of time // iterate through all the statements
this.allstmts.forEach((stmt, pc) => { this.allstmts.forEach((stmt, pc) => {
// compile statements ahead of time
this.curpc = pc + 1; // for error reporting this.curpc = pc + 1; // for error reporting
this.compileStatement(stmt); this.compileStatement(stmt);
});
// parse DATA literals // parse DATA literals
this.allstmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => { if (stmt.command == 'DATA') {
this.label2dataptr[datastmt.$loc.label] = this.datums.length; this.label2dataptr[stmt.$loc.label] = this.datums.length;
(datastmt as basic.DATA_Statement).datums.forEach(datum => { (stmt as basic.DATA_Statement).datums.forEach(datum => {
this.curpc = datastmt.$loc.offset; // for error reporting this.curpc = stmt.$loc.offset; // for error reporting
this.datums.push(datum); this.datums.push(datum);
}); });
}
}); });
// try to resume where we left off after loading // try to resume where we left off after loading
if (this.label2pc[prevlabel] != null) { if (this.label2pc[prevlabel] != null) {
@ -163,7 +167,7 @@ export class BASICRuntime {
this.exited = false; this.exited = false;
} }
clearVars() { clearVars() {
this.vars = {}; this.globals = this.vars = {};
this.arrays = {}; this.arrays = {};
this.defs = {}; // TODO? only in interpreters this.defs = {}; // TODO? only in interpreters
this.forLoops = {}; this.forLoops = {};
@ -342,6 +346,15 @@ export class BASICRuntime {
} }
} }
newLocalScope() {
this.vars = Object.create(this.vars);
}
popLocalScope() {
if (this.vars !== this.globals)
this.vars = Object.getPrototypeOf(this.vars);
}
gosubLabel(label) { gosubLabel(label) {
if (this.returnStack.length > 32767) // TODO: const? if (this.returnStack.length > 32767) // TODO: const?
this.runtimeError(`I did too many GOSUBs without a RETURN.`) this.runtimeError(`I did too many GOSUBs without a RETURN.`)
@ -354,6 +367,7 @@ export class BASICRuntime {
this.runtimeError("I tried to RETURN, but there wasn't a corresponding GOSUB."); // RETURN BEFORE GOSUB this.runtimeError("I tried to RETURN, but there wasn't a corresponding GOSUB."); // RETURN BEFORE GOSUB
var pc = this.returnStack.pop(); var pc = this.returnStack.pop();
this.curpc = pc; this.curpc = pc;
this.popLocalScope();
} }
popReturnStack() { popReturnStack() {
@ -477,7 +491,7 @@ export class BASICRuntime {
s += this.array2js(expr, opts); s += this.array2js(expr, opts);
} }
} else { // just a variable } else { // just a variable
s = `this.vars.${expr.name}`; s = `this.globals.${expr.name}`;
} }
return s; return s;
} }
@ -769,7 +783,7 @@ export class BASICRuntime {
for (var lexpr of stmt.lexprs) { for (var lexpr of stmt.lexprs) {
// HP BASIC string-slice syntax? // HP BASIC string-slice syntax?
if (this.opts.arraysContainChars && lexpr.args && lexpr.name.endsWith('$')) { if (this.opts.arraysContainChars && lexpr.args && lexpr.name.endsWith('$')) {
s += `this.vars.${lexpr.name} = this.modifyStringSlice(this.vars.${lexpr.name}, _right, ` s += `this.globals.${lexpr.name} = this.modifyStringSlice(this.vars.${lexpr.name}, _right, `
s += lexpr.args.map((arg) => this.expr2js(arg)).join(', '); s += lexpr.args.map((arg) => this.expr2js(arg)).join(', ');
s += ');'; s += ');';
} else { } else {
@ -974,6 +988,30 @@ export class BASICRuntime {
} }
} }
do__SUB(stmt: basic.SUB_Statement) {
this.subroutines[stmt.lexpr.name] = stmt;
// skip the SUB definition
return `this.curpc = ${stmt.endpc}`
}
do__CALL(stmt: basic.CALL_Statement) {
var substmt : basic.SUB_Statement = this.subroutines[stmt.call.name];
if (substmt == null)
this.runtimeError(`I can't find a subroutine named "${stmt.call.name}".`);
var subargs = substmt.lexpr.args || [];
var callargs = stmt.call.args || [];
if (subargs.length != callargs.length)
this.runtimeError(`I tried to call ${stmt.call.name} with the wrong number of parameters.`);
var s = '';
s += `this.gosubLabel(${JSON.stringify(stmt.call.name)});`
s += `this.newLocalScope();`
for (var i=0; i<subargs.length; i++) {
var arg = subargs[i] as basic.IndOp;
s += `this.vars.${arg.name} = ${this.expr2js(callargs[i])};`
}
return s;
}
// TODO: ONERR, ON ERROR GOTO // TODO: ONERR, ON ERROR GOTO
// TODO: memory quota // TODO: memory quota
// TODO: useless loop (! 4th edition) // TODO: useless loop (! 4th edition)

View File

@ -1916,9 +1916,11 @@ function globalErrorHandler(msgevent) {
requestPersistPermission(false, false); requestPersistPermission(false, false);
} else { } else {
var err = msgevent.error || msgevent.reason; var err = msgevent.error || msgevent.reason;
msg = err.message || msg; if (err != null && err instanceof EmuHalt) {
msg = (err && err.message) || msg;
showExceptionAsError(err, msg); showExceptionAsError(err, msg);
} }
}
} }
// catch errors // catch errors

View File

@ -124,6 +124,7 @@ class BASICPlatform implements Platform {
resize: () => void; resize: () => void;
loadROM(title, data) { loadROM(title, data) {
// TODO: disable hot reload if error
// TODO: only hot reload when we hit a label? // TODO: only hot reload when we hit a label?
var didExit = this.runtime.exited; var didExit = this.runtime.exited;
this.program = data; this.program = data;
@ -131,8 +132,6 @@ class BASICPlatform implements Platform {
this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO? this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO?
// map editor to uppercase-only if need be // map editor to uppercase-only if need be
views.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null; views.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null;
// HP 2000 has cute lil small caps (TODO: messes up grid alignment tho)
//this.tty.page.style.fontVariant = (this.program.opts.dialectName == 'HP2000') ? 'small-caps' : 'normal';
// only reset if we exited, or couldn't restart at label (PC reset to 0) // only reset if we exited, or couldn't restart at label (PC reset to 0)
if (!this.hotReload || didExit || !resumePC) if (!this.hotReload || didExit || !resumePC)
this.reset(); this.reset();