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.
|
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
|
```basic
|
||||||
A# = 3
|
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
|
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.
|
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
|
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
|
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
|
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
|
octal — `010` is ten, because a leading zero in a listing is far more often padding
|
||||||
than a base.
|
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
|
||||||
|
|
||||||
Strings are up to 255 characters and are written in double quotes. There is no
|
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
|
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.
|
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
|
### Structures are an addition
|
||||||
|
|
||||||
BASIC 7.0 has no records at all. `TYPE`/`END TYPE`, `DIM X@ AS T`, the `@` suffix, `.` and
|
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,
|
- **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.
|
so anything drawn is overwritten on the next frame unless the program redraws it.
|
||||||
This is a defect, not a design.
|
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.
|
- **`CHAR` ignores its colour argument** and needs a text device with a cursor.
|
||||||
|
|
||||||
## Sound
|
## Sound
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
* exactly, including the parts that look wrong -- see TODO.md section 12 for the
|
* exactly, including the parts that look wrong -- see TODO.md section 12 for the
|
||||||
* catalogue and the reason they are not fixed yet.
|
* 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
|
* 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
|
* 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
|
* `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);
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
akbasic_Value *out = NULL;
|
akbasic_Value *out = NULL;
|
||||||
@@ -310,6 +361,7 @@ int main(void)
|
|||||||
|
|
||||||
test_maximum_length_string();
|
test_maximum_length_string();
|
||||||
test_pool_starts_empty();
|
test_pool_starts_empty();
|
||||||
|
test_left_operand_decides_the_type();
|
||||||
|
|
||||||
return akbasic_test_failures;
|
return akbasic_test_failures;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user