aboutsummaryrefslogtreecommitdiff
path: root/PROPOSALS.md
blob: a31e5c0f2f34b6bf9e60e54596cde9cad7cd69e9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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.