diff options
40 files changed, 250 insertions, 109 deletions
@@ -169,7 +169,7 @@ fn main() { } ``` -declarations are C-style, type first: `int a = 1, b;`, `node* p = rt.new(node);`, `char[4] buf;`, `point origin;`. `let` declares without a type: it infers `char*` from a word-string literal and `bstr` from a packed byte-string literal, infers other pointers and function types from the initializer, uses a pointer alias for an in-place array or struct, and keeps numbers as plain words. `let a[10];` is an untyped array. `const k = 5;` inside a function is a local constant with no type: it takes the initializer's type, needs an initializer, and can't be assigned. statements are declarations, `if`/`else`, `while`, `do`/`while`, `for`, `switch`, `break`, `continue`, `return`, `defer`, `{ }` blocks, `asm { }` and expressions. `do` body `while (` cond `);` runs the body once before the test, and `continue` jumps to the test. `defer expr;` or `defer { ... }` registers code that runs when the enclosing block ends, on every way out of it: falling off the end, `return`, `break` and `continue`. several defers in a block run last registered first, the deferred code is compiled at each exit and so reads the variables as they are when it runs (a `return e;` computes `e` first, then runs the defers), a defer that was never reached registers nothing, and `return`, `break` and `continue` inside a deferred statement are errors. a deferred `rt.free(p);` is the transfer of `p` for the analyzer at the point it runs: `p` stays usable until the block ends, is not freed or reported a second time, and freeing an owner that was moved away meanwhile is the usual use-after-move error. defer also works in `unchecked` code. `enum color { RED, GREEN = 5, BLUE };` makes global constants, and `color` is a type of its own in typed code (`typecheck on;`): a member takes the enum, a plain int does not become a `color`, and a `color` still becomes an `int`. untyped code treats the members as plain ints. each member is also reached as `color.BLUE`, in an expression or a `case` label. a member can carry fields too: `enum shape { circle { int r; }, rect { int w; int h; }, dot };` makes the constructors `shape.circle(3)` and `shape.rect(4, 5)`, which return an owned `shape*`, and `shape.dot()`. the tag is the word at offset 0 of the object. a `switch` over a `shape*` matches a member with `case circle(r):`, which binds its fields in that arm only. a switch on a tagged enum that leaves out a member and has no `default` gets a warning on its line (not an error). `(T)e` is a cast with the same meaning as `e as T`: the bits stay, the type changes. it has to be a simple type the parser can see in a few tokens, `int` or `*node`, not a nested array. `include "path";` reads a file and compiles it at that point, signatures first, so a library can live in its own file. each file has its own namespace and imports; dependencies included by a file do not import into the caller. `u`, `m` and `d` are ordinary names. a tag is `.u`, `.m` or `.d`, and `u*x` with no space before the star is still a recast. `switch (e) { case 1, 2: ... default: ... }` takes constant labels, and its cases don't fall through: each ends where the next begins, and `break` leaves early. operators follow C precedence, including `c ? a : b`, plus `p.x` and `p->x` (the same thing: `.` goes through a pointer on its own), `e as T` to change a type without changing the bits, and the casts below. comparisons and `&& ||` give 0 or 1. `/`, `%` and `<<` compile to native enhancement opcodes. character literals, `0x` hex and the usual escapes are there. +declarations are C-style, type first: `int a = 1, b;`, `node* p = rt.new(node);`, `char[4] buf;`, `point origin;`. `let` declares without a type: it infers `char*` from a word-string literal and `bstr` from a packed byte-string literal, infers other pointers and function types from the initializer, uses a pointer alias for an in-place array or struct, and keeps numbers as plain words. `let a[10];` is an untyped array. `const k = 5;` inside a function is a local constant with no type: it takes the initializer's type, needs an initializer, and can't be assigned; with a constant initializer it can be used in constant expressions (array lengths, `case` labels), which follow C precedence. statements are declarations, `if`/`else`, `while`, `do`/`while`, `for`, `switch`, `break`, `continue`, `return`, `defer`, `{ }` blocks, `asm { }` and expressions. `do` body `while (` cond `);` runs the body once before the test, and `continue` jumps to the test. `defer expr;` or `defer { ... }` registers code that runs when the enclosing block ends, on every way out of it: falling off the end, `return`, `break` and `continue`. several defers in a block run last registered first, the deferred code is compiled at each exit and so reads the variables as they are when it runs (a `return e;` computes `e` first, then runs the defers), a defer that was never reached registers nothing, and `return`, `break` and `continue` inside a deferred statement are errors. a deferred `rt.free(p);` is the transfer of `p` for the analyzer at the point it runs: `p` stays usable until the block ends, is not freed or reported a second time, and freeing an owner that was moved away meanwhile is the usual use-after-move error. defer also works in `unchecked` code. `enum color { RED, GREEN = 5, BLUE };` makes global constants, and `color` is a type of its own in typed code (`typecheck on;`): a member takes the enum, a plain int does not become a `color`, and a `color` still becomes an `int`. untyped code treats the members as plain ints. each member is also reached as `color.BLUE`, in an expression or a `case` label. a member can carry fields too: `enum shape { circle { int r; }, rect { int w; int h; }, dot };` makes the constructors `shape.circle(3)` and `shape.rect(4, 5)`, which return an owned `shape*`, and `shape.dot()`. the tag is the word at offset 0 of the object. a `switch` over a `shape*` matches a member with `case circle(r):`, which binds its fields in that arm only. a switch on a tagged enum that leaves out a member and has no `default` gets a warning on its line (not an error). `(T)e` is a cast with the same meaning as `e as T`: the bits stay, the type changes. it has to be a simple type the parser can see in a few tokens, `int` or `node*` (pointer types are postfix), not a nested array. `include "path";` reads a file and compiles it at that point, signatures first, so a library can live in its own file. each file has its own namespace and imports; dependencies included by a file do not import into the caller. `u`, `m` and `d` are ordinary names. a tag is `.u`, `.m` or `.d`, and `u*x` with no space before the star is still a recast. `switch (e) { case 1, 2: ... default: ... }` takes constant labels (`case _:` is a catch-all like `default:`, and a label repeated in one switch is an error), and its cases don't fall through: each ends where the next begins, and `break` leaves early. operators follow C precedence, including `c ? a : b`, plus `p.x` and `p->x` (the same thing: `.` goes through a pointer on its own), `e as T` to change a type without changing the bits, and the casts below. comparisons and `&& ||` give 0 or 1. `/`, `%` and `<<` compile to native enhancement opcodes. character literals, `0x` hex and the usual escapes are there; an embedded `\0` ends a string literal (`"ab\0cd"` is `ab`, `b"hi\n\0x"` has `len` 3). `typedef T name;` gives a type another name, as in c, at the top level or inside a function (where it lasts to the end of its block, like a local): `typedef node* link;`, `typedef int[4] quad;`, `typedef int(int, int) binop;`, `typedef list<int> ints;`. `type name = T;` says the same thing. a typedef is only a new spelling: `link` and `node*` are the very same type record, so they mix freely in declarations, parameters, results, casts, `sizeof`, struct fields, generic arguments and `typecheck on;` code, and they print as the target type. `T` can be any type the parser accepts, but not a tag (`.u`, `.m` and `.d` belong to a declaration). a typedef can't name a type that isn't declared yet, so a struct comes before the typedefs of its pointers; using a name that is already defined is an error, and a typedef of `void` is `void` (a variable of it is the usual error). `string` is built in as a typedef of `char*`, so `string s = "hi";` and `string f(string a)` work with no declaration; a function, variable or parameter of the same name shadows it. @@ -372,7 +372,7 @@ ordinary comments immediately above a declaration provide its hover documentatio ### reflection -`typeof(e)` and `typeof(T)` give a type record and `sizeof(...)` its size. both work on any expression, which is compiled dry and never runs. the readers are compiler functions, so they need `include run;`: `run.tkind(t)` (one of the `run.TY_` constants), `run.tsize`, `run.tname`, `run.tlen`, `run.tnfields`, `run.tfname(t, i)`, `run.tftype`, `run.tfoff`, `run.tret`, `run.tnparams`, `run.tparam`, `run.tparamtag` (the u/m/d tag of a parameter), `run.tretu`, `run.tnargs` and `run.targ` for generic instances, and `run.tprint(t)`. `run.sigof(f)` gives the type of a function by address (untyped ones get a signature built from their arity and tags), `run.sigofs("name")` by name, and `rt.typeof_obj(p)` reads the type stamped on a heap object (0 when the header has no type yet: `rt.new(n)` or `rt.new_raw`). +`typeof(e)` and `typeof(T)` give a type record and `sizeof(...)` its size. both work on any expression, which is compiled dry and never runs. the readers are compiler functions, so they need `include run;`: `run.tkind(t)` (one of the `run.TY_` constants), `run.tsize`, `run.tname`, `run.tlen`, `run.tnfields`, `run.tfname(t, i)`, `run.tftype`, `run.tfoff`, `run.tret`, `run.tnparams`, `run.tparam`, `run.tparamtag` (a parameter's tag: 0 untagged, 1 `.m`, 2 `.d`; a parameter has no `.u`), `run.tretu`, `run.tnargs` and `run.targ` for generic instances, and `run.tprint(t)`. `run.sigof(f)` gives the type of a function by address (untyped ones get a signature built from their arity and tags), `run.sigofs("name")` by name, and `rt.typeof_obj(p)` reads the type stamped on a heap object (0 when the header has no type yet: `rt.new(n)` or `rt.new_raw`). ### the typechecker @@ -522,6 +522,7 @@ brief someone without re-reading the code. - [x] remaining zc0 gaps, one commit each on work/xref: string literals have no length limit (the token text grows; `1-source/lit_string_over_4095_bytes.zc`); `.u` on a typed parameter is rejected with the compiler's message, "a parameter tag is .m or .d" (`5-declarations/fn_param_tag_u_rejected.zc`); `x as T` reads a whole type, so `x as int*` and `x as char**` work (`3-expressions/cast_as_pointer_type.zc`); `embed` of an array type inside a function puts the elements in the code and the local holds their address (`tests/programs/embed_local_array.zc`, a parity program); integer literals as the compiler reads them, so `010` is 10, `0x` is 0 and values wrap modulo 2^64 (the eleven class b literal entries left `known.txt`). `docs/history/43-stdlib/notes.md` has a forward note that zc0 no longer reserves u/m/d. the genome is byte-identical to main's. - [x] verify both compilers against `docs/SPEC.md`: `tests/conformance/<section>/` (sections 1 to 8 plus `9-appendix-b`, 657 programs), runner `tests/conformance/run.sh` (`--zc0`, `--zym`, `--zy`, `-w`), understood failures in `tests/conformance/known.txt` (class a self-hosted bug, b zc0, c spec issue), wired into the interp section of `tests/test.sh`. zy 626 ok, 31 known; zc0 221 ok, 38 known, 398 skipped (outside appendix a). appendix b.2 items 1 to 16 confirmed except b.2.8, which is documented behavior (5.3.2, 3.14.2; the spec wins). top-level `o3 { }` is rejected (4.10.2, class c, left alone). the seven class a bugs are fixed, one commit each: a bad literal in a function's first statement (capture_scan left panic set), `u*d` with visible `u`/`d`, the bstr flag surviving a cast, the 24-parameter limit, `return e;` in a void function (now dropped, yields 0), `memdebug scrub;`, and `case member():` on a member with fields. the conformance run is in `tests/test.sh` but not in `-q`. open: `rt.memreport()` reports buddy capacity (`rt.new(2)` is 3 words), whether that is the spec's W; whether top-level `unchecked;` is in the zc0 subset. +- [x] (work/known-bugs) constant-expression precedence (b.1.3), local const in constant expressions (b.1.7/b.2.16), duplicate case labels (b.2.15), unterminated string literals (b.2.11), droppable cycles drop whole in one collection (6.15.3). still open from the line below: b.2.10 (by design), top-level `o3 { }` (4.10.2). - [ ] fix the implementation bugs still open in `docs/SPEC.md` appendix b: unterminated string literals are only reported as a later syntax error (b.2.11 fixed the `/*` half); duplicate `case` labels are not diagnosed (b.2.15); a local `const` cannot be used in a constant expression (b.2.16, b.1.7); constant expressions have no precedence, so `1+2*3` is 9 (b.1.3); syntax-error recovery hides errors in the skipped statements (b.2.10, by design; decide whether to keep it). fixed and merged from work/spec: enum members naming earlier members (b.2.2), function redefinition (b.2.3), prototype after the definition (b.2.4), `?:` with int and float branches (b.2.5), `bool` normalisation (b.2.6), float global initializers (b.2.7, integer constants too via 46825f2), `include std;` in a function body (b.2.9), unterminated `/*` (b.2.11), `x as int * y` settled as a syntax error (b.2.12, doc only), unnamed prototype parameters (b.2.13), `len(char*)` with typecheck off (b.2.14). fix the prose items appendix b.1 lists too (verify which are still open). - [ ] optional, low priority: plain enums and `?:` in zc0 (both are small and produce ordinary code), so parity covers more programs. no structs in zc0 (decided). - [ ] group the compiler's parallel per-local arrays (`lname`, `ltype`, `lstate`, `lhome`, `lown`, `lslot`, `ltag`, `lpos`, `lnsz`, `lleaf`, ...) without structs: declare each family together in one place, with one comment block stating each array's meaning and invariant (for example when `lhome` is meaningful), route snapshot and restore of the family through one pair of functions, and use the cross-reference report to confirm nothing else writes them. then symbols and types the same way. fixpoint and analyzer behavior unchanged. 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. diff --git a/docs/design/destructors.md b/docs/design/destructors.md index c828cbd..189c9c6 100644 --- a/docs/design/destructors.md +++ b/docs/design/destructors.md @@ -179,5 +179,5 @@ untyped `rt.new(n)` blocks take a drop like any other value, so there is no sepa - `gc_drops`: drops of marked values are marked (to a fixpoint); each unmarked value whose drop hasn't run gets `F_FINAL` and is kept with what it reaches; a candidate reached by another candidate waits for a later collection, so parents drop before children. - o3: `rt.o3_enter`/`rt.o3_leave` around `o3 { }` blocks and calls from normal code into o3 functions; `gc` inside sets `gc_pending`, the outermost leave runs it. - compiler: `heap(e)`, the three drop forms (a closure there gets `this`), the struct `drop` method as type drop (`set_tdrop` after `new(T)` and `heap`), set-once and set-early on locals, temporaries held in a hidden slot until the statement ends with a hint, the actor `send` check, the o3 errors. -- since: reassigning an `m` owner frees the old value with a hint (6.8.5; `reassign_free` in analyzer.zc, which frees without freevar's clearing store, since a captured local's slot holds its cell); `pop` on an array with a drop, or an array field of one, is refused (6.15.5; fields were already borrowed projections); the spawn hint on `x.spawn()` of a mailbox struct (`spawn_hint`); memreport's `(drop)` marker; `break`/`continue` out of an `o3 { }` block call `o3_leave` for each block they leave (`lpo3`, the depth each loop began at); an o3 receiver method called from normal code enters and leaves the region (`methcall`); a candidate that reaches itself in `gc_drops` (`F_CYCLE`, a cycle or a drop capturing its value) drops now, one per collection. +- since: reassigning an `m` owner frees the old value with a hint (6.8.5; `reassign_free` in analyzer.zc, which frees without freevar's clearing store, since a captured local's slot holds its cell); `pop` on an array with a drop, or an array field of one, is refused (6.15.5; fields were already borrowed projections); the spawn hint on `x.spawn()` of a mailbox struct (`spawn_hint`); memreport's `(drop)` marker; `break`/`continue` out of an `o3 { }` block call `o3_leave` for each block they leave (`lpo3`, the depth each loop began at); an o3 receiver method called from normal code enters and leaves the region (`methcall`); a candidate that reaches itself in `gc_drops` (`F_CYCLE`, a cycle or a drop capturing its value) drops now, one per sweep; `gc` sweeps again while a cycle member dropped, so the whole cycle drops in one collection. - first users: `std.File` (`Host.open` returns a `File*.u` whose drop closes the fd; `writefile` is built on it), and every declared actor struct without its own `drop` gets a generated one that waits for the replies its receiver is owed and closes the endpoint (the generated handler reaches the struct through `endpoint.state` rather than capturing `this`, since a captured value is shared and a shared value's free is the collector's; the generated drop returns at once in a worker, whose snapshot owns nothing). the spawn hint skips mailbox structs. diff --git a/examples/actors_demo.zc b/examples/actors_demo.zc index 656e131..9e4a8ed 100644 --- a/examples/actors_demo.zc +++ b/examples/actors_demo.zc @@ -62,24 +62,31 @@ fn main() { while (!done) for (int i = 0; i < N; i++) ring[i].poll(); for (int i = 0; i < N; i++) ring[i].close(); - // fan-out: W workers in parallel, results arrive as they finish + // fan-out: W workers in parallel. replies arrive in whatever order the workers + // finish, so each is stored by worker and printed in worker order afterwards color(2); rt.put_s("\nfan-out: "); rt.put_n(W); rt.put_s(" crunchers, collatz over 80000 starts\n"); plain(); Crunch*[4] pool; + int[4] chain; + int* cp = &chain[0]; let answers = 0, best = 0; for (int i = 0; i < W; i++) { pool[i] = rt.new(Crunch); pool[i].spawn(); let me = i; pool[i].recv(fn(int k) { answers++; if (k > best) best = k; - color(4); rt.put_s(" worker "); rt.put_n(me); plain(); - rt.put_s(" ["); rt.put_n(me * 20000 + 1); rt.put_s(".."); rt.put_n(me * 20000 + 20000); rt.put_s("] longest chain "); - color(6); rt.put_n(k); plain(); rt.put_c(' '); bar(k / 8, 4 + me % 3); rt.put_c(10); + cp[me] = k; return 0; }); pool[i].send(i * 20000 + 1); } for (int i = 0; i < W; i++) pool[i].await(); for (int i = 0; i < W; i++) pool[i].close(); + for (int i = 0; i < W; i++) { + int k = chain[i]; + color(4); rt.put_s(" worker "); rt.put_n(i); plain(); + rt.put_s(" ["); rt.put_n(i * 20000 + 1); rt.put_s(".."); rt.put_n(i * 20000 + 20000); rt.put_s("] longest chain "); + color(6); rt.put_n(k); plain(); rt.put_c(' '); bar(k / 8, 4 + i % 3); rt.put_c(10); + } color(5); rt.put_s("\n== summary ==\n"); plain(); rt.put_s(" ring hops: "); color(3); rt.put_n(hops); plain(); rt.put_c(10); diff --git a/seed/MANIFEST b/seed/MANIFEST index 10e92d5..c83f684 100644 --- a/seed/MANIFEST +++ b/seed/MANIFEST @@ -1,8 +1,8 @@ # the pinned bootstrap seed. make checks seed-sha256 before it runs the seed. # regenerate with: make seed -seed-sha256 984f6a085e586b502af13a99ea91ebe94ae27265384d2ef401be1f37e56ad820 -seed-size 3305045 -seed-size-unoptimized 3305124 +seed-sha256 a902fee5d9daf6fcf2a38bb3f504e7545f0760656424121c44ecb02abf005361 +seed-size 3325910 +seed-size-unoptimized 3325989 wasm-opt wasm-opt version 133 (version_133) -image-sha256 c079adab065766a108a551767a57678d027d7a33d0552b9a6627167c13871104 -image-size 3558360 +image-sha256 d5ab2ac3f36d7ad1b9196518bab02c9a779351a47c7b0a27da3fe32522f23f27 +image-size 3583472 diff --git a/seed/zc.wasm b/seed/zc.wasm Binary files differindex 4a9b4fe..32fbb8f 100644 --- a/seed/zc.wasm +++ b/seed/zc.wasm diff --git a/src/compiler/base.zc b/src/compiler/base.zc index a209f2b..3135b39 100644 --- a/src/compiler/base.zc +++ b/src/compiler/base.zc @@ -400,7 +400,7 @@ int nerr, panic; const int LASG = 0x1000, LREAD = 0x2000, LDECL = 0x4000, LCONST = 0x8000, LCNT1 = 0x100000000000000; const int LWASREAD = 0x10000; // an earlier value was read: a fresh value assigned over it clears LREAD, so its drop can still be set (6.15.1) const int NEWTYPED = 0x800000; -word[NL] lpos_initial; word lpos = lpos_initial; word[NL] lreadpos_initial; word lreadpos = lreadpos_initial; word[NL] lnsz_initial; word lnsz = lnsz_initial; word[NL] lleaf_initial; word lleaf = lleaf_initial; word[NL] lrsoff_initial; word lrsoff = lrsoff_initial; word[NL] lrskind_initial; word lrskind = lrskind_initial; int newsz, newleaf, nomagic, callpos; +word[NL] lpos_initial; word lpos = lpos_initial; word[NL] lreadpos_initial; word lreadpos = lreadpos_initial; word[NL] lnsz_initial; word lnsz = lnsz_initial; word[NL] lleaf_initial; word lleaf = lleaf_initial; word[NL] lkval_initial; word lkval = lkval_initial; word[NL] lrsoff_initial; word lrsoff = lrsoff_initial; word[NL] lrskind_initial; word lrskind = lrskind_initial; int newsz, newleaf, nomagic, callpos; word[NL] lptr_initial; word lptr = lptr_initial; // annotate mode: the local was indexed or dereferenced, so it holds an address // storage facts are shared by aliases. they describe one allocation, not a // variable slot: [length, lifetime, kind, allocation place, end place, owner]. @@ -772,6 +772,7 @@ int reserve_locals(int need) { lreadpos = chk_resize(lreadpos, nloc, cap); lnsz = chk_resize(lnsz, nloc, cap); lleaf = chk_resize(lleaf, nloc, cap); + lkval = chk_resize(lkval, nloc, cap); lrsoff = chk_resize(lrsoff, nloc, cap); lrskind = chk_resize(lrskind, nloc, cap); lptr = chk_resize(lptr, nloc, cap); diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc index 2935a9b..e437d2f 100644 --- a/src/compiler/expr.zc +++ b/src/compiler/expr.zc @@ -1072,6 +1072,7 @@ int primary() { if (isfl(ltype[i])) xfloat = 1; xstr = getstr(i); if (xty == tbstr) xstr = 1; lv = 1; lvk = LV_LOCAL; lvi = i; + if ((ltag[i] & LCONST) && treadonly(xty)) { xconst = 1; xcval = lkval[i]; } // a local const's value is known (spec 3.14.2) if (isalias(i)) { touch(i); xbor = lroot(i); return C_VAR + i; } if (tracked(i) || lrskind[i] == RS_PARAM || (local_origin_mask && local_origin_mask[i])) return C_VAR + i; return C_PLAIN; @@ -1762,17 +1763,31 @@ int unary() { // level tighter and emit its code sequence from the operator table. && and || // short-circuit with jumps; floats get their own opcodes; a * or / by a // constant that is a power of two gets a shift hint in check mode. +// fold a binary operator on two known integers; *ok is set to 0 when it can't be folded +int cfold(int o, int a, int b, word ok) { + if (o == bname[0]) return a * b; + if (o == bname[1] || o == bname[2]) { if (b == 0) { ok[0] = 0; return 0; } if (o == bname[1]) return a / b; return a % b; } + if (o == bname[3]) return a + b; + if (o == bname[4]) return a - b; + if (o == bname[5]) return a << b; + if (o == bname[6]) return a >> b; + if (o == bname[13]) return a & b; + if (o == bname[14]) return a ^ b; + if (o == bname[15]) return a | b; + ok[0] = 0; return 0; +} int if_comparison(int name) { return name == s_eqeq || name == s_ne || name == s_lt || name == s_gt || name == s_le || name == s_ge; } int bin(int minp) { - int c = unary(), b, p, a, j, lt, lb, rb, src, lf, rf, ls, rs, fresh, top = 0, il = -1, opp, lu, ru, xu; word borrowed, rightborrow, left_mask, right_mask, infer_left, infer_right; + int lk = 0, lkv = 0, c = unary(), b, p, a, j, lt, lb, rb, src, lf, rf, ls, rs, fresh, top = 0, il = -1, opp, lu, ru, xu; word borrowed, rightborrow, left_mask, right_mask, infer_left, infer_right; if (fortest == 1) { fortest = 2; top = 1; ft_nops = 0; } while (tk == T_OP) { b = binfind(tv); if (b < 0) return c; p = bprec[b]; if (p < minp) return c; + lk = 0; lb = -1; if (c >= C_VAR && tracked(c - C_VAR)) lb = lroot(c - C_VAR); else lb = xbor; borrowed = xaf; left_mask = effect_mask(c); lf = xfloat; ls = xstr; fresh = 0; @@ -1807,6 +1822,7 @@ int bin(int minp) { setlit(j, here); xfloat = 0; xstr = 0; } else { + lk = xconst && !lf && !ls; lkv = xcval; c = bin(p + 1); infer_right = if_value; rightborrow = xaf; right_mask = effect_mask(c); rb = -1; if (c >= C_VAR && tracked(c - C_VAR)) rb = lroot(c - C_VAR); else rb = xbor; @@ -1818,6 +1834,7 @@ int bin(int minp) { shifthint(opp, xcval); } rf = xfloat; rs = xstr; + if (lk && xconst && !rf && !rs && !isfl(xty)) { lk = 2; lkv = cfold(bname[b], lkv, xcval, &lk); } if (isfl(xty)) rf = 1; if ((ls || rs) && (bname[b] == s_plus || bname[b] == s_eqeq || bname[b] == s_ne || bname[b] == s_lt || bname[b] == s_gt || bname[b] == s_le || bname[b] == s_ge)) { if (!(ls && rs)) serr("a byte string needs a byte string on the other side"); @@ -1856,6 +1873,7 @@ int bin(int minp) { if (xu > 0x40000000) xu = 0 - 1; xub = xu; xubat = here; lv = 0; lvk = LV_NONE; c = C_PLAIN; xzero = 0; xconst = 0; xlenloc = -1; xstamp = 0; xaf = 0; xvk = 0; + if (lk == 2 && blen[b] > 0) { xconst = 1; xcval = lkv; } // both sides constant: the value is known effect_value = 0; if (isptr(xty) || xty == tword) effect_value = left_mask | right_mask; if (fresh) c = C_FRESH; if (borrowed && borrowed[2] == 5 && (isptr(xty) || xty == tword) && (bname[b] == s_plus || bname[b] == s_minus)) xaf = borrowed; @@ -2032,7 +2050,7 @@ int blockexpr() { // constant expressions, evaluated while compiling: numbers, named constants, // addresses of globals and functions, unary minus and ~, parentheses, and the -// operators + - * | & << >>. used for array lengths, enum values, const +// operators * / % + - << >> & ^ | at c precedence. used for array lengths, enum values, const // initializers and asm operands. word cprim() { int v, s, mc; @@ -2040,6 +2058,10 @@ word cprim() { if (accept(s_minus)) return 0 - cprim(); if (tk == T_OP && tv == uname[2]) { next(); return ~cprim(); } if (accept(s_lp)) { v = cexpr(); expect(s_rp); return v; } + if (tk == T_ID && nloc > 0) { // a local const with a constant initializer (spec 3.14.2) + s = findloc(tv); + if (s >= 0 && (ltag[s] & LCONST) && !larr[s] && ltype[s] && treadonly(ltype[s])) { next(); return lkval[s]; } + } if (tk == T_ID) { s = lookup(tv); if (mstruct && !ginst) { mc = lookup(methname(mstruct, tv)); if (mc >= 0 && skind[mc] == K_CONST) s = mc; } @@ -2065,20 +2087,38 @@ word cprim() { die("expected a constant"); return 0; } -// compile an expression: a primary, then the operators that follow it. -int cexpr() { - int v = cprim(), o; +// a constant expression with c precedence: * / % above + -, above << >>, +// above &, above ^, above | (spec 3.14). binary operators by precedence +// climbing, each level reading its right side one level tighter. +int cprec(int o) { + if (o == bname[0] || o == bname[1] || o == bname[2]) return 10; + if (o == bname[3] || o == bname[4]) return 9; + if (o == bname[5] || o == bname[6]) return 8; + if (o == bname[13]) return 5; + if (o == bname[14]) return 4; + if (o == bname[15]) return 3; + return 0; +} +int cbin(int minp) { + int v = cprim(), o, p, w; while (tk == T_OP) { - o = tv; - if (o == bname[3]) { next(); v = v + cprim(); } - else if (o == bname[4]) { next(); v = v - cprim(); } - else if (o == bname[0]) { next(); v = v * cprim(); } - else if (o == bname[15]) { next(); v = v | cprim(); } - else if (o == bname[13]) { next(); v = v & cprim(); } - else if (o == bname[5]) { next(); v = v << cprim(); } - else if (o == bname[6]) { next(); v = v >> cprim(); } - else return v; + o = tv; p = cprec(o); + if (p == 0 || p < minp) return v; + next(); w = cbin(p + 1); + if (o == bname[0]) v = v * w; + else if (o == bname[1] || o == bname[2]) { + if (w == 0) { die("division by zero in a constant expression"); return 0; } + if (o == bname[1]) v = v / w; else v = v % w; + } + else if (o == bname[3]) v = v + w; + else if (o == bname[4]) v = v - w; + else if (o == bname[5]) v = v << w; + else if (o == bname[6]) v = v >> w; + else if (o == bname[13]) v = v & w; + else if (o == bname[14]) v = v ^ w; + else v = v | w; } return v; } +int cexpr() { return cbin(1); } diff --git a/src/compiler/lex.zc b/src/compiler/lex.zc index 712eff8..95415a6 100644 --- a/src/compiler/lex.zc +++ b/src/compiler/lex.zc @@ -533,8 +533,9 @@ int lex1() { while (isidc(ch)) { n = token_char(n, ch); nextch(); } tbuf[n] = 0; if (n == 1 && tbuf[0] == 'b' && ch == 34) { - n = 0; nextch(); + n = 0; dg = line; nextch(); while (ch >= 0 && ch != 34) { v = escch(); n = token_char(n, v); } + if (ch < 0) { tline = dg; serr("unterminated string literal"); } // spec 1.4.4 nextch(); tbuf[n] = 0; tk = T_BSTR; tv = intern(tbuf); return 0; @@ -556,8 +557,9 @@ int lex1() { return 0; } if (ch == 34) { - nextch(); + dg = line; nextch(); while (ch >= 0 && ch != 34) { v = escch(); n = token_char(n, v); } + if (ch < 0) { tline = dg; serr("unterminated string literal"); } // spec 1.4.4 nextch(); tbuf[n] = 0; tk = T_STR; tv = intern(tbuf); return 0; diff --git a/src/compiler/stmt.zc b/src/compiler/stmt.zc index 007306c..79e5fb8 100644 --- a/src/compiler/stmt.zc +++ b/src/compiler/stmt.zc @@ -144,7 +144,7 @@ int localdecl(int typed) { newsz = -1; newleaf = 0; if (tk == T_NUM) nomagic = 1; if (t && t[0] == TY_DYN && is_op(s_lb)) c = array_dynamic_literal(t); else c = expr(); rval(); nomagic = 0; kc = xconst && !xfloat; - if (kc) ltag[i] = ltag[i] | LCONST; + if (kc) { ltag[i] = ltag[i] | LCONST; lkval[i] = xcval; } if (c == C_FRESH) { lnsz[i] = newsz; lleaf[i] = newleaf; if (drop_tail == here) local_dropset[i] = 1; } if (t) fcoerce(t); vaddr(i); @@ -566,11 +566,19 @@ int casebody(int s0, int live0) { live = 0; return 0; } +// a case label seen twice in one switch is an error (spec 4.7.2): the second +// arm could never run. the first 256 labels of a switch are checked. +int case_seen(word seen, int n, int v) { + int i = 0; + while (i < n) { if (seen[i] == v) { serr("duplicate case label"); return n; } i++; } + if (n < 256) { seen[n] = v; n++; } + return n; +} // compile a switch statement. int kw_switch() { int sl = tline; int save = nloc, tmp, c, s0, live0, defent = -1, skip, nxt, bj, v, a = -1, st, tg = 0, cov = 0, gi, nb = 0, j, arm, bi; - word[8] pbn; word[8] pbl; + word[8] pbn; word[8] pbl; word[256] seen; int nseen = 0; stmtonly(); next(); expect(s_lp); c = expr(); st = xty; rval(); plainuse(c); expect(s_rp); @@ -609,6 +617,7 @@ int kw_switch() { expect(s_rp); if (nb && nb != pm_nf[gi]) serr("a case binds every field of its member"); // member() with no names matches by tag (spec 4.7.3) v = pm_tag[gi]; + nseen = case_seen(seen, nseen, v); if (v < 63) cov = cov | (1 << v); laddr(lslot[tmp]); op(O_LD); op(O_LD); lit(v); op(O_SUB); bj = fixnew(hole(), bj); op(O_JZ); @@ -633,6 +642,7 @@ int kw_switch() { } else { while (1) { v = cexpr(); + nseen = case_seen(seen, nseen, v); if (tg && v >= 0 && v < 63) cov = cov | (1 << v); laddr(lslot[tmp]); op(O_LD); if (tg) op(O_LD); diff --git a/src/js/tinyvm.ts b/src/js/tinyvm.ts index 51b390b..5ee29c7 100644 --- a/src/js/tinyvm.ts +++ b/src/js/tinyvm.ts @@ -179,6 +179,7 @@ export class TinyVM { should resume it after its event loop has run, whether or not input came. */ public polling: boolean = false; private outputBuffer: number[] = []; + private exitCode: number | null = null; private onOutput?: (byte: number) => void; private onMessage?: (msg: string) => void; @@ -439,7 +440,8 @@ export class TinyVM { return -1n; case 6: // time return BigInt(Math.floor(Date.now() / 1000)); - case 7: // exit + case 7: // exit: the run stops here with this status, as the native core's exit() does + this.exitCode = Number(BigInt.asIntN(32, a)) & 255; return 0n; case 8: // rand return BigInt(Math.floor(Math.random() * 0x7FFFFFFF)); @@ -781,6 +783,7 @@ export class TinyVM { mem = this.mem; bytes = this.bytes; view = this.view; mw = this.mw; // a grow may have moved them mem[Number(a)] = result; s = this.rs; r = this.rr; + if (this.exitCode !== null) { const code = this.exitCode; this.exitCode = null; return code; } break; } else if (Number(a) >= 0 && Number(a) < mw) { mem[Number(a)] = b; diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc index 149138b..4f96320 100644 --- a/src/runtime/rt.zc +++ b/src/runtime/rt.zc @@ -428,7 +428,7 @@ const int F_SHARE = 8; // table (dtab), its drop has run (it never runs again), and the collector found it // unreachable and runs its drop once the sweep is over. const int F_HASDROP = 16, F_DROPPED = 32, F_FINAL = 64, F_CYCLE = 128; // F_CYCLE: during gc_drops only, a candidate its own contents reach -int o3_depth, gc_pending; +int o3_depth, gc_pending, gc_cyc; // gc_cyc: the last sweep found a cycle with drops to run // flags live in these bits; the size class is above them. const int F_LOW = 65535; int hbump; word[32] freel; int gc_runs, gc_freed, gc_auto = 1; @@ -1456,7 +1456,7 @@ int o3_leave() { // an unreachable value with a drop that hasn't run is kept for one more cycle and // flagged F_FINAL, unless another such value reaches it (its parent drops first and // it waits for a later collection) and it doesn't reach itself (a cycle drops one -// value per collection). nothing runs user code here. +// value per sweep, and gc() repeats the sweep until the cycle is done). nothing runs user code here. int gc_drops() { int i, again = 1; word k; while (again) { @@ -1486,9 +1486,9 @@ int gc_drops() { k = dtab[i * 3]; if (k > 1 && (k[-4] & F_FINAL)) { // reached from another candidate: later. one in a cycle has nothing outside it - // to drop first, so it drops now (it is marked, so kept), and the rest of the - // cycle in later collections, one each, never twice - if ((k[-4] & 7) == 1) mark(k); else if (!(k[-4] & F_CYCLE)) k[-4] = k[-4] & ~F_FINAL; + // to drop first, so it drops now (it is marked, so kept); gc() then sweeps again + // for the rest of the cycle, one each, never twice + if ((k[-4] & 7) == 1) mark(k); else if (!(k[-4] & F_CYCLE)) k[-4] = k[-4] & ~F_FINAL; else gc_cyc = 1; k[-4] = k[-4] & ~F_CYCLE; } i++; @@ -1549,8 +1549,12 @@ unchecked int gc_sweep() { unchecked int gc() { int n; if (o3_depth) { gc_pending = 1; return 0; } + gc_cyc = 0; n = gc_sweep(); if (dcount) gc_finalize(); + // an unreachable cycle with drops finishes in this collection: each sweep drops one + // member (nothing outside the cycle has to go first), and every value drops once + while (gc_cyc) { gc_cyc = 0; n = n + gc_sweep(); if (dcount) gc_finalize(); } return n; } // the number of live heap objects, for tests and leak checks. diff --git a/src/tools/zc0.c b/src/tools/zc0.c index 6c62abf..229734c 100644 --- a/src/tools/zc0.c +++ b/src/tools/zc0.c @@ -396,16 +396,42 @@ static int64_t cprim(void) if (tk == T_ID && (s = find(ts)) && (s->kind == K_CONST || (s->kind != K_GVAR && s->defd))) { next(); return s->val; } die("constant expected"); return 0; } -static int64_t cexpr(void) +/* c precedence (spec 3.14.1): * / % above + -, above << >>, above &, ^, | */ +static int cprec(void) { - int64_t v = cprim(); - for (;;) { - if (accept("+")) v += cprim(); else if (accept("-")) v -= cprim(); - else if (accept("*")) v *= cprim(); else if (accept("|")) v |= cprim(); - else if (accept("&")) v &= cprim(); else if (accept("<<")) v <<= cprim(); - else if (accept(">>")) v >>= cprim(); else return v; + if (is("*") || is("/") || is("%")) return 10; + if (is("+") || is("-")) return 9; + if (is("<<") || is(">>")) return 8; + if (is("&")) return 5; + if (is("^")) return 4; + if (is("|")) return 3; + return 0; +} +static int64_t cbin(int minp) +{ + int64_t v = cprim(), w; int p; char o[3]; + while ((p = cprec()) && p >= minp) { + o[0] = ts[0]; o[1] = ts[1]; o[2] = 0; + next(); w = cbin(p + 1); + switch (o[0]) { + case '*': v *= w; break; + case '/': case '%': + if (w == 0) { die("division by zero in a constant expression"); return 0; } + if (w == -1) v = o[0] == '/' ? (int64_t)(0 - (uint64_t)v) : 0; + else v = o[0] == '/' ? v / w : v % w; + break; + case '+': v += w; break; + case '-': v -= w; break; + case '<': v = (int64_t)((uint64_t)v << (w & 63)); break; + case '>': v >>= (w & 63); break; + case '&': v &= w; break; + case '^': v ^= w; break; + default: v |= w; break; + } } + return v; } +static int64_t cexpr(void) { return cbin(1); } /* ---- types ---- zc0 has no types: a type is read and thrown away. all it has to know is the diff --git a/tests/conformance/1-source/string_unterminated.zc b/tests/conformance/1-source/string_unterminated.zc new file mode 100644 index 0000000..aca63d3 --- /dev/null +++ b/tests/conformance/1-source/string_unterminated.zc @@ -0,0 +1,5 @@ +// expect error: zc: line 4: unterminated string literal +// spec 1.4.4 (was appendix b.2.11): a string with no closing quote is reported where it starts +include rt; +fn main() { rt.put_s("never closed); return 0; } +// zc0: skip diff --git a/tests/conformance/3-expressions/const_expr_array_length.zc b/tests/conformance/3-expressions/const_expr_array_length.zc index 07434a9..c6f11e3 100644 --- a/tests/conformance/3-expressions/const_expr_array_length.zc +++ b/tests/conformance/3-expressions/const_expr_array_length.zc @@ -1,4 +1,4 @@ -// expect: 10/ +// expect: 8/ include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } fn main() { diff --git a/tests/conformance/3-expressions/const_expr_case_label.zc b/tests/conformance/3-expressions/const_expr_case_label.zc index 3f1e781..0c76d4b 100644 --- a/tests/conformance/3-expressions/const_expr_case_label.zc +++ b/tests/conformance/3-expressions/const_expr_case_label.zc @@ -1,9 +1,9 @@ -// expect: nine/ +// expect: seven/ include rt; fn main() { - int k = 9; + int k = 7; switch (k) { - case 1 + 2 * 3: rt.put_s("nine"); + case 1 + 2 * 3: rt.put_s("seven"); default: rt.put_s("other"); } rt.put_c(10); diff --git a/tests/conformance/3-expressions/const_expr_division.zc b/tests/conformance/3-expressions/const_expr_division.zc new file mode 100644 index 0000000..90db20c --- /dev/null +++ b/tests/conformance/3-expressions/const_expr_division.zc @@ -0,0 +1,7 @@ +// expect: 3/-3/2/ +include rt; +void pn(int x) { rt.put_n(x); rt.put_c(10); } +const Z = 6 / 2; +const N = -7 / 2; +const P = 1 + 6 / 3 * 2 - 3; +fn main() { pn(Z); pn(N + 0); pn(P); return 0; } diff --git a/tests/conformance/3-expressions/const_expr_division_by_zero.zc b/tests/conformance/3-expressions/const_expr_division_by_zero.zc new file mode 100644 index 0000000..f1011ff --- /dev/null +++ b/tests/conformance/3-expressions/const_expr_division_by_zero.zc @@ -0,0 +1,4 @@ +// expect error: division by zero in a constant expression +include rt; +const Z = 1 / (2 - 2); +fn main() { return 0; } diff --git a/tests/conformance/3-expressions/const_expr_division_rejected.zc b/tests/conformance/3-expressions/const_expr_division_rejected.zc deleted file mode 100644 index f095247..0000000 --- a/tests/conformance/3-expressions/const_expr_division_rejected.zc +++ /dev/null @@ -1,4 +0,0 @@ -// expect error: expected ';', found '/' -include rt; -const Z = 6 / 2; -fn main() { return 0; } diff --git a/tests/conformance/3-expressions/const_expr_local_const.zc b/tests/conformance/3-expressions/const_expr_local_const.zc new file mode 100644 index 0000000..46a8d55 --- /dev/null +++ b/tests/conformance/3-expressions/const_expr_local_const.zc @@ -0,0 +1,11 @@ +// expect: 5/11/7/ +include rt; +void pn(int x) { rt.put_n(x); rt.put_c(10); } +fn main() { + const n = 5; + const m = n * 2 + 1; + int[m] a; + const int k = 7; + pn(n); pn(len(a)); switch (7) { case k: pn(k); } + return 0; +} diff --git a/tests/conformance/3-expressions/const_expr_local_const_rejected.zc b/tests/conformance/3-expressions/const_expr_local_const_rejected.zc deleted file mode 100644 index edab64e..0000000 --- a/tests/conformance/3-expressions/const_expr_local_const_rejected.zc +++ /dev/null @@ -1,7 +0,0 @@ -// expect error: expected a constant -include rt; -fn main() { - const n = 5; - int[n] a; - return 0; -} diff --git a/tests/conformance/3-expressions/const_expr_modulo.zc b/tests/conformance/3-expressions/const_expr_modulo.zc new file mode 100644 index 0000000..3f32f46 --- /dev/null +++ b/tests/conformance/3-expressions/const_expr_modulo.zc @@ -0,0 +1,6 @@ +// expect: 1/10/ +include rt; +void pn(int x) { rt.put_n(x); rt.put_c(10); } +const Z = 7 % 2; +const Q = 1 + 20 % 7 * 3; +fn main() { pn(Z); pn(Q - 9); return 0; } diff --git a/tests/conformance/3-expressions/const_expr_modulo_rejected.zc b/tests/conformance/3-expressions/const_expr_modulo_rejected.zc deleted file mode 100644 index 4c54ffe..0000000 --- a/tests/conformance/3-expressions/const_expr_modulo_rejected.zc +++ /dev/null @@ -1,4 +0,0 @@ -// expect error: zc: line 3: -include rt; -const Z = 7 % 2; -fn main() { return 0; } diff --git a/tests/conformance/3-expressions/const_expr_neg_not_parens.zc b/tests/conformance/3-expressions/const_expr_neg_not_parens.zc index 6e96bdc..714990a 100644 --- a/tests/conformance/3-expressions/const_expr_neg_not_parens.zc +++ b/tests/conformance/3-expressions/const_expr_neg_not_parens.zc @@ -1,4 +1,4 @@ -// expect: -3/-6/-1/24/ +// expect: -3/-4/-1/24/ include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } const A = -3; diff --git a/tests/conformance/3-expressions/const_expr_left_to_right.zc b/tests/conformance/3-expressions/const_expr_precedence.zc index 68e26ab..c173959 100644 --- a/tests/conformance/3-expressions/const_expr_left_to_right.zc +++ b/tests/conformance/3-expressions/const_expr_precedence.zc @@ -1,8 +1,9 @@ -// expect: 9/5/7/255/ +// expect: 7/5/7/255/16/ include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } const X = 1 + 2 * 3; const Y = 10 - 2 - 3; const V = 1 | 2 + 4; const W = ~0 & 255; -fn main() { pn(X); pn(Y); pn(V); pn(W); return 0; } +const S = 1 + 3 << 1 + 1 >> 1 - 1 | 6 & 2; +fn main() { pn(X); pn(Y); pn(V); pn(W); pn(S - 2); return 0; } diff --git a/tests/conformance/3-expressions/const_expr_xor.zc b/tests/conformance/3-expressions/const_expr_xor.zc new file mode 100644 index 0000000..30ca0a7 --- /dev/null +++ b/tests/conformance/3-expressions/const_expr_xor.zc @@ -0,0 +1,6 @@ +// expect: 5/7/ +include rt; +void pn(int x) { rt.put_n(x); rt.put_c(10); } +const Z = 7 ^ 2; +const Q = 1 ^ 2 | 4 & 4; +fn main() { pn(Z); pn(Q); return 0; } diff --git a/tests/conformance/3-expressions/const_expr_xor_rejected.zc b/tests/conformance/3-expressions/const_expr_xor_rejected.zc deleted file mode 100644 index eb1ad25..0000000 --- a/tests/conformance/3-expressions/const_expr_xor_rejected.zc +++ /dev/null @@ -1,4 +0,0 @@ -// expect error: zc: line 3: -include rt; -const Z = 7 ^ 2; -fn main() { return 0; } diff --git a/tests/conformance/4-statements/switch_duplicate_label_rejected.zc b/tests/conformance/4-statements/switch_duplicate_label_rejected.zc new file mode 100644 index 0000000..54f3b56 --- /dev/null +++ b/tests/conformance/4-statements/switch_duplicate_label_rejected.zc @@ -0,0 +1,6 @@ +// expect error: duplicate case label +// spec 4.7.2: a label seen twice in one switch is an error +include rt; +enum color { RED, GREEN }; +fn main() { int k = 1; switch (k) { case RED, GREEN: k = 2; case 2 - 1: k = 3; } return 0; } +// zc0: skip diff --git a/tests/conformance/4-statements/switch_duplicate_labels_first_wins.zc b/tests/conformance/4-statements/switch_duplicate_labels_first_wins.zc deleted file mode 100644 index 67a0204..0000000 --- a/tests/conformance/4-statements/switch_duplicate_labels_first_wins.zc +++ /dev/null @@ -1,11 +0,0 @@ -// expect: first/ -// spec 4.7.2: duplicate labels are not an error, the first matching case in source order runs -include rt; -void pn(int x) { rt.put_n(x); rt.put_c(10); } -void ps(char* s) { rt.put_s(s); rt.put_c(10); } -fn main() { - int k = 4; - switch (k) { case 4: ps("first"); case 4: ps("second"); } - return 0; -} -// zc0: skip diff --git a/tests/conformance/4-statements/switch_label_forms.zc b/tests/conformance/4-statements/switch_label_forms.zc index cfe2712..ef5e33b 100644 --- a/tests/conformance/4-statements/switch_label_forms.zc +++ b/tests/conformance/4-statements/switch_label_forms.zc @@ -1,5 +1,5 @@ // expect: neg/chr/sum/enum/ -// spec 4.7.1 and 3.14: labels are constant expressions (no precedence: 1 + 1 * 3 is 6) +// spec 4.7.1 and 3.14: labels are constant expressions (c precedence: 1 + 1 * 3 is 4) include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } enum color { RED, GREEN }; @@ -12,5 +12,5 @@ void t(int k) { } rt.put_c(10); } -fn main() { t(-1); t('a'); t(6); t(1); return 0; } +fn main() { t(-1); t('a'); t(4); t(1); return 0; } // zc0: skip diff --git a/tests/conformance/4-statements/switch_same_label_other_switch.zc b/tests/conformance/4-statements/switch_same_label_other_switch.zc new file mode 100644 index 0000000..bb52a94 --- /dev/null +++ b/tests/conformance/4-statements/switch_same_label_other_switch.zc @@ -0,0 +1,5 @@ +// expect: 2/ +// spec 4.7.2: equal labels in two different switches are fine +include rt; +fn main() { int k = 1; switch (k) { case 1: k = 2; } switch (k) { case 1: k = 9; case 2: rt.put_n(k); } rt.put_c(10); return 0; } +// zc0: skip diff --git a/tests/conformance/5-declarations/global_const_expr_init.zc b/tests/conformance/5-declarations/global_const_expr_init.zc index 8a9d30b..64a4d06 100644 --- a/tests/conformance/5-declarations/global_const_expr_init.zc +++ b/tests/conformance/5-declarations/global_const_expr_init.zc @@ -1,5 +1,5 @@ -// expect: 4/ -// spec 5.3.2 and 3.14.1 (left to right: ((1+2)*2)-2) +// expect: 3/ +// spec 5.3.2 and 3.14.1 (c precedence: 1+(2*2)-2) include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } int g = 1 + 2 * 2 - 2; diff --git a/tests/conformance/6-ownership/drop_gc_cycle.zc b/tests/conformance/6-ownership/drop_gc_cycle.zc index 01db6b8..d31dc6b 100644 --- a/tests/conformance/6-ownership/drop_gc_cycle.zc +++ b/tests/conformance/6-ownership/drop_gc_cycle.zc @@ -1,5 +1,5 @@ -// expect: 0 1 2 2 0/ -// destructors 6.15.3: two unreachable values with drops that reach each other are finalized one per collection (each is in a cycle, so nothing outside it drops first), each exactly once, then freed +// expect: 0 2 2 2 0/ +// destructors 6.15.3: two unreachable values with drops that reach each other are both finalized in one collection (each is in a cycle, so nothing outside it drops first), each exactly once, then freed include rt; int n; word g; diff --git a/tests/conformance/6-ownership/drop_gc_cycle_long.zc b/tests/conformance/6-ownership/drop_gc_cycle_long.zc new file mode 100644 index 0000000..3de3f1e --- /dev/null +++ b/tests/conformance/6-ownership/drop_gc_cycle_long.zc @@ -0,0 +1,24 @@ +// expect: 0 5 15 5 0/ +// destructors 6.15.3: an unreachable cycle of five values with drops is finalized whole by one collection, each value once, then freed +include rt; +int n, sum; +word g; +struct node { node* next; int v; void drop() { n++; sum = sum + this.v; } } +unchecked fn make() { + node* a = rt.new(node); node* p = a; int i; + a.v = 1; + for (i = 2; i <= 5; i++) { node* q = rt.new(node); q.v = i; p.next = q; p = q; } + p.next = a; + g = a; + return 0; +} +fn main() { + make(); + rt.gc(); rt.put_n(n); rt.put_c(32); + g = 0; + rt.gc(); rt.put_n(n); rt.put_c(32); rt.put_n(sum); rt.put_c(32); + rt.gc(); rt.put_n(n); rt.put_c(32); + rt.put_n(rt.dcount); rt.put_c(10); + return 0; +} +// zc0: skip diff --git a/tests/conformance/9-appendix-b/b2_15_duplicate_case.zc b/tests/conformance/9-appendix-b/b2_15_duplicate_case.zc index 63c30ec..e724c63 100644 --- a/tests/conformance/9-appendix-b/b2_15_duplicate_case.zc +++ b/tests/conformance/9-appendix-b/b2_15_duplicate_case.zc @@ -1,5 +1,5 @@ // expect error: duplicate -// spec appendix b.2.15: duplicate case labels are not diagnosed (4.7.2 says they are allowed) +// spec 4.7.2 (was appendix b.2.15): duplicate case labels are an error include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } fn main() { int k = 1; switch (k) { case 1: pn(1); case 1: pn(2); } return 0; } diff --git a/tests/conformance/9-appendix-b/b2_16_local_const_in_const_expr.zc b/tests/conformance/9-appendix-b/b2_16_local_const_in_const_expr.zc index 70093c9..649321f 100644 --- a/tests/conformance/9-appendix-b/b2_16_local_const_in_const_expr.zc +++ b/tests/conformance/9-appendix-b/b2_16_local_const_in_const_expr.zc @@ -1,5 +1,5 @@ // expect: 5/ -// spec appendix b.2.16 and 3.14.2: a local const is not a constant expression +// spec 3.14.2 (was appendix b.2.16): a local const is a constant expression include rt; void pn(int x) { rt.put_n(x); rt.put_c(10); } fn main() { const n = 5; int[n] a; pn(5); return 0; } diff --git a/tests/conformance/known.txt b/tests/conformance/known.txt index 8eeb0d3..685b089 100644 --- a/tests/conformance/known.txt +++ b/tests/conformance/known.txt @@ -3,8 +3,6 @@ # appendix b.2.8 (a global named in an initializer is its address) is documented behavior per 5.3.2 and 3.14.2: no entry 4-statements/o3_outside_function_ordinary_block.zc zy c top-level o3 { } is rejected; spec 4.10.2 says it is an ordinary block 9-appendix-b/b2_10_recovery_hides_errors.zc zy c appendix b.2.10 confirmed; by design per 8.2 -9-appendix-b/b2_15_duplicate_case.zc zy c appendix b.2.15 confirmed; 4.7.2 says duplicates are allowed -9-appendix-b/b2_16_local_const_in_const_expr.zc zy c appendix b.2.16 confirmed; 3.14.2 documents it 2-types/conv_argument_and_return.zc zc0 b types are read and dropped: no conversion, no sized cut (appendix a) 2-types/conv_assignment_to_float.zc zc0 b types are read and dropped: no conversion, no sized cut (appendix a) 2-types/conv_f32_rounds.zc zc0 b types are read and dropped: no conversion, no sized cut (appendix a) diff --git a/tests/test.sh b/tests/test.sh index c31cfb7..45883d0 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -733,6 +733,7 @@ s_js() { chk "$(node $T/jsvm/tinyvm.js examples/primes.zc | tail -1)" "widest gap: 72, after 31397" "js vm: primes" chk "$(node $T/jsvm/tinyvm.js tests/programs/growone.zc)" "12 1" "js vm: a block past the initial heap grows the machine" chk "$(node $T/jsvm/tinyvm.js tests/programs/trace.zc 2>&1 | md5sum)" "$(md5sum < tests/programs/trace.expected)" "js vm: backtraces read the return stack through gate op 56" + node $T/jsvm/tinyvm.js tests/programs/trace.zc >/dev/null 2>&1; chk "$?" "2" "js vm: a runtime fault stops the run with exit status 2 (syscall 7), as the native core does" chk "$(node $T/jsvm/tinyvm.js tests/programs/replace_owner.zc)" "$(cat tests/programs/replace_owner.expected)" "js vm: replacement ownership and cleanup" chk "$(node $T/jsvm/tinyvm.js tests/programs/embed.zc | tr '\n' '/')" "hello/world 11 42 5 255 8 -1 0 119 15 5 255 3 98 321 63/" "js vm: embed declarations read their files through node" if sh src/tools/findcc.sh --wasm >/dev/null 2>&1 && command -v wasm-ld >/dev/null; then diff --git a/tests/xref.baseline b/tests/xref.baseline index 2f000af..3011e75 100644 --- a/tests/xref.baseline +++ b/tests/xref.baseline @@ -308,6 +308,8 @@ lkeycap local_bind lkeyn local_bind zc_init lkeys local_bind lkeys_initial +lkval localdecl reserve_locals +lkval_initial lleaf assigncheck embed_bind expr localdecl reassign_free reserve_locals lleaf_initial lname newloc reserve_locals @@ -721,7 +723,7 @@ xc_on xc_init xc_src unit_stdin xcls array_cap array_len array_pop array_push array_resize chk_reset ctx_pop kw_asm kwcall quote_tokens xconst array_dynamic_literal array_literal_tokens bin call chk_reset closure drop_attach heap_box kw_share_value methcall postfix_tail primary printcall unary -xcval array_literal_tokens primary unary +xcval array_literal_tokens bin primary unary xfloat array_dynamic_literal array_len array_literal_tokens array_pop array_push array_resize bin blockexpr call closure drop_attach expr fcoerce femit ftest heap_box incdec kw_cfunc kw_for methcall postfix_tail primary printcall reassign_free ternary unary xgive argcheck args chk_reset xlend assigncheck expr unary |
