diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..14dd702 --- /dev/null +++ b/TODO.md @@ -0,0 +1,34 @@ +Error if `DIM z(size)` isn't called before usage + +``` +100 PROC x(a(),size,2) TO result:PRINT result:STOP +110 DEFPROC x(inp(),size,y) +120 LOCAL z(),tot +130 DIM z(size) +140 tot=0 +150 FOR i=1 TO size:z(i)=inp(i)*y:NEXT i +160 FOR i=1 TO size:tot=tot+z(i):NEXT i +170 ENDPROC = tot +``` + + + +--- + + +Test defaults in defproc: + +``` +100 PROC x(12,,"bob"):STOP +110 DEFPROC x(a=1,b$="alice",c$,d=-5) +120 LOCAL e=3 +130 PRINT a,b$,c$,d,e +140 ENDPROC +``` + +--- + +## Validation + +- Validate missing `ENDIF` +- `IF 0` alone should fail … right? diff --git a/__tests__/index.test.js b/__tests__/index.test.js index d7da56e..e9c7e02 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -96,3 +96,26 @@ test('validateTxt', (t) => { const res = validateTxt(src); t.is(res.length, 0, 'no errors'); }); + +test('exact matches', (t) => { + const tests = [ + `10 %a=1 +20 PRINT (%a)+1`, + `100 REPEAT +110 INPUT n +120 IF n=33 THEN EXIT 150 +130 REPEAT UNTIL n < 0 +140 PRINT "Loop completed normally": STOP +150 PRINT "Loop ended early": STOP`, + ]; + + tests.forEach((src) => { + src = src + .split('\n') + .map((_) => _.trim()) + .join('\n'); + const bytes = file2bas(src); + const txt = file2txt(bytes); + t.is(txt.trim(), src.trim(), 'matches'); + }); +}); diff --git a/__tests__/txt2bas.test.js b/__tests__/txt2bas.test.js index ce28d30..b3dab84 100644 --- a/__tests__/txt2bas.test.js +++ b/__tests__/txt2bas.test.js @@ -103,17 +103,7 @@ test('comments', (t) => { test('end with $', (t) => { let src = '202 IF INKEY$="s"'; let expect = [ - 0x00, - 0xca, - 0x07, - 0x00, - 0xfa, - 0xa6, - 0x3d, - 0x22, - 0x73, - 0x22, - 0x0d, + 0x00, 0xca, 0x07, 0x00, 0xfa, 0xa6, 0x3d, 0x22, 0x73, 0x22, 0x0d, ]; const res = Array.from(parseLine(src)); @@ -303,6 +293,16 @@ test('INT function', (t) => { t.is(token.name, 'NUMBER'); }); +test('ELSE IF', (t) => { + let src, res; + src = '20 ELSE IF 1 < 2 PRINT "ELSE"'; + res = parseLines(src).statements[0]; + + t.is(res.tokens[0].text, 'ELSE', 'has else'); + t.is(res.tokens[1].text, 'IF', 'then if'); + t.is(res.tokens[1].value, 0x83, 'correct IF value for following ELSE'); +}); + test('in the wild', (t) => { let src, res; diff --git a/__tests__/validate.test.js b/__tests__/validate.test.js index 74f4483..eb5d7dc 100644 --- a/__tests__/validate.test.js +++ b/__tests__/validate.test.js @@ -17,17 +17,17 @@ function contains(str) { }; } -test('test bad if', (t) => { - const src = '10 IF 0'; - const line = asBasic(src); - t.throws( - () => { - validateStatement(line); - }, - contains('IF statement must have THEN'), - src - ); -}); +// test('test bad if', (t) => { +// const src = '10 IF 0'; +// const line = asBasic(src); +// t.throws( +// () => { +// validateStatement(line); +// }, +// contains('IF statement must have THEN'), +// src +// ); +// }); test('validator works with autoline', async (t) => { const fixture = await readFile(__dirname + '/fixtures/autoline.txt'); @@ -184,9 +184,22 @@ notThrows( ); notThrows('10 %a = 10'); notThrows('10 PRINT ("TRUE" AND b)+("FALSE" AND NOT b)'); +notThrows('100 PROC x(12,,”bob”):STOP'); +notThrows('120 PRINT name$'); +notThrows('10 DEF FN ian$(REF jenny$(),index)=jenny$(index)'); +notThrows('10 LET x,y = y,x'); +notThrows('10 a,b,c,d$,e$,f = 1,2,3,"xyz","zzz",g*h'); +notThrows('10 y$ *= 2'); +notThrows('120 PRINT "Press a key for left":x=INPUT -2'); +notThrows('10 " Hello There! "[<+->]'); +notThrows('10 a$(5)[-](3 TO 7)[<]'); +notThrows('30 PRINT "perftest() took ";TIME;" frames"'); +notThrows('20 PRINT %a+1'); +notThrows('20 %a=1'); /********************************************/ +// throws('20 PRINT (%a)+1'); throws( '330 REPEAT UNTIL %(c=13) AND %(j > 8)', 'Cannot redeclare integer expression' @@ -202,7 +215,7 @@ throws('10 DEFPROC _foo()', 'Function names can only contain letters'); throws('10 DEFPROC 5foo()', 'Function names can only contain letters'); throws('760 ', 'Empty line'); -throws('945 IF %i = 20 ENDPROC', 'IF statement must have THEN'); +// throws('945 IF %i = 20 ENDPROC', 'IF statement must have THEN'); throws('10 % sprite continue %', 'Cannot redeclare integer expression whilst'); throws('10 IF %b=%c THEN ENDPROC', 'Cannot redeclare integer expression', { message: 'integer expression function on either side of IF comparator', diff --git a/codes.js b/codes.js index c203a72..9c8849f 100644 --- a/codes.js +++ b/codes.js @@ -9,8 +9,8 @@ export default { // new in 2.08 0x81: 'TIME', 0x82: 'PRIVATE', - 0x83: 'ELSEIF', - 0x84: 'ENDIF', // internal use only: displays as IF, but indicates ELSE is present on same line + 0x83: 'IF', // internal use only: displays as IF (which is managed here), but is converted from 0x83 via the aliases in op-table + 0x84: 'ENDIF', 0x85: 'EXIT', 0x86: 'REF', diff --git a/txt2bas/index.js b/txt2bas/index.js index 2c85dd9..aea8aa1 100644 --- a/txt2bas/index.js +++ b/txt2bas/index.js @@ -68,6 +68,14 @@ import { * @property {Statement} value */ +/** + * Where a bank starts + * + * @typedef BankSplit + * @property {string} bankFile the filename + * @property {number} line the starting line + */ + /** * Auto increment line * @@ -202,6 +210,7 @@ export function parseLineWithData(line, autoline = null) { * @property {string} filename * @property {Autoline} autoline * @property {Define[]} defines + * @property {BankSplit[]} bankSplits */ /** @@ -229,6 +238,8 @@ export function parseLines( let filename = null; const defines = []; + const bankSplits = []; + const autoline = new Autoline(); for (let i = 0; i < lines.length; i++) { @@ -242,6 +253,11 @@ export function parseLines( filename = line.split(' ')[1]; } + if (line.startsWith('#bankfile ')) { + const bankFile = line.split(' ')[1]; + bankSplits.push({ bank: bankFile, line: i + 1 }); + } + if (line.startsWith('#autoline')) { autoline.parse(line); } @@ -311,6 +327,7 @@ export function parseLines( filename, autoline, defines, + bankSplits, }; } @@ -758,6 +775,23 @@ export class Statement { return this.line.substring(start, pos); } + peekPrevToken(at = this.pos) { + let pos = at - 1; + const end = pos; + let allowSpace = true; + while (pos >= 0 || tests._isDigit(this.line.charAt(pos))) { + if (tests._isSpace(this.line.charAt(pos)) && !allowSpace) { + pos++; + break; + } + if (tests._isSpace(this.line.charAt(pos))) { + allowSpace = false; + } + pos--; + } + return this.line.substring(pos, end); + } + findOpCode(endPos) { const peek = this.peek(endPos); const moreToken = tests._isAlpha(peek); @@ -771,8 +805,16 @@ export class Statement { return false; } + if (curr === 'IF') { + // look for ELSE before hand + const prev = this.peekPrevToken(); + if (prev === 'ELSE') { + curr = 'ELSE IF'; + } + } + // be wary that this could be something like `DEF FN` - if (peek === ' ' && !opTable[curr]) { + else if (peek === ' ' && !opTable[curr]) { const next = this.peekToken(endPos + 1).toUpperCase(); const test = `${curr} ${next}`; diff --git a/txt2bas/op-table.js b/txt2bas/op-table.js index ee25d33..335656d 100644 --- a/txt2bas/op-table.js +++ b/txt2bas/op-table.js @@ -11,5 +11,6 @@ export const opTable = Object.entries(codes).reduce( GOSUB: 0xed, RAND: 0xf9, CONT: 0xe8, + 'ELSE IF': 0x83, } ); diff --git a/txt2bas/validator.js b/txt2bas/validator.js index 590158a..f6958ed 100644 --- a/txt2bas/validator.js +++ b/txt2bas/validator.js @@ -144,10 +144,11 @@ class Scope { } /** - * @returns {Token} + * @returns {Token|null} */ peekNext() { let next = this.tokens[0]; + if (!next) return null; if (next.name === WHITE_SPACE) next = this.tokens[1]; return next; } @@ -343,6 +344,8 @@ export function validateStatement(tokens, debug = {}) { // scope state scope.push(OUTER_IF); scope.push(IF); + + // should I look ahead for a THEN to detect the type of IF? } if (value === opTable.UNTIL) { @@ -358,8 +361,14 @@ export function validateStatement(tokens, debug = {}) { scope.resetExpression(); } - if (value === opTable.ELSE && scope.includes(OUTER_IF)) { - throw new Error('Statement separator (:) expected before ELSE'); + if (value === opTable.ELSE) { + const next = scope.peekNext(); + + if (next && next.text === IF) { + // change the token to ELSEIF and drop the IF + } else if (scope.includes(OUTER_IF)) { + throw new Error('Statement separator (:) expected before ELSE'); + } } if ( @@ -675,7 +684,11 @@ export function validateStatementStarters(token, scope) { */ export function validateEndOfStatement(scope) { if (scope.includes(IF)) { - throw new Error('IF statement must have THEN'); + if (parser.getParser() === parser.v207) { + throw new Error('IF statement must have THEN'); + } else { + // we need to stack up an open IF statement + } } const open = scope.findIndex((_) => _.startsWith('OPEN_'));