mirror of
https://github.com/inexorabletash/jsbasic.git
synced 2024-11-26 21:50:49 +00:00
2395 lines
77 KiB
JavaScript
2395 lines
77 KiB
JavaScript
//
|
|
// BASIC Compiler and Executor
|
|
//
|
|
|
|
// API:
|
|
// var program = basic.compile(source);
|
|
// // may throw basic.ParseError
|
|
//
|
|
// program.init({tty: ..., hires: .e.., lores: ...})
|
|
//
|
|
// // if TTY input is blocking:
|
|
//
|
|
// var state;
|
|
// do {
|
|
// state = program.step();
|
|
// // may throw basic.RuntimeError
|
|
// } while (state !== basic.STATE_STOPPED);
|
|
//
|
|
// // if TTY input is non-blocking:
|
|
// function driver() {
|
|
// var state;
|
|
// do {
|
|
// state = program.step(driver);
|
|
// // may throw basic.RuntimeError
|
|
// } while (state === basic.STATE_RUNNING);
|
|
// }
|
|
// driver(); // step until done or blocked
|
|
// // driver will also be called after input is unblocked
|
|
// // driver may want to yield via setTimeout() after N steps
|
|
|
|
this.basic = (function() {
|
|
|
|
var basic = {
|
|
STATE_STOPPED: 0,
|
|
STATE_RUNNING: 1,
|
|
STATE_BLOCKED: 2
|
|
};
|
|
|
|
//
|
|
// Thrown if parsing fails
|
|
//
|
|
basic.ParseError = function(msg, line, column) {
|
|
this.name = 'ParseError';
|
|
this.message = msg || '';
|
|
this.line = line;
|
|
this.column = column;
|
|
};
|
|
basic.ParseError.prototype = new Error();
|
|
|
|
|
|
//
|
|
// Thrown when a program is running; can be caught by ONERR
|
|
//
|
|
basic.RuntimeError = function(msg, code) {
|
|
this.name = 'RuntimeError';
|
|
this.message = msg;
|
|
this.code = code;
|
|
};
|
|
basic.RuntimeError.prototype = new Error();
|
|
|
|
function runtime_error(msg) {
|
|
if (typeof msg === 'object' && msg.length && msg.length >= 2) {
|
|
throw new basic.RuntimeError(msg[1], msg[0]);
|
|
} else {
|
|
throw new basic.RuntimeError(msg);
|
|
}
|
|
}
|
|
|
|
var ERRORS = {
|
|
NEXT_WITHOUT_FOR: [0, "Next without for"],
|
|
SYNTAX_ERROR: [16, "Syntax error"],
|
|
RETURN_WITHOUT_GOSUB: [22, "Return without gosub"],
|
|
OUT_OF_DATA: [42, "Out of data"],
|
|
ILLEGAL_QUANTITY: [53, "Illegal quantity"],
|
|
OVERFLOW: [69, "Overflow"],
|
|
OUT_OF_MEMORY: [77, "Out of memory"],
|
|
UNDEFINED_STATEMENT: [90, "Undefined statement"],
|
|
BAD_SUBSCRIPT: [107, "Bad subscript"],
|
|
REDIMED_ARRAY: [120, "Redimensioned array"],
|
|
DIVISION_BY_ZERO: [133, "Division by zero"],
|
|
TYPE_MISMATCH: [163, "Type mismatch"],
|
|
STRING_TOO_LONG: [176, "String too long"],
|
|
FORMULA_TOO_COMPLEX: [191, "Formula too complex"],
|
|
UNDEFINED_FUNCTION: [224, "Undefined function"],
|
|
REENTER: [254, "Re-enter"],
|
|
INTERRUPT: [255, "Break"]
|
|
};
|
|
|
|
// Keyword table - these can be altered (e.g. localized) here
|
|
var kws = {
|
|
ABS: "ABS",
|
|
AND: "AND",
|
|
ASC: "ASC",
|
|
ATN: "ATN",
|
|
AT: "AT",
|
|
CALL: "CALL",
|
|
CHR$: "CHR$",
|
|
CLEAR: "CLEAR",
|
|
COLOR: "COLOR=",
|
|
CONT: "CONT",
|
|
COS: "COS",
|
|
DATA: "DATA",
|
|
DEF: "DEF",
|
|
DEL: "DEL",
|
|
DIM: "DIM",
|
|
DRAW: "DRAW",
|
|
END: "END",
|
|
EXP: "EXP",
|
|
FLASH: "FLASH",
|
|
FN: "FN",
|
|
FOR: "FOR",
|
|
FRE: "FRE",
|
|
GET: "GET",
|
|
GOSUB: "GOSUB",
|
|
GOTO: "GOTO",
|
|
GR: "GR",
|
|
HCOLOR: "HCOLOR=",
|
|
HGR2: "HGR2",
|
|
HGR: "HGR",
|
|
HIMEM: "HIMEM:",
|
|
HLIN: "HLIN",
|
|
HOME: "HOME",
|
|
HPLOT: "HPLOT",
|
|
HTAB: "HTAB",
|
|
IF: "IF",
|
|
IN: "IN#",
|
|
INPUT: "INPUT",
|
|
INT: "INT",
|
|
INVERSE: "INVERSE",
|
|
LEFT$: "LEFT$",
|
|
LEN: "LEN",
|
|
LET: "LET",
|
|
LIST: "LIST",
|
|
LOAD: "LOAD",
|
|
LOG: "LOG",
|
|
LOMEM: "LOMEM:",
|
|
MID$: "MID$",
|
|
NEW: "NEW",
|
|
NEXT: "NEXT",
|
|
NORMAL: "NORMAL",
|
|
NOTRACE: "NOTRACE",
|
|
NOT: "NOT",
|
|
ONERR: "ONERR",
|
|
ON: "ON",
|
|
OR: "OR",
|
|
PDL: "PDL",
|
|
PEEK: "PEEK",
|
|
PLOT: "PLOT",
|
|
POKE: "POKE",
|
|
POP: "POP",
|
|
POS: "POS",
|
|
PRINT: "PRINT",
|
|
PR: "PR#",
|
|
READ: "READ",
|
|
RECALL: "RECALL",
|
|
REM: "REM",
|
|
RESTORE: "RESTORE",
|
|
RESUME: "RESUME",
|
|
RETURN: "RETURN",
|
|
RIGHT$: "RIGHT$",
|
|
RND: "RND",
|
|
ROT: "ROT=",
|
|
RUN: "RUN",
|
|
SAVE: "SAVE",
|
|
SCALE: "SCALE=",
|
|
SCRN: "SCRN",
|
|
SGN: "SGN",
|
|
SHLOAD: "SHLOAD",
|
|
SIN: "SIN",
|
|
SPC: "SPC",
|
|
SPEED: "SPEED=",
|
|
SQR: "SQR",
|
|
STEP: "STEP",
|
|
STOP: "STOP",
|
|
STORE: "STORE",
|
|
STR$: "STR$",
|
|
TAB: "TAB",
|
|
TAN: "TAN",
|
|
TEXT: "TEXT",
|
|
THEN: "THEN",
|
|
TO: "TO",
|
|
TRACE: "TRACE",
|
|
USR: "USR",
|
|
VAL: "VAL",
|
|
VLIN: "VLIN",
|
|
VTAB: "VTAB",
|
|
WAIT: "WAIT",
|
|
XDRAW: "XDRAW",
|
|
AMPERSAND: "&",
|
|
QUESTION: "?",
|
|
HSCRN: "HSCRN"
|
|
};
|
|
|
|
//
|
|
// Runtime flow control
|
|
//
|
|
function EndProgram() { }
|
|
function GoToLine(n) { this.line = n; }
|
|
function NextLine() { }
|
|
function BlockingInput(method, callback) {
|
|
this.method = method;
|
|
this.callback = callback;
|
|
}
|
|
|
|
// Adapted from:
|
|
// http://stackoverflow.com/questions/424292/how-to-create-my-own-javascript-random-number-generator-that-i-can-also-set-the-s
|
|
function PRNG() {
|
|
var S = 2345678901, // seed
|
|
A = 48271, // const
|
|
M = 2147483647, // const
|
|
Q = M / A, // const
|
|
R = M % A; // const
|
|
|
|
this.next = function PRNG_next() {
|
|
var hi = S / Q,
|
|
lo = S % Q,
|
|
t = A * lo - R * hi;
|
|
S = (t > 0) ? t : t + M;
|
|
this.last = S / M;
|
|
return this.last;
|
|
};
|
|
this.seed = function PRNG_seed(x) {
|
|
S = Math.floor(Math.abs(x));
|
|
};
|
|
this.next();
|
|
}
|
|
|
|
// Multidimensional array, with auto-dimensioning on first access
|
|
function BASICArray(type, dims) {
|
|
|
|
var array, dimensions;
|
|
|
|
function offset(dims, subscripts) {
|
|
if (subscripts.length !== dimensions.length) {
|
|
runtime_error(ERRORS.BAD_SUBSCRIPT);
|
|
}
|
|
|
|
var k, l, s = 0, p, ss;
|
|
for (k = 0; k < dims.length; k += 1) {
|
|
|
|
ss = subscripts[k];
|
|
if (ss < 0) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
ss = ss >> 0;
|
|
if (ss >= dims[k]) {
|
|
runtime_error(ERRORS.BAD_SUBSCRIPT);
|
|
}
|
|
|
|
p = 1;
|
|
for (l = k + 1; l < dims.length; l += 1) {
|
|
p *= dims[l];
|
|
}
|
|
s += p * ss;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
this.dim = function dim(dims) {
|
|
if (array) {
|
|
runtime_error(ERRORS.REDIMED_ARRAY);
|
|
}
|
|
|
|
dimensions = dims.map(function(n) { return (Number(n) >> 0) + 1; });
|
|
|
|
var i, len = dimensions.reduce(function(a, b) { return a * b; }),
|
|
defval = (type === 'string') ? '' : 0;
|
|
|
|
array = [];
|
|
for (i = 0; i < len; i += 1) {
|
|
array[i] = defval;
|
|
}
|
|
};
|
|
|
|
this.get = function get(subscripts) {
|
|
if (!array) {
|
|
this.dim(subscripts.map(function() { return 10; }));
|
|
}
|
|
|
|
|
|
return array[offset(dimensions, subscripts)];
|
|
};
|
|
|
|
this.set = function set(subscripts, value) {
|
|
if (!array) {
|
|
this.dim(subscripts.map(function() { return 10; }));
|
|
}
|
|
|
|
array[offset(dimensions, subscripts)] = value;
|
|
};
|
|
|
|
this.toJSON = function toJSON() {
|
|
return { type: type, dimensions: dimensions, array: array };
|
|
};
|
|
|
|
if (dims) {
|
|
this.dim(dims);
|
|
}
|
|
}
|
|
|
|
// Stream API, for parsing source and INPUT entry
|
|
function Stream(string) {
|
|
this.line = 0;
|
|
this.column = 0;
|
|
|
|
this.match = function match(re) {
|
|
var m = string.match(re), lines;
|
|
if (m) {
|
|
string = string.substring(m[0].length);
|
|
lines = m[0].split('\n');
|
|
if (lines.length > 1) {
|
|
this.line += lines.length - 1;
|
|
this.column = lines[lines.length - 1].length;
|
|
} else {
|
|
this.column += m[0].length;
|
|
}
|
|
|
|
this.lastMatch = m;
|
|
return m;
|
|
}
|
|
return (void 0);
|
|
};
|
|
|
|
this.eof = function eof() {
|
|
return string.length === 0;
|
|
};
|
|
}
|
|
|
|
// Used for DATA (compile-time) and INPUT (runtime)
|
|
// Parses source string for optionally quoted, comma-
|
|
// separated values and adds them to items. Returns
|
|
// the substring consumed.
|
|
var parseDataInput = (function() {
|
|
|
|
var regexWhitespace = /^[ \t]+/,
|
|
regexQuotedString = /^"([^"]*?)(?:"|(?=\n|\r|$))/,
|
|
regexUnquotedString = /^[^:,\r\n]*/,
|
|
regexComma = /^,/;
|
|
|
|
return function parseDataInput(stream, items) {
|
|
|
|
do {
|
|
stream.match(regexWhitespace);
|
|
|
|
if (stream.match(regexQuotedString)) {
|
|
// quoted string
|
|
items.push(stream.lastMatch[1]);
|
|
} else if (stream.match(regexUnquotedString)) {
|
|
// unquoted string
|
|
items.push(stream.lastMatch[0]);
|
|
}
|
|
} while (stream.match(regexComma));
|
|
};
|
|
} ());
|
|
|
|
|
|
basic.compile = function compile(source) {
|
|
"use strict";
|
|
|
|
function vartype(name) {
|
|
var s = name.charAt(name.length - 1);
|
|
return s === '$' ? 'string' : s === '%' ? 'int' : 'float';
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
//
|
|
// Runtime Environment (bound to compiled program)
|
|
//
|
|
//----------------------------------------------------------------------
|
|
|
|
var env, // Environment - passed in to program, contains tty, graphics, etc.
|
|
state, // Program state - initialized at runtime
|
|
lib, // Statement Library (closure over state and env)
|
|
funlib, // Function Library (closure over state and env)
|
|
peek_table, // Native memory access shims (PEEK, POKE, CALL)
|
|
poke_table,
|
|
call_table;
|
|
|
|
//
|
|
// NOTE: tempting to make these part of env but some access/modify program state,
|
|
// e.g. onerr
|
|
//
|
|
peek_table = {
|
|
// Text window
|
|
0x0020: function() { return env.tty.textWindow ? env.tty.textWindow.left : 0; },
|
|
0x0021: function() { return env.tty.textWindow ? env.tty.textWindow.width : 80; },
|
|
0x0022: function() { return env.tty.textWindow ? env.tty.textWindow.top : 0; },
|
|
0x0023: function() { return env.tty.textWindow ? env.tty.textWindow.top + env.tty.textWindow.height : 24; },
|
|
0x0024: function() { return env.tty.getCursorPosition().x; },
|
|
0x0025: function() { return env.tty.getCursorPosition().y; },
|
|
|
|
// Random number field
|
|
0x004e: function() { return (Math.random() * 256) & 0xff; },
|
|
0x004f: function() { return (Math.random() * 256) & 0xff; },
|
|
|
|
// Last error code
|
|
0x00de: function() { return state.onerr_code; },
|
|
|
|
// Hires Plotting Page (32=1, 64=2, 96=3)
|
|
0x00e6: function() { return env.display ? (env.display.hires_plotting_page === 2 ? 64 : 32) : 0; },
|
|
|
|
// TODO: 0x3D0 = 0x4C if DOS is present.
|
|
|
|
// Keyboard
|
|
0xC000: function() { return env.tty.getKeyboardRegister ? env.tty.getKeyboardRegister() : 0; },
|
|
0xC010: function() { return env.tty.clearKeyboardStrobe ? env.tty.clearKeyboardStrobe() : 0; },
|
|
|
|
// Speaker toggle
|
|
0xC030: function() { return 0; },
|
|
|
|
// Buttons
|
|
0xC060: function() { return env.tty.getButtonState ? env.tty.getButtonState(3) : 0; },
|
|
0xC061: function() { return env.tty.getButtonState ? env.tty.getButtonState(0) : 0; },
|
|
0xC062: function() { return env.tty.getButtonState ? env.tty.getButtonState(1) : 0; },
|
|
0xC063: function() { return env.tty.getButtonState ? env.tty.getButtonState(2) : 0; },
|
|
|
|
// Graphics State
|
|
0xC01A: function() { return (env.display && !env.display.getState().graphics) * 128; },
|
|
0xC01B: function() { return (env.display && !env.display.getState().full) * 128; },
|
|
0xC01C: function() { return (env.display && !env.display.getState().page1) * 128; },
|
|
0xC01D: function() { return (env.display && !env.display.getState().lores) * 128; },
|
|
0xC01E: function() { return (env.tty.isAltCharset && env.tty.isAltCharset()) * 128; },
|
|
0xC01F: function() { return (env.tty.isFirmwareActive && env.tty.isFirmwareActive()) * 128; }
|
|
};
|
|
|
|
poke_table = {
|
|
// Text window
|
|
0x0020: function(v) { if (env.tty.textWindow) { env.tty.textWindow.left = v; } },
|
|
0x0021: function(v) { if (env.tty.textWindow) { env.tty.textWindow.width = v; } },
|
|
0x0022: function(v) { if (env.tty.textWindow) {
|
|
var bottom = env.tty.textWindow.top + env.tty.textWindow.height;
|
|
env.tty.textWindow.top = v;
|
|
env.tty.textWindow.height = bottom - env.tty.textWindow.top;
|
|
} },
|
|
0x0023: function(v) { if (env.tty.textWindow) { env.tty.textWindow.height = v - env.tty.textWindow.top; } },
|
|
0x0024: function(v) { env.tty.setCursorPosition(v, void 0); },
|
|
0x0025: function(v) { env.tty.setCursorPosition(void 0, v); },
|
|
|
|
// ONERR flag
|
|
0x00D8: function(v) { if (v < 0x80) { state.onerr_handler = (void 0); } },
|
|
|
|
// Hires Plotting Page (32=1, 64=2, 96=3)
|
|
0x00E6: function(v) { if (env.display) { env.display.hires_plotting_page = (v === 64 ? 2 : 1); } },
|
|
|
|
// Keyboard strobe
|
|
0xC010: function() { if (env.tty.clearKeyboardStrobe) { env.tty.clearKeyboardStrobe(); } },
|
|
|
|
// Display switches
|
|
0xC050: function() { if (env.display) { env.display.setState("graphics", true); } }, // Graphics
|
|
0xC051: function() { if (env.display) { env.display.setState("graphics", false); } }, // Text
|
|
0xC052: function() { if (env.display) { env.display.setState("full", true); } }, // Full Graphics
|
|
0xC053: function() { if (env.display) { env.display.setState("full", false); } }, // Split Screen
|
|
0xC054: function() { if (env.display) { env.display.setState("page1", true); } }, // Page 1
|
|
0xC055: function() { if (env.display) { env.display.setState("page1", false); } }, // Page 2
|
|
0xC056: function() { if (env.display) { env.display.setState("lores", true); } }, // Lo-Res
|
|
0xC057: function() { if (env.display) { env.display.setState("lores", false); } }, // Hi-Res
|
|
|
|
// Speaker toggle
|
|
0xC030: function() { } // no-op
|
|
};
|
|
|
|
call_table = {
|
|
0xD683: function() { // Clear stack
|
|
state.stack = [];
|
|
},
|
|
0xF328: function() { // Pop error entry off stack
|
|
var stack_record = state.stack.pop();
|
|
if (!{}.hasOwnProperty.call(stack_record, 'resume_stmt_index')) {
|
|
runtime_error(ERRORS.SYNTAX_ERROR);
|
|
return;
|
|
}
|
|
},
|
|
0xF3E4: function() { // Reveal hi-res page 1
|
|
if (!env.hires) { runtime_error('Hires graphics not supported'); }
|
|
env.display.setState('graphics', true, 'full', true, 'page1', true, 'lores', false);
|
|
},
|
|
0xF3F2: function() { // Clear hi-res screen to black
|
|
var hires = env.display.hires_plotting_page === 2 ? env.hires2 : env.hires;
|
|
if (!hires) { runtime_error('Hires graphics not supported'); }
|
|
hires.clear();
|
|
},
|
|
0xF3F6: function() { // Clear hi-res screen to last color Hplotted
|
|
var hires = env.display.hires_plotting_page === 2 ? env.hires2 : env.hires;
|
|
if (!hires) { runtime_error('Hires graphics not supported'); }
|
|
hires.clear(hires.color);
|
|
},
|
|
0xFBF4: function() { // Move cursor right
|
|
if (env.tty.cursorRight) { env.tty.cursorRight(); }
|
|
},
|
|
0xFC10: function() { // Move cursor left
|
|
if (env.tty.cursorLeft) { env.tty.cursorLeft(); }
|
|
},
|
|
0xFC1A: function() { // Move cursor up
|
|
if (env.tty.cursorUp) { env.tty.cursorUp(); }
|
|
},
|
|
0xFC42: function() { // Clear text from cursor to bottom
|
|
if (env.tty.clearEOS) { env.tty.clearEOS(); }
|
|
},
|
|
0xFC66: function() { // Move cursor down
|
|
if (env.tty.cursorDown) { env.tty.cursorDown(); }
|
|
},
|
|
0xFC9C: function() { // Clear from cursor to right
|
|
if (env.tty.clearEOL) { env.tty.clearEOL(); }
|
|
},
|
|
0xFD0C: function() { // Wait for key press
|
|
throw new BlockingInput(env.tty.readChar, function(_){});
|
|
},
|
|
0xFE84: function() { // Normal
|
|
if (env.tty.setTextStyle) { env.tty.setTextStyle(env.tty.TEXT_STYLE_NORMAL); }
|
|
},
|
|
0xFE80: function() { // Inverse
|
|
if (env.tty.setTextStyle) { env.tty.setTextStyle(env.tty.TEXT_STYLE_INVERSE); }
|
|
}
|
|
};
|
|
|
|
lib = {
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Variable Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'clear': function CLEAR() {
|
|
state.clear();
|
|
},
|
|
|
|
'dim': function DIM(name, subscripts) {
|
|
state.arrays[name].dim(subscripts);
|
|
},
|
|
|
|
'def': function DEF(name, func) {
|
|
state.functions[name] = func;
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Flow Control Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'goto': function GOTO(line) {
|
|
throw new GoToLine(line);
|
|
},
|
|
|
|
'on_goto': function ON_GOTO(index /* , ...lines */) {
|
|
index = Math.floor(index);
|
|
if (index < 0 || index > 255) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
--index;
|
|
var lines = Array.prototype.slice.call(arguments, 1);
|
|
|
|
if (index >= 0 && index < lines.length) {
|
|
throw new GoToLine(lines[index]);
|
|
}
|
|
},
|
|
|
|
'gosub': function GOSUB(line) {
|
|
state.stack.push({
|
|
gosub_return: state.stmt_index,
|
|
line_number: state.line_number
|
|
});
|
|
throw new GoToLine(line);
|
|
},
|
|
|
|
'on_gosub': function ON_GOSUB(index /* , ...lines */) {
|
|
index = Math.floor(index);
|
|
if (index < 0 || index > 255) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
--index;
|
|
var lines = Array.prototype.slice.call(arguments, 1);
|
|
if (index >= 0 && index < lines.length) {
|
|
state.stack.push({
|
|
gosub_return: state.stmt_index,
|
|
line_number: state.line_number
|
|
});
|
|
throw new GoToLine(lines[index]);
|
|
}
|
|
},
|
|
|
|
'return': function RETURN() {
|
|
var stack_record;
|
|
while (state.stack.length) {
|
|
stack_record = state.stack.pop();
|
|
if ({}.hasOwnProperty.call(stack_record, 'gosub_return')) {
|
|
state.stmt_index = stack_record.gosub_return;
|
|
state.line_number = stack_record.line_number;
|
|
return;
|
|
}
|
|
}
|
|
runtime_error(ERRORS.RETURN_WITHOUT_GOSUB);
|
|
},
|
|
|
|
'pop': function POP() {
|
|
var stack_record = state.stack.pop();
|
|
if (!{}.hasOwnProperty.call(stack_record, 'gosub_return')) {
|
|
runtime_error(ERRORS.RETURN_WITHOUT_GOSUB);
|
|
return;
|
|
}
|
|
},
|
|
|
|
'for': function FOR(varname, to, step) {
|
|
state.stack.push({
|
|
index: varname,
|
|
to: to,
|
|
step: step,
|
|
for_next: state.stmt_index,
|
|
line_number: state.line_number
|
|
});
|
|
},
|
|
|
|
'next': function NEXT(/* ...varnames */) {
|
|
var varnames = Array.prototype.slice.call(arguments),
|
|
varname, stack_record, value;
|
|
do {
|
|
varname = varnames.shift();
|
|
|
|
do {
|
|
stack_record = state.stack.pop();
|
|
if (!stack_record || !{}.hasOwnProperty.call(stack_record, 'for_next')) {
|
|
runtime_error(ERRORS.NEXT_WITHOUT_FOR);
|
|
}
|
|
} while (varname !== (void 0) && stack_record.index !== varname);
|
|
|
|
value = state.variables[stack_record.index];
|
|
|
|
value = value + stack_record.step;
|
|
state.variables[stack_record.index] = value;
|
|
|
|
if (!(stack_record.step > 0 && value > stack_record.to) &&
|
|
!(stack_record.step < 0 && value < stack_record.to) &&
|
|
!(stack_record.step === 0 && value === stack_record.to)) {
|
|
state.stack.push(stack_record);
|
|
state.stmt_index = stack_record.for_next;
|
|
state.line_number = stack_record.line_number;
|
|
return;
|
|
}
|
|
} while (varnames.length);
|
|
},
|
|
|
|
'if': function IF(value) {
|
|
if (!value) {
|
|
throw new NextLine();
|
|
}
|
|
},
|
|
|
|
'stop': function STOP() {
|
|
runtime_error(ERRORS.INTERRUPT);
|
|
},
|
|
|
|
'end': function END() {
|
|
throw new EndProgram();
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Error Handling Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'onerr_goto': function ONERR_GOTO(line) {
|
|
state.onerr_handler = line;
|
|
throw new NextLine();
|
|
},
|
|
|
|
'resume': function RESUME() {
|
|
var stack_record = state.stack.pop();
|
|
if (!{}.hasOwnProperty.call(stack_record, 'resume_stmt_index')) {
|
|
runtime_error(ERRORS.SYNTAX_ERROR);
|
|
return;
|
|
}
|
|
state.line_number = stack_record.resume_line_number;
|
|
state.stmt_index = stack_record.resume_stmt_index;
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Inline Data Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'restore': function RESTORE() {
|
|
state.data_index = 0;
|
|
},
|
|
|
|
// PERF: optimize by turning into a function, e.g. "state.parsevar(name, lib.read())"
|
|
'read': function READ(/* ...lvalues */) {
|
|
var lvalues = Array.prototype.slice.call(arguments);
|
|
while (lvalues.length) {
|
|
if (state.data_index >= state.data.length) {
|
|
runtime_error(ERRORS.OUT_OF_DATA);
|
|
}
|
|
(lvalues.shift())(state.data[state.data_index]);
|
|
state.data_index += 1;
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// I/O Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'print': function PRINT(/* ...strings */) {
|
|
var args = Array.prototype.slice.call(arguments), arg;
|
|
while (args.length) {
|
|
arg = args.shift();
|
|
if (typeof arg === 'function') {
|
|
arg = arg();
|
|
}
|
|
env.tty.writeString(String(arg));
|
|
}
|
|
},
|
|
|
|
'comma': function COMMA() {
|
|
return function() {
|
|
var cur = env.tty.getCursorPosition().x,
|
|
pos = (cur + 16) - (cur % 16);
|
|
if (pos >= env.tty.getScreenSize().width) {
|
|
return '\r';
|
|
} else {
|
|
return ' '.repeat(pos - cur);
|
|
}
|
|
};
|
|
},
|
|
|
|
'spc': function SPC(n) {
|
|
n = n >> 0;
|
|
if (n < 0 || n > 255) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
return function() {
|
|
return ' '.repeat(n);
|
|
};
|
|
},
|
|
|
|
'tab': function TAB(n) {
|
|
n = n >> 0;
|
|
if (n < 0 || n > 255) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
if (n === 0) { n = 256; }
|
|
|
|
return function() {
|
|
var pos = env.tty.getCursorPosition().x + 1;
|
|
return ' '.repeat(pos >= n ? 0 : n - pos);
|
|
};
|
|
},
|
|
|
|
'get': function GET(lvalue) {
|
|
throw new BlockingInput(env.tty.readChar, function(entry) { lvalue(entry); });
|
|
},
|
|
|
|
'input': function INPUT(prompt /* , ...varlist */) {
|
|
var varlist = Array.prototype.slice.call(arguments, 1); // copy for closure
|
|
var im = function(cb) { return env.tty.readLine(cb, prompt); };
|
|
var ih = function(entry) {
|
|
var parts = [],
|
|
stream = new Stream(entry);
|
|
|
|
parseDataInput(stream, parts);
|
|
|
|
while (varlist.length && parts.length) {
|
|
try {
|
|
varlist.shift()(parts.shift());
|
|
} catch (e) {
|
|
if (e instanceof basic.RuntimeError &&
|
|
e.code === ERRORS.TYPE_MISMATCH[0]) {
|
|
e.code = ERRORS.REENTER[0];
|
|
e.message = ERRORS.REENTER[1];
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
if (varlist.length) {
|
|
prompt = '??';
|
|
throw new BlockingInput(im, ih);
|
|
}
|
|
|
|
if (parts.length) {
|
|
env.tty.writeString('?EXTRA IGNORED\r');
|
|
}
|
|
};
|
|
throw new BlockingInput(im, ih);
|
|
},
|
|
|
|
'home': function HOME() {
|
|
if (env.tty.clearScreen) { env.tty.clearScreen(); }
|
|
},
|
|
|
|
'htab': function HTAB(pos) {
|
|
if (pos < 1 || pos >= env.tty.getScreenSize().width + 1) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
if (env.tty.textWindow) {
|
|
pos += env.tty.textWindow.left;
|
|
}
|
|
|
|
env.tty.setCursorPosition(pos - 1, void 0);
|
|
},
|
|
|
|
'vtab': function VTAB(pos) {
|
|
if (pos < 1 || pos >= env.tty.getScreenSize().height + 1) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
env.tty.setCursorPosition(void 0, pos - 1);
|
|
},
|
|
|
|
'inverse': function INVERSE() {
|
|
if (env.tty.setTextStyle) { env.tty.setTextStyle(env.tty.TEXT_STYLE_INVERSE); }
|
|
},
|
|
'flash': function FLASH() {
|
|
if (env.tty.setTextStyle) { env.tty.setTextStyle(env.tty.TEXT_STYLE_FLASH); }
|
|
},
|
|
'normal': function NORMAL() {
|
|
if (env.tty.setTextStyle) { env.tty.setTextStyle(env.tty.TEXT_STYLE_NORMAL); }
|
|
},
|
|
'text': function TEXT() {
|
|
if (env.display) {
|
|
env.display.setState("graphics", false);
|
|
}
|
|
|
|
if (env.tty.textWindow) {
|
|
// Reset text window
|
|
env.tty.textWindow = {
|
|
left: 0,
|
|
top: 0,
|
|
width: env.tty.getScreenSize().width,
|
|
height: env.tty.getScreenSize().height
|
|
};
|
|
}
|
|
env.tty.setCursorPosition(0, env.tty.getScreenSize().height - 1);
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Miscellaneous Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'notrace': function NOTRACE() {
|
|
state.trace_mode = false;
|
|
},
|
|
'trace': function TRACE() {
|
|
state.trace_mode = true;
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Lores Graphics
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'gr': function GR() {
|
|
if (!env.lores) { runtime_error('Lores graphics not supported'); }
|
|
env.display.setState("lores", true, "full", false, "graphics", true);
|
|
env.lores.clear();
|
|
|
|
if (env.tty.textWindow) {
|
|
env.tty.textWindow.left = 0;
|
|
env.tty.textWindow.width = env.tty.getScreenSize().width;
|
|
env.tty.textWindow.top = env.tty.getScreenSize().height - 4;
|
|
env.tty.textWindow.height = 4;
|
|
}
|
|
|
|
env.tty.setCursorPosition(0, env.tty.getScreenSize().height);
|
|
},
|
|
|
|
'color': function COLOR(n) {
|
|
if (!env.lores) { runtime_error('Lores graphics not supported'); }
|
|
|
|
n = n >> 0;
|
|
if (n < 0 || n > 255) { runtime_error(ERRORS.ILLEGAL_QUANTITY); }
|
|
|
|
env.lores.setColor(n);
|
|
},
|
|
|
|
'plot': function PLOT(x, y) {
|
|
if (!env.lores) { runtime_error('Lores graphics not supported'); }
|
|
|
|
x = x >> 0;
|
|
y = y >> 0;
|
|
|
|
var size = env.lores.getScreenSize();
|
|
if (x < 0 || y < 0 || x >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
env.lores.plot(x, y);
|
|
},
|
|
|
|
'hlin': function HLIN(x1, x2, y) {
|
|
if (!env.lores) { runtime_error('Lores graphics not supported'); }
|
|
|
|
x1 = x1 >> 0;
|
|
x2 = x2 >> 0;
|
|
y = y >> 0;
|
|
|
|
var size = env.lores.getScreenSize();
|
|
if (x1 < 0 || x2 < 0 || y < 0 ||
|
|
x1 >= size.width || x2 >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
env.lores.hlin(x1, x2, y);
|
|
},
|
|
|
|
'vlin': function VLIN(y1, y2, x) {
|
|
if (!env.lores) { runtime_error('Lores graphics not supported'); }
|
|
|
|
y1 = y1 >> 0;
|
|
y2 = y2 >> 0;
|
|
x = x >> 0;
|
|
|
|
var size = env.lores.getScreenSize();
|
|
if (x < 0 || y1 < 0 || y2 < 0 ||
|
|
x >= size.width || y1 >= size.height || y2 >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
env.lores.vlin(y1, y2, x);
|
|
},
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Hires Graphics
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
'hgr': function HGR() {
|
|
if (!env.hires) { runtime_error('Hires graphics not supported'); }
|
|
env.display.setState("lores", false, "full", false, "page1", true, "graphics", true);
|
|
env.display.hires_plotting_page = 1;
|
|
env.hires.clear();
|
|
},
|
|
|
|
'hgr2': function HGR2() {
|
|
if (!env.hires) { runtime_error('Hires graphics not supported'); }
|
|
env.display.setState("lores", false, "full", true, "page1", false, "graphics", true);
|
|
env.display.hires_plotting_page = 2;
|
|
env.hires2.clear();
|
|
},
|
|
|
|
'hcolor': function HCOLOR(n) {
|
|
if (!env.hires) { runtime_error('Hires graphics not supported'); }
|
|
n = n >> 0;
|
|
if (n < 0 || n > 7) { runtime_error(ERRORS.ILLEGAL_QUANTITY); }
|
|
env.hires.setColor(n);
|
|
if (env.hires2) { env.hires2.setColor(n); }
|
|
},
|
|
|
|
'hplot': function HPLOT(/* ...coords */) {
|
|
var hires = env.display.hires_plotting_page === 2 ? env.hires2 : env.hires;
|
|
if (!hires) { runtime_error('Hires graphics not supported'); }
|
|
|
|
var coords = Array.prototype.slice.call(arguments),
|
|
size = hires.getScreenSize(),
|
|
x, y;
|
|
|
|
x = coords.shift() >> 0;
|
|
y = coords.shift() >> 0;
|
|
|
|
if (x < 0 || y < 0 || x >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
hires.plot(x, y);
|
|
while (coords.length) {
|
|
x = coords.shift() >> 0;
|
|
y = coords.shift() >> 0;
|
|
if (x < 0 || y < 0 || x >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
hires.plot_to(x, y);
|
|
}
|
|
},
|
|
|
|
'hplot_to': function HPLOT_TO(/* ...coords */) {
|
|
var hires = env.display.hires_plotting_page === 2 ? env.hires2 : env.hires;
|
|
if (!hires) { runtime_error('Hires graphics not supported'); }
|
|
|
|
var coords = Array.prototype.slice.call(arguments),
|
|
size = hires.getScreenSize(), x, y;
|
|
|
|
while (coords.length) {
|
|
x = coords.shift() >> 0;
|
|
y = coords.shift() >> 0;
|
|
|
|
if (x < 0 || y < 0 || x >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
hires.plot_to(x, y);
|
|
}
|
|
},
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Compatibility shims
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'pr#': function PR(slot) {
|
|
if (slot === 0) {
|
|
if (env.tty.setFirmwareActive) { env.tty.setFirmwareActive(false); }
|
|
} else if (slot === 3) {
|
|
if (env.tty.setFirmwareActive) { env.tty.setFirmwareActive(true); }
|
|
}
|
|
},
|
|
|
|
'poke': function POKE(address, value) {
|
|
address = address & 0xffff;
|
|
|
|
value = value >> 0;
|
|
if (value < 0 || value > 255) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
if (!({}.hasOwnProperty.call(poke_table, address))) {
|
|
runtime_error("Unsupported POKE location: " + address);
|
|
}
|
|
|
|
poke_table[address](value);
|
|
},
|
|
|
|
'call': function CALL(address) {
|
|
address = address & 0xffff;
|
|
|
|
if (!({}.hasOwnProperty.call(call_table, address))) {
|
|
runtime_error("Unsupported POKE location: " + address);
|
|
}
|
|
|
|
call_table[address]();
|
|
},
|
|
|
|
'speed': function SPEED(n) {
|
|
n = n >> 0;
|
|
if (n < 0 || n > 255) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
env.tty.speed = n;
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Referenced by compiled functions
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
'div': function div(n, d) {
|
|
var r = n / d;
|
|
if (!isFinite(r)) { runtime_error(ERRORS.DIVISION_BY_ZERO); }
|
|
return r;
|
|
},
|
|
|
|
'fn': function fn(name, arg) {
|
|
if (!{}.hasOwnProperty.call(state.functions, name)) {
|
|
runtime_error(ERRORS.UNDEFINED_FUNCTION);
|
|
}
|
|
return state.functions[name](arg);
|
|
},
|
|
|
|
'checkFinite': function checkFinite(n) {
|
|
if (!isFinite(n)) { runtime_error(ERRORS.OVERFLOW); }
|
|
return n;
|
|
},
|
|
|
|
'toint': function toint(n) {
|
|
n = n >> 0;
|
|
if (n > 0x7fff || n < -0x8000) { runtime_error(ERRORS.ILLEGAL_QUANTITY); }
|
|
return n;
|
|
}
|
|
};
|
|
|
|
// Apply a signature [return_type, arg0_type, arg1_type, ...] to a function
|
|
function funcsign(func /*, return_type, ...arg_types */) {
|
|
func.signature = Array.prototype.slice.call(arguments, 1);
|
|
return func;
|
|
}
|
|
|
|
funlib = {};
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Functions
|
|
//
|
|
// name: [ impl, returntype, [arg0type [, arg1type [, ... ] ]
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
funlib[kws.ABS] = funcsign(Math.abs, 'number', 'number');
|
|
funlib[kws.ASC] = funcsign(function(s) {
|
|
if (s.length < 1) { runtime_error(ERRORS.ILLEGAL_QUANTITY); }
|
|
return s.charCodeAt(0);
|
|
}, 'number', 'string');
|
|
funlib[kws.ATN] = funcsign(Math.atan, 'number', 'number');
|
|
funlib[kws.CHR$] = funcsign(String.fromCharCode, 'string', 'number');
|
|
funlib[kws.COS] = funcsign(Math.cos, 'number', 'number');
|
|
funlib[kws.EXP] = funcsign(Math.exp, 'number', 'number');
|
|
funlib[kws.INT] = funcsign(Math.floor, 'number', 'number');
|
|
funlib[kws.LEN] = funcsign(function LEN(s) { return s.length; }, 'number', 'string');
|
|
funlib[kws.LOG] = funcsign(Math.log, 'number', 'number');
|
|
funlib[kws.SGN] = funcsign(function SGN(n) { return n > 0 ? 1 : n < 0 ? -1 : 0; }, 'number', 'number');
|
|
funlib[kws.SIN] = funcsign(Math.sin, 'number', 'number');
|
|
funlib[kws.SQR] = funcsign(Math.sqrt, 'number', 'number');
|
|
funlib[kws.STR$] = funcsign(function STR$(n) { return n.toString(); }, 'string', 'number');
|
|
funlib[kws.TAN] = funcsign(Math.tan, 'number', 'number');
|
|
funlib[kws.VAL] = funcsign(function VAL(s) {
|
|
var n = parseFloat(s);
|
|
return isFinite(n) ? n : 0;
|
|
}, 'number', 'string');
|
|
|
|
funlib[kws.RND] = funcsign(function RND(n) {
|
|
if (n > 0) {
|
|
// Next in PRNG sequence
|
|
return state.prng.next();
|
|
} else if (n < 0) {
|
|
// Re-seed. NOTE: Not predictable as in Applesoft
|
|
state.prng.seed(n);
|
|
return state.prng.next();
|
|
}
|
|
return state.prng.last;
|
|
}, 'number', 'number');
|
|
|
|
funlib[kws.LEFT$] = funcsign(function LEFT$(s, n) { return s.substring(0, n); }, 'string', 'string', 'number');
|
|
funlib[kws.MID$] = funcsign(function MID$(s, n, n2) { return n2 === (void 0) ? s.substring(n - 1) : s.substring(n - 1, n + n2 - 1); }, 'string', 'string', 'number', 'number?');
|
|
funlib[kws.RIGHT$] = funcsign(function RIGHT$(s, n) { return s.length < n ? s : s.substring(s.length - n); }, 'string', 'string', 'number');
|
|
|
|
funlib[kws.POS] = funcsign(function POS(n) { return env.tty.getCursorPosition().x; }, 'number', 'number');
|
|
funlib[kws.SCRN] = funcsign(function SCRN(x, y) {
|
|
if (!env.lores) { runtime_error("Graphics not supported"); }
|
|
x = x >> 0;
|
|
y = y >> 0;
|
|
var size = env.lores.getScreenSize();
|
|
if (x < 0 || y < 0 || x >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
return env.lores.getPixel(x, y);
|
|
}, 'number', 'number', 'number');
|
|
funlib[kws.HSCRN] = funcsign(function HSCRN(x, y) {
|
|
var hires = env.display.hires_plotting_page === 2 ? env.hires2 : env.hires;
|
|
if (!hires) { runtime_error("Graphics not supported"); }
|
|
|
|
x = x >> 0;
|
|
y = y >> 0;
|
|
var size = hires.getScreenSize();
|
|
if (x < 0 || y < 0 || x >= size.width || y >= size.height) {
|
|
runtime_error(ERRORS.ILLEGAL_QUANTITY);
|
|
}
|
|
|
|
return hires.getPixel(x, y);
|
|
}, 'number', 'number', 'number');
|
|
|
|
funlib[kws.PDL] = funcsign(function PDL(n) {
|
|
if (env.paddle) {
|
|
return (env.paddle(n) * 255) & 0xff;
|
|
} else {
|
|
return runtime_error('Paddles not attached');
|
|
}
|
|
}, 'number', 'number');
|
|
funlib[kws.FRE] = funcsign(function FRE(n) {
|
|
return JSON ? JSON.stringify([state.variables, state.arrays, state.functions]).length : 0;
|
|
}, 'number', 'number');
|
|
funlib[kws.PEEK] = funcsign(function PEEK(address) {
|
|
address = address & 0xffff;
|
|
if (!{}.hasOwnProperty.call(peek_table, address)) {
|
|
runtime_error("Unsupported PEEK location: " + address);
|
|
}
|
|
return peek_table[address]();
|
|
}, 'number', 'number');
|
|
|
|
// Not supported
|
|
funlib[kws.USR] = funcsign(function USR(n) { runtime_error("USR Function not supported"); }, 'number', 'number');
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
//
|
|
// Parser / Compiler
|
|
//
|
|
//----------------------------------------------------------------------
|
|
|
|
var program = (function() {
|
|
|
|
var identifiers = {
|
|
variables: {},
|
|
arrays: {}
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Lexical Analysis
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
var match, test, endOfStatement, endOfProgram,
|
|
currLine = 0, currColumn = 0,
|
|
currLineNumber = 0;
|
|
|
|
function parse_error(msg) {
|
|
return new basic.ParseError(msg + " in line " + currLineNumber,
|
|
currLine, currColumn);
|
|
}
|
|
|
|
|
|
(function(source) {
|
|
function munge(kw) {
|
|
// Escape special characters
|
|
function escape(c) { return (/[\[\]\\\^\$\.\|\?\*\+\(\)]/).test(c) ? '\\' + c : c; }
|
|
// Allow linear whitespace between characters
|
|
//return kw.split('').map(escape).join('[ \\t]*');
|
|
|
|
// Allow linear whitespace in HCOLOR=, HIMEM:, CHR$, etc
|
|
return kw.split(/(?=\W)/).map(escape).join('[ \\t]*');
|
|
}
|
|
|
|
var RESERVED_WORDS = [
|
|
kws.ABS, kws.AND, kws.ASC, kws.ATN, kws.AT, kws.CALL,
|
|
kws.CHR$, kws.CLEAR, kws.COLOR, kws.CONT, kws.COS,
|
|
/*kws.DATA,*/ kws.DEF, kws.DEL, kws.DIM, kws.DRAW, kws.END,
|
|
kws.EXP, kws.FLASH, kws.FN, kws.FOR, kws.FRE, kws.GET,
|
|
kws.GOSUB, kws.GOTO, kws.GR, kws.HCOLOR, kws.HGR2, kws.HGR,
|
|
kws.HIMEM, kws.HLIN, kws.HOME, kws.HPLOT, kws.HTAB, kws.IF,
|
|
kws.IN, kws.INPUT, kws.INT, kws.INVERSE, kws.LEFT$, kws.LEN,
|
|
kws.LET, kws.LIST, kws.LOAD, kws.LOG, kws.LOMEM, kws.MID$,
|
|
kws.NEW, kws.NEXT, kws.NORMAL, kws.NOTRACE, kws.NOT,
|
|
kws.ONERR, kws.ON, kws.OR, kws.PDL, kws.PEEK, kws.PLOT,
|
|
kws.POKE, kws.POP, kws.POS, kws.PRINT, kws.PR, kws.READ,
|
|
kws.RECALL, /*kws.REM,*/ kws.RESTORE, kws.RESUME,
|
|
kws.RETURN, kws.RIGHT$, kws.RND, kws.ROT, kws.RUN, kws.SAVE,
|
|
kws.SCALE, kws.SCRN, kws.SGN, kws.SHLOAD, kws.SIN, kws.SPC,
|
|
kws.SPEED, kws.SQR, kws.STEP, kws.STOP, kws.STORE, kws.STR$,
|
|
kws.TAB, kws.TAN, kws.TEXT, kws.THEN, kws.TO, kws.TRACE,
|
|
kws.USR, kws.VAL, kws.VLIN, kws.VTAB, kws.WAIT, kws.XDRAW,
|
|
kws.AMPERSAND, kws.QUESTION, kws.HSCRN
|
|
];
|
|
// NOTE: keywords that are stems of other words need to go after (e.g. "NOTRACE", "NOT)
|
|
RESERVED_WORDS.sort();
|
|
RESERVED_WORDS.reverse();
|
|
|
|
var regexReservedWords = new RegExp("^(" + RESERVED_WORDS.map(munge).join("|") + ")", "i"),
|
|
regexIdentifier = /^([A-Za-z][A-Za-z0-9]?)[A-Za-z0-9]*(\$|%)?/,
|
|
regexStringLiteral = /^"([^"]*?)(?:"|(?=\n|\r|$))/,
|
|
regexNumberLiteral = /^[0-9]*\.?[0-9]+(?:[eE]\s*[\-+]?\s*[0-9]+)?/,
|
|
regexHexLiteral = /^\$[0-9A-Fa-f]+/,
|
|
regexOperator = /^[;=<>+\-*\/\^(),]/,
|
|
|
|
regexLineNumber = /^[0-9]+/,
|
|
regexSeparator = /^:/,
|
|
|
|
regexRemark = new RegExp('^(' + munge(kws.REM) + '([^\r\n]*))', 'i'),
|
|
regexData = new RegExp('^(' + munge(kws.DATA) + ')', 'i'),
|
|
|
|
regexLinearWhitespace = /^[ \t]+/,
|
|
regexNewline = /^\r?\n/;
|
|
|
|
// Token types:
|
|
// lineNumber - start of a new line
|
|
// separator - separates statements on same line
|
|
// reserved - reserved keyword (command, function, etc)
|
|
// identifier - variable name
|
|
// string - string literal
|
|
// number - number literal
|
|
// operator - operator
|
|
// remark - REM blah
|
|
// data - DATA blah,"blah",blah
|
|
|
|
var start = true,
|
|
stream = new Stream(source);
|
|
|
|
function nextToken() {
|
|
var token = {}, newline = start, ws;
|
|
start = false;
|
|
|
|
currLine = stream.line + 1;
|
|
currColumn = stream.column + 1;
|
|
|
|
// Consume whitespace
|
|
do {
|
|
ws = false;
|
|
if (stream.match(regexLinearWhitespace)) {
|
|
ws = true;
|
|
} else if (stream.match(regexNewline)) {
|
|
ws = true;
|
|
newline = true;
|
|
}
|
|
} while (ws);
|
|
|
|
if (stream.eof()) {
|
|
return (void 0);
|
|
}
|
|
|
|
if (newline) {
|
|
if (stream.match(regexLineNumber)) {
|
|
token.lineNumber = Number(stream.lastMatch[0]);
|
|
} else if (stream.match(regexSeparator)) {
|
|
// Extension - allow leading : to continue previous line
|
|
token.separator = stream.lastMatch[0];
|
|
} else {
|
|
throw parse_error("Syntax error: Expected line number or separator");
|
|
}
|
|
} else if (stream.match(regexRemark)) {
|
|
token.remark = stream.lastMatch[2];
|
|
} else if (stream.match(regexData)) {
|
|
token.data = [];
|
|
parseDataInput(stream, token.data);
|
|
} else if (stream.match(regexReservedWords)) {
|
|
token.reserved = stream.lastMatch[1].toUpperCase().replace(/\s+/g, '');
|
|
if (token.reserved === kws.QUESTION) { token.reserved = kws.PRINT; } // HACK
|
|
} else if (stream.match(regexIdentifier)) {
|
|
token.identifier = stream.lastMatch[1].toUpperCase() + (stream.lastMatch[2] || ''); // Canonicalize identifier name
|
|
} else if (stream.match(regexStringLiteral)) {
|
|
token.string = stream.lastMatch[1];
|
|
} else if (stream.match(regexNumberLiteral)) {
|
|
token.number = parseFloat(stream.lastMatch[0].replace(/\s+/g, ''));
|
|
} else if (stream.match(regexHexLiteral)) {
|
|
token.number = parseInt(stream.lastMatch[0].substring(1), 16);
|
|
} else if (stream.match(regexOperator)) {
|
|
token.operator = stream.lastMatch[0].replace(/\s+/g, '');
|
|
} else if (stream.match(regexSeparator)) {
|
|
token.separator = stream.lastMatch[0];
|
|
} else {
|
|
throw parse_error("Syntax error: Unexpected '" + source.substr(0, 40) + "'");
|
|
}
|
|
return token;
|
|
}
|
|
|
|
var lookahead = nextToken();
|
|
|
|
match = function match(type, value) {
|
|
|
|
if (!lookahead) {
|
|
throw parse_error("Syntax error: Expected " + type + ", saw end of file");
|
|
}
|
|
|
|
var token = lookahead;
|
|
if ('lineNumber' in token) {
|
|
currLineNumber = token.lineNumber;
|
|
}
|
|
lookahead = nextToken();
|
|
|
|
if (!{}.hasOwnProperty.call(token, type)) {
|
|
throw parse_error("Syntax error: Expected " + type + ", saw " + JSON.stringify(token));
|
|
}
|
|
|
|
if (value !== (void 0) && token[type] !== value) {
|
|
throw parse_error("Syntax error: Expected '" + value + "', saw " + JSON.stringify(token));
|
|
}
|
|
|
|
return token[type];
|
|
};
|
|
|
|
test = function test(type, value, consume) {
|
|
if (lookahead && {}.hasOwnProperty.call(lookahead, type) &&
|
|
(value === (void 0) || lookahead[type] === value)) {
|
|
|
|
if (consume) {
|
|
var token = lookahead;
|
|
if ('lineNumber' in token) {
|
|
currLineNumber = token.lineNumber;
|
|
}
|
|
lookahead = nextToken();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
endOfStatement = function endOfStatement() {
|
|
return !lookahead ||
|
|
{}.hasOwnProperty.call(lookahead, 'separator') ||
|
|
{}.hasOwnProperty.call(lookahead, 'lineNumber');
|
|
};
|
|
|
|
endOfProgram = function endOfProgram() {
|
|
return !lookahead;
|
|
};
|
|
|
|
} (source));
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Compiler utility functions
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
function quote(string) {
|
|
return JSON.stringify(string);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Recursive Descent Parser
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
var parseExpression, parseSubscripts;
|
|
|
|
//
|
|
// Type Checking
|
|
//
|
|
|
|
function parseAnyExpression() {
|
|
var expr = parseExpression();
|
|
return expr.source;
|
|
}
|
|
|
|
function enforce_type(actual, expected) {
|
|
if (actual !== expected) {
|
|
throw parse_error('Type mismatch error: Expected ' + expected);
|
|
}
|
|
}
|
|
|
|
function parseStringExpression() {
|
|
var expr = parseExpression();
|
|
enforce_type(expr.type, 'string');
|
|
return expr.source;
|
|
}
|
|
|
|
function parseNumericExpression() {
|
|
var expr = parseExpression();
|
|
enforce_type(expr.type, 'number');
|
|
return expr.source;
|
|
}
|
|
|
|
//
|
|
// Variables
|
|
//
|
|
|
|
parseSubscripts = function() {
|
|
var subscripts; // undefined = no subscripts
|
|
|
|
if (test('operator', '(', true)) {
|
|
|
|
subscripts = [];
|
|
|
|
do {
|
|
subscripts.push(parseNumericExpression());
|
|
} while (test('operator', ',', true));
|
|
|
|
match("operator", ")");
|
|
|
|
return subscripts.join(',');
|
|
}
|
|
return (void 0);
|
|
};
|
|
|
|
function parsePValue() {
|
|
var name = match('identifier'),
|
|
subscripts = parseSubscripts();
|
|
|
|
if (subscripts) {
|
|
identifiers.arrays[name] = true;
|
|
return '(function (value){state.parsevar(' +
|
|
quote(name) + ',[' + subscripts + '],value);})';
|
|
} else {
|
|
identifiers.variables[name] = true;
|
|
return '(function (value){state.parsevar(' +
|
|
quote(name) + ',value);})';
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Expressions
|
|
//
|
|
|
|
parseExpression = (function() {
|
|
// closure to keep things tidy
|
|
|
|
function parseUserfunction() {
|
|
var name = match('identifier'),
|
|
type = vartype(name) === 'string' ? 'string' : 'number',
|
|
expr;
|
|
|
|
// FUTURE: Allow differing argument type and return type
|
|
// (may require runtime type checks)
|
|
|
|
// Determine the function argument
|
|
match("operator", "(");
|
|
expr = type === 'string' ? parseStringExpression() : parseNumericExpression();
|
|
match("operator", ")");
|
|
|
|
return { source: 'lib.fn(' + quote(name) + ',' + expr + ')', type: type };
|
|
}
|
|
|
|
|
|
function parsefunction(name) {
|
|
if (!{}.hasOwnProperty.call(funlib, name)) {
|
|
throw parse_error("Undefined function: " + name);
|
|
}
|
|
|
|
match("operator", "(");
|
|
|
|
var func = funlib[name],
|
|
funcdesc = func.signature.slice(),
|
|
rtype = funcdesc.shift(),
|
|
args = [],
|
|
atype;
|
|
|
|
while (funcdesc.length) {
|
|
atype = funcdesc.shift();
|
|
|
|
if (/\?$/.test(atype)) {
|
|
if (test('operator', ')')) {
|
|
break;
|
|
} else {
|
|
atype = atype.substring(0, atype.length - 1);
|
|
}
|
|
}
|
|
if (args.length) {
|
|
match("operator", ",");
|
|
}
|
|
|
|
if (atype === 'string') {
|
|
args.push(parseStringExpression());
|
|
} else if (atype === 'number') {
|
|
args.push(parseNumericExpression());
|
|
} else {
|
|
throw new Error("Invalid function definition");
|
|
}
|
|
}
|
|
|
|
match("operator", ")");
|
|
|
|
return { source: 'funlib.' + name + '(' + args.join(',') + ')', type: rtype };
|
|
}
|
|
|
|
function parseFinalExpression() {
|
|
if (test('number')) {
|
|
return { source: String(match('number')), type: 'number' };
|
|
} else if (test('string')) {
|
|
return { source: quote(match('string')), type: 'string' };
|
|
} else if (test('reserved', kws.FN, true)) {
|
|
return parseUserfunction();
|
|
} else if (test('reserved')) {
|
|
return parsefunction(match('reserved'));
|
|
} else if (test('identifier')) {
|
|
var name = match('identifier'),
|
|
type = vartype(name) === 'string' ? 'string' : 'number',
|
|
subscripts = parseSubscripts();
|
|
if (subscripts) {
|
|
identifiers.arrays[name] = true;
|
|
return { source: 'state.arrays[' + quote(name) + '].get([' + subscripts + '])', type: type };
|
|
} else {
|
|
identifiers.variables[name] = true;
|
|
return { source: 'state.variables[' + quote(name) + ']', type: type };
|
|
}
|
|
} else {
|
|
match("operator", "(");
|
|
var expr = parseExpression();
|
|
match("operator", ")");
|
|
return expr;
|
|
}
|
|
}
|
|
|
|
function parseUnaryExpression() {
|
|
var rhs, op;
|
|
|
|
if (test('operator', '+') || test('operator', '-')) {
|
|
op = match('operator');
|
|
} else if (test('reserved', kws.NOT)) {
|
|
op = match('reserved');
|
|
}
|
|
|
|
if (op) {
|
|
rhs = parseUnaryExpression();
|
|
|
|
enforce_type(rhs.type, 'number');
|
|
|
|
switch (op) {
|
|
case "+": return rhs;
|
|
case "-": return { source: '(-' + rhs.source + ')', type: 'number' };
|
|
case kws.NOT: return { source: '((!' + rhs.source + ')?1:0)', type: 'number' };
|
|
}
|
|
}
|
|
return parseFinalExpression();
|
|
}
|
|
|
|
function parsePowerExpression() {
|
|
var lhs = parseUnaryExpression(), rhs;
|
|
while (test('operator', '^', true)) {
|
|
rhs = parseUnaryExpression();
|
|
|
|
enforce_type(lhs.type, 'number');
|
|
enforce_type(rhs.type, 'number');
|
|
|
|
lhs = { source: 'Math.pow(' + lhs.source + ',' + rhs.source + ')', type: 'number' };
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
function parseMultiplicativeExpression() {
|
|
var lhs = parsePowerExpression(), rhs, op;
|
|
while (test('operator', '*') || test('operator', '/')) {
|
|
op = match('operator');
|
|
rhs = parsePowerExpression();
|
|
|
|
enforce_type(lhs.type, 'number');
|
|
enforce_type(rhs.type, 'number');
|
|
|
|
switch (op) {
|
|
case "*": lhs = { source: '(' + lhs.source + '*' + rhs.source + ')', type: 'number' }; break;
|
|
case "/": lhs = { source: 'lib.div(' + lhs.source + ',' + rhs.source + ')', type: 'number' }; break;
|
|
}
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
function parseAdditiveExpression() {
|
|
var lhs = parseMultiplicativeExpression(), rhs, op;
|
|
while (test('operator', '+') || test('operator', '-')) {
|
|
op = match('operator');
|
|
rhs = parseMultiplicativeExpression();
|
|
|
|
switch (op) {
|
|
case "+":
|
|
enforce_type(rhs.type, lhs.type);
|
|
lhs = { source: '(' + lhs.source + '+' + rhs.source + ')', type: lhs.type }; break;
|
|
case "-":
|
|
enforce_type(lhs.type, 'number');
|
|
enforce_type(rhs.type, 'number');
|
|
lhs = { source: '(' + lhs.source + '-' + rhs.source + ')', type: lhs.type }; break;
|
|
}
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
function parseRelationalExpression() {
|
|
var lhs = parseAdditiveExpression(), rhs, op;
|
|
while (test('operator', '<') || test('operator', '>') || test('operator', '=')) {
|
|
|
|
op = match('operator');
|
|
switch (op) {
|
|
case '<':
|
|
if (test('operator', '=', true)) { op = '<='; break; }
|
|
if (test('operator', '>', true)) { op = '!=='; break; }
|
|
break;
|
|
case '>':
|
|
if (test('operator', '=', true)) { op = '>='; break; }
|
|
if (test('operator', '<', true)) { op = '!=='; break; }
|
|
break;
|
|
case '=':
|
|
if (test('operator', '<', true)) { op = '<='; break; }
|
|
if (test('operator', '>', true)) { op = '>='; break; }
|
|
if (test('operator', '=', true)) { op = '==='; break; }
|
|
op = '===';
|
|
}
|
|
|
|
rhs = parseAdditiveExpression();
|
|
|
|
enforce_type(rhs.type, lhs.type);
|
|
lhs = lhs = { source: '((' + lhs.source + op + rhs.source + ')?1:0)', type: 'number' };
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
function parseAndExpression() {
|
|
var lhs = parseRelationalExpression(), rhs;
|
|
while (test('reserved', kws.AND, true)) {
|
|
rhs = parseRelationalExpression();
|
|
|
|
enforce_type(lhs.type, 'number');
|
|
enforce_type(rhs.type, 'number');
|
|
|
|
lhs = {
|
|
source: '((' + lhs.source + '&&' + rhs.source + ')?1:0)',
|
|
type: 'number'
|
|
};
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
function parseOrExpression() {
|
|
var lhs = parseAndExpression(), rhs;
|
|
while (test('reserved', kws.OR, true)) {
|
|
rhs = parseAndExpression();
|
|
|
|
enforce_type(lhs.type, 'number');
|
|
enforce_type(rhs.type, 'number');
|
|
|
|
lhs = {
|
|
source: '((' + lhs.source + '||' + rhs.source + ')?1:0)',
|
|
type: 'number'
|
|
};
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
return parseOrExpression;
|
|
} ());
|
|
|
|
|
|
//
|
|
// Statements
|
|
//
|
|
|
|
function parseCommand() {
|
|
|
|
function slib(name /* , ...args */) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return 'lib[' + quote(name) + '](' + args.join(',') + ');';
|
|
}
|
|
|
|
var keyword = test('identifier') ? kws.LET : match('reserved'),
|
|
name, type, subscripts, is_to, expr, param, args, prompt, trailing, js;
|
|
|
|
switch (keyword) {
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Variable Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.CLEAR: // Clear all variables
|
|
return slib('clear');
|
|
|
|
case kws.LET: // Assign a variable, LET x = expr
|
|
name = match('identifier');
|
|
subscripts = parseSubscripts();
|
|
match('operator', '=');
|
|
|
|
type = vartype(name);
|
|
if (type === 'int') {
|
|
expr = 'lib.toint(lib.checkFinite(' + parseNumericExpression() + '))';
|
|
} else if (type === 'float') {
|
|
expr = 'lib.checkFinite(' + parseNumericExpression() + ')';
|
|
} else { // type === 'string')
|
|
expr = parseStringExpression();
|
|
}
|
|
|
|
if (!subscripts) {
|
|
identifiers.variables[name] = true;
|
|
return 'state.variables[' + quote(name) + '] = ' + expr;
|
|
}
|
|
identifiers.arrays[name] = true;
|
|
return 'state.arrays[' + quote(name) + '].set([' + subscripts + '], ' + expr + ')';
|
|
|
|
case kws.DIM:
|
|
js = '';
|
|
do {
|
|
name = match('identifier');
|
|
subscripts = parseSubscripts();
|
|
identifiers.arrays[name] = true;
|
|
js += slib('dim', quote(name), '[' + subscripts + ']');
|
|
} while (test('operator', ',', true));
|
|
return js;
|
|
|
|
case kws.DEF: // DEF FN A(X) = expr
|
|
match("reserved", kws.FN);
|
|
name = match('identifier');
|
|
match("operator", "(");
|
|
param = match('identifier');
|
|
match("operator", ")");
|
|
match("operator", "=");
|
|
|
|
if (vartype(name) !== vartype(param)) {
|
|
throw parse_error("DEF FN function type and argument type must match");
|
|
}
|
|
|
|
expr = vartype(name) === 'string' ?
|
|
parseStringExpression() : parseNumericExpression();
|
|
|
|
return slib('def', quote(name),
|
|
'function (arg){' +
|
|
// Save the current context/variable so we can evaluate
|
|
'var rv,ov=state.variables[' + quote(param) + '];' +
|
|
// Swap in the argument
|
|
'state.variables[' + quote(param) + ']=arg;' +
|
|
// Evaluate the user-function expression
|
|
'rv=' + expr + ';' +
|
|
// Restore
|
|
'state.variables[' + quote(param) + ']=ov;' +
|
|
'return rv;' +
|
|
'}');
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Flow Control Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.GOTO: // GOTO linenum
|
|
return slib('goto', match("number"));
|
|
|
|
case kws.ON: // ON expr (GOTO|GOSUB) linenum[,linenum ... ]
|
|
expr = parseNumericExpression();
|
|
|
|
keyword = match('reserved');
|
|
if (keyword !== kws.GOTO && keyword !== kws.GOSUB) {
|
|
throw parse_error("Syntax error: Expected " + kws.GOTO + " or " + kws.GOSUB);
|
|
}
|
|
|
|
args = [];
|
|
do {
|
|
args.push(match("number"));
|
|
} while (test("operator", ",", true));
|
|
|
|
return slib(keyword === kws.GOSUB ? 'on_gosub' : 'on_goto', expr, args.join(','));
|
|
|
|
case kws.GOSUB: // GOSUB linenum
|
|
return slib('gosub', match("number"));
|
|
|
|
case kws.RETURN: // Return from the last GOSUB
|
|
return slib('return');
|
|
|
|
case kws.POP: // Turn last GOSUB into a GOTO
|
|
return slib('pop');
|
|
|
|
case kws.FOR: // FOR i = m TO n STEP s
|
|
name = match('identifier');
|
|
if (vartype(name) !== 'float') {
|
|
throw parse_error("Syntax error: Expected floating point variable");
|
|
}
|
|
identifiers.variables[name] = true;
|
|
return 'state.variables[' + quote(name) + '] = ' +
|
|
(match("operator", "=") && parseNumericExpression()) + ';' +
|
|
slib('for', quote(name),
|
|
match("reserved", kws.TO) && parseNumericExpression(),
|
|
test('reserved', kws.STEP, true) ? parseNumericExpression() : '1');
|
|
|
|
case kws.NEXT: // NEXT [i [,j ... ] ]
|
|
args = [];
|
|
if (test('identifier')) {
|
|
args.push(quote(match('identifier')));
|
|
while (test("operator", ",", true)) {
|
|
args.push(quote(match('identifier')));
|
|
}
|
|
}
|
|
|
|
return slib('next', args.join(','));
|
|
|
|
case kws.IF: // IF expr (GOTO linenum|THEN linenum|THEN statement [:statement ... ]
|
|
expr = parseAnyExpression();
|
|
|
|
js = slib('if', expr);
|
|
|
|
if (test('reserved', kws.GOTO, true)) {
|
|
// IF expr GOTO linenum
|
|
return js + slib('goto', match('number'));
|
|
}
|
|
|
|
match('reserved', kws.THEN);
|
|
if (test('number')) {
|
|
// IF expr THEN linenum
|
|
return js + slib('goto', match('number'));
|
|
}
|
|
// IF expr THEN statement
|
|
return js + parseCommand(); // recurse
|
|
|
|
case kws.END: // End program
|
|
return slib('end');
|
|
|
|
case kws.STOP: // Break, like an error
|
|
return slib('stop');
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Error Handling Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.ONERR: // ONERR GOTO linenum
|
|
return slib('onerr_goto',
|
|
match("reserved", kws.GOTO) && match("number"));
|
|
|
|
case kws.RESUME:
|
|
return slib('resume');
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Inline Data Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.RESTORE:
|
|
return slib('restore');
|
|
|
|
case kws.READ:
|
|
args = [];
|
|
do {
|
|
args.push(parsePValue());
|
|
} while (test("operator", ",", true));
|
|
|
|
return slib('read', args.join(','));
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// I/O Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.PRINT: // Output to the screen
|
|
args = [];
|
|
trailing = true;
|
|
while (!endOfStatement()) {
|
|
if (test('operator', ';', true)) {
|
|
trailing = false;
|
|
} else if (test('operator', ',', true)) {
|
|
trailing = false;
|
|
args.push('lib.comma()');
|
|
} else if (test('reserved', kws.SPC) || test('reserved', kws.TAB)) {
|
|
trailing = false;
|
|
keyword = match('reserved');
|
|
match("operator", "(");
|
|
expr = parseNumericExpression();
|
|
match("operator", ")");
|
|
|
|
args.push('lib.' + (keyword === kws.SPC ? 'spc' : 'tab') + '(' + expr + ')');
|
|
} else {
|
|
trailing = true;
|
|
args.push(parseAnyExpression());
|
|
}
|
|
}
|
|
if (trailing) {
|
|
args.push(quote('\r'));
|
|
}
|
|
|
|
return slib('print', args.join(','));
|
|
|
|
case kws.INPUT: // Read input from keyboard
|
|
prompt = '?';
|
|
if (test('string')) {
|
|
prompt = match('string');
|
|
match("operator", ";");
|
|
}
|
|
|
|
args = [];
|
|
|
|
do {
|
|
args.push(parsePValue());
|
|
} while (test("operator", ",", true));
|
|
|
|
return slib('input', quote(prompt), args.join(','));
|
|
|
|
case kws.GET: // Read character from keyboard
|
|
return slib('get', parsePValue());
|
|
|
|
case kws.HOME: // Clear text screen
|
|
return slib('home');
|
|
|
|
case kws.HTAB: // Set horizontal cursor position
|
|
return slib('htab', parseNumericExpression());
|
|
|
|
case kws.VTAB: // Set vertical cursor position
|
|
return slib('vtab', parseNumericExpression());
|
|
|
|
case kws.INVERSE: // Inverse text
|
|
return slib('inverse');
|
|
|
|
case kws.FLASH: // Flashing text
|
|
return slib('flash');
|
|
|
|
case kws.NORMAL: // Normal text
|
|
return slib('normal');
|
|
|
|
case kws.TEXT: // Set display mode to text
|
|
return slib('text');
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Miscellaneous Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.NOTRACE: // Turn off line tracing
|
|
return slib('notrace');
|
|
|
|
case kws.TRACE: // Turn on line tracing
|
|
return slib('trace');
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Lores Graphics
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.GR: // Set display mode to lores graphics, clear screen
|
|
return slib('gr');
|
|
|
|
case kws.COLOR: // Set lores color
|
|
return slib('color', parseNumericExpression());
|
|
|
|
case kws.PLOT: // Plot lores point
|
|
return slib('plot',
|
|
parseNumericExpression(),
|
|
match("operator", ",") && parseNumericExpression());
|
|
|
|
case kws.HLIN: // Draw lores horizontal line
|
|
return slib('hlin',
|
|
parseNumericExpression(),
|
|
match("operator", ",") && parseNumericExpression(),
|
|
match("reserved", kws.AT) && parseNumericExpression());
|
|
|
|
case kws.VLIN: // Draw lores vertical line
|
|
return slib('vlin',
|
|
parseNumericExpression(),
|
|
match("operator", ",") && parseNumericExpression(),
|
|
match("reserved", kws.AT) && parseNumericExpression());
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Hires Graphics
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
// Hires Display Routines
|
|
case kws.HGR: // Set display mode to hires graphics, clear screen
|
|
return slib('hgr');
|
|
|
|
case kws.HGR2: // Set display mode to hires graphics, page 2, clear screen
|
|
return slib('hgr2');
|
|
|
|
case kws.HCOLOR: // Set hires color
|
|
return slib('hcolor', parseNumericExpression());
|
|
|
|
case kws.HPLOT: // Draw hires line
|
|
is_to = test('reserved', kws.TO, true);
|
|
|
|
args = [];
|
|
do {
|
|
args.push(parseNumericExpression());
|
|
match("operator", ",");
|
|
args.push(parseNumericExpression());
|
|
} while (test('reserved', kws.TO, true));
|
|
|
|
return slib(is_to ? 'hplot_to' : 'hplot', args.join(','));
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Compatibility shims
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.PR: // Direct output to slot
|
|
return slib('pr#', parseNumericExpression());
|
|
|
|
case kws.CALL: // Call native routine
|
|
return slib('call', parseNumericExpression());
|
|
|
|
case kws.POKE: // Set memory value
|
|
return slib('poke',
|
|
parseNumericExpression(),
|
|
match("operator", ",") && parseNumericExpression());
|
|
|
|
case kws.SPEED: // Output speed
|
|
return slib('speed', parseNumericExpression());
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// INTROSPECTION
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
case kws.LIST: // List program statements
|
|
throw parse_error("Introspection statement not supported: " + keyword);
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Statements that will never be implemented
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
// Shape tables
|
|
case kws.ROT: // Set rotation angle for hires shape
|
|
case kws.SCALE: // Set rotation angle for hires shape
|
|
case kws.DRAW: // Draw hires shape
|
|
case kws.XDRAW: // XOR draw hires shape
|
|
throw parse_error("Display statement not supported: " + keyword);
|
|
|
|
// Interpreter Routines
|
|
case kws.CONT: // Continue stopped program (immediate mode)
|
|
case kws.DEL: // Deletes program statements
|
|
case kws.NEW: // Wipe program
|
|
case kws.RUN: // Execute program
|
|
throw parse_error("Interpreter statement not supported: " + keyword);
|
|
|
|
// Native Routines
|
|
case kws.HIMEM: // Set upper bound of variable memory
|
|
case kws.IN: // Direct input from slot
|
|
case kws.LOMEM: // Set low bound of variable memory
|
|
case kws.WAIT: // Wait for memory value to match a condition
|
|
case kws.AMPERSAND: // Command hook
|
|
throw parse_error("Native interop statement not supported: " + keyword);
|
|
|
|
// Tape Routines
|
|
case kws.LOAD: // Load program from cassette port
|
|
case kws.RECALL: // Load array from cassette port
|
|
case kws.SAVE: // Save program to cassette port
|
|
case kws.STORE: // Store array to cassette port
|
|
case kws.SHLOAD: // Load shape table from cassette port
|
|
throw parse_error("Tape statement not supported: " + keyword);
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
//
|
|
// NYI Statements
|
|
//
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
// Parts of other statements - AT, FN, STEP, TO, THEN, etc.
|
|
default:
|
|
throw parse_error("Syntax error: " + keyword);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Top-level Program Structure
|
|
//
|
|
|
|
var parseProgram = function() {
|
|
|
|
var program = {
|
|
statements: [], // array of: [ line-number | statement-function ]
|
|
data: [], // array of [ string | number ]
|
|
jump: [] // map of: { line-number: statement-index }
|
|
};
|
|
|
|
function mkfun(js) {
|
|
var fun; // NOTE: for IE; would prefer Function()
|
|
eval('fun = (function (){' + js + '});');
|
|
return fun;
|
|
}
|
|
|
|
function empty_statement() { }
|
|
|
|
// Statement = data-declaration | remark | Command | EmptyStatement
|
|
// Command = identifier /*...*/ | reserved /*...*/
|
|
function parseStatement() {
|
|
if (test('data')) {
|
|
program.data = program.data.concat(match('data'));
|
|
return undefined;
|
|
} else if (test('remark', void 0, true)) {
|
|
return undefined;
|
|
} else if (test('reserved') || test('identifier')) {
|
|
return mkfun(parseCommand());
|
|
} else {
|
|
// So TRACE output is correct
|
|
return empty_statement;
|
|
}
|
|
}
|
|
|
|
// Line = line-number Statement { separator Statement }
|
|
function parseLine() {
|
|
var num = match('lineNumber');
|
|
var statements = [];
|
|
var statement = parseStatement();
|
|
if (statement) statements.push(statement);
|
|
while (test('separator', ':', true)) {
|
|
statement = parseStatement();
|
|
if (statement) statements.push(statement);
|
|
}
|
|
insertLine(num, statements);
|
|
}
|
|
|
|
function insertLine(number, statements) {
|
|
var remove = 0;
|
|
for (var i = 0, len = program.statements.length; i < len; ++i) {
|
|
if (typeof program.statements[i] !== 'number')
|
|
continue;
|
|
if (program.statements[i] < number)
|
|
continue;
|
|
if (program.statements[i] === number) {
|
|
var n = i;
|
|
do {
|
|
++n;
|
|
++remove;
|
|
} while (n < len && typeof program.statements[n] !== 'number');
|
|
}
|
|
break;
|
|
}
|
|
var args = [i, remove, number].concat(statements);
|
|
program.statements.splice.apply(program.statements, args);
|
|
}
|
|
|
|
// Program = Line { Line }
|
|
while (!endOfProgram()) {
|
|
parseLine();
|
|
}
|
|
|
|
// Produce jump table
|
|
program.statements.forEach(function(stmt, index) {
|
|
if (typeof stmt === 'number') {
|
|
program.jump[stmt] = index;
|
|
}
|
|
});
|
|
|
|
program.variable_identifiers = Object.keys(identifiers.variables);
|
|
program.array_identifiers = Object.keys(identifiers.arrays);
|
|
|
|
return program;
|
|
};
|
|
|
|
return parseProgram();
|
|
} ());
|
|
|
|
program.init = function init(environment) {
|
|
|
|
// stuff these into runtime library closure/binding
|
|
env = environment;
|
|
state = {
|
|
variables: {},
|
|
arrays: {},
|
|
functions: {},
|
|
data: this.data,
|
|
data_index: 0,
|
|
stmt_index: 0,
|
|
line_number: 0,
|
|
stack: [],
|
|
prng: new PRNG(),
|
|
|
|
onerr_code: 255,
|
|
onerr_handler: void 0,
|
|
trace_mode: false,
|
|
|
|
input_continuation: null,
|
|
|
|
clear: function() {
|
|
program.variable_identifiers.forEach(function(identifier) {
|
|
state.variables[identifier] = vartype(identifier) === 'string' ? '' : 0;
|
|
});
|
|
|
|
program.array_identifiers.forEach(function(identifier) {
|
|
state.arrays[identifier] = new BASICArray(vartype(identifier));
|
|
});
|
|
|
|
state.functions = {};
|
|
state.data_index = 0;
|
|
}
|
|
};
|
|
|
|
state.clear();
|
|
|
|
state.parsevar = function parsevar(name, subscripts, input) {
|
|
|
|
if (arguments.length === 2) {
|
|
input = arguments[1];
|
|
subscripts = void 0;
|
|
}
|
|
var value;
|
|
|
|
switch (vartype(name)) {
|
|
case 'string':
|
|
value = input;
|
|
break;
|
|
|
|
case 'int':
|
|
value = Number(input);
|
|
if (!isFinite(value)) { runtime_error(ERRORS.TYPE_MISMATCH); }
|
|
value = lib.toint(value);
|
|
break;
|
|
|
|
case 'float':
|
|
value = Number(input);
|
|
if (!isFinite(value)) { runtime_error(ERRORS.TYPE_MISMATCH); }
|
|
break;
|
|
}
|
|
|
|
if (subscripts) {
|
|
state.arrays[name].set(subscripts, value);
|
|
} else {
|
|
state.variables[name] = value;
|
|
}
|
|
};
|
|
};
|
|
|
|
program.step = function step(driver) {
|
|
|
|
function gotoline(line) {
|
|
if (!{}.hasOwnProperty.call(program.jump, line)) {
|
|
runtime_error(ERRORS.UNDEFINED_STATEMENT);
|
|
}
|
|
state.stmt_index = program.jump[line];
|
|
}
|
|
|
|
var stmt;
|
|
|
|
try {
|
|
// for RuntimeError
|
|
|
|
try {
|
|
if (state.input_continuation) {
|
|
var cont = state.input_continuation;
|
|
state.input_continuation = null;
|
|
cont(state.input_buffer);
|
|
} else if (state.stmt_index >= program.statements.length) {
|
|
return basic.STATE_STOPPED;
|
|
} else {
|
|
|
|
stmt = program.statements[state.stmt_index];
|
|
|
|
if (typeof stmt === 'number') {
|
|
state.line_number = stmt;
|
|
} else if (typeof stmt === 'function') {
|
|
if (state.trace_mode) {
|
|
env.tty.writeString('#' + state.line_number + ' ');
|
|
}
|
|
stmt();
|
|
} else {
|
|
throw "WTF?";
|
|
}
|
|
}
|
|
|
|
state.stmt_index += 1;
|
|
return basic.STATE_RUNNING;
|
|
|
|
} catch (e) {
|
|
// These may throw RuntimeError
|
|
if (e instanceof basic.RuntimeError) {
|
|
throw e; // let outer catch block handle it
|
|
} else if (e instanceof GoToLine) {
|
|
gotoline(e.line);
|
|
return basic.STATE_RUNNING;
|
|
} else if (e instanceof NextLine) {
|
|
while (state.stmt_index < program.statements.length &&
|
|
typeof program.statements[state.stmt_index] !== 'number') {
|
|
state.stmt_index += 1;
|
|
}
|
|
return basic.STATE_RUNNING;
|
|
} else if (e instanceof BlockingInput) {
|
|
// what to call on next step() after input is handled
|
|
state.input_continuation = e.callback;
|
|
|
|
// call input method to prepare async input
|
|
e.method(function(v) {
|
|
state.input_buffer = v;
|
|
if (driver) { driver(); }
|
|
});
|
|
|
|
return basic.STATE_BLOCKED;
|
|
} else if (e instanceof EndProgram) {
|
|
return basic.STATE_STOPPED;
|
|
} else if (e instanceof Error && /stack|recursion/i.test(e.message)) {
|
|
// IE: Error "Out of stack space"
|
|
// Firefox: InternalError "too much recursion"
|
|
// Safari: RangeError "Maximum call stack size exceeded"
|
|
// Chrome: RangeError "Maximum call stack size exceeded"
|
|
// Opera: Error "Maximum recursion depth exceeded"
|
|
runtime_error(ERRORS.FORMULA_TOO_COMPLEX);
|
|
} else if (e instanceof Error && /memory|overflow/i.test(e.message)) {
|
|
// IE: Error "Out of memory"
|
|
// Firefox: InternalError "allocation size overflow"
|
|
// Safari: Error "Out of memory"
|
|
// Chrome: (not catchable)
|
|
// Opera: (not catchable)
|
|
runtime_error(ERRORS.OUT_OF_MEMORY);
|
|
// NOTE: not reliably generated; don't unit test
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
} catch (rte) {
|
|
if (rte instanceof basic.RuntimeError) {
|
|
state.onerr_code = rte.code || 0;
|
|
if (state.onerr_handler !== void 0) {
|
|
state.stack.push({
|
|
resume_stmt_index: state.stmt_index,
|
|
resume_line_number: state.line_number
|
|
});
|
|
gotoline(state.onerr_handler);
|
|
return basic.STATE_RUNNING;
|
|
} else if (rte.code === ERRORS.REENTER[0]) {
|
|
env.tty.writeString('?REENTER\r');
|
|
return basic.STATE_RUNNING;
|
|
} else {
|
|
// annotate and report to the user
|
|
rte.message += " in line " + state.line_number;
|
|
throw rte;
|
|
}
|
|
} else {
|
|
throw rte;
|
|
}
|
|
}
|
|
};
|
|
|
|
return program;
|
|
};
|
|
|
|
return basic;
|
|
|
|
} ());
|
|
|
|
// TODO: Unit tests for compile errors
|