aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--TODO.md29
-rw-r--r--docs/SPEC.md17
-rw-r--r--editors/vscode/syntaxes/zc.tmLanguage.json2
-rw-r--r--src/compiler/expr.zc14
-rw-r--r--src/lsp/server.zc3
-rw-r--r--tests/conformance/3-expressions/dyn_array_const_index_warning.zc2
-rw-r--r--tests/conformance/3-expressions/dyn_array_index_negative.zc2
-rw-r--r--tests/conformance/3-expressions/dyn_array_index_out_of_range.zc2
-rw-r--r--tests/conformance/3-expressions/dyn_array_pop_empty.zc2
10 files changed, 58 insertions, 19 deletions
diff --git a/README.md b/README.md
index 75d01fb..ed6e902 100644
--- a/README.md
+++ b/README.md
@@ -176,6 +176,8 @@ functions are declared like C, return type first: `int f(int x)`, `node*.u make(
array types put the element type first. `T[n]` has its size in the type and lives in place. `T[]` also lives in place; its length is inferred from an initializer such as `int[] a = {1, 2, 3};` and cannot grow. `T[?]` can also be initialized in a function with a list, such as `int[?] a = {1, 2};`. it is a growable array: a small handle object that owns its items, made with `rt.new(T[?])` or `rt.new(T[?], n)`, indexed with a bounds check, and changed with `push(a, x)`, `pop(a)` and `resize(a, n)`. `len(a)` works on both (for `T[n]` it's the constant) and `cap(a)` gives the room before the next grow. `fixed T[?]` is a `T[?]` that can't change size: `rt.new(fixed T[?], n)` needs the length, and `push`, `pop` and `resize` on it are compile errors. a `fixed T[?]` can't be passed where a `T[?]` is expected, the other way round is fine, and a constant index outside a `T[n]` is caught at compile time. the items belong to the array, so freeing it frees them, and objects stored into a `T*[?]` are its children like any container's.
+**bounds checks.** every index is checked at run time unless you say otherwise. a `T[?]` is checked against its length, a `T[n]` with a variable index against `n` (`i | (n - 1 - i)` is negative exactly when `i` is outside, so it is a subtract, an or and a compare inline), and a pointer against the payload of the heap block it starts, through the `rt.bnd` enhancement and `rt.bchk` to report. a pointer into the middle of a block, a frame array, a global or memory managed by hand has no known length and is not checked, and a heap block's length is its buddy capacity (`rt.new(4)` holds 11 words), so a pointer check catches running off the block, not off the size you asked for. a constant index 0 or one the analyzer knows is inside the allocation, and an index that a loop test `i < k` with constant `k` has already compared are not checked again. a failed check prints `zy: index I out of range (length N)` and the backtrace, and stops. `unbounded` turns the checks off, used like `unchecked`: `unbounded fn f()`, `unbounded { ... }`, `unbounded stmt;` and `unbounded;` for the rest of the unit. o3 implies it. the runtime, the core, the compiler, the language server, `run.zc` and `build.zc` start with `unbounded;` (zc0 accepts the word and ignores it, so the genome's first stage is unchanged).
+
the array intrinsics also accept receiver syntax: `a.len()`, `a.cap()`, `a.push(x)`, `a.pop()` and `a.resize(n)`. fixed arrays provide `len()` and `cap()` but reject `push()`, `pop()` and `resize()`. [the learning example](examples/learn/19-growable.zc) shows both receiver forms.
`fixed const T[n]` and `const fixed T[n]` both make the array cells and aliases read-only. element writes, increments, compound assignments, and rebinding a `const fixed T[?]` handle are rejected even when `typecheck off;` is active. a read-only array reference cannot become a mutable reference. a formal parameter such as `const fixed int[2] values` accepts either read-only or mutable arrays. element values can still be copied into ordinary mutable scalars. [the learning example](examples/learn/33-const-fixed.zc) shows both qualifier orders and a read-only formal parameter.
@@ -419,7 +421,7 @@ fn main() {
what it gives up, as asked: variables declared in o3 code must be typed (`let` and untyped parameters are errors), they have no address (`&x` and naming one from `asm` are errors), an untyped `rt.new(n)` in o3 code is `new_oz` (no type stamp, and the words asked for are zero), and an o3 function skips the frame stack check in its prologue. typed `rt.new(T)` and `rt.new(T, n)` stamp the header and zero the payload, in o3 the same as outside it. what it gets: the compiler hands each o3 function body and each o3 block to the core as a region (install kind 2: the range of vm words plus its typed scalar variables' frame slots, each with a register), and the core compiles the region right away. the cpu has 7 registers for variables. a variable takes the one most recently freed by a scope that closed, or the next unused one, so loop counters in sibling loops share a register, and a variable declared when all 7 are busy stays in memory. a register shared by several slots gets written back to all of them when the region exits, and they never hold different live values, since their scopes don't overlap.
-inside the core, a region is cut into basic blocks at jump targets, branch fallthroughs and call return points. an empty jump is then folded into its target, a block with one predecessor is pasted onto that predecessor, and a straight loop is rotated so one iteration is a single block that branches to itself. a call, a return, a value left on the stack, or a second branch stays where it is, and an entry is never pasted over. each block is lifted to ssa the way enhancements are: the vm stack becomes values, and only what's still on it at the end of the block is written to memory. the frame pointer and the promoted slots become variables that live in registers across the whole region, so `i++` on an o3 int is one add. enhancements are spliced into the block, so `/`, `%` and `<<` become one instruction or a magic-number sequence. a call to `ld8`, `st8`, `ld16`, `st16`, `ld32` or `st32` from o3 code emits opcodes 612..617, and the region lifts those instead of calling out. every block then goes through the same optimizer (folding, algebra, cse, dce, division by constants). in o3 code `a[i]` on a `T[?]` is compiled inline with its bounds check (`outside(i, len)`, one enhancement), and `len(a)` is a load. a `fixed T[?]` in an o3 variable does better: its items pointer and length can't change, so the compiler gives it two hidden o3 variables holding them. they're loaded when the region starts and whenever the array variable is assigned, so indexing is a compare against a register and an indexed load, with no loads through the handle.
+inside the core, a region is cut into basic blocks at jump targets, branch fallthroughs and call return points. an empty jump is then folded into its target, a block with one predecessor is pasted onto that predecessor, and a straight loop is rotated so one iteration is a single block that branches to itself. a call, a return, a value left on the stack, or a second branch stays where it is, and an entry is never pasted over. each block is lifted to ssa the way enhancements are: the vm stack becomes values, and only what's still on it at the end of the block is written to memory. the frame pointer and the promoted slots become variables that live in registers across the whole region, so `i++` on an o3 int is one add. enhancements are spliced into the block, so `/`, `%` and `<<` become one instruction or a magic-number sequence. a call to `ld8`, `st8`, `ld16`, `st16`, `ld32` or `st32` from o3 code emits opcodes 612..617, and the region lifts those instead of calling out. every block then goes through the same optimizer (folding, algebra, cse, dce, division by constants). o3 code is unbounded, so `a[i]` on a `T[?]` there is compiled inline as an address with no bounds check, and `len(a)` is a load. a `fixed T[?]` in an o3 variable does better: its items pointer and length can't change, so the compiler gives it two hidden o3 variables holding them. they're loaded when the region starts and whenever the array variable is assigned, so indexing is an indexed load, with no loads through the handle.
the backends: on arm64 the region becomes one piece of native code. variables sit in w0..w7, block values in w8..w14 and w26..w28, constants are folded into instructions where they fit, loads from a fixed address are one `ldr`, and blocks branch to each other directly with no dispatch and no stack checks. on the llvm build the region becomes one function, with the variables in allocas that mem2reg turns into ssa across blocks, and it goes through llvm's O3 pipeline. on the plain interpreter, a small region interpreter runs the optimized blocks with the variables in locals.
diff --git a/TODO.md b/TODO.md
index e601b10..9a2f52f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -371,6 +371,33 @@ of the open list.
scratchpad `base/`, not in the zip); `make parity`. the 35% fixed-array cost
is worth a look before merging (hoist checks out of loops whose bound is the
array length, extend `proven`).
+ resumed on work/bounds (ported onto main f75ce6a, conflicts in base, decl
+ and expr; the context save is 50 words, `fbnd` at 49 after `dfr_base`).
+ done: o3call's main is `unbounded`; `o3_bounds` and `bounds_hi` now expect
+ no check (o3 implies unbounded); new boundsrules, bounds_fixed, bounds_heap,
+ zc0unbounded (also a run-only image) with test.sh lines; bounds_fixed and
+ bounds_heap are in parity.known; docs in README ("bounds checks"), SPEC
+ 2.6.1, 3.7.4, 3.13.4, 4.10.1, new 4.15, 5.1.1 and the keyword table, the
+ server's `kwtext` and `iskeyword`, the vscode grammar. genome 395477 to
+ 399590 words (+4113, +1.0%); the self-compile takes about 1.1 to 1.2 s
+ before and after. the fixed-array cost: the `for (i < K)` proof never fired
+ (the loop shape needed a local on the right of `<`), fixed now, so a
+ constant-bound loop costs nothing; the inline check is now plain ops
+ (`i | (n - 1 - i)` < 0) instead of the `outside` enhancement, which the
+ x86-64 jit calls rather than inlines. best of 3, ms, main / port / final:
+ hash-index kernel jit 37 / 72 / 44, interpreter 412 / 504 / 542; loop with a
+ variable bound jit 55 / 113 / 69, interpreter 741 / 907 / 865; loop with a
+ constant bound jit 46 / 100 / 53, interpreter 663 / 822 / 690. the plain-op
+ check is slightly slower than the enhancement on the interpreter for an
+ unprovable index, and probably on arm64, where enhancements are inlined (not
+ measured here). still open: pointers, jit 52 to 279 ms and interpreter 710
+ to 1141 ms on a 256-word sweep (`rt.bnd` is an enhancement call on x86-64);
+ a variable loop bound is not hoisted (needs a loop-entry test of `n <= N`, or
+ of the heap block's payload for a pointer, and a flag the body tests).
+ inlining enhancements in the x86-64 jit would help both. also fixed here:
+ four conformance expectations on main lacked the backtrace line that line
+ tables print (3-expressions/dyn_array_*), and test.sh's run-only gc sed
+ still looked for `f >= 990`.
- spec conformance (`agents/spec-conformance`, last 808886b; the first,
interrupted attempt is `agents/spec-conformance-first-attempt`, already
folded in). done: sections 1 source (114 programs, zy 103 ok), 2 types
@@ -480,7 +507,7 @@ brief someone without re-reading the code.
- [ ] continue removing `unchecked` from the runtime. a reverted trial showed: `copy_w`/`fill_w` pass the analyzer when their gate stores go through `syscall(SYS_COPY, ...)`, `dgrow` with `own(w, it)` instead of `w[-2] = it`, `dpush` with `own` before the store, `dnew` as is; but the checked versions changed behavior (7 program checks failed, `tests/programs/share.zc` hung with a checked `cons`), so checked code inserts cleanup these functions can't afford. each needs its contract taught to the analyzer. `free`, `recycle`, `recycle_leaf`, `gc` are the allocator and collector the analyzer's own cleanup calls: document them as the irreducible escapes. `src/lib/actor.zc` has about 20 more.
- [ ] revise diagnostics, comments and learning/reference prose using professional compiler documentation guidance, preserving lowercase and adding message regressions. touches messages everywhere, so do it when few other compiler changes are in flight.
- [ ] (partly done, see "agent work at the end of the session") test blocks: `test "name" { ... }` with `expect(cond)`, compiled only by `zy --test file.zc`, which runs each block and prints one line per test with the position of the first failed `expect`; skipped at parse time otherwise. build it as a library through `run.syntax` and `run.statement_hook`, like `src/lib/actor.zc`.
-- [ ] (partly done, see "agent work at the end of the session") bounds checking on by default, `unbounded` to turn it off (function prefix, block, top-level `unbounded;`, mirroring `unchecked`), implied by `o3`. architecture: indexing is compiled in the postfix `[` handling of the expressions part; growable arrays already check (`dat`, `range_jz`, the `outside` enhancement, `proven()` removing checks a loop test already did). fixed arrays `T[n]` with a variable index get the same inline check against the constant `n`. pointers get a runtime call (for example `rt.bchk(p, i)`) that checks only when `p` is the start of a live heap block (header check word `p ^ MAGIC`, `paylen`), skipping constant index 0 and sizes the analyzer knows (`lnsz`). reports go through `oob`. the runtime, core, compiler parts, server, `run.zc` and `build.zc` start with `unbounded;`, which zc0 accepts and ignores (genome bytes unchanged). keep std, icon and actor checked unless a function touches raw memory on purpose. measure interpreter time before and after.
+- [x] (done on work/bounds, see "agent work at the end of the session") bounds checking on by default, `unbounded` to turn it off (function prefix, block, top-level `unbounded;`, mirroring `unchecked`), implied by `o3`. architecture: indexing is compiled in the postfix `[` handling of the expressions part; growable arrays already check (`dat`, `range_jz`, the `outside` enhancement, `proven()` removing checks a loop test already did). fixed arrays `T[n]` with a variable index get the same inline check against the constant `n`. pointers get a runtime call (for example `rt.bchk(p, i)`) that checks only when `p` is the start of a live heap block (header check word `p ^ MAGIC`, `paylen`), skipping constant index 0 and sizes the analyzer knows (`lnsz`). reports go through `oob`. the runtime, core, compiler parts, server, `run.zc` and `build.zc` start with `unbounded;`, which zc0 accepts and ignores (genome bytes unchanged). keep std, icon and actor checked unless a function touches raw memory on purpose. measure interpreter time before and after.
- [ ] after the fuzzing work, add more general functionality to the standard library.
- [ ] (partly done, see "agent work at the end of the session") struct destructors: a struct may declare `fn drop()`, called wherever the compiler frees an owned value of that type, before the memory is released. architecture: the compiler frees owned locals in `freevar` (analyzer part), called from `exit_vars`, reassignment and explicit `rt.free`/`rt.recycle` calls (`s_free`, `s_recycle` handling in the call code); when the local's static type is a pointer to a struct with a `Name.drop` method, emit a call to it first. objects freed by the collector or inside unchecked code do not run `drop`: document that. first users: file descriptors in `std.Host`, processes in `actor.zc`.
- [x] (done on work/line-tables, see "agent work at the end of the session") runtime source positions. architecture: at each statement start (`stmt()`) record (code address `here`, line); per function a permanent data block [entry, end, name, file, count, pairs...] linked into a runtime list (`rt.line_blocks`), or appended to run-only images by `build_image` like the symbol blob. `lines on;`/`lines off;` like `docs on;`; on for user programs, off for the genome unless the growth is small. a new syscall op returns the return-stack pointer (the return stack is in vm memory, 0x1A4100..0x1B40FF, growing up); implement it in `src/core/core.c`, `src/micro/micro.c`, `src/js/tinyvm.ts` and the wasm port. `rt.trace()` walks the return stack, printing ` at file:line in function` for words inside a known function range. the runtime's fatal reports (`oob`, bad heap pointer, double free, out of memory, frame overflow) print the trace after their unchanged first line. check mode skips all of it.
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 5618a5e..6df55f6 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -60,6 +60,7 @@ a name that looks like `x.y` where `x` is not a known module is the three tokens
| `memdebug` | S | T | | 4.11 |
| `hints` | S | T | | 4.12 |
| `unchecked` | S | | | 4.9, 5.1 |
+| `unbounded` | S | | | 4.15, 5.1 |
| `o3` | S | | | 4.10 |
| `asm` | S | | E | raw vm words (4.13) |
| `run.method` | S | T | | register a receiver method (7.7) |
@@ -190,7 +191,7 @@ u32 z = 0; z--; // z is 4294967295
### 2.6 arrays
-2.6.1 `T[n]`: `n` is a constant expression (3.14) and the array lives in place: in a frame, a global, or inside a struct. its value is its address. `sizeof(T[n])` is the words the elements take (packed for sized types). there is no run-time bound check; a constant index outside `0..n-1` is error: the index is out of range for this array.
+2.6.1 `T[n]`: `n` is a constant expression (3.14) and the array lives in place: in a frame, a global, or inside a struct. its value is its address. `sizeof(T[n])` is the words the elements take (packed for sized types). a variable index is checked at run time against `n` (3.7.4) unless the code is unbounded (4.15); a constant index outside `0..n-1` is error: the index is out of range for this array.
2.6.2 `T[]` is an in-place array whose length comes from its initializer: `int[] a = {1, 2, 3};` has length 3. without an initializer, error: T[] needs an initializer to determine its length.
@@ -426,7 +427,7 @@ pn((int)(p + 1) - (int)p); // 1: p + 1 moves one word
3.7.3 `&x` is the address of a variable, field or element. it applies to a local (which then lives in memory and cannot be an o3 variable), a global, a field and an element of a non-packed array. an element of a packed array has no address (2.3.3).
-3.7.4 indexing a new object (`rt.new(int)[0]`) is error: index a new object only after binding it to a name. indexing a `T[?]` is bounds checked (3.13). indexing a `T[n]` or a pointer is not checked at run time.
+3.7.4 indexing a new object (`rt.new(int)[0]`) is error: index a new object only after binding it to a name. indexing is bounds checked at run time unless the function, block, statement or unit is unbounded or the code is o3 (4.15). a `T[?]` is checked against its length (3.13), a `T[n]` against `n`, and a pointer only when it points at the first word of a live heap block, against that block's payload (a buddy block rounds the payload up: `rt.new(4)` has 11 words). a pointer into the middle of a block, a frame array, a global or memory managed by hand is not checked. a constant index 0, and an index a loop test `i < k` with constant `k` (or `i < len(a)`) has already proved, are not checked again. a failed check prints `zy: index I out of range (length N)` and stops, as in 3.13.4.
3.7.5 an index must be a number (typecheck on).
@@ -480,7 +481,7 @@ pn((int)(p + 1) - (int)p); // 1: p + 1 moves one word
3.13.3 `push(a, x)` appends and grows; `pop(a)` removes and returns the last element; `resize(a, n)` sets the length, zero-filling new elements and dropping the rest (freeing owned elements). `push` and `pop` on aggregate element types are errors (push takes scalar or reference elements; use resize for aggregates). `push`, `pop`, `resize` on a `fixed` array: error: a fixed array can't grow or shrink.
-3.13.4 indexing: `a[i]` with `i < 0` or `i >= len(a)` prints `zy: index I out of range (length N)` on the output and stops the program (`rt.halt`, exit status 0). `pop` on an empty array reports the same with index -1. a constant index known to be out of range also produces a compile-time warning: `zc: line N: warning: out-of-bounds access: index 2 is outside length 2 (storage declared or allocated on line N)`, and the program still runs.
+3.13.4 indexing (outside unbounded and o3 code, 4.15): `a[i]` with `i < 0` or `i >= len(a)` prints `zy: index I out of range (length N)` on the output and stops the program (`rt.halt`, exit status 0). `pop` on an empty array reports the same with index -1. a constant index known to be out of range also produces a compile-time warning: `zc: line N: warning: out-of-bounds access: index 2 is outside length 2 (storage declared or allocated on line N)`, and the program still runs.
```
int[?] a = {1, 2};
push(a, 3); a.push(4);
@@ -611,7 +612,7 @@ switch (s) { case circle(r): pn(r); case rect(w, h): pn(w * h); case dot: ps("do
### 4.10 o3
-4.10.1 `o3 { ... }` marks a block, `o3 T x = e;` one variable, and `o3 T f(...) { ... }` / `o3 fn f(...)` a function, for the optimising tier. o3 changes speed, not meaning, subject to these restrictions on code in an o3 region:
+4.10.1 `o3 { ... }` marks a block, `o3 T x = e;` one variable, and `o3 T f(...) { ... }` / `o3 fn f(...)` a function, for the optimising tier. o3 changes speed, not meaning, except that o3 code is unbounded (4.15), subject to these restrictions on code in an o3 region:
- every variable and parameter must be typed (errors: o3 variables need a type, o3 parameters need a type).
- no address-of on an o3 variable and no naming one in `asm` (errors: an o3 variable has no address, asm can't reach an o3 variable).
- no closures (3.15.3).
@@ -655,11 +656,17 @@ int y = 7; asm { y @ 1 + y ! } // y is 8
4.14.8 defer works in `unchecked` code: the analyzer's cleanup is off there, but the deferred code runs. a defer keeps the checked or unchecked mode it was written in. a function that falls off its end runs its function-level defers before returning 0.
+### 4.15 unbounded
+
+4.15.1 `unbounded stmt` (a block or a single statement) compiles the statement without index bounds checks (3.7.4). `unbounded;` at top level does it for the rest of the unit. `unbounded fn f() {}` (also `unbounded T f(...)`, a method or a closure) makes one function unbounded; the prefix combines with `unchecked`, `immediate` and `o3` in any order. o3 code is unbounded. `unbounded` changes no result of a program whose indexes are in range.
+
+4.15.2 the runtime, the core, the compiler, the language server, `run.zc` and `build.zc` are unbounded; std, icon and actor are checked except where a function reads headers on purpose.
+
## 5. declarations at the top level
### 5.1 items
-5.1.1 a unit is a sequence of items. an item is: a function definition or prototype, a global variable, `const`, `struct`, `type`, `enum`, `include`, `embed`, `namespace`, `typecheck`, `docs`, `lines`, `memdebug`, `hints`, `enhance`, `extern`, `unchecked;`, or a use of a top-level parse word (7.2).
+5.1.1 a unit is a sequence of items. an item is: a function definition or prototype, a global variable, `const`, `struct`, `type`, `enum`, `include`, `embed`, `namespace`, `typecheck`, `docs`, `lines`, `memdebug`, `hints`, `enhance`, `extern`, `unchecked;`, `unbounded;`, or a use of a top-level parse word (7.2).
5.1.2 `struct`, `type`, `typecheck`, `docs`, `lines`, `namespace`, `enhance` and `extern` are valid only at the top level. `enum`, `embed`, `memdebug`, `hints` and `const` also appear as statements in functions. `include` is top-level only (5.12.7).
diff --git a/editors/vscode/syntaxes/zc.tmLanguage.json b/editors/vscode/syntaxes/zc.tmLanguage.json
index fab026b..3869418 100644
--- a/editors/vscode/syntaxes/zc.tmLanguage.json
+++ b/editors/vscode/syntaxes/zc.tmLanguage.json
@@ -91,7 +91,7 @@
},
{
"name": "keyword.other.zc",
- "match": "\\b(as|unchecked|asm|quote|sizeof|typeof|len|cap|push|pop|resize|o3|include|embed|memdebug|fixed|typecheck)\\b"
+ "match": "\\b(as|unchecked|unbounded|asm|quote|sizeof|typeof|len|cap|push|pop|resize|o3|include|embed|memdebug|fixed|typecheck)\\b"
},
{
"name": "keyword.operator.memory.zc",
diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc
index c76b74c..1536744 100644
--- a/src/compiler/expr.zc
+++ b/src/compiler/expr.zc
@@ -1322,12 +1322,14 @@ int range_jz(int ix, int src, int fld) {
return 0;
}
// the inline check of a fixed array T[n]: the index is on top of the stack and
-// stays there. one "outside" enhancement against the constant length and a
-// branch to the oob report, the same shape as range_jz.
+// stays there. i is outside 0..n-1 exactly when i | (n - 1 - i) is negative:
+// one subtract, one or and one compare, then a branch to the oob report. no
+// enhancement: the x86-64 jit calls one rather than inlining it.
int fixed_chk(int n) {
- int k = lookup(s_outside), j;
- if (k < 0 || skind[k] != K_ENH) return 0;
- lit(0); op(O_PICK); lit(n); op(XOP0 + sval[k]);
+ int j;
+ lit(0); op(O_PICK); lit(n - 1); op(O_SWAP); op(O_SUB);
+ lit(1); op(O_PICK); op(17);
+ lit(0); op(O_LT);
j = hole(); op(O_JZ);
lit(n); callname(s_oob);
setlit(j, here);
@@ -1753,7 +1755,7 @@ int bin(int minp) {
if (top) {
ft_nops++;
if (ft_nops > 1) ft_iv = -1;
- else if (bname[b] == s_lt && blen[b] > 0 && il >= 0 && ft_ir >= 0 && !lf && !rf && !ls && !rs) { ft_iv = il; ft_bd = ft_ir; }
+ else if (bname[b] == s_lt && blen[b] > 0 && il >= 0 && (ft_ir >= 0 || ft_ck >= 0) && !lf && !rf && !ls && !rs) { ft_iv = il; ft_bd = ft_ir; }
}
lv = 0; lvk = LV_NONE; c = C_PLAIN; xzero = 0; xconst = 0; xlenloc = -1; xstamp = 0; xaf = 0; xvk = 0;
effect_value = 0; if (isptr(xty) || xty == tword) effect_value = left_mask | right_mask;
diff --git a/src/lsp/server.zc b/src/lsp/server.zc
index 9a903f0..52e5e9a 100644
--- a/src/lsp/server.zc
+++ b/src/lsp/server.zc
@@ -391,7 +391,7 @@ int isopc(int c) {
// is the name n one of the words the highlighter shows as keywords even when the
// compiler has no record for it.
int iskeyword(int n) {
- return n == run.s_fn || n == run.s_var || n == run.s_const || n == run.s_else || n == run.s_as || n == run.s_unchecked || n == run.s_immediate
+ return n == run.s_fn || n == run.s_var || n == run.s_const || n == run.s_else || n == run.s_as || n == run.s_unchecked || n == run.s_unbounded || n == run.s_immediate
|| n == run.s_case || n == run.s_default || n == run.s_do || n == run.s_while || n == run.s_enum || n == run.s_include || n == run.s_memdebug
|| n == run.s_enhance || n == run.s_micro || n == run.s_vm || n == run.s_struct || n == run.s_type || n == run.s_typedef || n == run.s_typecheck || n == run.s_static;
}
@@ -1683,6 +1683,7 @@ char* kwtext() {
if (hname_is("const")) return "const [type] NAME = e / const fixed T[N] name~declares a compile-time constant or read-only storage. fixed and const may appear in either order; read-only storage cannot be modified or converted to a mutable reference.";
if (hname_is("else")) return "else~the other branch of an if. after an enhance's body, else names the fallback function that runs where the enhancement can't.";
if (hname_is("as")) return "e as T~changes the type of an expression without changing its bits.";
+ if (hname_is("unbounded")) return "unbounded fn f() { } / unbounded { } / unbounded;~turns index bounds checks off for a function, a block, a statement, or the rest of the unit. o3 code is unbounded too.";
if (hname_is("unchecked")) return "unchecked fn f() { } / unchecked { } / unchecked;~turns the lifetime analyzer off for a function, a block, or the rest of the unit. the code is not checked, so the analyzer's rules don't apply to it.";
if (hname_is("immediate")) return "immediate fn name() { }~a parse word. it runs during compilation whenever its name shows up as a statement, inside an expression or at the top level.";
if (hname_is("case")) return "case v:~a label in a switch. each case ends where the next one begins, so cases don't fall through.";
diff --git a/tests/conformance/3-expressions/dyn_array_const_index_warning.zc b/tests/conformance/3-expressions/dyn_array_const_index_warning.zc
index 36890d7..9639128 100644
--- a/tests/conformance/3-expressions/dyn_array_const_index_warning.zc
+++ b/tests/conformance/3-expressions/dyn_array_const_index_warning.zc
@@ -1,4 +1,4 @@
-// expect: zc: line 5: warning: out-of-bounds access: index 2 is outside length 2 (storage declared or allocated on line 4)/zy: index 2 out of range (length 2)/
+// expect: zc: line 5: warning: out-of-bounds access: index 2 is outside length 2 (storage declared or allocated on line 4)/zy: index 2 out of range (length 2)/ at <stdin>:5 in main/
include rt;
fn main() {
int[?] a = {1, 2};
diff --git a/tests/conformance/3-expressions/dyn_array_index_negative.zc b/tests/conformance/3-expressions/dyn_array_index_negative.zc
index c87e786..023951e 100644
--- a/tests/conformance/3-expressions/dyn_array_index_negative.zc
+++ b/tests/conformance/3-expressions/dyn_array_index_negative.zc
@@ -1,4 +1,4 @@
-// expect: zy: index -1 out of range (length 2)/
+// expect: zy: index -1 out of range (length 2)/ at <stdin>:7 in main/
include rt;
void pn(int x) { rt.put_n(x); rt.put_c(10); }
fn main() {
diff --git a/tests/conformance/3-expressions/dyn_array_index_out_of_range.zc b/tests/conformance/3-expressions/dyn_array_index_out_of_range.zc
index eff8061..e613b70 100644
--- a/tests/conformance/3-expressions/dyn_array_index_out_of_range.zc
+++ b/tests/conformance/3-expressions/dyn_array_index_out_of_range.zc
@@ -1,4 +1,4 @@
-// expect: 2/zy: index 2 out of range (length 2)/
+// expect: 2/zy: index 2 out of range (length 2)/ at <stdin>:8 in main/
include rt;
void pn(int x) { rt.put_n(x); rt.put_c(10); }
fn main() {
diff --git a/tests/conformance/3-expressions/dyn_array_pop_empty.zc b/tests/conformance/3-expressions/dyn_array_pop_empty.zc
index 6f5fdd0..ce08fca 100644
--- a/tests/conformance/3-expressions/dyn_array_pop_empty.zc
+++ b/tests/conformance/3-expressions/dyn_array_pop_empty.zc
@@ -1,4 +1,4 @@
-// expect: zy: index -1 out of range (length 0)/
+// expect: zy: index -1 out of range (length 0)/ at <stdin>:6 in main/
include rt;
void pn(int x) { rt.put_n(x); rt.put_c(10); }
fn main() {