apple2js/test/js/applesoft/compiler.spec.ts

432 lines
20 KiB
TypeScript

import ApplesoftCompiler from 'js/applesoft/compiler';
import RAM from 'js/ram';
import { Memory } from 'js/types';
// Zero page locations used by Applesoft. The names come from
// the commented decompilation produced by the Merlin Pro
// assembler, revision 4/27/84. There is evidence from
// https://www.pagetable.com/?p=774 that the original Microsoft
// BASIC source code used these names as well.
const TXTTAB = 0x67; // start of program, word
const VARTAB = 0x69; // start of variables, word
const ARYTAB = 0x6B; // start of arrays, word
const STREND = 0x6D; // end of strings, word
const PRGEND = 0xAF; // end of program, word
function compileToMemory(ram: Memory, program: string) {
ApplesoftCompiler.compileToMemory(ram, program);
}
// Manual decompilation based on "Applesoft Internal Structure"
// by C.K. Mesztenyi/Washington Apple Pi, from Call—A.P.P.L.E.,
// January, 1982. Archived at:
// https://archive.org/details/DTCA2DOC-045_applesoft_internal
// Decompilation verified on the emulator by typing in the
// program, then:
// ]CALL -151
// *800.820
// and comparing the resulting bytes (starting at 801).
describe('ApplesoftCompiler', () => {
it('compiles a one-line hello world', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10 PRINT "HELLO, WORLD!"');
expect(compiler.program()).toEqual(new Uint8Array([
0x16, 0x08, 0x0a, 0x00, 0xba, 0x22, 0x48, 0x45,
0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52,
0x4c, 0x44, 0x21, 0x22, 0x00, 0x00, 0x00
]));
});
it('compiles a one-line hello world into memory', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 PRINT "HELLO, WORLD!"');
expect(ram.read(0x08, 0x01)).toBe(0x16); // pointer to next line low
expect(ram.read(0x08, 0x02)).toBe(0x08); // pointer to next line high
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
expect(ram.read(0x08, 0x06)).toBe(0x22); // "
expect(ram.read(0x08, 0x07)).toBe(0x48); // H
expect(ram.read(0x08, 0x08)).toBe(0x45); // E
expect(ram.read(0x08, 0x09)).toBe(0x4C); // L
expect(ram.read(0x08, 0x0a)).toBe(0x4C); // L
expect(ram.read(0x08, 0x0b)).toBe(0x4F); // O
expect(ram.read(0x08, 0x0c)).toBe(0x2C); // ,
expect(ram.read(0x08, 0x0d)).toBe(0x20); // space
expect(ram.read(0x08, 0x0e)).toBe(0x57); // W
expect(ram.read(0x08, 0x0f)).toBe(0x4F); // O
expect(ram.read(0x08, 0x10)).toBe(0x52); // R
expect(ram.read(0x08, 0x11)).toBe(0x4C); // L
expect(ram.read(0x08, 0x12)).toBe(0x44); // D
expect(ram.read(0x08, 0x13)).toBe(0x21); // !
expect(ram.read(0x08, 0x14)).toBe(0x22); // "
expect(ram.read(0x08, 0x15)).toBe(0x00); // end of line
expect(ram.read(0x08, 0x16)).toBe(0x00); // end of program low
expect(ram.read(0x08, 0x17)).toBe(0x00); // end of program high
expect(ram.read(0x00, TXTTAB)).toBe(0x01); // start of program low
expect(ram.read(0x00, TXTTAB + 1)).toBe(0x08); // start of program high
expect(ram.read(0x00, VARTAB)).toBe(0x19); // start of variables low
expect(ram.read(0x00, VARTAB + 1)).toBe(0x08); // start of variables high
expect(ram.read(0x00, ARYTAB)).toBe(0x19); // start of arrays low
expect(ram.read(0x00, ARYTAB + 1)).toBe(0x08); // start of arrays high
expect(ram.read(0x00, STREND)).toBe(0x19); // end of strings low
expect(ram.read(0x00, STREND + 1)).toBe(0x08); // end of strings high
expect(ram.read(0x00, PRGEND)).toBe(0x19); // end of program low
expect(ram.read(0x00, PRGEND + 1)).toBe(0x08); // end of program high
});
it('uppercases normal-mode text, like variables', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 fori=xtoz');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x81); // FOR
expect(ram.read(0x08, 0x06)).toBe(0x49); // I
expect(ram.read(0x08, 0x07)).toBe(0xd0); // = (token)
expect(ram.read(0x08, 0x08)).toBe(0x58); // X
expect(ram.read(0x08, 0x09)).toBe(0xc1); // TO
expect(ram.read(0x08, 0x0a)).toBe(0x5a); // Z
expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
});
it('allows lower-case characters in strings', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 PRINT "Hello!"');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
expect(ram.read(0x08, 0x06)).toBe(0x22); // "
expect(ram.read(0x08, 0x07)).toBe(0x48); // H
expect(ram.read(0x08, 0x08)).toBe(0x65); // e
expect(ram.read(0x08, 0x09)).toBe(0x6C); // l
expect(ram.read(0x08, 0x0a)).toBe(0x6C); // l
expect(ram.read(0x08, 0x0b)).toBe(0x6F); // o
});
it('allows lower-case characters in comments', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 REM Hello!');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0xb2); // REM
expect(ram.read(0x08, 0x06)).toBe(0x20); // space
expect(ram.read(0x08, 0x07)).toBe(0x48); // H
expect(ram.read(0x08, 0x08)).toBe(0x65); // e
expect(ram.read(0x08, 0x09)).toBe(0x6C); // l
expect(ram.read(0x08, 0x0a)).toBe(0x6C); // l
expect(ram.read(0x08, 0x0b)).toBe(0x6F); // o
});
it('allows lower-case tokens', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 print "Hello!"');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
});
it('accepts out-of-order lines', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '20 GOTO 10\n10 PRINT "HELLO');
expect(ram.read(0x08, 0x01)).toBe(0x0d); // pointer to next line low
expect(ram.read(0x08, 0x02)).toBe(0x08); // pointer to next line high
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
expect(ram.read(0x08, 0x06)).toBe(0x22); // "
expect(ram.read(0x08, 0x07)).toBe(0x48); // H
expect(ram.read(0x08, 0x08)).toBe(0x45); // E
expect(ram.read(0x08, 0x09)).toBe(0x4C); // L
expect(ram.read(0x08, 0x0a)).toBe(0x4C); // L
expect(ram.read(0x08, 0x0b)).toBe(0x4F); // O
expect(ram.read(0x08, 0x0c)).toBe(0x00); // end of line
expect(ram.read(0x08, 0x0d)).toBe(0x15); // pointer to next line low
expect(ram.read(0x08, 0x0e)).toBe(0x08); // pointer to next line high
expect(ram.read(0x08, 0x0f)).toBe(0x14); // line number low
expect(ram.read(0x08, 0x10)).toBe(0x00); // line number high
expect(ram.read(0x08, 0x11)).toBe(0xab); // GOTO
expect(ram.read(0x08, 0x12)).toBe(0x31); // 1
expect(ram.read(0x08, 0x13)).toBe(0x30); // 0
expect(ram.read(0x08, 0x14)).toBe(0x00); // end of line
expect(ram.read(0x08, 0x15)).toBe(0x00); // end of program low
expect(ram.read(0x08, 0x16)).toBe(0x00); // end of program high
});
it('prefers ATN to AT', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 X = ATN(20)');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x58); // X
expect(ram.read(0x08, 0x06)).toBe(0xd0); // = (token)
expect(ram.read(0x08, 0x07)).toBe(0xe1); // ATN
expect(ram.read(0x08, 0x08)).toBe(0x28); // (
expect(ram.read(0x08, 0x09)).toBe(0x32); // 2
expect(ram.read(0x08, 0x0a)).toBe(0x30); // 0
expect(ram.read(0x08, 0x0b)).toBe(0x29); // )
expect(ram.read(0x08, 0x0c)).toBe(0x00); // end of line
});
it('prefers TO to AT', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 FORI=ATOZ');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x81); // FOR
expect(ram.read(0x08, 0x06)).toBe(0x49); // I
expect(ram.read(0x08, 0x07)).toBe(0xd0); // = (token)
expect(ram.read(0x08, 0x08)).toBe(0x41); // A
expect(ram.read(0x08, 0x09)).toBe(0xc1); // TO
expect(ram.read(0x08, 0x0a)).toBe(0x5a); // Z
expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
});
it('parses DATA statements that start with space', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATA 1,2,3');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x20); // space
expect(ram.read(0x08, 0x07)).toBe(0x31); // 1
expect(ram.read(0x08, 0x08)).toBe(0x2c); // ,
expect(ram.read(0x08, 0x09)).toBe(0x32); // 2
expect(ram.read(0x08, 0x0a)).toBe(0x2c); // ,
expect(ram.read(0x08, 0x0b)).toBe(0x33); // 3
expect(ram.read(0x08, 0x0c)).toBe(0x00); // end of line
});
it('parses DATA statements with numbers', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATA1,2,3');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x31); // 1
expect(ram.read(0x08, 0x07)).toBe(0x2c); // ,
expect(ram.read(0x08, 0x08)).toBe(0x32); // 2
expect(ram.read(0x08, 0x09)).toBe(0x2c); // ,
expect(ram.read(0x08, 0x0a)).toBe(0x33); // 3
expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
});
it('parses DATA statements with strings including lower-case', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATA"abc"');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x22); // "
expect(ram.read(0x08, 0x07)).toBe(0x61); // a
expect(ram.read(0x08, 0x08)).toBe(0x62); // b
expect(ram.read(0x08, 0x09)).toBe(0x63); // c
expect(ram.read(0x08, 0x0a)).toBe(0x22); // "
expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
});
it('parses DATA statements with literals', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAHELLO');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x48); // H
expect(ram.read(0x08, 0x07)).toBe(0x45); // E
expect(ram.read(0x08, 0x08)).toBe(0x4C); // L
expect(ram.read(0x08, 0x09)).toBe(0x4C); // L
expect(ram.read(0x08, 0x0a)).toBe(0x4F); // O
expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
});
it('parses DATA statements with literals including lower-case', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAHello');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x48); // H
expect(ram.read(0x08, 0x07)).toBe(0x65); // e
expect(ram.read(0x08, 0x08)).toBe(0x6C); // l
expect(ram.read(0x08, 0x09)).toBe(0x6C); // l
expect(ram.read(0x08, 0x0a)).toBe(0x6F); // o
expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
});
it('parses DATA statements with literals including quotes', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAAA"B');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x41); // A
expect(ram.read(0x08, 0x07)).toBe(0x41); // A
expect(ram.read(0x08, 0x08)).toBe(0x22); // "
expect(ram.read(0x08, 0x09)).toBe(0x42); // B
expect(ram.read(0x08, 0x0a)).toBe(0x00); // end of line
});
it('parses DATA statements with literals including spaces', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAA B');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x41); // A
expect(ram.read(0x08, 0x07)).toBe(0x20); // space
expect(ram.read(0x08, 0x08)).toBe(0x20); // space
expect(ram.read(0x08, 0x09)).toBe(0x42); // B
expect(ram.read(0x08, 0x0a)).toBe(0x00); // end of line
});
it('terminates DATA statements at colons', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAAA:FORI=1TO1');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x41); // A
expect(ram.read(0x08, 0x07)).toBe(0x41); // A
expect(ram.read(0x08, 0x08)).toBe(0x3a); // :
expect(ram.read(0x08, 0x09)).toBe(0x81); // FOR
expect(ram.read(0x08, 0x0a)).toBe(0x49); // I
expect(ram.read(0x08, 0x0b)).toBe(0xd0); // = (token)
expect(ram.read(0x08, 0x0c)).toBe(0x31); // 1
expect(ram.read(0x08, 0x0d)).toBe(0xc1); // TO
expect(ram.read(0x08, 0x0e)).toBe(0x31); // 1
expect(ram.read(0x08, 0x0f)).toBe(0x00); // end of line
});
it('does not terminate DATA statements with a literal with a quote at colon', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAA":FORI=1TO1');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x41); // A
expect(ram.read(0x08, 0x07)).toBe(0x22); // "
expect(ram.read(0x08, 0x08)).toBe(0x3a); // :
expect(ram.read(0x08, 0x09)).toBe(0x46); // F
expect(ram.read(0x08, 0x0a)).toBe(0x4F); // O
expect(ram.read(0x08, 0x0b)).toBe(0x52); // R
expect(ram.read(0x08, 0x0c)).toBe(0x49); // I
expect(ram.read(0x08, 0x0d)).toBe(0x3D); // =
expect(ram.read(0x08, 0x0e)).toBe(0x31); // 1
expect(ram.read(0x08, 0x0f)).toBe(0x54); // T
expect(ram.read(0x08, 0x10)).toBe(0x4F); // O
expect(ram.read(0x08, 0x11)).toBe(0x31); // 1
expect(ram.read(0x08, 0x12)).toBe(0x00); // end of line
});
it('does terminate DATA statements with a literal with two quotes at colon', () => {
const ram = new RAM(0, 0xff); // 64K of RAM
compileToMemory(ram, '10 DATAA"":FORI=1TO1');
expect(ram.read(0x08, 0x03)).toBe(10); // line number low
expect(ram.read(0x08, 0x04)).toBe(0); // line number high
expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
expect(ram.read(0x08, 0x06)).toBe(0x41); // A
expect(ram.read(0x08, 0x07)).toBe(0x22); // "
expect(ram.read(0x08, 0x08)).toBe(0x22); // "
expect(ram.read(0x08, 0x09)).toBe(0x3a); // :
expect(ram.read(0x08, 0x0a)).toBe(0x81); // FOR
expect(ram.read(0x08, 0x0b)).toBe(0x49); // I
expect(ram.read(0x08, 0x0c)).toBe(0xd0); // = (token)
expect(ram.read(0x08, 0x0d)).toBe(0x31); // 1
expect(ram.read(0x08, 0x0e)).toBe(0xc1); // TO
expect(ram.read(0x08, 0x0f)).toBe(0x31); // 1
expect(ram.read(0x08, 0x10)).toBe(0x00); // end of line
});
it('does not require a space after line number', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10PRINT"HELLO, WORLD!"');
expect(compiler.program()).toEqual(new Uint8Array([
0x16, 0x08, 0x0a, 0x00, 0xba, 0x22, 0x48, 0x45,
0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52,
0x4c, 0x44, 0x21, 0x22, 0x00, 0x00, 0x00
]));
});
it('parses ? as PRINT', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10 ?"HELLO, WORLD!"');
expect(compiler.program()).toEqual(new Uint8Array([
0x16, 0x08, 0x0a, 0x00, 0xba, 0x22, 0x48, 0x45,
0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52,
0x4c, 0x44, 0x21, 0x22, 0x00, 0x00, 0x00
]));
});
it('skips spaces when reading tokens', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10 T H E N');
expect(compiler.program()).toEqual(new Uint8Array([
0x07, 0x08, 0x0a, 0x00, 0xc4, 0x00, 0x00, 0x00,
]));
});
it('skips spaces and ignores case when reading tokens', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10 T h E n');
expect(compiler.program()).toEqual(new Uint8Array([
0x07, 0x08, 0x0a, 0x00, 0xc4, 0x00, 0x00, 0x00,
]));
});
it('smashes tokens together', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10 NOT RACE A THEN B');
expect(compiler.program()).toEqual(new Uint8Array([
0x0c, 0x08, 0x0a, 0x00, 0x9c, 0xc5, 0x48, 0x45,
0x4e, 0x42, 0x00, 0x00, 0x00,
]));
});
it('parses 10ATOZ correctly', () => {
const compiler = new ApplesoftCompiler();
compiler.compile('10ATOZ');
expect(compiler.program()).toEqual(new Uint8Array([
0x09, 0x08, 0x0a, 0x00, 0x41, 0xc1, 0x5a, 0x00,
0x00, 0x00,
]));
});
it('parses a bunch of crazy correctly', () => {
const compiler = new ApplesoftCompiler();
compiler.compile([
'10 A THEN B',
'30 A TO Z',
'40 AT N',
'50 A TN',
'60 N O T R A C E',
'70 NOT RACE'].join('\n'));
expect(compiler.program()).toEqual(new Uint8Array([
0x0b, 0x08, 0x0a, 0x00, 0xc5, 0x48, 0x45,
0x4e, 0x42, 0x00, 0x13, 0x08, 0x1e, 0x00, 0x41,
0xc1, 0x5a, 0x00, 0x1a, 0x08, 0x28, 0x00, 0xc5,
0x4e, 0x00, 0x20, 0x08, 0x32, 0x00, 0xe1, 0x00,
0x26, 0x08, 0x3c, 0x00, 0x9c, 0x00, 0x2c, 0x08,
0x46, 0x00, 0x9c, 0x00, 0x00, 0x00,
]));
});
});