2020-08-11 02:35:25 +00:00
|
|
|
import { WorkerError, CodeListingMap, SourceLocation, SourceLine } from "../workertypes";
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
export interface SourceLocated {
|
2020-08-11 02:35:25 +00:00
|
|
|
$loc?: SourceLine;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class CompileError extends Error {
|
|
|
|
constructor(msg: string) {
|
|
|
|
super(msg);
|
|
|
|
Object.setPrototypeOf(this, CompileError.prototype);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lexer regular expression -- each (capture group) handles a different token type
|
|
|
|
|
2020-08-09 16:23:49 +00:00
|
|
|
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|[0]*(\d+)|(['].*)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()?\\])|(\S+)/gi;
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
export enum TokenType {
|
|
|
|
EOL = 0,
|
|
|
|
Float1,
|
|
|
|
Float2,
|
|
|
|
Int,
|
|
|
|
Remark,
|
|
|
|
Ident,
|
|
|
|
String,
|
|
|
|
Relational,
|
|
|
|
Operator,
|
|
|
|
CatchAll,
|
|
|
|
_LAST,
|
|
|
|
}
|
|
|
|
|
|
|
|
export type ExprTypes = BinOp | UnOp | IndOp | Literal;
|
|
|
|
|
2020-08-10 01:40:17 +00:00
|
|
|
export type Expr = ExprTypes; // & SourceLocated;
|
2020-08-05 04:48:29 +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)
2020-08-08 16:22:56 +00:00
|
|
|
export type Opcode = string;
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
export type Value = string | number;
|
|
|
|
|
|
|
|
export interface Literal {
|
|
|
|
value: Value;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface BinOp {
|
|
|
|
op: Opcode;
|
|
|
|
left: Expr;
|
|
|
|
right: Expr;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface UnOp {
|
2020-08-09 16:23:49 +00:00
|
|
|
op: 'neg' | 'lnot' | 'bnot';
|
2020-08-05 04:48:29 +00:00
|
|
|
expr: Expr;
|
|
|
|
}
|
|
|
|
|
2020-08-10 01:40:17 +00:00
|
|
|
export interface IndOp {
|
2020-08-05 04:48:29 +00:00
|
|
|
name: string;
|
|
|
|
args: Expr[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface PRINT_Statement {
|
|
|
|
command: "PRINT";
|
|
|
|
args: Expr[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface LET_Statement {
|
|
|
|
command: "LET";
|
|
|
|
lexpr: IndOp;
|
|
|
|
right: Expr;
|
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
export interface DIM_Statement {
|
|
|
|
command: "DIM";
|
|
|
|
args: IndOp[];
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
export interface GOTO_Statement {
|
|
|
|
command: "GOTO";
|
|
|
|
label: Expr;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GOSUB_Statement {
|
|
|
|
command: "GOSUB";
|
|
|
|
label: Expr;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface RETURN_Statement {
|
|
|
|
command: "RETURN";
|
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
export interface ONGOTO_Statement {
|
2020-08-10 13:07:09 +00:00
|
|
|
command: "ONGOTO" | "ONGOSUB";
|
2020-08-09 02:36:21 +00:00
|
|
|
expr: Expr;
|
|
|
|
labels: Expr[];
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
export interface IF_Statement {
|
|
|
|
command: "IF";
|
|
|
|
cond: Expr;
|
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
export interface ELSE_Statement {
|
|
|
|
command: "ELSE";
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
export interface FOR_Statement {
|
|
|
|
command: "FOR";
|
|
|
|
lexpr: IndOp;
|
|
|
|
initial: Expr;
|
|
|
|
target: Expr;
|
|
|
|
step?: Expr;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface NEXT_Statement {
|
|
|
|
command: "NEXT";
|
|
|
|
lexpr?: IndOp;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface INPUT_Statement {
|
|
|
|
command: "INPUT";
|
|
|
|
prompt: Expr;
|
|
|
|
args: IndOp[];
|
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
export interface DATA_Statement {
|
|
|
|
command: "DATA";
|
|
|
|
datums: Expr[];
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
export interface READ_Statement {
|
2020-08-09 02:36:21 +00:00
|
|
|
command: "READ";
|
2020-08-05 04:48:29 +00:00
|
|
|
args: IndOp[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface DEF_Statement {
|
|
|
|
command: "DEF";
|
|
|
|
lexpr: IndOp;
|
|
|
|
def: Expr;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface OPTION_Statement {
|
|
|
|
command: "OPTION";
|
|
|
|
optname: string;
|
|
|
|
optargs: string[];
|
|
|
|
}
|
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
export interface GET_Statement {
|
|
|
|
command: "GET";
|
|
|
|
lexpr: IndOp;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface NoArgStatement {
|
|
|
|
command: string;
|
|
|
|
}
|
|
|
|
|
2020-08-05 04:48:29 +00:00
|
|
|
export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement
|
2020-08-09 02:36:21 +00:00
|
|
|
| IF_Statement | FOR_Statement | NEXT_Statement | DIM_Statement
|
|
|
|
| INPUT_Statement | READ_Statement | DEF_Statement | ONGOTO_Statement
|
|
|
|
| DATA_Statement | OPTION_Statement | NoArgStatement;
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
export type Statement = StatementTypes & SourceLocated;
|
|
|
|
|
|
|
|
export interface BASICLine {
|
|
|
|
label: string;
|
|
|
|
stmts: Statement[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface BASICProgram {
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
opts: BASICOptions;
|
2020-08-05 04:48:29 +00:00
|
|
|
lines: BASICLine[];
|
|
|
|
}
|
|
|
|
|
|
|
|
class Token {
|
|
|
|
str: string;
|
|
|
|
type: TokenType;
|
|
|
|
$loc: SourceLocation;
|
|
|
|
}
|
|
|
|
|
|
|
|
const OPERATORS = {
|
2020-08-09 16:23:49 +00:00
|
|
|
'IMP': {f:'bimp',p:4},
|
|
|
|
'EQV': {f:'beqv',p:5},
|
|
|
|
'XOR': {f:'bxor',p:6},
|
|
|
|
'OR': {f:'lor',p:7}, // or "bor"
|
|
|
|
'AND': {f:'land',p:8}, // or "band"
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
'=': {f:'eq',p:50},
|
|
|
|
'<>': {f:'ne',p:50},
|
|
|
|
'<': {f:'lt',p:50},
|
|
|
|
'>': {f:'gt',p:50},
|
|
|
|
'<=': {f:'le',p:50},
|
|
|
|
'>=': {f:'ge',p:50},
|
2020-08-05 04:48:29 +00:00
|
|
|
'+': {f:'add',p:100},
|
|
|
|
'-': {f:'sub',p:100},
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
'%': {f:'mod',p:140},
|
|
|
|
'\\': {f:'idiv',p:150},
|
2020-08-05 04:48:29 +00:00
|
|
|
'*': {f:'mul',p:200},
|
|
|
|
'/': {f:'div',p:200},
|
|
|
|
'^': {f:'pow',p:300}
|
|
|
|
};
|
|
|
|
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
function getOperator(op: string) {
|
|
|
|
return OPERATORS[op];
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getPrecedence(tok: Token): number {
|
|
|
|
switch (tok.type) {
|
|
|
|
case TokenType.Operator:
|
|
|
|
case TokenType.Relational:
|
|
|
|
case TokenType.Ident:
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
let op = getOperator(tok.str);
|
2020-08-05 04:48:29 +00:00
|
|
|
if (op) return op.p;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// is token an end of statement marker? (":" or end of line)
|
|
|
|
function isEOS(tok: Token) {
|
2020-08-09 02:36:21 +00:00
|
|
|
return tok.type == TokenType.EOL || tok.type == TokenType.Remark
|
|
|
|
|| tok.str == ':' || tok.str == 'ELSE'; // TODO: only ELSE if ifElse==true
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function stripQuotes(s: string) {
|
|
|
|
// TODO: assert
|
|
|
|
return s.substr(1, s.length-2);
|
|
|
|
}
|
|
|
|
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
// TODO: implement these
|
2020-08-05 04:48:29 +00:00
|
|
|
export interface BASICOptions {
|
2020-08-09 02:36:21 +00:00
|
|
|
dialectName : string; // use this to select the dialect
|
|
|
|
asciiOnly : boolean; // reject non-ASCII chars?
|
2020-08-05 04:48:29 +00:00
|
|
|
uppercaseOnly : boolean; // convert everything to uppercase?
|
2020-08-09 02:36:21 +00:00
|
|
|
optionalLabels : boolean; // can omit line numbers and use labels?
|
2020-08-05 04:48:29 +00:00
|
|
|
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
|
2020-08-09 16:23:49 +00:00
|
|
|
tickComments : boolean; // support 'comments?
|
|
|
|
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
|
2020-08-05 04:48:29 +00:00
|
|
|
sharedArrayNamespace : boolean; // arrays and variables have same namespace? (conflict)
|
2020-08-09 16:23:49 +00:00
|
|
|
defaultArrayBase : number; // arrays start at this number (0 or 1) (TODO: check)
|
2020-08-05 04:48:29 +00:00
|
|
|
defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0)
|
|
|
|
stringConcat : boolean; // can concat strings with "+" operator?
|
2020-08-09 02:36:21 +00:00
|
|
|
typeConvert : boolean; // type convert strings <-> numbers? (TODO)
|
2020-08-09 16:23:49 +00:00
|
|
|
checkOverflow : boolean; // check for overflow of numerics?
|
2020-08-09 02:36:21 +00:00
|
|
|
sparseArrays : boolean; // true == don't require DIM for arrays (TODO)
|
2020-08-05 04:48:29 +00:00
|
|
|
printZoneLength : number; // print zone length
|
2020-08-09 02:36:21 +00:00
|
|
|
numericPadding : boolean; // " " or "-" before and " " after numbers?
|
2020-08-10 03:19:27 +00:00
|
|
|
outOfOrderNext : boolean; // can we skip a NEXT statement? (can't interleave tho)
|
2020-08-09 02:36:21 +00:00
|
|
|
multipleNextVars : boolean; // NEXT Y,X (TODO)
|
|
|
|
ifElse : boolean; // IF...ELSE construct
|
2020-08-09 16:23:49 +00:00
|
|
|
bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops
|
|
|
|
maxDimensions : number; // max number of dimensions for arrays
|
|
|
|
maxDefArgs : number; // maximum # of arguments for user-defined functions
|
|
|
|
maxStringLength : number; // maximum string length in chars
|
|
|
|
validKeywords : string[]; // valid keywords (or null for accept all)
|
|
|
|
validFunctions : string[]; // valid functions (or null for accept all)
|
|
|
|
validOperators : string[]; // valid operators (or null for accept all)
|
2020-08-10 03:19:27 +00:00
|
|
|
commandsPerSec? : number; // how many commands per second?
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
///// BASIC PARSER
|
|
|
|
|
|
|
|
export class BASICParser {
|
2020-08-09 16:23:49 +00:00
|
|
|
opts : BASICOptions = ALTAIR_BASIC40;
|
2020-08-05 04:48:29 +00:00
|
|
|
errors: WorkerError[];
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
listings: CodeListingMap;
|
2020-08-05 04:48:29 +00:00
|
|
|
labels: { [label: string]: BASICLine };
|
|
|
|
targets: { [targetlabel: string]: SourceLocation };
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
decls: { [name: string]: SourceLocation }; // declared/set vars
|
|
|
|
refs: { [name: string]: SourceLocation }; // references
|
|
|
|
|
2020-08-09 21:32:52 +00:00
|
|
|
path : string;
|
2020-08-05 04:48:29 +00:00
|
|
|
lineno : number;
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
tokens: Token[];
|
|
|
|
eol: Token;
|
2020-08-05 04:48:29 +00:00
|
|
|
curlabel: string;
|
|
|
|
lasttoken: Token;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.labels = {};
|
|
|
|
this.targets = {};
|
|
|
|
this.errors = [];
|
|
|
|
this.lineno = 0;
|
|
|
|
this.curlabel = null;
|
|
|
|
this.listings = {};
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
this.decls = {};
|
|
|
|
this.refs = {};
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
compileError(msg: string, loc?: SourceLocation) {
|
|
|
|
if (!loc) loc = this.peekToken().$loc;
|
|
|
|
// TODO: pass SourceLocation to errors
|
2020-08-10 01:40:17 +00:00
|
|
|
this.errors.push({path:loc.path, line:loc.line, label:loc.label, msg:msg});
|
2020-08-05 04:48:29 +00:00
|
|
|
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
|
|
|
|
}
|
|
|
|
dialectError(what: string, loc?: SourceLocation) {
|
2020-08-10 16:08:43 +00:00
|
|
|
this.compileError(`The selected BASIC dialect (${this.opts.dialectName}) doesn't support ${what}.`, loc); // TODO
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
consumeToken(): Token {
|
|
|
|
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
|
|
|
|
return tok;
|
|
|
|
}
|
2020-08-09 16:23:49 +00:00
|
|
|
expectToken(str: string, msg?: string) : Token {
|
2020-08-05 04:48:29 +00:00
|
|
|
var tok = this.consumeToken();
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
var tokstr = tok.str;
|
2020-08-05 04:48:29 +00:00
|
|
|
if (str != tokstr) {
|
2020-08-09 16:23:49 +00:00
|
|
|
this.compileError(msg || `There should be a "${str}" here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
return tok;
|
|
|
|
}
|
|
|
|
peekToken(): Token {
|
|
|
|
var tok = this.tokens[0];
|
|
|
|
return tok ? tok : this.eol;
|
|
|
|
}
|
|
|
|
pushbackToken(tok: Token) {
|
|
|
|
this.tokens.unshift(tok);
|
|
|
|
}
|
|
|
|
parseOptLabel(line: BASICLine) {
|
|
|
|
let tok = this.consumeToken();
|
|
|
|
switch (tok.type) {
|
2020-08-09 02:36:21 +00:00
|
|
|
case TokenType.Ident:
|
|
|
|
if (this.opts.optionalLabels) {
|
|
|
|
if (this.peekToken().str == ':') { // is it a label:
|
|
|
|
this.consumeToken(); // eat the ":"
|
|
|
|
// fall through to the next case
|
|
|
|
} else {
|
|
|
|
this.pushbackToken(tok); // nope
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else this.dialectError(`optional line numbers`);
|
2020-08-05 04:48:29 +00:00
|
|
|
case TokenType.Int:
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
if (this.labels[tok.str] != null) this.compileError(`There's a duplicated label "${tok.str}".`);
|
2020-08-05 04:48:29 +00:00
|
|
|
this.labels[tok.str] = line;
|
|
|
|
line.label = tok.str;
|
|
|
|
this.curlabel = tok.str;
|
|
|
|
break;
|
2020-08-09 01:03:48 +00:00
|
|
|
case TokenType.Float1:
|
|
|
|
case TokenType.Float2:
|
|
|
|
this.compileError(`Line numbers must be positive integers.`);
|
|
|
|
break;
|
2020-08-05 04:48:29 +00:00
|
|
|
default:
|
2020-08-09 02:36:21 +00:00
|
|
|
if (this.opts.optionalLabels) this.compileError(`A line must start with a line number, command, or label.`);
|
|
|
|
else this.compileError(`A line must start with a line number.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
parseFile(file: string, path: string) : BASICProgram {
|
2020-08-09 21:32:52 +00:00
|
|
|
this.path = path;
|
2020-08-05 04:48:29 +00:00
|
|
|
var pgmlines = file.split("\n").map((line) => this.parseLine(line));
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
var program = { opts: this.opts, lines: pgmlines };
|
|
|
|
this.checkAll(program);
|
2020-08-05 04:48:29 +00:00
|
|
|
this.listings[path] = this.generateListing(file, program);
|
|
|
|
return program;
|
|
|
|
}
|
|
|
|
parseLine(line: string) : BASICLine {
|
|
|
|
try {
|
|
|
|
this.tokenize(line);
|
|
|
|
return this.parse();
|
|
|
|
} catch (e) {
|
|
|
|
if (!(e instanceof CompileError)) throw e; // ignore compile errors since errors[] list captures them
|
|
|
|
return {label:null, stmts:[]};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tokenize(line: string) : void {
|
|
|
|
this.lineno++;
|
|
|
|
this.tokens = [];
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
var m : RegExpMatchArray;
|
2020-08-05 04:48:29 +00:00
|
|
|
while (m = re_toks.exec(line)) {
|
|
|
|
for (var i = 1; i < TokenType._LAST; i++) {
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
let s : string = m[i];
|
2020-08-05 04:48:29 +00:00
|
|
|
if (s != null) {
|
2020-08-09 02:36:21 +00:00
|
|
|
// maybe we don't support unicode in 1975?
|
|
|
|
if (this.opts.asciiOnly && !/^[\x00-\x7F]*$/.test(s))
|
|
|
|
this.dialectError(`non-ASCII characters`);
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
// uppercase all identifiers, and maybe more
|
|
|
|
if (i == TokenType.Ident || this.opts.uppercaseOnly)
|
|
|
|
s = s.toUpperCase();
|
|
|
|
// add token to list
|
2020-08-05 04:48:29 +00:00
|
|
|
this.tokens.push({
|
|
|
|
str: s,
|
|
|
|
type: i,
|
2020-08-10 01:40:17 +00:00
|
|
|
$loc: { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length, label: this.curlabel }
|
2020-08-05 04:48:29 +00:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-10 01:40:17 +00:00
|
|
|
this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length, label: this.curlabel } };
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
parse() : BASICLine {
|
|
|
|
var line = {label: null, stmts: []};
|
|
|
|
// not empty line?
|
|
|
|
if (this.tokens.length) {
|
|
|
|
this.parseOptLabel(line);
|
2020-08-09 01:03:48 +00:00
|
|
|
if (this.tokens.length) {
|
|
|
|
line.stmts = this.parseCompoundStatement();
|
|
|
|
}
|
2020-08-10 01:40:17 +00:00
|
|
|
this.curlabel = null;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
return line;
|
|
|
|
}
|
|
|
|
parseCompoundStatement(): Statement[] {
|
|
|
|
var list = this.parseList(this.parseStatement, ':');
|
2020-08-09 02:36:21 +00:00
|
|
|
var next = this.peekToken();
|
|
|
|
if (!isEOS(next))
|
|
|
|
this.compileError(`Expected end of line or ':'`, next.$loc);
|
|
|
|
if (next.str == 'ELSE')
|
|
|
|
return list.concat(this.parseCompoundStatement());
|
|
|
|
else
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
validKeyword(keyword: string) : string {
|
|
|
|
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
parseStatement(): Statement | null {
|
|
|
|
var cmdtok = this.consumeToken();
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
var cmd = cmdtok.str;
|
2020-08-05 04:48:29 +00:00
|
|
|
var stmt;
|
|
|
|
switch (cmdtok.type) {
|
|
|
|
case TokenType.Remark:
|
|
|
|
if (!this.opts.tickComments) this.dialectError(`tick remarks`);
|
|
|
|
return null;
|
2020-08-09 02:36:21 +00:00
|
|
|
case TokenType.Operator:
|
|
|
|
if (cmd == this.validKeyword('?')) cmd = 'PRINT';
|
2020-08-05 04:48:29 +00:00
|
|
|
case TokenType.Ident:
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
// remark? ignore all tokens to eol
|
2020-08-05 04:48:29 +00:00
|
|
|
if (cmd == 'REM') {
|
|
|
|
while (this.consumeToken().type != TokenType.EOL) { }
|
|
|
|
return null;
|
|
|
|
}
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
// look for "GO TO" and "GO SUB"
|
2020-08-05 04:48:29 +00:00
|
|
|
if (cmd == 'GO' && this.peekToken().str == 'TO') {
|
|
|
|
this.consumeToken();
|
|
|
|
cmd = 'GOTO';
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
} else if (cmd == 'GO' && this.peekToken().str == 'SUB') {
|
|
|
|
this.consumeToken();
|
|
|
|
cmd = 'GOSUB';
|
2020-08-05 04:48:29 +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)
2020-08-08 16:22:56 +00:00
|
|
|
// lookup JS function for command
|
2020-08-05 04:48:29 +00:00
|
|
|
var fn = this['stmt__' + cmd];
|
|
|
|
if (fn) {
|
2020-08-09 02:36:21 +00:00
|
|
|
if (this.validKeyword(cmd) == null)
|
2020-08-05 04:48:29 +00:00
|
|
|
this.dialectError(`the ${cmd} keyword`);
|
|
|
|
stmt = fn.bind(this)() as Statement;
|
|
|
|
break;
|
|
|
|
} else if (this.peekToken().str == '=' || this.peekToken().str == '(') {
|
|
|
|
// 'A = expr' or 'A(X) = expr'
|
|
|
|
this.pushbackToken(cmdtok);
|
|
|
|
stmt = this.stmt__LET();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case TokenType.EOL:
|
|
|
|
default:
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
this.compileError(`There should be a command here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
return null;
|
|
|
|
}
|
2020-08-10 01:40:17 +00:00
|
|
|
if (stmt) stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start, label: this.curlabel };
|
2020-08-05 04:48:29 +00:00
|
|
|
return stmt;
|
|
|
|
}
|
2020-08-09 01:25:58 +00:00
|
|
|
parseVarSubscriptOrFunc(): IndOp {
|
2020-08-05 04:48:29 +00:00
|
|
|
var tok = this.consumeToken();
|
|
|
|
switch (tok.type) {
|
|
|
|
case TokenType.Ident:
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
this.refs[tok.str] = tok.$loc;
|
2020-08-05 04:48:29 +00:00
|
|
|
let args = null;
|
|
|
|
if (this.peekToken().str == '(') {
|
|
|
|
this.expectToken('(');
|
|
|
|
args = this.parseExprList();
|
2020-08-09 16:23:49 +00:00
|
|
|
this.expectToken(')', `There should be another expression or a ")" here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
2020-08-10 01:40:17 +00:00
|
|
|
return { name: tok.str, args: args };
|
2020-08-05 04:48:29 +00:00
|
|
|
default:
|
2020-08-09 01:25:58 +00:00
|
|
|
this.compileError(`There should be a variable name here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-08-09 01:25:58 +00:00
|
|
|
parseLexpr(): IndOp {
|
|
|
|
var lexpr = this.parseVarSubscriptOrFunc();
|
|
|
|
this.validateVarName(lexpr);
|
|
|
|
return lexpr;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
parseList<T>(parseFunc:()=>T, delim:string): T[] {
|
|
|
|
var sep;
|
|
|
|
var list = [];
|
|
|
|
do {
|
2020-08-09 01:25:58 +00:00
|
|
|
var el = parseFunc.bind(this)(); // call parse function
|
|
|
|
if (el != null) list.push(el); // add parsed element to list
|
|
|
|
sep = this.consumeToken(); // consume seperator token
|
2020-08-05 04:48:29 +00:00
|
|
|
} while (sep.str == delim);
|
|
|
|
this.pushbackToken(sep);
|
|
|
|
return list;
|
|
|
|
}
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
parseLexprList(): IndOp[] {
|
2020-08-09 01:25:58 +00:00
|
|
|
var list = this.parseList(this.parseLexpr, ',');
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
list.forEach((lexpr) => this.decls[lexpr.name] = this.lasttoken.$loc);
|
|
|
|
return list;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
parseExprList(): Expr[] {
|
|
|
|
return this.parseList(this.parseExpr, ',');
|
|
|
|
}
|
|
|
|
parseLabelList(): Expr[] {
|
|
|
|
return this.parseList(this.parseLabel, ',');
|
|
|
|
}
|
|
|
|
parseLabel() : Expr {
|
|
|
|
var tok = this.consumeToken();
|
|
|
|
switch (tok.type) {
|
2020-08-09 02:36:21 +00:00
|
|
|
case TokenType.Ident:
|
|
|
|
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
|
2020-08-05 04:48:29 +00:00
|
|
|
case TokenType.Int:
|
2020-08-09 02:36:21 +00:00
|
|
|
var label = tok.str;
|
2020-08-05 04:48:29 +00:00
|
|
|
this.targets[label] = tok.$loc;
|
|
|
|
return {value:label};
|
|
|
|
default:
|
2020-08-09 02:36:21 +00:00
|
|
|
if (this.opts.optionalLabels) this.compileError(`There should be a line number or label here.`);
|
|
|
|
else this.compileError(`There should be a line number here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
parsePrimary(): Expr {
|
|
|
|
let tok = this.consumeToken();
|
|
|
|
switch (tok.type) {
|
|
|
|
case TokenType.Int:
|
|
|
|
case TokenType.Float1:
|
|
|
|
case TokenType.Float2:
|
2020-08-10 01:40:17 +00:00
|
|
|
return { value: this.parseNumber(tok.str)/*, $loc: tok.$loc*/ };
|
2020-08-05 04:48:29 +00:00
|
|
|
case TokenType.String:
|
2020-08-10 01:40:17 +00:00
|
|
|
return { value: stripQuotes(tok.str)/*, $loc: tok.$loc*/ };
|
2020-08-05 04:48:29 +00:00
|
|
|
case TokenType.Ident:
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
if (tok.str == 'NOT') {
|
|
|
|
let expr = this.parsePrimary();
|
2020-08-09 16:23:49 +00:00
|
|
|
return { op: this.opts.bitwiseLogic ? 'bnot' : 'lnot', expr: expr };
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
} else {
|
|
|
|
this.pushbackToken(tok);
|
2020-08-09 01:25:58 +00:00
|
|
|
return this.parseVarSubscriptOrFunc();
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
case TokenType.Operator:
|
|
|
|
if (tok.str == '(') {
|
|
|
|
let expr = this.parseExpr();
|
2020-08-09 16:23:49 +00:00
|
|
|
this.expectToken(')', `There should be another expression or a ")" here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
return expr;
|
|
|
|
} else if (tok.str == '-') {
|
|
|
|
let expr = this.parsePrimary(); // TODO: -2^2=-4 and -2-2=-4
|
|
|
|
return { op: 'neg', expr: expr };
|
|
|
|
} else if (tok.str == '+') {
|
2020-08-09 16:23:49 +00:00
|
|
|
return this.parsePrimary(); // ignore unary +
|
2020-08-05 04:48:29 +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)
2020-08-08 16:22:56 +00:00
|
|
|
case TokenType.EOL:
|
|
|
|
this.compileError(`The expression is incomplete.`);
|
|
|
|
return;
|
2020-08-05 04:48:29 +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)
2020-08-08 16:22:56 +00:00
|
|
|
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;
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
parseExpr1(left: Expr, minPred: number): Expr {
|
|
|
|
let look = this.peekToken();
|
|
|
|
while (getPrecedence(look) >= minPred) {
|
|
|
|
let op = this.consumeToken();
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
if (this.opts.validOperators && this.opts.validOperators.indexOf(op.str) < 0)
|
|
|
|
this.dialectError(`the "${op.str}" operator`);
|
2020-08-05 04:48:29 +00:00
|
|
|
let right: Expr = this.parsePrimary();
|
|
|
|
look = this.peekToken();
|
|
|
|
while (getPrecedence(look) > getPrecedence(op)) {
|
|
|
|
right = this.parseExpr1(right, getPrecedence(look));
|
|
|
|
look = this.peekToken();
|
|
|
|
}
|
2020-08-09 16:23:49 +00:00
|
|
|
var opfn = getOperator(op.str).f;
|
|
|
|
if (this.opts.bitwiseLogic && opfn == 'land') opfn = 'band';
|
|
|
|
if (this.opts.bitwiseLogic && opfn == 'lor') opfn = 'bor';
|
|
|
|
left = { op:opfn, left: left, right: right };
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
return left;
|
|
|
|
}
|
|
|
|
parseExpr(): Expr {
|
|
|
|
return this.parseExpr1(this.parsePrimary(), 0);
|
|
|
|
}
|
2020-08-09 01:25:58 +00:00
|
|
|
validateVarName(lexpr: IndOp) {
|
|
|
|
if (this.opts.strictVarNames) {
|
|
|
|
if (lexpr.args == null && !/^[A-Z][0-9]?[$]?$/.test(lexpr.name))
|
|
|
|
this.dialectError(`variable names other than a letter followed by an optional digit`);
|
|
|
|
if (lexpr.args != null && !/^[A-Z]?[$]?$/.test(lexpr.name))
|
|
|
|
this.dialectError(`array names other than a single letter`);
|
|
|
|
}
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
|
|
|
|
//// STATEMENTS
|
|
|
|
|
|
|
|
stmt__LET(): LET_Statement {
|
2020-08-09 01:25:58 +00:00
|
|
|
var lexpr = this.parseLexpr();
|
2020-08-05 04:48:29 +00:00
|
|
|
this.expectToken("=");
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
this.decls[lexpr.name] = this.lasttoken.$loc;
|
2020-08-05 04:48:29 +00:00
|
|
|
var right = this.parseExpr();
|
|
|
|
return { command: "LET", lexpr: lexpr, right: right };
|
|
|
|
}
|
|
|
|
stmt__PRINT(): PRINT_Statement {
|
|
|
|
var sep, lastsep;
|
|
|
|
var list = [];
|
|
|
|
do {
|
|
|
|
sep = this.peekToken();
|
|
|
|
if (isEOS(sep)) {
|
|
|
|
break;
|
|
|
|
} else if (sep.str == ';') {
|
|
|
|
this.consumeToken();
|
|
|
|
lastsep = sep;
|
|
|
|
} else if (sep.str == ',') {
|
|
|
|
this.consumeToken();
|
|
|
|
list.push({value:'\t'});
|
|
|
|
lastsep = sep;
|
|
|
|
} else {
|
|
|
|
list.push(this.parseExpr());
|
|
|
|
lastsep = null;
|
|
|
|
}
|
|
|
|
} while (true);
|
|
|
|
if (!(lastsep && (lastsep.str == ';' || sep.str != ','))) {
|
|
|
|
list.push({value:'\n'});
|
|
|
|
}
|
|
|
|
return { command: "PRINT", args: list };
|
|
|
|
}
|
|
|
|
stmt__GOTO(): GOTO_Statement {
|
|
|
|
return { command: "GOTO", label: this.parseLabel() };
|
|
|
|
}
|
|
|
|
stmt__GOSUB(): GOSUB_Statement {
|
|
|
|
return { command: "GOSUB", label: this.parseLabel() };
|
|
|
|
}
|
|
|
|
stmt__IF(): IF_Statement {
|
|
|
|
var cond = this.parseExpr();
|
|
|
|
var iftrue: Statement[];
|
2020-08-10 16:08:43 +00:00
|
|
|
// we accept GOTO or THEN if line number provided
|
|
|
|
if (this.peekToken().str == 'GOTO') this.consumeToken();
|
|
|
|
else this.expectToken('THEN');
|
2020-08-05 04:48:29 +00:00
|
|
|
var lineno = this.peekToken();
|
|
|
|
// assume GOTO if number given after THEN
|
|
|
|
if (lineno.type == TokenType.Int) {
|
|
|
|
this.pushbackToken({type:TokenType.Ident, str:'GOTO', $loc:lineno.$loc});
|
|
|
|
}
|
|
|
|
// add fake ":"
|
|
|
|
this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
|
|
|
|
return { command: "IF", cond: cond };
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
stmt__ELSE(): ELSE_Statement {
|
|
|
|
if (!this.opts.ifElse) this.dialectError(`IF...ELSE statements`);
|
|
|
|
var lineno = this.peekToken();
|
|
|
|
// assume GOTO if number given after ELSE
|
|
|
|
if (lineno.type == TokenType.Int) {
|
|
|
|
this.pushbackToken({type:TokenType.Ident, str:'GOTO', $loc:lineno.$loc});
|
|
|
|
}
|
|
|
|
// add fake ":"
|
|
|
|
this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
|
|
|
|
return { command: "ELSE" };
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
stmt__FOR() : FOR_Statement {
|
2020-08-09 01:25:58 +00:00
|
|
|
var lexpr = this.parseLexpr(); // TODO: parseNumVar()
|
2020-08-05 04:48:29 +00:00
|
|
|
this.expectToken('=');
|
|
|
|
var init = this.parseExpr();
|
|
|
|
this.expectToken('TO');
|
|
|
|
var targ = this.parseExpr();
|
|
|
|
if (this.peekToken().str == 'STEP') {
|
|
|
|
this.consumeToken();
|
|
|
|
var step = this.parseExpr();
|
|
|
|
}
|
|
|
|
return { command:'FOR', lexpr:lexpr, initial:init, target:targ, step:step };
|
|
|
|
}
|
|
|
|
stmt__NEXT() : NEXT_Statement {
|
|
|
|
var lexpr = null;
|
|
|
|
if (!isEOS(this.peekToken())) {
|
|
|
|
lexpr = this.parseExpr();
|
|
|
|
}
|
|
|
|
return { command:'NEXT', lexpr:lexpr };
|
|
|
|
}
|
|
|
|
stmt__DIM() : DIM_Statement {
|
2020-08-09 01:25:58 +00:00
|
|
|
var lexprs = this.parseLexprList();
|
|
|
|
lexprs.forEach((arr) => {
|
|
|
|
if (arr.args == null || arr.args.length == 0)
|
|
|
|
this.compileError(`An array defined by DIM must have at least one dimension.`)
|
|
|
|
else if (arr.args.length > this.opts.maxDimensions)
|
|
|
|
this.dialectError(`more than ${this.opts.maxDimensions} dimensional arrays`);
|
|
|
|
});
|
|
|
|
return { command:'DIM', args:lexprs };
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
stmt__INPUT() : INPUT_Statement {
|
|
|
|
var prompt = this.consumeToken();
|
|
|
|
var promptstr;
|
|
|
|
if (prompt.type == TokenType.String) {
|
|
|
|
this.expectToken(';');
|
|
|
|
promptstr = stripQuotes(prompt.str);
|
|
|
|
} else {
|
|
|
|
this.pushbackToken(prompt);
|
|
|
|
promptstr = "";
|
|
|
|
}
|
2020-08-10 01:40:17 +00:00
|
|
|
return { command:'INPUT', prompt:{ value: promptstr }, args:this.parseLexprList() };
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
stmt__DATA() : DATA_Statement {
|
|
|
|
return { command:'DATA', datums:this.parseExprList() };
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
stmt__READ() : READ_Statement {
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
return { command:'READ', args:this.parseLexprList() };
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
stmt__RESTORE() {
|
|
|
|
return { command:'RESTORE' };
|
|
|
|
}
|
|
|
|
stmt__RETURN() {
|
|
|
|
return { command:'RETURN' };
|
|
|
|
}
|
|
|
|
stmt__STOP() {
|
|
|
|
return { command:'STOP' };
|
|
|
|
}
|
|
|
|
stmt__END() {
|
|
|
|
return { command:'END' };
|
|
|
|
}
|
|
|
|
stmt__ON() : ONGOTO_Statement {
|
|
|
|
var expr = this.parseExpr();
|
2020-08-10 13:07:09 +00:00
|
|
|
var gotok = this.consumeToken();
|
|
|
|
var cmd = {GOTO:'ONGOTO', GOSUB:'ONGOSUB'}[gotok.str];
|
|
|
|
if (!cmd) this.compileError(`There should be a GOTO or GOSUB here.`);
|
2020-08-05 04:48:29 +00:00
|
|
|
var labels = this.parseLabelList();
|
2020-08-10 13:07:09 +00:00
|
|
|
return { command:cmd, expr:expr, labels:labels };
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
stmt__DEF() : DEF_Statement {
|
2020-08-09 01:25:58 +00:00
|
|
|
var lexpr = this.parseVarSubscriptOrFunc();
|
2020-08-09 02:36:21 +00:00
|
|
|
if (lexpr.args && lexpr.args.length > this.opts.maxDefArgs)
|
|
|
|
this.compileError(`There can be no more than ${this.opts.maxDefArgs} arguments to a function or subscript.`);
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
2020-08-05 04:48:29 +00:00
|
|
|
this.expectToken("=");
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
this.decls[lexpr.name] = this.lasttoken.$loc;
|
2020-08-05 04:48:29 +00:00
|
|
|
var func = this.parseExpr();
|
|
|
|
return { command:'DEF', lexpr:lexpr, def:func };
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
stmt__POP() : NoArgStatement {
|
|
|
|
return { command:'POP' };
|
|
|
|
}
|
|
|
|
stmt__GET() : GET_Statement {
|
|
|
|
var lexpr = this.parseLexpr();
|
|
|
|
this.decls[lexpr.name] = this.lasttoken.$loc;
|
|
|
|
return { command:'GET', lexpr:lexpr };
|
|
|
|
}
|
2020-08-10 13:07:09 +00:00
|
|
|
stmt__CLEAR() : NoArgStatement {
|
|
|
|
return { command:'CLEAR' };
|
|
|
|
}
|
2020-08-09 02:36:21 +00:00
|
|
|
// TODO: CHANGE A TO A$ (4th edition, A(0) is len and A(1..) are chars)
|
2020-08-05 04:48:29 +00:00
|
|
|
stmt__OPTION() : OPTION_Statement {
|
|
|
|
var tokname = this.consumeToken();
|
2020-08-09 02:36:21 +00:00
|
|
|
if (tokname.type != TokenType.Ident) this.compileError(`There must be a name after the OPTION statement.`)
|
2020-08-05 04:48:29 +00:00
|
|
|
var list : string[] = [];
|
|
|
|
var tok;
|
|
|
|
do {
|
|
|
|
tok = this.consumeToken();
|
|
|
|
if (isEOS(tok)) break;
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
list.push(tok.str);
|
2020-08-05 04:48:29 +00:00
|
|
|
} while (true);
|
2020-08-09 01:03:48 +00:00
|
|
|
this.pushbackToken(tok);
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
var stmt : OPTION_Statement = { command:'OPTION', optname:tokname.str, optargs:list };
|
|
|
|
this.parseOptions(stmt);
|
|
|
|
return stmt;
|
|
|
|
}
|
|
|
|
parseOptions(stmt: OPTION_Statement) {
|
2020-08-10 03:19:27 +00:00
|
|
|
var arg = stmt.optargs[0];
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
switch (stmt.optname) {
|
|
|
|
case 'BASE':
|
2020-08-10 03:19:27 +00:00
|
|
|
let base = parseInt(arg);
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
if (base == 0 || base == 1) this.opts.defaultArrayBase = base;
|
|
|
|
else this.compileError("OPTION BASE can only be 0 or 1.");
|
|
|
|
break;
|
|
|
|
case 'DIALECT':
|
2020-08-10 03:19:27 +00:00
|
|
|
let dname = arg || "";
|
2020-08-09 02:36:21 +00:00
|
|
|
let dialect = DIALECTS[dname.toUpperCase()];
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
if (dialect) this.opts = dialect;
|
2020-08-10 03:19:27 +00:00
|
|
|
else this.compileError(`OPTION DIALECT ${dname} is not supported by this compiler.`);
|
|
|
|
break;
|
|
|
|
case 'CPUSPEED':
|
|
|
|
if (!(this.opts.commandsPerSec = Math.min(1e7, arg=='MAX' ? Infinity : parseFloat(arg))))
|
|
|
|
this.compileError(`OPTION CPUSPEED takes a positive number or MAX.`);
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this.compileError(`OPTION ${stmt.optname} is not supported by this compiler.`);
|
|
|
|
break;
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// for workermain
|
|
|
|
generateListing(file: string, program: BASICProgram) {
|
|
|
|
var srclines = [];
|
|
|
|
var pc = 0;
|
|
|
|
program.lines.forEach((line, idx) => {
|
2020-08-11 02:35:25 +00:00
|
|
|
line.stmts.forEach((stmt) => {
|
|
|
|
var sl = stmt.$loc;
|
|
|
|
sl.offset = pc++; // TODO: could Statement have offset field?
|
|
|
|
srclines.push(sl);
|
|
|
|
});
|
2020-08-05 04:48:29 +00:00
|
|
|
});
|
|
|
|
return { lines: srclines };
|
|
|
|
}
|
|
|
|
getListings() : CodeListingMap {
|
|
|
|
return this.listings;
|
|
|
|
}
|
|
|
|
|
|
|
|
// LINT STUFF
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
checkAll(program : BASICProgram) {
|
|
|
|
this.checkLabels();
|
|
|
|
//this.checkUnsetVars();
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
checkLabels() {
|
|
|
|
for (let targ in this.targets) {
|
|
|
|
if (this.labels[targ] == null) {
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
this.compileError(`There isn't a line number ${targ}.`, this.targets[targ]);
|
2020-08-05 04:48:29 +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)
2020-08-08 16:22:56 +00:00
|
|
|
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.`);
|
|
|
|
}
|
|
|
|
}
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
///// BASIC DIALECTS
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
|
|
|
|
export const ECMA55_MINIMAL : BASICOptions = {
|
2020-08-09 02:36:21 +00:00
|
|
|
dialectName: "ECMA55",
|
|
|
|
asciiOnly : true,
|
2020-08-05 04:48:29 +00:00
|
|
|
uppercaseOnly : true,
|
2020-08-09 01:03:48 +00:00
|
|
|
optionalLabels : false,
|
2020-08-05 04:48:29 +00:00
|
|
|
strictVarNames : true,
|
|
|
|
sharedArrayNamespace : true,
|
|
|
|
defaultArrayBase : 0,
|
|
|
|
defaultArraySize : 11,
|
|
|
|
defaultValues : false,
|
|
|
|
stringConcat : false,
|
|
|
|
typeConvert : false,
|
|
|
|
maxDimensions : 2,
|
2020-08-09 02:36:21 +00:00
|
|
|
maxDefArgs : 255,
|
|
|
|
maxStringLength : 255,
|
2020-08-05 04:48:29 +00:00
|
|
|
sparseArrays : false,
|
|
|
|
tickComments : false,
|
|
|
|
validKeywords : ['BASE','DATA','DEF','DIM','END',
|
|
|
|
'FOR','GO','GOSUB','GOTO','IF','INPUT','LET','NEXT','ON','OPTION','PRINT',
|
|
|
|
'RANDOMIZE','READ','REM','RESTORE','RETURN','STEP','STOP','SUB','THEN','TO'
|
2020-08-10 13:07:09 +00:00
|
|
|
], // TODO: no ON...GOSUB
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN'],
|
2020-08-05 04:48:29 +00:00
|
|
|
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
|
|
|
|
printZoneLength : 15,
|
2020-08-09 02:36:21 +00:00
|
|
|
numericPadding : true,
|
2020-08-05 04:48:29 +00:00
|
|
|
checkOverflow : true,
|
2020-08-10 03:19:27 +00:00
|
|
|
outOfOrderNext : false,
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
multipleNextVars : false,
|
2020-08-09 02:36:21 +00:00
|
|
|
ifElse : false,
|
2020-08-09 16:23:49 +00:00
|
|
|
bitwiseLogic : false,
|
2020-08-05 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const ALTAIR_BASIC40 : BASICOptions = {
|
2020-08-09 02:36:21 +00:00
|
|
|
dialectName: "ALTAIR40",
|
|
|
|
asciiOnly : true,
|
2020-08-05 04:48:29 +00:00
|
|
|
uppercaseOnly : true,
|
2020-08-09 01:03:48 +00:00
|
|
|
optionalLabels : false,
|
2020-08-09 02:36:21 +00:00
|
|
|
strictVarNames : false,
|
2020-08-05 04:48:29 +00:00
|
|
|
sharedArrayNamespace : true,
|
|
|
|
defaultArrayBase : 0,
|
|
|
|
defaultArraySize : 11,
|
2020-08-09 02:36:21 +00:00
|
|
|
defaultValues : true,
|
|
|
|
stringConcat : true,
|
2020-08-05 04:48:29 +00:00
|
|
|
typeConvert : false,
|
2020-08-09 02:36:21 +00:00
|
|
|
maxDimensions : 128, // "as many as will fit on a single line" ... ?
|
|
|
|
maxDefArgs : 255,
|
|
|
|
maxStringLength : 255,
|
2020-08-05 04:48:29 +00:00
|
|
|
sparseArrays : false,
|
|
|
|
tickComments : false,
|
|
|
|
validKeywords : null, // all
|
|
|
|
validFunctions : null, // all
|
2020-08-10 13:07:09 +00:00
|
|
|
validOperators : null, // all
|
2020-08-05 04:48:29 +00:00
|
|
|
printZoneLength : 15,
|
2020-08-09 02:36:21 +00:00
|
|
|
numericPadding : true,
|
|
|
|
checkOverflow : true,
|
2020-08-10 03:19:27 +00:00
|
|
|
outOfOrderNext : true,
|
2020-08-09 02:36:21 +00:00
|
|
|
multipleNextVars : true, // TODO: not supported
|
|
|
|
ifElse : true,
|
2020-08-09 16:23:49 +00:00
|
|
|
bitwiseLogic : true,
|
2020-08-09 02:36:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const APPLESOFT_BASIC : BASICOptions = {
|
|
|
|
dialectName: "APPLESOFT",
|
|
|
|
asciiOnly : true,
|
|
|
|
uppercaseOnly : false,
|
|
|
|
optionalLabels : false,
|
|
|
|
strictVarNames : false, // TODO: first two alphanum chars
|
|
|
|
sharedArrayNamespace : false,
|
|
|
|
defaultArrayBase : 0,
|
|
|
|
defaultArraySize : 9, // A(0) to A(8)
|
|
|
|
defaultValues : true,
|
|
|
|
stringConcat : true,
|
|
|
|
typeConvert : false,
|
|
|
|
maxDimensions : 88,
|
|
|
|
maxDefArgs : 1, // TODO: no string FNs
|
|
|
|
maxStringLength : 255,
|
|
|
|
sparseArrays : false,
|
|
|
|
tickComments : false,
|
2020-08-10 13:07:09 +00:00
|
|
|
validKeywords : [
|
2020-08-10 16:08:43 +00:00
|
|
|
'OPTION',
|
2020-08-10 13:07:09 +00:00
|
|
|
'CLEAR','LET','DIM','DEF','FN','GOTO','GOSUB','RETURN','ON','POP',
|
|
|
|
'FOR','TO','NEXT','IF','THEN','END','STOP','ONERR','RESUME',
|
|
|
|
'PRINT','INPUT','GET','HOME','HTAB','VTAB',
|
|
|
|
'INVERSE','FLASH','NORMAL','TEXT',
|
|
|
|
'GR','COLOR','PLOT','HLIN','VLIN',
|
|
|
|
'HGR','HGR2','HPLOT','HCOLOR','AT',
|
|
|
|
'DATA','READ','RESTORE',
|
|
|
|
'REM','TRACE','NOTRACE'],
|
|
|
|
validFunctions : [
|
|
|
|
'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN',
|
|
|
|
'LEN','LEFT$','MID$','RIGHT$','STR$','VAL','CHR$','ASC',
|
|
|
|
'FRE','SCRN','PDL','PEEK','POS'],
|
2020-08-09 02:36:21 +00:00
|
|
|
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^', 'AND', 'NOT', 'OR'],
|
|
|
|
printZoneLength : 16,
|
|
|
|
numericPadding : false,
|
|
|
|
checkOverflow : true,
|
2020-08-10 03:19:27 +00:00
|
|
|
outOfOrderNext : true,
|
2020-08-09 02:36:21 +00:00
|
|
|
multipleNextVars : false,
|
|
|
|
ifElse : false,
|
2020-08-09 16:23:49 +00:00
|
|
|
bitwiseLogic : false,
|
2020-08-09 02:36:21 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 16:08:43 +00:00
|
|
|
export const MODERN_BASIC : BASICOptions = {
|
|
|
|
dialectName: "MODERN",
|
2020-08-09 02:36:21 +00:00
|
|
|
asciiOnly : false,
|
|
|
|
uppercaseOnly : false,
|
|
|
|
optionalLabels : true,
|
2020-08-10 16:08:43 +00:00
|
|
|
strictVarNames : false,
|
2020-08-09 02:36:21 +00:00
|
|
|
sharedArrayNamespace : false,
|
|
|
|
defaultArrayBase : 0,
|
|
|
|
defaultArraySize : 11,
|
2020-08-09 16:23:49 +00:00
|
|
|
defaultValues : false,
|
2020-08-09 02:36:21 +00:00
|
|
|
stringConcat : true,
|
|
|
|
typeConvert : true,
|
|
|
|
maxDimensions : 255,
|
2020-08-10 16:08:43 +00:00
|
|
|
maxDefArgs : 255,
|
2020-08-09 16:23:49 +00:00
|
|
|
maxStringLength : 1024, // TODO?
|
2020-08-09 02:36:21 +00:00
|
|
|
sparseArrays : false,
|
|
|
|
tickComments : true,
|
|
|
|
validKeywords : null, // all
|
2020-08-09 16:23:49 +00:00
|
|
|
validFunctions : null, // all
|
|
|
|
validOperators : null, // all
|
2020-08-09 02:36:21 +00:00
|
|
|
printZoneLength : 15,
|
|
|
|
numericPadding : false,
|
2020-08-05 04:48:29 +00:00
|
|
|
checkOverflow : true,
|
2020-08-10 03:19:27 +00:00
|
|
|
outOfOrderNext : true,
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
multipleNextVars : true,
|
2020-08-09 02:36:21 +00:00
|
|
|
ifElse : true,
|
2020-08-09 16:23:49 +00:00
|
|
|
bitwiseLogic : true,
|
2020-08-05 04:48:29 +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)
2020-08-08 16:22:56 +00:00
|
|
|
|
2020-08-09 02:36:21 +00:00
|
|
|
// TODO: integer vars
|
2020-08-10 16:08:43 +00:00
|
|
|
// TODO: short-circuit FOR loop
|
2020-08-09 02:36:21 +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)
2020-08-08 16:22:56 +00:00
|
|
|
export const DIALECTS = {
|
|
|
|
"DEFAULT": ALTAIR_BASIC40,
|
|
|
|
"ALTAIR": ALTAIR_BASIC40,
|
|
|
|
"ALTAIR40": ALTAIR_BASIC40,
|
|
|
|
"ECMA55": ECMA55_MINIMAL,
|
|
|
|
"MINIMAL": ECMA55_MINIMAL,
|
2020-08-09 02:36:21 +00:00
|
|
|
"APPLESOFT": APPLESOFT_BASIC,
|
2020-08-10 16:08:43 +00:00
|
|
|
"MODERN": MODERN_BASIC,
|
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)
2020-08-08 16:22:56 +00:00
|
|
|
};
|