@moonbase-machines/c64 0.0.0-stage → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Wharton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # Temporary Holding Version
1
+ # @moonbase-machines/c64
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Commodore 64 as far as the games use it: the 6502 (`@moonbase-machines/mos6502`), the VIC-II drawn a raster
4
+ line at a time, the SID's three voices, a 1541 and its `.d64` disks, a printer, the keyboard and
5
+ joystick, and Moonbase's own KERNAL, BASIC and characters in place of Commodore's ROMs, so that
6
+ nothing of Commodore's is needed to run.
7
+
8
+ And for taking a title's disks apart: a 1541 disk read as well as written (`d64.js`), and a BASIC program listed
9
+ as the 64 lists it (`basic-list.js`).
10
+
11
+ Part of Moonbase (moonbase.au): Mark Wharton's titles of 1985 to 1995, running again in the browser
12
+ on these machines. MIT; see LICENSE. Each file opens with what it is and what it covers.
@@ -0,0 +1,171 @@
1
+ // A program's statements, as read from Blitz!'s code or by basic-lang.js from BASIC's text,
2
+ // compiled to JavaScript: a module, a step a statement, which an engine runs, each step's work
3
+ // done by the BASIC of basic.js (`b`, the engine's `basic`).
4
+ //
5
+ // compile(statements, { name, source, mem, cost, render }) the module's text
6
+ // instantiate(statements, { ... }) the same, as the program itself
7
+ // statements the tree: statements, and labels where a jump goes
8
+ // mem memory as the program has it, for the text after each SYS: its parameters'
9
+ // expressions are compiled too (`params`), for the ROM's routines that the
10
+ // machine code reads them with
11
+ // cost the cycles a statement takes, for the page's clock
12
+ // render a statement as BASIC, each step's text
13
+ import { expression } from './basic-lang.js';
14
+
15
+ const hex = (v, w = 4) => v.toString(16).toUpperCase().padStart(w, '0');
16
+ const q = s => JSON.stringify(s);
17
+
18
+ // where an operation that can fail is, as the last argument of its call (evaluated after the
19
+ // others, so after any inner operation's): BASIC's error gives it (basic.js `here`)
20
+ const here = x => x.op !== undefined ? `, b.here(0x${x.op.toString(16)})` : '';
21
+ // an expression as JavaScript, and whether it is a string
22
+ function js(x) {
23
+ switch (x.k) {
24
+ case 'num': return String(x.v);
25
+ case 'str': return q(x.s);
26
+ case 'special': return { ti: 'b.ti()', 'ti$': 'b.tis()', st: 'b.st()', pi: String(Math.PI) }[x.n];
27
+ case 'var': return `b.get(${q(x.n)})`;
28
+ case 'elem': return `b.elem(${q(x.n)}, [${x.i.map(js).join(', ')}]${here(x)})`;
29
+ case 'neg': return `-b.num(${js(x.a)})`;
30
+ case 'not': return `b.not(${js(x.a)})`;
31
+ case 'op': {
32
+ const a = js(x.a), c = js(x.b);
33
+ switch (x.o) {
34
+ case '+': return `b.add(${a}, ${c}${here(x)})`;
35
+ case '-': return `(b.num(${a}) - b.num(${c}))`;
36
+ case '*': return `(b.num(${a}) * b.num(${c}))`;
37
+ case '/': return `b.div(${a}, ${c}${here(x)})`;
38
+ case '^': return `b.pow(${a}, ${c}${here(x)})`;
39
+ case 'and': return `b.and(${a}, ${c}${here(x)})`;
40
+ case 'or': return `b.or(${a}, ${c}${here(x)})`;
41
+ default: return `b.cmp(${a}, ${c}, ${q(x.o)}${here(x)})`;
42
+ }
43
+ }
44
+ case 'fn-call': return `b.fn(${q(x.n)}, ${js(x.a)})`;
45
+ case 'fn': {
46
+ const a = x.args.map(js);
47
+ const F = { 'str$': 'str', 'chr$': 'chr', 'left$': 'left', 'right$': 'right', 'mid$': 'mid' };
48
+ return `b.${F[x.n] ?? x.n}(${a.join(', ')}${a.length < 3 && (x.n === 'mid$') ? ', undefined' : ''}${here(x)})`;
49
+ }
50
+ }
51
+ throw new Error('no expression ' + x.k);
52
+ }
53
+
54
+ // a place to store into: a variable or an array's element
55
+ const store = (t, v, s) => t.k === 'elem' ? `b.setElem(${q(t.n)}, [${t.i.map(js).join(', ')}], ${v}${s ? here(s) : ''})` : `b.set(${q(t.n)}, ${v}${s ? here(s) : ''})`;
56
+ const isString = t => t.n.endsWith('$');
57
+
58
+ export function compile(statements, { name, source, mem, cost = 0, render }) {
59
+ // each statement a step, each label the step after it
60
+ const labels = {}, steps = [];
61
+ for (const s of statements) { if (s.label !== undefined) labels['L' + hex(typeof s.label === 'number' ? s.label : parseInt(s.label.slice(1), 16))] = steps.length; else steps.push(s); }
62
+ // (a jump to a line not there is UNDEF'D STATEMENT when it is taken: `missing`)
63
+ const step = to => { if (to?.missing !== undefined) return null; const k = labels['L' + hex(to)]; if (k === undefined) throw new Error('no label for $' + hex(to)); return k; };
64
+ const params = new Map();
65
+ const body = steps.map((s, k) => {
66
+ const go = to => to?.missing !== undefined ? 'b.undef();' : `e.pc = ${step(to)};`;
67
+ let c;
68
+ switch (s.k) {
69
+ case 'let': c = store(s.t, js(s.e), s); break;
70
+ case 'dim': c = `b.dim(${q(s.n)}, [${s.sizes.map(js).join(', ')}]);`; break;
71
+ case 'poke': c = `b.poke(${js(s.a)}, ${js(s.v)}${here(s)});`; break;
72
+ case 'for': c = `b.for(${q(s.v)}, ${js(s.to)}, ${s.step ? js(s.step) : 1}, ${k + 1});`; break;
73
+ case 'next': case 'next-var': c = `{ const back = b.next(${s.v ? q(s.v) : 'null'}); if (back !== null) e.pc = back; }`; break;
74
+ case 'goto': c = go(s.to); break;
75
+ case 'gosub': c = `b.gosub(${k + 1}); ${go(s.to)}`; break;
76
+ case 'return': c = 'e.pc = b.ret();'; break;
77
+ case 'on': c = `{ const n = b.on(${js(s.e)}${here(s)}); if (n >= 1 && n <= ${s.to.length}) { const to = [${s.to.map(step).join(', ')}][n - 1]; if (to === null) b.undef(); ${s.gosub ? `b.gosub(${k + 1}); ` : ''}e.pc = to; } }`; break;
78
+ case 'unless': c = s.to < 0 ? `if (!b.num(${js(s.e)})) e.pc = ${steps.length};` : `if (!b.num(${js(s.e)})) ${go(s.to)}`; break;
79
+ case 'if-goto': c = `if (b.num(${js(s.e)})) ${go(s.to)}`; break;
80
+ case 'if-return': c = `if (b.num(${js(s.e)})) e.pc = b.ret();`; break;
81
+ case 'print': c = `b.print(${js(s.args[0])});`; break;
82
+ case 'print,': c = `b.print(${js(s.args[0])}); b.comma();`; break;
83
+ case 'print-line': c = `b.print(${js(s.args[0])}); b.newline();`; break;
84
+ case 'print-line#': c = `b.print(${js(s.args[0])}); b.newline(); b.clrchn();`; break;
85
+ case 'print#': c = `b.output(${js(s.args[0])});`; break;
86
+ case 'print#-line': c = `b.output(${js(s.args[0])}); b.newline(); b.clrchn();`; break;
87
+ case 'clrchn': c = 'b.clrchn();'; break;
88
+ case 'input#': c = `b.input(${js(s.args[0])});`; break;
89
+ // GET#: the file the input, a character for each variable (none, ""), and the keyboard again
90
+ case 'get#': c = `b.input(${js(s.args[0])}); ${s.targets.map(t => store(t, isString(t) ? 'b.getFrom()' : 'b.val(b.getFrom())')).join('; ')}; b.clrchn();`; break;
91
+ // (an item of an INPUT given up, a read from the file timed out, leaves its variable)
92
+ case 'input': case 'input-item': c = `{ const v = b.item(${isString(s.t)}, ${s.k === 'input'}); if (v !== undefined) ${store(s.t, 'v')}; }` + (s.last ? ' b.inputDone();' : ''); break;
93
+ case 'input-start': c = ''; break;
94
+ case 'open': c = `b.open([${s.args.map(js).join(', ')}]);`; break;
95
+ case 'close': c = `b.close(${js(s.args[0])});`; break;
96
+ // WAIT: this step again until the place's bits come, the wait the run stops for
97
+ case 'wait': c = `if (!b.wait(${s.args.map(js).join(', ')})) { e.pc = ${k}; if (++e.idle > (e.routines.polls ?? 200) && !e.clock) e.polled = true; }`; break;
98
+ case 'print-comma': c = 'b.comma();'; break;
99
+ case 'newline': c = 'b.newline();'; break;
100
+ case 'tab': c = `b.tab(${js(s.args[0])}${here(s)});`; break;
101
+ case 'spc': c = `b.spc(${js(s.args[0])}${here(s)});`; break;
102
+ case 'get': c = `{ const k = b.key(); if (k === null) { e.pc = ${k}; return; } ${store(s.t, isString(s.t) ? 'k' : 'b.val(k)')} }`; break;
103
+ case 'read': case 'read$': c = store(s.t, `b.read(${isString(s.t)})`); break;
104
+ case 'clr': c = 'b.clr();'; break;
105
+ case 'restore': c = 'b.restore();'; break;
106
+ // DEF FN: the function kept, its expression a function of the BASIC it runs on
107
+ case 'def': c = `b.def(${q(s.n)}, ${q(s.p)}, b => ${js(s.e)});`; break;
108
+ case 'end': case 'stop': c = 'e.ended = true;'; break;
109
+ case 'ti$=': c = `b.setTis(${js(s.args[0])});`; break;
110
+ // RUN, Blitz!'s: the files closed, the variables cleared, the program's start run again (its
111
+ // DIMs), and on from after the RUN; the start's jump past the DATA is to there, or else to
112
+ // the main code
113
+ case 'run': c = `b.run(${k + 1}); ${s.to !== undefined ? go(s.to) : 'e.pc = 0;'}`; break;
114
+ // a statement BASIC could not read: its error, when it is reached
115
+ case 'fail': c = `b.failWith(${q(s.what)});`; break;
116
+ case 'run-main': c = `e.pc = b.resume ?? ${steps.length ? step(s.to) : 0}; b.resume = undefined;`; break;
117
+ case 'nop': case 'flag+40': case 'flag-40': case 'flag+80': case 'flag-80': c = ''; break;
118
+ // SYS: the machine code at the address, with its text from just after SYS for its
119
+ // parameters
120
+ case 'sys': {
121
+ c = `return b.sys(${js(s.e)}, 0x${hex(s.text)}, s[${k}]);`;
122
+ if (mem) for (let at = s.text; at < s.end;) {
123
+ while (mem[at] === 0x20 || mem[at] === 0x2c) at++;
124
+ if (at >= s.end) break;
125
+ // (as far as there are expressions: what follows SYS 2076, Blitz!'s own bytes, BASIC
126
+ // never reads, the machine code never coming back to it)
127
+ let x; try { x = expression(mem, at); } catch { break; }
128
+ params.set(at, [x.end, js(x.e), x.e.k === 'str' ? x.e.at : null]);
129
+ at = x.end;
130
+ }
131
+ break;
132
+ }
133
+ // what goes through the KERNAL's files and the rest: the library's
134
+ default: c = `return b.statement(${q(s.k)}, [${(s.args ?? []).map(js).join(', ')}]${s.t ? ', ' + q(s.t.n) : ''}, s[${k}]);`;
135
+ }
136
+ // (and where the statement's own instruction is, for an error of its own, RETURN WITHOUT
137
+ // GOSUB, before anything else in it notes its place)
138
+ return (s.op !== undefined ? `b.here(0x${s.op.toString(16)}); ` : '') + c;
139
+ });
140
+ const out = [];
141
+ out.push(`// ${name}, compiled from ${source} by @moonbase-machines/c64/basic-compile.js: compiled again, never edited`);
142
+ out.push('export default {');
143
+ out.push(` labels: ${JSON.stringify(labels)},`);
144
+ out.push(' data: [],');
145
+ out.push(' steps: [');
146
+ for (const [k, s] of steps.entries()) out.push(` [0x${hex(s.at)}, ${k}, ${q(render(s))}],`);
147
+ out.push(' ],');
148
+ out.push(' run: {');
149
+ for (const [k, c] of body.entries()) out.push(` ${k}: (e, s) => { const b = e.basic;${cost ? ` b.cost(${cost});` : ''} ${c} },`);
150
+ out.push(' },');
151
+ // each SYS's parameters by where their text starts: where it ends, its value, and a string's
152
+ // place in memory
153
+ out.push(' params: {');
154
+ for (const [at, [end, v, place]] of [...params].sort((a, b) => a[0] - b[0])) out.push(` 0x${hex(at)}: [0x${hex(end)}, b => ${v}${place !== null ? `, 0x${hex(place)}` : ''}],`);
155
+ out.push(' },');
156
+ out.push('};');
157
+ return out.join('\n') + '\n';
158
+ }
159
+
160
+ // The same, as the program itself rather than its module's text: for a program compiled as it
161
+ // runs (moonbase's BASIC ROM's RUN, rom.js), from the very code the remake's are written
162
+ // with. (Its steps' functions are the text's, made by the browser's or Node's own JavaScript.)
163
+ export function instantiate(statements, options) {
164
+ const text = compile(statements, options);
165
+ return new Function('return ' + text.slice(text.indexOf('export default ') + 'export default '.length).replace(/;\s*$/, ''))();
166
+ }
167
+
168
+ // An expression of the tree as a function of BASIC (`b`, basic.js's methods), from the same
169
+ // code the compiled programs' are: for moonbase's BASIC ROM's evaluator, an expression read from
170
+ // BASIC's text as the original runs (basic-rom.js)
171
+ export const evaluator = x => new Function('b', 'return ' + js(x));
package/basic-lang.js ADDED
@@ -0,0 +1,394 @@
1
+ // Commodore BASIC's tokenized text read into the tree made of Blitz!'s code: expressions, for
2
+ // the text after a SYS that the machine code reads its parameters from, and statements, for
3
+ // Goldfields' boot program, GOLDFIELDS. Shared: a remake is built with it (basic-compile.js), and
4
+ // moonbase's BASIC ROM reads with it as the original runs (basic-rom.js).
5
+ //
6
+ // expression(mem, at) { e, end }: the expression at `at`, and where it ends
7
+ // lines(mem, from) the program at `from` as statements, each line a label of its address
8
+ // crunch(text) a typed line's text to BASIC's tokenized bytes, as the ROM's CRUNCH
9
+ // list(bytes) a line's bytes back to the text LIST prints
10
+ // store(mem, txttab, n, bytes) a line put in its place in the program, or taken out
11
+
12
+ import { fail, BasicError } from './numbers.js';
13
+
14
+ // text BASIC cannot read: its SYNTAX ERROR when the statement is reached, as BASIC finds it only
15
+ // then (a line changed by a POKE, say); what was wrong kept for whoever looks
16
+ const syntax = detail => Object.assign(new BasicError('?SYNTAX ERROR'), { detail });
17
+
18
+ // the tokens from $80, as BASIC V2 has them
19
+ export const TOKENS = ('END FOR NEXT DATA INPUT# INPUT DIM READ LET GOTO RUN IF RESTORE GOSUB RETURN REM STOP ON WAIT LOAD SAVE VERIFY ' +
20
+ 'DEF POKE PRINT# PRINT CONT LIST CLR CMD SYS OPEN CLOSE GET NEW TAB( TO FN SPC( THEN NOT STEP + - * / ^ AND OR > = < ' +
21
+ 'SGN INT ABS USR FRE POS SQR RND LOG EXP COS SIN TAN ATN PEEK LEN STR$ VAL ASC CHR$ LEFT$ RIGHT$ MID$ GO').split(' ');
22
+ const T = Object.fromEntries(TOKENS.map((t, k) => [t, 0x80 + k]));
23
+ const FUNCTIONS = new Set(['SGN', 'INT', 'ABS', 'USR', 'FRE', 'POS', 'SQR', 'RND', 'LOG', 'EXP', 'COS', 'SIN', 'TAN', 'ATN', 'PEEK', 'LEN', 'STR$', 'VAL', 'ASC', 'CHR$', 'LEFT$', 'RIGHT$', 'MID$']);
24
+ // the binary operators by their token, and how tightly each binds, as BASIC evaluates them
25
+ const BINARY = { [T['OR']]: ['or', 1], [T['AND']]: ['and', 2], [T['>']]: ['>', 4], [T['=']]: ['=', 4], [T['<']]: ['<', 4], [T['+']]: ['+', 5], [T['-']]: ['-', 5], [T['*']]: ['*', 6], [T['/']]: ['/', 6], [T['^']]: ['^', 8] };
26
+
27
+ // a reader of the text at `at`: BASIC skips spaces between everything outside a string
28
+ function reader(mem, at) {
29
+ const r = {
30
+ at,
31
+ peek() { while (mem[r.at] === 0x20) r.at++; return mem[r.at]; },
32
+ next() { const b = r.peek(); r.at++; return b; },
33
+ end() { const b = r.peek(); return b === 0 || b === 0x3a; },
34
+ expect(b) { if (r.next() !== b) throw syntax(`$${(r.at - 1).toString(16)}: expected $${b.toString(16)}`); },
35
+ };
36
+ return r;
37
+ }
38
+
39
+ const letter = b => b >= 0x41 && b <= 0x5a, digit = b => b >= 0x30 && b <= 0x39;
40
+
41
+ // a variable's name: its first two characters (the rest are BASIC's to ignore), and $ or %
42
+ function name(r) {
43
+ let n = String.fromCharCode(r.next());
44
+ while (letter(mem(r)) || digit(mem(r))) { const c = String.fromCharCode(r.next()); if (n.length < 2) n += c; }
45
+ if (mem(r) === 0x24 || mem(r) === 0x25) n += String.fromCharCode(r.next());
46
+ return n;
47
+ }
48
+ const mem = r => r.peek();
49
+
50
+ function primary(r, m) {
51
+ const b = r.peek();
52
+ if (b === 0x22) { // a string, to its closing quote or the line's end
53
+ r.at++; const from = r.at; while (m[r.at] && m[r.at] !== 0x22) r.at++;
54
+ const s = String.fromCharCode(...m.subarray(from, r.at)); if (m[r.at] === 0x22) r.at++;
55
+ return { k: 'str', s, at: from };
56
+ }
57
+ if (digit(b) || b === 0x2e) { // a number: digits, a point, an exponent
58
+ let t = ''; const take = () => { t += String.fromCharCode(r.next()); };
59
+ while (digit(r.peek()) || r.peek() === 0x2e) take();
60
+ if (r.peek() === 0x45) { take(); if (r.peek() === T['-'] || r.peek() === T['+']) { r.at++; t += m[r.at - 1] === T['-'] ? '-' : '+'; } while (digit(r.peek())) take(); }
61
+ return { k: 'num', v: Number(t) };
62
+ }
63
+ if (b === 0x28) { r.at++; const e = expr(r, m, 0); r.expect(0x29); return e; }
64
+ if (b === T['-']) { r.at++; return { k: 'neg', a: expr(r, m, 7) }; }
65
+ if (b === T['+']) { r.at++; return expr(r, m, 7); }
66
+ if (b === T['NOT']) { r.at++; return { k: 'not', a: expr(r, m, 3) }; }
67
+ if (b === T['FN']) { r.at++; const n = name(r); r.expect(0x28); const a = expr(r, m, 0); r.expect(0x29); return { k: 'fn-call', n, a }; }
68
+ if (b >= 0x80 && FUNCTIONS.has(TOKENS[b - 0x80])) {
69
+ r.at++; r.expect(0x28); const args = [expr(r, m, 0)];
70
+ while (r.peek() === 0x2c) { r.at++; args.push(expr(r, m, 0)); }
71
+ r.expect(0x29);
72
+ return { k: 'fn', n: TOKENS[b - 0x80].toLowerCase(), args };
73
+ }
74
+ if (letter(b)) {
75
+ const n = name(r);
76
+ if (n === 'TI' || n === 'ST' || n === 'TI$') return { k: 'special', n: n.toLowerCase() };
77
+ if (r.peek() === 0x28) { r.at++; const i = [expr(r, m, 0)]; while (r.peek() === 0x2c) { r.at++; i.push(expr(r, m, 0)); } r.expect(0x29); return { k: 'elem', n, i }; }
78
+ return { k: 'var', n };
79
+ }
80
+ throw syntax(`$${r.at.toString(16)}: not an expression ($${b.toString(16)})`);
81
+ }
82
+
83
+ // an expression, by precedence: a relation of two of > = < at once (>=, <>) is one operator
84
+ function expr(r, m, min) {
85
+ let a = primary(r, m);
86
+ for (;;) {
87
+ const b = r.peek(), op = BINARY[b];
88
+ if (!op || op[1] < min) return a;
89
+ r.at++;
90
+ let o = op[0];
91
+ if (op[1] === 4 && BINARY[r.peek()]?.[1] === 4) { o = ({ '<>': '<>', '><': '<>', '<=': '<=', '=<': '<=', '>=': '>=', '=>': '>=' })[o + BINARY[r.peek()][0]]; r.at++; }
92
+ a = { k: 'op', o, a, b: expr(r, m, op[1] + 1) };
93
+ }
94
+ }
95
+
96
+ // The items of the program's DATA statements, in order, as READ finds them ($AC06): each its text,
97
+ // a quoted string to its closing quote, else to a comma, a colon or the line's end, its leading
98
+ // spaces passed over; whether it was quoted; and its line, for an error's IN. A REM's line, and
99
+ // a string's, hold no DATA.
100
+ export function dataItems(m, from) {
101
+ const items = [];
102
+ for (const l of program(m, from)) {
103
+ const b = l.bytes;
104
+ for (let i = 0, q = false; b[i]; i++) {
105
+ if (b[i] === 0x22) q = !q;
106
+ if (q) continue;
107
+ if (b[i] === REM_TOKEN) break;
108
+ if (b[i] !== DATA_TOKEN) continue;
109
+ for (i++; ; i++) {
110
+ while (b[i] === 0x20) i++;
111
+ let text = '', quoted = false;
112
+ if (b[i] === 0x22) { quoted = true; for (i++; b[i] && b[i] !== 0x22; i++) text += String.fromCharCode(b[i]); if (b[i] === 0x22) i++; while (b[i] && b[i] !== 0x2c && b[i] !== 0x3a) i++; }
113
+ else for (; b[i] && b[i] !== 0x2c && b[i] !== 0x3a; i++) text += String.fromCharCode(b[i]);
114
+ items.push({ text, quoted, line: l.n });
115
+ if (b[i] !== 0x2c) break;
116
+ }
117
+ if (!b[i]) break;
118
+ }
119
+ }
120
+ return items;
121
+ }
122
+
123
+ // A line typed with no number (direct mode), its statements after the program's at `from`, as
124
+ // BASIC runs it from its buffer at `at`: a jump from it goes into the program, and the program
125
+ // ends before it. The direct statements start at the label 'direct'.
126
+ export function direct(m, from, at) {
127
+ const out = lines(m, from);
128
+ const numbers = new Map(out.filter(s => s.label !== undefined).map(s => [s.line, s.label]));
129
+ out.push({ at: 0, k: 'end', args: [] }, { label: 'direct', line: 0xffff });
130
+ statement(reader(m, at), m, out, n => numbers.has(n) ? numbers.get(n) : { missing: n }, null);
131
+ out.push({ at: 0, k: 'end', args: [] });
132
+ return out;
133
+ }
134
+
135
+ export function expression(m, at) {
136
+ const r = reader(m, at), e = expr(r, m, 0);
137
+ return { e, end: r.at };
138
+ }
139
+
140
+ // The program from `from`, BASIC's own: each line its next line's address, its number, its
141
+ // text, a zero; two zeros end it. The next line is the one after the zero, as BASIC links them
142
+ // again when it loads a program (GOLDFIELDS was saved from elsewhere). Each statement as the
143
+ // tree has Blitz!'s, `to` a line's address; the lines numbered as `numbers` says.
144
+ export function lines(m, from) {
145
+ const out = [], numbers = new Map(), order = [];
146
+ for (let at = from; m[at] | m[at + 1] << 8; at = m.indexOf(0, at + 4) + 1) { numbers.set(m[at + 2] | m[at + 3] << 8, at); order.push(at); }
147
+ // (a line not there, a jump's error when it is taken: UNDEF'D STATEMENT, as BASIC finds it then)
148
+ const line = n => numbers.has(n) ? numbers.get(n) : { missing: n };
149
+ for (const [k, at] of order.entries()) {
150
+ out.push({ label: at, line: m[at + 2] | m[at + 3] << 8 });
151
+ const next = order[k + 1] ?? null, r = reader(m, at + 4);
152
+ statement(r, m, out, line, next);
153
+ }
154
+ return out;
155
+ }
156
+
157
+ // the statements of one line from the reader's place, to its end
158
+ function statement(r, m, out, line, next) {
159
+ while (!r.end() || r.peek() === 0x3a) {
160
+ if (r.peek() === 0x3a) { r.at++; continue; }
161
+ const at = r.at;
162
+ // (a statement that cannot be read is its error when BASIC reaches it, and the rest of
163
+ // the line goes with it)
164
+ try { one(r, m, out, line, next, at); }
165
+ catch (err) {
166
+ if (!(err instanceof BasicError)) throw err;
167
+ out.push({ at, k: 'fail', what: /^\?(.*) ERROR$/.exec(err.message)[1], args: [] });
168
+ while (m[r.at]) r.at++;
169
+ }
170
+ }
171
+ }
172
+ function one(r, m, out, line, next, at) {
173
+ {
174
+ const b = r.next(), word = b >= 0x80 ? TOKENS[b - 0x80] : null;
175
+ const say = x => out.push({ at, ...x });
176
+ const target = () => { const v = expression(m, r.at); r.at = v.end; return v.e; };
177
+ // PRINT's items, each with what follows it: ; nothing, , a zone, else a new line; none at
178
+ // all, a new line
179
+ const printItems = () => {
180
+ if (r.end()) { say({ k: 'newline', args: [] }); return; }
181
+ while (!r.end()) {
182
+ const b2 = r.peek();
183
+ if (b2 === T['TAB('] || b2 === T['SPC(']) { r.at++; const e = target(); r.expect(0x29); say({ k: b2 === T['TAB('] ? 'tab' : 'spc', args: [e] }); }
184
+ else if (b2 === 0x3b) { r.at++; continue; }
185
+ else if (b2 === 0x2c) { r.at++; say({ k: 'print-comma', args: [] }); continue; }
186
+ else say({ k: 'print', args: [target()] });
187
+ if (r.end()) { say({ k: 'newline', args: [] }); break; }
188
+ if (r.peek() === 0x3b) { r.at++; if (r.end()) break; }
189
+ }
190
+ };
191
+ if (!word || word === 'LET') { // an assignment
192
+ if (word) r.peek(); else r.at--;
193
+ const t = primary(r, m); r.expect(T['=']);
194
+ say({ k: 'let', t, e: target() });
195
+ } else if (word === 'IF') {
196
+ const e = target();
197
+ if (r.peek() === T['GOTO']) { r.at++; say({ k: 'if-goto', e, to: line(number(r)) }); return; }
198
+ r.expect(T['THEN']);
199
+ if (digit(r.peek())) { say({ k: 'if-goto', e, to: line(number(r)) }); return; }
200
+ // the rest of the line runs when it holds: on to the next line when not
201
+ say({ k: 'unless', e, to: next ?? -1 });
202
+ } else if (word === 'GOTO' || word === 'GOSUB') say({ k: word.toLowerCase(), to: line(number(r)) });
203
+ else if (word === 'GO') { r.expect(T['TO']); say({ k: 'goto', to: line(number(r)) }); } // GO TO
204
+ // (a token past the statements', $A3 on, TAB( to GO: no statement begins with one)
205
+ else if (b >= 0xa3) throw syntax(`$${at.toString(16)}: ${word} begins no statement`);
206
+ // DATA, its items read by READ (`dataItems`), passed over to its statement's end; REM, the rest
207
+ // of its line; RESTORE
208
+ else if (word === 'DATA') { let q = false; while (m[r.at] && (q || m[r.at] !== 0x3a)) { if (m[r.at] === 0x22) q = !q; r.at++; } }
209
+ else if (word === 'REM') { while (m[r.at]) r.at++; }
210
+ else if (word === 'RESTORE') say({ k: 'restore', args: [] });
211
+ else if (word === 'DIM') { // DIM, each array and its sizes in turn
212
+ for (;;) { const t = primary(r, m); if (t.k !== 'elem') fail('SYNTAX'); say({ k: 'dim', n: t.n, sizes: t.i }); if (r.peek() !== 0x2c) break; r.at++; }
213
+ }
214
+ else if (word === 'DEF') { // DEF FN name(parameter) = its expression
215
+ r.expect(T['FN']); const n = name(r); r.expect(0x28); const p = name(r); r.expect(0x29); r.expect(T['=']);
216
+ say({ k: 'def', n, p, e: target() });
217
+ }
218
+ else if (word === 'INPUT') { // INPUT, a prompt (a string, then ;) if any, and its variables
219
+ let prompt = null;
220
+ if (r.peek() === 0x22) { const p = primary(r, m); prompt = p.s; if (r.next() !== 0x3b) fail('SYNTAX'); }
221
+ const targets = [];
222
+ for (;;) { const t = primary(r, m); if (t.k !== 'var' && t.k !== 'elem') fail('SYNTAX'); targets.push(t); if (r.peek() !== 0x2c) break; r.at++; }
223
+ say({ k: 'input-keys', prompt, targets });
224
+ }
225
+ else if (word === 'READ') { // READ, each variable (or element) in turn
226
+ for (;;) { const t = primary(r, m); if (t.k !== 'var' && t.k !== 'elem') fail('SYNTAX'); say({ k: t.n.endsWith('$') ? 'read$' : 'read', t }); if (r.peek() !== 0x2c) break; r.at++; }
227
+ }
228
+ else if (word === 'ON') { // ON e GOTO or GOSUB, its lines
229
+ const e = target(), go = r.next();
230
+ if (go !== T['GOTO'] && go !== T['GOSUB']) fail('SYNTAX');
231
+ const to = [line(number(r))]; while (r.peek() === 0x2c) { r.at++; to.push(line(number(r))); }
232
+ say({ k: 'on', e, to, gosub: go === T['GOSUB'] });
233
+ }
234
+ else if (word === 'RETURN' || word === 'CLR' || word === 'END' || word === 'STOP' || word === 'NEW' || word === 'CONT') say({ k: word.toLowerCase(), args: [] });
235
+ // RUN, from the start or a line; LIST, all of it or a range (a number, -, a number, either
236
+ // left out), the numbers as LINGET reads them
237
+ else if (word === 'RUN') say(digit(r.peek()) ? { k: 'run', to: line(number(r)), args: [] } : { k: 'run', args: [] });
238
+ else if (word === 'LIST') { // ($A6A4) the first number or 0; after a -, the last or 0; an end of 0 the program's end
239
+ const from = number(r); let to = from;
240
+ if (r.peek() === T['-']) { r.at++; to = number(r); }
241
+ if (!r.end()) fail('SYNTAX');
242
+ say({ k: 'list', args: [{ k: 'num', v: from }, { k: 'num', v: to || 0xffff }] });
243
+ }
244
+ else if (word === 'POKE') { const a = target(); r.expect(0x2c); say({ k: 'poke', a, v: target() }); }
245
+ else if (word === 'SYS') { // its address, and the rest of its text for the machine code
246
+ const e = target(), text = r.at; while (m[r.at] && m[r.at] !== 0x3a) r.at++;
247
+ say({ k: 'sys', e, text, end: r.at, words: '' });
248
+ } else if (word === 'LOAD') {
249
+ const args = [target()]; while (r.peek() === 0x2c) { r.at++; args.push(target()); }
250
+ say({ k: 'load', args });
251
+ } else if (word === 'GET' && r.peek() === 0x23) { // GET#: its file, then each variable, a character each
252
+ r.at++; const n = target(); r.expect(0x2c);
253
+ const targets = [primary(r, m)]; while (r.peek() === 0x2c) { r.at++; targets.push(primary(r, m)); }
254
+ say({ k: 'get#', args: [n], targets });
255
+ } else if (word === 'GET') say({ k: 'get', t: primary(r, m) });
256
+ else if (word === 'INPUT#') { // INPUT#: its file, then its variables, as Blitz!'s INPUT# has them
257
+ const n = target(); r.expect(0x2c);
258
+ const targets = [primary(r, m)]; while (r.peek() === 0x2c) { r.at++; targets.push(primary(r, m)); }
259
+ say({ k: 'input#', args: [n] });
260
+ targets.forEach((t, i) => say({ k: i ? 'input-item' : 'input', t, last: i === targets.length - 1 }));
261
+ }
262
+ else if (word === 'WAIT') { // WAIT place, mask[, flip]
263
+ const args = [target()]; r.expect(0x2c); args.push(target());
264
+ if (r.peek() === 0x2c) { r.at++; args.push(target()); }
265
+ say({ k: 'wait', args });
266
+ }
267
+ else if (word === 'SAVE' || word === 'VERIFY') { // its name, device and secondary address, each if given
268
+ const args = r.end() ? [] : [target()]; while (r.peek() === 0x2c) { r.at++; args.push(target()); }
269
+ say({ k: word.toLowerCase(), args });
270
+ }
271
+ else if (word === 'FOR') {
272
+ const v = name(r); r.expect(T['=']); const from = target(); r.expect(T['TO']);
273
+ const to = target(), step = r.peek() === T['STEP'] ? (r.at++, target()) : undefined;
274
+ say({ k: 'let', t: { k: 'var', n: v }, e: from }); out.push({ at, k: 'for', v, to, ...step ? { step } : {} });
275
+ } else if (word === 'NEXT') { // NEXT, or NEXT and its variables, each in turn: NEXT J,I
276
+ if (!letter(r.peek())) { say({ k: 'next', v: null, args: [] }); return; }
277
+ for (;;) { say({ k: 'next-var', v: name(r), args: [] }); if (r.peek() !== 0x2c) break; r.at++; }
278
+ }
279
+ else if (word === 'PRINT') printItems();
280
+ // CMD: the output to a file, and PRINT's items after a comma; PRINT#, the same, and then the
281
+ // output back to the screen (Microsoft's PRINTN: CMD, then IODONE)
282
+ else if (word === 'CMD' || word === 'PRINT#') {
283
+ say({ k: 'print#', args: [target()] });
284
+ if (r.peek() === 0x2c) r.at++; else if (!r.end()) fail('SYNTAX');
285
+ printItems();
286
+ if (word === 'PRINT#') say({ k: 'clrchn', args: [] });
287
+ }
288
+ else if (word === 'OPEN') { const args = r.end() ? [] : [target()]; while (r.peek() === 0x2c) { r.at++; args.push(target()); } say({ k: 'open', args }); }
289
+ else if (word === 'CLOSE') say({ k: 'close', args: [target()] }); else throw new Error(`$${at.toString(16)}: ${word} is not handled`);
290
+ }
291
+ }
292
+
293
+ const number = r => { let n = 0; while (digit(r.peek())) n = n * 10 + r.next() - 0x30; return n; };
294
+
295
+ // The keywords as BASIC's ROM keeps them (RESLST, $A09E): each one's characters, its last with
296
+ // its top bit set, in token order, then a zero: 256 bytes exactly, which LIST indexes with the
297
+ // Y register, so that past GO it comes round to the start again
298
+ const RESLST = [...TOKENS.flatMap(t => [...t].map((c, i, a) => c.charCodeAt(0) | (i === a.length - 1 ? 0x80 : 0))), 0];
299
+ const PRINT_TOKEN = T['PRINT'], DATA_TOKEN = T['DATA'], REM_TOKEN = T['REM'];
300
+
301
+ // CRUNCH ($A579): a line's text, as typed (PETSCII, after its number), to the bytes BASIC keeps,
302
+ // its keywords as tokens, as the ROM makes them, its quirks too. Outside a string, a space, a
303
+ // digit, : and ; are kept as they are, ? is PRINT, and a shifted character is dropped (pi is
304
+ // kept); else the keywords are tried in their order, a character at a time, the first that
305
+ // matches taken (so INPUT# before INPUT, and TO inside TOTAL), a shifted letter ending one early
306
+ // (P shifted-R is PRINT); no keyword, and the character is kept. A string is kept to its closing
307
+ // quote, the rest of a line after REM all of it, and after DATA all but a string's quotes to the
308
+ // next colon. The bytes end with the line's zero.
309
+ export function crunch(text) {
310
+ const buf = [...text, 0], out = [];
311
+ let x = 0, dores = 0;
312
+ const stuff = a => {
313
+ out.push(a); x++;
314
+ if (a === 0) return 'done';
315
+ if (a === 0x3a) dores = 0; else if (a === DATA_TOKEN) dores = DATA_TOKEN - 0x3a;
316
+ return a === REM_TOKEN ? 'rem' : null;
317
+ };
318
+ // a string's or REM's characters, to the end character (a quote, or the line's zero)
319
+ const copy = end => { for (;;) { const ch = buf[x]; if (ch === 0 || ch === end) return stuff(ch); out.push(ch); x++; } };
320
+ for (;;) {
321
+ let ch = buf[x], r = null;
322
+ if (ch >= 0x80) { if (ch !== 0xff) { x++; continue; } r = stuff(ch); }
323
+ else if (ch === 0x20) r = stuff(ch);
324
+ else if (ch === 0x22) { out.push(ch); x++; r = copy(0x22); }
325
+ else if (dores & 0x40) r = stuff(ch);
326
+ else if (ch === 0x3f) r = stuff(PRINT_TOKEN);
327
+ else if (ch >= 0x30 && ch < 0x3c) r = stuff(ch);
328
+ else {
329
+ // the keywords tried: the next character against the list's, until a difference of $80
330
+ // (the keyword's last, or a shifted letter in the text) ends it, or another moves on
331
+ // (one place in the list, as the ROM's Y: a keyword that does not match is passed from
332
+ // where it stopped matching, which a shifted letter matching a keyword's last may carry
333
+ // into the next)
334
+ let y = 0, count = 0, found = null;
335
+ for (;;) {
336
+ let k = x;
337
+ for (;;) { const d = (buf[k] - RESLST[y]) & 0xff; if (d === 0) { k++; y++; continue; } if (d === 0x80) found = { token: 0x80 | count, end: k }; break; }
338
+ if (found) break;
339
+ count++;
340
+ do y++; while (!(RESLST[y - 1] & 0x80));
341
+ if (RESLST[y] === 0) break;
342
+ }
343
+ if (found) { x = found.end; r = stuff(found.token); }
344
+ else r = stuff(ch);
345
+ }
346
+ if (r === 'done') return out;
347
+ if (r === 'rem' && copy(0) === 'done') return out;
348
+ }
349
+ }
350
+
351
+ // LIST's text of a line ($A71A): its bytes after the number as BASIC prints them, each token
352
+ // its keyword's characters, and inside a string, the bytes as they are; pi as its character.
353
+ // Past GO the keywords come round again, Y being a byte: $CC is the table's last zero, which
354
+ // printed ends the keyword at once (CHROUT leaves Z set), and $CD on are FOR, NEXT and the rest.
355
+ // Even after REM a byte of $80 or more is a keyword's: BASIC's own oddity.
356
+ export function list(bytes) {
357
+ const out = [];
358
+ let quote = false;
359
+ for (const b of bytes) {
360
+ if (b === 0) break;
361
+ if (b < 0x80 || b === 0xff || quote) { if (b === 0x22) quote = !quote; out.push(b); continue; }
362
+ let n = b - 0x7f, y = 0xff;
363
+ while (--n) { do y = (y + 1) & 0xff; while (!(RESLST[y] & 0x80)); }
364
+ for (;;) { y = (y + 1) & 0xff; const c = RESLST[y]; if (c & 0x80) { out.push(c & 0x7f); break; } out.push(c); if (!c) break; }
365
+ }
366
+ return out;
367
+ }
368
+
369
+ // The program kept at `txttab` in `mem`: a line put in by its number, in its place, over one
370
+ // of the same number, or taken out when it is empty, as BASIC's line entry does ($A49C), and
371
+ // every line linked to the next again (LINKPRG, $A533). Gives the program's end, where the
372
+ // variables start (VARTAB).
373
+ export function program(mem, txttab) {
374
+ const lines = [];
375
+ for (let at = txttab; mem[at] | mem[at + 1] << 8; ) {
376
+ const end = mem.indexOf(0, at + 4);
377
+ lines.push({ n: mem[at + 2] | mem[at + 3] << 8, bytes: [...mem.subarray(at + 4, end + 1)] });
378
+ at = end + 1;
379
+ }
380
+ return lines;
381
+ }
382
+ export function store(mem, txttab, n, bytes) {
383
+ const lines = program(mem, txttab).filter(l => l.n !== n);
384
+ if (bytes.length > 1) lines.push({ n, bytes });
385
+ lines.sort((a, b) => a.n - b.n);
386
+ let at = txttab;
387
+ for (const l of lines) {
388
+ const next = at + 4 + l.bytes.length;
389
+ mem.set([next & 0xff, next >> 8, l.n & 0xff, l.n >> 8, ...l.bytes], at);
390
+ at = next;
391
+ }
392
+ mem[at] = 0; mem[at + 1] = 0;
393
+ return at + 2;
394
+ }
package/basic-list.js ADDED
@@ -0,0 +1,21 @@
1
+ // A Commodore BASIC V2 program as text, a line to a line: each line as the 64's own LIST prints it (basic-lang.js
2
+ // list, from its ROM at $A71A, its oddities kept), then as moonbase's listings write PETSCII (petscii.js text: the
3
+ // control characters by their names in braces). A program is lines, each the address of the next line, the line
4
+ // number, then the line, ending in a zero; two zeros end it. `lower`: the letters as the second character set
5
+ // shows them.
6
+ //
7
+ // listing(bytes, { lower }) the program's text (its bytes after the load address)
8
+ import { list } from './basic-lang.js';
9
+ import { text } from './petscii.js';
10
+
11
+ export function listing(bytes, { lower = false } = {}) {
12
+ const lines = [];
13
+ for (let at = 0; at + 4 <= bytes.length && (bytes[at] | bytes[at + 1] << 8); ) {
14
+ const number = bytes[at + 2] | bytes[at + 3] << 8;
15
+ let end = at + 4;
16
+ while (end < bytes.length && bytes[end]) end++;
17
+ lines.push(number + ' ' + text(list(bytes.subarray(at + 4, end)), { lower }));
18
+ at = end + 1;
19
+ }
20
+ return lines.join('\n') + '\n';
21
+ }