Skip to content

Commit

Permalink
fix: ELSE and IF ELSE statement
Browse files Browse the repository at this point in the history
  • Loading branch information
remy committed Mar 3, 2024
1 parent 73ae895 commit 1706d14
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 52 deletions.
8 changes: 8 additions & 0 deletions __tests__/bas2txt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
61 changes: 61 additions & 0 deletions __tests__/peek-token.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
11 changes: 7 additions & 4 deletions __tests__/txt2bas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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');
});

Expand Down
2 changes: 1 addition & 1 deletion cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 15 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -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[];
};
169 changes: 123 additions & 46 deletions txt2bas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,6 +26,7 @@ import {
DEFFN_SIG,
DEF_FN_ARG,
IF,
ELSEIF,
DEFFN_ARGS,
NUMBER_DATA,
KEYWORD,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions txt2bas/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 1706d14

Please sign in to comment.