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) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 00:23:20 -04:00
parent d5f3c3ef5b
commit c8b917d205
5 changed files with 139 additions and 2 deletions

27
TODO.md
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}