mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-11-24 12:31:25 +00:00
fixed null err msg during exception, basic: multiline if/else/then, MOD, SUB, CALL, newLocalScope()
This commit is contained in:
parent
edb8a58aab
commit
13a4570745
@ -444,6 +444,12 @@ div.markdown th {
|
||||
-webkit-background-clip: text;
|
||||
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 {
|
||||
user-select: none;
|
||||
}
|
||||
|
@ -635,12 +635,5 @@ $( ".dropdown-submenu" ).click(function(event) {
|
||||
startUI();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
/*
|
||||
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
if (!isFirefox && platform_id != 'vcs') { $("#best_in_firefox").show(); }
|
||||
*/
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -15,8 +15,9 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
|
||||
var ERRORCLASS = 'error';
|
||||
|
||||
function wordRegexp(words, crunch) {
|
||||
return new RegExp("^((" + words.join(")|(") + "))", "i");
|
||||
//return new RegExp("^((" + words.join(")|(") + "))\\b", "i");
|
||||
// for token crunching
|
||||
//return new RegExp("^((" + words.join(")|(") + "))", "i");
|
||||
return new RegExp("^((" + words.join(")|(") + "))\\b", "i");
|
||||
}
|
||||
|
||||
var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]");
|
||||
@ -26,16 +27,17 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
|
||||
var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
|
||||
var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
|
||||
|
||||
var openingKeywords = ['if','for'];
|
||||
var middleKeywords = ['to','then'];
|
||||
var endKeywords = ['next','end'];
|
||||
var openingKeywords = ['if','for','while'];
|
||||
var middleKeywords = ['to','then','else'];
|
||||
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 commonKeywords = [
|
||||
'BASE','DATA','DEF','DIM',
|
||||
'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'];
|
||||
|
||||
@ -278,6 +280,6 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
|
||||
return external;
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-vb", "vb");
|
||||
CodeMirror.defineMIME("text/x-basic", "basic");
|
||||
|
||||
});
|
||||
|
@ -172,7 +172,9 @@ export class BreakpointList {
|
||||
}
|
||||
}
|
||||
}
|
||||
export type Breakpoint = {cond:DebugCondition};
|
||||
export interface Breakpoint {
|
||||
cond: DebugCondition;
|
||||
};
|
||||
|
||||
export interface EmuRecorder {
|
||||
frameRequested() : boolean;
|
||||
|
80
src/common/basic/README.md
Normal file
80
src/common/basic/README.md
Normal 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).
|
@ -44,6 +44,7 @@ export interface BASICOptions {
|
||||
restoreWithLabel : boolean; // RESTORE <label>
|
||||
endStmtRequired : boolean; // need END at end?
|
||||
// MISC
|
||||
multilineIfThen? : boolean; // multi-line IF .. ELSE .. END IF?
|
||||
commandsPerSec? : number; // how many commands per second?
|
||||
maxLinesPerFile? : number; // limit on # of lines
|
||||
maxArrayElements? : number; // max array elements (all dimensions)
|
||||
@ -196,6 +197,10 @@ export interface WEND_Statement extends ScopeEndStatement {
|
||||
command: "WEND";
|
||||
}
|
||||
|
||||
export interface END_Statement extends ScopeEndStatement {
|
||||
command: "END";
|
||||
}
|
||||
|
||||
export interface INPUT_Statement extends Statement {
|
||||
command: "INPUT";
|
||||
prompt: Expr;
|
||||
@ -230,6 +235,16 @@ export interface DEF_Statement extends Statement {
|
||||
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 {
|
||||
command: "OPTION";
|
||||
optname: string;
|
||||
@ -263,7 +278,7 @@ export interface BASICProgram {
|
||||
labels: { [label: string]: number }; // label -> PC
|
||||
}
|
||||
|
||||
class Token {
|
||||
class Token implements SourceLocated {
|
||||
str: string;
|
||||
type: TokenType;
|
||||
$loc: SourceLocation;
|
||||
@ -292,6 +307,7 @@ const OPERATORS = {
|
||||
'+': {f:'add',p:100},
|
||||
'-': {f:'sub',p:100},
|
||||
'%': {f:'mod',p:140},
|
||||
'MOD': {f:'mod',p:140},
|
||||
'\\': {f:'idiv',p:150},
|
||||
'*': {f:'mul',p:200},
|
||||
'/': {f:'div',p:200},
|
||||
@ -364,6 +380,7 @@ export class BASICParser {
|
||||
varrefs: { [name: string]: SourceLocation }; // variable references
|
||||
fnrefs: { [name: string]: string[] }; // DEF FN call graph
|
||||
scopestack: number[];
|
||||
elseifcount: number;
|
||||
|
||||
path : string;
|
||||
lineno : number;
|
||||
@ -385,14 +402,16 @@ export class BASICParser {
|
||||
this.varrefs = {};
|
||||
this.fnrefs = {};
|
||||
this.scopestack = [];
|
||||
this.elseifcount = 0;
|
||||
}
|
||||
addError(msg: string, loc?: SourceLocation) {
|
||||
var tok = this.lasttoken || this.peekToken();
|
||||
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});
|
||||
}
|
||||
compileError(msg: string, loc?: SourceLocation) {
|
||||
compileError(msg: string, loc?: SourceLocation, loc2?: SourceLocation) {
|
||||
this.addError(msg, loc);
|
||||
//if (loc2 != null) this.addError(`...`, loc2);
|
||||
throw new CompileError(msg, loc);
|
||||
}
|
||||
dialectError(what: string, loc?: SourceLocation) {
|
||||
@ -482,9 +501,9 @@ export class BASICParser {
|
||||
// add to list
|
||||
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}".`);
|
||||
this.labels[str] = this.getPC();
|
||||
this.labels[str] = this.getPC() + (offset || 0);
|
||||
this.curlabel = str;
|
||||
this.tokens.forEach((tok) => tok.$loc.label = str);
|
||||
}
|
||||
@ -654,7 +673,7 @@ export class BASICParser {
|
||||
modifyScope(stmt: Statement) {
|
||||
if (this.opts.compiledBlocks) {
|
||||
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
|
||||
} else if (cmd == 'NEXT') {
|
||||
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 popstmt : ScopeStartStatement = popidx != null ? this.stmts[popidx] : null;
|
||||
if (popstmt == null)
|
||||
this.compileError(`There's a ${close.command} without a matching ${open}.`, close.$loc);
|
||||
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
|
||||
&& 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
|
||||
close.startpc = popidx;
|
||||
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 {
|
||||
var tok = this.consumeToken();
|
||||
switch (tok.type) {
|
||||
@ -1003,6 +1036,10 @@ export class BASICParser {
|
||||
// we accept GOTO or THEN if line number provided (DEC accepts GO TO)
|
||||
var thengoto = this.expectTokens(['THEN','GOTO','GO']);
|
||||
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
|
||||
this.parseGotoOrStatements();
|
||||
// is the next statement an ELSE?
|
||||
@ -1015,13 +1052,24 @@ export class BASICParser {
|
||||
ifstmt.endpc = this.getPC();
|
||||
}
|
||||
}
|
||||
}
|
||||
stmt__ELSE(): void {
|
||||
var elsestmt : ELSE_Statement = { command: "ELSE" };
|
||||
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
|
||||
this.parseGotoOrStatements();
|
||||
elsestmt.endpc = this.getPC();
|
||||
}
|
||||
}
|
||||
parseGotoOrStatements() {
|
||||
var lineno = this.peekToken();
|
||||
// assume GOTO if number given after THEN
|
||||
@ -1123,8 +1171,20 @@ export class BASICParser {
|
||||
return { command:'STOP' };
|
||||
}
|
||||
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' };
|
||||
}
|
||||
}
|
||||
stmt__ON() : ONGO_Statement {
|
||||
var expr = this.parseExprWithType("number");
|
||||
var gotok = this.consumeToken();
|
||||
@ -1134,16 +1194,11 @@ export class BASICParser {
|
||||
return { command:cmd, expr:expr, labels:labels };
|
||||
}
|
||||
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)
|
||||
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)
|
||||
// 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.markVarDefs(lexpr); // local variables need to be marked as referenced (TODO: only for this scope)
|
||||
this.expectToken("=");
|
||||
var func = this.parseExpr();
|
||||
// build call graph to detect cycles
|
||||
@ -1157,6 +1212,25 @@ export class BASICParser {
|
||||
this.checkCallGraph(lexpr.name, new Set());
|
||||
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'
|
||||
checkCallGraph(name: string, visited: Set<string>) {
|
||||
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() {
|
||||
if (this.opts.compiledBlocks && this.scopestack.length) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1313,7 +1387,7 @@ export const ECMA55_MINIMAL : BASICOptions = {
|
||||
validKeywords : [
|
||||
'BASE','DATA','DEF','DIM','END',
|
||||
'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 : [
|
||||
'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'
|
||||
@ -1361,7 +1435,7 @@ export const DARTMOUTH_4TH_EDITION : BASICOptions = {
|
||||
validKeywords : [
|
||||
'BASE','DATA','DEF','DIM','END',
|
||||
'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*',
|
||||
],
|
||||
validFunctions : [
|
||||
@ -1458,7 +1532,7 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
|
||||
validKeywords : [
|
||||
'BASE','DATA','DEF','DIM','END',
|
||||
'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'
|
||||
],
|
||||
validFunctions : [
|
||||
@ -1627,7 +1701,7 @@ export const BASICODE : BASICOptions = {
|
||||
validKeywords : [
|
||||
'BASE','DATA','DEF','DIM','END',
|
||||
'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'
|
||||
],
|
||||
validFunctions : [
|
||||
@ -1682,7 +1756,8 @@ export const ALTAIR_BASIC41 : BASICOptions = {
|
||||
'READ','REM','RESTORE','RESUME','RETURN','STOP','SWAP',
|
||||
'TROFF','TRON','WAIT',
|
||||
'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 : [
|
||||
'ABS','ASC','ATN','CDBL','CHR$','CINT','COS','ERL','ERR',
|
||||
@ -1870,6 +1945,7 @@ export const MODERN_BASIC : BASICOptions = {
|
||||
chainAssignments : true,
|
||||
optionalLet : true,
|
||||
compiledBlocks : true,
|
||||
multilineIfThen : true,
|
||||
}
|
||||
|
||||
// TODO: integer vars
|
||||
|
@ -88,9 +88,11 @@ export class BASICRuntime {
|
||||
|
||||
curpc : number;
|
||||
dataptr : number;
|
||||
vars : {};
|
||||
vars : {[name:string] : any}; // actually Value, but += doesn't work
|
||||
globals : {[name:string] : any};
|
||||
arrays : {};
|
||||
defs : {};
|
||||
subroutines : {};
|
||||
forLoops : { [varname:string] : { $next:(name:string) => void } };
|
||||
forLoopStack: string[];
|
||||
whileLoops : number[];
|
||||
@ -123,6 +125,7 @@ export class BASICRuntime {
|
||||
this.label2dataptr = {};
|
||||
this.pc2label = new Map();
|
||||
this.datums = [];
|
||||
this.subroutines = {};
|
||||
this.builtins = this.getBuiltinFunctions();
|
||||
// TODO: detect undeclared vars
|
||||
// build PC -> label lookup
|
||||
@ -130,18 +133,19 @@ export class BASICRuntime {
|
||||
var targetpc = program.labels[label];
|
||||
this.pc2label.set(targetpc, label);
|
||||
}
|
||||
// compile statements ahead of time
|
||||
// iterate through all the statements
|
||||
this.allstmts.forEach((stmt, pc) => {
|
||||
// compile statements ahead of time
|
||||
this.curpc = pc + 1; // for error reporting
|
||||
this.compileStatement(stmt);
|
||||
});
|
||||
// parse DATA literals
|
||||
this.allstmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => {
|
||||
this.label2dataptr[datastmt.$loc.label] = this.datums.length;
|
||||
(datastmt as basic.DATA_Statement).datums.forEach(datum => {
|
||||
this.curpc = datastmt.$loc.offset; // for error reporting
|
||||
if (stmt.command == 'DATA') {
|
||||
this.label2dataptr[stmt.$loc.label] = this.datums.length;
|
||||
(stmt as basic.DATA_Statement).datums.forEach(datum => {
|
||||
this.curpc = stmt.$loc.offset; // for error reporting
|
||||
this.datums.push(datum);
|
||||
});
|
||||
}
|
||||
});
|
||||
// try to resume where we left off after loading
|
||||
if (this.label2pc[prevlabel] != null) {
|
||||
@ -163,7 +167,7 @@ export class BASICRuntime {
|
||||
this.exited = false;
|
||||
}
|
||||
clearVars() {
|
||||
this.vars = {};
|
||||
this.globals = this.vars = {};
|
||||
this.arrays = {};
|
||||
this.defs = {}; // TODO? only in interpreters
|
||||
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) {
|
||||
if (this.returnStack.length > 32767) // TODO: const?
|
||||
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
|
||||
var pc = this.returnStack.pop();
|
||||
this.curpc = pc;
|
||||
this.popLocalScope();
|
||||
}
|
||||
|
||||
popReturnStack() {
|
||||
@ -477,7 +491,7 @@ export class BASICRuntime {
|
||||
s += this.array2js(expr, opts);
|
||||
}
|
||||
} else { // just a variable
|
||||
s = `this.vars.${expr.name}`;
|
||||
s = `this.globals.${expr.name}`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
@ -769,7 +783,7 @@ export class BASICRuntime {
|
||||
for (var lexpr of stmt.lexprs) {
|
||||
// HP BASIC string-slice syntax?
|
||||
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 += ');';
|
||||
} 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: memory quota
|
||||
// TODO: useless loop (! 4th edition)
|
||||
|
@ -1916,10 +1916,12 @@ function globalErrorHandler(msgevent) {
|
||||
requestPersistPermission(false, false);
|
||||
} else {
|
||||
var err = msgevent.error || msgevent.reason;
|
||||
msg = err.message || msg;
|
||||
if (err != null && err instanceof EmuHalt) {
|
||||
msg = (err && err.message) || msg;
|
||||
showExceptionAsError(err, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// catch errors
|
||||
function installErrorHandler() {
|
||||
|
@ -124,6 +124,7 @@ class BASICPlatform implements Platform {
|
||||
resize: () => void;
|
||||
|
||||
loadROM(title, data) {
|
||||
// TODO: disable hot reload if error
|
||||
// TODO: only hot reload when we hit a label?
|
||||
var didExit = this.runtime.exited;
|
||||
this.program = data;
|
||||
@ -131,8 +132,6 @@ class BASICPlatform implements Platform {
|
||||
this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO?
|
||||
// map editor to uppercase-only if need be
|
||||
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)
|
||||
if (!this.hotReload || didExit || !resumePC)
|
||||
this.reset();
|
||||
|
Loading…
Reference in New Issue
Block a user