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:
parent
edb8a58aab
commit
13a4570745
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
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>
|
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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user