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:
27
TODO.md
27
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user