1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-06-04 11:29:28 +00:00

basic: added type-checking in the compile stage

This commit is contained in:
Steven Hugg 2020-08-20 12:58:14 -05:00
parent eb7b665536
commit e9d3fbcb62
2 changed files with 141 additions and 27 deletions

View File

@ -1,5 +1,6 @@
import { WorkerError, CodeListingMap, SourceLocation, SourceLine } from "../workertypes";
import { BasicMachineState } from "../devices";
import { merge } from "jquery";
export interface BASICOptions {
dialectName : string; // use this to select the dialect
@ -85,29 +86,31 @@ export enum TokenType {
}
export type ExprTypes = BinOp | UnOp | IndOp | Literal;
export type Expr = ExprTypes; // & SourceLocated;
export type Opcode = string;
export type Value = number | string;
export type ValueType = 'number' | 'string' | 'label';
export type Value = string | number;
export interface ExprBase extends SourceLocated {
valtype: ValueType;
}
export interface Literal {
export interface Literal extends ExprBase {
value: Value;
}
export interface BinOp {
export interface BinOp extends ExprBase {
op: Opcode;
left: Expr;
right: Expr;
}
export interface UnOp {
export interface UnOp extends ExprBase {
op: 'neg' | 'lnot' | 'bnot';
expr: Expr;
}
export interface IndOp {
export interface IndOp extends ExprBase {
name: string;
args: Expr[];
}
@ -327,8 +330,9 @@ export class BASICParser {
errors: WorkerError[];
listings: CodeListingMap;
labels: { [label: string]: number }; // label -> PC
targets: { [targetlabel: string]: SourceLocation };
varrefs: { [name: string]: SourceLocation }; // references
targets: { [targetlabel: string]: SourceLocation }; // targets of GOTOs etc
vardefs: { [name: string]: IndOp }; // LET or DIM
varrefs: { [name: string]: SourceLocation }; // variable references
fnrefs: { [name: string]: string[] }; // DEF FN call graph
scopestack: ScopeStartStatement[];
@ -348,6 +352,7 @@ export class BASICParser {
this.targets = {};
this.errors = [];
this.listings = {};
this.vardefs = {};
this.varrefs = {};
this.fnrefs = {};
this.scopestack = [];
@ -553,6 +558,9 @@ export class BASICParser {
validKeyword(keyword: string) : string {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
}
validFunction(funcname: string) : string {
return (this.opts.validFunctions && this.opts.validFunctions.indexOf(funcname) < 0) ? null : funcname;
}
supportsCommand(cmd: string) : () => Statement {
if (cmd == '?') return this.stmt__PRINT;
else return this['stmt__' + cmd];
@ -640,14 +648,14 @@ export class BASICParser {
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
this.varrefs[tok.str] = tok.$loc;
let args = null;
if (this.peekToken().str == '(') {
this.expectToken('(');
args = this.parseExprList();
this.expectToken(')', `There should be another expression or a ")" here.`);
}
return { name: tok.str, args: args };
var valtype = this.exprTypeForSubscript(tok.str, args);
return { valtype: valtype, name: tok.str, args: args };
default:
this.compileError(`There should be a variable name here.`);
break;
@ -655,6 +663,7 @@ export class BASICParser {
}
parseLexpr(): IndOp {
var lexpr = this.parseVarSubscriptOrFunc();
this.vardefs[lexpr.name] = lexpr;
this.validateVarName(lexpr);
return lexpr;
}
@ -701,7 +710,7 @@ export class BASICParser {
case TokenType.Int:
var label = tok.str;
this.targets[label] = tok.$loc;
return {value:label};
return {valtype:'label', value:label};
default:
var what = this.opts.optionalLabels ? "label or line number" : "line number";
this.compileError(`There should be a ${what} here.`);
@ -723,7 +732,7 @@ export class BASICParser {
}
if (tok.str == '-' && this.peekToken().type <= TokenType.HexOctalInt) {
tok = this.consumeToken();
return { value: -this.parseValue(tok).value };
return { valtype:'number', value: -this.parseValue(tok).value };
}
if (tok.str == '+' && this.peekToken().type <= TokenType.HexOctalInt) {
tok = this.consumeToken();
@ -737,7 +746,7 @@ export class BASICParser {
tok = this.consumeToken();
}
this.pushbackToken(tok);
return { value: s }; // trim leading and trailing whitespace
return { valtype:'string', value: s }; // trim leading and trailing whitespace
}
parseValue(tok: Token): Literal {
switch (tok.type) {
@ -745,14 +754,14 @@ export class BASICParser {
if (!this.opts.hexOctalConsts)
this.dialectErrorNoSupport(`hex/octal constants`);
let base = tok.str.startsWith('H') ? 16 : 8;
return { value: parseInt(tok.str.substr(1), base) };
return { valtype:'number', value: parseInt(tok.str.substr(1), base) };
case TokenType.Int:
case TokenType.Float:
return { value: this.parseNumber(tok.str) };
return { valtype:'number', value: this.parseNumber(tok.str) };
case TokenType.String:
return { value: stripQuotes(tok.str) };
return { valtype:'string', value: stripQuotes(tok.str) };
default:
return { value: tok.str }; // only used in DATA statement
return { valtype:'string', value: tok.str }; // only used in DATA statement
}
}
parsePrimary(): Expr {
@ -766,7 +775,7 @@ export class BASICParser {
case TokenType.Ident:
if (tok.str == 'NOT') {
let expr = this.parsePrimary();
return { op: this.opts.bitwiseLogic ? 'bnot' : 'lnot', expr: expr };
return { valtype:'number', op: this.opts.bitwiseLogic ? 'bnot' : 'lnot', expr: expr };
} else {
this.pushbackToken(tok);
return this.parseVarSubscriptOrFunc();
@ -778,7 +787,7 @@ export class BASICParser {
return expr;
} else if (tok.str == '-') {
let expr = this.parsePrimary(); // TODO: -2^2=-4 and -2-2=-4
return { op: 'neg', expr: expr };
return { valtype:'number', op: 'neg', expr: expr };
} else if (tok.str == '+') {
return this.parsePrimary(); // ignore unary +
}
@ -786,7 +795,6 @@ export class BASICParser {
this.compileError(`The expression is incomplete.`);
return;
}
this.compileError(`There was an unexpected "${tok.str}" in this expression.`);
}
parseNumber(str: string) : number {
var n = parseFloat(str);
@ -812,13 +820,20 @@ export class BASICParser {
// use logical operators instead of bitwise?
if (!this.opts.bitwiseLogic && op.str == 'AND') opfn = 'land';
if (!this.opts.bitwiseLogic && op.str == 'OR') opfn = 'lor';
left = { op:opfn, left: left, right: right };
var valtype = this.exprTypeForOp(opfn, left, right);
left = { valtype:valtype, op:opfn, left: left, right: right };
}
return left;
}
parseExpr(): Expr {
return this.parseExpr1(this.parsePrimary(), 0);
}
parseExprWithType(expecttype: ValueType): Expr {
var expr = this.parseExpr();
if (expr.valtype != expecttype)
this.compileError(`There should be a ${expecttype} here, but this expression evaluates to a ${expr.valtype}.`, expr.$loc);
return expr;
}
validateVarName(lexpr: IndOp) {
switch (this.opts.varNaming) {
case 'A': // TINY BASIC, no strings
@ -853,6 +868,33 @@ export class BASICParser {
}
callback(expr);
}
// type-checking
exprTypeForOp(fnname: string, left: Expr, right: Expr) : ValueType {
if (fnname == 'add' && (left.valtype == 'string' || right.valtype == 'string')) {
if (!this.opts.stringConcat) this.dialectErrorNoSupport(`the "+" operator to concatenate strings`);
return 'string'; // string concatenation
} else {
return 'number';
}
}
exprTypeForSubscript(fnname: string, args: Expr[]) : ValueType {
args = args || [];
// first check the built-in functions
var defs = BUILTIN_MAP[fnname];
if (defs != null) {
if (!this.validFunction(fnname)) this.dialectErrorNoSupport(`the ${fnname} function`);
for (var def of defs) {
if (args.length == def.args.length)
return def.result; // TODO: check arg types?
}
// TODO: check func arg types
this.compileError(`The ${fnname} function takes ${def.args.length} arguments, but ${args.length} are given.`);
}
// no function found, assume it's an array ref
// TODO: validateVarName() later?
this.varrefs[fnname] = this.lasttoken.$loc; // TODO?
return fnname.endsWith('$') ? 'string' : 'number';
}
//// STATEMENTS
@ -864,7 +906,7 @@ export class BASICParser {
lexprs.push(this.parseLexpr());
this.expectToken("=");
}
var right = this.parseExpr();
var right = this.parseExprWithType(lexprs[0].valtype);
return { command: "LET", lexprs: lexprs, right: right };
}
stmt__PRINT(): PRINT_Statement {
@ -936,7 +978,7 @@ export class BASICParser {
// assume GOTO if number given after THEN
if (lineno.type == TokenType.Int) {
this.parseLabel();
var gotostmt : GOTO_Statement = { command:'GOTO', label: {value:lineno.str} }
var gotostmt : GOTO_Statement = { command:'GOTO', label: {valtype:'label', value:lineno.str} }
this.addStatement(gotostmt, lineno);
} else {
// parse rest of IF clause
@ -1000,7 +1042,7 @@ export class BASICParser {
this.pushbackToken(prompt);
promptstr = "";
}
return { command:'INPUT', prompt:{ value: promptstr }, args:this.parseLexprList() };
return { command:'INPUT', prompt:{ valtype:'string', value: promptstr }, args:this.parseLexprList() };
}
/* for HP BASIC only */
stmt__ENTER() : INPUT_Statement {
@ -1045,6 +1087,12 @@ export class BASICParser {
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.`);
if (!lexpr.name.startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
// local variables need to be marked as referenced (TODO: only for this scope)
this.vardefs[lexpr.name] = lexpr;
if (lexpr.args != null)
for (let arg of lexpr.args)
if (isLookup(arg))
this.vardefs[arg.name] = arg;
this.expectToken("=");
var func = this.parseExpr();
// build call graph to detect cycles
@ -1163,6 +1211,7 @@ export class BASICParser {
checkAll(program : BASICProgram) {
this.checkLabels();
this.checkScopes();
this.checkVarRefs();
}
checkLabels() {
for (let targ in this.targets) {
@ -1179,6 +1228,14 @@ export class BASICParser {
this.compileError(`Don't forget to add a matching ${close[open.command]} statement.`, open.$loc);
}
}
checkVarRefs() {
if (!this.opts.defaultValues) {
for (var varname in this.varrefs) {
if (this.vardefs[varname] == null)
this.compileError(`The variable ${varname} isn't defined anywhere in the program.`, this.varrefs[varname]);
}
}
}
}
///// BASIC DIALECTS
@ -1779,6 +1836,63 @@ export const MODERN_BASIC : BASICOptions = {
// TODO: excess INPUT ignored, error msg
// TODO: out of order line numbers
type BuiltinFunctionDef = [string, ValueType[], ValueType];
const BUILTIN_DEFS : BuiltinFunctionDef[] = [
['ABS', ['number'], 'number' ],
['ASC', ['string'], 'number' ],
['ATN', ['number'], 'number' ],
['CHR$', ['number'], 'string' ],
['CINT', ['number'], 'number' ],
['COS', ['number'], 'number' ],
['COT', ['number'], 'number' ],
['CTL', ['number'], 'string' ],
['EXP', ['number'], 'number' ],
['FIX', ['number'], 'number' ],
['HEX$', ['number'], 'string' ],
['INSTR', ['number', 'string', 'string'], 'number' ],
['INSTR', ['string', 'string'], 'number' ],
['INT', ['number'], 'number' ],
['LEFT$', ['string', 'number'], 'string' ],
['LEN', ['string'], 'number' ],
['LIN', ['number'], 'string' ],
['LOG', ['number'], 'number' ],
['LOG10', ['number'], 'number' ],
['MID$', ['string', 'number'], 'string'],
['MID$', ['string', 'number', 'number'], 'string'],
['OCT$', ['number'], 'string' ],
['PI', [], 'number'],
['POS', ['number'], 'number' ], // arg ignored
['LEFT$', ['string', 'number'], 'string' ],
['RND', [], 'number' ],
['RND', ['number'], 'number' ],
['ROUND', ['number'], 'number' ],
['SGN', ['number'], 'number' ],
['SIN', ['number'], 'number' ],
['SPACE$', ['number'], 'string' ],
['SPC', ['number'], 'string' ],
['SQR', ['number'], 'number' ],
['STR$', ['number'], 'string' ],
['STRING$', ['number', 'number'], 'string'],
['STRING$', ['number', 'string'], 'string'],
['TAB', ['number'], 'string' ],
['TAN', ['number'], 'number' ],
['TIM', ['number'], 'number' ], // only HP BASIC?
['TIMER', [], 'number' ],
['UPS$', ['string'], 'string' ],
['VAL', ['string'], 'number' ],
['LPAD$', ['string', 'number'], 'string' ],
['RPAD$', ['string', 'number'], 'string' ],
['NFORMAT$', ['number', 'number'], 'string' ],
];
var BUILTIN_MAP : { [name:string] : {args:ValueType[], result:ValueType}[] } = {};
BUILTIN_DEFS.forEach( (def, idx) => {
let [name, args, result] = def;
if (!BUILTIN_MAP[name]) BUILTIN_MAP[name] = [];
BUILTIN_MAP[name].push({args: args, result: result});
});
export const DIALECTS = {
"DEFAULT": MODERN_BASIC,
"DARTMOUTH": DARTMOUTH_4TH_EDITION,

View File

@ -1142,10 +1142,10 @@ export class BASICRuntime {
}
MID$(arg : string, start : number, count : number) : string {
arg = this.checkString(arg);
if (!count) count = arg.length;
start = this.ROUND(start);
count = this.ROUND(count);
if (start < 1) this.runtimeError(`I can't compute MID$ if the starting index is less than 1.`)
if (count == 0) count = arg.length;
return arg.substr(start-1, count);
}
OCT$(arg : number) : string {
@ -1209,7 +1209,7 @@ export class BASICRuntime {
TAN(arg : number) : number {
return this.checkNum(Math.tan(arg));
}
TIM(arg : number) { // only HP BASIC?
TIM(arg : number) : number { // only HP BASIC?
var d = new Date();
switch (this.ROUND(arg)) {
case 0: return d.getMinutes();