aboutsummaryrefslogtreecommitdiff
path: root/docs/SPEC.md
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-15 21:44:40 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-15 21:44:40 +0330
commitf3dcfd9805e1bd23b002f648d4391ec397122d9c (patch)
tree91acab7da09c9225a2d838791d70e5c2e7db54b9 /docs/SPEC.md
parentmerge work/small-oob: bytes test out-of-bounds read (diff)
parentexamples/actors_demo: print the fan-out replies in worker order, not arrival ... (diff)
merge work/known-bugs: constant-expression precedence, local const in constant expressions, duplicate case labels, unterminated strings, droppable cycles in one collection, js vm exit, docs, actors_demo orderHEADmain
Diffstat (limited to 'docs/SPEC.md')
-rw-r--r--docs/SPEC.md35
1 files changed, 18 insertions, 17 deletions
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 1c65f42..f655395 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -91,7 +91,7 @@ a keyword marked only S used inside an expression is the error `NAME: can't be u
1.4.3 float literals are ieee-754 binary64 values stored in one word. forms: `1.5`, `1.` , `.5`, `1e3`, `1.5e-3`, `1E+3`. the lexer reads the digits, optional `.digits`, and an optional exponent `e|E [+-] digits`; an exponent letter without digits after it is error: bad float. a dot after digits is a fraction when a digit, `e` or `E` follows it, is a field access when a letter or `_` (other than `e`/`E`) follows (`1.field`), and otherwise ends the literal (`1.` is a float). the type is `float`.
-1.4.4 word strings. `"..."` is a string of type `char*`: the bytes are stored one per word in the code area, followed by a 0 word. escapes are as for character literals. a `\0` (or any byte 0) inside the literal ends the string there (`"ab\0cd"` is `"ab"`). a literal may contain a raw line feed. an unterminated string runs to the end of the unit and then error: expected ')' (or the like) at the end of the file. adjacent literals are not concatenated. the literal is static storage that is not a heap object; it is read-only by convention (writing through it is unspecified).
+1.4.4 word strings. `"..."` is a string of type `char*`: the bytes are stored one per word in the code area, followed by a 0 word. escapes are as for character literals. a `\0` (or any byte 0) inside the literal ends the string there (`"ab\0cd"` is `"ab"`). a literal may contain a raw line feed. a string with no closing quote is error: unterminated string literal, reported on the line where it starts. adjacent literals are not concatenated. the literal is static storage that is not a heap object; it is read-only by convention (writing through it is unspecified).
1.4.5 packed byte strings. `b"..."`, where `b` touches the opening quote, has type `bstr`: a pointer to a block whose word 0 is the length in bytes, followed by the bytes eight to a word, little-endian, followed by a 0 byte. escapes and the `\0` truncation rule are as for word strings. `b "x"` (with a space) is the name `b` then a string.
```
@@ -498,16 +498,17 @@ a[2]; // zy: index 2 out of range (length 2)
### 3.14 constant expressions
-3.14.1 constant expressions are used for array lengths, enum values, `const` initializers, global initializers, `case` labels, `embed` array sizes and asm operands. the grammar is: number | character | named constant | `-` operand | `~` operand | `( constant )` | and the binary operators `+ - * | & << >>` only, evaluated left to right with no precedence.
+3.14.1 constant expressions are used for array lengths, enum values, `const` initializers, global initializers, `case` labels, `embed` array sizes and asm operands. the grammar is: number | character | named constant | `-` operand | `~` operand | `( constant )` | and the binary operators `* / % + - << >> & ^ |`, with the c precedences (3.3): `* / %` bind tightest, then `+ -`, `<< >>`, `&`, `^` and `|`; operators of one level group left to right.
```
-const X = 1 + 2 * 3; // 9, not 7
+const X = 1 + 2 * 3; // 7
const Y = 10 - 2 - 3; // 5
const V = 1 | 2 + 4; // 7
const W = ~0 & 255; // 255
+const Z = 7 - 6 / 2 % 2; // 6
```
-other operators (`/ % ^ < ?:` and so on) are not part of a constant expression: `const Z = 6 / 2;` is error: expected ';', found '/'.
+division truncates toward zero as at run time; a division or remainder by zero is error: division by zero in a constant expression. other operators (`< == && ?:` and so on) are not part of a constant expression: `const Z = 1 < 2;` is a syntax error.
-3.14.2 the names allowed are global constants (`const`, enum members, `Enum.MEMBER`, `Struct.CONST` inside that struct as the bare name), global variables and arrays (their value is their address), and defined functions (their address). a local `const` is not visible to a constant expression: `const n = 5; int[n] a;` inside a function is error: expected a constant.
+3.14.2 the names allowed are global constants (`const`, enum members, `Enum.MEMBER`, `Struct.CONST` inside that struct as the bare name), global variables and arrays (their value is their address), defined functions (their address), and a local `const` whose initializer is itself constant: `const n = 5; const m = n * 2; int[m] a;` inside a function gives `a` 10 elements. a local `const` with a run-time initializer is not a constant: `const n = f(); int[n] a;` is error: expected a constant.
### 3.15 closures
@@ -594,7 +595,7 @@ pn(n); // 13
switch (k) { case 1, 2: ps("one-two"); case 3: ps("three"); break; default: ps("d"); }
// k = 2: prints one-two only. no fallthrough into case 3.
```
-4.7.2 duplicate labels are not an error: the first matching case in source order runs. a non-constant label is error: expected a constant. the statements of a case form their own block.
+4.7.2 a label value that appears twice in one switch (two equal constants, or a tagged member twice) is error: duplicate case label. a non-constant label is error: expected a constant. the statements of a case form their own block.
4.7.3 tagged enums. when `e` has type pointer-to-tagged-enum, `case member(a, b):` matches the member and binds each payload field, in order, to a new local scoped to that case; the name count must equal the member's field count (error: a case binds every field of its member). `case member:` or `case member():` with no names matches by tag. a switch on a tagged enum with no `default` that omits a member prints a warning `zc: line N: warning: this switch doesn't cover M1, M2` (not an error; compile continues).
```
@@ -1027,7 +1028,7 @@ rt.put_n(*heap(15).drop(fn() { rt.put_s(" bye "); rt.put_n(*this); })); // 15
let guard = drop(heap(0), fn() { unlock(); }); // unlocks at block end
```
6.15.2 a value dies, and its drop runs, where 6.5 frees it: at the end of its owner's block or an early exit, at `rt.free`, when its owner is reassigned (6.8.5), or with its container, whose drop runs before its children's. a new value assigned to the owner can have its own drop set on the next statement, like a declaration's. a value with a drop that nothing holds (a temporary) dies at the end of its full statement, with hint: value with drop is never bound; it drops at the end of this statement. an owned local with a drop is not "never used" (6.5.2): holding it is its use.
-6.15.3 the collector runs the drop of an unreachable value after its sweep, never during it, and at most once: the value is kept until a later collection finds it unreachable again. a drop that stores `this` somewhere reachable keeps the value alive, and its drop does not run again. among unreachable values, one reached by another waits until that one has dropped (parent first); values that reach each other (a cycle, or a drop that captures its own value) have nothing outside to drop first, so one of them drops per collection, in an unspecified order, each once. there is no collection at exit, so collector-owned values are not dropped then. a freed block forgets its drop, so a block reused at the same address has none.
+6.15.3 the collector runs the drop of an unreachable value after its sweep, never during it, and at most once: the value is kept until a later collection finds it unreachable again. a drop that stores `this` somewhere reachable keeps the value alive, and its drop does not run again. among unreachable values, one reached by another waits until that one has dropped (parent first); values that reach each other (a cycle, or a drop that captures its own value) have nothing outside to drop first, so they all drop in the same collection, in an unspecified order, each once. there is no collection at exit, so collector-owned values are not dropped then. a freed block forgets its drop, so a block reused at the same address has none.
6.15.4 copying a value with a drop into an actor message is error: x: has drop and can't be copied into a message. `spawn` copies the whole vm into the worker process, so both processes hold an owned value that is live at the call and both run its drop: hint: x: has drop and is live at spawn; the worker's copy drops it too. the hint is not given for structs declared with `actor`: one that declares no `drop` gets one that waits for the replies its receiver is owed, closes the endpoint, and does nothing in the worker (docs/ACTOR.md).
6.15.5 nothing moves out of a value with a drop, whose drop may need what it holds. a field or element of it is a borrowed projection and can't be given away (error: a borrowed projection cannot transfer ownership; transfer its owner), and `pop` on an array that has a drop, or on an array field of a value with a drop, is error: a: can't move out of a value with drop. there is no `take`. borrowing a child is fine, and an array without a drop hands out its elements with `pop` as before, drops and all.
@@ -1144,13 +1145,13 @@ each item gives a minimal program, the expected behavior (per this spec) and the
### b.1 disagreements between the prose docs and the code
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.
-5. README ("zc, the language"): "a unit is read whole and scanned for function signatures before it compiles, so calls can come before definitions, tags included, and prototypes are optional". prototypes that are written must name their parameters (`int f(int);` => `expected a name, found ')'`).
-6. README/LEARN do not say that an embedded `\0` ends a string literal: `b"hi\n\0x"` has `len` 3 and `"ab\0cd"` is `ab`.
-7. README ("zc, the language") says `const k = 5;` in a function is a local constant. it is not a constant expression: `const n = 5; int[n] a;` in a function => `expected a constant`.
-8. README (reflection) describes `run.tparamtag` as "the u/m/d tag of a parameter"; the code returns 0 untagged, 1 `.m`, 2 `.d` (there is no u at a parameter).
+2. fixed (README now says `node*`): README ("zc, the language") said 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. fixed (3.14.1): constant expressions now follow c precedence and accept `/ % ^`. it was: 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. fixed (README documents `case _:`): README listed `switch` forms `case 1, 2:` and `default:`. the catch-all `case _:` exists in the compiler and is not documented.
+5. fixed (b.2.13: unnamed prototype parameters are accepted): README ("zc, the language"): "a unit is read whole and scanned for function signatures before it compiles, so calls can come before definitions, tags included, and prototypes are optional". prototypes that are written must name their parameters (`int f(int);` => `expected a name, found ')'`).
+6. fixed (README says it now): README/LEARN did not say that an embedded `\0` ends a string literal: `b"hi\n\0x"` has `len` 3 and `"ab\0cd"` is `ab`.
+7. fixed (3.14.2): README ("zc, the language") says `const k = 5;` in a function is a local constant. it is not a constant expression: `const n = 5; int[n] a;` in a function => `expected a constant`.
+8. fixed (README now gives 0/1/2): README (reflection) described `run.tparamtag` as "the u/m/d tag of a parameter"; the code returns 0 untagged, 1 `.m`, 2 `.d` (there is no u at a parameter).
9. README/LEARN do not say that local variables and arrays are not zeroed (4.1.5).
10. zc0 vs compiler, the case that needed a referee: with `||` and `&&` now at c precedence, zc0 and compiler.zc agree on `1 || 0 && 0` => 1, `0 && 0 || 1` => 1, `1 | 2 && 0` => 0, `1 + 2 << 1` => 6, `7 & 3 ^ 1 | 8` => 10, `6 & 3 == 3` => 0 (checked on both).
11. zc0 vs compiler, integer literals. zc0 reads them with `strtoll` base 0: `rt.put_n(010)` prints 8 in zc0 and 10 in the compiler; `99999999999999999999` and `0xFFFFFFFFFFFFFFFF` saturate to 9223372036854775807 in zc0 and wrap to 7766279631452241919 and -1 in the compiler. this spec follows the compiler.
@@ -1167,12 +1168,12 @@ each item gives a minimal program, the expected behavior (per this spec) and the
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. fixed (1.2.2): an unterminated `/* ...` comment was accepted silently; it is error: unterminated comment. (an unterminated string literal is still only reported as a later syntax error.)
+11. fixed (1.2.2): an unterminated `/* ...` comment was accepted silently; it is error: unterminated comment. an unterminated string literal is error: unterminated string literal (1.4.4), fixed too.
12. settled (3.2.3): `x as int * y` parses `int*` as the type. this is the rule (the type is read greedily, as zc0 does too), so the program is a syntax error.
13. fixed (5.2.1): a prototype's parameters may be unnamed (`int f(int, int*);`). a lone type name counts as a type only in a prototype; with a body it is still the parameter's name, and an unnamed typed parameter there is `a parameter needs a name`.
14. fixed (3.13.5): `len(s)` on a `char*` used to return word 0 of the string when `typecheck` was off; it is now a compile error.
-15. duplicate `case` labels are not diagnosed (4.7.2).
-16. a local `const` cannot be used in a constant expression (b.1.7).
+15. fixed (4.7.2): duplicate `case` labels were not diagnosed; they are error: duplicate case label.
+16. fixed (3.14.2): a local `const` could not 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.