Fix five defects the port uncovered in the reference parser and scanner
Chained subtraction and exponentiation fold left instead of abandoning the rest of the line -- `1 - 2 - 3` gave -1 and dropped the `- 3`. A unary leaf's operand moves to .left, so a negative literal is one argument again and a second argument no longer overwrites it. An operator in a line's final column survives. Hex literals reach the parser whole. A leading zero is padding, not a radix. Item 16 stays pinned: the fix works and costs an upstream golden case, which is a decision rather than a defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,14 +36,20 @@ int main(void)
|
||||
TEST_REQUIRE_INT(LEAVES[1].literal_int, 255);
|
||||
|
||||
/*
|
||||
* A leading zero selects base 8, so 010 is 8 and 08 will not parse.
|
||||
* Commodore BASIC has no octal literals; this is TODO.md section 12 item 10,
|
||||
* reproduced deliberately and pinned here so the eventual fix is a
|
||||
* deliberate change to this assertion.
|
||||
* A leading zero is padding, not a radix -- TODO.md section 6 item 10's
|
||||
* regression test. The reference selects base 8 for any lexeme starting with
|
||||
* '0' (basicgrammar.go:224), so 010 was 8 and 08 would not parse at all.
|
||||
* Commodore BASIC has no octal literals.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[1], "010"));
|
||||
TEST_REQUIRE_INT(LEAVES[1].literal_int, 10);
|
||||
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[1], "08"));
|
||||
TEST_REQUIRE_INT(LEAVES[1].literal_int, 8);
|
||||
TEST_REQUIRE_ANY_ERROR(akbasic_leaf_new_literal_int(&LEAVES[1], "08"));
|
||||
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[1], "0"));
|
||||
TEST_REQUIRE_INT(LEAVES[1].literal_int, 0);
|
||||
/* 0x still selects base 16, which is the one prefix that means anything. */
|
||||
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[1], "0x10"));
|
||||
TEST_REQUIRE_INT(LEAVES[1].literal_int, 16);
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_leaf_new_literal_float(&LEAVES[2], "2.5"));
|
||||
TEST_REQUIRE_FEQ(LEAVES[2].literal_float, 2.5);
|
||||
|
||||
@@ -10,72 +10,37 @@
|
||||
* When one of these is fixed, this test starts passing and CTest reports
|
||||
* "unexpectedly passed". That is the cue to split the fixed assertion out into
|
||||
* the corresponding suite in AKBASIC_TESTS and strike the item from TODO.md
|
||||
* section 12.
|
||||
* section 6.
|
||||
*
|
||||
* Five of the six this file was written with are gone that way. What is left is
|
||||
* the one whose fix is not a coding question but a decision, and it is described
|
||||
* where it is asserted.
|
||||
*/
|
||||
|
||||
#include "harness.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akbasic_ASTLeaf *leaf = NULL;
|
||||
akbasic_Value *out = NULL;
|
||||
char rendered[AKBASIC_MAX_STRING_LENGTH];
|
||||
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
|
||||
/*
|
||||
* 12.12 -- subtraction stops after one operator, so `1 - 2 - 3` parses as
|
||||
* `1 - 2` and abandons the rest of the line. It should fold left, the way
|
||||
* addition does.
|
||||
*/
|
||||
TEST_REQUIRE_OK(harness_parse("A# = 1 - 2 - 3", &leaf));
|
||||
TEST_REQUIRE_OK(akbasic_leaf_to_string(leaf, rendered, sizeof(rendered)));
|
||||
TEST_REQUIRE_STR(rendered, "( A# (- (- 1 2) 3))");
|
||||
|
||||
/*
|
||||
* 12.13 -- a unary-minus argument inflates the arity count, because the
|
||||
* counter walks .right and a unary leaf keeps its operand there. ABS(-9)
|
||||
* should be one argument.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||
TEST_REQUIRE_OK(harness_parse("A# = ABS(-9)", &leaf));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||||
TEST_REQUIRE_INT(out->intval, 9);
|
||||
|
||||
/*
|
||||
* 12.14 -- a comparison operator in the final column of a line is dropped,
|
||||
* because matchNextChar returns without setting a token type when it cannot
|
||||
* peek past the end.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0));
|
||||
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nexttoken, 2);
|
||||
|
||||
/*
|
||||
* 12.15 -- hex literals do not survive the scanner. matchNumber lets 'x'
|
||||
* through but not the digits after it, so the parser's base-16 branch is
|
||||
* unreachable.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0));
|
||||
TEST_REQUIRE_STR(HARNESS_RUNTIME.environment->tokens[1].lexeme, "0xff");
|
||||
|
||||
/*
|
||||
* 12.16 -- "Reserved word in variable name" never fires, because the lexeme
|
||||
* still carries its type suffix when the verb table is searched. PRINT$
|
||||
* should be refused as a variable name.
|
||||
* 6.16 -- "Reserved word in variable name" never fires, because the lexeme
|
||||
* still carries its type suffix when the verb table is searched, so PRINT$
|
||||
* misses every table and becomes an ordinary string variable.
|
||||
*
|
||||
* Still here rather than fixed, and the reason is worth knowing before
|
||||
* anybody "just fixes it": stripping the suffix before the lookup is four
|
||||
* lines and it works, and it costs a golden case. The reference's own
|
||||
* examples/strreverse.bas names a variable INPUT$ -- which a real C128 would
|
||||
* refuse too, so the reference program is the wrong one. But the corpus is
|
||||
* the acceptance contract and it lives in a submodule this repository may
|
||||
* not edit, so the diagnostic and the corpus cannot both be right. TODO.md
|
||||
* section 6 records the trade rather than making it silently.
|
||||
*/
|
||||
HARNESS_RUNTIME.hasError = false;
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0));
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.hasError, "PRINT$ should be refused as a variable name");
|
||||
|
||||
/*
|
||||
* 12.10 -- a leading zero selects base 8, so `PRINT 010` prints 8 and
|
||||
* `PRINT 08` will not parse. Commodore BASIC has no octal literals.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||
TEST_REQUIRE_OK(harness_parse("A# = 010", &leaf));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||||
TEST_REQUIRE_INT(out->intval, 10);
|
||||
|
||||
harness_stop();
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
10 REM The reference selects base 8 for any lexeme starting with 0, so 010 is 8
|
||||
20 REM and 08 is a parse error. Commodore BASIC has no octal literals -- this is
|
||||
30 REM TODO.md section 6 item 10, reproduced deliberately until it is fixed on
|
||||
40 REM purpose. Pinned here so the eventual fix has to change a golden file.
|
||||
50 PRINT 010
|
||||
60 PRINT 08
|
||||
10 REM A leading zero is padding, not a radix. The reference selects base 8 for
|
||||
20 REM any lexeme starting with 0, so 010 printed 8 and 08 was a parse error --
|
||||
30 REM TODO.md section 6 item 10, fixed. Commodore BASIC has no octal literals.
|
||||
40 REM 0x is the one prefix that changes the base, and it now reaches the scanner
|
||||
50 REM whole: that was section 6 item 15.
|
||||
60 PRINT 010
|
||||
70 PRINT 08
|
||||
80 PRINT 0xff
|
||||
90 PRINT 0x10 + 1
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
10
|
||||
8
|
||||
? 60 : PARSE ERROR trailing junk "8" after the number in "08"
|
||||
|
||||
255
|
||||
17
|
||||
|
||||
@@ -73,19 +73,18 @@ int main(void)
|
||||
/* AND and OR sit below comparison. */
|
||||
expect_tree("A# = 1 == 1 AND 2 == 2", "( A# (AND (= 1 1) (= 2 2)))");
|
||||
|
||||
/* Exponent. */
|
||||
expect_tree("A# = 2 ^ 3", "( A# (^ 2 3))");
|
||||
|
||||
/*
|
||||
* Addition loops over its operator; subtraction returns after one. So
|
||||
* `1 + 2 + 3` folds left, while `1 - 2 - 3` parses only `1 - 2` and leaves
|
||||
* `- 3` in the token stream for the statement loop to pick up as a second
|
||||
* statement. That silently computes the wrong answer -- TODO.md section 12
|
||||
* item 12 -- and is pinned here so the fix is a deliberate change to this
|
||||
* assertion and not an accident.
|
||||
* Every same-precedence run folds left, which is what a C128 does and what
|
||||
* TODO.md section 6 item 12 was about: the reference returns after one
|
||||
* operator in subtraction() and exponent(), so `1 - 2 - 3` computed `1 - 2`
|
||||
* and left `- 3` in the token stream for the statement loop to evaluate and
|
||||
* discard. A wrong answer, silently. These four are the regression test.
|
||||
*/
|
||||
expect_tree("A# = 1 + 2 + 3", "( A# (+ (+ 1 2) 3))");
|
||||
expect_tree("A# = 1 - 2 - 3", "( A# (- 1 2))");
|
||||
expect_tree("A# = 1 - 2 - 3", "( A# (- (- 1 2) 3))");
|
||||
expect_tree("A# = 10 - 1 - 2 - 3", "( A# (- (- (- 10 1) 2) 3))");
|
||||
expect_tree("A# = 2 ^ 3", "( A# (^ 2 3))");
|
||||
expect_tree("A# = 2 ^ 3 ^ 2", "( A# (^ (^ 2 3) 2))");
|
||||
|
||||
/* An array reference parses as an identifier carrying a subscript list. */
|
||||
TEST_REQUIRE_OK(harness_parse("A#(2) = 1", &leaf));
|
||||
|
||||
@@ -109,18 +109,29 @@ int main(void)
|
||||
test_discard_error(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Built-in functions dispatch through the table.
|
||||
*
|
||||
* Note the arguments are never negative *literals*: a unary-minus leaf uses
|
||||
* its .right for the operand, and the parser counts arity by walking .right,
|
||||
* so `ABS(-9)` reports two arguments and is rejected. TODO.md section 12
|
||||
* item 13. The golden corpus sidesteps it the same way -- sgn.bas assigns
|
||||
* -1 to a variable first.
|
||||
*/
|
||||
/* Built-in functions dispatch through the table. */
|
||||
expect_int("N# = -9", -9);
|
||||
expect_int("A# = ABS(N#)", 9);
|
||||
expect_int("A# = SGN(N#)", -1);
|
||||
|
||||
/*
|
||||
* A negative *literal* argument, which is TODO.md section 6 item 13's
|
||||
* regression test. The reference hangs a unary leaf's operand off .right,
|
||||
* which is also where an argument list chains its arguments, so `ABS(-9)`
|
||||
* counted as two arguments and was refused outright -- no builtin could be
|
||||
* called with a negative literal at all. The corpus hid it: sgn.bas assigns
|
||||
* -1 to a variable first, which is what the three lines above do.
|
||||
*/
|
||||
expect_int("A# = ABS(-9)", 9);
|
||||
expect_int("A# = SGN(-5)", -1);
|
||||
/*
|
||||
* And with a second argument after the unary one, which is the other half of
|
||||
* the same defect: chaining the next argument through .right overwrote the
|
||||
* operand the unary leaf was keeping there.
|
||||
*/
|
||||
expect_int("A# = SHL(-1, 0)", -1);
|
||||
expect_int("A# = MOD(-7, 3)", -1);
|
||||
expect_int("A# = INSTR(\"HELLO\", \"L\")", 2);
|
||||
expect_int("A# = LEN(\"HELLO\")", 5);
|
||||
expect_int("A# = SHL(1, 4)", 16);
|
||||
expect_int("A# = SHR(16, 4)", 1);
|
||||
|
||||
@@ -43,13 +43,7 @@ int main(void)
|
||||
expect_token(6, AKBASIC_TOK_RIGHT_PAREN, ")");
|
||||
expect_token(7, AKBASIC_TOK_COMMA, ",");
|
||||
|
||||
/*
|
||||
* Two-character comparisons, and the one-character fallbacks. The trailing
|
||||
* space is not decoration: matchNextChar cannot peek past the end of the
|
||||
* line, and when it fails it returns without setting a token type, so a
|
||||
* comparison operator in the final column is silently dropped. TODO.md
|
||||
* section 12 item 14.
|
||||
*/
|
||||
/* Two-character comparisons, and the one-character fallbacks. */
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "<= <> >= < > == = ", NULL, 0));
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 7);
|
||||
expect_token(0, AKBASIC_TOK_LESS_THAN_EQUAL, NULL);
|
||||
@@ -60,9 +54,25 @@ int main(void)
|
||||
expect_token(5, AKBASIC_TOK_EQUAL, NULL);
|
||||
expect_token(6, AKBASIC_TOK_ASSIGNMENT, NULL);
|
||||
|
||||
/* Without the trailing space the final operator vanishes. */
|
||||
/*
|
||||
* An operator in the final column survives, with no trailing space to help
|
||||
* it. matchNextChar cannot peek past the end of the line, and the reference
|
||||
* returns from that without setting a token type (basicscanner.go:272), so
|
||||
* `A# =` produced one token and the parse error that followed pointed
|
||||
* somewhere else. TODO.md section 6 item 14; this is its regression test.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0));
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 1);
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 2);
|
||||
expect_token(0, AKBASIC_TOK_IDENTIFIER_INT, "A#");
|
||||
expect_token(1, AKBASIC_TOK_ASSIGNMENT, "=");
|
||||
|
||||
/* The same for the other two, each alone in the final column. */
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# <", NULL, 0));
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 2);
|
||||
expect_token(1, AKBASIC_TOK_LESS_THAN, "<");
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# >", NULL, 0));
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 2);
|
||||
expect_token(1, AKBASIC_TOK_GREATER_THAN, ">");
|
||||
|
||||
/* Literals. */
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 1.5", NULL, 0));
|
||||
@@ -70,13 +80,25 @@ int main(void)
|
||||
expect_token(1, AKBASIC_TOK_LITERAL_FLOAT, "1.5");
|
||||
|
||||
/*
|
||||
* A hex literal does not survive the scanner: matchNumber allows 'x' through
|
||||
* but not the hex digits after it, so "0xff" lexes as "0x" and the "ff" is
|
||||
* scanned as an identifier. The parser's base-16 branch is therefore
|
||||
* unreachable. TODO.md section 12 item 15.
|
||||
* A hex literal survives the scanner whole. The reference lets 'x' through
|
||||
* but stops at the hex digits after it (basicscanner.go:318), so "0xff"
|
||||
* lexed as "0x" plus an identifier "ff", the parser's base-16 branch was
|
||||
* unreachable and the README's hex support did not exist. TODO.md section 6
|
||||
* item 15; this is its regression test.
|
||||
*/
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0));
|
||||
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0x");
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 2);
|
||||
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0xff");
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xDEAD", NULL, 0));
|
||||
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0xDEAD");
|
||||
|
||||
/* And the hex run ends where it should, rather than eating what follows. */
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0x10 + 1", NULL, 0));
|
||||
TEST_REQUIRE_INT(env()->nexttoken, 4);
|
||||
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0x10");
|
||||
expect_token(2, AKBASIC_TOK_PLUS, "+");
|
||||
expect_token(3, AKBASIC_TOK_LITERAL_INT, "1");
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"HELLO\"", NULL, 0));
|
||||
expect_token(1, AKBASIC_TOK_LITERAL_STRING, "HELLO");
|
||||
|
||||
Reference in New Issue
Block a user