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:
@@ -185,14 +185,19 @@ akerr_ErrorContext *akbasic_leaf_new_binary(akbasic_ASTLeaf *obj, akbasic_ASTLea
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *right)
|
||||
akerr_ErrorContext *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *operand)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_unary");
|
||||
FAIL_ZERO_RETURN(errctx, (right != NULL), AKERR_NULLPOINTER, "nil pointer arguments");
|
||||
FAIL_ZERO_RETURN(errctx, (operand != NULL), AKERR_NULLPOINTER, "nil pointer arguments");
|
||||
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_UNARY));
|
||||
obj->right = right;
|
||||
/*
|
||||
* .left, not .right. An argument list chains its arguments through .right,
|
||||
* so an operand there is indistinguishable from a second argument: ABS(-9)
|
||||
* counted as two and was refused. See the note on the declaration.
|
||||
*/
|
||||
obj->left = operand;
|
||||
obj->operator_ = op;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -268,16 +273,14 @@ akerr_ErrorContext *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const cha
|
||||
FAIL_ZERO_RETURN(errctx, (lexeme[0] != '\0'), AKBASIC_ERR_VALUE, "Empty integer literal");
|
||||
|
||||
/*
|
||||
* The reference selects base 8 for any lexeme starting with '0', so `010`
|
||||
* parses as 8 and `08` is an error. Commodore BASIC has no octal literals
|
||||
* and this is TODO.md section 12 item 10 -- but it is reproduced here,
|
||||
* because "port the behaviour first" is the rule and the fix is its own
|
||||
* commit with its own test.
|
||||
* Base 10 unless the lexeme is prefixed `0x`. The reference selects base 8
|
||||
* for *any* lexeme starting with '0' (basicgrammar.go:224), so `PRINT 010`
|
||||
* printed 8 and `PRINT 08` was a parse error -- TODO.md section 6 item 10.
|
||||
* Commodore BASIC has no octal literals, and a leading zero in a listing is
|
||||
* padding, not a radix.
|
||||
*/
|
||||
if ( strlen(lexeme) > 2 && strncmp(lexeme, "0x", 2) == 0 ) {
|
||||
base = 16;
|
||||
} else if ( lexeme[0] == '0' ) {
|
||||
base = 8;
|
||||
}
|
||||
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_INT));
|
||||
PASS(errctx, aksl_strtoll(lexeme, NULL, base, &value));
|
||||
@@ -366,7 +369,7 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si
|
||||
snprintf(dest, len, "NOT IMPLEMENTED");
|
||||
break;
|
||||
case AKBASIC_LEAF_UNARY:
|
||||
PASS(errctx, akbasic_leaf_to_string(self->right, sub1, sizeof(sub1)));
|
||||
PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1)));
|
||||
snprintf(dest, len, "(%s %s)", operator_to_str(self->operator_), sub1);
|
||||
break;
|
||||
case AKBASIC_LEAF_BINARY:
|
||||
|
||||
36
src/parser.c
36
src/parser.c
@@ -349,10 +349,15 @@ akerr_ErrorContext *akbasic_parser_relation(akbasic_Parser *obj, akbasic_ASTLeaf
|
||||
}
|
||||
|
||||
/*
|
||||
* subtraction and exponent return after one operator where addition,
|
||||
* multiplication and division loop. That asymmetry is the reference's, not a
|
||||
* transcription slip: `1 - 2 - 3` parses as `1 - (2 - 3)` there. Reproduced,
|
||||
* because the golden corpus encodes the observed associativity.
|
||||
* Loops, where the reference returns after one operator (basicparser.go:329).
|
||||
* That asymmetry was reproduced faithfully at first and is TODO.md section 6
|
||||
* item 12: `1 - 2 - 3` computed `1 - 2` and abandoned the rest of the line,
|
||||
* which the statement loop then picked up as a second statement and discarded.
|
||||
* A wrong answer, silently -- the highest-impact item on that list.
|
||||
*
|
||||
* Left-associative, matching addition, multiplication and division here and
|
||||
* matching a C128, which evaluates a run of same-precedence operators left to
|
||||
* right.
|
||||
*/
|
||||
static akerr_ErrorContext *subtraction(akbasic_Parser *obj, akbasic_ASTLeaf **dest)
|
||||
{
|
||||
@@ -363,15 +368,16 @@ static akerr_ErrorContext *subtraction(akbasic_Parser *obj, akbasic_ASTLeaf **de
|
||||
akbasic_Token *operator_ = NULL;
|
||||
|
||||
PASS(errctx, addition(obj, &left));
|
||||
if ( akbasic_parser_match1(obj, AKBASIC_TOK_MINUS) ) {
|
||||
while ( akbasic_parser_match1(obj, AKBASIC_TOK_MINUS) ) {
|
||||
PASS(errctx, akbasic_parser_previous(obj, &operator_));
|
||||
PASS(errctx, addition(obj, &right));
|
||||
if ( expr != NULL ) {
|
||||
left = expr;
|
||||
}
|
||||
PASS(errctx, akbasic_parser_new_leaf(obj, &expr));
|
||||
PASS(errctx, akbasic_leaf_new_binary(expr, left, operator_->tokentype, right));
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
*dest = left;
|
||||
*dest = (expr != NULL ? expr : left);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -469,15 +475,21 @@ static akerr_ErrorContext *exponent(akbasic_Parser *obj, akbasic_ASTLeaf **dest)
|
||||
akbasic_Token *operator_ = NULL;
|
||||
|
||||
PASS(errctx, function_call(obj, &left));
|
||||
if ( akbasic_parser_match1(obj, AKBASIC_TOK_CARAT) ) {
|
||||
/*
|
||||
* Loops for the same reason subtraction does -- `2 ^ 3 ^ 2` abandoned the
|
||||
* second operator before this. Left-associative, which is a C128: 2^3^2 is
|
||||
* 64 there, not the 512 a right-associative reading gives.
|
||||
*/
|
||||
while ( akbasic_parser_match1(obj, AKBASIC_TOK_CARAT) ) {
|
||||
PASS(errctx, akbasic_parser_previous(obj, &operator_));
|
||||
PASS(errctx, function_call(obj, &right));
|
||||
if ( expr != NULL ) {
|
||||
left = expr;
|
||||
}
|
||||
PASS(errctx, akbasic_parser_new_leaf(obj, &expr));
|
||||
PASS(errctx, akbasic_leaf_new_binary(expr, left, operator_->tokentype, right));
|
||||
*dest = expr;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
*dest = left;
|
||||
*dest = (expr != NULL ? expr : left);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -453,7 +453,8 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
case AKBASIC_LEAF_UNARY:
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &rval));
|
||||
/* .left: a unary leaf's operand, kept clear of the argument chain. */
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, &rval));
|
||||
PASS(errctx, akbasic_environment_new_value(obj->environment, &scratch));
|
||||
if ( expr->operator_ == AKBASIC_TOK_MINUS ) {
|
||||
PASS(errctx, akbasic_value_invert(rval, scratch, dest));
|
||||
|
||||
@@ -377,8 +377,8 @@ static akerr_ErrorContext *parse_line_range(akbasic_Runtime *obj, akbasic_ASTLea
|
||||
}
|
||||
if ( expr->right->leaftype == AKBASIC_LEAF_UNARY &&
|
||||
expr->right->operator_ == AKBASIC_TOK_MINUS ) {
|
||||
/* -n: from the start through n. */
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right->right, &value));
|
||||
/* -n: from the start through n. A unary leaf's operand is on .left. */
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right->left, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
||||
"Expected a line number range");
|
||||
*endidx = value->intval;
|
||||
|
||||
@@ -103,6 +103,15 @@ static bool match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType tru
|
||||
char nc = '\0';
|
||||
|
||||
if ( !peek(obj, &nc) ) {
|
||||
/*
|
||||
* Nothing left to peek at, so the operator is whatever it is on its
|
||||
* own. The reference returns here *without* setting a type
|
||||
* (basicscanner.go:272), which silently drops a comparison operator in
|
||||
* the final column of a line: `A# =` produced one token, not two, and
|
||||
* the parse error that followed pointed somewhere else entirely.
|
||||
* TODO.md section 6 item 14.
|
||||
*/
|
||||
obj->tokentype = falsetype;
|
||||
return false;
|
||||
}
|
||||
if ( nc == cm ) {
|
||||
@@ -144,6 +153,7 @@ static akerr_ErrorContext *match_number(akbasic_Runtime *obj)
|
||||
char nc = '\0';
|
||||
int64_t lineno = 0;
|
||||
long long converted = 0;
|
||||
bool hex = false;
|
||||
|
||||
obj->tokentype = AKBASIC_TOK_LITERAL_INT;
|
||||
while ( !is_at_end(obj) ) {
|
||||
@@ -156,8 +166,27 @@ static akerr_ErrorContext *match_number(akbasic_Runtime *obj)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
obj->tokentype = AKBASIC_TOK_LITERAL_FLOAT;
|
||||
} else if ( !isdigit((unsigned char)c) && c != 'x' ) {
|
||||
/* 'x' is allowed through so 0x-prefixed hex reaches the parser. */
|
||||
} else if ( c == 'x' && !hex ) {
|
||||
/*
|
||||
* An 'x' in a number run. The reference lets it through and then
|
||||
* stops at the first hex digit after it (basicscanner.go:318), so
|
||||
* `0xff` lexed as `0x` followed by 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. Once the 'x' is seen, keep
|
||||
* going over hex digits.
|
||||
*
|
||||
* Accepted anywhere in the run rather than only after a leading `0`,
|
||||
* which is the reference's rule and is worth keeping: it is what
|
||||
* makes `1x2` one malformed token that the line-number conversion
|
||||
* then diagnoses by name, instead of two tokens that fail somewhere
|
||||
* less helpful.
|
||||
*/
|
||||
hex = true;
|
||||
} else if ( hex ) {
|
||||
if ( !isxdigit((unsigned char)c) ) {
|
||||
break;
|
||||
}
|
||||
} else if ( !isdigit((unsigned char)c) ) {
|
||||
break;
|
||||
}
|
||||
obj->current += 1;
|
||||
@@ -231,6 +260,12 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj)
|
||||
}
|
||||
|
||||
PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme)));
|
||||
/*
|
||||
* Searched with the type suffix still attached, which is the reference's
|
||||
* rule (basicscanner.go:349) and is why the "Reserved word in variable name"
|
||||
* branch below is unreachable -- TODO.md section 6 item 16, which explains
|
||||
* why it is still here rather than fixed.
|
||||
*/
|
||||
PASS(errctx, akbasic_verb_lookup(lexeme, &verb));
|
||||
|
||||
ATTEMPT {
|
||||
|
||||
Reference in New Issue
Block a user