diff options
Diffstat (limited to 'PROPOSALS.md')
| -rw-r--r-- | PROPOSALS.md | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/PROPOSALS.md b/PROPOSALS.md new file mode 100644 index 0000000..a31e5c0 --- /dev/null +++ b/PROPOSALS.md @@ -0,0 +1,132 @@ +# proposals + +features i would like to see in zc, written after working through the compiler, the +analyzer, the language server and the libraries. each one says what it is, why it +would help, how it would sit on the machinery that already exists, and what it +costs. they are ordered roughly by how much day-to-day pain they would remove. none +of them is implemented. + +## 1. source positions at run time + +a program that reads far out of range today can exit with status 0 and no +message. `int f(word p) { return p[100000000]; }` called with 0 does exactly that. +when something does go wrong at run time there is no line number, no function +name and no backtrace, so the only tools are print statements and `memdebug`. + +the compiler already knows the position of every token (`tpack`), and check mode +already turns positions into diagnostics. the proposal is a line table emitted +next to each function: pairs of (code offset, packed position), written into a +side section of the image the way `docs on;` already writes documentation. a +fault, a failed `rt.abort`, or a new `rt.trace()` would walk the return stack, +map each return address through the table and print `file:line in function` for +every frame. the interpreter, the microcore and the javascript vm share the image, +so they would all get it from one change to the runtime. the jit and llvm +backends would need their own map from native addresses back to vm code; i +haven't checked how much of that they keep today. + +cost: image size (a few words per statement; it could be gated by a +`lines on;` switch like `docs on;`) and a little work in the fault path. the +compiler's own genome could ship without it. + +## 2. checked indexing in debug builds + +every heap block has a header that knows its payload size, and growable arrays +know their length. a `bounds on;` switch, in the spirit of `memdebug on;`, could +make `p[i]` on a pointer into the heap check `0 <= i < payload` and stop with the +line from proposal 1. typed arrays (`T[n]`, `T[?]`) would check against their +declared or current length. the analyzer already knows when an index is a +constant inside a fixed array, so those checks can be dropped at compile time, +and o3 regions would skip them as they skip other metadata. + +cost: one compare and branch per index when on, nothing when off. the +enhancement system could make it a single opcode on the cores that matter. + +## 3. a `drop` method for resources that are not memory + +the ownership analyzer already decides exactly where an owned value dies and +emits `rt.free` or `rt.recycle` there. file descriptors, actor processes and +arena handles die at the same points but nothing closes them. a struct could +declare `fn drop() { ... }`, and the compiler would call it wherever it would +have freed an owned value of that type, before the memory goes back. `actor.zc` +(which closes processes by hand) and `std.Host` would be the first users. + +this keeps the language free of a `defer` keyword: the place where cleanup runs +is already computed, and the analyzer would refuse the same leaks it refuses for +memory. the open question is moves into unchecked code, which would have to +either forbid owned values with a `drop` or document that unchecked code takes +responsibility. + +## 4. returning a borrow tied to an argument + +`find` in a string or `first` in a list wants to hand back a pointer into its +argument. today that result is either a plain word (unchecked) or an owned copy. +the lend rules added to the analyzer already track that a borrow came from a +particular owner inside one function. extending that to signatures, + +``` +char*.from(s) skip_space(char*.m s); +``` + +would let the caller's analyzer treat the result as a borrow of whatever was +passed as `s`, so freeing or moving `s` while the result is live becomes an +error at the call site. the callee is checked to return only borrows of `s`. + +cost: a new tag form in the type grammar and one more case in `argcheck` and +`retcheck`. no run-time cost. + +## 5. test blocks + +``` +test "itoa handles negatives" { + char[24] buf; + expect(std.Text.eq(std.Text.itoa(-5, buf), "-5")); +} +``` + +`zy --test file.zc` would compile the file with its `test` blocks, run each one, +and print a line per test with the position of the first failed `expect`. +without `--test` the blocks are skipped at parse time and cost nothing. the +actor library already shows how a library can add a keyword with +`run.syntax` and `run.statement_hook`, so this could live in a `test` module +rather than in the compiler. the test suite's many small programs with expected +output strings in `tests/test.sh` could gradually move into the files they test. + +## 6. formatted output as a library syntax + +printing a value with a label today is a chain of `rt.put_s`, `rt.put_n` and +`rt.put_c(10)`. a parse word in std could expand + +``` +std.say("x = {x}, name = {name}\n"); +``` + +at compile time into that chain, using the type of each name to pick +`put_n`, `put_s`, `put_bs` or the float printer. nothing happens at run time +that the hand-written chain doesn't already do, and the compile-time code is +the kind `lang.zc` already demonstrates. + +## 7. a parity mode between zc0 and the self-hosted compiler + +while testing the module scanner i found that the self-hosted compiler bound +`||` tighter than `&&`, while zc0 and the readme follow c. the bootstrap +fixpoint can't see this kind of bug: g1 and g2 are both compiled by the same +wrong table, so they match. a `make parity` target could compile every program +in the zc0 subset (the `tests/programs` that zc0 accepts, and the genome sources +themselves) with both compilers and run both images, comparing output. a +mismatch means the two front ends disagree about the language. the corpus is +already there; only the runner is missing. + +## 8. untyped const at the top level and in structs + +`const k = 5;` now works at the top level and inside functions. struct-level +constants (`struct Text { const MAX = 24; }`, reached as `std.Text.MAX`) would +let libraries keep their limits next to the functions that use them, instead of +in the module's global namespace. + +## 9. a typo pass over field and method names + +the missing-name diagnostics now suggest the closest function or module export. +field access (`p.lenght`) and static calls (`std.Text.itao`) still only say that +the name is wrong. the same edit-distance search over the struct's fields and +methods would give "did you mean `length`?", and the language server's +change-to quick fix would apply to it unchanged. |
