mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-06-12 18:42:14 +00:00
first crack at BASIC
This commit is contained in:
parent
9ab7801fa8
commit
3a4b39b01c
|
@ -371,7 +371,7 @@ span.CodeMirror-selectedtext { background: none; }
|
||||||
|
|
||||||
.cm-s-mbo span.cm-property, .cm-s-mbo span.cm-attribute { color: #9ddfe9; }
|
.cm-s-mbo span.cm-property, .cm-s-mbo span.cm-attribute { color: #9ddfe9; }
|
||||||
.cm-s-mbo span.cm-keyword { color: #ffb928; }
|
.cm-s-mbo span.cm-keyword { color: #ffb928; }
|
||||||
.cm-s-mbo span.cm-string { color: #ffcf6c; }
|
.cm-s-mbo span.cm-string { color: #b4fdb7; }
|
||||||
.cm-s-mbo span.cm-string.cm-property { color: #ffffec; }
|
.cm-s-mbo span.cm-string.cm-property { color: #ffffec; }
|
||||||
|
|
||||||
.cm-s-mbo span.cm-variable { color: #ffffec; }
|
.cm-s-mbo span.cm-variable { color: #ffffec; }
|
||||||
|
|
11
css/ui.css
11
css/ui.css
|
@ -353,7 +353,7 @@ div.replaydiv {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 50%;
|
background-position: 50%;
|
||||||
pointer-events:auto;
|
pointer-events:auto;
|
||||||
z-index:1;
|
z-index:2;
|
||||||
}
|
}
|
||||||
.gutter.gutter-horizontal {
|
.gutter.gutter-horizontal {
|
||||||
background-image: url('grips/vertical.png');
|
background-image: url('grips/vertical.png');
|
||||||
|
@ -631,6 +631,15 @@ div.asset_toolbar {
|
||||||
.transcript-input-char {
|
.transcript-input-char {
|
||||||
max-width:5em;
|
max-width:5em;
|
||||||
}
|
}
|
||||||
|
.transcript-print-head {
|
||||||
|
background: url('../images/print-head.png') no-repeat scroll 0 0 transparent;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 3em;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.tree-header {
|
.tree-header {
|
||||||
border: 2px solid #555;
|
border: 2px solid #555;
|
||||||
border-radius:8px;
|
border-radius:8px;
|
||||||
|
|
|
@ -361,6 +361,7 @@ body {
|
||||||
<script src="src/codemirror/bataribasic.js"></script>
|
<script src="src/codemirror/bataribasic.js"></script>
|
||||||
<script src="src/codemirror/inform6.js"></script>
|
<script src="src/codemirror/inform6.js"></script>
|
||||||
<script src="src/codemirror/fastbasic.js"></script>
|
<script src="src/codemirror/fastbasic.js"></script>
|
||||||
|
<script src="src/codemirror/basic.js"></script>
|
||||||
<link rel="stylesheet" href="css/codemirror.css">
|
<link rel="stylesheet" href="css/codemirror.css">
|
||||||
<script src="codemirror/addon/edit/matchbrackets.js"></script>
|
<script src="codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
<script src="codemirror/addon/search/search.js"></script>
|
<script src="codemirror/addon/search/search.js"></script>
|
||||||
|
|
|
@ -556,6 +556,7 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
|
||||||
<script src="src/codemirror/bataribasic.js"></script>
|
<script src="src/codemirror/bataribasic.js"></script>
|
||||||
<script src="src/codemirror/inform6.js"></script>
|
<script src="src/codemirror/inform6.js"></script>
|
||||||
<script src="src/codemirror/fastbasic.js"></script>
|
<script src="src/codemirror/fastbasic.js"></script>
|
||||||
|
<script src="src/codemirror/basic.js"></script>
|
||||||
<link rel="stylesheet" href="css/codemirror.css">
|
<link rel="stylesheet" href="css/codemirror.css">
|
||||||
<script src="codemirror/addon/edit/matchbrackets.js"></script>
|
<script src="codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
<script src="codemirror/addon/search/search.js"></script>
|
<script src="codemirror/addon/search/search.js"></script>
|
||||||
|
|
10
presets/basic/hello.bas
Normal file
10
presets/basic/hello.bas
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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$;"."
|
286
src/codemirror/basic.js
Normal file
286
src/codemirror/basic.js
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
CodeMirror.defineMode("basic", function(conf, parserConf) {
|
||||||
|
var ERRORCLASS = 'error';
|
||||||
|
|
||||||
|
function wordRegexp(words) {
|
||||||
|
return new RegExp("^((" + words.join(")|(") + "))\\b", "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
var singleOperators = new RegExp("^[\\+\\-\\*/%&\\\\|\\^~<>!]");
|
||||||
|
var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]');
|
||||||
|
var doubleOperators = new RegExp("^((==)|(<>)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))");
|
||||||
|
var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))");
|
||||||
|
var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
|
||||||
|
var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
|
||||||
|
|
||||||
|
var openingKeywords = ['if','for'];
|
||||||
|
var middleKeywords = ['to'];
|
||||||
|
var endKeywords = ['next','end'];
|
||||||
|
|
||||||
|
var operatorKeywords = ['and', 'or', 'not', 'xor', 'in'];
|
||||||
|
var wordOperators = wordRegexp(operatorKeywords);
|
||||||
|
var commonKeywords = [
|
||||||
|
'let','print','go','goto','gosub','next','dim','input','data',
|
||||||
|
'read','restore','return','stop','on','def','option','then','step',
|
||||||
|
];
|
||||||
|
var commontypes = ['xxxxbyte','xxxxword'];
|
||||||
|
|
||||||
|
var keywords = wordRegexp(commonKeywords);
|
||||||
|
var types = wordRegexp(commontypes);
|
||||||
|
var stringPrefixes = '"';
|
||||||
|
|
||||||
|
var opening = wordRegexp(openingKeywords);
|
||||||
|
var middle = wordRegexp(middleKeywords);
|
||||||
|
var closing = wordRegexp(endKeywords);
|
||||||
|
var doubleClosing = wordRegexp(['end']);
|
||||||
|
var doOpening = wordRegexp(['do']);
|
||||||
|
|
||||||
|
var indentInfo = null;
|
||||||
|
|
||||||
|
CodeMirror.registerHelper("hintWords", "vb", openingKeywords.concat(middleKeywords).concat(endKeywords)
|
||||||
|
.concat(operatorKeywords).concat(commonKeywords).concat(commontypes));
|
||||||
|
|
||||||
|
function indent(_stream, state) {
|
||||||
|
state.currentIndent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedent(_stream, state) {
|
||||||
|
state.currentIndent--;
|
||||||
|
}
|
||||||
|
// tokenizers
|
||||||
|
function tokenBase(stream, state) {
|
||||||
|
if (stream.eatSpace()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ch = stream.peek();
|
||||||
|
|
||||||
|
// Handle Comments
|
||||||
|
if (ch === "'") {
|
||||||
|
stream.skipToEnd();
|
||||||
|
return 'comment';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Handle Number Literals
|
||||||
|
if (stream.match(/^(\$)?[0-9\.a-f]/i, false)) {
|
||||||
|
var floatLiteral = false;
|
||||||
|
// Floats
|
||||||
|
if (stream.match(/^\d*\.\d+F?/i)) { floatLiteral = true; }
|
||||||
|
else if (stream.match(/^\d+\.\d*F?/)) { floatLiteral = true; }
|
||||||
|
else if (stream.match(/^\.\d+F?/)) { floatLiteral = true; }
|
||||||
|
|
||||||
|
if (floatLiteral) {
|
||||||
|
// Float literals may be "imaginary"
|
||||||
|
stream.eat(/J/i);
|
||||||
|
return 'number';
|
||||||
|
}
|
||||||
|
// Integers
|
||||||
|
var intLiteral = false;
|
||||||
|
// Hex
|
||||||
|
if (stream.match(/^\$[0-9a-f]+/i)) { intLiteral = true; }
|
||||||
|
// Octal
|
||||||
|
else if (stream.match(/^&O[0-7]+/i)) { intLiteral = true; }
|
||||||
|
// Decimal
|
||||||
|
else if (stream.match(/^[1-9]\d*F?/)) {
|
||||||
|
// Decimal literals may be "imaginary"
|
||||||
|
stream.eat(/J/i);
|
||||||
|
// TODO - Can you have imaginary longs?
|
||||||
|
intLiteral = true;
|
||||||
|
}
|
||||||
|
// Zero by itself with no other piece of number.
|
||||||
|
else if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; }
|
||||||
|
if (intLiteral) {
|
||||||
|
// Integer literals may be "long"
|
||||||
|
stream.eat(/L/i);
|
||||||
|
return 'number';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle RAM
|
||||||
|
if (stream.match(/\bREM/i)) {
|
||||||
|
stream.skipToEnd();
|
||||||
|
return 'comment';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Strings
|
||||||
|
if (stream.match(stringPrefixes)) {
|
||||||
|
state.tokenize = tokenStringFactory(stream.current());
|
||||||
|
return state.tokenize(stream, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle operators and Delimiters
|
||||||
|
if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (stream.match(doubleOperators)
|
||||||
|
|| stream.match(singleOperators)
|
||||||
|
|| stream.match(wordOperators)) {
|
||||||
|
return 'operator';
|
||||||
|
}
|
||||||
|
if (stream.match(singleDelimiters)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (stream.match(doOpening)) {
|
||||||
|
/*
|
||||||
|
indent(stream,state);
|
||||||
|
state.doInCurrentLine = true;
|
||||||
|
*/
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
if (stream.match(opening)) {
|
||||||
|
/*
|
||||||
|
if (! state.doInCurrentLine)
|
||||||
|
indent(stream,state);
|
||||||
|
else
|
||||||
|
state.doInCurrentLine = false;
|
||||||
|
*/
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
if (stream.match(middle)) {
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.match(doubleClosing)) {
|
||||||
|
dedent(stream,state);
|
||||||
|
dedent(stream,state);
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
if (stream.match(closing)) {
|
||||||
|
dedent(stream,state);
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.match(types)) {
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.match(keywords)) {
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.match(identifiers)) {
|
||||||
|
return 'variable';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-detected items
|
||||||
|
stream.next();
|
||||||
|
return ERRORCLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenStringFactory(delimiter) {
|
||||||
|
var singleline = delimiter.length == 1;
|
||||||
|
var OUTCLASS = 'string';
|
||||||
|
|
||||||
|
return function(stream, state) {
|
||||||
|
while (!stream.eol()) {
|
||||||
|
stream.eatWhile(/[^'"]/);
|
||||||
|
if (stream.match(delimiter)) {
|
||||||
|
state.tokenize = tokenBase;
|
||||||
|
return OUTCLASS;
|
||||||
|
} else {
|
||||||
|
stream.eat(/['"]/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (singleline) {
|
||||||
|
if (parserConf.singleLineStringErrors) {
|
||||||
|
return ERRORCLASS;
|
||||||
|
} else {
|
||||||
|
state.tokenize = tokenBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OUTCLASS;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function tokenLexer(stream, state) {
|
||||||
|
var style = state.tokenize(stream, state);
|
||||||
|
var current = stream.current();
|
||||||
|
|
||||||
|
// Handle '.' connected identifiers
|
||||||
|
if (current === '.') {
|
||||||
|
style = state.tokenize(stream, state);
|
||||||
|
if (style === 'variable') {
|
||||||
|
return 'variable';
|
||||||
|
} else {
|
||||||
|
return ERRORCLASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var delimiter_index = '[({'.indexOf(current);
|
||||||
|
if (delimiter_index !== -1) {
|
||||||
|
indent(stream, state );
|
||||||
|
}
|
||||||
|
if (indentInfo === 'dedent') {
|
||||||
|
if (dedent(stream, state)) {
|
||||||
|
return ERRORCLASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delimiter_index = '])}'.indexOf(current);
|
||||||
|
if (delimiter_index !== -1) {
|
||||||
|
if (dedent(stream, state)) {
|
||||||
|
return ERRORCLASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
var external = {
|
||||||
|
electricChars:"dDpPtTfFeE ",
|
||||||
|
startState: function() {
|
||||||
|
return {
|
||||||
|
tokenize: tokenBase,
|
||||||
|
lastToken: null,
|
||||||
|
currentIndent: 0,
|
||||||
|
nextLineIndent: 0,
|
||||||
|
doInCurrentLine: false
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
token: function(stream, state) {
|
||||||
|
if (stream.sol()) {
|
||||||
|
state.currentIndent += state.nextLineIndent;
|
||||||
|
state.nextLineIndent = 0;
|
||||||
|
state.doInCurrentLine = 0;
|
||||||
|
}
|
||||||
|
var style = tokenLexer(stream, state);
|
||||||
|
|
||||||
|
state.lastToken = {style:style, content: stream.current()};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return style;
|
||||||
|
},
|
||||||
|
|
||||||
|
indent: function(state, textAfter) {
|
||||||
|
var trueText = textAfter.replace(/^\s+|\s+$/g, '') ;
|
||||||
|
if (trueText.match(closing) || trueText.match(doubleClosing) || trueText.match(middle)) return conf.indentUnit*(state.currentIndent-1);
|
||||||
|
if(state.currentIndent < 0) return 0;
|
||||||
|
return state.currentIndent * conf.indentUnit;
|
||||||
|
},
|
||||||
|
|
||||||
|
lineComment: "'"
|
||||||
|
};
|
||||||
|
return external;
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineMIME("text/x-vb", "vb");
|
||||||
|
|
||||||
|
});
|
|
@ -215,7 +215,6 @@ export abstract class BaseDebugPlatform extends BasePlatform {
|
||||||
frameCount : number = 0;
|
frameCount : number = 0;
|
||||||
|
|
||||||
abstract getCPUState() : CpuState;
|
abstract getCPUState() : CpuState;
|
||||||
abstract readAddress(addr:number) : number;
|
|
||||||
|
|
||||||
setBreakpoint(id : string, cond : DebugCondition) {
|
setBreakpoint(id : string, cond : DebugCondition) {
|
||||||
if (cond) {
|
if (cond) {
|
||||||
|
@ -416,6 +415,7 @@ export abstract class Base6502Platform extends BaseDebugPlatform {
|
||||||
getSP() { return this.getCPUState().SP };
|
getSP() { return this.getCPUState().SP };
|
||||||
getPC() { return this.getCPUState().PC };
|
getPC() { return this.getCPUState().PC };
|
||||||
isStable() { return !this.getCPUState()['T']; }
|
isStable() { return !this.getCPUState()['T']; }
|
||||||
|
abstract readAddress(addr:number) : number;
|
||||||
|
|
||||||
newCPU(membus : MemoryBus) {
|
newCPU(membus : MemoryBus) {
|
||||||
var cpu = new jt.M6502();
|
var cpu = new jt.M6502();
|
||||||
|
|
679
src/common/basic/compiler.ts
Normal file
679
src/common/basic/compiler.ts
Normal file
|
@ -0,0 +1,679 @@
|
||||||
|
import { WorkerError, CodeListingMap, SourceLocation } from "../workertypes";
|
||||||
|
|
||||||
|
export interface SourceLocated {
|
||||||
|
$loc?: SourceLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const re_toks = /([0-9.]+[E][+-]?\d+)|(\d*[.]\d*[E0-9]*)|(\d+)|(['].*)|(\bAND\b)|(\bOR\b)|(\w+[$]?)|(".*?")|([<>]?[=<>])|([-+*/^,;:()])|(\S+)/gi;
|
||||||
|
|
||||||
|
export enum TokenType {
|
||||||
|
EOL = 0,
|
||||||
|
Float1,
|
||||||
|
Float2,
|
||||||
|
Int,
|
||||||
|
Remark,
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
Ident,
|
||||||
|
String,
|
||||||
|
Relational,
|
||||||
|
Operator,
|
||||||
|
CatchAll,
|
||||||
|
_LAST,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExprTypes = BinOp | UnOp | IndOp | Literal;
|
||||||
|
|
||||||
|
export type Expr = ExprTypes & SourceLocated;
|
||||||
|
|
||||||
|
export type Opcode = 'add' | 'sub' | 'mul' | 'div' | 'pow' | 'eq' | 'ne' | 'lt' | 'gt' | 'le' | 'ge' | 'land' | 'lor';
|
||||||
|
|
||||||
|
export type Value = string | number;
|
||||||
|
|
||||||
|
export interface Literal {
|
||||||
|
value: Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BinOp {
|
||||||
|
op: Opcode;
|
||||||
|
left: Expr;
|
||||||
|
right: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnOp {
|
||||||
|
op: 'neg';
|
||||||
|
expr: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndOp extends SourceLocated {
|
||||||
|
name: string;
|
||||||
|
args: Expr[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PRINT_Statement {
|
||||||
|
command: "PRINT";
|
||||||
|
args: Expr[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LET_Statement {
|
||||||
|
command: "LET";
|
||||||
|
lexpr: IndOp;
|
||||||
|
right: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GOTO_Statement {
|
||||||
|
command: "GOTO";
|
||||||
|
label: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GOSUB_Statement {
|
||||||
|
command: "GOSUB";
|
||||||
|
label: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RETURN_Statement {
|
||||||
|
command: "RETURN";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IF_Statement {
|
||||||
|
command: "IF";
|
||||||
|
cond: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FOR_Statement {
|
||||||
|
command: "FOR";
|
||||||
|
lexpr: IndOp;
|
||||||
|
initial: Expr;
|
||||||
|
target: Expr;
|
||||||
|
step?: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NEXT_Statement {
|
||||||
|
command: "NEXT";
|
||||||
|
lexpr?: IndOp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DIM_Statement {
|
||||||
|
command: "DIM";
|
||||||
|
args: IndOp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INPUT_Statement {
|
||||||
|
command: "INPUT";
|
||||||
|
prompt: Expr;
|
||||||
|
args: IndOp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface READ_Statement {
|
||||||
|
command: "INPUT";
|
||||||
|
args: IndOp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DEF_Statement {
|
||||||
|
command: "DEF";
|
||||||
|
lexpr: IndOp;
|
||||||
|
def: Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ONGOTO_Statement {
|
||||||
|
command: "ONGOTO";
|
||||||
|
expr: Expr;
|
||||||
|
labels: Expr[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DATA_Statement {
|
||||||
|
command: "DATA";
|
||||||
|
datums: Expr[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OPTION_Statement {
|
||||||
|
command: "OPTION";
|
||||||
|
optname: string;
|
||||||
|
optargs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StatementTypes = PRINT_Statement | LET_Statement | GOTO_Statement | GOSUB_Statement
|
||||||
|
| IF_Statement | FOR_Statement | DATA_Statement;
|
||||||
|
|
||||||
|
export type Statement = StatementTypes & SourceLocated;
|
||||||
|
|
||||||
|
export interface BASICLine {
|
||||||
|
label: string;
|
||||||
|
stmts: Statement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BASICProgram {
|
||||||
|
lines: BASICLine[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
str: string;
|
||||||
|
type: TokenType;
|
||||||
|
$loc: SourceLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPERATORS = {
|
||||||
|
'AND': {f:'land',p:5},
|
||||||
|
'OR': {f:'lor',p:5},
|
||||||
|
'=': {f:'eq',p:10},
|
||||||
|
'<>': {f:'ne',p:10},
|
||||||
|
'<': {f:'lt',p:10},
|
||||||
|
'>': {f:'gt',p:10},
|
||||||
|
'<=': {f:'le',p:10},
|
||||||
|
'>=': {f:'ge',p:10},
|
||||||
|
'+': {f:'add',p:100},
|
||||||
|
'-': {f:'sub',p:100},
|
||||||
|
'*': {f:'mul',p:200},
|
||||||
|
'/': {f:'div',p:200},
|
||||||
|
'^': {f:'pow',p:300}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOpcodeForOperator(op: string): Opcode {
|
||||||
|
return OPERATORS[op].f as Opcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrecedence(tok: Token): number {
|
||||||
|
switch (tok.type) {
|
||||||
|
case TokenType.Operator:
|
||||||
|
case TokenType.Relational:
|
||||||
|
case TokenType.Ident:
|
||||||
|
let op = OPERATORS[tok.str]
|
||||||
|
if (op) return op.p;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is token an end of statement marker? (":" or end of line)
|
||||||
|
function isEOS(tok: Token) {
|
||||||
|
return (tok.type == TokenType.EOL) || (tok.type == TokenType.Operator && tok.str == ':');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripQuotes(s: string) {
|
||||||
|
// TODO: assert
|
||||||
|
return s.substr(1, s.length-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
export interface BASICOptions {
|
||||||
|
uppercaseOnly : boolean; // convert everything to uppercase?
|
||||||
|
strictVarNames : boolean; // only allow A0-9 for numerics, single letter for arrays/strings
|
||||||
|
sharedArrayNamespace : boolean; // arrays and variables have same namespace? (conflict)
|
||||||
|
defaultArrayBase : number; // arrays start at this number (0 or 1)
|
||||||
|
defaultArraySize : number; // arrays are allocated w/ this size (starting @ 0)
|
||||||
|
maxDimensions : number; // max number of dimensions for arrays
|
||||||
|
stringConcat : boolean; // can concat strings with "+" operator?
|
||||||
|
typeConvert : boolean; // type convert strings <-> numbers?
|
||||||
|
maxArguments : number; // maximum # of arguments for user-defined functions
|
||||||
|
sparseArrays : boolean; // true == don't require DIM for arrays
|
||||||
|
tickComments : boolean; // support 'comments?
|
||||||
|
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)
|
||||||
|
printZoneLength : number; // print zone length
|
||||||
|
printPrecision : number; // print precision # of digits
|
||||||
|
checkOverflow : boolean; // check for overflow of numerics?
|
||||||
|
defaultValues : boolean; // initialize unset variables to default value? (0 or "")
|
||||||
|
}
|
||||||
|
|
||||||
|
///// BASIC PARSER
|
||||||
|
|
||||||
|
export class BASICParser {
|
||||||
|
tokens: Token[];
|
||||||
|
errors: WorkerError[];
|
||||||
|
labels: { [label: string]: BASICLine };
|
||||||
|
targets: { [targetlabel: string]: SourceLocation };
|
||||||
|
eol: Token;
|
||||||
|
lineno : number;
|
||||||
|
curlabel: string;
|
||||||
|
listings: CodeListingMap;
|
||||||
|
lasttoken: Token;
|
||||||
|
opts : BASICOptions = ALTAIR_BASIC40;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.labels = {};
|
||||||
|
this.targets = {};
|
||||||
|
this.errors = [];
|
||||||
|
this.lineno = 0;
|
||||||
|
this.curlabel = null;
|
||||||
|
this.listings = {};
|
||||||
|
}
|
||||||
|
compileError(msg: string, loc?: SourceLocation) {
|
||||||
|
if (!loc) loc = this.peekToken().$loc;
|
||||||
|
// TODO: pass SourceLocation to errors
|
||||||
|
this.errors.push({line:loc.line, msg:msg});
|
||||||
|
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
|
||||||
|
}
|
||||||
|
dialectError(what: string, loc?: SourceLocation) {
|
||||||
|
this.compileError(`The selected BASIC dialect doesn't support ${what}`, loc); // TODO
|
||||||
|
}
|
||||||
|
consumeToken(): Token {
|
||||||
|
var tok = this.lasttoken = (this.tokens.shift() || this.eol);
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
expectToken(str: string) : Token {
|
||||||
|
var tok = this.consumeToken();
|
||||||
|
var tokstr = tok.str.toUpperCase();
|
||||||
|
if (str != tokstr) {
|
||||||
|
this.compileError(`I expected "${str}" here, but I saw "${tokstr}".`);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
case TokenType.Int:
|
||||||
|
if (this.labels[tok.str] != null) this.compileError(`I saw a duplicated label "${tok.str}".`);
|
||||||
|
this.labels[tok.str] = line;
|
||||||
|
line.label = tok.str;
|
||||||
|
this.curlabel = tok.str;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// TODO
|
||||||
|
this.pushbackToken(tok);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseFile(file: string, path: string) : BASICProgram {
|
||||||
|
var pgmlines = file.split("\n").map((line) => this.parseLine(line));
|
||||||
|
this.checkLabels();
|
||||||
|
var program = { lines: pgmlines };
|
||||||
|
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 = [];
|
||||||
|
var m;
|
||||||
|
while (m = re_toks.exec(line)) {
|
||||||
|
for (var i = 1; i < TokenType._LAST; i++) {
|
||||||
|
let s = m[i];
|
||||||
|
if (s != null) {
|
||||||
|
this.tokens.push({
|
||||||
|
str: s,
|
||||||
|
type: i,
|
||||||
|
$loc: { line: this.lineno, start: m.index, end: m.index+s.length }
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.eol = { type: TokenType.EOL, str: "", $loc: { line: this.lineno, start: line.length } };
|
||||||
|
}
|
||||||
|
parse() : BASICLine {
|
||||||
|
var line = {label: null, stmts: []};
|
||||||
|
// not empty line?
|
||||||
|
if (this.tokens.length) {
|
||||||
|
this.parseOptLabel(line);
|
||||||
|
line.stmts = this.parseCompoundStatement();
|
||||||
|
this.curlabel = null;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
parseCompoundStatement(): Statement[] {
|
||||||
|
var list = this.parseList(this.parseStatement, ':');
|
||||||
|
if (!isEOS(this.peekToken())) this.compileError(`Expected end of line or ':'`, this.peekToken().$loc);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
parseStatement(): Statement | null {
|
||||||
|
var cmdtok = this.consumeToken();
|
||||||
|
var stmt;
|
||||||
|
switch (cmdtok.type) {
|
||||||
|
case TokenType.Remark:
|
||||||
|
if (!this.opts.tickComments) this.dialectError(`tick remarks`);
|
||||||
|
return null;
|
||||||
|
case TokenType.Ident:
|
||||||
|
var cmd = cmdtok.str.toUpperCase();
|
||||||
|
// remark? ignore to eol
|
||||||
|
if (cmd == 'REM') {
|
||||||
|
while (this.consumeToken().type != TokenType.EOL) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// look for "GO TO"
|
||||||
|
if (cmd == 'GO' && this.peekToken().str == 'TO') {
|
||||||
|
this.consumeToken();
|
||||||
|
cmd = 'GOTO';
|
||||||
|
}
|
||||||
|
var fn = this['stmt__' + cmd];
|
||||||
|
if (fn) {
|
||||||
|
if (this.opts.validKeywords && this.opts.validKeywords.indexOf(cmd) < 0)
|
||||||
|
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:
|
||||||
|
this.compileError(`I expected a command here`);
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
this.compileError(`Unknown command "${cmdtok.str}"`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (stmt) stmt.$loc = { line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start };
|
||||||
|
return stmt;
|
||||||
|
}
|
||||||
|
parseVarOrIndexedOrFunc(): IndOp {
|
||||||
|
return this.parseVarOrIndexed();
|
||||||
|
}
|
||||||
|
parseVarOrIndexed(): IndOp {
|
||||||
|
var tok = this.consumeToken();
|
||||||
|
switch (tok.type) {
|
||||||
|
case TokenType.Ident:
|
||||||
|
let args = null;
|
||||||
|
if (this.peekToken().str == '(') {
|
||||||
|
this.expectToken('(');
|
||||||
|
args = this.parseExprList();
|
||||||
|
this.expectToken(')');
|
||||||
|
}
|
||||||
|
return { name: tok.str, args: args, $loc: tok.$loc };
|
||||||
|
default:
|
||||||
|
this.compileError("Expected variable or array index");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseList<T>(parseFunc:()=>T, delim:string): T[] {
|
||||||
|
var sep;
|
||||||
|
var list = [];
|
||||||
|
do {
|
||||||
|
var el = parseFunc.bind(this)()
|
||||||
|
if (el != null) list.push(el);
|
||||||
|
sep = this.consumeToken();
|
||||||
|
} while (sep.str == delim);
|
||||||
|
this.pushbackToken(sep);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
parseVarOrIndexedList(): IndOp[] {
|
||||||
|
return this.parseList(this.parseVarOrIndexed, ',');
|
||||||
|
}
|
||||||
|
parseExprList(): Expr[] {
|
||||||
|
return this.parseList(this.parseExpr, ',');
|
||||||
|
}
|
||||||
|
parseLabelList(): Expr[] {
|
||||||
|
return this.parseList(this.parseLabel, ',');
|
||||||
|
}
|
||||||
|
parseLabel() : Expr {
|
||||||
|
var tok = this.consumeToken();
|
||||||
|
switch (tok.type) {
|
||||||
|
case TokenType.Int:
|
||||||
|
var label = parseInt(tok.str).toString();
|
||||||
|
this.targets[label] = tok.$loc;
|
||||||
|
return {value:label};
|
||||||
|
default:
|
||||||
|
this.compileError(`I expected a line number here`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsePrimary(): Expr {
|
||||||
|
let tok = this.consumeToken();
|
||||||
|
switch (tok.type) {
|
||||||
|
case TokenType.Int:
|
||||||
|
case TokenType.Float1:
|
||||||
|
case TokenType.Float2:
|
||||||
|
return { value: parseFloat(tok.str), $loc: tok.$loc };
|
||||||
|
case TokenType.String:
|
||||||
|
return { value: stripQuotes(tok.str), $loc: tok.$loc };
|
||||||
|
case TokenType.Ident:
|
||||||
|
this.pushbackToken(tok);
|
||||||
|
return this.parseVarOrIndexedOrFunc();
|
||||||
|
case TokenType.Operator:
|
||||||
|
if (tok.str == '(') {
|
||||||
|
let expr = this.parseExpr();
|
||||||
|
this.expectToken(')');
|
||||||
|
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 == '+') {
|
||||||
|
return this.parsePrimary(); // TODO?
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
this.compileError(`Unexpected "${tok.str}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseExpr1(left: Expr, minPred: number): Expr {
|
||||||
|
let look = this.peekToken();
|
||||||
|
while (getPrecedence(look) >= minPred) {
|
||||||
|
let op = this.consumeToken();
|
||||||
|
let right: Expr = this.parsePrimary();
|
||||||
|
look = this.peekToken();
|
||||||
|
while (getPrecedence(look) > getPrecedence(op)) {
|
||||||
|
right = this.parseExpr1(right, getPrecedence(look));
|
||||||
|
look = this.peekToken();
|
||||||
|
}
|
||||||
|
left = { op: getOpcodeForOperator(op.str), left: left, right: right };
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
parseExpr(): Expr {
|
||||||
|
return this.parseExpr1(this.parsePrimary(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//// STATEMENTS
|
||||||
|
|
||||||
|
stmt__LET(): LET_Statement {
|
||||||
|
var lexpr = this.parseVarOrIndexed();
|
||||||
|
this.expectToken("=");
|
||||||
|
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[];
|
||||||
|
this.expectToken('THEN');
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
stmt__FOR() : FOR_Statement {
|
||||||
|
var lexpr = this.parseVarOrIndexed(); // TODO: parseNumVar()
|
||||||
|
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 {
|
||||||
|
return { command:'DIM', args:this.parseVarOrIndexedList() };
|
||||||
|
}
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseVarOrIndexedList() };
|
||||||
|
}
|
||||||
|
stmt__DATA() : DATA_Statement {
|
||||||
|
return { command:'DATA', datums:this.parseExprList() };
|
||||||
|
}
|
||||||
|
stmt__READ() {
|
||||||
|
return { command:'READ', args:this.parseVarOrIndexedList() };
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
this.expectToken('GOTO');
|
||||||
|
var labels = this.parseLabelList();
|
||||||
|
return { command:'ONGOTO', expr:expr, labels:labels };
|
||||||
|
}
|
||||||
|
stmt__DEF() : DEF_Statement {
|
||||||
|
var lexpr = this.parseVarOrIndexed();
|
||||||
|
if (!lexpr.name.toUpperCase().startsWith('FN')) this.compileError(`Functions defined with DEF must begin with the letters "FN".`)
|
||||||
|
this.expectToken("=");
|
||||||
|
var func = this.parseExpr();
|
||||||
|
return { command:'DEF', lexpr:lexpr, def:func };
|
||||||
|
}
|
||||||
|
stmt__OPTION() : OPTION_Statement {
|
||||||
|
var tokname = this.consumeToken();
|
||||||
|
if (tokname.type != TokenType.Ident) this.compileError(`I expected a name after the OPTION statement.`)
|
||||||
|
var list : string[] = [];
|
||||||
|
var tok;
|
||||||
|
do {
|
||||||
|
tok = this.consumeToken();
|
||||||
|
if (isEOS(tok)) break;
|
||||||
|
list.push(tok.str.toUpperCase());
|
||||||
|
} while (true);
|
||||||
|
return { command:'OPTION', optname:tokname.str.toUpperCase(), optargs:list };
|
||||||
|
}
|
||||||
|
|
||||||
|
// for workermain
|
||||||
|
generateListing(file: string, program: BASICProgram) {
|
||||||
|
var srclines = [];
|
||||||
|
var pc = 0;
|
||||||
|
program.lines.forEach((line, idx) => {
|
||||||
|
srclines.push({offset: pc, line: idx+1});
|
||||||
|
pc += line.stmts.length;
|
||||||
|
});
|
||||||
|
return { lines: srclines };
|
||||||
|
}
|
||||||
|
getListings() : CodeListingMap {
|
||||||
|
return this.listings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LINT STUFF
|
||||||
|
checkLabels() {
|
||||||
|
for (let targ in this.targets) {
|
||||||
|
if (this.labels[targ] == null) {
|
||||||
|
this.compileError(`I couldn't find line number ${targ}`, this.targets[targ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///// BASIC DIALECTS
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
export const ECMA55_MINIMAL : BASICOptions = {
|
||||||
|
uppercaseOnly : true,
|
||||||
|
strictVarNames : true,
|
||||||
|
sharedArrayNamespace : true,
|
||||||
|
defaultArrayBase : 0,
|
||||||
|
defaultArraySize : 11,
|
||||||
|
defaultValues : false,
|
||||||
|
stringConcat : false,
|
||||||
|
typeConvert : false,
|
||||||
|
maxDimensions : 2,
|
||||||
|
maxArguments : Infinity,
|
||||||
|
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'
|
||||||
|
],
|
||||||
|
validFunctions : ['ABS','ATN','COS','EXP','INT','LOG','RND','SGN','SIN','SQR','TAN'],
|
||||||
|
validOperators : ['=', '<>', '<', '>', '<=', '>=', '+', '-', '*', '/', '^'],
|
||||||
|
printZoneLength : 15,
|
||||||
|
printPrecision : 6,
|
||||||
|
checkOverflow : true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ALTAIR_BASIC40 : BASICOptions = {
|
||||||
|
uppercaseOnly : true,
|
||||||
|
strictVarNames : true,
|
||||||
|
sharedArrayNamespace : true,
|
||||||
|
defaultArrayBase : 0,
|
||||||
|
defaultArraySize : 11,
|
||||||
|
defaultValues : false,
|
||||||
|
stringConcat : false,
|
||||||
|
typeConvert : false,
|
||||||
|
maxDimensions : 2,
|
||||||
|
maxArguments : Infinity,
|
||||||
|
sparseArrays : false,
|
||||||
|
tickComments : false,
|
||||||
|
validKeywords : null, // all
|
||||||
|
validFunctions : null, // all
|
||||||
|
validOperators : null, // all ['\\','MOD','NOT','AND','OR','XOR','EQV','IMP'],
|
||||||
|
printZoneLength : 15,
|
||||||
|
printPrecision : 6,
|
||||||
|
checkOverflow : true,
|
||||||
|
}
|
24
src/common/basic/main.ts
Normal file
24
src/common/basic/main.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
})
|
54
src/common/basic/run.ts
Normal file
54
src/common/basic/run.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
import { BASICParser, ECMA55_MINIMAL } from "./compiler";
|
||||||
|
import { BASICRuntime } from "./runtime";
|
||||||
|
|
||||||
|
var readline = require('readline');
|
||||||
|
var rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
//terminal: false
|
||||||
|
});
|
||||||
|
|
||||||
|
var parser = new BASICParser();
|
||||||
|
parser.opts = ECMA55_MINIMAL;
|
||||||
|
var fs = require('fs');
|
||||||
|
var filename = process.argv[2];
|
||||||
|
var data = fs.readFileSync(filename, 'utf-8');
|
||||||
|
try {
|
||||||
|
var pgm = parser.parseFile(data, filename);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("@@@ " + e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime = new BASICRuntime();
|
||||||
|
runtime.trace = process.argv[3] == '-v';
|
||||||
|
runtime.load(pgm);
|
||||||
|
runtime.reset();
|
||||||
|
runtime.print = (s:string) => {
|
||||||
|
fs.writeSync(1, s+"");
|
||||||
|
}
|
||||||
|
runtime.input = async (prompt:string) => {
|
||||||
|
return new Promise( (resolve, reject) => {
|
||||||
|
fs.writeSync(1, "\n");
|
||||||
|
rl.question(prompt, (answer) => {
|
||||||
|
var vals = answer.toUpperCase().split(',');
|
||||||
|
resolve(vals);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runtime.resume = function() {
|
||||||
|
process.nextTick(() => {
|
||||||
|
try {
|
||||||
|
if (runtime.step()) {
|
||||||
|
if (runtime.running) runtime.resume();
|
||||||
|
} else if (runtime.exited) {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("### " + e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
runtime.resume();
|
646
src/common/basic/runtime.ts
Normal file
646
src/common/basic/runtime.ts
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
|
||||||
|
import * as basic from "./compiler";
|
||||||
|
import { EmuHalt } from "../emu";
|
||||||
|
|
||||||
|
function isLiteral(arg: basic.Expr): arg is basic.Literal {
|
||||||
|
return (arg as any).value != null;
|
||||||
|
}
|
||||||
|
function isLookup(arg: basic.Expr): arg is basic.IndOp {
|
||||||
|
return (arg as any).name != null;
|
||||||
|
}
|
||||||
|
function isBinOp(arg: basic.Expr): arg is basic.BinOp {
|
||||||
|
return (arg as any).op != null && (arg as any).left != null && (arg as any).right != null;
|
||||||
|
}
|
||||||
|
function isUnOp(arg: basic.Expr): arg is basic.UnOp {
|
||||||
|
return (arg as any).op != null && (arg as any).expr != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExprOptions {
|
||||||
|
check: boolean;
|
||||||
|
isconst?: boolean;
|
||||||
|
locals?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompiledStatement {
|
||||||
|
$run?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BASICRuntime {
|
||||||
|
|
||||||
|
program : basic.BASICProgram;
|
||||||
|
allstmts : basic.Statement[];
|
||||||
|
line2pc : number[];
|
||||||
|
pc2line : Map<number,number>;
|
||||||
|
label2lineidx : {[label : string] : number};
|
||||||
|
label2pc : {[label : string] : number};
|
||||||
|
datums : basic.Literal[];
|
||||||
|
|
||||||
|
curpc : number;
|
||||||
|
dataptr : number;
|
||||||
|
vars : {};
|
||||||
|
arrays : {};
|
||||||
|
forLoops : {};
|
||||||
|
returnStack : number[];
|
||||||
|
column : number;
|
||||||
|
abase : number; // array base
|
||||||
|
|
||||||
|
running : boolean = false;
|
||||||
|
exited : boolean = false;
|
||||||
|
trace : boolean = false;
|
||||||
|
|
||||||
|
load(program: basic.BASICProgram) {
|
||||||
|
this.program = program;
|
||||||
|
this.label2lineidx = {};
|
||||||
|
this.label2pc = {};
|
||||||
|
this.allstmts = [];
|
||||||
|
this.line2pc = [];
|
||||||
|
this.pc2line = new Map();
|
||||||
|
this.datums = [];
|
||||||
|
// TODO: lines start @ 1?
|
||||||
|
program.lines.forEach((line, idx) => {
|
||||||
|
// make lookup tables
|
||||||
|
if (line.label != null) this.label2lineidx[line.label] = idx;
|
||||||
|
if (line.label != null) this.label2pc[line.label] = this.allstmts.length;
|
||||||
|
this.line2pc.push(this.allstmts.length);
|
||||||
|
this.pc2line.set(this.allstmts.length, idx);
|
||||||
|
// combine all statements into single list
|
||||||
|
line.stmts.forEach((stmt) => this.allstmts.push(stmt));
|
||||||
|
// parse DATA literals
|
||||||
|
line.stmts.filter((stmt) => stmt.command == 'DATA').forEach((datastmt) => {
|
||||||
|
(datastmt as basic.DATA_Statement).datums.forEach(d => {
|
||||||
|
var functext = this.expr2js(d, {check:true, isconst:true});
|
||||||
|
// TODO: catch exceptions
|
||||||
|
// TODO: any value doing this ahead of time?
|
||||||
|
var value = new Function(`return ${functext};`).bind(this)();
|
||||||
|
this.datums.push({value:value});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// TODO: compile statements?
|
||||||
|
//line.stmts.forEach((stmt) => this.compileStatement(stmt));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.curpc = 0;
|
||||||
|
this.dataptr = 0;
|
||||||
|
this.vars = {};
|
||||||
|
this.arrays = {};
|
||||||
|
this.forLoops = {};
|
||||||
|
this.returnStack = [];
|
||||||
|
this.column = 0;
|
||||||
|
this.abase = 1;
|
||||||
|
this.running = true;
|
||||||
|
this.exited = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeError(msg : string) {
|
||||||
|
this.curpc--; // we did curpc++ before executing statement
|
||||||
|
// TODO: pass source location to error
|
||||||
|
throw new EmuHalt(`${msg} (line ${this.getLabelForPC(this.curpc)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: sometimes on next line
|
||||||
|
getLineForPC(pc:number) {
|
||||||
|
var line;
|
||||||
|
do {
|
||||||
|
line = this.pc2line.get(pc);
|
||||||
|
if (line != null) break;
|
||||||
|
} while (--pc >= 0);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabelForPC(pc:number) {
|
||||||
|
var lineno = this.getLineForPC(pc);
|
||||||
|
var pgmline = this.program.lines[lineno];
|
||||||
|
return pgmline ? pgmline.label : '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatement() {
|
||||||
|
return this.allstmts[this.curpc];
|
||||||
|
}
|
||||||
|
|
||||||
|
step() : boolean {
|
||||||
|
if (!this.running) return false;
|
||||||
|
var stmt = this.getStatement();
|
||||||
|
// end of program?
|
||||||
|
if (!stmt) {
|
||||||
|
this.running = false;
|
||||||
|
this.exited = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.trace) console.log(this.curpc, stmt, this.vars, Object.keys(this.arrays));
|
||||||
|
// skip to next statment
|
||||||
|
this.curpc++;
|
||||||
|
// compile statement to JS?
|
||||||
|
this.compileStatement(stmt);
|
||||||
|
this.executeStatement(stmt);
|
||||||
|
return this.running;
|
||||||
|
}
|
||||||
|
|
||||||
|
compileStatement(stmt: basic.Statement & CompiledStatement) {
|
||||||
|
if (stmt.$run == null) {
|
||||||
|
var stmtfn = this['do__' + stmt.command];
|
||||||
|
if (stmtfn == null) this.runtimeError(`I don't know how to "${stmt.command}".`);
|
||||||
|
var functext = stmtfn.bind(this)(stmt);
|
||||||
|
if (this.trace) console.log(functext);
|
||||||
|
stmt.$run = new Function(functext).bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executeStatement(stmt: basic.Statement & CompiledStatement) {
|
||||||
|
// run compiled statement
|
||||||
|
stmt.$run();
|
||||||
|
}
|
||||||
|
|
||||||
|
skipToEOL() {
|
||||||
|
do {
|
||||||
|
this.curpc++;
|
||||||
|
} while (this.curpc < this.allstmts.length && !this.pc2line.get(this.curpc));
|
||||||
|
}
|
||||||
|
|
||||||
|
skipToEOF() {
|
||||||
|
this.curpc = this.allstmts.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
gotoLabel(label) {
|
||||||
|
var pc = this.label2pc[label];
|
||||||
|
if (pc >= 0) {
|
||||||
|
this.curpc = pc;
|
||||||
|
} else {
|
||||||
|
this.runtimeError(`I tried to go to the label "${label}" but couldn't find it.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gosubLabel(label) {
|
||||||
|
this.returnStack.push(this.curpc + 1);
|
||||||
|
this.gotoLabel(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnFromGosub() {
|
||||||
|
if (this.returnStack.length == 0)
|
||||||
|
this.runtimeError("I tried to RETURN, but there wasn't a corresponding GOSUB."); // RETURN BEFORE GOSUB
|
||||||
|
var pc = this.returnStack.pop();
|
||||||
|
this.curpc = pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueToString(obj) : string {
|
||||||
|
var str;
|
||||||
|
if (typeof obj === 'number') {
|
||||||
|
var numstr = obj.toString().toUpperCase();
|
||||||
|
var prec = 11;
|
||||||
|
while (numstr.length > 11) {
|
||||||
|
numstr = obj.toPrecision(prec--);
|
||||||
|
}
|
||||||
|
if (numstr.startsWith('0.'))
|
||||||
|
numstr = numstr.substr(1);
|
||||||
|
else if (numstr.startsWith('-0.'))
|
||||||
|
numstr = '-'+numstr.substr(2);
|
||||||
|
if (numstr.startsWith('-')) {
|
||||||
|
str = `${numstr} `;
|
||||||
|
} else {
|
||||||
|
str = ` ${numstr} `;
|
||||||
|
}
|
||||||
|
} else if (obj == '\n') {
|
||||||
|
this.column = 0;
|
||||||
|
str = obj;
|
||||||
|
} else if (obj == '\t') {
|
||||||
|
var curgroup = Math.floor(this.column / 15);
|
||||||
|
var nextcol = (curgroup + 1) * 15;
|
||||||
|
str = this.TAB(nextcol);
|
||||||
|
} else {
|
||||||
|
str = `${obj}`;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
printExpr(obj) {
|
||||||
|
var str = this.valueToString(obj);
|
||||||
|
this.column += str.length;
|
||||||
|
this.print(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// override this
|
||||||
|
print(str: string) {
|
||||||
|
console.log(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// override this
|
||||||
|
async input(prompt: string, nargs: number) : Promise<string[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// override this
|
||||||
|
resume() { }
|
||||||
|
|
||||||
|
expr2js(expr: basic.Expr, opts: ExprOptions) : string {
|
||||||
|
if (isLiteral(expr)) {
|
||||||
|
return JSON.stringify(expr.value);
|
||||||
|
} else if (isLookup(expr)) {
|
||||||
|
if (opts.locals && opts.locals.indexOf(expr.name) >= 0) {
|
||||||
|
return expr.name; // local arg in DEF
|
||||||
|
} else {
|
||||||
|
if (opts.isconst) this.runtimeError(`I expected a constant value here`);
|
||||||
|
var s = '';
|
||||||
|
if (expr.args && this[expr.name]) { // is it a function?
|
||||||
|
s += `this.${expr.name}(`;
|
||||||
|
s += expr.args.map((arg) => this.expr2js(arg, opts)).join(', ');
|
||||||
|
s += ')';
|
||||||
|
} else if (expr.args) { // is it a subscript?
|
||||||
|
s += `this.getArray(${JSON.stringify(expr.name)}, ${expr.args.length})`;
|
||||||
|
s += expr.args.map((arg) => '[this.ROUND('+this.expr2js(arg, opts)+')]').join('');
|
||||||
|
} else {
|
||||||
|
// just a variable
|
||||||
|
s = `this.vars.${expr.name}`;
|
||||||
|
}
|
||||||
|
if (opts.check)
|
||||||
|
return `this.checkValue(${s}, ${JSON.stringify(expr.name)})`; // TODO: better error
|
||||||
|
else
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
} else if (isBinOp(expr)) {
|
||||||
|
var left = this.expr2js(expr.left, opts);
|
||||||
|
var right = this.expr2js(expr.right, opts);
|
||||||
|
return `this.${expr.op}(${left}, ${right})`;
|
||||||
|
} else if (isUnOp(expr) && expr.op == 'neg') {
|
||||||
|
var e = this.expr2js(expr.expr, opts);
|
||||||
|
return `-(${e})`; // TODO: other ops?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startForLoop(name, init, targ, step) {
|
||||||
|
// TODO: check for loop params
|
||||||
|
var pc = this.curpc;
|
||||||
|
if (!step) step = 1;
|
||||||
|
this.vars[name] = init;
|
||||||
|
if (this.trace) console.log(`FOR ${name} = ${this.vars[name]} TO ${targ} STEP ${step}`);
|
||||||
|
this.forLoops[name] = {
|
||||||
|
next: () => {
|
||||||
|
var done = step >= 0 ? this.vars[name] >= targ : this.vars[name] <= targ;
|
||||||
|
if (done) {
|
||||||
|
delete this.forLoops[name];
|
||||||
|
} else {
|
||||||
|
this.vars[name] += step;
|
||||||
|
this.curpc = pc;
|
||||||
|
}
|
||||||
|
if (this.trace) console.log(`NEXT ${name}: ${this.vars[name]} TO ${targ} STEP ${step} DONE=${done}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
nextForLoop(name) {
|
||||||
|
// TODO: check for for loop
|
||||||
|
var fl = this.forLoops[name];
|
||||||
|
if (!fl) this.runtimeError(`I couldn't find a matching FOR for this NEXT.`)
|
||||||
|
this.forLoops[name].next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts a variable to string/number based on var name
|
||||||
|
convert(name: string, right: number|string) : number|string {
|
||||||
|
// TODO: error check?
|
||||||
|
if (name.endsWith("$"))
|
||||||
|
return right+"";
|
||||||
|
else if (typeof right === 'string')
|
||||||
|
return parseFloat(right);
|
||||||
|
else if (typeof right === 'number')
|
||||||
|
return right;
|
||||||
|
else
|
||||||
|
return this.checkValue(right, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dimension array
|
||||||
|
dimArray(name: string, ...dims:number[]) {
|
||||||
|
var isstring = name.endsWith('$');
|
||||||
|
// TODO: option for undefined float array elements?
|
||||||
|
var arrcons = isstring ? Array : Float64Array;
|
||||||
|
var ab = this.abase;
|
||||||
|
if (dims.length == 1) {
|
||||||
|
this.arrays[name] = new arrcons(dims[0]+ab);
|
||||||
|
} else if (dims.length == 2) {
|
||||||
|
this.arrays[name] = new Array(dims[0]+ab);
|
||||||
|
for (var i=ab; i<dims[0]+ab; i++)
|
||||||
|
this.arrays[name][i] = new arrcons(dims[1]+ab);
|
||||||
|
} else {
|
||||||
|
this.runtimeError(`I only support arrays of one or two dimensions.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getArray(name: string, order: number) : [] {
|
||||||
|
if (!this.arrays[name]) {
|
||||||
|
if (order == 1)
|
||||||
|
this.dimArray(name, 11);
|
||||||
|
else if (order == 2)
|
||||||
|
this.dimArray(name, 11, 11);
|
||||||
|
else
|
||||||
|
this.runtimeError(`I only support arrays of one or two dimensions.`)
|
||||||
|
}
|
||||||
|
return this.arrays[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
onGotoLabel(value: number, ...labels: string[]) {
|
||||||
|
value = this.ROUND(value);
|
||||||
|
if (value < 1 || value > labels.length)
|
||||||
|
this.runtimeError(`I needed a number between 1 and ${labels.length}, but I got ${value}.`);
|
||||||
|
this.gotoLabel(labels[value-1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDatum() : string|number {
|
||||||
|
if (this.dataptr >= this.datums.length)
|
||||||
|
this.runtimeError("I tried to READ, but ran out of data.");
|
||||||
|
return this.datums[this.dataptr++].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//// STATEMENTS
|
||||||
|
|
||||||
|
do__PRINT(stmt : basic.PRINT_Statement) {
|
||||||
|
var s = '';
|
||||||
|
for (var arg of stmt.args) {
|
||||||
|
var expr = this.expr2js(arg, {check:true});
|
||||||
|
s += `this.printExpr(${expr});`;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__INPUT(stmt : basic.INPUT_Statement) {
|
||||||
|
var prompt = this.expr2js(stmt.prompt, {check:true});
|
||||||
|
var setvals = '';
|
||||||
|
stmt.args.forEach((arg, index) => {
|
||||||
|
var lexpr = this.expr2js(arg, {check:false});
|
||||||
|
setvals += `${lexpr} = this.convert(${JSON.stringify(arg.name)}, vals[${index}]);`
|
||||||
|
});
|
||||||
|
return `this.running=false; this.input(${prompt}, ${stmt.args.length}).then((vals) => {${setvals}; this.running=true; this.resume();})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__LET(stmt : basic.LET_Statement) {
|
||||||
|
var lexpr = this.expr2js(stmt.lexpr, {check:false});
|
||||||
|
var right = this.expr2js(stmt.right, {check:true});
|
||||||
|
return `${lexpr} = this.convert(${JSON.stringify(stmt.lexpr.name)}, ${right});`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__FOR(stmt : basic.FOR_Statement) {
|
||||||
|
var name = JSON.stringify(stmt.lexpr.name); // TODO: args?
|
||||||
|
var init = this.expr2js(stmt.initial, {check:true});
|
||||||
|
var targ = this.expr2js(stmt.target, {check:true});
|
||||||
|
var step = stmt.step ? this.expr2js(stmt.step, {check:true}) : 'null';
|
||||||
|
return `this.startForLoop(${name}, ${init}, ${targ}, ${step})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__NEXT(stmt : basic.NEXT_Statement) {
|
||||||
|
var name = JSON.stringify(stmt.lexpr.name); // TODO: args? lexpr == null?
|
||||||
|
return `this.nextForLoop(${name})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__IF(stmt : basic.IF_Statement) {
|
||||||
|
var cond = this.expr2js(stmt.cond, {check:true});
|
||||||
|
return `if (!(${cond})) { this.skipToEOL(); }`
|
||||||
|
}
|
||||||
|
|
||||||
|
do__DEF(stmt : basic.DEF_Statement) {
|
||||||
|
var lexpr = `this.${stmt.lexpr.name}`;
|
||||||
|
var args = [];
|
||||||
|
for (var arg of stmt.lexpr.args) {
|
||||||
|
if (isLookup(arg)) {
|
||||||
|
args.push(arg.name);
|
||||||
|
} else {
|
||||||
|
this.runtimeError("I found a DEF statement with arguments other than variable names.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var functext = this.expr2js(stmt.def, {check:true, locals:args});
|
||||||
|
// TODO: use stmt.args to add function params
|
||||||
|
return `${lexpr} = function(${args.join(',')}) { return ${functext}; }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_DIM(dim : basic.IndOp) {
|
||||||
|
var argsstr = '';
|
||||||
|
for (var arg of dim.args) {
|
||||||
|
// TODO: check for float
|
||||||
|
argsstr += ', ' + this.expr2js(arg, {check:true});
|
||||||
|
}
|
||||||
|
return `this.dimArray(${JSON.stringify(dim.name)}${argsstr});`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__DIM(stmt : basic.DIM_Statement) {
|
||||||
|
var s = '';
|
||||||
|
stmt.args.forEach((dim) => s += this._DIM(dim));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__GOTO(stmt : basic.GOTO_Statement) {
|
||||||
|
var label = this.expr2js(stmt.label, {check:true});
|
||||||
|
return `this.gotoLabel(${label})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__GOSUB(stmt : basic.GOSUB_Statement) {
|
||||||
|
var label = this.expr2js(stmt.label, {check:true});
|
||||||
|
return `this.gosubLabel(${label})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__RETURN(stmt : basic.RETURN_Statement) {
|
||||||
|
return `this.returnFromGosub()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__ONGOTO(stmt : basic.ONGOTO_Statement) {
|
||||||
|
var expr = this.expr2js(stmt.expr, {check:true});
|
||||||
|
var labels = stmt.labels.map((arg) => this.expr2js(arg, {check:true})).join(', ');
|
||||||
|
return `this.onGotoLabel(${expr}, ${labels})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__DATA() {
|
||||||
|
// data is preprocessed
|
||||||
|
}
|
||||||
|
|
||||||
|
do__READ(stmt : basic.READ_Statement) {
|
||||||
|
var s = '';
|
||||||
|
stmt.args.forEach((arg) => {
|
||||||
|
s += `${this.expr2js(arg, {check:false})} = this.convert(${JSON.stringify(arg.name)}, this.nextDatum());`;
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__RESTORE() {
|
||||||
|
this.dataptr = 0; // TODO: line number?
|
||||||
|
}
|
||||||
|
|
||||||
|
do__END() {
|
||||||
|
return `this.skipToEOF()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__STOP() {
|
||||||
|
return `this.skipToEOF()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
do__OPTION(stmt: basic.OPTION_Statement) {
|
||||||
|
switch (stmt.optname) {
|
||||||
|
case 'BASE':
|
||||||
|
let base = parseInt(stmt.optargs[0]);
|
||||||
|
if (base == 0 || base == 1) this.abase = base;
|
||||||
|
else this.runtimeError("OPTION BASE can only be 0 or 1.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.runtimeError(`OPTION ${stmt.optname} is not supported by this compiler.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: "SUBSCRIPT ERROR"
|
||||||
|
// TODO: gosubs nested too deeply
|
||||||
|
// TODO: memory quota
|
||||||
|
|
||||||
|
// FUNCTIONS
|
||||||
|
|
||||||
|
checkValue(obj:number|string, exprname:string) {
|
||||||
|
if (typeof obj !== 'number' && typeof obj !== 'string') {
|
||||||
|
if (exprname != null && obj == null) {
|
||||||
|
this.runtimeError(`I didn't find a value for ${exprname}`);
|
||||||
|
} else if (exprname != null) {
|
||||||
|
this.runtimeError(`I got an invalid value for ${exprname}: ${obj}`);
|
||||||
|
} else {
|
||||||
|
this.runtimeError(`I got an invalid value: ${obj}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkNum(n:number) : number {
|
||||||
|
if (n === Infinity) this.runtimeError(`I computed a number too big to store.`);
|
||||||
|
if (isNaN(n)) this.runtimeError(`I computed an invalid number.`);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkString(s:string) : string {
|
||||||
|
if (typeof s !== 'string') this.runtimeError(`I expected a string here.`);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(a, b) : number|string {
|
||||||
|
// TODO: if string-concat
|
||||||
|
if (typeof a === 'number' && typeof b === 'number')
|
||||||
|
return this.checkNum(a + b);
|
||||||
|
else
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
sub(a:number, b:number) : number {
|
||||||
|
return this.checkNum(a - b);
|
||||||
|
}
|
||||||
|
mul(a:number, b:number) : number {
|
||||||
|
return this.checkNum(a * b);
|
||||||
|
}
|
||||||
|
div(a:number, b:number) : number {
|
||||||
|
if (b == 0) this.runtimeError(`I can't divide by zero.`);
|
||||||
|
return this.checkNum(a / b);
|
||||||
|
}
|
||||||
|
pow(a:number, b:number) : number {
|
||||||
|
if (a == 0 && b < 0) this.runtimeError(`I can't raise zero to a negative power.`);
|
||||||
|
return this.checkNum(Math.pow(a, b));
|
||||||
|
}
|
||||||
|
land(a:number, b:number) : number {
|
||||||
|
return a && b;
|
||||||
|
}
|
||||||
|
lor(a:number, b:number) : number {
|
||||||
|
return a || b;
|
||||||
|
}
|
||||||
|
eq(a:number, b:number) : boolean {
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
ne(a:number, b:number) : boolean {
|
||||||
|
return a != b;
|
||||||
|
}
|
||||||
|
lt(a:number, b:number) : boolean {
|
||||||
|
return a < b;
|
||||||
|
}
|
||||||
|
gt(a:number, b:number) : boolean {
|
||||||
|
return a > b;
|
||||||
|
}
|
||||||
|
le(a:number, b:number) : boolean {
|
||||||
|
return a <= b;
|
||||||
|
}
|
||||||
|
ge(a:number, b:number) : boolean {
|
||||||
|
return a >= b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUNCTIONS (uppercase)
|
||||||
|
|
||||||
|
ABS(arg : number) : number {
|
||||||
|
return this.checkNum(Math.abs(arg));
|
||||||
|
}
|
||||||
|
ASC(arg : string) : number {
|
||||||
|
return arg.charCodeAt(0);
|
||||||
|
}
|
||||||
|
ATN(arg : number) : number {
|
||||||
|
return this.checkNum(Math.atan(arg));
|
||||||
|
}
|
||||||
|
CHR$(arg : number) : string {
|
||||||
|
return String.fromCharCode(this.checkNum(arg));
|
||||||
|
}
|
||||||
|
COS(arg : number) : number {
|
||||||
|
return this.checkNum(Math.cos(arg));
|
||||||
|
}
|
||||||
|
EXP(arg : number) : number {
|
||||||
|
return this.checkNum(Math.exp(arg));
|
||||||
|
}
|
||||||
|
FIX(arg : number) : number {
|
||||||
|
return this.checkNum(arg - Math.floor(arg));
|
||||||
|
}
|
||||||
|
HEX$(arg : number) : string {
|
||||||
|
return arg.toString(16);
|
||||||
|
}
|
||||||
|
INSTR(a, b, c) : number {
|
||||||
|
if (c != null) {
|
||||||
|
return this.checkString(c).indexOf(this.checkString(b), a) + 1;
|
||||||
|
} else {
|
||||||
|
return this.checkString(b).indexOf(this.checkString(a)) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
INT(arg : number) : number {
|
||||||
|
return this.checkNum(Math.floor(arg));
|
||||||
|
}
|
||||||
|
LEFT$(arg : string, count : number) : string {
|
||||||
|
return arg.substr(0, count);
|
||||||
|
}
|
||||||
|
LEN(arg : string) : number {
|
||||||
|
return arg.length;
|
||||||
|
}
|
||||||
|
LOG(arg : number) : number {
|
||||||
|
if (arg == 0) this.runtimeError(`I can't take the logarithm of zero (${arg}).`)
|
||||||
|
if (arg < 0) this.runtimeError(`I can't take the logarithm of a negative number (${arg}).`)
|
||||||
|
return this.checkNum(Math.log(arg));
|
||||||
|
}
|
||||||
|
MID$(arg : string, start : number, count : number) : string {
|
||||||
|
if (start < 1) this.runtimeError(`The second parameter to MID$ must be between 1 and the length of the string in the first parameter.`)
|
||||||
|
return arg.substr(start-1, count);
|
||||||
|
}
|
||||||
|
RIGHT$(arg : string, count : number) : string {
|
||||||
|
return arg.substr(arg.length - count, count);
|
||||||
|
}
|
||||||
|
RND(arg : number) : number {
|
||||||
|
return Math.random(); // argument ignored
|
||||||
|
}
|
||||||
|
ROUND(arg : number) : number {
|
||||||
|
return this.checkNum(Math.round(arg)); // TODO?
|
||||||
|
}
|
||||||
|
SGN(arg : number) : number {
|
||||||
|
return (arg < 0) ? -1 : (arg > 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
SIN(arg : number) : number {
|
||||||
|
return this.checkNum(Math.sin(arg));
|
||||||
|
}
|
||||||
|
SPACE$(arg : number) : string {
|
||||||
|
return ' '.repeat(this.checkNum(arg));
|
||||||
|
}
|
||||||
|
SQR(arg : number) : number {
|
||||||
|
if (arg < 0) this.runtimeError(`I can't take the square root of a negative number (${arg}).`)
|
||||||
|
return this.checkNum(Math.sqrt(arg));
|
||||||
|
}
|
||||||
|
STR$(arg) : string {
|
||||||
|
return this.valueToString(arg);
|
||||||
|
}
|
||||||
|
TAB(arg : number) : string {
|
||||||
|
if (arg < 0) this.runtimeError(`I got a negative value for the TAB() function.`);
|
||||||
|
var spaces = this.ROUND(arg) - this.column;
|
||||||
|
return (spaces > 0) ? ' '.repeat(spaces) : '';
|
||||||
|
}
|
||||||
|
TAN(arg : number) : number {
|
||||||
|
return this.checkNum(Math.tan(arg));
|
||||||
|
}
|
||||||
|
VAL(arg) : number {
|
||||||
|
return parseFloat(arg+"");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,12 @@
|
||||||
|
|
||||||
export type FileData = string | Uint8Array;
|
export type FileData = string | Uint8Array;
|
||||||
|
|
||||||
|
export interface SourceLocation {
|
||||||
|
line: number;
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SourceLine {
|
export interface SourceLine {
|
||||||
offset:number;
|
offset:number;
|
||||||
line:number;
|
line:number;
|
||||||
|
|
|
@ -59,6 +59,7 @@ var lastViewClicked = null;
|
||||||
var lastBreakExpr = "c.PC == 0x6000";
|
var lastBreakExpr = "c.PC == 0x6000";
|
||||||
|
|
||||||
// TODO: codemirror multiplex support?
|
// TODO: codemirror multiplex support?
|
||||||
|
// TODO: move to views.ts?
|
||||||
var TOOL_TO_SOURCE_STYLE = {
|
var TOOL_TO_SOURCE_STYLE = {
|
||||||
'dasm': '6502',
|
'dasm': '6502',
|
||||||
'acme': '6502',
|
'acme': '6502',
|
||||||
|
@ -80,6 +81,7 @@ var TOOL_TO_SOURCE_STYLE = {
|
||||||
'smlrc': 'text/x-csrc',
|
'smlrc': 'text/x-csrc',
|
||||||
'inform6': 'inform6',
|
'inform6': 'inform6',
|
||||||
'fastbasic': 'fastbasic',
|
'fastbasic': 'fastbasic',
|
||||||
|
'basic': 'basic',
|
||||||
}
|
}
|
||||||
|
|
||||||
function gaEvent(category:string, action:string, label?:string, value?:string) {
|
function gaEvent(category:string, action:string, label?:string, value?:string) {
|
||||||
|
|
|
@ -70,9 +70,10 @@ const MODEDEFS = {
|
||||||
gas: { isAsm: true },
|
gas: { isAsm: true },
|
||||||
inform6: { theme: 'cobalt' },
|
inform6: { theme: 'cobalt' },
|
||||||
markdown: { lineWrap: true },
|
markdown: { lineWrap: true },
|
||||||
|
fastbasic: { noGutters: true },
|
||||||
|
basic: { noLineNumbers: true, noGutters: true }, // TODO: not used?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SourceEditor implements ProjectView {
|
export class SourceEditor implements ProjectView {
|
||||||
constructor(path:string, mode:string) {
|
constructor(path:string, mode:string) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
@ -106,17 +107,20 @@ export class SourceEditor implements ProjectView {
|
||||||
newEditor(parent:HTMLElement, isAsmOverride?:boolean) {
|
newEditor(parent:HTMLElement, isAsmOverride?:boolean) {
|
||||||
var modedef = MODEDEFS[this.mode] || MODEDEFS.default;
|
var modedef = MODEDEFS[this.mode] || MODEDEFS.default;
|
||||||
var isAsm = isAsmOverride || modedef.isAsm;
|
var isAsm = isAsmOverride || modedef.isAsm;
|
||||||
var lineWrap = modedef.lineWrap;
|
var lineWrap = !!modedef.lineWrap;
|
||||||
var theme = modedef.theme || MODEDEFS.default.theme;
|
var theme = modedef.theme || MODEDEFS.default.theme;
|
||||||
|
var lineNums = !modedef.noLineNumbers;
|
||||||
|
var gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"];
|
||||||
|
if (isAsm) gutters = ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"];
|
||||||
|
if (modedef.noGutters) gutters = ["gutter-info"];
|
||||||
this.editor = CodeMirror(parent, {
|
this.editor = CodeMirror(parent, {
|
||||||
theme: theme,
|
theme: theme,
|
||||||
lineNumbers: true,
|
lineNumbers: lineNums,
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
tabSize: 8,
|
tabSize: 8,
|
||||||
indentAuto: true,
|
indentAuto: true,
|
||||||
lineWrapping: lineWrap,
|
lineWrapping: lineWrap,
|
||||||
gutters: isAsm ? ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"]
|
gutters: gutters
|
||||||
: ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +328,7 @@ export class SourceEditor implements ProjectView {
|
||||||
this.editor.setGutterMarker(line, "gutter-info", div);
|
this.editor.setGutterMarker(line, "gutter-info", div);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearCurrentLine();
|
this.clearCurrentLine(moveCursor);
|
||||||
if (line>0) {
|
if (line>0) {
|
||||||
addCurrentMarker(line-1);
|
addCurrentMarker(line-1);
|
||||||
if (moveCursor)
|
if (moveCursor)
|
||||||
|
@ -333,18 +337,18 @@ export class SourceEditor implements ProjectView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCurrentLine() {
|
clearCurrentLine(moveCursor:boolean) {
|
||||||
if (this.currentDebugLine) {
|
if (this.currentDebugLine) {
|
||||||
this.editor.clearGutter("gutter-info");
|
this.editor.clearGutter("gutter-info");
|
||||||
this.editor.setSelection(this.editor.getCursor());
|
if (moveCursor) this.editor.setSelection(this.editor.getCursor());
|
||||||
this.currentDebugLine = 0;
|
this.currentDebugLine = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveLine() {
|
getActiveLine() {
|
||||||
var state = lastDebugState;
|
var state = lastDebugState;
|
||||||
if (state && state.c && this.sourcefile) {
|
if (this.sourcefile && state) {
|
||||||
var EPC = state.c.EPC || state.c.PC;
|
var EPC = (state && state.c && (state.c.EPC || state.c.PC)); // || (platform.getPC && platform.getPC());
|
||||||
var res = this.sourcefile.findLineForOffset(EPC, 15);
|
var res = this.sourcefile.findLineForOffset(EPC, 15);
|
||||||
return res && res.line;
|
return res && res.line;
|
||||||
} else
|
} else
|
||||||
|
@ -352,7 +356,7 @@ export class SourceEditor implements ProjectView {
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshDebugState(moveCursor:boolean) {
|
refreshDebugState(moveCursor:boolean) {
|
||||||
this.clearCurrentLine();
|
this.clearCurrentLine(moveCursor);
|
||||||
var line = this.getActiveLine();
|
var line = this.getActiveLine();
|
||||||
if (line >= 0) {
|
if (line >= 0) {
|
||||||
this.setCurrentLine(line, moveCursor);
|
this.setCurrentLine(line, moveCursor);
|
||||||
|
|
414
src/platform/basic.ts
Normal file
414
src/platform/basic.ts
Normal file
|
@ -0,0 +1,414 @@
|
||||||
|
|
||||||
|
import { Platform, BreakpointCallback } from "../common/baseplatform";
|
||||||
|
import { PLATFORMS, AnimationTimer } from "../common/emu";
|
||||||
|
import { loadScript } from "../ide/ui";
|
||||||
|
import { BASICRuntime } from "../common/basic/runtime";
|
||||||
|
import { BASICProgram } from "../common/basic/compiler";
|
||||||
|
|
||||||
|
const BASIC_PRESETS = [
|
||||||
|
{ id: 'hello.bas', name: 'Hello World' }
|
||||||
|
];
|
||||||
|
|
||||||
|
class TeleType {
|
||||||
|
page: HTMLElement;
|
||||||
|
fixed: boolean;
|
||||||
|
scrolldiv: HTMLElement;
|
||||||
|
|
||||||
|
curline: HTMLElement;
|
||||||
|
curstyle: number;
|
||||||
|
reverse: boolean;
|
||||||
|
col: number;
|
||||||
|
row: number;
|
||||||
|
lines: HTMLElement[];
|
||||||
|
ncharsout : number;
|
||||||
|
|
||||||
|
constructor(page: HTMLElement, fixed: boolean) {
|
||||||
|
this.page = page;
|
||||||
|
this.fixed = fixed;
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.curline = null;
|
||||||
|
this.curstyle = 0;
|
||||||
|
this.reverse = false;
|
||||||
|
this.col = 0;
|
||||||
|
this.row = -1;
|
||||||
|
this.lines = [];
|
||||||
|
this.ncharsout = 0;
|
||||||
|
$(this.page).empty();
|
||||||
|
}
|
||||||
|
ensureline() {
|
||||||
|
if (this.curline == null) {
|
||||||
|
this.curline = this.lines[++this.row];
|
||||||
|
if (this.curline == null) {
|
||||||
|
this.curline = $('<div class="transcript-line"/>')[0];
|
||||||
|
this.page.appendChild(this.curline);
|
||||||
|
this.lines[this.row] = this.curline;
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushline() {
|
||||||
|
this.curline = null;
|
||||||
|
}
|
||||||
|
// TODO: support fixed-width window (use CSS grid?)
|
||||||
|
addtext(line: string, style: number) {
|
||||||
|
this.ensureline();
|
||||||
|
if (line.length) {
|
||||||
|
// in fixed mode, only do characters
|
||||||
|
if (this.fixed && line.length > 1) {
|
||||||
|
for (var i = 0; i < line.length; i++)
|
||||||
|
this.addtext(line[i], style);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var span = $("<span/>").text(line);
|
||||||
|
for (var i = 0; i < 8; i++) {
|
||||||
|
if (style & (1 << i))
|
||||||
|
span.addClass("transcript-style-" + (1 << i));
|
||||||
|
}
|
||||||
|
if (this.reverse) span.addClass("transcript-reverse");
|
||||||
|
//span.data('vmip', this.vm.pc);
|
||||||
|
// in fixed mode, we can overwrite individual characters
|
||||||
|
if (this.fixed && line.length == 1 && this.col < this.curline.childNodes.length) {
|
||||||
|
this.curline.replaceChild(span[0], this.curline.childNodes[this.col]);
|
||||||
|
} else {
|
||||||
|
span.appendTo(this.curline);
|
||||||
|
}
|
||||||
|
this.col += line.length;
|
||||||
|
this.ncharsout += line.length;
|
||||||
|
//this.movePrintHead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newline() {
|
||||||
|
this.flushline();
|
||||||
|
this.col = 0;
|
||||||
|
}
|
||||||
|
// TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines?
|
||||||
|
print(val: string) {
|
||||||
|
// split by newlines
|
||||||
|
var lines = val.split("\n");
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
if (i > 0) this.newline();
|
||||||
|
this.addtext(lines[i], this.curstyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move_cursor(col: number, row: number) {
|
||||||
|
if (!this.fixed) return; // fixed windows only
|
||||||
|
// ensure enough row elements
|
||||||
|
while (this.lines.length <= row) {
|
||||||
|
this.flushline();
|
||||||
|
this.ensureline();
|
||||||
|
}
|
||||||
|
// select row element
|
||||||
|
this.curline = this.lines[row];
|
||||||
|
this.row = row;
|
||||||
|
// get children in row (individual text cells)
|
||||||
|
var children = $(this.curline).children();
|
||||||
|
// add whitespace to line?
|
||||||
|
if (children.length > col) {
|
||||||
|
this.col = col;
|
||||||
|
} else {
|
||||||
|
while (this.col < col)
|
||||||
|
this.addtext(' ', this.curstyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setrows(size: number) {
|
||||||
|
if (!this.fixed) return; // fixed windows only
|
||||||
|
// truncate rows?
|
||||||
|
var allrows = $(this.page).children();
|
||||||
|
if (allrows.length > size) {
|
||||||
|
this.flushline();
|
||||||
|
allrows.slice(size).remove();
|
||||||
|
this.lines = this.lines.slice(0, size);
|
||||||
|
//this.move_cursor(0,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formfeed() {
|
||||||
|
this.newline();
|
||||||
|
}
|
||||||
|
scrollToBottom() {
|
||||||
|
this.curline.scrollIntoView();
|
||||||
|
}
|
||||||
|
movePrintHead() {
|
||||||
|
var x = $(this.page).position().left + this.col * ($(this.page).width() / 80);
|
||||||
|
$("#printhead").offset({left: x});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TeleTypeWithKeyboard extends TeleType {
|
||||||
|
input : HTMLInputElement;
|
||||||
|
runtime : BASICRuntime;
|
||||||
|
platform : BASICPlatform;
|
||||||
|
keepinput : boolean = true;
|
||||||
|
|
||||||
|
focused : boolean = true;
|
||||||
|
scrolling : number = 0;
|
||||||
|
waitingfor : string;
|
||||||
|
resolveInput;
|
||||||
|
|
||||||
|
constructor(page: HTMLElement, fixed: boolean, input: HTMLInputElement, platform: BASICPlatform) {
|
||||||
|
super(page, fixed);
|
||||||
|
this.input = input;
|
||||||
|
this.platform = platform;
|
||||||
|
this.runtime = platform.runtime;
|
||||||
|
this.runtime.input = async (prompt:string) => {
|
||||||
|
return new Promise( (resolve, reject) => {
|
||||||
|
this.addtext(prompt, 0);
|
||||||
|
this.addtext('? ', 0);
|
||||||
|
this.waitingfor = 'line';
|
||||||
|
this.focusinput();
|
||||||
|
this.resolveInput = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.input.onkeypress = (e) => {
|
||||||
|
this.sendkey(e);
|
||||||
|
};
|
||||||
|
this.input.onfocus = (e) => {
|
||||||
|
this.focused = true;
|
||||||
|
console.log('inputline gained focus');
|
||||||
|
};
|
||||||
|
$("#workspace").on('click', (e) => {
|
||||||
|
this.focused = false;
|
||||||
|
console.log('inputline lost focus');
|
||||||
|
});
|
||||||
|
this.page.onclick = (e) => {
|
||||||
|
this.input.focus();
|
||||||
|
};
|
||||||
|
this.hideinput();
|
||||||
|
}
|
||||||
|
focusinput() {
|
||||||
|
this.ensureline();
|
||||||
|
// don't steal focus while editing
|
||||||
|
if (this.keepinput)
|
||||||
|
$(this.input).css('visibility', 'visible');
|
||||||
|
else
|
||||||
|
$(this.input).appendTo(this.curline).show()[0];
|
||||||
|
this.scrollToBottom();
|
||||||
|
if (this.focused) {
|
||||||
|
$(this.input).focus();
|
||||||
|
}
|
||||||
|
// change size
|
||||||
|
if (this.waitingfor == 'char')
|
||||||
|
$(this.input).addClass('transcript-input-char')
|
||||||
|
else
|
||||||
|
$(this.input).removeClass('transcript-input-char')
|
||||||
|
}
|
||||||
|
hideinput() {
|
||||||
|
if (this.keepinput)
|
||||||
|
$(this.input).css('visibility','hidden');
|
||||||
|
else
|
||||||
|
$(this.input).appendTo($(this.page).parent()).hide();
|
||||||
|
}
|
||||||
|
clearinput() {
|
||||||
|
this.input.value = '';
|
||||||
|
this.waitingfor = null;
|
||||||
|
}
|
||||||
|
sendkey(e: KeyboardEvent) {
|
||||||
|
if (this.waitingfor == 'line') {
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
this.sendinput(this.input.value.toString());
|
||||||
|
}
|
||||||
|
} else if (this.waitingfor == 'char') {
|
||||||
|
this.sendchar(e.keyCode);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendinput(s: string) {
|
||||||
|
if (this.resolveInput) {
|
||||||
|
s = s.toUpperCase();
|
||||||
|
this.addtext(s, 4);
|
||||||
|
this.flushline();
|
||||||
|
this.resolveInput(s.split(','));
|
||||||
|
this.resolveInput = null;
|
||||||
|
}
|
||||||
|
this.clearinput();
|
||||||
|
this.hideinput(); // keep from losing input handlers
|
||||||
|
}
|
||||||
|
sendchar(code: number) {
|
||||||
|
}
|
||||||
|
ensureline() {
|
||||||
|
if (!this.keepinput) $(this.input).hide();
|
||||||
|
super.ensureline();
|
||||||
|
}
|
||||||
|
scrollToBottom() {
|
||||||
|
// TODO: fails when lots of lines are scrolled
|
||||||
|
if (this.scrolldiv) {
|
||||||
|
this.scrolling++;
|
||||||
|
$(this.scrolldiv).stop().animate({scrollTop: $(this.page).height()}, 200, 'swing', () => {
|
||||||
|
this.scrolling = 0;
|
||||||
|
this.ncharsout = 0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.input.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isBusy() {
|
||||||
|
// stop execution when scrolling and printing non-newlines
|
||||||
|
return this.scrolling > 0 && this.ncharsout > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BASICPlatform implements Platform {
|
||||||
|
mainElement: HTMLElement;
|
||||||
|
program: BASICProgram;
|
||||||
|
runtime: BASICRuntime;
|
||||||
|
timer: AnimationTimer;
|
||||||
|
tty: TeleTypeWithKeyboard;
|
||||||
|
ips: number = 500;
|
||||||
|
clock: number = 0;
|
||||||
|
|
||||||
|
constructor(mainElement: HTMLElement) {
|
||||||
|
//super();
|
||||||
|
this.mainElement = mainElement;
|
||||||
|
mainElement.style.overflowY = 'auto';
|
||||||
|
mainElement.style.backgroundColor = 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await loadScript('./gen/common/basic/runtime.js');
|
||||||
|
// create runtime
|
||||||
|
this.runtime = new BASICRuntime();
|
||||||
|
this.runtime.reset();
|
||||||
|
// create divs
|
||||||
|
var parent = this.mainElement;
|
||||||
|
// TODO: input line should be always flush left
|
||||||
|
var gameport = $('<div id="gameport" style="margin-top:80vh"/>').appendTo(parent);
|
||||||
|
var windowport = $('<div id="windowport" class="transcript transcript-style-2"/>').appendTo(gameport);
|
||||||
|
var inputline = $('<input class="transcript-input transcript-style-2" type="text" style="max-width:95%"/>').appendTo(parent);
|
||||||
|
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
|
||||||
|
this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this);
|
||||||
|
this.tty.scrolldiv = parent;
|
||||||
|
this.timer = new AnimationTimer(60, this.advance1_60.bind(this));
|
||||||
|
this.resize = () => {
|
||||||
|
// set font size proportional to window width
|
||||||
|
var charwidth = $(gameport).width() * 1.6 / 80;
|
||||||
|
$(windowport).css('font-size', charwidth + 'px');
|
||||||
|
this.tty.scrollToBottom();
|
||||||
|
}
|
||||||
|
this.resize();
|
||||||
|
this.runtime.print = this.tty.print.bind(this.tty);
|
||||||
|
this.runtime.resume = this.resume.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
advance1_60() {
|
||||||
|
if (this.tty.isBusy()) return;
|
||||||
|
this.clock += this.ips/60;
|
||||||
|
while (!this.runtime.exited && this.clock-- > 0) {
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advance(novideo?: boolean) : number {
|
||||||
|
if (this.runtime.running) {
|
||||||
|
try {
|
||||||
|
var more = this.runtime.step();
|
||||||
|
if (!more) {
|
||||||
|
this.pause();
|
||||||
|
if (this.runtime.exited) {
|
||||||
|
this.tty.print("\n\n");
|
||||||
|
this.tty.addtext("*** END OF PROGRAM ***", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.break();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize: () => void;
|
||||||
|
|
||||||
|
loadROM(title, data) {
|
||||||
|
this.reset();
|
||||||
|
this.program = data;
|
||||||
|
this.runtime.load(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getROMExtension() {
|
||||||
|
return ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
var didExit = this.runtime.exited;
|
||||||
|
this.tty.clear();
|
||||||
|
this.runtime.reset();
|
||||||
|
// restart program if it's finished, otherwise reset and hold
|
||||||
|
if (didExit) {
|
||||||
|
this.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(): void {
|
||||||
|
this.timer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
resume(): void {
|
||||||
|
this.clock = 0;
|
||||||
|
this.timer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning() { return this.timer.isRunning(); }
|
||||||
|
getDefaultExtension() { return ".bas"; }
|
||||||
|
getToolForFilename() { return "basic"; }
|
||||||
|
getPresets() { return BASIC_PRESETS; }
|
||||||
|
|
||||||
|
getPC() {
|
||||||
|
return this.runtime.curpc;
|
||||||
|
}
|
||||||
|
getSP() {
|
||||||
|
return 0x1000 - this.runtime.returnStack.length;
|
||||||
|
}
|
||||||
|
isStable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCPUState() {
|
||||||
|
return { PC: this.getPC(), SP: this.getSP() }
|
||||||
|
}
|
||||||
|
saveState() {
|
||||||
|
return {
|
||||||
|
c: this.getCPUState(),
|
||||||
|
rt: $.extend(true, {}, this.runtime) // TODO: don't take all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadState(state) {
|
||||||
|
$.extend(true, this.runtime, state);
|
||||||
|
}
|
||||||
|
getDebugTree() {
|
||||||
|
return this.runtime;
|
||||||
|
}
|
||||||
|
inspect(sym: string) {
|
||||||
|
var o = this.runtime.vars[sym];
|
||||||
|
if (o != null) {
|
||||||
|
return o.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: debugging (get running state, etc)
|
||||||
|
|
||||||
|
onBreakpointHit : BreakpointCallback;
|
||||||
|
|
||||||
|
setupDebug(callback : BreakpointCallback) : void {
|
||||||
|
this.onBreakpointHit = callback;
|
||||||
|
}
|
||||||
|
clearDebug() {
|
||||||
|
this.onBreakpointHit = null;
|
||||||
|
}
|
||||||
|
step() {
|
||||||
|
this.pause();
|
||||||
|
this.advance();
|
||||||
|
this.break();
|
||||||
|
}
|
||||||
|
break() {
|
||||||
|
if (this.onBreakpointHit) {
|
||||||
|
this.onBreakpointHit(this.saveState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
PLATFORMS['basic'] = BASICPlatform;
|
|
@ -311,9 +311,9 @@ var workerseq : number = 0;
|
||||||
|
|
||||||
function compareData(a:FileData, b:FileData) : boolean {
|
function compareData(a:FileData, b:FileData) : boolean {
|
||||||
if (a.length != b.length) return false;
|
if (a.length != b.length) return false;
|
||||||
if (typeof a === 'string' && typeof b === 'string')
|
if (typeof a === 'string' && typeof b === 'string') {
|
||||||
return a==b;
|
return a == b;
|
||||||
else {
|
} else {
|
||||||
for (var i=0; i<a.length; i++) {
|
for (var i=0; i<a.length; i++) {
|
||||||
//if (a[i] != b[i]) console.log('differ at byte',i,a[i],b[i]);
|
//if (a[i] != b[i]) console.log('differ at byte',i,a[i],b[i]);
|
||||||
if (a[i] != b[i]) return false;
|
if (a[i] != b[i]) return false;
|
||||||
|
@ -2636,6 +2636,34 @@ function compileFastBasic(step:BuildStep) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compileBASIC(step:BuildStep) {
|
||||||
|
var jsonpath = step.path + ".json";
|
||||||
|
gatherFiles(step);
|
||||||
|
if (staleFiles(step, [jsonpath])) {
|
||||||
|
setupRequireFunction();
|
||||||
|
loadGen("common/basic/compiler");
|
||||||
|
var parser = new emglobal['BASICParser']();
|
||||||
|
delete emglobal['require'];
|
||||||
|
var code = getWorkFileAsString(step.path);
|
||||||
|
try {
|
||||||
|
var ast = parser.parseFile(code, step.path);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
if (parser.errors.length == 0) throw e;
|
||||||
|
}
|
||||||
|
if (parser.errors.length) {
|
||||||
|
return {errors: parser.errors};
|
||||||
|
}
|
||||||
|
// put AST into JSON (sans source locations) to see if it has changed
|
||||||
|
var json = JSON.stringify(ast, (key,value) => { return (key=='$loc'?undefined:value) });
|
||||||
|
putWorkFile(jsonpath, json);
|
||||||
|
if (anyTargetChanged(step, [jsonpath])) return {
|
||||||
|
output: ast,
|
||||||
|
listings: parser.getListings(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////
|
////////////////////////////
|
||||||
|
|
||||||
var TOOLS = {
|
var TOOLS = {
|
||||||
|
@ -2668,6 +2696,7 @@ var TOOLS = {
|
||||||
'inform6': compileInform6,
|
'inform6': compileInform6,
|
||||||
'merlin32': assembleMerlin32,
|
'merlin32': assembleMerlin32,
|
||||||
'fastbasic': compileFastBasic,
|
'fastbasic': compileFastBasic,
|
||||||
|
'basic': compileBASIC,
|
||||||
}
|
}
|
||||||
|
|
||||||
var TOOL_PRELOADFS = {
|
var TOOL_PRELOADFS = {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user