@moonbase-machines/c64 0.0.0-stage → 0.5.0
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 +21 -0
- package/README.md +11 -2
- package/basic-compile.js +171 -0
- package/basic-lang.js +394 -0
- package/basic-list.js +21 -0
- package/basic-rom.js +572 -0
- package/basic.js +275 -0
- package/browser.js +116 -0
- package/d64.js +191 -0
- package/dist/moonbase-c64.js +9 -0
- package/dist/moonbase-c64.js.map +7 -0
- package/drive.js +209 -0
- package/editor.js +267 -0
- package/float.js +584 -0
- package/glyph-hash.js +8 -0
- package/glyph-hashes.js +70 -0
- package/index.js +27 -0
- package/kernal.js +319 -0
- package/keys.js +21 -0
- package/machine.js +209 -0
- package/numbers.js +64 -0
- package/package.json +14 -4
- package/paint.js +23 -0
- package/petscii.js +54 -0
- package/printer.js +95 -0
- package/rom.js +537 -0
- package/saves.js +10 -0
- package/sid.js +49 -0
- package/start.js +33 -0
- package/test/disks.test.js +35 -0
- package/vic.js +122 -0
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
|
-
#
|
|
1
|
+
# @moonbase-machines/c64
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/basic-compile.js
ADDED
|
@@ -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
|
+
}
|