basic: got rid of BASICLine in favor of list of statements, multipleStmtsPerLine

This commit is contained in:
Steven Hugg 2020-08-19 12:05:30 -05:00
parent e5de701419
commit 74238b334f
4 changed files with 202 additions and 96 deletions

View File

@ -1,12 +1,99 @@
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!"
35 PRINT
40 INPUT "NOW TELL ME YOUR FAVORITE NUMBER";N
45 PRINT
50 PRINT "THAT'S A GOOD ONE! I LIKE";N^2;"MYSELF."
60 PRINT "NICE MEETING YOU, ";A$;"."
001 REM THIS IS A PROGRAM WRITTEN IN BASIC.
002 REM YOU CAN WRITE ONE STATEMENT PER LINE.
003 REM EACH STATEMENT MUST HAVE A LINE NUMBER.
004 REM THE "REM" LINES ARE COMMENTS, WHICH ARE IGNORED
005 ' YOU CAN ALSO DO COMMENTS WITH APOSTROPHES
006 ' LET'S START WITH A "PRINT"...
100 PRINT "HELLO! LET'S PROGRAM IN BASIC."
101 PRINT ' <-- THIS PRINTS A BLANK LINE, IT LOOKS NICER
105 ' "INPUT" WAITS FOR USER INPUT FROM THE KEYBOARD
106 ' THE NUMBER ENTERED WILL GO INTO THE VARIABLE "N"
110 INPUT "FIRST, TELL ME YOUR FAVORITE NUMBER";N
111 PRINT
115 ' YOU CAN DO CALCULATIONS WITH THE + - / * OPERATORS
116 ' THE ^ IS AN EXPONENT, SO N^2 IS N-SQUARED
119 ' "LET" ASSIGNS THE RESULT TO A VARIABLE
120 LET Z = N^2
121 ' THE SEMICOLON ";" SEPARATES THINGS TO BE PRINTED
122 PRINT "THAT'S A GOOD ONE! I LIKE";Z;"MYSELF."
123 PRINT
125 ' IF A VARIABLE ENDS IN "$" IT IS A STRING
126 ' AND CONTAINS CHARACTERS INSTEAD OF A NUMBER
127 ' LET'S READ FROM THE KEYBOARD INTO THE A$ VARIABLE
130 INPUT "WOULD YOU MIND TYPING IN YOUR NAME";A$
131 PRINT
135 ' WE CAN PRINT MULTIPLE THINGS PER LINE
136 ' IN THIS BASIC DIALECT, NUMBERS ARE PRINTED
137 ' WITH SPACES BEFORE AND AFTER
140 PRINT "GOOD TO MEET YOU, ";A$;" WHO LOVES";N;"!"
141 PRINT
150 INPUT "WOULD YOU LIKE TO SEE ME CALCULATE";B$
151 PRINT
155 ' "IF" CAN TEST A CONDITION
156 ' AND JUMP TO A DIFFERENT LINE IF IT IS TRUE
160 IF B$ = "Y" THEN 180
161 IF B$ = "YES" THEN 180
170 PRINT "TOO BAD, ";A$;"! WE'RE DOING THIS."
180 PRINT
190 PRINT "WATCH ME COUNT..."
195 ' THIS "FOR" STATEMENT STARTS A LOOP WHICH SETS
196 ' VARIABLE I TO 1 AND COUNTS TO 50
200 FOR I = 1 TO 50
210 PRINT I, ' USE A COMMA FOR NEAT PRINT ZONES
220 NEXT I ' REPEAT LOOP
225 ' USING THE COMMA (OR SEMICOLON) IN THE PRINT STATEMENT
226 ' KEEPS THE CURSOR ON THE SAME LINE, SO WE PRINT
227 ' A BLANK LINE TO RETURN TO THE LEFT MARGIN
230 PRINT
240 PRINT "I CAN ALSO GRAPH TRIG FUNCTIONS!"
245 ' YOU CAN DEFINE YOUR OWN FUNCTIONS WITH "DEF"
246 ' FUNCTIONS MUST START WITH "FN"
250 DEF FNY(X) = (SIN(X*0.3)+1)*30+2
260 FOR I = 1 TO 25
270 PRINT TAB(FNY(I));"*"
280 NEXT I
300 ' YOU CAN DEFINE AN ARRAY WITH "DIM"
301 ' YOU CAN HAVE ARRAYS OF EITHER NUMBERS OR STRINGS
310 DIM T$(15),A$(15),W(15)
315 ' "READ" LOADS FROM "DATA" STATEMENTS
316 ' DATA CAN BE ANYWHERE, OURS IS AT LINE 900
320 FOR I = 1 TO 15
330 READ T$(I),A$(I),W(I)
340 NEXT I
350 ' NOW THE ARRAYS ARE FILLED WITH DATA
351 ' SO LET'S PICK A VALUE AT RANDOM
352 ' THE RND FUNCTION RETURNS A RANDOM NUMBER
353 ' FIRST WE USE "RANDOMIZE" TO INITIALIZE THE
354 ' RANDOM NUMBER GENERATOR
360 RANDOMIZE
362 ' "LET" ASSIGNS A VARIABLE FROM AN EXPRESSION
370 LET J = INT(15*RND)
380 PRINT "MY FAVORITE SONG IS '";T$(J);"'"
390 PRINT "BY ";A$(J);", DO YOU KNOW IT?"
400 PRINT "IT SPENT";W(J);"WEEKS ON THE CHARTS."
410 PRINT
890 ' HERE'S THE DATA WE READ EARLIER
891 ' WE CAN USE NUMBERS, QUOTED OR UNQUOTED STRINGS
900 DATA "HELLO, GOODBYE",THE BEATLES,2
901 DATA "JUDY IN DISGUISE",JOHN FRED AND HIS PLAYBOY BAND,2
902 DATA "GREEN TAMBOURINE",THE LEMON PIPERS,1
903 DATA "LOVE IS BLUE",PAUL MAURIAT,5
904 DATA "(SITTIN' ON) THE DOCK OF THE BAY",OTIS REDDING,4
905 DATA "HONEY",BOBBY GOLDSBORO,5
906 DATA "TIGHTEN UP",ARCHIE BELL & THE DRELLS,3
907 DATA "MRS. ROBINSON",SIMON & GARFUNKEL,3
908 DATA "THIS GUY'S IN LOVE WITH YOU",HERB ALPERT,4
909 DATA "GRAZING IN THE GRASS",HUGH MASEKELA,2
910 DATA "HELLO, I LOVE YOU",THE DOORS,2
911 DATA "PEOPLE GOT TO BE FREE",THE RASCALS,5
912 DATA "HARPER VALLEY PTA",JEANNIE C. RILEY,1
913 DATA "HEY JUDE",THE BEATLES,9
914 DATA "LOVE CHILD",DIANA ROSS & THE SUPREMES,2
915 DATA "I HEARD IT THROUGH THE GRAPEVINE",MARVIN GAYE,3
979 ' WE'RE DONE, SAY GOODBYE TO THE USER
980 PRINT "I APOLOGIZE, BUT I AM FATIGUED FROM ALL OF THIS"
981 PRINT "CALCULATING. I SHALL TAKE MY LEAVE OF YOU NOW."
990 PRINT "NICE MEETING YOU, ";A$;"."
998 ' PROGRAMS MUST END WITH "END" IN THIS DIALECT
999 END

View File

@ -8,6 +8,7 @@ export interface BASICOptions {
uppercaseOnly : boolean; // convert everything to uppercase?
optionalLabels : boolean; // can omit line numbers and use labels?
optionalWhitespace : boolean; // can "crunch" keywords? also, eat extra ":" delims
multipleStmtsPerLine : boolean; // multiple statements separated by ":"
varNaming : 'A'|'A1'|'AA'|'*'; // only allow A0-9 for numerics, single letter for arrays/strings
squareBrackets : boolean; // "[" and "]" interchangable with "(" and ")"?
tickComments : boolean; // support 'comments?
@ -234,14 +235,10 @@ export type ScopeStartStatement = (IF_Statement | FOR_Statement | WHILE_Statemen
export type ScopeEndStatement = NEXT_Statement | WEND_Statement;
export interface BASICLine {
label: string;
stmts: Statement[];
}
export interface BASICProgram {
opts: BASICOptions;
lines: BASICLine[];
stmts: Statement[];
labels: { [label: string]: number }; // label -> PC
}
class Token {
@ -326,9 +323,10 @@ export class BASICParser {
opts : BASICOptions = DIALECTS['DEFAULT'];
optionCount : number; // how many OPTION stmts so far?
maxlinelen : number = 255; // maximum line length
stmts : Statement[];
errors: WorkerError[];
listings: CodeListingMap;
labels: { [label: string]: BASICLine };
labels: { [label: string]: number }; // label -> PC
targets: { [targetlabel: string]: SourceLocation };
varrefs: { [name: string]: SourceLocation }; // references
fnrefs: { [name: string]: string[] }; // DEF FN call graph
@ -343,11 +341,12 @@ export class BASICParser {
constructor() {
this.optionCount = 0;
this.lineno = 0;
this.curlabel = null;
this.stmts = [];
this.labels = {};
this.targets = {};
this.errors = [];
this.lineno = 0;
this.curlabel = null;
this.listings = {};
this.varrefs = {};
this.fnrefs = {};
@ -394,7 +393,8 @@ export class BASICParser {
pushbackToken(tok: Token) {
this.tokens.unshift(tok);
}
parseOptLabel(line: BASICLine) {
// this parses either a line number or "label:" -- or adds a default label to a line
parseOptLabel() {
let tok = this.consumeToken();
switch (tok.type) {
case TokenType.Ident:
@ -410,8 +410,9 @@ export class BASICParser {
} else
this.dialectError(`Each line must begin with a line number`);
case TokenType.Int:
this.setCurrentLabel(line, tok.str);
break;
this.addLabel(tok.str);
return;
// label added, return from function... other cases add default label
case TokenType.HexOctalInt:
case TokenType.Float:
this.compileError(`Line numbers must be positive integers.`);
@ -429,30 +430,41 @@ export class BASICParser {
case TokenType.Remark:
break;
}
// add default label
this.addLabel('#'+this.lineno);
}
setCurrentLabel(line: BASICLine, str: string) {
if (this.labels[str] != null) this.compileError(`There's a duplicated label "${str}".`);
this.labels[str] = line;
line.label = str;
getPC() : number {
return this.stmts.length;
}
addStatement(stmt: Statement, cmdtok: Token, endtok?: Token) {
// check IF/THEN WHILE/WEND FOR/NEXT etc
this.modifyScope(stmt);
// set location for statement
if (endtok == null) endtok = this.peekToken();
stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: endtok.$loc.start, label: this.curlabel, offset: null };
this.stmts.push(stmt);
}
addLabel(str: string) {
if (this.labels[str] != null) this.compileError(`There's a duplicated label named "${str}".`);
this.labels[str] = this.getPC();
this.curlabel = str;
this.tokens.forEach((tok) => tok.$loc.label = str);
}
parseFile(file: string, path: string) : BASICProgram {
this.path = path;
var txtlines = file.split(/\n|\r\n?/);
var pgmlines = txtlines.map((line) => this.parseLine(line));
var program = { opts: this.opts, lines: pgmlines };
txtlines.forEach((line) => this.parseLine(line));
var program = { opts: this.opts, stmts: this.stmts, labels: this.labels };
this.checkAll(program);
this.listings[path] = this.generateListing(file, program);
return program;
}
parseLine(line: string) : BASICLine {
parseLine(line: string) : void {
try {
this.tokenize(line);
return this.parse();
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 {
@ -517,27 +529,25 @@ export class BASICParser {
if (line.length > this.maxlinelen) this.compileError(`A line should be no more than ${this.maxlinelen} characters long.`);
this._tokenize(line);
}
parse() : BASICLine {
var line = {label: null, stmts: []};
parse() : void {
// not empty line?
if (this.tokens.length) {
this.parseOptLabel(line);
this.parseOptLabel();
if (this.tokens.length) {
line.stmts = this.parseCompoundStatement();
this.parseCompoundStatement();
}
var next = this.peekToken();
if (!isEOS(next)) this.compileError(`Expected end of line or ':'`, next.$loc);
this.curlabel = null;
}
return line;
}
parseCompoundStatement(): Statement[] {
var list = this.parseList(this.parseStatement, ':');
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;
parseCompoundStatement() : void {
if (this.opts.multipleStmtsPerLine) {
this.parseList(this.parseStatement, ':');
} else {
this.parseList(this.parseStatement, '\0');
if (this.peekToken().str == ':') this.dialectErrorNoSupport(`multiple statements on a line`);
}
}
validKeyword(keyword: string) : string {
return (this.opts.validKeywords && this.opts.validKeywords.indexOf(keyword) < 0) ? null : keyword;
@ -578,7 +588,6 @@ export class BASICParser {
if (this.validKeyword(cmd) == null)
this.dialectErrorNoSupport(`the ${cmd} statement`);
stmt = fn.bind(this)();
this.modifyScope(stmt);
break;
} else if (this.peekToken().str == '=' || this.peekToken().str == '(') {
if (!this.opts.optionalLet)
@ -596,7 +605,8 @@ export class BASICParser {
this.compileError(`There should be a command here.`);
return null;
}
if (stmt) stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start, label: this.curlabel, offset: null };
// add statement to list
if (stmt != null) this.addStatement(stmt, cmdtok);
return stmt;
}
// check scope stuff (if compiledBlocks is true)
@ -898,30 +908,39 @@ export class BASICParser {
return { command: cmd, label: expr };
}
}
stmt__IF(): IF_Statement {
stmt__IF(): void {
var cmdtok = this.lasttoken;
var cond = this.parseExpr();
var iftrue: Statement[];
var ifstmt = { command: "IF", cond: cond };
this.addStatement(ifstmt, cmdtok);
// we accept GOTO or THEN if line number provided (DEC accepts GO TO)
var thengoto = this.expectTokens(['THEN','GOTO','GO']);
if (thengoto.str == 'GO') this.expectToken('TO');
// parse line number or statement clause
this.parseGotoOrStatements();
// is the next statement an ELSE?
// gotta parse it now because it's an end-of-statement token
if (this.peekToken().str == 'ELSE') {
this.expectToken('ELSE');
this.stmt__ELSE();
}
}
stmt__ELSE(): void {
this.addStatement({ command: "ELSE" }, this.lasttoken);
// parse line number or statement clause
this.parseGotoOrStatements();
}
parseGotoOrStatements() {
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});
this.parseLabel();
var gotostmt : GOTO_Statement = { command:'GOTO', label: {value:lineno.str} }
this.addStatement(gotostmt, lineno);
} else {
// parse rest of IF clause
this.parseCompoundStatement();
}
// add fake ":"
this.pushbackToken({type:TokenType.Operator, str:':', $loc:lineno.$loc});
return { command: "IF", cond: cond };
}
stmt__ELSE(): ELSE_Statement {
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" };
}
stmt__FOR() : FOR_Statement {
var lexpr = this.parseForNextLexpr();
@ -1124,15 +1143,12 @@ export class BASICParser {
// for workermain
generateListing(file: string, program: BASICProgram) {
var srclines = [];
var pc = 0;
var laststmt : Statement;
program.lines.forEach((line, idx) => {
line.stmts.forEach((stmt) => {
laststmt = stmt;
var sl = stmt.$loc;
sl.offset = pc++; // TODO: could Statement have offset field?
srclines.push(sl);
});
program.stmts.forEach((stmt, idx) => {
laststmt = stmt;
var sl = stmt.$loc;
sl.offset = idx;
srclines.push(sl);
});
if (this.opts.endStmtRequired && (laststmt == null || laststmt.command != 'END'))
this.dialectError(`All programs must have a final END statement`);
@ -1172,6 +1188,7 @@ export const ECMA55_MINIMAL : BASICOptions = {
uppercaseOnly : true,
optionalLabels : false,
optionalWhitespace : false,
multipleStmtsPerLine : false,
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : true,
@ -1220,6 +1237,7 @@ export const DARTMOUTH_4TH_EDITION : BASICOptions = {
uppercaseOnly : true,
optionalLabels : false,
optionalWhitespace : false,
multipleStmtsPerLine : false,
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : false,
@ -1271,6 +1289,7 @@ export const TINY_BASIC : BASICOptions = {
uppercaseOnly : true,
optionalLabels : false,
optionalWhitespace : false,
multipleStmtsPerLine : false,
varNaming : "A",
staticArrays : false,
sharedArrayNamespace : true,
@ -1317,6 +1336,7 @@ export const HP_TIMESHARED_BASIC : BASICOptions = {
uppercaseOnly : false, // the terminal is usually uppercase
optionalLabels : false,
optionalWhitespace : false,
multipleStmtsPerLine : true,
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : false,
@ -1370,6 +1390,7 @@ export const DEC_BASIC_11 : BASICOptions = {
uppercaseOnly : true, // translates all lower to upper
optionalLabels : false,
optionalWhitespace : false,
multipleStmtsPerLine : false, // actually "\"
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : false,
@ -1424,6 +1445,7 @@ export const DEC_BASIC_PLUS : BASICOptions = {
uppercaseOnly : false,
optionalLabels : false,
optionalWhitespace : false,
multipleStmtsPerLine : true,
varNaming : "A1",
staticArrays : true,
sharedArrayNamespace : false,
@ -1485,6 +1507,7 @@ export const BASICODE : BASICOptions = {
uppercaseOnly : false,
optionalLabels : false,
optionalWhitespace : true,
multipleStmtsPerLine : true,
varNaming : "AA",
staticArrays : true,
sharedArrayNamespace : false,
@ -1535,6 +1558,7 @@ export const ALTAIR_BASIC41 : BASICOptions = {
uppercaseOnly : true,
optionalLabels : false,
optionalWhitespace : true,
multipleStmtsPerLine : true,
varNaming : "*", // or AA
staticArrays : false,
sharedArrayNamespace : true,
@ -1593,6 +1617,7 @@ export const APPLESOFT_BASIC : BASICOptions = {
uppercaseOnly : false,
optionalLabels : false,
optionalWhitespace : true,
multipleStmtsPerLine : true,
varNaming : "*", // or AA
staticArrays : false,
sharedArrayNamespace : false,
@ -1652,6 +1677,7 @@ export const BASIC80 : BASICOptions = {
uppercaseOnly : false,
optionalLabels : false,
optionalWhitespace : true,
multipleStmtsPerLine : true,
varNaming : "*",
staticArrays : false,
sharedArrayNamespace : true,
@ -1712,6 +1738,7 @@ export const MODERN_BASIC : BASICOptions = {
uppercaseOnly : false,
optionalLabels : true,
optionalWhitespace : false,
multipleStmtsPerLine : true,
varNaming : "*",
staticArrays : false,
sharedArrayNamespace : false,
@ -1749,6 +1776,7 @@ export const MODERN_BASIC : BASICOptions = {
// TODO: integer vars
// TODO: DEFINT/DEFSTR
// TODO: excess INPUT ignored, error msg
// TODO: out of order line numbers
export const DIALECTS = {
"DEFAULT": MODERN_BASIC,

View File

@ -72,8 +72,6 @@ export class BASICRuntime {
program : basic.BASICProgram;
allstmts : basic.Statement[];
line2pc : number[];
pc2line : Map<number,number>;
pc2label : Map<number,string>;
label2pc : {[label : string] : number};
label2dataptr : {[label : string] : number};
@ -103,7 +101,7 @@ export class BASICRuntime {
let prevlabel = null;
let prevpcofs = 0;
if (this.pc2label != null) {
var pc = this.curpc;
let pc = this.curpc;
while (pc > 0 && (prevlabel = this.pc2label.get(pc)) == null) {
pc--;
}
@ -114,27 +112,18 @@ export class BASICRuntime {
this.program = program;
this.opts = program.opts;
if (!this.opts.maxArrayElements) this.opts.maxArrayElements = DEFAULT_MAX_ARRAY_ELEMENTS;
this.label2pc = {};
this.allstmts = program.stmts;
this.label2pc = program.labels;
this.label2dataptr = {};
this.allstmts = [];
this.line2pc = [];
this.pc2line = new Map();
this.pc2label = new Map();
this.datums = [];
this.builtins = this.getBuiltinFunctions();
// TODO: detect undeclared vars
program.lines.forEach((line, idx) => {
// make lookup tables
var pc = this.allstmts.length;
if (line.label != null) {
this.label2pc[line.label] = pc;
this.pc2label.set(pc, line.label);
}
this.line2pc.push(pc);
this.pc2line.set(pc, idx);
// combine all statements into single list
line.stmts.forEach((stmt) => this.allstmts.push(stmt));
});
// build PC -> label lookup
for (var label in program.labels) {
var targetpc = program.labels[label];
this.pc2label.set(targetpc, label);
}
// compile statements ahead of time
this.allstmts.forEach((stmt, pc) => {
this.curpc = pc + 1; // for error reporting
@ -278,10 +267,11 @@ export class BASICRuntime {
stmt.$run();
}
// TODO: this only works because each line has a label
skipToEOL() {
do {
this.curpc++;
} while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
} while (this.curpc < this.allstmts.length && !this.pc2label.get(this.curpc));
}
skipToElse() {
@ -292,7 +282,7 @@ export class BASICRuntime {
if (cmd == 'ELSE') { this.curpc++; break; }
else if (cmd == 'IF') return this.skipToEOL();
this.curpc++;
if (this.pc2line.get(this.curpc))
if (this.pc2label.get(this.curpc))
break;
}
}
@ -380,9 +370,10 @@ export class BASICRuntime {
this.column = 0;
str = obj;
} else if (obj == '\t') {
var curgroup = Math.floor(this.column / this.opts.printZoneLength);
var l = this.opts.printZoneLength;
var curgroup = Math.floor(this.column / l);
var nextcol = (curgroup + 1) * this.opts.printZoneLength;
if (nextcol >= this.margin) { this.column = 0; str = "\n"; } // return to left margin
if (nextcol+l > this.margin) { this.column = 0; str = "\n"; } // return to left margin
else str = this.TAB(nextcol); // next column
} else {
str = `${obj}`;

View File

@ -8,7 +8,7 @@ import { BASICProgram } from "../common/basic/compiler";
import { TeleTypeWithKeyboard } from "../common/teletype";
const BASIC_PRESETS = [
{ id: 'hello.bas', name: 'Hello World' },
{ id: 'hello.bas', name: 'Tutorial' },
{ id: 'sieve.bas', name: 'Sieve Benchmark' },
{ id: 'mortgage.bas', name: 'Interest Calculator' },
{ id: '23match.bas', name: '23 Matches' },
@ -27,7 +27,7 @@ class BASICPlatform implements Platform {
clock: number = 0;
timer: AnimationTimer;
tty: TeleTypeWithKeyboard;
hotReload: boolean = true;
hotReload: boolean = false;
animcount: number = 0;
constructor(mainElement: HTMLElement) {