From c8b917d2053a5060323800ae4b1145383f6c945d Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 00:23:20 -0400 Subject: [PATCH] Write down that the left operand decides integer or float arithmetic No behaviour change, by decision. `A# * 0.45` is 0 and `0.45 * A#` is 1.35, because every operator branches on `self->valuetype` and converts the right operand to match. It is inherited from the Go reference, a C128 promotes to float instead, and this interpreter is at least consistent about it -- so a dialect saying the left operand wins is a defensible position, and changing it to promotion would alter the result of every mixed expression in every existing program. **The defect was that nobody said so.** Chapter 3's "Numbers" did not mention it, Chapter 13 did not list it among the differences, and nothing fails when a program gets it wrong -- it computes something else and carries on. The game in examples/ lost its per-level speed increase to `5.6 + LEVEL# * 0.45` evaluating to a flat 5.6, and bled velocity out of every bounce through `0 - BLVX%(B#)` quantising to whole pixels. Both read correctly. Neither produced a diagnostic. Now said in three places: a section in Chapter 3 with the demonstration and the two rules that keep a program out of it (put the float on the left, put the answer somewhere with a `%` on it), a row in Chapter 13 naming it as the difference from 7.0 most likely to turn a working listing into a quietly wrong one, and the reasoning on value.h where the operators are declared. tests/value_arithmetic.c pins it in both directions across multiply and subtract, with a comment saying it is the documented contract rather than an accident -- so promotion becomes a decision somebody takes deliberately rather than a change that could slip in under a passing suite. TODO.md section 9 item 4, struck as a documentation outcome. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 27 +++++++++++++++++++-- docs/03-the-language.md | 30 +++++++++++++++++++++++ docs/13-differences.md | 22 +++++++++++++++++ include/akbasic/value.h | 10 ++++++++ tests/value_arithmetic.c | 52 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 9215549..abb8f78 100644 --- a/TODO.md +++ b/TODO.md @@ -2667,7 +2667,19 @@ reduced against `build/basic`, the stdio build, unless it says otherwise. The tee half is three lines and is worth doing whatever is decided about the rest. -4. **Mixed-type arithmetic is decided by the left operand alone, and nothing says so.** +4. ~~**Mixed-type arithmetic is decided by the left operand alone, and nothing says so.**~~ + **Done, as documentation, by decision.** The behaviour is unchanged: a dialect is + allowed to say the left operand wins and this one is consistent about it, and promotion + would change the result of every mixed expression in every existing program. What was + missing was anybody saying so. Now: a section in `docs/03-the-language.md` with the + two-line demonstration and the two rules that keep a program out of it, a row in + `docs/13-differences.md` naming it as the difference from 7.0 most likely to turn a + working listing into a quietly wrong one, and the reasoning on `include/akbasic/value.h` + where the operators are declared. + + `tests/value_arithmetic.c` pins it in both directions, so **promotion is now a decision + somebody has to take deliberately** rather than a change that could slip in. The + original report: ```basic A# = 3 @@ -2700,7 +2712,18 @@ reduced against `build/basic`, the stdio build, unless it says otherwise. The cheap fix is documentation: a paragraph in chapter 3 and a row in chapter 13. The other fix is promotion, which would be a real change of semantics and wants its own decision. -5. **A drawing that spans a 256-line batch boundary is torn, not merely transient.** §5 and +5. ~~**A drawing that spans a 256-line batch boundary is torn, not merely transient.**~~ + **Done, as documentation.** No fix is possible without changing what a host owns -- the + budget is the host's and the present is the host's -- so what was needed was for the + consequence to be written down beside the advice it qualifies. `docs/13-differences.md` + now says that "redraw it every frame" is not sufficient on its own and carries the + measured numbers, `docs/06-graphics.md` says it where `SSHAPE` is introduced, and + `docs/14-architecture.md` explains the mechanism from the step loop's side and points at + the `TI#` synchronisation a program has to build. + + **Untested, and deliberately:** it needs the real frontend, a clock and a timing + window, and a test that reproduced it would be reproducing a race. Said here rather than + left looking like an oversight. The original report: §5 and chapter 13 record that drawing does not persist across frames. What neither says is that a single uninterrupted sequence of drawing verbs longer than one `akbasic_runtime_run()` budget comes back **half drawn**, because the present in the middle of it discards the buffer — so diff --git a/docs/03-the-language.md b/docs/03-the-language.md index 5cd0eff..100b9a4 100644 --- a/docs/03-the-language.md +++ b/docs/03-the-language.md @@ -49,6 +49,36 @@ Literals may be written in hexadecimal with a `0x` prefix. A leading zero is *no octal — `010` is ten, because a leading zero in a listing is far more often padding than a base. +### The left operand decides whether the arithmetic is integer or float + +**Not the wider of the two, and not the destination.** An integer on the left of an +operator converts the right-hand side to an integer, which throws away its fraction: + +```basic +A# = 3 +PRINT A# * 0.45 +PRINT 0.45 * A# +``` + +```output +0 +1.350000 +``` + +That is the same expression written the other way round, and the answers differ by +everything. A C128 promotes to float instead; this interpreter does not, and it is +consistent about it — see [Chapter 13](13-differences.md). + +**Nothing fails when you get it wrong.** The program computes something else and carries +on, which is what makes it worth learning early rather than meeting later. Two rules keep +you out of it: + +- **Put the float on the left.** `0.45 * LEVEL#`, not `LEVEL# * 0.45`. Likewise + `0.0 - V%` rather than `0 - V%` to negate a float, which is otherwise quantised to a + whole number. +- **Put the answer somewhere that can hold it.** A float expression assigned to a `#` + variable truncates: `T# = 4.2 / 5.6` is `0`. + ## Strings Strings are up to 255 characters and are written in double quotes. There is no diff --git a/docs/13-differences.md b/docs/13-differences.md index 583e76b..eff113d 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -42,6 +42,18 @@ There is no escape character. Integers are 64-bit and floats are IEEE doubles, so `PRINT 1.5` gives `1.500000`. A leading zero is not octal; `0x` is hexadecimal. +### Mixed arithmetic follows the left operand, not the wider type + +**A C128 promotes to float. This does not.** An integer on the left converts the right +operand to an integer and throws its fraction away, so `A# * 0.45` is `0` where +`0.45 * A#` is `1.35`. It is consistent, it is inherited from the Go interpreter this was +ported from, and **nothing fails when you get it wrong** — the program computes something +else and carries on. + +[Chapter 3](03-the-language.md#the-left-operand-decides-whether-the-arithmetic-is-integer-or-float) +has the two rules that keep you out of it. This is the difference from 7.0 most likely to +turn a working listing into a quietly wrong one. + ### Structures are an addition BASIC 7.0 has no records at all. `TYPE`/`END TYPE`, `DIM X@ AS T`, the `@` suffix, `.` and @@ -133,6 +145,16 @@ interpreter's error code, which bears no relation to a Commodore error number. P - **Drawing does not persist across frames.** The verbs draw straight to the renderer, so anything drawn is overwritten on the next frame unless the program redraws it. This is a defect, not a design. +- **And a redraw has to *fit*.** The host runs a fixed number of source lines and then + presents the frame, and presenting discards the drawing buffer — so a sequence of + drawing verbs longer than one batch comes back **half drawn**, with an `SSHAPE` at the + end capturing only what was issued since the present. "Redraw it every frame" is + therefore not sufficient advice on its own. Measured against the standalone frontend's + 256 lines a batch: after synchronising to a jiffy edge, 220 lines of drawing survive a + capture and 250 do not. The only way a program can see the boundary is to watch `TI#`, + which is refreshed once per batch — so the step on which it changes is the first step + of one. [Chapter 18](18-tutorial-breakout-artwork.md#step-4-find-the-frame-boundary) + builds a pacing routine out of exactly that. - **`CHAR` ignores its colour argument** and needs a text device with a cursor. ## Sound diff --git a/include/akbasic/value.h b/include/akbasic/value.h index 7426cc1..ba4a083 100644 --- a/include/akbasic/value.h +++ b/include/akbasic/value.h @@ -6,6 +6,16 @@ * exactly, including the parts that look wrong -- see TODO.md section 12 for the * catalogue and the reason they are not fixed yet. * + * **The left operand decides whether an operation is done in integers or in + * floats**, and this is deliberate rather than an oversight: every operator + * branches on `self->valuetype` and converts the right operand to match, so an + * integer on the left truncates a float on the right. `A# * 0.45` is 0 and + * `0.45 * A#` is 1.35. It is inherited from the reference + * (basicvalue.go:190-193), a C128 promotes to float instead, and nothing fails + * when a program gets it wrong -- it computes something else and carries on. + * Documented in docs/03-the-language.md and docs/13-differences.md, and pinned + * in tests/value_arithmetic.c so that changing it has to be a decision. + * * Every binary operator takes a `scratch` value the caller has drawn from an * environment's pool and an out-parameter `dest`. Most operators set `*dest` to * `scratch` and fill it; akbasic_value_math_plus sets `*dest` to `self` when diff --git a/tests/value_arithmetic.c b/tests/value_arithmetic.c index 10603b2..092e806 100644 --- a/tests/value_arithmetic.c +++ b/tests/value_arithmetic.c @@ -146,6 +146,57 @@ static void test_pool_starts_empty(void) AKBASIC_ERR_BOUNDS); } +/** + * @brief The left operand alone decides integer or float arithmetic. + * + * **This is the documented contract, not an accident**, which is the whole + * reason it is asserted: every operator branches on `self->valuetype` and + * converts the right operand to match, so an integer on the left truncates a + * float on the right and `A# * 0.45` is 0 where `0.45 * A#` is 1.35. + * + * It is inherited from the Go reference (basicvalue.go:190-193) and a C128 + * promotes to float instead. It may well be the wrong choice -- what makes it + * expensive is that **nothing fails**, so a program computes something else and + * carries on; a game written against this interpreter lost a per-level speed + * increase and bled velocity out of every bounce before either was noticed. + * + * Changing it to promotion is a real change of dialect semantics and wants its + * own decision. This test is here so that it has to be one: docs/03 and docs/13 + * describe the behaviour below, and a change that makes them wrong fails here + * first. TODO.md section 9 item 4. + */ +static void test_left_operand_decides_the_type(void) +{ + akbasic_Value *out = NULL; + + /* Integer on the left: the float on the right is truncated to 0. */ + set_int(&A, 3); + set_float(&B, 0.45); + TEST_REQUIRE_OK(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out)); + TEST_REQUIRE_INT(out->valuetype, AKBASIC_TYPE_INTEGER); + TEST_REQUIRE_INT(out->intval, 0); + + /* Float on the left: the integer on the right is widened. */ + set_float(&A, 0.45); + set_int(&B, 3); + TEST_REQUIRE_OK(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out)); + TEST_REQUIRE_INT(out->valuetype, AKBASIC_TYPE_FLOAT); + TEST_REQUIRE_FEQ(out->floatval, 1.35); + + /* The same asymmetry in subtraction, which is how a program negates. */ + set_int(&A, 0); + set_float(&B, 6.4); + TEST_REQUIRE_OK(akbasic_value_math_minus(&A, &B, &SCRATCH, &out)); + TEST_REQUIRE_INT(out->valuetype, AKBASIC_TYPE_INTEGER); + TEST_REQUIRE_INT(out->intval, -6); + + set_float(&A, 0.0); + set_float(&B, 6.4); + TEST_REQUIRE_OK(akbasic_value_math_minus(&A, &B, &SCRATCH, &out)); + TEST_REQUIRE_INT(out->valuetype, AKBASIC_TYPE_FLOAT); + TEST_REQUIRE_FEQ(out->floatval, -6.4); +} + int main(void) { akbasic_Value *out = NULL; @@ -310,6 +361,7 @@ int main(void) test_maximum_length_string(); test_pool_starts_empty(); + test_left_operand_decides_the_type(); return akbasic_test_failures; }