@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/basic-rom.js ADDED
@@ -0,0 +1,572 @@
1
+ // BASIC's ROM, moonbase's own: the routines of Commodore BASIC that a program's machine code calls,
2
+ // answered at their addresses, as the ROM's would leave the registers and memory. Shared by every
3
+ // layer that runs such code: the original on moonbase's ROMs (rom.js, on the 6510) and a remake
4
+ // (on its stand-in for the processor), as kernal.js is for the KERNAL. Its rules for numbers are
5
+ // numbers.js's, its arithmetic float.js's.
6
+ //
7
+ // install({ cpu, read, write, evaluate, chrout })
8
+ // cpu a processor, or a stand-in for one: its registers, and `hooks`, which this fills
9
+ // read, write memory as the processor sees it
10
+ // evaluate the expression in BASIC's text at an address: { value, end, place }, its value (a
11
+ // number or a string), where it ends, and for a string written in the text, where
12
+ // its characters are. The remake takes its compiled parameters' (compiled with
13
+ // the program, basic-compile.js); the original reads the text and BASIC's
14
+ // variables in memory.
15
+ // chrout CHROUT, a character in A to the output (the KERNAL's $FFD2 unless given)
16
+ //
17
+ // A routine that returns from its caller too (`'return'`: $E206, at a statement's end) leaves
18
+ // that to the layer running it.
19
+ import { BasicError, ERRORS } from './numbers.js';
20
+ import { setFac, Floats, FloatError, unpack } from './float.js';
21
+ import { Basic } from './basic.js';
22
+ import { expression } from './basic-lang.js';
23
+ import { evaluator } from './basic-compile.js';
24
+ import { seedRandom, nextRandom } from './numbers.js';
25
+
26
+ const hex4 = n => n.toString(16).toUpperCase().padStart(4, '0');
27
+ const FAC_EXP = 0x61;
28
+ // BASIC's functions' routines, SGN to MID$, as its table at $A052 holds them
29
+ const FUNCTIONS = [0xbc39, 0xbccc, 0xbc58, 0x0310, 0xb37d, 0xb39e, 0xbf71, 0xe097, 0xb9ea, 0xbfed, 0xe264, 0xe26b, 0xe2b4, 0xe30e,
30
+ 0xb80d, 0xb77c, 0xb465, 0xb7ad, 0xb78b, 0xb6ec, 0xb700, 0xb72c, 0xb737];
31
+
32
+ export function install({ cpu, read, write, evaluate, chrout = c => cpu.hooks.get(0xffd2)(c) }) {
33
+ // BASIC's text pointer ($7A), and what it reads there: CHRGOT ($0079) and CHRGET ($0073) as
34
+ // BASIC's text pointer reads, spaces passed over, Z at a statement's end, C clear for a digit
35
+ const txt = () => read(0x7a) | read(0x7b) << 8, setTxt = a => { write(0x7a, a & 0xff); write(0x7b, a >> 8 & 0xff); };
36
+ const syntax = () => { throw new BasicError('?SYNTAX ERROR'); };
37
+ const chrgot = c => { let a = txt(); while (read(a) === 0x20) a++; setTxt(a); const v = read(a); c.a = v; c.z = v === 0 || v === 0x3a ? 1 : 0; c.c = v >= 0x30 && v <= 0x39 ? 0 : 1; c.n = v >> 7; };
38
+ const chkcom = c => { chrgot(c); if (c.a !== 0x2c) syntax(); setTxt(txt() + 1); chrgot(c); };
39
+ // the expression at the text pointer: a number into the accumulator ($0D 0, numeric), a string
40
+ // as BASIC's descriptor at $19 ($0D $FF, string)
41
+ let last = 0;
42
+ const frm = (c, number) => {
43
+ chrgot(c);
44
+ const at = txt(), { value, end, place } = evaluate(at);
45
+ if (typeof value === 'string') {
46
+ if (number) throw new BasicError('?TYPE MISMATCH ERROR');
47
+ if (place === undefined) throw new Error('a string parameter not in the text, at $' + hex4(at) + ': not handled');
48
+ write(0x19, value.length); write(0x1a, place & 0xff); write(0x1b, place >> 8); write(0x64, 0x19); write(0x65, 0); write(0x0d, 0xff);
49
+ } else { last = value; write(0x0d, 0); setFac({ read, write }, value); }
50
+ setTxt(end); chrgot(c);
51
+ };
52
+ const whole = (lo, hi) => { if (last !== Math.floor(last) || last < lo || last > hi) throw new BasicError('?ILLEGAL QUANTITY ERROR'); return last; };
53
+ const getbyt = c => { frm(c, true); c.x = whole(0, 255); };
54
+ // (A left as the last character out, as the ROM's leave it)
55
+ const out = ch => { cpu.a = ch; chrout(cpu); };
56
+ const H = {
57
+ 0x79: chrgot, 0x73: c => { setTxt(txt() + 1); chrgot(c); },
58
+ // CHKCOM ($AEFD), FRMNUM ($AD8A), FRMEVL ($AD9E), GETADR ($B7F7), GETBYT ($B79E); FRESTR's below ($B6A3)
59
+ 0xaefd: chkcom, 0xad8a: c => frm(c, true), 0xad9e: c => frm(c, false),
60
+ 0xb7f7: c => { const x = whole(0, 65535); write(0x14, x & 0xff); write(0x15, x >> 8); c.y = x & 0xff; c.a = x >> 8; },
61
+ 0xb79e: getbyt,
62
+ // the LOAD and OPEN parameters': $E200 a comma and a byte, $E206 the statement's end, which
63
+ // returns from its caller, $E20E a comma and more
64
+ 0xe200: c => { chkcom(c); getbyt(c); },
65
+ 0xe206: c => { chrgot(c); if (c.z) return 'return'; },
66
+ 0xe20e: c => { chkcom(c); if (c.z) syntax(); },
67
+ // STROUT ($AB1E), the text at A/Y to its 0; the screen cleared ($E544) and a line of it ($E9FF)
68
+ 0xab1e: c => { for (let a = c.a | c.y << 8, k = 0; read(a + k) && k < 256; k++) out(read(a + k)); },
69
+ 0xe544: () => out(0x93),
70
+ 0xe9ff: c => { const at = (read(0x288) << 8) + c.x * 40; for (let k = 0; k < 40; k++) { write(at + k, 0x20); write(0xd800 + c.x * 40 + k, read(0x286)); } },
71
+ };
72
+ // ---- the routines Blitz!'s runtime calls (Goldfields' GAME), on the original only ----
73
+
74
+ // ($B194) where an array's elements begin, its header ($5F) and two bytes for each of its
75
+ // dimensions ($0B) past it: $58/$59, and A/Y
76
+ H[0xb194] = c => {
77
+ let t = read(0x0b) << 1, carry = t >> 8; t &= 0xff;
78
+ t += 5 + carry; carry = t >> 8; t &= 0xff;
79
+ t += read(0x5f) + carry; carry = t >> 8; t &= 0xff;
80
+ let y = read(0x60); if (carry) y = (y + 1) & 0xff;
81
+ write(0x58, t); write(0x59, y); c.a = t; c.y = y; c.c = carry;
82
+ const flag = carry ? y : t; c.z = flag ? 0 : 1; c.n = flag >> 7;
83
+ };
84
+ // REASON ($A408): whether the address in A/Y is below the strings' bottom (FRETOP, $33), the
85
+ // flags as the comparison leaves them (C clear); if not, the strings collected (GARBAG, $B526)
86
+ // and the comparison again, its registers kept across it; and if still not, OUT OF MEMORY
87
+ const below = (a, y) => { const top = read(0x34); if (y !== top) return y < top; return a < read(0x33); };
88
+ const compare = (c, a, y) => { const top = read(0x34), v = y !== top ? y - top : a - read(0x33); c.c = v >= 0 ? 1 : 0; c.z = (v & 0xff) ? 0 : 1; c.n = v >> 7 & 1; };
89
+ H[0xa408] = c => {
90
+ const { a, y } = c;
91
+ if (below(a, y)) return compare(c, a, y);
92
+ const kept = []; for (let k = 0x57; k <= 0x60; k++) kept.push(read(k));
93
+ H[0xb526](c);
94
+ kept.forEach((v, k) => write(0x57 + k, v));
95
+ Object.assign(c, { a, y });
96
+ if (below(a, y)) return compare(c, a, y);
97
+ throw new BasicError('?OUT OF MEMORY ERROR');
98
+ };
99
+ // UMULT ($B34C): an array's index ($71/$72) times its dimension's size, read from its header at
100
+ // ($5F)+Y (the low byte) and Y-1 (the high), into X/Y (A the high byte too); past 16 bits is OUT
101
+ // OF MEMORY. $71/$72 are shifted out to 0, $5D counted down to 0, as the ROM leaves them.
102
+ H[0xb34c] = c => {
103
+ write(0x22, c.y);
104
+ const base = read(0x5f) | read(0x60) << 8, size = read(base + c.y) | read(base + ((c.y - 1) & 0xff)) << 8;
105
+ write(0x28, size & 0xff); write(0x29, size >> 8);
106
+ const index = read(0x71) | read(0x72) << 8, product = index * size;
107
+ // (the ROM's own test: a shift or a sum carrying out of 16 bits on the way, which for a
108
+ // product past 16 bits it always meets)
109
+ if (product > 0xffff) throw new BasicError('?OUT OF MEMORY ERROR');
110
+ write(0x71, 0); write(0x72, 0); write(0x5d, 0);
111
+ Object.assign(c, { x: product & 0xff, y: product >> 8, a: product >> 8, c: 0, z: 1, n: 0 });
112
+ };
113
+ // ($B2AA) DIM's end: the array's elements, A/X bytes of them (high, low) from $58/$59, past the
114
+ // end of memory OUT OF MEMORY; then REASON for its end, the arrays' end (STREND, $31) there,
115
+ // its elements cleared to 0 (a page at a time down from the last, as the ROM does), and its
116
+ // length into its header, at ($5F)+2. DIM's flag ($0C) set, back to DIM; an array made by its
117
+ // first use, which goes on to find its element, is not handled (an error).
118
+ H[0xb2aa] = c => {
119
+ const start = read(0x58) | read(0x59) << 8;
120
+ let hi = c.a + read(0x59) + c.c;
121
+ if (hi > 255) throw new BasicError('?OUT OF MEMORY ERROR');
122
+ write(0x59, hi);
123
+ let lo = c.x + read(0x58);
124
+ if (lo > 255) { lo &= 0xff; hi = (hi + 1) & 0xff; if (!hi) throw new BasicError('?OUT OF MEMORY ERROR'); }
125
+ const kept = { a: c.a, x: c.x };
126
+ Object.assign(c, { a: lo, y: hi }); H[0xa408](c);
127
+ write(0x31, lo); write(0x32, hi);
128
+ const end = lo | hi << 8;
129
+ for (let a = start; a < end; a++) write(a, 0);
130
+ write(0x59, start >> 8); write(0x72, 0);
131
+ const base = read(0x5f) | read(0x60) << 8, length = end - base;
132
+ write(base + 2, length & 0xff); write(base + 3, length >> 8);
133
+ if (!read(0x0c)) throw new Error('an array made by its first use ($B2AA, DIM\'s flag clear): not handled');
134
+ const f = read(0x0c);
135
+ Object.assign(c, { a: f, x: kept.x, y: 3, c: 1, z: f ? 0 : 1, n: f >> 7 });
136
+ };
137
+ // SYS's own ($E130): a return pushed to $E147, the registers from $030C to $030F (A, X, Y, the
138
+ // flags), and on to the address at $14/$15 (GETADR's), where the machine code runs; and $E147, as
139
+ // it comes back (or as a routine ends that jumps there), the registers kept at $030C again
140
+ H[0xe130] = c => {
141
+ c.push(0xe1); c.push(0x46);
142
+ Object.assign(c, { a: read(0x30c), x: read(0x30d), y: read(0x30e) }); c.p = read(0x30f);
143
+ c.pc = read(0x14) | read(0x15) << 8;
144
+ return false;
145
+ };
146
+ H[0xe147] = c => { write(0x30c, c.a); write(0x30d, c.x); write(0x30e, c.y); write(0x30f, c.p); };
147
+ // The accumulator onto the processor's stack, for the evaluator ($AE38): the caller's return
148
+ // taken into $22/$23, its low byte one on (only the low byte: the ROM's INC carries no further),
149
+ // A pushed (the sign), then ($AE43) the accumulator rounded and its five bytes pushed, the
150
+ // exponent last, and on to the caller at ($22)
151
+ const f = new Floats({ read, write });
152
+ // (a routine called from the ROM's code at `from`, its JSR's return address on the stack for the
153
+ // while, as an observer sees the ROM's own calls)
154
+ const call = (c, at, from) => { const r = from + 2; c.push(r >> 8); c.push(r & 0xff); try { return cpu.hooks.get(at)(c); } finally { c.pull(); c.pull(); } };
155
+ // (the floating point's errors, an overflow or a division by zero, as BASIC's)
156
+ const arith = g => { try { return g(); } catch (err) { if (err instanceof FloatError) throw new BasicError('?' + ERRORS[err.x - 1] + ' ERROR'); throw err; } };
157
+ H[0xae38] = c => {
158
+ const y = c.a, lo = c.pull(), hi = c.pull();
159
+ write(0x22, (lo + 1) & 0xff); write(0x23, hi); c.y = y; c.push(y);
160
+ return H[0xae43](c);
161
+ };
162
+ H[0xae43] = c => {
163
+ f.round();
164
+ for (const at of [0x65, 0x64, 0x63, 0x62, 0x61]) c.push(read(at));
165
+ c.a = read(0x61); c.z = c.a ? 0 : 1; c.n = c.a >> 7;
166
+ c.pc = read(0x22) | read(0x23) << 8;
167
+ return false;
168
+ };
169
+ // The floating point's entries (float.js, each held to the ROM's): the address of a
170
+ // number in memory in A/Y (X/Y for a store, FORPNT $49 for BBD0's, $5C and $57 for BBC7's and
171
+ // BBCA's); A the result's exponent, or the comparison's or the sign's answer, its flags N and Z.
172
+ // An overflow or a division by zero is BASIC's error.
173
+ const ay = c => c.a | c.y << 8, nz = (c, v) => { c.z = v & 0xff ? 0 : 1; c.n = v >> 7 & 1; };
174
+ const floating = (run, regs = () => ({})) => c => {
175
+ const r = arith(() => run(c));
176
+ Object.assign(c, regs(c, r) ?? {});
177
+ nz(c, c.a);
178
+ };
179
+ const A = (c, r) => ({ a: r?.a ?? read(0x61) });
180
+ Object.assign(H, {
181
+ 0xbba2: floating(c => f.movfm(ay(c)), (c, r) => ({ a: r.a, y: 0 })),
182
+ 0xbbd4: floating(c => f.movmf(c.x | c.y << 8), (c, r) => ({ a: r.a, y: 0 })),
183
+ 0xbbd0: floating(c => { c.x = read(0x49); c.y = read(0x4a); return f.movmf(c.x | c.y << 8); }, (c, r) => ({ a: r.a, y: 0 })),
184
+ 0xbbc7: floating(c => { c.x = 0x5c; return f.movmf(0x5c); }, (c, r) => ({ a: r.a, y: 0 })),
185
+ 0xbbca: floating(c => { c.x = 0x57; return f.movmf(0x57); }, (c, r) => ({ a: r.a, y: 0 })),
186
+ 0xbbfc: floating(() => f.movfa(), () => ({ a: read(0x69), x: 0 })),
187
+ 0xbc0c: floating(() => f.movaf(), () => ({ a: read(0x61), x: 0 })),
188
+ 0xbc0f: floating(() => f.movef(), () => ({ a: read(0x61), x: 0 })),
189
+ 0xbc1b: floating(() => f.round(), () => ({ a: read(0x61) })),
190
+ 0xbc2b: floating(() => ({ a: f.sign() }), (c, r) => ({ a: r.a, c: read(0x66) >> 7 })),
191
+ 0xbc5b: floating(c => f.compare(ay(c)), (c, r) => r),
192
+ 0xbc5d: floating(c => f.compare(read(0x24) | c.y << 8), (c, r) => r),
193
+ 0xb867: floating(c => f.fadd(ay(c)), A), 0xb86a: floating(() => f.faddt(), A),
194
+ 0xb850: floating(c => f.fsub(ay(c)), A), 0xb853: floating(() => f.fsubt(), A),
195
+ 0xba28: floating(c => f.fmult(ay(c)), A), 0xba2b: floating(() => f.fmultt(), A),
196
+ 0xbb0f: floating(c => f.fdiv(ay(c)), A), 0xbb12: floating(() => f.fdivt(), A),
197
+ 0xbccc: floating(() => f.int(), A), 0xbc9b: floating(() => f.qint(), () => ({ y: 0 })),
198
+ // ($BCDB) a 32-bit whole number in $62 to $65 as a number, negative when A's top bit is set,
199
+ // as INT enters it and Blitz! does
200
+ 0xbcdb: floating(c => f.float32(c.a), A),
201
+ // the functions on the series: LOG, EXP, the power (entered past its test of the FAC, at $BF7B,
202
+ // its Z flag that test's), SQR (a half's power), SIN and COS
203
+ 0xb9ea: floating(() => f.log(), A), 0xbfed: floating(c => f.exp(c.c), A),
204
+ 0xbf7b: floating(c => f.fpwrt(c.z, c.c), A), 0xbf71: floating(c => { f.movaf(); f.movfm(0xbf11); return f.fpwrt(0, c.c); }, A),
205
+ 0xe26b: floating(() => f.sin(), A), 0xe264: floating(() => f.cos(), A),
206
+ });
207
+ // RND ($E097): the next of the seed's numbers (numbers.js, the remake's own) into the
208
+ // accumulator, whatever RND's argument; A its exponent, so that the original and the remake draw
209
+ // the same numbers.
210
+ let randomState = null;
211
+ H[0xe097] = c => {
212
+ randomState = nextRandom(randomState ?? seedRandom(0x11));
213
+ setFac({ read, write }, randomState / 2 ** 32);
214
+ c.a = read(0x61); nz(c, c.a);
215
+ };
216
+ // ($BF04) FOUT's end, which Blitz! calls to end a number's text: A at $00FF+Y, a 0 after it, and
217
+ // the text's address, $0100, in A/Y
218
+ H[0xbf04] = c => { write(0xff + c.y, c.a); write(0x100 + c.y, 0); Object.assign(c, { a: 0, y: 1, z: 0, n: 0 }); };
219
+ // FIN ($BCF3): the number in BASIC's text at its pointer, read with CHRGET, into the FAC, A its
220
+ // first character and C clear for a digit, as CHRGOT leaves them. As the ROM builds it (each
221
+ // step float.js's): $5D to $67 cleared, the FAC with them; a sign; each digit the FAC ten
222
+ // times and the digit added, those after the point counted ($5D); a second point the end; an E,
223
+ // its sign ($60) and its digits ($5E), capped at 100 when negative and an overflow past 9 when
224
+ // not; then ten times or a tenth for each power left, and the sign last.
225
+ H[0xbcf3] = c => {
226
+ for (let x = 0x0a; x >= 0; x--) write(0x5d + x, 0);
227
+ const next = () => H[0x73](c), digit = () => !c.c;
228
+ if (!digit()) {
229
+ if (c.a === 0x2d) { write(0x67, 0xff); next(); }
230
+ else if (c.a === 0x2b) next();
231
+ }
232
+ let power;
233
+ for (;;) {
234
+ if (digit()) {
235
+ const ch = c.a;
236
+ if (read(0x5f) & 0x80) write(0x5d, read(0x5d) + 1);
237
+ arith(() => { f.mul10(); f.addDigit(ch - 0x30); });
238
+ next(); continue;
239
+ }
240
+ if (c.a === 0x2e) {
241
+ write(0x5f, (read(0x5f) >> 1) | 0x80);
242
+ if (read(0x5f) & 0x40) { power = read(0x5e); break; }
243
+ next(); continue;
244
+ }
245
+ if (c.a === 0x45) {
246
+ next();
247
+ if (c.c && (c.a === 0xab || c.a === 0x2d)) { write(0x60, (read(0x60) >> 1) | 0x80); next(); }
248
+ else if (c.c && (c.a === 0xaa || c.a === 0x2b)) next();
249
+ while (digit()) {
250
+ const e = read(0x5e);
251
+ if (e >= 10) { if (!(read(0x60) & 0x80)) throw new BasicError('?OVERFLOW ERROR'); write(0x5e, 100); }
252
+ else write(0x5e, e * 10 + c.a - 0x30);
253
+ next();
254
+ }
255
+ power = read(0x60) & 0x80 ? (0 - read(0x5e)) & 0xff : read(0x5e);
256
+ break;
257
+ }
258
+ power = read(0x5e); break;
259
+ }
260
+ let n = (power - read(0x5d)) & 0xff; write(0x5e, n);
261
+ if (n && n < 0x80) for (; n; n = (n - 1) & 0xff) { arith(() => f.mul10()); write(0x5e, n - 1); }
262
+ else if (n) for (; n; n = (n + 1) & 0xff) { arith(() => f.div10()); write(0x5e, (n + 1) & 0xff); }
263
+ if (read(0x67) & 0x80) f.negop();
264
+ c.a = read(read(0x67) & 0x80 ? 0x66 : 0x67); nz(c, c.a);
265
+ };
266
+ // TI ($AF7B): the clock read (RDTIM, $FFDE: A, X, Y its low, middle and high bytes) as a 24-bit
267
+ // whole number in the FAC ($62 0, $63 to $65 the clock), exponent $A0, its low bits and sign 0,
268
+ // then normalised; as the ROM goes on through $B8D2, a carry clear on entry has it negated first
269
+ // ($AF84) the clock (RDTIM) into the FAC's mantissa, $63 to $65, $62 0; Y 0
270
+ const clock = c => { call(c, 0xffde, 0xaf84); write(0x64, c.x); write(0x63, c.y); write(0x65, c.a); write(0x62, 0); c.y = 0; };
271
+ H[0xaf7b] = c => {
272
+ const carry = c.c;
273
+ clock(c);
274
+ write(FAC_EXP, 0xa0); write(0x70, 0); write(0x66, 0);
275
+ if (!carry) f.negate();
276
+ const r = f.normal(); Object.assign(c, { a: r.a, y: 0 }); nz(c, c.a);
277
+ };
278
+ // WAIT's loop ($B83C, X the bits to flip, $49 the mask, $14 the address): at $B840, the address
279
+ // read until its bits, flipped, meet the mask; the machine goes on meanwhile, as the ROM's loop
280
+ // does
281
+ H[0xb83c] = c => { write(0x4a, c.x); c.y = 0; c.pc = 0xb840; return false; };
282
+ H[0xb840] = c => {
283
+ const v = (read((read(0x14) | read(0x15) << 8) + c.y) ^ read(0x4a)) & read(0x49);
284
+ if (!v) return 'wait';
285
+ c.a = v; nz(c, v);
286
+ };
287
+ // FREFAC ($B6A6): the string whose descriptor is at $64/$65, its length in A, its characters'
288
+ // address in $22/$23 (X/Y); its descriptor let go if it is the last temporary one (FRETMS,
289
+ // $B6DB: $16 and $17 three back), and its characters too if they are the strings' bottom
290
+ // (FRETOP, $33, moved up past them)
291
+ H[0xb6a6] = c => {
292
+ const at = read(0x64) | read(0x65) << 8;
293
+ write(0x22, at & 0xff); write(0x23, at >> 8);
294
+ const freed = H[0xb6db]({ a: at & 0xff, y: at >> 8 });
295
+ const len = read(at), x = read(at + 1), y = read(at + 2);
296
+ if (freed && y === read(0x34) && x === read(0x33)) { const top = read(0x33) + len; write(0x33, top & 0xff); if (top > 255) write(0x34, read(0x34) + 1); }
297
+ write(0x22, x); write(0x23, y);
298
+ Object.assign(c, { a: len, x, y }); nz(c, len);
299
+ };
300
+ H[0xb6db] = c => {
301
+ if (c.y !== read(0x18) || c.a !== read(0x17)) { c.z = 0; return false; }
302
+ write(0x16, c.a); write(0x17, (c.a - 3) & 0xff); c.y = 0; c.z = 1;
303
+ return true;
304
+ };
305
+ // ($AFD5) a function's call, the evaluator's: A its token times two, its routine's address from
306
+ // BASIC's table ($A052) into JMPER's ($55/$56) and called there; then CHKNUM ($AD8D), its
307
+ // result a number or TYPE MISMATCH
308
+ H[0xafd5] = c => {
309
+ const to = FUNCTIONS[(c.a - 0x68) >> 1];
310
+ if (to === undefined || c.a & 1) throw new Error('a function at $' + hex4(0x9fea + c.a) + ' ($AFD5): not handled');
311
+ write(0x55, to & 0xff); write(0x56, to >> 8);
312
+ c.y = c.a; c.push(0xaf); c.push(0xe2); c.pc = to;
313
+ return false;
314
+ };
315
+ H[0xafe3] = c => { const t = read(0x0d); if (t & 0x80) throw new BasicError('?TYPE MISMATCH ERROR'); c.n = 1 & t >> 7; c.v = 1 & t >> 6; c.z = c.a & t ? 0 : 1; };
316
+ // VAL's own ($B7B0), past LEN's start ($B782: A the string's length, $22/$23 its characters):
317
+ // none, the FAC 0 ($B8F7); else the text pointer kept at $71/$72 and moved to the string, a 0
318
+ // put past its end ($24/$25) for the while, and FIN; then the byte and the pointer back
319
+ H[0xb7b0] = c => {
320
+ if (!c.a) { write(FAC_EXP, 0); write(0x66, 0); c.a = 0; c.z = 1; c.n = 0; return; }
321
+ const from = read(0x22) | read(0x23) << 8, end = from + c.a;
322
+ write(0x71, read(0x7a)); write(0x72, read(0x7b));
323
+ setTxt(from); write(0x24, end & 0xff); write(0x25, end >> 8 & 0xff);
324
+ const kept = read(end); write(end, 0);
325
+ H[0x79](c); H[0xbcf3](c);
326
+ write(end, kept);
327
+ Object.assign(c, { a: kept, x: read(0x71), y: read(0x72) }); nz(c, c.y);
328
+ setTxt(c.x | c.y << 8);
329
+ };
330
+ // TI$ set ($A9E0, the string evaluated): FREFAC, six characters or ILLEGAL QUANTITY; the FAC 0,
331
+ // then for each pair a character added ($AA1D), ten times, the next added and six times (twice
332
+ // over, then $BAED, unless 0): the time in jiffies over ten, ten times again, QINT, and the
333
+ // clock set (SETTIM, $FFDB) from $65, $64 and $63. A character is a digit for the ROM if it is
334
+ // "0" or more, CHRGOT entered past its colon test ($0080); a space goes on to CHRGET itself.
335
+ H[0xa9e0] = c => {
336
+ H[0xb6a6](c);
337
+ if (c.a !== 6) throw new BasicError('?ILLEGAL QUANTITY ERROR');
338
+ write(FAC_EXP, 0); write(0x66, 0);
339
+ const character = y => {
340
+ c.a = read((read(0x22) | read(0x23) << 8) + y);
341
+ if (c.a === 0x20) H[0x73](c); else c.c = c.a >= 0x30 ? 0 : 1;
342
+ if (c.c) throw new BasicError('?ILLEGAL QUANTITY ERROR');
343
+ arith(() => f.addDigit((c.a - 0x30) & 0xff));
344
+ };
345
+ for (let y = 0; y < 6; y += 2) {
346
+ write(0x71, y); character(y); arith(() => f.mul10());
347
+ write(0x71, y + 1); character(y + 1);
348
+ const { a } = f.movaf(); if (a) arith(() => f.addDoubled(a + 1));
349
+ }
350
+ arith(() => f.mul10()); f.qint();
351
+ Object.assign(c, { x: read(0x64), y: read(0x63), a: read(0x65) });
352
+ return cpu.hooks.get(0xffdb)(c);
353
+ };
354
+ // FOUT's digits ($BE68, Y the table's place, $BF16 on): the 32-bit whole number in $62 to $65
355
+ // counted down (or up) by each power until it passes 0, the count a digit, to $00FF on at $71+1
356
+ // (a point after the $5D'th); to Y $24, or $3C (TI$'s). Then ($BEC4) the zeros at the end let go,
357
+ // and a point there, an exponent from $5E, and a 0 after
358
+ const digits = c => {
359
+ let x = 0x80, y = c.y, cy = 0;
360
+ for (;;) {
361
+ do {
362
+ let sum = 0; cy = 0;
363
+ for (let k = 3; k >= 0; k--) { sum = read(0x62 + k) + read(0xbf16 + y + k) + cy; write(0x62 + k, sum); cy = sum >> 8; }
364
+ x = (x + 1) & 0xff;
365
+ } while (cy ? x & 0x80 : !(x & 0x80));
366
+ let a = x;
367
+ if (cy) { const t = (a ^ 0xff) + 0x0a + 1; a = t & 0xff; cy = t >> 8; }
368
+ a = (a + 0x2f + cy) & 0xff;
369
+ y = (y + 4) & 0xff; write(0x47, y);
370
+ let at = (read(0x71) + 1) & 0xff;
371
+ write(0xff + at, a & 0x7f);
372
+ write(0x5d, read(0x5d) - 1);
373
+ if (!read(0x5d)) { at = (at + 1) & 0xff; write(0xff + at, 0x2e); }
374
+ write(0x71, at);
375
+ x = (a ^ 0xff) & 0x80;
376
+ if (y === 0x24 || y === 0x3c) break;
377
+ }
378
+ let at = read(0x71), ch;
379
+ do { ch = read(0xff + at); at = (at - 1) & 0xff; } while (ch === 0x30);
380
+ if (ch !== 0x2e) at = (at + 1) & 0xff;
381
+ let e = read(0x5e);
382
+ if (e) {
383
+ let sign = 0x2b; if (e & 0x80) { e = (0 - e) & 0xff; sign = 0x2d; }
384
+ write(0x101 + at, sign); write(0x100 + at, 0x45);
385
+ write(0x103 + at, 0x30 + e % 10); write(0x102 + at, 0x30 + Math.floor(e / 10)); write(0x104 + at, 0);
386
+ } else write(0x100 + at, 0);
387
+ Object.assign(c, { a: 0, y: 1, z: 0, n: 0 });
388
+ };
389
+ H[0xbe68] = digits;
390
+ // GETSPA ($B4F4): A bytes of the strings' space, down from FRETOP ($33), FRESPC ($35) too, X/Y
391
+ // their address; below STREND ($31), the strings collected first ($0F the flag), then OUT OF
392
+ // MEMORY
393
+ H[0xb4f4] = c => {
394
+ const len = c.a;
395
+ write(0x0f, read(0x0f) >> 1);
396
+ for (;;) {
397
+ const at = r16(0x33) - len;
398
+ if (at >= r16(0x31) && at >= 0) { w16(0x33, at); w16(0x35, at); Object.assign(c, { a: len, x: at & 0xff, y: at >> 8 }); return; }
399
+ if (read(0x0f) & 0x80) throw new BasicError('?OUT OF MEMORY ERROR');
400
+ H[0xb526](c); write(0x0f, 0x80);
401
+ }
402
+ };
403
+ // STRINI ($B475: $50/$51 the descriptor's at $64/$65 kept; $B47D): a string of A bytes made
404
+ // room for, its descriptor in the FAC's $61 to $63
405
+ H[0xb47d] = c => { H[0xb4f4](c); write(0x62, c.x); write(0x63, c.y); write(0x61, c.a); };
406
+ H[0xb475] = c => { write(0x50, read(0x64)); write(0x51, read(0x65)); return H[0xb47d](c); };
407
+ // MOVSTR ($B688): A bytes from X/Y to FRESPC ($35), and FRESPC on past them
408
+ H[0xb688] = c => {
409
+ write(0x22, c.x); write(0x23, c.y);
410
+ const from = c.x | c.y << 8, to = r16(0x35), len = c.a;
411
+ for (let k = len - 1; k >= 0; k--) write(to + k, read(from + k));
412
+ w16(0x35, to + len);
413
+ };
414
+ // PUTNEW ($B4CA): the FAC's descriptor ($61 to $63) pushed on the temporary ones (TEMPPT, $16;
415
+ // full at $22, FORMULA TOO COMPLEX), its address at $64/$65, LASTPT ($17) it, $0D a string
416
+ H[0xb4ca] = c => {
417
+ const x = read(0x16);
418
+ if (x === 0x22) throw new BasicError('?FORMULA TOO COMPLEX ERROR');
419
+ write(x, read(0x61)); write(x + 1, read(0x62)); write(x + 2, read(0x63));
420
+ write(0x64, x); write(0x65, 0); write(0x70, 0); write(0x0d, 0xff); write(0x17, x); write(0x16, x + 3);
421
+ Object.assign(c, { x: x + 3, y: 0xff });
422
+ };
423
+ // STRLIT ($B487, the text at A/Y; $B46F at $00FF, FOUT's): to a 0 or a quote; copied into the
424
+ // strings' space if it is in the zero page or the input buffer (page 0 or 2), else left where it
425
+ // is; and PUTNEW
426
+ H[0xb487] = c => {
427
+ write(0x07, 0x22); write(0x08, 0x22);
428
+ const from = c.a | c.y << 8;
429
+ write(0x6f, c.a); write(0x70, c.y); write(0x62, c.a); write(0x63, c.y);
430
+ let y = 0, quote = 0;
431
+ for (; ; y++) { const ch = read(from + y); if (!ch) break; if (ch === 0x22) { quote = 1; break; } }
432
+ write(0x61, y);
433
+ const end = from + y + quote; write(0x71, end & 0xff); write(0x72, end >> 8 & 0xff);
434
+ if (c.y === 0 || c.y === 2) { c.a = y; H[0xb475](c); Object.assign(c, { a: y, x: from & 0xff, y: from >> 8 }); H[0xb688](c); }
435
+ return H[0xb4ca](c);
436
+ };
437
+ H[0xb46f] = c => { Object.assign(c, { a: 0xff, y: 0 }); return H[0xb487](c); };
438
+ // TI$ ($AF48): the clock, as TI's, counted out to its six digits (hours, minutes, seconds; $5E
439
+ // no exponent, $71 the text's start, a point after the sixth and let go), and a string of them
440
+ H[0xaf48] = c => {
441
+ clock(c);
442
+ write(0x5e, 0); write(0x71, 0xff); write(0x5d, 6);
443
+ c.y = 0x24; digits(c);
444
+ return H[0xb46f](c);
445
+ };
446
+ // ($B248) ILLEGAL QUANTITY, as Blitz! jumps there
447
+ H[0xb248] = () => { throw new BasicError('?ILLEGAL QUANTITY ERROR'); };
448
+ // FRESTR ($B6A3): CHKSTR ($AD8F), a string or TYPE MISMATCH, then FREFAC
449
+ H[0xb6a3] = c => { if (!(read(0x0d) & 0x80)) throw new BasicError('?TYPE MISMATCH ERROR'); return H[0xb6a6](c); };
450
+ // ($E25A) a file's name, the string evaluated: FRESTR, and SETNAM ($FFBD) with it
451
+ H[0xe25a] = c => { H[0xb6a3](c); c.x = read(0x22); c.y = read(0x23); return cpu.hooks.get(0xffbd)(c); };
452
+ // GARBAG ($B526), the strings collected, as the ROM does it, byte for byte: the strings' bottom
453
+ // (FRETOP, $33) at the top of memory (MEMSIZ, $37), then, over and over, the highest string
454
+ // below it still to move found (its address $5F/$60, from STREND $31 up; its descriptor's
455
+ // $4E/$4F, and $55 the step it was found with) among the temporary descriptors ($19 to TEMPPT,
456
+ // $16, a step of $53 as it stands), the variables (VARTAB $2D to ARYTAB $2F, seven bytes each, a
457
+ // string's two name bytes top bit clear then set) and the string arrays' elements (to STREND,
458
+ // three bytes each, past each one's header), and moved up to just under the bottom (BLTUC,
459
+ // $A3BF), its descriptor pointed there, and that the bottom; until none is left
460
+ const r16 = a => read(a) | read(a + 1) << 8, w16 = (a, v) => { write(a, v & 0xff); write(a + 1, v >> 8 & 0xff); };
461
+ // (one descriptor at ($22)+y ($B5C7): a string, below the bottom and above the best so far, the
462
+ // best; then $22 on by the step, $53)
463
+ const candidate = y => {
464
+ const at = r16(0x22);
465
+ if (read(at + y)) {
466
+ const lo = read(at + y + 1), hi = read(at + y + 2);
467
+ const below = hi < read(0x34) || (hi === read(0x34) && lo < read(0x33));
468
+ const above = hi > read(0x60) || (hi === read(0x60) && lo >= read(0x5f));
469
+ if (below && above) { write(0x5f, lo); write(0x60, hi); w16(0x4e, at); write(0x55, read(0x53)); }
470
+ }
471
+ w16(0x22, at + read(0x53));
472
+ };
473
+ // BLTUC ($A3BF): the bytes from $5F/$60 to $5A/$5B moved to end at $58/$59, from the top down,
474
+ // a page at a time, those pointers left as the ROM's loop leaves them
475
+ const bltuc = () => {
476
+ const n = (r16(0x5a) - r16(0x5f)) & 0xffff;
477
+ let y = n & 0xff, x = ((n >> 8) + 1) & 0xff;
478
+ write(0x22, y);
479
+ const copy = k => write(r16(0x58) + k, read(r16(0x5a) + k));
480
+ if (y) { w16(0x5a, r16(0x5a) - y); w16(0x58, r16(0x58) - y); }
481
+ for (let first = true; ; first = false) {
482
+ if (!first || y) { do { y = (y - 1) & 0xff; if (y) copy(y); } while (y); copy(0); }
483
+ write(0x5b, (read(0x5b) - 1) & 0xff); write(0x59, (read(0x59) - 1) & 0xff);
484
+ x = (x - 1) & 0xff; if (!x) break;
485
+ }
486
+ };
487
+ H[0xb526] = c => {
488
+ let bottom = r16(0x37);
489
+ for (;;) {
490
+ w16(0x33, bottom); w16(0x4e, 0); w16(0x5f, r16(0x31));
491
+ for (w16(0x22, 0x19); read(0x22) !== read(0x16);) candidate(0);
492
+ write(0x53, 7);
493
+ for (w16(0x22, r16(0x2d)); r16(0x22) !== r16(0x2f);) {
494
+ const at = r16(0x22);
495
+ if (!(read(at) & 0x80) && read(at + 1) & 0x80) candidate(2); else w16(0x22, at + 7);
496
+ }
497
+ w16(0x58, r16(0x2f));
498
+ write(0x53, 3);
499
+ while (r16(0x58) !== r16(0x31)) {
500
+ const at = r16(0x58); w16(0x22, at);
501
+ // (its length added with the carry the comparison with STREND left, clear below it)
502
+ w16(0x58, at + r16(at + 2) + (at >= r16(0x31) ? 1 : 0));
503
+ if (read(at) & 0x80 || !(read(at + 1) & 0x80)) continue;
504
+ // (past its header: five bytes and two for each dimension, the ROM's sum carrying once only)
505
+ const v = ((read(at + 4) << 1) & 0xff) + 5 + (read(at + 4) >> 7);
506
+ w16(0x22, at + (v & 0xff) + (v >> 8));
507
+ while (r16(0x22) !== r16(0x58)) candidate(0);
508
+ }
509
+ if (!r16(0x4e)) { Object.assign(c, { a: 0, x: read(0x23), y: 0, z: 1, n: 0 }); return; }
510
+ const y = (read(0x55) & 4) >> 1; write(0x55, y);
511
+ w16(0x5a, r16(0x5f) + read(r16(0x4e) + y));
512
+ w16(0x58, r16(0x33));
513
+ bltuc();
514
+ write(r16(0x4e) + y + 1, read(0x58));
515
+ write(0x59, (read(0x59) + 1) & 0xff);
516
+ write(r16(0x4e) + y + 2, read(0x59));
517
+ bottom = r16(0x58);
518
+ }
519
+ };
520
+ for (const [a, f] of Object.entries(H)) cpu.hooks.set(+a, f);
521
+ }
522
+
523
+ // A BASIC error, as the ROM's routine gives it ($A43A, through the vector at $0300): the output
524
+ // back to the screen (CLRCHN), a new line, ? and the message, ERROR, IN and the "line" (for
525
+ // Blitz!'s code its program counter at the instruction that failed), then READY. False if the
526
+ // message is not one of BASIC's.
527
+ export function printError({ cpu, write }, message, line) {
528
+ const what = /^\?(.*) ERROR$/.exec(message)?.[1];
529
+ if (!ERRORS.includes(what)) return false;
530
+ const out = ch => { cpu.a = ch; cpu.hooks.get(0xffd2)(cpu); };
531
+ const text = s => { for (const ch of s) out(ch.charCodeAt(0)); };
532
+ cpu.hooks.get(0xffcc)(cpu); write(0x13, 0);
533
+ out(0x0d); out(0x3f); text(what); text(' ERROR');
534
+ if (line !== undefined && line >> 8 !== 0xff) { text(' IN '); text(String(line)); }
535
+ text('\r\nREADY.\r\n');
536
+ write(0x9d, 0x80);
537
+ return true;
538
+ }
539
+
540
+ // The evaluator on the original: the expression in BASIC's text at an address read
541
+ // (basic-lang.js), made a function by the compiler's own code (basic-compile.js),
542
+ // and worked out by BASIC's own rules (basic.js's), its variables read where BASIC keeps
543
+ // them: its table from VARTAB ($2D) to ARYTAB ($2F), seven bytes each, two of name (a whole
544
+ // number's both with the top bit set, a string's the second) and five of value (a number's five
545
+ // bytes; a whole number's high and low; a string's length and where it is). RND is the ROM's
546
+ // routine at $E097, called as the ROM's evaluator calls it. Each address's function is kept.
547
+ export function evaluateInMemory({ cpu, read, write, ram }) {
548
+ const f = new Floats({ read, write }), made = new Map();
549
+ const variable = n => {
550
+ const kind = n.endsWith('%') ? '%' : n.endsWith('$') ? '$' : '', name = kind ? n.slice(0, -1) : n;
551
+ const c1 = name.charCodeAt(0) | (kind === '%' ? 0x80 : 0), c2 = (name.charCodeAt(1) || 0) | (kind ? 0x80 : 0);
552
+ const end = read(0x2f) | read(0x30) << 8;
553
+ for (let at = read(0x2d) | read(0x2e) << 8; at < end; at += 7) {
554
+ if (read(at) !== c1 || read(at + 1) !== c2) continue;
555
+ const v = [2, 3, 4, 5, 6].map(k => read(at + k));
556
+ if (kind === '%') return (v[0] << 8 | v[1]) << 16 >> 16;
557
+ if (kind === '$') { let s = ''; const p = v[1] | v[2] << 8; for (let k = 0; k < v[0]; k++) s += String.fromCharCode(read(p + k)); return s; }
558
+ return unpack(v);
559
+ }
560
+ throw new Error(`the variable ${n} not in BASIC's table: its making by PTRGET not handled`);
561
+ };
562
+ const b = Object.assign(Object.create(Basic.prototype), {
563
+ get: variable,
564
+ rnd: () => { cpu.hooks.get(0xe097)(cpu); return f.fac(); },
565
+ });
566
+ // (the text read from RAM, where a program's is: `ram`)
567
+ return at => {
568
+ const { e, end } = expression(ram, at);
569
+ let fn = made.get(at); if (!fn) made.set(at, fn = evaluator(e));
570
+ return { value: fn(b), end, place: e.k === 'str' ? e.at : undefined };
571
+ };
572
+ }