mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2025-01-10 16:29:48 +00:00
basic: added operators, check inputs, better line feed, fixed return, FOR stack, moved OPTION to compile-time, fixed DEFs, uppercase, hot-loading (43, 45, 36)
This commit is contained in:
parent
3a4b39b01c
commit
14524726e6
@ -510,3 +510,19 @@ in devices:
|
|||||||
|
|
||||||
Should call trap() every cycle or insn of frame, or exit when returns true?
|
Should call trap() every cycle or insn of frame, or exit when returns true?
|
||||||
|
|
||||||
|
|
||||||
|
BETTER DEBUGGING
|
||||||
|
|
||||||
|
Need to mark start/end columns, not just line number
|
||||||
|
Know if we are actively debugging or trap occurred
|
||||||
|
isRunning() = stopped, running, waiting, debugging...
|
||||||
|
Showing running PC may be distracting, maybe lines visited?
|
||||||
|
Don't grab cursor focus when trap occurs (how do we know?)
|
||||||
|
Use tick() and refresh(), not callbacks
|
||||||
|
Show current datum when using READ
|
||||||
|
Use https://codemirror.net/doc/manual.html#markText
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1
presets/basic/skeleton.basic
Normal file
1
presets/basic/skeleton.basic
Normal file
@ -0,0 +1 @@
|
|||||||
|
10 PRINT "EXAMPLE BASIC PROGRAM"
|
@ -26,14 +26,15 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
|
|||||||
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'];
|
||||||
var middleKeywords = ['to'];
|
var middleKeywords = ['to','then'];
|
||||||
var endKeywords = ['next','end'];
|
var endKeywords = ['next','end'];
|
||||||
|
|
||||||
var operatorKeywords = ['and', 'or', 'not', 'xor', 'in'];
|
var operatorKeywords = ['and', 'or', 'not', 'xor', 'eqv', 'imp'];
|
||||||
var wordOperators = wordRegexp(operatorKeywords);
|
var wordOperators = wordRegexp(operatorKeywords);
|
||||||
var commonKeywords = [
|
var commonKeywords = [
|
||||||
'let','print','go','goto','gosub','next','dim','input','data',
|
'BASE','DATA','DEF','DIM',
|
||||||
'read','restore','return','stop','on','def','option','then','step',
|
'GO','GOSUB','GOTO','INPUT','LET','ON','OPTION','PRINT',
|
||||||
|
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB'
|
||||||
];
|
];
|
||||||
var commontypes = ['xxxxbyte','xxxxword'];
|
var commontypes = ['xxxxbyte','xxxxword'];
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class CompileError extends Error {
|
|||||||
|
|
||||||
// Lexer regular expression -- each (capture group) handles a different token type
|
// Lexer regular expression -- each (capture group) handles a different token type
|
||||||
|
|
||||||
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\bAND\b)|(\bOR\b)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi;
|
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi;
|
||||||
|
|
||||||
export enum TokenType {
|
export enum TokenType {
|
||||||
EOL = 0,
|
EOL = 0,
|
||||||
@ -21,8 +21,6 @@ export enum TokenType {
|
|||||||
Float2,
|
Float2,
|
||||||
Int,
|
Int,
|
||||||
Remark,
|
Remark,
|
||||||
And,
|
|
||||||
Or,
|
|
||||||
Ident,
|
Ident,
|
||||||
String,
|
String,
|
||||||
Relational,
|
Relational,
|
||||||
@ -35,7 +33,7 @@ export type ExprTypes = BinOp | UnOp | IndOp | Literal;
|
|||||||
|
|
||||||
export type Expr = ExprTypes & SourceLocated;
|
export type Expr = ExprTypes & SourceLocated;
|
||||||
|
|
||||||
export type Opcode = 'add' | 'sub' | 'mul' | 'div' | 'pow' | 'eq' | 'ne' | 'lt' | 'gt' | 'le' | 'ge' | 'land' | 'lor';
|
export type Opcode = string;
|
||||||
|
|
||||||
export type Value = string | number;
|
export type Value = string | number;
|
||||||
|
|
||||||
@ -50,7 +48,7 @@ export interface BinOp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UnOp {
|
export interface UnOp {
|
||||||
op: 'neg';
|
op: 'neg' | 'lnot';
|
||||||
expr: Expr;
|
expr: Expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +150,7 @@ export interface BASICLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BASICProgram {
|
export interface BASICProgram {
|
||||||
|
opts: BASICOptions;
|
||||||
lines: BASICLine[];
|
lines: BASICLine[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,23 +161,25 @@ class Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OPERATORS = {
|
const OPERATORS = {
|
||||||
'AND': {f:'land',p:5},
|
'OR': {f:'bor',p:7},
|
||||||
'OR': {f:'lor',p:5},
|
'AND': {f:'band',p:8},
|
||||||
'=': {f:'eq',p:10},
|
'=': {f:'eq',p:50},
|
||||||
'<>': {f:'ne',p:10},
|
'<>': {f:'ne',p:50},
|
||||||
'<': {f:'lt',p:10},
|
'<': {f:'lt',p:50},
|
||||||
'>': {f:'gt',p:10},
|
'>': {f:'gt',p:50},
|
||||||
'<=': {f:'le',p:10},
|
'<=': {f:'le',p:50},
|
||||||
'>=': {f:'ge',p:10},
|
'>=': {f:'ge',p:50},
|
||||||
'+': {f:'add',p:100},
|
'+': {f:'add',p:100},
|
||||||
'-': {f:'sub',p:100},
|
'-': {f:'sub',p:100},
|
||||||
|
'%': {f:'mod',p:140},
|
||||||
|
'\\': {f:'idiv',p:150},
|
||||||
'*': {f:'mul',p:200},
|
'*': {f:'mul',p:200},
|
||||||
'/': {f:'div',p:200},
|
'/': {f:'div',p:200},
|
||||||
'^': {f:'pow',p:300}
|
'^': {f:'pow',p:300}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getOpcodeForOperator(op: string): Opcode {
|
function getOperator(op: string) {
|
||||||
return OPERATORS[op].f as Opcode;
|
return OPERATORS[op];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrecedence(tok: Token): number {
|
function getPrecedence(tok: Token): number {
|
||||||
@ -186,7 +187,7 @@ function getPrecedence(tok: Token): number {
|
|||||||
case TokenType.Operator:
|
case TokenType.Operator:
|
||||||
case TokenType.Relational:
|
case TokenType.Relational:
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
let op = OPERATORS[tok.str]
|
let op = getOperator(tok.str);
|
||||||
if (op) return op.p;
|
if (op) return op.p;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@ -202,7 +203,7 @@ function stripQuotes(s: string) {
|
|||||||
return s.substr(1, s.length-2);
|
return s.substr(1, s.length-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO: implement these
|
||||||
export interface BASICOptions {
|
export interface BASICOptions {
|
||||||
uppercaseOnly : boolean; // convert everything to uppercase?
|
uppercaseOnly : boolean; // convert everything to uppercase?
|
||||||
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
|
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
|
||||||
@ -222,21 +223,25 @@ export interface BASICOptions {
|
|||||||
printPrecision : number; // print precision # of digits
|
printPrecision : number; // print precision # of digits
|
||||||
checkOverflow : boolean; // check for overflow of numerics?
|
checkOverflow : boolean; // check for overflow of numerics?
|
||||||
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
|
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
|
||||||
|
multipleNextVars : boolean; // NEXT Y,X
|
||||||
}
|
}
|
||||||
|
|
||||||
///// BASIC PARSER
|
///// BASIC PARSER
|
||||||
|
|
||||||
export class BASICParser {
|
export class BASICParser {
|
||||||
tokens: Token[];
|
opts : BASICOptions = ALTAIR_BASIC40;
|
||||||
errors: WorkerError[];
|
errors: WorkerError[];
|
||||||
|
listings: CodeListingMap;
|
||||||
labels: { [label: string]: BASICLine };
|
labels: { [label: string]: BASICLine };
|
||||||
targets: { [targetlabel: string]: SourceLocation };
|
targets: { [targetlabel: string]: SourceLocation };
|
||||||
eol: Token;
|
decls: { [name: string]: SourceLocation }; // declared/set vars
|
||||||
|
refs: { [name: string]: SourceLocation }; // references
|
||||||
|
|
||||||
lineno : number;
|
lineno : number;
|
||||||
|
tokens: Token[];
|
||||||
|
eol: Token;
|
||||||
curlabel: string;
|
curlabel: string;
|
||||||
listings: CodeListingMap;
|
|
||||||
lasttoken: Token;
|
lasttoken: Token;
|
||||||
opts : BASICOptions = ALTAIR_BASIC40;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.labels = {};
|
this.labels = {};
|
||||||
@ -245,6 +250,8 @@ export class BASICParser {
|
|||||||
this.lineno = 0;
|
this.lineno = 0;
|
||||||
this.curlabel = null;
|
this.curlabel = null;
|
||||||
this.listings = {};
|
this.listings = {};
|
||||||
|
this.decls = {};
|
||||||
|
this.refs = {};
|
||||||
}
|
}
|
||||||
compileError(msg: string, loc?: SourceLocation) {
|
compileError(msg: string, loc?: SourceLocation) {
|
||||||
if (!loc) loc = this.peekToken().$loc;
|
if (!loc) loc = this.peekToken().$loc;
|
||||||
@ -253,7 +260,7 @@ export class BASICParser {
|
|||||||
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
|
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
|
||||||
}
|
}
|
||||||
dialectError(what: string, loc?: SourceLocation) {
|
dialectError(what: string, loc?: SourceLocation) {
|
||||||
this.compileError(`The selected BASIC dialect doesn't support ${what}`, loc); // TODO
|
this.compileError(`The selected BASIC dialect doesn't support ${what}.`, loc); // TODO
|
||||||
}
|
}
|
||||||
consumeToken(): Token {
|
consumeToken(): Token {
|
||||||
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
|
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
|
||||||
@ -261,9 +268,9 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
expectToken(str: string) : Token {
|
expectToken(str: string) : Token {
|
||||||
var tok = this.consumeToken();
|
var tok = this.consumeToken();
|
||||||
var tokstr = tok.str.toUpperCase();
|
var tokstr = tok.str;
|
||||||
if (str != tokstr) {
|
if (str != tokstr) {
|
||||||
this.compileError(`I expected "${str}" here, but I saw "${tokstr}".`);
|
this.compileError(`There should be a "${str}" here.`);
|
||||||
}
|
}
|
||||||
return tok;
|
return tok;
|
||||||
}
|
}
|
||||||
@ -278,7 +285,7 @@ export class BASICParser {
|
|||||||
let tok = this.consumeToken();
|
let tok = this.consumeToken();
|
||||||
switch (tok.type) {
|
switch (tok.type) {
|
||||||
case TokenType.Int:
|
case TokenType.Int:
|
||||||
if (this.labels[tok.str] != null) this.compileError(`I saw a duplicated label "${tok.str}".`);
|
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
|
||||||
this.labels[tok.str] = line;
|
this.labels[tok.str] = line;
|
||||||
line.label = tok.str;
|
line.label = tok.str;
|
||||||
this.curlabel = tok.str;
|
this.curlabel = tok.str;
|
||||||
@ -291,8 +298,8 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
parseFile(file: string, path: string) : BASICProgram {
|
parseFile(file: string, path: string) : BASICProgram {
|
||||||
var pgmlines = file.split("\n").map((line) => this.parseLine(line));
|
var pgmlines = file.split("\n").map((line) => this.parseLine(line));
|
||||||
this.checkLabels();
|
var program = { opts: this.opts, lines: pgmlines };
|
||||||
var program = { lines: pgmlines };
|
this.checkAll(program);
|
||||||
this.listings[path] = this.generateListing(file, program);
|
this.listings[path] = this.generateListing(file, program);
|
||||||
return program;
|
return program;
|
||||||
}
|
}
|
||||||
@ -308,11 +315,15 @@ export class BASICParser {
|
|||||||
tokenize(line: string) : void {
|
tokenize(line: string) : void {
|
||||||
this.lineno++;
|
this.lineno++;
|
||||||
this.tokens = [];
|
this.tokens = [];
|
||||||
var m;
|
var m : RegExpMatchArray;
|
||||||
while (m = re_toks.exec(line)) {
|
while (m = re_toks.exec(line)) {
|
||||||
for (var i = 1; i < TokenType._LAST; i++) {
|
for (var i = 1; i < TokenType._LAST; i++) {
|
||||||
let s = m[i];
|
let s : string = m[i];
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
|
// uppercase all identifiers, and maybe more
|
||||||
|
if (i == TokenType.Ident || this.opts.uppercaseOnly)
|
||||||
|
s = s.toUpperCase();
|
||||||
|
// add token to list
|
||||||
this.tokens.push({
|
this.tokens.push({
|
||||||
str: s,
|
str: s,
|
||||||
type: i,
|
type: i,
|
||||||
@ -341,23 +352,27 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
parseStatement(): Statement | null {
|
parseStatement(): Statement | null {
|
||||||
var cmdtok = this.consumeToken();
|
var cmdtok = this.consumeToken();
|
||||||
|
var cmd = cmdtok.str;
|
||||||
var stmt;
|
var stmt;
|
||||||
switch (cmdtok.type) {
|
switch (cmdtok.type) {
|
||||||
case TokenType.Remark:
|
case TokenType.Remark:
|
||||||
if (!this.opts.tickComments) this.dialectError(`tick remarks`);
|
if (!this.opts.tickComments) this.dialectError(`tick remarks`);
|
||||||
return null;
|
return null;
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
var cmd = cmdtok.str.toUpperCase();
|
// remark? ignore all tokens to eol
|
||||||
// remark? ignore to eol
|
|
||||||
if (cmd == 'REM') {
|
if (cmd == 'REM') {
|
||||||
while (this.consumeToken().type != TokenType.EOL) { }
|
while (this.consumeToken().type != TokenType.EOL) { }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// look for "GO TO"
|
// look for "GO TO" and "GO SUB"
|
||||||
if (cmd == 'GO' && this.peekToken().str == 'TO') {
|
if (cmd == 'GO' && this.peekToken().str == 'TO') {
|
||||||
this.consumeToken();
|
this.consumeToken();
|
||||||
cmd = 'GOTO';
|
cmd = 'GOTO';
|
||||||
|
} else if (cmd == 'GO' && this.peekToken().str == 'SUB') {
|
||||||
|
this.consumeToken();
|
||||||
|
cmd = 'GOSUB';
|
||||||
}
|
}
|
||||||
|
// lookup JS function for command
|
||||||
var fn = this['stmt__' + cmd];
|
var fn = this['stmt__' + cmd];
|
||||||
if (fn) {
|
if (fn) {
|
||||||
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
|
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
|
||||||
@ -371,10 +386,8 @@ export class BASICParser {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TokenType.EOL:
|
case TokenType.EOL:
|
||||||
this.compileError(`I expected a command here`);
|
|
||||||
return null;
|
|
||||||
default:
|
default:
|
||||||
this.compileError(`Unknown command "${cmdtok.str}"`);
|
this.compileError(`There should be a command here.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start };
|
if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start };
|
||||||
@ -387,6 +400,7 @@ export class BASICParser {
|
|||||||
var tok = this.consumeToken();
|
var tok = this.consumeToken();
|
||||||
switch (tok.type) {
|
switch (tok.type) {
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
|
this.refs[tok.str] = tok.$loc;
|
||||||
let args = null;
|
let args = null;
|
||||||
if (this.peekToken().str == '(') {
|
if (this.peekToken().str == '(') {
|
||||||
this.expectToken('(');
|
this.expectToken('(');
|
||||||
@ -410,8 +424,10 @@ export class BASICParser {
|
|||||||
this.pushbackToken(sep);
|
this.pushbackToken(sep);
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
parseVarOrIndexedList(): IndOp[] {
|
parseLexprList(): IndOp[] {
|
||||||
return this.parseList(this.parseVarOrIndexed, ',');
|
var list = this.parseList(this.parseVarOrIndexed, ',');
|
||||||
|
list.forEach((lexpr) => this.decls[lexpr.name] = this.lasttoken.$loc);
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
parseExprList(): Expr[] {
|
parseExprList(): Expr[] {
|
||||||
return this.parseList(this.parseExpr, ',');
|
return this.parseList(this.parseExpr, ',');
|
||||||
@ -427,7 +443,7 @@ export class BASICParser {
|
|||||||
this.targets[label] = tok.$loc;
|
this.targets[label] = tok.$loc;
|
||||||
return {value:label};
|
return {value:label};
|
||||||
default:
|
default:
|
||||||
this.compileError(`I expected a line number here`);
|
this.compileError(`There should be a line number here.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -437,12 +453,17 @@ export class BASICParser {
|
|||||||
case TokenType.Int:
|
case TokenType.Int:
|
||||||
case TokenType.Float1:
|
case TokenType.Float1:
|
||||||
case TokenType.Float2:
|
case TokenType.Float2:
|
||||||
return { value: parseFloat(tok.str), $loc: tok.$loc };
|
return { value: this.parseNumber(tok.str), $loc: tok.$loc };
|
||||||
case TokenType.String:
|
case TokenType.String:
|
||||||
return { value: stripQuotes(tok.str), $loc: tok.$loc };
|
return { value: stripQuotes(tok.str), $loc: tok.$loc };
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
this.pushbackToken(tok);
|
if (tok.str == 'NOT') {
|
||||||
return this.parseVarOrIndexedOrFunc();
|
let expr = this.parsePrimary();
|
||||||
|
return { op: 'lnot', expr: expr };
|
||||||
|
} else {
|
||||||
|
this.pushbackToken(tok);
|
||||||
|
return this.parseVarOrIndexedOrFunc();
|
||||||
|
}
|
||||||
case TokenType.Operator:
|
case TokenType.Operator:
|
||||||
if (tok.str == '(') {
|
if (tok.str == '(') {
|
||||||
let expr = this.parseExpr();
|
let expr = this.parseExpr();
|
||||||
@ -454,21 +475,33 @@ export class BASICParser {
|
|||||||
} else if (tok.str == '+') {
|
} else if (tok.str == '+') {
|
||||||
return this.parsePrimary(); // TODO?
|
return this.parsePrimary(); // TODO?
|
||||||
}
|
}
|
||||||
default:
|
case TokenType.EOL:
|
||||||
this.compileError(`Unexpected "${tok.str}"`);
|
this.compileError(`The expression is incomplete.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.compileError(`There was an unexpected "${tok.str}" in this expression.`);
|
||||||
|
}
|
||||||
|
parseNumber(str: string) : number {
|
||||||
|
var n = parseFloat(str);
|
||||||
|
if (isNaN(n))
|
||||||
|
this.compileError(`The number ${str} is not a valid floating-point number.`);
|
||||||
|
if (this.opts.checkOverflow && !isFinite(n))
|
||||||
|
this.compileError(`The number ${str} is too big to fit into a floating-point value.`);
|
||||||
|
return n;
|
||||||
}
|
}
|
||||||
parseExpr1(left: Expr, minPred: number): Expr {
|
parseExpr1(left: Expr, minPred: number): Expr {
|
||||||
let look = this.peekToken();
|
let look = this.peekToken();
|
||||||
while (getPrecedence(look) >= minPred) {
|
while (getPrecedence(look) >= minPred) {
|
||||||
let op = this.consumeToken();
|
let op = this.consumeToken();
|
||||||
|
if (this.opts.validOperators && this.opts.validOperators.indexOf(op.str) < 0)
|
||||||
|
this.dialectError(`the "${op.str}" operator`);
|
||||||
let right: Expr = this.parsePrimary();
|
let right: Expr = this.parsePrimary();
|
||||||
look = this.peekToken();
|
look = this.peekToken();
|
||||||
while (getPrecedence(look) > getPrecedence(op)) {
|
while (getPrecedence(look) > getPrecedence(op)) {
|
||||||
right = this.parseExpr1(right, getPrecedence(look));
|
right = this.parseExpr1(right, getPrecedence(look));
|
||||||
look = this.peekToken();
|
look = this.peekToken();
|
||||||
}
|
}
|
||||||
left = { op: getOpcodeForOperator(op.str), left: left, right: right };
|
left = { op: getOperator(op.str).f, left: left, right: right };
|
||||||
}
|
}
|
||||||
return left;
|
return left;
|
||||||
}
|
}
|
||||||
@ -481,6 +514,7 @@ export class BASICParser {
|
|||||||
stmt__LET(): LET_Statement {
|
stmt__LET(): LET_Statement {
|
||||||
var lexpr = this.parseVarOrIndexed();
|
var lexpr = this.parseVarOrIndexed();
|
||||||
this.expectToken("=");
|
this.expectToken("=");
|
||||||
|
this.decls[lexpr.name] = this.lasttoken.$loc;
|
||||||
var right = this.parseExpr();
|
var right = this.parseExpr();
|
||||||
return { command: "LET", lexpr: lexpr, right: right };
|
return { command: "LET", lexpr: lexpr, right: right };
|
||||||
}
|
}
|
||||||
@ -547,7 +581,7 @@ export class BASICParser {
|
|||||||
return { command:'NEXT', lexpr:lexpr };
|
return { command:'NEXT', lexpr:lexpr };
|
||||||
}
|
}
|
||||||
stmt__DIM() : DIM_Statement {
|
stmt__DIM() : DIM_Statement {
|
||||||
return { command:'DIM', args:this.parseVarOrIndexedList() };
|
return { command:'DIM', args:this.parseLexprList() };
|
||||||
}
|
}
|
||||||
stmt__INPUT() : INPUT_Statement {
|
stmt__INPUT() : INPUT_Statement {
|
||||||
var prompt = this.consumeToken();
|
var prompt = this.consumeToken();
|
||||||
@ -559,13 +593,13 @@ export class BASICParser {
|
|||||||
this.pushbackToken(prompt);
|
this.pushbackToken(prompt);
|
||||||
promptstr = "";
|
promptstr = "";
|
||||||
}
|
}
|
||||||
return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseVarOrIndexedList() };
|
return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseLexprList() };
|
||||||
}
|
}
|
||||||
stmt__DATA() : DATA_Statement {
|
stmt__DATA() : DATA_Statement {
|
||||||
return { command:'DATA', datums:this.parseExprList() };
|
return { command:'DATA', datums:this.parseExprList() };
|
||||||
}
|
}
|
||||||
stmt__READ() {
|
stmt__READ() {
|
||||||
return { command:'READ', args:this.parseVarOrIndexedList() };
|
return { command:'READ', args:this.parseLexprList() };
|
||||||
}
|
}
|
||||||
stmt__RESTORE() {
|
stmt__RESTORE() {
|
||||||
return { command:'RESTORE' };
|
return { command:'RESTORE' };
|
||||||
@ -587,22 +621,43 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
stmt__DEF() : DEF_Statement {
|
stmt__DEF() : DEF_Statement {
|
||||||
var lexpr = this.parseVarOrIndexed();
|
var lexpr = this.parseVarOrIndexed();
|
||||||
if (!lexpr.name.toUpperCase().startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
||||||
this.expectToken("=");
|
this.expectToken("=");
|
||||||
|
this.decls[lexpr.name] = this.lasttoken.$loc;
|
||||||
var func = this.parseExpr();
|
var func = this.parseExpr();
|
||||||
return { command:'DEF', lexpr:lexpr, def:func };
|
return { command:'DEF', lexpr:lexpr, def:func };
|
||||||
}
|
}
|
||||||
stmt__OPTION() : OPTION_Statement {
|
stmt__OPTION() : OPTION_Statement {
|
||||||
var tokname = this.consumeToken();
|
var tokname = this.consumeToken();
|
||||||
if (tokname.type != TokenType.Ident) this.compileError(`I expected a name after the OPTION statement.`)
|
if (tokname.type != TokenType.Ident) this.compileError(`There should be a name after the OPTION statement.`)
|
||||||
var list : string[] = [];
|
var list : string[] = [];
|
||||||
var tok;
|
var tok;
|
||||||
do {
|
do {
|
||||||
tok = this.consumeToken();
|
tok = this.consumeToken();
|
||||||
if (isEOS(tok)) break;
|
if (isEOS(tok)) break;
|
||||||
list.push(tok.str.toUpperCase());
|
list.push(tok.str);
|
||||||
} while (true);
|
} while (true);
|
||||||
return { command:'OPTION', optname:tokname.str.toUpperCase(), optargs:list };
|
var stmt : OPTION_Statement = { command:'OPTION', optname:tokname.str, optargs:list };
|
||||||
|
this.parseOptions(stmt);
|
||||||
|
return stmt;
|
||||||
|
}
|
||||||
|
parseOptions(stmt: OPTION_Statement) {
|
||||||
|
switch (stmt.optname) {
|
||||||
|
case 'BASE':
|
||||||
|
let base = parseInt(stmt.optargs[0]);
|
||||||
|
if (base == 0 || base == 1) this.opts.defaultArrayBase = base;
|
||||||
|
else this.compileError("OPTION BASE can only be 0 or 1.");
|
||||||
|
break;
|
||||||
|
case 'DIALECT':
|
||||||
|
let dname = stmt.optargs[0] || "";
|
||||||
|
let dialect = DIALECTS[dname];
|
||||||
|
if (dialect) this.opts = dialect;
|
||||||
|
else this.compileError(`The dialect named "${dname}" is not supported by this compiler.`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.compileError(`OPTION ${stmt.optname} is not supported by this compiler.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for workermain
|
// for workermain
|
||||||
@ -620,13 +675,23 @@ export class BASICParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LINT STUFF
|
// LINT STUFF
|
||||||
|
checkAll(program : BASICProgram) {
|
||||||
|
this.checkLabels();
|
||||||
|
//this.checkUnsetVars();
|
||||||
|
}
|
||||||
checkLabels() {
|
checkLabels() {
|
||||||
for (let targ in this.targets) {
|
for (let targ in this.targets) {
|
||||||
if (this.labels[targ] == null) {
|
if (this.labels[targ] == null) {
|
||||||
this.compileError(`I couldn't find line number ${targ}`, this.targets[targ]);
|
this.compileError(`There isn't a line number ${targ}.`, this.targets[targ]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
checkUnsetVars() {
|
||||||
|
for (var ref in this.refs) {
|
||||||
|
if (this.decls[ref] == null)
|
||||||
|
this.compileError(`The variable "${ref}" was used but not set with a LET, DIM, READ, or INPUT statement.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///// BASIC DIALECTS
|
///// BASIC DIALECTS
|
||||||
@ -643,18 +708,19 @@ export const ECMA55_MINIMAL : BASICOptions = {
|
|||||||
stringConcat : false,
|
stringConcat : false,
|
||||||
typeConvert : false,
|
typeConvert : false,
|
||||||
maxDimensions : 2,
|
maxDimensions : 2,
|
||||||
maxArguments : Infinity,
|
maxArguments : 255,
|
||||||
sparseArrays : false,
|
sparseArrays : false,
|
||||||
tickComments : false,
|
tickComments : false,
|
||||||
validKeywords : ['BASE','DATA','DEF','DIM','END',
|
validKeywords : ['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','SUB','THEN','TO'
|
||||||
],
|
],
|
||||||
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN'],
|
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'],
|
||||||
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
|
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
|
||||||
printZoneLength : 15,
|
printZoneLength : 15,
|
||||||
printPrecision : 6,
|
printPrecision : 6,
|
||||||
checkOverflow : true,
|
checkOverflow : true,
|
||||||
|
multipleNextVars : false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALTAIR_BASIC40 : BASICOptions = {
|
export const ALTAIR_BASIC40 : BASICOptions = {
|
||||||
@ -667,7 +733,7 @@ export const ALTAIR_BASIC40 : BASICOptions = {
|
|||||||
stringConcat : false,
|
stringConcat : false,
|
||||||
typeConvert : false,
|
typeConvert : false,
|
||||||
maxDimensions : 2,
|
maxDimensions : 2,
|
||||||
maxArguments : Infinity,
|
maxArguments : 255,
|
||||||
sparseArrays : false,
|
sparseArrays : false,
|
||||||
tickComments : false,
|
tickComments : false,
|
||||||
validKeywords : null, // all
|
validKeywords : null, // all
|
||||||
@ -676,4 +742,13 @@ export const ALTAIR_BASIC40 : BASICOptions = {
|
|||||||
printZoneLength : 15,
|
printZoneLength : 15,
|
||||||
printPrecision : 6,
|
printPrecision : 6,
|
||||||
checkOverflow : true,
|
checkOverflow : true,
|
||||||
|
multipleNextVars : true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DIALECTS = {
|
||||||
|
"DEFAULT": ALTAIR_BASIC40,
|
||||||
|
"ALTAIR": ALTAIR_BASIC40,
|
||||||
|
"ALTAIR40": ALTAIR_BASIC40,
|
||||||
|
"ECMA55": ECMA55_MINIMAL,
|
||||||
|
"MINIMAL": ECMA55_MINIMAL,
|
||||||
|
};
|
||||||
|
@ -17,9 +17,13 @@ var data = fs.readFileSync(filename, 'utf-8');
|
|||||||
try {
|
try {
|
||||||
var pgm = parser.parseFile(data, filename);
|
var pgm = parser.parseFile(data, filename);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("@@@ " + e.message);
|
if (parser.errors.length == 0)
|
||||||
throw e;
|
console.log("@@@ " + e.msg);
|
||||||
|
else
|
||||||
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
parser.errors.forEach((err) => console.log("@@@ " + err.msg));
|
||||||
|
if (parser.errors.length) process.exit(2);
|
||||||
|
|
||||||
var runtime = new BASICRuntime();
|
var runtime = new BASICRuntime();
|
||||||
runtime.trace = process.argv[3] == '-v';
|
runtime.trace = process.argv[3] == '-v';
|
||||||
@ -47,7 +51,7 @@ runtime.resume = function() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("### " + e.message);
|
console.log("### " + e.message);
|
||||||
throw e;
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -39,16 +39,17 @@ export class BASICRuntime {
|
|||||||
dataptr : number;
|
dataptr : number;
|
||||||
vars : {};
|
vars : {};
|
||||||
arrays : {};
|
arrays : {};
|
||||||
forLoops : {};
|
defs : {};
|
||||||
|
forLoops : {next:(name:string) => void}[];
|
||||||
returnStack : number[];
|
returnStack : number[];
|
||||||
column : number;
|
column : number;
|
||||||
abase : number; // array base
|
|
||||||
|
|
||||||
running : boolean = false;
|
running : boolean = false;
|
||||||
exited : boolean = false;
|
exited : boolean = true;
|
||||||
trace : boolean = false;
|
trace : boolean = false;
|
||||||
|
|
||||||
load(program: basic.BASICProgram) {
|
load(program: basic.BASICProgram) {
|
||||||
|
let prevlabel = this.label2pc && this.getLabelForPC(this.curpc);
|
||||||
this.program = program;
|
this.program = program;
|
||||||
this.label2lineidx = {};
|
this.label2lineidx = {};
|
||||||
this.label2pc = {};
|
this.label2pc = {};
|
||||||
@ -59,6 +60,7 @@ export class BASICRuntime {
|
|||||||
// TODO: lines start @ 1?
|
// TODO: lines start @ 1?
|
||||||
program.lines.forEach((line, idx) => {
|
program.lines.forEach((line, idx) => {
|
||||||
// make lookup tables
|
// make lookup tables
|
||||||
|
this.curpc = this.allstmts.length + 1; // set for error reporting
|
||||||
if (line.label != null) this.label2lineidx[line.label] = idx;
|
if (line.label != null) this.label2lineidx[line.label] = idx;
|
||||||
if (line.label != null) this.label2pc[line.label] = this.allstmts.length;
|
if (line.label != null) this.label2pc[line.label] = this.allstmts.length;
|
||||||
this.line2pc.push(this.allstmts.length);
|
this.line2pc.push(this.allstmts.length);
|
||||||
@ -69,8 +71,6 @@ export class BASICRuntime {
|
|||||||
line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => {
|
line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => {
|
||||||
(datastmt as basic.DATA_Statement).datums.forEach(d => {
|
(datastmt as basic.DATA_Statement).datums.forEach(d => {
|
||||||
var functext = this.expr2js(d, {check:true, isconst:true});
|
var functext = this.expr2js(d, {check:true, isconst:true});
|
||||||
// TODO: catch exceptions
|
|
||||||
// TODO: any value doing this ahead of time?
|
|
||||||
var value = new Function(`return ${functext};`).bind(this)();
|
var value = new Function(`return ${functext};`).bind(this)();
|
||||||
this.datums.push({value:value});
|
this.datums.push({value:value});
|
||||||
});
|
});
|
||||||
@ -78,6 +78,9 @@ export class BASICRuntime {
|
|||||||
// TODO: compile statements?
|
// TODO: compile statements?
|
||||||
//line.stmts.forEach((stmt) => this.compileStatement(stmt));
|
//line.stmts.forEach((stmt) => this.compileStatement(stmt));
|
||||||
});
|
});
|
||||||
|
// try to resume where we left off after loading
|
||||||
|
this.curpc = this.label2pc[prevlabel] || 0;
|
||||||
|
this.dataptr = Math.min(this.dataptr, this.datums.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
@ -85,21 +88,29 @@ export class BASICRuntime {
|
|||||||
this.dataptr = 0;
|
this.dataptr = 0;
|
||||||
this.vars = {};
|
this.vars = {};
|
||||||
this.arrays = {};
|
this.arrays = {};
|
||||||
this.forLoops = {};
|
this.defs = this.getBuiltinFunctions();
|
||||||
|
this.forLoops = [];
|
||||||
this.returnStack = [];
|
this.returnStack = [];
|
||||||
this.column = 0;
|
this.column = 0;
|
||||||
this.abase = 1;
|
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this.exited = false;
|
this.exited = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBuiltinFunctions() {
|
||||||
|
var fnames = this.program && this.program.opts.validFunctions;
|
||||||
|
// if no valid function list, look for ABC...() functions in prototype
|
||||||
|
if (!fnames) fnames = Object.keys(BASICRuntime.prototype).filter((name) => /^[A-Z]{3,}[$]?$/.test(name));
|
||||||
|
var dict = {};
|
||||||
|
for (var fn of fnames) dict[fn] = this[fn].bind(this);
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
runtimeError(msg : string) {
|
runtimeError(msg : string) {
|
||||||
this.curpc--; // we did curpc++ before executing statement
|
this.curpc--; // we did curpc++ before executing statement
|
||||||
// TODO: pass source location to error
|
// TODO: pass source location to error
|
||||||
throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`);
|
throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: sometimes on next line
|
|
||||||
getLineForPC(pc:number) {
|
getLineForPC(pc:number) {
|
||||||
var line;
|
var line;
|
||||||
do {
|
do {
|
||||||
@ -131,7 +142,7 @@ export class BASICRuntime {
|
|||||||
if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays));
|
if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays));
|
||||||
// skip to next statment
|
// skip to next statment
|
||||||
this.curpc++;
|
this.curpc++;
|
||||||
// compile statement to JS?
|
// compile (unless cached) and execute statement
|
||||||
this.compileStatement(stmt);
|
this.compileStatement(stmt);
|
||||||
this.executeStatement(stmt);
|
this.executeStatement(stmt);
|
||||||
return this.running;
|
return this.running;
|
||||||
@ -171,7 +182,7 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gosubLabel(label) {
|
gosubLabel(label) {
|
||||||
this.returnStack.push(this.curpc + 1);
|
this.returnStack.push(this.curpc);
|
||||||
this.gotoLabel(label);
|
this.gotoLabel(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,9 +251,9 @@ export class BASICRuntime {
|
|||||||
} else {
|
} else {
|
||||||
if (opts.isconst) this.runtimeError(`I expected a constant value here`);
|
if (opts.isconst) this.runtimeError(`I expected a constant value here`);
|
||||||
var s = '';
|
var s = '';
|
||||||
if (expr.args && this[expr.name]) { // is it a function?
|
if (this.defs[expr.name]) { // is it a function?
|
||||||
s += `this.${expr.name}(`;
|
s += `this.defs.${expr.name}(`;
|
||||||
s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
|
if (expr.args) s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
|
||||||
s += ')';
|
s += ')';
|
||||||
} else if (expr.args) { // is it a subscript?
|
} else if (expr.args) { // is it a subscript?
|
||||||
s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
|
s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
|
||||||
@ -252,7 +263,7 @@ export class BASICRuntime {
|
|||||||
s = `this.vars.${expr.name}`;
|
s = `this.vars.${expr.name}`;
|
||||||
}
|
}
|
||||||
if (opts.check)
|
if (opts.check)
|
||||||
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error
|
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`;
|
||||||
else
|
else
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@ -260,64 +271,85 @@ export class BASICRuntime {
|
|||||||
var left = this.expr2js(expr.left, opts);
|
var left = this.expr2js(expr.left, opts);
|
||||||
var right = this.expr2js(expr.right, opts);
|
var right = this.expr2js(expr.right, opts);
|
||||||
return `this.${expr.op}(${left}, ${right})`;
|
return `this.${expr.op}(${left}, ${right})`;
|
||||||
} else if (isUnOp(expr) && expr.op == 'neg') {
|
} else if (isUnOp(expr)) {
|
||||||
var e = this.expr2js(expr.expr, opts);
|
var e = this.expr2js(expr.expr, opts);
|
||||||
return `-(${e})`; // TODO: other ops?
|
return `this.${expr.op}(${e})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startForLoop(name, init, targ, step) {
|
startForLoop(forname, init, targ, step) {
|
||||||
// TODO: check for loop params
|
// TODO: support 0-iteration loops
|
||||||
var pc = this.curpc;
|
var pc = this.curpc;
|
||||||
if (!step) step = 1;
|
if (!step) step = 1;
|
||||||
this.vars[name] = init;
|
this.vars[forname] = init;
|
||||||
if (this.trace) console.log(`FOR ${name} = ${this.vars[name]} TO ${targ} STEP ${step}`);
|
if (this.trace) console.log(`FOR ${forname} = ${init} TO ${targ} STEP ${step}`);
|
||||||
this.forLoops[name] = {
|
this.forLoops.unshift({
|
||||||
next: () => {
|
next: (nextname:string) => {
|
||||||
var done = step >= 0 ? this.vars[name] >= targ : this.vars[name] <= targ;
|
if (nextname && forname != nextname)
|
||||||
|
this.runtimeError(`I executed NEXT "${nextname}", but the last FOR was for "${forname}".`)
|
||||||
|
this.vars[forname] += step;
|
||||||
|
var done = step >= 0 ? this.vars[forname] > targ : this.vars[forname] < targ;
|
||||||
if (done) {
|
if (done) {
|
||||||
delete this.forLoops[name];
|
this.forLoops.shift(); // pop FOR off the stack and continue
|
||||||
} else {
|
} else {
|
||||||
this.vars[name] += step;
|
this.curpc = pc; // go back to FOR location
|
||||||
this.curpc = pc;
|
|
||||||
}
|
}
|
||||||
if (this.trace) console.log(`NEXT ${name}: ${this.vars[name]} TO ${targ} STEP ${step} DONE=${done}`);
|
if (this.trace) console.log(`NEXT ${forname}: ${this.vars[forname]} TO ${targ} STEP ${step} DONE=${done}`);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
nextForLoop(name) {
|
nextForLoop(name) {
|
||||||
// TODO: check for for loop
|
var fl = this.forLoops[0];
|
||||||
var fl = this.forLoops[name];
|
if (!fl) this.runtimeError(`I couldn't find a FOR for this NEXT.`)
|
||||||
if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`)
|
else fl.next(name);
|
||||||
this.forLoops[name].next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// converts a variable to string/number based on var name
|
// converts a variable to string/number based on var name
|
||||||
|
assign(name: string, right: number|string) : number|string {
|
||||||
|
if (this.program.opts.typeConvert)
|
||||||
|
return this.convert(name, right);
|
||||||
|
// TODO: use options
|
||||||
|
if (name.endsWith("$")) {
|
||||||
|
return this.convertToString(right, name);
|
||||||
|
} else {
|
||||||
|
return this.convertToNumber(right, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
convert(name: string, right: number|string) : number|string {
|
convert(name: string, right: number|string) : number|string {
|
||||||
// TODO: error check?
|
if (name.endsWith("$")) {
|
||||||
if (name.endsWith("$"))
|
|
||||||
return right+"";
|
return right+"";
|
||||||
else if (typeof right === 'string')
|
} else if (typeof right === 'number') {
|
||||||
return parseFloat(right);
|
|
||||||
else if (typeof right === 'number')
|
|
||||||
return right;
|
return right;
|
||||||
else
|
} else {
|
||||||
return this.checkValue(right, name);
|
return parseFloat(right+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToString(right: number|string, name?: string) {
|
||||||
|
if (typeof right !== 'string') this.runtimeError(`I can't convert ${right} to a string.`);
|
||||||
|
else return right;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToNumber(right: number|string, name?: string) {
|
||||||
|
if (typeof right !== 'number') this.runtimeError(`I can't convert ${right} to a number.`);
|
||||||
|
else return this.checkNum(right);
|
||||||
}
|
}
|
||||||
|
|
||||||
// dimension array
|
// dimension array
|
||||||
dimArray(name: string, ...dims:number[]) {
|
dimArray(name: string, ...dims:number[]) {
|
||||||
|
if (this.arrays[name]) this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
|
||||||
var isstring = name.endsWith('$');
|
var isstring = name.endsWith('$');
|
||||||
// TODO: option for undefined float array elements?
|
// if defaultValues is true, we use Float64Array which inits to 0
|
||||||
var arrcons = isstring ? Array : Float64Array;
|
var arrcons = isstring || !this.program.opts.defaultValues ? Array : Float64Array;
|
||||||
var ab = this.abase;
|
// TODO? var ab = this.program.opts.defaultArrayBase;
|
||||||
if (dims.length == 1) {
|
if (dims.length == 1) {
|
||||||
this.arrays[name] = new arrcons(dims[0]+ab);
|
this.arrays[name] = new arrcons(dims[0]+1);
|
||||||
} else if (dims.length == 2) {
|
} else if (dims.length == 2) {
|
||||||
this.arrays[name] = new Array(dims[0]+ab);
|
this.arrays[name] = new Array(dims[0]+1);
|
||||||
for (var i=ab; i<dims[0]+ab; i++)
|
for (var i=0; i<dims[0]+1; i++)
|
||||||
this.arrays[name][i] = new arrcons(dims[1]+ab);
|
this.arrays[name][i] = new arrcons(dims[1]+1);
|
||||||
} else {
|
} else {
|
||||||
this.runtimeError(`I only support arrays of one or two dimensions.`)
|
this.runtimeError(`I only support arrays of one or two dimensions.`)
|
||||||
}
|
}
|
||||||
@ -364,15 +396,22 @@ export class BASICRuntime {
|
|||||||
var setvals = '';
|
var setvals = '';
|
||||||
stmt.args.forEach((arg, index) => {
|
stmt.args.forEach((arg, index) => {
|
||||||
var lexpr = this.expr2js(arg, {check:false});
|
var lexpr = this.expr2js(arg, {check:false});
|
||||||
setvals += `${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);`
|
setvals += `valid &= this.isValid(${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]));`
|
||||||
});
|
});
|
||||||
return `this.running=false; this.input(${prompt}, ${stmt.args.length}).then((vals) => {${setvals}; this.running=true; this.resume();})`;
|
return `this.running=false;
|
||||||
|
this.input(${prompt}, ${stmt.args.length}).then((vals) => {
|
||||||
|
let valid = 1;
|
||||||
|
${setvals}
|
||||||
|
if (!valid) this.curpc--;
|
||||||
|
this.running=true;
|
||||||
|
this.resume();
|
||||||
|
})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
do__LET(stmt : basic.LET_Statement) {
|
do__LET(stmt : basic.LET_Statement) {
|
||||||
var lexpr = this.expr2js(stmt.lexpr, {check:false});
|
var lexpr = this.expr2js(stmt.lexpr, {check:false});
|
||||||
var right = this.expr2js(stmt.right, {check:true});
|
var right = this.expr2js(stmt.right, {check:true});
|
||||||
return `${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
|
return `${lexpr} = this.assign(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
|
||||||
}
|
}
|
||||||
|
|
||||||
do__FOR(stmt : basic.FOR_Statement) {
|
do__FOR(stmt : basic.FOR_Statement) {
|
||||||
@ -384,7 +423,7 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do__NEXT(stmt : basic.NEXT_Statement) {
|
do__NEXT(stmt : basic.NEXT_Statement) {
|
||||||
var name = JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
|
var name = stmt.lexpr && JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
|
||||||
return `this.nextForLoop(${name})`;
|
return `this.nextForLoop(${name})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,9 +433,9 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do__DEF(stmt : basic.DEF_Statement) {
|
do__DEF(stmt : basic.DEF_Statement) {
|
||||||
var lexpr = `this.${stmt.lexpr.name}`;
|
var lexpr = `this.defs.${stmt.lexpr.name}`;
|
||||||
var args = [];
|
var args = [];
|
||||||
for (var arg of stmt.lexpr.args) {
|
for (var arg of stmt.lexpr.args || []) {
|
||||||
if (isLookup(arg)) {
|
if (isLookup(arg)) {
|
||||||
args.push(arg.name);
|
args.push(arg.name);
|
||||||
} else {
|
} else {
|
||||||
@ -404,14 +443,13 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var functext = this.expr2js(stmt.def, {check:true, locals:args});
|
var functext = this.expr2js(stmt.def, {check:true, locals:args});
|
||||||
// TODO: use stmt.args to add function params
|
return `${lexpr} = function(${args.join(',')}) { return ${functext}; }.bind(this)`;
|
||||||
return `${lexpr} = function(${args.join(',')}) { return ${functext}; }`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_DIM(dim : basic.IndOp) {
|
_DIM(dim : basic.IndOp) {
|
||||||
var argsstr = '';
|
var argsstr = '';
|
||||||
for (var arg of dim.args) {
|
for (var arg of dim.args) {
|
||||||
// TODO: check for float
|
// TODO: check for float (or at compile time)
|
||||||
argsstr += ', ' + this.expr2js(arg, {check:true});
|
argsstr += ', ' + this.expr2js(arg, {check:true});
|
||||||
}
|
}
|
||||||
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
|
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
|
||||||
@ -450,13 +488,13 @@ export class BASICRuntime {
|
|||||||
do__READ(stmt : basic.READ_Statement) {
|
do__READ(stmt : basic.READ_Statement) {
|
||||||
var s = '';
|
var s = '';
|
||||||
stmt.args.forEach((arg) => {
|
stmt.args.forEach((arg) => {
|
||||||
s += `${this.expr2js(arg, {check:false})} = this.convert(${JSON.stringify(arg.name)}, this.nextDatum());`;
|
s += `${this.expr2js(arg, {check:false})} = this.assign(${JSON.stringify(arg.name)}, this.nextDatum());`;
|
||||||
});
|
});
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
do__RESTORE() {
|
do__RESTORE() {
|
||||||
this.dataptr = 0; // TODO: line number?
|
this.dataptr = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
do__END() {
|
do__END() {
|
||||||
@ -468,26 +506,30 @@ export class BASICRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do__OPTION(stmt: basic.OPTION_Statement) {
|
do__OPTION(stmt: basic.OPTION_Statement) {
|
||||||
switch (stmt.optname) {
|
// already parsed in compiler
|
||||||
case 'BASE':
|
|
||||||
let base = parseInt(stmt.optargs[0]);
|
|
||||||
if (base == 0 || base == 1) this.abase = base;
|
|
||||||
else this.runtimeError("OPTION BASE can only be 0 or 1.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.runtimeError(`OPTION ${stmt.optname} is not supported by this compiler.`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: "SUBSCRIPT ERROR"
|
// TODO: "SUBSCRIPT ERROR" (range check)
|
||||||
// TODO: gosubs nested too deeply
|
// TODO: gosubs nested too deeply
|
||||||
// TODO: memory quota
|
// TODO: memory quota
|
||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
|
|
||||||
checkValue(obj:number|string, exprname:string) {
|
isValid(obj:number|string) : boolean {
|
||||||
|
if (typeof obj === 'number' && !isNaN(obj))
|
||||||
|
return true;
|
||||||
|
else if (typeof obj === 'string')
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
checkValue(obj:number|string, exprname:string) : number|string {
|
||||||
|
// check for unreferenced value
|
||||||
if (typeof obj !== 'number' && typeof obj !== 'string') {
|
if (typeof obj !== 'number' && typeof obj !== 'string') {
|
||||||
|
// assign default value?
|
||||||
|
if (obj == null && this.program.opts.defaultValues) {
|
||||||
|
return exprname.endsWith("$") ? "" : 0;
|
||||||
|
}
|
||||||
if (exprname != null && obj == null) {
|
if (exprname != null && obj == null) {
|
||||||
this.runtimeError(`I didn't find a value for ${exprname}`);
|
this.runtimeError(`I didn't find a value for ${exprname}`);
|
||||||
} else if (exprname != null) {
|
} else if (exprname != null) {
|
||||||
@ -527,6 +569,12 @@ export class BASICRuntime {
|
|||||||
if (b == 0) this.runtimeError(`I can't divide by zero.`);
|
if (b == 0) this.runtimeError(`I can't divide by zero.`);
|
||||||
return this.checkNum(a / b);
|
return this.checkNum(a / b);
|
||||||
}
|
}
|
||||||
|
idiv(a:number, b:number) : number {
|
||||||
|
return this.div(Math.floor(a), Math.floor(b));
|
||||||
|
}
|
||||||
|
mod(a:number, b:number) : number {
|
||||||
|
return this.checkNum(a % b);
|
||||||
|
}
|
||||||
pow(a:number, b:number) : number {
|
pow(a:number, b:number) : number {
|
||||||
if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`);
|
if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`);
|
||||||
return this.checkNum(Math.pow(a, b));
|
return this.checkNum(Math.pow(a, b));
|
||||||
@ -537,6 +585,18 @@ export class BASICRuntime {
|
|||||||
lor(a:number, b:number) : number {
|
lor(a:number, b:number) : number {
|
||||||
return a || b;
|
return a || b;
|
||||||
}
|
}
|
||||||
|
lnot(a:number) : number {
|
||||||
|
return a ? 0 : 1;
|
||||||
|
}
|
||||||
|
neg(a:number) : number {
|
||||||
|
return -a;
|
||||||
|
}
|
||||||
|
band(a:number, b:number) : number {
|
||||||
|
return a & b;
|
||||||
|
}
|
||||||
|
bor(a:number, b:number) : number {
|
||||||
|
return a | b;
|
||||||
|
}
|
||||||
eq(a:number, b:number) : boolean {
|
eq(a:number, b:number) : boolean {
|
||||||
return a == b;
|
return a == b;
|
||||||
}
|
}
|
||||||
@ -604,7 +664,7 @@ export class BASICRuntime {
|
|||||||
return this.checkNum(Math.log(arg));
|
return this.checkNum(Math.log(arg));
|
||||||
}
|
}
|
||||||
MID$(arg : string, start : number, count : number) : string {
|
MID$(arg : string, start : number, count : number) : string {
|
||||||
if (start < 1) this.runtimeError(`The second parameter to MID$ must be between 1 and the length of the string in the first parameter.`)
|
if (start < 1) this.runtimeError(`I tried to compute MID$ but the second parameter is less than zero (${start}).`)
|
||||||
return arg.substr(start-1, count);
|
return arg.substr(start-1, count);
|
||||||
}
|
}
|
||||||
RIGHT$(arg : string, count : number) : string {
|
RIGHT$(arg : string, count : number) : string {
|
||||||
@ -614,7 +674,7 @@ export class BASICRuntime {
|
|||||||
return Math.random(); // argument ignored
|
return Math.random(); // argument ignored
|
||||||
}
|
}
|
||||||
ROUND(arg : number) : number {
|
ROUND(arg : number) : number {
|
||||||
return this.checkNum(Math.round(arg)); // TODO?
|
return this.checkNum(Math.round(arg));
|
||||||
}
|
}
|
||||||
SGN(arg : number) : number {
|
SGN(arg : number) : number {
|
||||||
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
||||||
@ -623,24 +683,25 @@ export class BASICRuntime {
|
|||||||
return this.checkNum(Math.sin(arg));
|
return this.checkNum(Math.sin(arg));
|
||||||
}
|
}
|
||||||
SPACE$(arg : number) : string {
|
SPACE$(arg : number) : string {
|
||||||
return ' '.repeat(this.checkNum(arg));
|
return (arg > 0) ? ' '.repeat(this.checkNum(arg)) : '';
|
||||||
}
|
}
|
||||||
SQR(arg : number) : number {
|
SQR(arg : number) : number {
|
||||||
if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`)
|
if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`)
|
||||||
return this.checkNum(Math.sqrt(arg));
|
return this.checkNum(Math.sqrt(arg));
|
||||||
}
|
}
|
||||||
STR$(arg) : string {
|
STR$(arg : number) : string {
|
||||||
return this.valueToString(arg);
|
return this.valueToString(this.checkNum(arg));
|
||||||
}
|
}
|
||||||
TAB(arg : number) : string {
|
TAB(arg : number) : string {
|
||||||
if (arg < 0) this.runtimeError(`I got a negative value for the TAB() function.`);
|
if (arg < 1) { arg = 1; } // TODO: SYSTEM MESSAGE IDENTIFYING THE EXCEPTION
|
||||||
var spaces = this.ROUND(arg) - this.column;
|
var spaces = this.ROUND(arg) - 1 - this.column;
|
||||||
return (spaces > 0) ? ' '.repeat(spaces) : '';
|
return (spaces > 0) ? ' '.repeat(spaces) : '';
|
||||||
}
|
}
|
||||||
TAN(arg : number) : number {
|
TAN(arg : number) : number {
|
||||||
return this.checkNum(Math.tan(arg));
|
return this.checkNum(Math.tan(arg));
|
||||||
}
|
}
|
||||||
VAL(arg) : number {
|
VAL(arg : string) : number {
|
||||||
return parseFloat(arg+"");
|
var n = parseFloat(this.checkString(arg));
|
||||||
|
return isNaN(n) ? 0 : n; // TODO? altair works this way
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,6 +225,10 @@ export class RAM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EmuHalt extends Error {
|
export class EmuHalt extends Error {
|
||||||
|
constructor(msg:string) {
|
||||||
|
super(msg);
|
||||||
|
Object.setPrototypeOf(this, EmuHalt.prototype);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AnimationTimer {
|
export class AnimationTimer {
|
||||||
|
@ -98,7 +98,7 @@ export class SourceEditor implements ProjectView {
|
|||||||
this.newEditor(div, asmOverride);
|
this.newEditor(div, asmOverride);
|
||||||
if (text) {
|
if (text) {
|
||||||
this.setText(text); // TODO: this calls setCode() and builds... it shouldn't
|
this.setText(text); // TODO: this calls setCode() and builds... it shouldn't
|
||||||
this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true});
|
this.editor.setSelection({line:0,ch:0}, {line:0,ch:0}, {scroll:true}); // move cursor to start
|
||||||
}
|
}
|
||||||
this.setupEditor();
|
this.setupEditor();
|
||||||
return div;
|
return div;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { Platform, BreakpointCallback } from "../common/baseplatform";
|
import { Platform, BreakpointCallback } from "../common/baseplatform";
|
||||||
import { PLATFORMS, AnimationTimer } from "../common/emu";
|
import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu";
|
||||||
import { loadScript } from "../ide/ui";
|
import { loadScript } from "../ide/ui";
|
||||||
import { BASICRuntime } from "../common/basic/runtime";
|
import { BASICRuntime } from "../common/basic/runtime";
|
||||||
import { BASICProgram } from "../common/basic/compiler";
|
import { BASICProgram } from "../common/basic/compiler";
|
||||||
@ -278,7 +278,7 @@ class BASICPlatform implements Platform {
|
|||||||
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
|
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
|
||||||
this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this);
|
this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this);
|
||||||
this.tty.scrolldiv = parent;
|
this.tty.scrolldiv = parent;
|
||||||
this.timer = new AnimationTimer(60, this.advance1_60.bind(this));
|
this.timer = new AnimationTimer(60, this.animate.bind(this));
|
||||||
this.resize = () => {
|
this.resize = () => {
|
||||||
// set font size proportional to window width
|
// set font size proportional to window width
|
||||||
var charwidth = $(gameport).width() * 1.6 / 80;
|
var charwidth = $(gameport).width() * 1.6 / 80;
|
||||||
@ -286,11 +286,15 @@ class BASICPlatform implements Platform {
|
|||||||
this.tty.scrollToBottom();
|
this.tty.scrollToBottom();
|
||||||
}
|
}
|
||||||
this.resize();
|
this.resize();
|
||||||
this.runtime.print = this.tty.print.bind(this.tty);
|
this.runtime.print = (s:string) => {
|
||||||
|
// TODO: why null sometimes?
|
||||||
|
this.clock = 0; // exit advance loop when printing
|
||||||
|
this.tty.print(s);
|
||||||
|
}
|
||||||
this.runtime.resume = this.resume.bind(this);
|
this.runtime.resume = this.resume.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
advance1_60() {
|
animate() {
|
||||||
if (this.tty.isBusy()) return;
|
if (this.tty.isBusy()) return;
|
||||||
this.clock += this.ips/60;
|
this.clock += this.ips/60;
|
||||||
while (!this.runtime.exited && this.clock-- > 0) {
|
while (!this.runtime.exited && this.clock-- > 0) {
|
||||||
@ -298,33 +302,36 @@ class BASICPlatform implements Platform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should not depend on tty state
|
||||||
advance(novideo?: boolean) : number {
|
advance(novideo?: boolean) : number {
|
||||||
if (this.runtime.running) {
|
if (this.runtime.running) {
|
||||||
try {
|
var more = this.runtime.step();
|
||||||
var more = this.runtime.step();
|
if (!more) {
|
||||||
if (!more) {
|
this.pause();
|
||||||
this.pause();
|
if (this.runtime.exited) {
|
||||||
if (this.runtime.exited) {
|
this.exitmsg();
|
||||||
this.tty.print("\n\n");
|
|
||||||
this.tty.addtext("*** END OF PROGRAM ***", 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
this.break();
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
// TODO: break() when EmuHalt at location?
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exitmsg() {
|
||||||
|
this.tty.print("\n\n");
|
||||||
|
this.tty.addtext("*** END OF PROGRAM ***", 1);
|
||||||
|
}
|
||||||
|
|
||||||
resize: () => void;
|
resize: () => void;
|
||||||
|
|
||||||
loadROM(title, data) {
|
loadROM(title, data) {
|
||||||
this.reset();
|
var didExit = this.runtime.exited;
|
||||||
this.program = data;
|
this.program = data;
|
||||||
this.runtime.load(data);
|
this.runtime.load(data);
|
||||||
|
// only reset if we exited, otherwise we try to resume
|
||||||
|
if (didExit) this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
getROMExtension() {
|
getROMExtension() {
|
||||||
@ -403,8 +410,9 @@ class BASICPlatform implements Platform {
|
|||||||
this.break();
|
this.break();
|
||||||
}
|
}
|
||||||
break() {
|
break() {
|
||||||
|
// TODO: don't highlight text in editor
|
||||||
if (this.onBreakpointHit) {
|
if (this.onBreakpointHit) {
|
||||||
this.onBreakpointHit(this.saveState());
|
//TODO: this.onBreakpointHit(this.saveState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user