Refuse an operand the numeric path cannot read

math_minus, math_multiply and math_divide were `if ( INTEGER ) ... else <treat
as float>`, and that else was a catch-all rather than a float branch: it read
floatval from whatever it was handed. A truth value keeps its payload in
boolvalue and leaves floatval zero, so a truth value on the left computed into a
field nothing reads and kept its BOOLEAN type --

    (A# == 1) - 1   printed true, should be -2
    (A# == 1) * 3   printed true, should be -3
    (A# == 1) + 1   correctly refused

wrong in value and in type, and silent. math_plus escaped only because it
enumerates its cases and ends in an error.

The three now share one require_numeric() guard, so a type added later is
refused by all of them at once instead of quietly taking the float branch in
each. The two operands have different rules and that asymmetry is the point: the
left one picks the branch and must be a number, while the right is read through
rval_as_int(), which handles -1/0 deliberately. `5 - (A# == 1)` is 6, and that
is the same property that lets AND and OR double as logical operators, so
refusing a truth value on the right would have broken every condition in the
language.

Found by asking what a structure operand would do to this path, which is where
AKBASIC_TYPE_STRUCT is about to arrive. The structures work did not create the
defect; it made an already-reachable one worth chasing.

Two stale comments went with it. src/value.c's header and a duplicate block
above rval_as_int both cited "TODO.md section 12", which does not exist -- the
defect list is section 6 -- and the duplicate described the summing behaviour
item 5 had already removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 11:22:50 -04:00
parent 3415c9aec6
commit 8117fb564d
5 changed files with 184 additions and 15 deletions

View File

@@ -249,6 +249,52 @@ int main(void)
TEST_REQUIRE_STATUS(akbasic_value_math_divide(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_invert(&A, &SCRATCH, &out), AKBASIC_ERR_TYPE);
/*
* A left operand the numeric path cannot read is refused, not computed.
*
* `math_minus`, `math_multiply` and `math_divide` were `if ( INTEGER ) ...
* else <treat as float>`, and that else was a catch-all: it read `floatval`
* from whatever it was handed. A truth value keeps its payload in
* `boolvalue` and leaves `floatval` zero, so the operation computed into a
* field nothing reads and left the type alone -- `(A# == 1) - 1` rendered
* `true` rather than -2. Silent, and wrong in both value and type.
*
* `math_plus` never had it, because it enumerates its cases and ends in an
* error. These assert the other three now agree with it.
*/
TEST_REQUIRE_OK(akbasic_value_set_bool(&A, true));
set_int(&B, 1);
TEST_REQUIRE_STATUS(akbasic_value_math_minus(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_math_divide(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_math_plus(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
/* An undefined value is refused for the same reason and by the same guard. */
TEST_REQUIRE_OK(akbasic_value_zero(&A));
TEST_REQUIRE_STATUS(akbasic_value_math_minus(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_math_divide(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
/*
* But a truth value on the *right* stays legal, and that asymmetry is the
* point: the left operand picks the branch and must be a number, while the
* right is read through rval_as_int(), which handles -1/0 on purpose. It is
* the same property that lets AND and OR double as logical operators, so
* refusing it here would break `5 - (A# == 1)`, which is 6.
*/
set_int(&A, 5);
TEST_REQUIRE_OK(akbasic_value_set_bool(&B, true));
TEST_REQUIRE_OK(akbasic_value_math_minus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 6);
TEST_REQUIRE_OK(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, -5);
/* And a string repeat is not caught by the numeric guard in front of it. */
set_string(&A, "xy");
set_int(&B, 2);
TEST_REQUIRE_OK(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_STR(out->stringval, "xyxy");
/* Division by zero raises rather than trapping or panicking. */
set_int(&A, 1);
set_int(&B, 0);