Applesoft compiler fixes (#98)
* Add tests for Applesoft compiler in preparation for refactoring
While refactoring the compiler, I found several small bugs:
* Lower-case letters in strings and REM statements were converted
to upper-case.
* Lines are stored in the order received, not sorted by line number.
* Does not prefer `ATN` to `AT`.
* Does not prefer `TO` to `AT`.
* `DATA` statements don't preserve spaces.
* `DATA` statements don't preserve lowercase.
These will be fixed in the upcoming refactoring.
* Refactor the Applesoft Compiler
Before, the compiler had a few bugs that were not trivial to solve
because the implementation was in one heavily-nested function.
In this refactoring of the compiler, things like tokenization have
been split into separate methods which makes them a bit easier to
understand.
This refactoring also passes all of the tests.
* Set `PRGEND` when compiling to memory
Before, `PRGEND` was not adjusted which made round-tripping from
the Applesoft compiler to the decompiler not work. This change
now updates `PRGEND` with the end-of-program + 2 bytes which seems
to be the most frequent value that I have observed.
* Fix two compiler bugs
In debugging the decompiler, I noticed two bugs in the compiler:
* The first character after a line number was skipped.
* `?` was not accepted as a shortcut for `PRINT`.
This change fixes these two problems and adds tests.
* Ignore spaces more aggressively
It turns out that Applesoft happily accepts 'T H E N' for `THEN`
but the parser did not. This change fixes that and adds tests for
some odd cases.
Interestingly, this means that there are some valid statements
that Applesoft can never parse correctly because it is greedy
and ignores (most) spaces. For example, `NOT RACE` will always
parse as `NOTRACE` even though `NOT RACE` is a valid expression.
* Move tokens into a separate file
Because the token lists are just maps in opposite directions, put
them in the same file. In the future, maybe we can build one
automatically.
* Fix `apple2.ts`
I had neglected to actually update `apple2.ts` to use the new
compiler and decompiler. They now do.
Also, the decompiler can be created from `Memory`. It assumes,
though, that the zero page pointers to the start and end of the
program are correct.
* Address comments
* No more `as const` for tokens.
* Extracted zero page constants to their own file.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-24 03:41:45 +00:00
|
|
|
import ApplesoftDecompiler from 'js/applesoft/decompiler';
|
|
|
|
import ApplesoftCompiler from 'js/applesoft/compiler';
|
|
|
|
import RAM from 'js/ram';
|
|
|
|
import { Memory } from 'js/types';
|
|
|
|
|
|
|
|
function decompileFromMemory(ram: Memory): string {
|
|
|
|
const decompiler = ApplesoftDecompiler.decompilerFromMemory(ram);
|
|
|
|
return decompiler.list();
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('ApplesoftDecompiler', () => {
|
|
|
|
it('decompiles one-line program from memory', () => {
|
|
|
|
const ram = new RAM(0x00, 0xff); // 64K
|
|
|
|
ApplesoftCompiler.compileToMemory(ram, '10 PRINT "Hello, World!"');
|
|
|
|
|
|
|
|
const program = decompileFromMemory(ram);
|
|
|
|
expect(program).toEqual(' 10 PRINT "Hello, World!"\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('decompiles REM statements correctly', () => {
|
|
|
|
const ram = new RAM(0x00, 0xff); // 64K
|
|
|
|
ApplesoftCompiler.compileToMemory(ram, '10 REMNo space before\n20 REM with space');
|
|
|
|
|
|
|
|
const program = decompileFromMemory(ram);
|
|
|
|
expect(program).toEqual(' 10 REM No space before\n 20 REM with space\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('lists a one-line program', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 PRINT "Hello, World!"');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list();
|
|
|
|
expect(program).toEqual(' 10 PRINT "Hello, World!"\n');
|
|
|
|
});
|
|
|
|
|
2022-07-23 19:00:38 +00:00
|
|
|
it('correctly computes the base address when 0 is passed in', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 PRINT "Hello, World!"\n20 GOTO 10');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program(), 0);
|
|
|
|
const program = decompiler.list();
|
|
|
|
expect(program).toEqual(' 10 PRINT "Hello, World!"\n 20 GOTO 10\n');
|
|
|
|
});
|
|
|
|
|
Applesoft compiler fixes (#98)
* Add tests for Applesoft compiler in preparation for refactoring
While refactoring the compiler, I found several small bugs:
* Lower-case letters in strings and REM statements were converted
to upper-case.
* Lines are stored in the order received, not sorted by line number.
* Does not prefer `ATN` to `AT`.
* Does not prefer `TO` to `AT`.
* `DATA` statements don't preserve spaces.
* `DATA` statements don't preserve lowercase.
These will be fixed in the upcoming refactoring.
* Refactor the Applesoft Compiler
Before, the compiler had a few bugs that were not trivial to solve
because the implementation was in one heavily-nested function.
In this refactoring of the compiler, things like tokenization have
been split into separate methods which makes them a bit easier to
understand.
This refactoring also passes all of the tests.
* Set `PRGEND` when compiling to memory
Before, `PRGEND` was not adjusted which made round-tripping from
the Applesoft compiler to the decompiler not work. This change
now updates `PRGEND` with the end-of-program + 2 bytes which seems
to be the most frequent value that I have observed.
* Fix two compiler bugs
In debugging the decompiler, I noticed two bugs in the compiler:
* The first character after a line number was skipped.
* `?` was not accepted as a shortcut for `PRINT`.
This change fixes these two problems and adds tests.
* Ignore spaces more aggressively
It turns out that Applesoft happily accepts 'T H E N' for `THEN`
but the parser did not. This change fixes that and adds tests for
some odd cases.
Interestingly, this means that there are some valid statements
that Applesoft can never parse correctly because it is greedy
and ignores (most) spaces. For example, `NOT RACE` will always
parse as `NOTRACE` even though `NOT RACE` is a valid expression.
* Move tokens into a separate file
Because the token lists are just maps in opposite directions, put
them in the same file. In the future, maybe we can build one
automatically.
* Fix `apple2.ts`
I had neglected to actually update `apple2.ts` to use the new
compiler and decompiler. They now do.
Also, the decompiler can be created from `Memory`. It assumes,
though, that the zero page pointers to the start and end of the
program are correct.
* Address comments
* No more `as const` for tokens.
* Extracted zero page constants to their own file.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-24 03:41:45 +00:00
|
|
|
it('lists a program with a long line', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 PRINT "Hello, World!"\n'
|
|
|
|
+ '20 PRINT "Hello, again, with a much longer line this time."\n'
|
|
|
|
+ '30 REM1234567890123456789012345678901234567890');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list();
|
|
|
|
expect(program).toEqual(' 10 PRINT "Hello, World!"\n'
|
|
|
|
+ ' 20 PRINT "Hello, again, with a \n'
|
|
|
|
+ ' much longer line this time."\n'
|
|
|
|
+ ' \n'
|
|
|
|
+ ' 30 REM 123456789012345678901234\n'
|
|
|
|
+ ' 5678901234567890\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('lists a program with a long line Apple ][+-style', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 PRINT "Hello, World!"\n'
|
|
|
|
+ '20 PRINT "Hello, again, with a much longer line this time."\n'
|
|
|
|
+ '30 REM1234567890123456789012345678901234567890');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list({ apple2: 'plus' });
|
|
|
|
expect(program).toEqual('10 PRINT "Hello, World!"\n'
|
|
|
|
+ '20 PRINT "Hello, again, with a m\n'
|
|
|
|
+ ' uch longer line this time."\n'
|
|
|
|
+ '30 REM 1234567890123456789012345\n'
|
|
|
|
+ ' 678901234567890\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('lists a range of lines', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 PRINT "Hello, World!"\n'
|
|
|
|
+ '20 PRINT "Hello, again, with a much longer line this time."\n'
|
|
|
|
+ '30 REM1234567890123456789012345678901234567890');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list({}, 10, 20);
|
|
|
|
expect(program).toEqual(' 10 PRINT "Hello, World!"\n'
|
|
|
|
+ ' 20 PRINT "Hello, again, with a \n'
|
|
|
|
+ ' much longer line this time."\n'
|
|
|
|
+ ' \n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('lists weird code correctly', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 NOT RACE A THEN B');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list();
|
|
|
|
expect(program).toEqual(' 10 NOTRACE AT HENB\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('lists 10ATOZ correctly', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10ATOZ');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list();
|
|
|
|
expect(program).toEqual(' 10 A TO Z\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('wraps correctly in 80-column mode', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 ?:?:?:?:?:?:?:?:?:?:?:?:?:?:?:?');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.list({ columns: 80 });
|
|
|
|
expect(program).toEqual(' 10 PRINT : PRINT : PRINT : PRINT : '
|
|
|
|
+ 'PRINT : PRINT : PRINT : PRINT : PRINT \n'
|
|
|
|
+ ' : PRINT : PRINT : PRINT : PRINT : PRINT : PRINT : '
|
|
|
|
+ 'PRINT \n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('decompiles compactly', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 ?:?:?:?:?:?:?:?:?:?:?:?:?:?:?:?');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'compact' });
|
|
|
|
expect(program).toEqual('10?:?:?:?:?:?:?:?:?:?:?:?:?:?:?:?');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling compactly, adds a space after the line', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 12345');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'compact' });
|
|
|
|
expect(program).toEqual('10 12345');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling compactly, adds a space after AT for token', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 AT NEXT');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'compact' });
|
|
|
|
expect(program).toEqual('10AT NEXT');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling compactly, adds a space after AT for literal', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 AT n');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'compact' });
|
|
|
|
expect(program).toEqual('10AT N');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling compactly, decompiles 10ATOZ correctly', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10ATOZ');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'compact' });
|
|
|
|
expect(program).toEqual('10ATOZ');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling compactly, adds a space to disambiguate tokens', () => {
|
|
|
|
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'));
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'compact' });
|
|
|
|
expect(program).toEqual([
|
|
|
|
'10ATHENB',
|
|
|
|
'30ATOZ',
|
|
|
|
'40AT N',
|
|
|
|
'50ATN',
|
|
|
|
'60NOTRACE',
|
|
|
|
'70NOTRACE'].join('\n'));
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling prettily, formats reasonably well', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 FORI=1TO10:PRINTI:NEXT');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'pretty' });
|
|
|
|
expect(program).toEqual('10 FOR I = 1 TO 10 : PRINT I : NEXT');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling prettily, formats relations', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 IFA<BORA>=BORB<=AORB=A THEN');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'pretty' });
|
|
|
|
expect(program).toEqual('10 IF A < B OR A >= B OR B <= A OR B = AT HEN');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling prettily, decompiles 10ATOZ correctly', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10ATOZ');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'pretty' });
|
|
|
|
expect(program).toEqual('10 A TO Z');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling prettily, does not insert extra spaces in strings', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10A="::::":B=","');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'pretty' });
|
|
|
|
expect(program).toEqual('10 A = "::::" : B = ","');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when decompiling prettily, inserts space after comma', () => {
|
|
|
|
const compiler = new ApplesoftCompiler();
|
|
|
|
compiler.compile('10 HPLOTX,Y:GOTO10');
|
|
|
|
|
|
|
|
const decompiler = new ApplesoftDecompiler(compiler.program());
|
|
|
|
const program = decompiler.decompile({ style: 'pretty' });
|
|
|
|
expect(program).toEqual('10 HPLOT X, Y : GOTO 10');
|
|
|
|
});
|
2022-07-23 19:00:38 +00:00
|
|
|
});
|