From 1706d14f171637f8742655da60ef504cf5378db0 Mon Sep 17 00:00:00 2001 From: Remy Sharp Date: Sun, 3 Mar 2024 18:28:31 +0000 Subject: [PATCH] fix: ELSE and IF ELSE statement Fixes https://github.com/remy/vscode-nextbasic/issues/45 --- __tests__/bas2txt.test.js | 8 ++ __tests__/peek-token.test.js | 61 +++++++++++++ __tests__/txt2bas.test.js | 11 ++- cli/index.js | 2 +- codes.js | 2 +- index.d.ts | 15 ++++ txt2bas/index.js | 169 +++++++++++++++++++++++++---------- txt2bas/types.js | 1 + 8 files changed, 217 insertions(+), 52 deletions(-) create mode 100644 __tests__/peek-token.test.js create mode 100644 index.d.ts diff --git a/__tests__/bas2txt.test.js b/__tests__/bas2txt.test.js index 8c363d0..7299185 100644 --- a/__tests__/bas2txt.test.js +++ b/__tests__/bas2txt.test.js @@ -65,3 +65,11 @@ test('comments are parsed as plain text', (t) => { res = line2txt(src.basic); t.is(res, text, 'comments are untouched with REM'); }); + +test('else if (2.08)', (t) => { + let text, src, res; + text = '20 ELSE IF 1 < 2 PRINT "ELSE"'; + src = parseLineWithData(text); // to binary + res = line2txt(src.basic); // to text + t.is(res, text, 'new IF correctly rendered'); +}); diff --git a/__tests__/peek-token.test.js b/__tests__/peek-token.test.js new file mode 100644 index 0000000..ad30144 --- /dev/null +++ b/__tests__/peek-token.test.js @@ -0,0 +1,61 @@ +import test from 'ava'; +import { Statement } from '../txt2bas'; + +/** + * @param {require("~/index.d.ts").Token} token string + * @returns {string} + */ +function text(token) { + return token.text || token.value; +} + +test('multi statement peek', (t) => { + let src, + token, + peek, + pos = 0; + src = '10 IF %z THEN: ELSE IF %z=34 THEN: ELSE PRINT "3"'; + + const statement = new Statement(src); + + token = statement.nextToken(); // move to IF + t.is(text(token), 'IF'); + pos = statement.pos; + peek = statement.peekToken(pos); + t.is(text(peek), '%'); + pos = peek.pos; + peek = statement.peekToken(pos); + t.is(text(peek), 'z'); + pos = peek.pos; + peek = statement.peekToken(pos); + t.is(text(peek), 'THEN'); + pos = peek.pos; + peek = statement.peekToken(pos); + t.is(peek.name, 'STATEMENT_SEP'); // should be nothing +}); + +test.only('multi statement peek with preview', (t) => { + let src, token, hasThen, statement; + + src = '10 IF %z THEN: ELSE IF %z=34 THEN: ELSE PRINT "3"'; + statement = new Statement(src); + token = statement.nextToken(); // move to IF + t.is(text(token), 'IF'); + hasThen = statement.peekStatementContains('THEN'); + + t.is(hasThen, true, 'first IF contains THEN'); + + src = '10 ELSE IF %z=34 THEN: ELSE PRINT "3"'; + statement = new Statement(src); + token = statement.nextToken(); // move to ELSE + t.is(text(token), 'ELSE'); + hasThen = statement.peekStatementContains('THEN'); + t.is(hasThen, true, 'first IF contains THEN'); + + src = '20 ELSE IF 1 < 2 PRINT "ELSE"'; + statement = new Statement(src); + token = statement.nextToken(); // move to ELSE + t.is(text(token), 'ELSE'); + hasThen = statement.peekStatementContains('THEN'); + t.is(hasThen, false, 'first IF does not contain THEN'); +}); diff --git a/__tests__/txt2bas.test.js b/__tests__/txt2bas.test.js index 1b2c8c5..62ab876 100644 --- a/__tests__/txt2bas.test.js +++ b/__tests__/txt2bas.test.js @@ -101,9 +101,9 @@ test('comments', (t) => { }); test('end with $', (t) => { - let src = '202 IF INKEY$="s"'; + let src = '202 IF INKEY$="s" THEN'; let expect = [ - 0x00, 0xca, 0x07, 0x00, 0xfa, 0xa6, 0x3d, 0x22, 0x73, 0x22, 0x0d, + 0x00, 0xca, 0x08, 0x00, 0xfa, 0xa6, 0x3d, 0x22, 0x73, 0x22, 0xcb, 0x0d, ]; const res = Array.from(parseLine(src)); @@ -293,13 +293,16 @@ test('INT function', (t) => { t.is(token.name, 'NUMBER'); }); -test('ELSE IF', (t) => { +test('ELSE IF (2.08)', (t) => { let src, res; + src = '20 ELSE IF 1 < 2 PRINT "ELSE"'; res = parseLines(src).statements[0]; + console.log(res.tokens[1]); + t.is(res.tokens[0].text, 'ELSE', 'has else'); - t.is(res.tokens[1].text, 'IF', 'then if'); + t.is(res.tokens[1].text, 'IF', 'then "special" if'); t.is(res.tokens[1].value, 0x83, 'correct IF value for following ELSE'); }); diff --git a/cli/index.js b/cli/index.js index 00193dc..122e944 100755 --- a/cli/index.js +++ b/cli/index.js @@ -222,7 +222,7 @@ function help(type) { console.log(''); if (type === 'txt') { console.log(' -f 3dos|tap ... set the output format'); - console.log(' -t ............ parse and validate the NextBASIC'); + console.log(' -t ............ test and validate the NextBASIC'); console.log(' -bank ......... output LOAD "file" BANK format'); console.log(' -C ............ strip comments from output'); console.log(' -define........ support #define constant transforms'); diff --git a/codes.js b/codes.js index 9c8849f..1cd686a 100644 --- a/codes.js +++ b/codes.js @@ -9,7 +9,7 @@ export default { // new in 2.08 0x81: 'TIME', 0x82: 'PRIVATE', - 0x83: 'IF', // internal use only: displays as IF (which is managed here), but is converted from 0x83 via the aliases in op-table + 0x83: 'IF', // internal use only: displays as IF (which is managed here), but is converted from 0x83 via the aliases in op-table as 'ELSE IF' 0x84: 'ENDIF', 0x85: 'EXIT', 0x86: 'REF', diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..6b6e395 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,15 @@ +type Token = { + name: string; + value: number | string; + text: string; + numeric: number; + integer: boolean; +}; + +type ParsedBasic = { + basic: Uint8Array; + line: string; + length: number; + lineNumber: number; + tokens: Token[]; +}; diff --git a/txt2bas/index.js b/txt2bas/index.js index aea8aa1..5b5c176 100644 --- a/txt2bas/index.js +++ b/txt2bas/index.js @@ -5,6 +5,7 @@ import tests from '../chr-tests'; import { TEXT } from '../unicode'; import { validateLineNumber, validateStatement } from './validator'; +import * as parser from '../parser-version'; import { DEFINE, @@ -25,6 +26,7 @@ import { DEFFN_SIG, DEF_FN_ARG, IF, + ELSEIF, DEFFN_ARGS, NUMBER_DATA, KEYWORD, @@ -561,8 +563,76 @@ export class Statement { return token; } + /** + * + * @param {Token} token + * @returns {Token} + */ manageTokenState(token) { - if (!token) return; + // if !token, it could be that it wasn't recognised + if (!token) { + // console log the state? + return; + } + + // TODO if the token isn't recognised, try to get the DEF FN or other + if (token.value === 'DEF') { + // this is more likely to be a DEF FN - so let's peek the next token + const peek = this.peekToken(this.pos); + if (peek.text === 'FN') { + token = { + name: KEYWORD, + text: 'DEF FN', + value: opTable['DEF FN'], + pos: this.pos, + }; + this.pos = peek.pos; // move to the end of DEF FN + } + } + + if (token.value === 'OPEN' || token.value === 'CLOSE') { + // this is more likely to be a OPEN # or CLOSE # + const peek = this.peek(); + + if (peek === '#') { + token = { + name: KEYWORD, + text: `${token.value} ${peek}`, + value: opTable[`${token.value} ${peek}`], + pos: this.pos, + }; + this.pos += 2; // allow for the space + } + } + + if (token.value === 'GO') { + // this is more likely to be a GO TO or GO SUB + const peek = this.peekToken(this.pos); + // note: peek.value when it's an identify + // peek.text when it's a keyword + if (peek.value === 'SUB' || peek.text === 'TO') { + token = { + name: KEYWORD, + text: `GO ${peek.text || peek.value}`, + value: opTable[`GO ${peek.text || peek.value}`], + pos: this.pos, + }; + this.pos = peek.pos; // move to the end of DEF FN + } + } + + if (parser.getParser() >= parser.v208 && token.text === 'IF') { + // look for ELSE before hand + const hasThen = this.peekStatementContains('THEN'); + if (!hasThen) { + token = { + name: KEYWORD, + text: codes[opTable[ELSEIF]], + value: opTable[ELSEIF], + pos: this.pos, + }; + } + } if (token.name !== WHITE_SPACE) { if (token.value !== '%') this.next = null; // always reset @@ -631,6 +701,10 @@ export class Statement { this.in.push(IF); } + if (token.value === opTable.ELSEIF) { + this.in.push(ELSEIF); + } + if (token.value === opTable.UNTIL) { this.in.push(UNTIL); } @@ -640,6 +714,11 @@ export class Statement { this.inIntExpression = false; } + if (token.value === opTable.ENDIF) { + this.popTo(ELSEIF); + this.inIntExpression = false; + } + if (token.value === opTable.BIN) { this.next = BINARY; } @@ -656,7 +735,7 @@ export class Statement { } if (token.value === '=') { - if (!this.isIn(IF) && !this.isIn(UNTIL)) { + if (!this.isIn(IF) && !this.isIn(ELSEIF) && !this.isIn(UNTIL)) { this.inIntExpression = false; } } @@ -763,33 +842,52 @@ export class Statement { } peekToken(at = this.pos) { - let pos = at + 1; - const start = at; - while ( - pos < this.line.length && - !tests._isSpace(this.line.charAt(pos)) && - !tests._isDigit(this.line.charAt(pos)) - ) { - pos++; + // cache state + const cache = { + pos: this.pos, + inIntExpression: this.inIntExpression, + lastToken: this.lastToken, + tokens: Array.from(this.tokens), + }; + + this.pos = at; + + /** @type {Token} */ + let token = this.token(); + + if (!token) { + return null; } - return this.line.substring(start, pos); + + if (token.name === WHITE_SPACE) { + token = this.token(); + } + + let tokenPosition = this.pos; + + // restore state + this.pos = cache.pos; + this.inIntExpression = cache.inIntExpression; + this.lastToken = cache.lastToken; + this.tokens = cache.tokens; + + return { ...token, pos: tokenPosition }; } - 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--; + // mostly only used to search for THEN + peekStatementContains(keyword) { + let frag = this.line.substring(this.pos); + + frag = frag.replace(/".*?"/g, '').trim(); + frag = frag.split(/\b/).map((_) => _.trim().toUpperCase()); + let end = frag.indexOf(':'); + if (end === -1) { + end = undefined; } - return this.line.substring(pos, end); + frag = frag.slice(0, end); + + // strip out any quoted strings + return frag.includes(keyword); } findOpCode(endPos) { @@ -805,27 +903,6 @@ 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` - else if (peek === ' ' && !opTable[curr]) { - const next = this.peekToken(endPos + 1).toUpperCase(); - const test = `${curr} ${next}`; - - if (opTable[test]) { - curr = test; - endPos = endPos + 1 + next.length; - } else { - return false; - } - } - if (opTable[curr] !== undefined) { const token = { name: KEYWORD, diff --git a/txt2bas/types.js b/txt2bas/types.js index eb98d67..3968c82 100644 --- a/txt2bas/types.js +++ b/txt2bas/types.js @@ -6,6 +6,7 @@ export const IDENTIFIER = 'IDENTIFIER'; export const DEFFN = 'DEFFN'; export const DEFFN_SIG = 'DEFFN_SIG'; export const IF = 'IF'; +export const ELSEIF = 'ELSE IF'; export const FOR = 'FOR'; export const OUTER_IF = 'OUTER_IF'; export const DEFFN_ARGS = 'DEFFN_ARGS';