basic: fixes for non-line-number mode (24, 62, 81), handle unhandledrejection, MODERN default dialect, DARTMOUTH

This commit is contained in:
Steven Hugg 2020-08-16 12:16:29 -05:00
parent 88fa924507
commit 9cedb1af08
21 changed files with 228 additions and 102 deletions

View File

@ -22,6 +22,12 @@
.currentpc-marker {
color: #ff66ee;
}
.currentpc-span-blocked {
background-color: #7e2a70;
}
.currentpc-marker-blocked {
color: #ffee33;
}
.tooltipbox {
position: relative;
display: inline-block;

View File

@ -55,7 +55,8 @@
"test-worker": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli/testworker.js",
"test-platforms": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli/testplatforms.js",
"test-profile": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 --prof test/cli",
"start": "electron ."
"start": "electron .",
"fuzzbasic": "jsfuzz gen/common/basic/fuzz.js ~/basic/corpus/ --versifier false"
},
"main": "electron.js",
"keywords": [

View File

@ -1,3 +1,5 @@
OPTION DIALECT DEC
0 REM from the DEC EduSystem Handbook
100 REM***23 MATCHES
110 LET M=23
115 PRINT

View File

@ -1,5 +1,5 @@
OPTION DIALECT HP2000
0 REM http://www.bitsavers.org/bits/HP/paperTapes/JeffM/CRAPS.BAS
1 OPTION DIALECT HP2000
10 REM THIS IS A SPECIAL VERSION OF CRAPS
20 REM 1) MAXIMUM BANK IS $1000
30 REM 2) THE PLAYER GETS 10 CHANCES TO PLACE A BET

View File

@ -1,3 +1,4 @@
OPTION DIALECT ALTAIR
10 REM *** CONVERTED FROM THE ORIGINAL FOCAL PROGRAM AND MODIFIED
20 REM *** FOR EDUSYSTEM 70 BY DAVID AHL, DIGITAL
30 REM *** MODIFIED FOR 8K MICROSOFT BASIC BY PETER TURNBULL

View File

@ -1,4 +1,4 @@
0 OPTION DIALECT DEC:REM http://www.bitsavers.org/bits/DEC/pdp10/games/
OPTION DIALECT DEC:REM http://www.bitsavers.org/bits/DEC/pdp10/games/
00001 PRINT " This is the latest version as of 2-Jun-82."
00005 DIM B$(30)
00010 F=0

View File

@ -1,8 +1,9 @@
OPTION DIALECT DARTMOUTH
10 PRINT "HELLO! LET'S PROGRAM IN BASIC."
15 PRINT
20 INPUT "WOULD YOU MIND TYPING IN YOUR NAME";A$
25 PRINT
30 PRINT "THANKS, ";A$;"! THIS WILL BE FUN!";CHR$(7)
30 PRINT "THANKS, ";A$;"! THIS WILL BE FUN!"
35 PRINT
40 INPUT "NOW TELL ME YOUR FAVORITE NUMBER";N
45 PRINT

View File

@ -1,4 +1,4 @@
0 OPTION DIALECT HP
OPTION DIALECT HP
1 REM **** HP BASIC PROGRAM LIBRARY **************************
2 REM
3 REM LANDER: ROCKET LANDING VEHICLE

View File

@ -1,4 +1,4 @@
1 OPTION DIALECT HP
OPTION DIALECT HP
10 PRINT "DANIEL O'ROURKE FEB. 23, 1977"
20 PRINT "MINI-COMPUTOR 102"
30 PRINT "SUBJECT: MORTGAGE PAYMENT"

View File

@ -1,5 +1,5 @@
001 OPTION DIALECT MODERN
002 OPTION CPUSPEED MAX
OPTION DIALECT MODERN
OPTION CPUSPEED MAX
010 REM***A PRIME NUMBER SIEVE BENCHMARK
020 T = TIMER
022 C = 0

View File

@ -1,8 +1,8 @@
OPTION DIALECT HP
OPTION BASE 0:REM I GUESS HP HAS ZERO BASE???
1 REM***STAR TRADER FROM
2 REM***http://www.dunnington.info/public/basicgames/
3 REM***2 chain files merged and ported to 8bitworkshop
5 OPTION DIALECT HP
6 OPTION BASE 0:REM I GUESS HP HAS ZERO BASE???
10010 DIM S[12,15],T[12,12],T$[72],B[3,12]
10020 REM COM W,D9,K9,X9,D1,X1,P9,T9,S9,Y9,H
10030 DIM M[6,3],C[6,3]:REM COM Y1,R9,G9,Q

View File

@ -1,6 +1,6 @@
OPTION DIALECT HP
1 REM from: http://www.dunnington.info/public/basicgames/WUMPUS.hp
2 REM extracted from HP library tape
5 OPTION DIALECT HP
10 REM- HUNT THE WUMPUS
20 PRINT "INSTRUCTIONS (Y-N)";
30 INPUT I$

View File

@ -238,7 +238,6 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
}
var external = {
electricChars:"dDpPtTfFeE ",
startState: function() {
return {
tokenize: tokenBase,
@ -246,8 +245,6 @@ CodeMirror.defineMode("basic", function(conf, parserConf) {
currentIndent: 0,
nextLineIndent: 0,
doInCurrentLine: false
};
},

View File

@ -11,6 +11,7 @@ export interface BASICOptions {
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
tickComments : boolean; // support 'comments?
hexOctalConsts : boolean; // support &H and &O integer constants?
optionalLet : boolean; // LET is optional
chainAssignments : boolean; // support A = B = value (HP2000)
validKeywords : string[]; // valid keywords (or null for accept all)
validFunctions : string[]; // valid functions (or null for accept all)
@ -18,7 +19,7 @@ export interface BASICOptions {
// VALUES AND OPERATORS
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
stringConcat : boolean; // can concat strings with "+" operator?
typeConvert : boolean; // type convert strings <-> numbers?
typeConvert : boolean; // type convert strings <-> numbers? (NOT USED)
checkOverflow : boolean; // check for overflow of numerics?
bitwiseLogic : boolean; // -1 = TRUE, 0 = FALSE, AND/OR/NOT done with bitwise ops
maxStringLength : number; // maximum string length in chars
@ -390,15 +391,17 @@ export class BASICParser {
let tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (this.opts.optionalLabels) {
if (this.peekToken().str == ':') { // is it a label:
if (this.opts.optionalLabels || tok.str == 'OPTION') {
// is it a "label :" and not a keyword like "PRINT : "
if (this.peekToken().str == ':' && !this.supportsCommand(tok.str)) {
this.consumeToken(); // eat the ":"
// fall through to the next case
} else {
this.pushbackToken(tok); // nope
break;
}
} else this.dialectError(`optional line numbers`);
} else
this.dialectError(`optional line numbers`);
case TokenType.Int:
this.setCurrentLabel(line, tok.str);
break;
@ -406,9 +409,16 @@ export class BASICParser {
case TokenType.Float:
this.compileError(`Line numbers must be positive integers.`);
break;
case TokenType.Operator:
if (this.supportsCommand(tok.str) && this.validKeyword(tok.str)) {
this.pushbackToken(tok);
break; // "?" is allowed
}
default:
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.`);
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.`);
case TokenType.Remark:
break;
}
@ -525,9 +535,9 @@ export class BASICParser {
validKeyword(keyword: string) : string {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
}
supportsKeyword(keyword: string) {
if (this['stmt__' + keyword] != null) return true;
return false;
supportsCommand(cmd: string) : () => Statement {
if (cmd == '?') return this.stmt__PRINT;
else return this['stmt__' + cmd];
}
parseStatement(): Statement | null {
// eat extra ":" (should have separate property for this)
@ -555,13 +565,15 @@ export class BASICParser {
cmd = 'GOSUB';
}
// lookup JS function for command
var fn = this['stmt__' + cmd];
var fn = this.supportsCommand(cmd);
if (fn) {
if (this.validKeyword(cmd) == null)
this.dialectError(`the ${cmd} keyword`);
stmt = fn.bind(this)() as Statement;
this.dialectError(`the ${cmd} statement`);
stmt = fn.bind(this)();
break;
} else if (this.peekToken().str == '=' || this.peekToken().str == '(') {
if (!this.opts.optionalLet)
this.dialectError(`assignments without a preceding LET`);
// 'A = expr' or 'A(X) = expr'
this.pushbackToken(cmdtok);
stmt = this.stmt__LET();
@ -631,23 +643,22 @@ export class BASICParser {
if (this.opts.computedGoto) {
// parse expression, but still add to list of label targets if constant
var expr = this.parseExpr();
if ((expr as Literal).value != null) {
this.targets[(expr as Literal).value] = this.lasttoken.$loc;
}
if (isLiteral(expr)) this.targets[expr.value] = this.lasttoken.$loc;
return expr;
}
// parse a single number or ident label
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
case TokenType.Int:
var label = tok.str;
this.targets[label] = tok.$loc;
return {value:label};
default:
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.`);
} else {
// parse a single number or ident label
var tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
if (!this.opts.optionalLabels) this.dialectError(`labels other than line numbers`)
case TokenType.Int:
var label = tok.str;
this.targets[label] = tok.$loc;
return {value:label};
default:
var what = this.opts.optionalLabels ? "label or line number" : "line number";
this.compileError(`There should be a ${what} here.`);
}
}
}
parseDatumList(): Literal[] {
@ -841,13 +852,14 @@ export class BASICParser {
__GO(cmd: "GOTO"|"GOSUB"): GOTO_Statement | GOSUB_Statement | ONGO_Statement {
var expr = this.parseLabel();
// GOTO (expr) OF (labels...)
if (this.opts.computedGoto && this.peekToken().str == 'OF') {
if (this.peekToken().str == this.validKeyword('OF')) {
this.expectToken('OF');
let newcmd : 'ONGOTO'|'ONGOSUB' = (cmd == 'GOTO') ? 'ONGOTO' : 'ONGOSUB';
return { command:newcmd, expr:expr, labels:this.parseLabelList() };
return { command: newcmd, expr: expr, labels: this.parseLabelList() };
} else {
// regular GOTO or GOSUB
return { command: cmd, label: expr };
}
// regular GOTO or GOSUB
return { command: cmd, label: expr };
}
stmt__IF(): IF_Statement {
var cond = this.parseExpr();
@ -1100,7 +1112,8 @@ export class BASICParser {
checkLabels() {
for (let targ in this.targets) {
if (this.labels[targ] == null) {
this.addError(`There isn't a line number ${targ}.`, this.targets[targ]);
var what = this.opts.optionalLabels && isNaN(parseInt(targ)) ? "label named" : "line number";
this.addError(`There isn't a ${what} ${targ}.`, this.targets[targ]);
}
}
}
@ -1152,6 +1165,56 @@ export const ECMA55_MINIMAL : BASICOptions = {
arraysContainChars : false,
endStmtRequired : true,
chainAssignments : false,
optionalLet : false,
}
export const DARTMOUTH_4TH_EDITION : BASICOptions = {
dialectName: "DARTMOUTH4",
asciiOnly : true,
uppercaseOnly : true,
optionalLabels : false,
optionalWhitespace : false,
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : false,
defaultArrayBase : 0,
defaultArraySize : 11,
defaultValues : false,
stringConcat : false,
typeConvert : false,
maxDimensions : 2,
maxDefArgs : 255,
maxStringLength : 255,
tickComments : true,
hexOctalConsts : 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',
'CHANGE','MAT','RANDOM','RESTORE$','RESTORE*',
],
validFunctions : [
'ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAB','TAN',
'TRN','INV','DET','NUM','ZER', // NUM = # of strings input for MAT INPUT
],
validOperators : [
'=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'
],
printZoneLength : 15,
numericPadding : true,
checkOverflow : true,
testInitialFor : true,
optionalNextVar : false,
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : true,
computedGoto : false,
restoreWithLabel : false,
squareBrackets : false,
arraysContainChars : false,
endStmtRequired : true,
chainAssignments : true,
optionalLet : false,
}
// TODO: only integers supported
@ -1191,15 +1254,15 @@ export const TINY_BASIC : BASICOptions = {
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : false,
computedGoto : true, // TODO: is it really though?
computedGoto : true,
restoreWithLabel : false,
squareBrackets : false,
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : false,
}
export const HP_TIMESHARED_BASIC : BASICOptions = {
dialectName: "HP2000",
asciiOnly : true,
@ -1242,12 +1305,13 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
multipleNextVars : false,
bitwiseLogic : false,
checkOnGotoIndex : false,
computedGoto : true,
computedGoto : true, // not really, but we do parse expressions for GOTO ... OF
restoreWithLabel : true,
squareBrackets : true,
arraysContainChars : true,
endStmtRequired : true,
chainAssignments : true,
optionalLet : true,
// TODO: max line number, array index 9999
}
@ -1298,6 +1362,7 @@ export const DEC_BASIC_11 : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
// TODO: max line number 32767
// TODO: \ separator, % int vars and constants, 'single' quoted
// TODO: can't compare strings and numbers
@ -1356,6 +1421,7 @@ export const DEC_BASIC_PLUS : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false, // TODO: can chain with "," not "="
optionalLet : true,
// TODO: max line number 32767
// TODO: \ separator, % int vars and constants, 'single' quoted
// TODO: can't compare strings and numbers
@ -1408,6 +1474,7 @@ export const BASICODE : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const ALTAIR_BASIC41 : BASICOptions = {
@ -1464,6 +1531,7 @@ export const ALTAIR_BASIC41 : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const APPLESOFT_BASIC : BASICOptions = {
@ -1521,6 +1589,7 @@ export const APPLESOFT_BASIC : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const BASIC80 : BASICOptions = {
@ -1579,6 +1648,7 @@ export const BASIC80 : BASICOptions = {
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
optionalLet : true,
}
export const MODERN_BASIC : BASICOptions = {
@ -1594,7 +1664,7 @@ export const MODERN_BASIC : BASICOptions = {
defaultArraySize : 0, // DIM required
defaultValues : false,
stringConcat : true,
typeConvert : true,
typeConvert : false,
maxDimensions : 255,
maxDefArgs : 255,
maxStringLength : 2048, // TODO?
@ -1611,12 +1681,13 @@ export const MODERN_BASIC : BASICOptions = {
multipleNextVars : true,
bitwiseLogic : true,
checkOnGotoIndex : true,
computedGoto : true,
computedGoto : false,
restoreWithLabel : true,
squareBrackets : true,
arraysContainChars : false,
endStmtRequired : false,
chainAssignments : false,
chainAssignments : true,
optionalLet : true,
}
// TODO: integer vars
@ -1624,7 +1695,9 @@ export const MODERN_BASIC : BASICOptions = {
// TODO: excess INPUT ignored, error msg
export const DIALECTS = {
"DEFAULT": ALTAIR_BASIC41,
"DEFAULT": MODERN_BASIC,
"DARTMOUTH": DARTMOUTH_4TH_EDITION,
"DARTMOUTH4": DARTMOUTH_4TH_EDITION,
"ALTAIR": ALTAIR_BASIC41,
"ALTAIR4": ALTAIR_BASIC41,
"ALTAIR41": ALTAIR_BASIC41,

44
src/common/basic/fuzz.ts Normal file
View File

@ -0,0 +1,44 @@
import { BASICParser, DIALECTS, BASICOptions, CompileError } from "./compiler";
import { BASICRuntime } from "./runtime";
import { EmuHalt } from "../emu";
process.on('unhandledRejection', (reason, promise) => {
if (!(reason instanceof EmuHalt))
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or other logic here
});
export function fuzz(buf) {
var parser = new BASICParser();
var str = buf.toString();
try {
var pgm = parser.parseFile(str, "test.bas");
var runtime = new BASICRuntime();
runtime.load(pgm);
runtime.reset();
runtime.print = (s) => {
if (s == null) throw new Error("PRINT null string");
}
runtime.input = function(prompt: string, nargs: number) : Promise<string[]> {
var p = new Promise<string[]>( (resolve, reject) => {
var arr = [];
for (var i=0; i<Math.random()*10; i++)
arr.push(i+"");
resolve(arr);
});
return p;
}
for (var i=0; i<50000; i++) {
if (!runtime.step()) break;
}
if (Math.random() < 0.001) runtime.load(pgm);
for (var i=0; i<50000; i++) {
if (!runtime.step()) break;
}
} catch (e) {
if (e instanceof EmuHalt) return;
if (e instanceof CompileError) return;
throw e;
}
}

View File

@ -1,24 +0,0 @@
import { BASICParser } from "./compiler";
var parser = new BASICParser();
var readline = require('readline');
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', function (line) {
parser.tokenize(line);
console.log(parser.tokens);
try {
var ast = parser.parse();
console.log(JSON.stringify(ast, null, 4));
} catch (e) {
console.log(e);
}
if (parser.errors.length) {
console.log(parser.errors);
parser.errors = [];
}
})

View File

@ -21,14 +21,22 @@ var fs = require('fs');
var parser = new BASICParser();
var runtime = new BASICRuntime();
function getCurrentLabel() {
var loc = runtime.getCurrentSourceLocation();
return loc ? loc.label : "?";
}
// parse args
var filename = '/dev/stdin';
var args = process.argv.slice(2);
var force = false;
for (var i=0; i<args.length; i++) {
if (args[i] == '-v')
runtime.trace = true;
else if (args[i] == '-d')
parser.opts = DIALECTS[args[++i]] || Error('no such dialect');
else if (args[i] == '-f')
force = true;
else if (args[i] == '--dialects')
dumpDialectInfo();
else
@ -45,13 +53,13 @@ try {
console.log(`@@@ ${e}`);
}
parser.errors.forEach((err) => console.log(`@@@ ${err.msg} (line ${err.label})`));
if (parser.errors.length) process.exit(2);
if (parser.errors.length && !force) process.exit(2);
// run program
try {
runtime.load(pgm);
} catch (e) {
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
console.log(`### ${e.message} (line ${getCurrentLabel()})`);
process.exit(1);
}
runtime.reset();
@ -86,7 +94,7 @@ runtime.resume = function() {
process.exit(0);
}
} catch (e) {
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
console.log(`### ${e.message} (line ${getCurrentLabel()})`);
process.exit(1);
}
});
@ -98,7 +106,7 @@ runtime.resume();
function dumpDialectInfo() {
var dialects = new Set<BASICOptions>();
var array = {};
var SELECTED_DIALECTS = ['TINY','ECMA55','HP','DEC','ALTAIR','BASIC80','MODERN'];
var SELECTED_DIALECTS = ['TINY','ECMA55','DARTMOUTH','HP','DEC','ALTAIR','BASIC80','MODERN'];
SELECTED_DIALECTS.forEach((dkey) => {
dialects.add(DIALECTS[dkey]);
});
@ -121,7 +129,7 @@ function dumpDialectInfo() {
});
dialects.forEach((dialect) => {
ALL_KEYWORDS.forEach((keyword) => {
if (parser.supportsKeyword(keyword)) {
if (parser.supportsCommand(keyword)) {
var has = dialect.validKeywords == null || dialect.validKeywords.indexOf(keyword) >= 0;
keyword = '`'+keyword+'`'
if (!array[keyword]) array[keyword] = [];

View File

@ -473,7 +473,7 @@ export class BASICRuntime {
this.runtimeError(`I can't call a function here.`);
// is it a subscript?
if (expr.args) {
// set array slice (HP BASIC)
// TODO: set array slice (HP BASIC)
if (this.opts.arraysContainChars && expr.name.endsWith('$')) {
this.runtimeError(`I can't set array slices via this command yet.`);
} else {
@ -566,8 +566,8 @@ export class BASICRuntime {
// converts a variable to string/number based on var name
assign(name: string, right: number|string, isRead?:boolean) : number|string {
// convert data? READ always converts if read into string
if (this.opts.typeConvert || (isRead && name.endsWith("$")))
return this.convert(name, right);
if (isRead && name.endsWith("$"))
return this.checkValue(this.convert(name, right), name);
// TODO: use options
if (name.endsWith("$")) {
return this.convertToString(right, name);
@ -604,9 +604,9 @@ export class BASICRuntime {
if (this.opts.staticArrays) return;
else this.runtimeError(`I already dimensioned this array (${name}) earlier.`)
}
if (this.getTotalArrayLength(dims) > this.opts.maxArrayElements) {
var total = this.getTotalArrayLength(dims);
if (total > this.opts.maxArrayElements)
this.runtimeError(`I can't create an array with this many elements.`);
}
var isstring = name.endsWith('$');
// if numeric value, we use Float64Array which inits to 0
var arrcons = isstring ? Array : Float64Array;
@ -635,7 +635,7 @@ export class BASICRuntime {
getArray(name: string, order: number) : [] {
if (!this.arrays[name]) {
if (this.opts.defaultArraySize == 0)
this.dialectError(`automatically declare arrays without a DIM statement`);
this.dialectError(`automatically declare arrays without a DIM statement (or did you mean to call a function?)`);
if (order == 1)
this.dimArray(name, this.opts.defaultArraySize-1);
else if (order == 2)
@ -648,7 +648,7 @@ export class BASICRuntime {
arrayGet(name: string, ...indices: number[]) : basic.Value {
var arr = this.getArray(name, indices.length);
indices = indices.map(Math.round);
indices = indices.map(this.ROUND.bind(this));
var v = arr;
for (var i=0; i<indices.length; i++) {
var idx = indices[i];
@ -668,10 +668,20 @@ export class BASICRuntime {
// for HP BASIC string slicing (TODO?)
modifyStringSlice(orig: string, add: string, start: number, end: number) : string {
orig = orig || "";
return (orig + ' '.repeat(start)).substr(0, start-1) + add + orig.substr(end);
this.checkString(orig);
this.checkString(add);
if (!end) end = start;
start = this.ROUND(start);
end = this.ROUND(end);
if (start < 1 || end < 1) this.dialectError(`accept a string slice index less than 1`);
return (orig + ' '.repeat(start)).substr(0, start-1) + add.substr(0, end+1-start) + orig.substr(end);
}
getStringSlice(s: string, start: number, end: number) {
s = this.checkString(s);
start = this.ROUND(start);
end = this.ROUND(end);
if (start < 1 || end < 1) this.dialectError(`accept a string slice index less than 1`);
return s.substr(start-1, end+1-start);
}
@ -707,7 +717,8 @@ export class BASICRuntime {
var s = '';
for (var arg of stmt.args) {
var expr = this.expr2js(arg);
s += `this.printExpr(${expr});`;
var name = (expr as any).name;
s += `this.printExpr(this.checkValue(${expr}, ${JSON.stringify(name)}));`;
}
return s;
}
@ -751,7 +762,7 @@ export class BASICRuntime {
if (this.opts.arraysContainChars && lexpr.args && lexpr.name.endsWith('$')) {
s += `this.vars.${lexpr.name} = this.modifyStringSlice(this.vars.${lexpr.name}, _right, `
s += lexpr.args.map((arg) => this.expr2js(arg)).join(', ');
s += ')';
s += ');';
} else {
var ljs = this.assign2js(lexpr);
s += `${ljs} = this.assign(${JSON.stringify(lexpr.name)}, _right);`;
@ -956,7 +967,7 @@ export class BASICRuntime {
return exprname.endsWith("$") ? "" : 0;
}
if (exprname != null && obj == null) {
this.runtimeError(`I haven't set a value for ${exprname}.`);
this.runtimeError(`I haven't assigned a value to ${exprname}.`);
} else if (exprname != null) {
this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`);
} else {
@ -1193,7 +1204,7 @@ export class BASICRuntime {
len = this.ROUND(len);
if (len <= 0) return '';
if (len > this.opts.maxStringLength)
this.runtimeError(`I can't create a string longer than ${this.opts.maxStringLength} characters.`);
this.dialectError(`create a string longer than ${this.opts.maxStringLength} characters`);
if (typeof chr === 'string')
return chr.substr(0,1).repeat(len);
else

View File

@ -1901,7 +1901,8 @@ function globalErrorHandler(msgevent) {
if (msg.indexOf("QuotaExceededError") >= 0) {
requestPersistPermission(false, false);
} else {
var err = msgevent.error;
var err = msgevent.error || msgevent.reason;
msg = err.message || msg;
showExceptionAsError(err, msg);
}
}
@ -1909,10 +1910,12 @@ function globalErrorHandler(msgevent) {
// catch errors
function installErrorHandler() {
window.addEventListener('error', globalErrorHandler);
window.addEventListener('unhandledrejection', globalErrorHandler);
}
function uninstallErrorHandler() {
window.removeEventListener('error', globalErrorHandler);
window.removeEventListener('unhandledrejection', globalErrorHandler);
}
function gotoNewLocation(replaceHistory? : boolean) {

View File

@ -329,10 +329,12 @@ export class SourceEditor implements ProjectView {
}
setCurrentLine(line:SourceLocation, moveCursor:boolean) {
var blocked = platform.isBlocked && platform.isBlocked();
var addCurrentMarker = (line:SourceLocation) => {
var div = document.createElement("div");
div.classList.add('currentpc-marker');
var cls = blocked ? 'currentpc-marker-blocked' : 'currentpc-marker';
div.classList.add(cls);
div.appendChild(document.createTextNode("\u25b6"));
this.editor.setGutterMarker(line.line-1, "gutter-info", div);
}
@ -343,7 +345,8 @@ export class SourceEditor implements ProjectView {
if (moveCursor) {
this.editor.setCursor({line:line.line-1,ch:line.start||0}, {scroll:true});
}
var markOpts = {className:'currentpc-span', inclusiveLeft:true};
var cls = blocked ? 'currentpc-span-blocked' : 'currentpc-span';
var markOpts = {className:cls, inclusiveLeft:true};
if (line.start || line.end)
this.markCurrentPC = this.editor.markText({line:line.line-1,ch:line.start}, {line:line.line-1,ch:line.end||line.start+1}, markOpts);
else
@ -379,7 +382,7 @@ export class SourceEditor implements ProjectView {
refreshDebugState(moveCursor:boolean) {
// TODO: only if line changed
// TODO: remove after compilation restarts platform
// TODO: remove after compilation
this.clearCurrentLine(moveCursor);
var line = this.getActiveLine();
if (line) {

View File

@ -125,14 +125,14 @@ class BASICPlatform implements Platform {
// TODO: only hot reload when we hit a label?
var didExit = this.runtime.exited;
this.program = data;
this.runtime.load(data);
var resumePC = this.runtime.load(data);
this.tty.uppercaseOnly = true; // this.program.opts.uppercaseOnly; //TODO?
// map editor to uppercase-only if need be
views.textMapFunctions.input = this.program.opts.uppercaseOnly ? (s) => s.toUpperCase() : null;
// HP 2000 has cute lil small caps (TODO: messes up grid alignment tho)
//this.tty.page.style.fontVariant = (this.program.opts.dialectName == 'HP2000') ? 'small-caps' : 'normal';
// only reset if we exited, or couldn't restart at label (PC reset to 0)
if (!this.hotReload || didExit || this.runtime.curpc == 0)
if (!this.hotReload || didExit || !resumePC)
this.reset();
}
@ -156,7 +156,7 @@ class BASICPlatform implements Platform {
this.timer.start();
}
isBlocked() { return this.tty.waitingfor != null; } // is blocked for input?
isBlocked() { return this.tty.waitingfor != null || this.runtime.exited; } // is blocked for input?
isRunning() { return this.timer.isRunning(); }
getDefaultExtension() { return ".bas"; }
getToolForFilename() { return "basic"; }