diff options
| author | 2026-10-14 16:38:21 +0330 | |
|---|---|---|
| committer | 2026-10-14 16:38:21 +0330 | |
| commit | 3dd842d6e8ccafce954b9c72346f2cfefc9e31d5 (patch) | |
| tree | 5092c4220d0c9dbb660a4d610d889e5a55294cf6 | |
| parent | Revert "zc0: read integer literals like the compiler: decimal with leading ze... (diff) | |
zc0umd: no recast uses a visible u/m/d as its tag (spec 1.6.3); spec: the fixed appendix b bugs are marked fixed
| -rw-r--r-- | docs/SPEC.md | 39 | ||||
| -rw-r--r-- | tests/programs/zc0umd.zc | 19 |
2 files changed, 33 insertions, 25 deletions
diff --git a/docs/SPEC.md b/docs/SPEC.md index 6df55f6..3eda4f3 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -55,7 +55,7 @@ a name that looks like `x.y` where `x` is not a known module is the three tokens | `break` `continue` `return` | S | | | 4.8 | | `defer` | S | | | 4.14 | | `enum` | S | T | | 5.8 | -| `include` | S | T | | 5.12 (a statement only by accident, 5.12.7) | +| `include` | S | T | | 5.12 (in a function body it is an error, 5.12.7) | | `embed` | S | T | | 5.11 | | `memdebug` | S | T | | 4.11 | | `hints` | S | T | | 4.12 | @@ -395,7 +395,7 @@ a = b = 7 // assigns both, value 7 3.5.4 `-x` of a float is computed as `0.0 - x`, so `-0.0` evaluates to `+0.0`. do not depend on the sign of zero. -3.5.5 `?:` with one float branch and one integer branch is broken in the reference implementation (appendix b). write both branches as floats. +3.5.5 `?:` with one float branch and one integer branch converts the integer branch to float: `1 ? 1 : 2.5` is 1.0, `1 ? 2.5 : 1` is 2.5. ### 3.6 evaluation order and short circuit @@ -507,7 +507,7 @@ other operators (`/ % ^ < ?:` and so on) are not part of a constant expression: ### 3.15 closures -3.15.1 `fn(a, b) { ... }` in an expression is a closure. parameters may be typed (`fn(int a)`) and tagged. `fn:T(a) { ... }` declares the result type `T` and coerces and checks returns against it. a closure is an owned heap object (a fresh object, 6.1): bind it to a local, pass it to a `.d` parameter or to a borrower, or return it from a `.u` function. +3.15.1 `fn(a, b) { ... }` in an expression is a closure. parameters (up to 24) may be typed (`fn(int a)`) and tagged. `fn:T(a) { ... }` declares the result type `T` and coerces and checks returns against it. a closure is an owned heap object (a fresh object, 6.1): bind it to a local, pass it to a `.d` parameter or to a borrower, or return it from a `.u` function. 3.15.2 a closure captures the locals and parameters of the enclosing function by reference (shared cells): later changes are visible both ways, and the cells outlive the creating function. ``` @@ -624,7 +624,7 @@ o3 int sum(fixed int[?] a) { int s = 0; for (int i = 0; i < len(a); i++) s += a[ ### 4.11 memdebug -`memdebug on;` turns on heap header checks and allocation-site tracking from this point in the file (top level) or from this point in the function (statement); the runtime enables the checks before `main` for the top-level form. `memdebug scrub;` additionally fills freed payloads with 57005; `memdebug off;` stops. with checks on, a use after free prints `zy: use after free from line N`, a double free prints `zy: double free from line N` and returns, a stray address prints `zy: bad heap pointer`. `rt.memreport()` prints a line `leak: line N W words` per live recorded object and returns the count. (`memdebug scrub;` is rejected by the reference implementation, appendix b.) +`memdebug on;` turns on heap header checks and allocation-site tracking from this point in the file (top level) or from this point in the function (statement); the runtime enables the checks before `main` for the top-level form. `memdebug scrub;` additionally fills freed payloads with 57005; `memdebug off;` stops. with checks on, a use after free prints `zy: use after free from line N`, a double free prints `zy: double free from line N` and returns, a stray address prints `zy: bad heap pointer`. `rt.memreport()` prints a line `leak: line N W words` per live recorded object and returns the count. ### 4.12 hints @@ -695,7 +695,7 @@ a bare `name(params) { ... }` with no `fn` and no type is not a function declara 5.2.5 `main`. after the unit compiles without errors, the runtime calls `main` with no arguments (declared parameters read unspecified values; `main(a, b)` prints 0 0 in the reference implementation). its return value is ignored and is not the exit status; `main` may be declared `int`, `void` or untyped, in a run-only image as in `zy`. 8.1. -5.2.6 a function is defined once. redefining a name that already has a body is, per the rule, error: f: is defined twice; the reference implementation does not detect an ordinary redefinition (appendix b). a prototype must agree with the definition: error: f: doesn't match its earlier declaration. +5.2.6 a function is defined once. redefining a name that already has a body in the same module is error: f: is defined twice. a prototype may come before or after the definition and must agree with it: error: f: doesn't match its earlier declaration. a prototype after the definition keeps the definition. 5.2.7 `immediate fn` defines a parse word (7.2). `unchecked fn` and `o3 fn` are 4.9 and 4.10. @@ -703,7 +703,7 @@ a bare `name(params) { ... }` with no `fn` and no type is not a function declara 5.3.1 `T a = e, b, c[10];`, `let g = 1, buf[10];`, `int[10] buf;`, `point origin;`. storage is emitted in place, zero initialised. -5.3.2 an initializer is: a constant expression (3.14), a word string, a byte string, or a brace list or compound literal for an array. anything else is an error: for `int a = f();` error: expected ';', found '('. a float literal initializer is rejected (error: expected a constant) in the reference implementation (appendix b); assign floats in code. a name of another global in the initializer evaluates to its address, not its value: `int a = 5; int b = a + 1;` gives `b` the value `&a + 1`. +5.3.2 an initializer is: a constant expression (3.14), a word string, a byte string, or a brace list or compound literal for an array. anything else is an error: for `int a = f();` error: expected ';', found '('. a `float`, `f64` or `f32` global takes a float literal initializer (`float g = 1.5;`). a name of another global in the initializer evaluates to its address, not its value: `int a = 5; int b = a + 1;` gives `b` the value `&a + 1`. 5.3.3 a global cannot be tagged (error: tags go on locals and parameters, not globals), cannot be `void`, and an array global needs a length or an initializer. @@ -782,7 +782,7 @@ rt.put_n(util.twice(1)); // error: module access requires `include util;` 5.12.6 in a namespaced file, a declaration `f` is `ns.f`. an unqualified name is looked up first in the file's namespace, then globally, and `rt` is not searched for bare names (3.11.5). -5.12.7 `include` is parsed at the top level; written inside a function body it mis-compiles (appendix b). +5.12.7 `include` is parsed at the top level; written inside a function body it is error: include is top level only. 5.12.8 when a library is included more than once, it is compiled once. @@ -1086,11 +1086,11 @@ not in the zc0 subset: `struct`, methods, `enum`, `type`, generics, `do`/`while` ## appendix b: disagreements and bugs -each item gives a minimal program, the expected behavior (per this spec) and the actual behavior of the reference implementation on `build/bin/zy` (unless it names zc0). none are fixed here. +each item gives a minimal program, the expected behavior (per this spec) and the actual behavior of the reference implementation on `build/bin/zy` (unless it names zc0). items marked fixed are kept for their numbers; the rest are open. ### b.1 disagreements between the prose docs and the code -1. README ("memory debug"): "`memdebug scrub;` also fills a freed payload with 57005". the compiler compares the word after `memdebug` with the interned name `rt.scrub` instead of `scrub`, so the statement is rejected: `memdebug scrub;` => `zc: line N: memdebug on, memdebug scrub or memdebug off`. (also b.2.1) +1. fixed: README ("memory debug") says `memdebug scrub;` also fills a freed payload with 57005; the compiler used to compare the word with `rt.scrub` and reject it. it is accepted now (also b.2.1). 2. README ("zc, the language") says a cast type "has to be a simple type ... `int` or `*node`". the prefix pointer spelling `*node` is not a type (README's own "pointer types" section says so, and `(*int)y` => `int: is a type, not a value`). the postfix form `(node*)x` is the one that works. 3. README says "operators follow C precedence". this holds for expressions. constant expressions (array lengths, enum values, `const`, `case` labels) have no precedence: `+ - * | & << >>` only, left to right (3.14). `const X = 1 + 2 * 3;` is 9, and `/ % ^` are rejected (`const Z = 6 / 2;` => `expected ';', found '/'`). README does not mention either fact. 4. README lists `switch` forms `case 1, 2:` and `default:`. the catch-all `case _:` exists in the compiler and is not documented. @@ -1104,15 +1104,15 @@ each item gives a minimal program, the expected behavior (per this spec) and the ### b.2 implementation bugs and limitations -1. `memdebug scrub;` is rejected (b.1.1). expected: poison freed payloads with 57005. +1. fixed: `memdebug scrub;` is accepted and poisons freed payloads with 57005 (b.1.1). 2. enum members cannot name earlier members of the same enum: `enum { HI = 16, LO = HI + 1 };` => `expected a constant` (the members are defined after the closing brace). members of an earlier enum and constants work. -3. a function can be silently redefined. `int f() { return 1; } int g() { return f() + 10; } int f() { return 2; }`, then `g()` gives 11 and a later call of `f()` gives 2. expected: error `f: is defined twice` (the message exists in the compiler and is unreachable for an ordinary second definition). -4. a prototype after the definition hides it. `int f() { return 1; } int f();` then `f()` => `zc: line 0: f is used but never defined`. expected: the definition stays. -5. `?:` with one float branch and one integer branch converts wrongly. `pf(1 ? 1 : 2.5)` prints `0.000000` (the integer 1's bits read as a float); `pf(1 ? 2.5 : 1)` prints `4612811918334230528.000000` (the taken float converted again as if an integer); `pf(0 ? 2.5 : 1)` prints 1.000000 and `pf(0 ? 1 : 2.5)` 2.500000 (these two are right). expected 1.0, 2.5, 1.0, 2.5. +3. fixed (now error: f: is defined twice): a function could be silently redefined. `int f() { return 1; } int g() { return f() + 10; } int f() { return 2; }`, then `g()` gives 11 and a later call of `f()` gives 2. expected: error `f: is defined twice` (the message exists in the compiler and is unreachable for an ordinary second definition). +4. fixed (the definition stays): a prototype after the definition hid it. `int f() { return 1; } int f();` then `f()` => `zc: line 0: f is used but never defined`. expected: the definition stays. +5. fixed (3.5.5): `?:` with one float branch and one integer branch converted wrongly. `pf(1 ? 1 : 2.5)` prints `0.000000` (the integer 1's bits read as a float); `pf(1 ? 2.5 : 1)` prints `4612811918334230528.000000` (the taken float converted again as if an integer); `pf(0 ? 2.5 : 1)` prints 1.000000 and `pf(0 ? 1 : 2.5)` 2.500000 (these two are right). expected 1.0, 2.5, 1.0, 2.5. 6. `bool` does not normalise: `bool b = 5; rt.put_n(b); rt.put_n(b == 1); rt.put_n(!b);` prints 5, 0, 0. expected 1, 1, 0 if `bool` is a 0/1 type; it behaves as a plain integer. -7. a float literal as a global initializer: `float g = 1.5;` at the top level => `expected a constant`. expected: allowed (a float local initializer works). -8. a global initializer that names another global uses the other global's address, not its value: `int a = 5; int b = a + 1;` gives `b` the value `&a + 1` (1 when `a` is at address 0). expected: the value of `a`, or an error. -9. `include std;` written inside a function body: `zc: line N: return outside a function`. expected: an error saying include is top level only. +7. fixed (5.3.2): a float literal as a global initializer (`float g = 1.5;`) was rejected. +8. not a bug: a global initializer that names another global uses the other global's address (5.3.2, 3.14.2): `int a = 5; int b = a + 1;` gives `b` the value `&a + 1`. +9. fixed (5.12.7): `include std;` inside a function body gave `return outside a function`; it is now error: include is top level only. 10. a syntax error hides errors in the statements skipped by recovery: `int a = ; int b = 3 +;` reports only the first error (8.2). by design, but easy to miss. 11. an unterminated `/* ...` comment is accepted silently (1.2.2); an unterminated string literal is only reported as a later syntax error. 12. `x as int * y` parses `int*` as the type (3.2.3). @@ -1120,6 +1120,13 @@ each item gives a minimal program, the expected behavior (per this spec) and the 14. `len(s)` on a `char*` returns word 0 of the string when `typecheck` is off. 15. duplicate `case` labels are not diagnosed (4.7.2). 16. a local `const` cannot be used in a constant expression (b.1.7). +17. fixed: a bad float or character literal in a function's first statement was not reported (1.4). +18. fixed: `u*d` with a visible variable `u` (local or global) was read as a recast; it is a product (1.6.3). +19. fixed: a cast such as `(int*)s` of a `bstr` kept the byte-string flag, so `p[0]` read a byte. +20. fixed: functions took 23 parameters and closures 19; both take 24 (5.2.2, 3.15.1). +21. fixed: `return e;` in a `void` function with `typecheck` off returned `e`; the value is dropped and the function yields 0 (4.8.3). +22. fixed: `case member():` on a tagged member with fields was rejected; it matches by tag (4.7.3). +23. fixed: `INT_MIN / -1` gave 0 and `INT_MIN % -1` gave INT_MIN in programs compiled by zc0 (software division, `rt.zdiv`); they give INT_MIN and 0 (3.4.3). ## appendix c: how this spec was checked diff --git a/tests/programs/zc0umd.zc b/tests/programs/zc0umd.zc index 9693111..b5a6d2a 100644 --- a/tests/programs/zc0umd.zc +++ b/tests/programs/zc0umd.zc @@ -2,12 +2,13 @@ namespace; include rt; // u, m and d are ordinary names, and a tag is written .u, .m or .d after a type. -// u*x, m*x and d*x with no space before the star is a recast, and u * x with a -// space is a product. the same program runs on the resident compiler and, built +// u*x, m*x and d*x with no space before the star is a recast when no variable +// named u, m or d is visible there (spec 1.6.3); otherwise, and with a space, it +// is a product. the same program runs on the resident compiler and, built // into a run-only image, on stage 0 (src/tools/zc0.c), which reads the tags and // recasts and does nothing with them. -int u = 2, m = 3; // globals +int uu = 2, m = 3; // globals (no global u, so u*x recasts below; m*x with m visible is a product) int[4] d4; int d(int x) { return x + 1; } // a function @@ -17,12 +18,12 @@ int tagged(int(int) f, int.m x) { return f(x); } // owned objects through locals named for their tags word*.u box(int v) { word* m = rt.new(2); m[0] = v; m[1] = v * 2; return u*m; } -word*.u pair(int u, int m) { +word*.u pair(int x, int m) { word* d = rt.new(2); - d[0] = u; d[1] = m; + d[0] = x; d[1] = m; return u*d; } -.u single(int v) { let u = rt.new(1); u[0] = v; return u*u; } +.u single(int v) { let d = rt.new(1); d[0] = v; return u*d; } int sum(word*.m m) { return m[0] + m[1]; } int eat(word*.d d) { return d[0] + d[1]; } int hold(.m u, .d d) { return u[0] * 100 + d[0]; } @@ -35,9 +36,9 @@ fn main() { .m q = single(6); word* r = pair(8, 9); let c = box(10), e = pair(1, 2); - rt.put_n(u * m); rt.put_c(32); // two globals multiplied - rt.put_n(u*=m); rt.put_c(32); // u *= m is not a recast either - rt.put_n(d(u) * d(m)); rt.put_c(32); + rt.put_n(uu * m); rt.put_c(32); // two globals multiplied + rt.put_n(uu*=m); rt.put_c(32); // *= is not a recast either + rt.put_n(d(uu) * d(m)); rt.put_c(32); rt.put_n(mul3(2, 3, 4)); rt.put_c(32); rt.put_n(mixed(7, 6, 5)); rt.put_c(32); rt.put_n(tagged(d, n)); rt.put_c(10); |
