aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md4
-rw-r--r--docs/SPEC.md4
-rw-r--r--src/compiler/expr.zc4
-rw-r--r--src/compiler/types.zc6
-rw-r--r--src/lib/std.zc5
-rw-r--r--src/lsp/server.zc40
-rw-r--r--src/runtime/run.zc5
-rw-r--r--tests/drivers/lsp_test.js12
-rw-r--r--tests/drivers/runtime_image_test.py2
-rw-r--r--tests/programs/capture_long.zc26
-rw-r--r--tests/programs/struct_static_call.zc31
-rw-r--r--tests/programs/void_main.zc8
-rwxr-xr-xtests/test.sh5
13 files changed, 133 insertions, 19 deletions
diff --git a/TODO.md b/TODO.md
index d093cab..e0918a9 100644
--- a/TODO.md
+++ b/TODO.md
@@ -207,10 +207,6 @@ sorted by how hard each is to implement; the final full suite stays last.
items with an architecture note carry the design worked out so far, enough to
brief someone without re-reading the code.
-- [ ] hover on standard-library members (`std.Text.itoa`, `std.array_contains`, a group name) returns nothing in the language server, though completion shows the same documentation; a struct static function in the document itself does show its hover. find where `do_hover` resolves qualified library members and fix it, with a hover check.
-- [ ] `std.capture.end` writes its terminator at `rt.outbuf[rt.outn]`; when more than 256 characters were captured, `outn` equals the capacity and the 0 lands one word past the buffer. check and fix, with a regression program.
-- [ ] a bare call to a sibling static function inside a struct's method (`lim()` for a static `lim`) reports `lim is used but never defined`; resolve it like the bare struct constants now are (`mstruct` in the declarations part).
-- [ ] a run-only image accepts `void main()` from zc0 but the self-hosted compiler rejects it against `run.zc`'s `int main();` prototype; make the two agree.
- [ ] remaining zc0 gaps: string literals over 4095 bytes, `embed` of an array type inside a function, `x as T` with more than a single name (`x as int*`), zc0 silently accepting `.u` on a parameter, and integer literals (`010` is 8 in zc0, 10 in the compiler; zc0 saturates on overflow). `docs/history/43-stdlib/notes.md` still says zc0 reserves u/m/d: add a pointer forward rather than rewriting history.
- [ ] (in flight) verify both compilers against `docs/SPEC.md` with a fresh agent that did not write it: expectations derived from the spec text before running anything, one program per rule under `tests/conformance/<section>/`, a runner `tests/conformance/run.sh`, each failure classified as self-hosted bug, zc0 bug, or spec error, and appendix b confirmed item by item. then add the runner to `tests/test.sh`.
- [ ] fix the implementation bugs listed in `docs/SPEC.md` appendix b, once confirmed: `memdebug scrub;` rejected; enum members can't reference earlier members; a plain function redefinition silently shadows the first; a prototype after the definition hides it; `?:` with one int and one float branch gives garbage; `bool` not normalised; float global initializers rejected; a global initializer naming another global uses its address; `include std;` inside a function body; syntax-error recovery hiding later errors; unterminated `/*` and string literals; `x as int * y`; prototypes needing parameter names; `len(char*)` with typecheck off; duplicate `case` labels; local `const` in constant expressions; constant expressions without precedence (`1+2*3` is 9). fix the prose items the appendix lists too.
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 51ed5d1..8a6d089 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -667,7 +667,7 @@ a bare `name(params) { ... }` with no `fn` and no type is not a function declara
5.2.4 untyped functions (`fn`) infer parameter and result types by propagating constraints from calls and returns until a fixed point (README, 32-inferred-calls); the exact algorithm is not specified here. typed and untyped functions can be called interchangeably.
-5.2.5 `main`. after the unit compiles without errors, the runtime calls `main` with no arguments (declared parameters read unspecified values; `main(a, b)` prints 0 0 in the reference implementation). its return value is ignored and is not the exit status. 8.1.
+5.2.5 `main`. after the unit compiles without errors, the runtime calls `main` with no arguments (declared parameters read unspecified values; `main(a, b)` prints 0 0 in the reference implementation). its return value is ignored and is not the exit status; `main` may be declared `int`, `void` or untyped, in a run-only image as in `zy`. 8.1.
5.2.6 a function is defined once. redefining a name that already has a body is, per the rule, error: f: is defined twice; the reference implementation does not detect an ordinary redefinition (appendix b). a prototype must agree with the definition: error: f: doesn't match its earlier declaration.
@@ -702,7 +702,7 @@ a field of a struct cannot have a tag.
5.5.4 `e.name(args)` calls method `name` with `e` as `this`, unless the struct has a field `name` (a field wins). when `e` is an integer or `word` and exactly one struct has a method of that name, that method is called (error if several: name: is a method of more than one struct).
-5.5.5 a static function has no `this` and is called `Struct.name(args)`.
+5.5.5 a static function has no `this` and is called `Struct.name(args)`, or by its bare name `name(args)` inside the struct's own methods and static functions. a bare name there is the struct's own constant or static function first, so it wins over a top-level function or constant of the same name; a local variable or parameter still wins over both. outside the struct only `Struct.name(args)` reaches it.
5.5.6 `Struct.MAX`, `Struct.name(...)` with a misspelled member: error: MAXX: is not a static function of this struct; did you mean 'MAX'?
diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc
index c6d29c6..40de6ef 100644
--- a/src/compiler/expr.zc
+++ b/src/compiler/expr.zc
@@ -1014,7 +1014,9 @@ int primary() {
return C_PLAIN;
}
s = source_lookup(n);
- if (mstruct && !ginst) { ms = lookup(methname(mstruct, n)); if (ms >= 0 && skind[ms] == K_CONST) s = ms; }
+ // in a method of a struct a bare name is that struct's own constant or static function first, so it
+ // wins over a global (a top-level function or constant) of the same name
+ if (mstruct && !ginst) { ms = lookup(methname(mstruct, n)); if (ms >= 0 && (skind[ms] == K_CONST || (skind[ms] == K_FN && (sflag[ms] & F_STATIC)))) s = ms; }
if (s < 0 && rt.eq_w(n, "new")) s = source_lookup(s_new);
if (s >= 0) rs_put(toff, RS_SYM, rs_sym(s), s);
if (s >= 0 && skind[s] == K_KW) {
diff --git a/src/compiler/types.zc b/src/compiler/types.zc
index c643f0b..6dda110 100644
--- a/src/compiler/types.zc
+++ b/src/compiler/types.zc
@@ -497,8 +497,9 @@ int dtype() {
}
return 0;
}
-// the struct whose body or methods are being compiled, when it declares constants: a bare
-// name there is looked up as Struct.name before the global scope
+// the struct whose body or methods are being compiled: a bare name there is looked up as Struct.name
+// (a constant, or a static function called by its bare name) before the global scope, so the struct's
+// own wins over a global of the same name
char* mstruct;
// does a constant declaration start in a struct body? const NAME = e; const T NAME = e;
// (a field of a const type has no initializer, so the = tells them apart)
@@ -652,6 +653,7 @@ int kw_struct() {
}
accept(s_semi);
i = 0;
+ if (!gen) mstruct = n;
while (i < nm) { fn_method_compile = 1; methdef(n, mn[i], mq[i], mst[i], mty[i], mrt[i], mrg[i]); fn_method_compile = 0; i++; }
mdg = 0;
mstruct = msv;
diff --git a/src/lib/std.zc b/src/lib/std.zc
index 33ab1e9..772b887 100644
--- a/src/lib/std.zc
+++ b/src/lib/std.zc
@@ -73,7 +73,8 @@ int libinit() {
// rt.put_s("hello world!"); // nothing on the terminal
// std.capture.end(buf);
// rt.put_s(buf); // hello world!
-// the buffer holds 256 characters, and output past that is dropped.
+// the buffer holds 256 characters, and output past that is dropped. it has one
+// more word for the 0 that end() writes, so a full capture is still a string.
// one capture at a time, and the buffer belongs to whoever holds it.
struct capture {
// starts a capture: output goes into a new buffer instead of the terminal.
@@ -81,7 +82,7 @@ struct capture {
static char* start() {
int cap = 256;
rt.outmode = 2; rt.outn = 0; rt.outcap = cap;
- rt.outbuf = rt.new(cap);
+ rt.outbuf = rt.new(cap + 1);
return rt.outbuf;
}
// stops the capture and ends the buffer with a 0.
diff --git a/src/lsp/server.zc b/src/lsp/server.zc
index afa8de4..a43f910 100644
--- a/src/lsp/server.zc
+++ b/src/lsp/server.zc
@@ -1761,6 +1761,12 @@ int hover_method(int i) {
if (!found) ls = le + 1;
}
if (!found) return 0;
+ return hover_method_at(i, sg);
+}
+// the markdown of the method whose signature starts at byte sg of document i: the
+// signature up to the body, a ; or the end of the line, then the docs above it
+int hover_method_at(int i, int sg) {
+ int base = doff[i], size = dsize[i], k, se;
// the signature: up to the body, a ; or the end of the line
se = sg;
while (se < size && dc(base, se) != 10 && dc(base, se) != '{' && dc(base, se) != ';') se++;
@@ -1919,17 +1925,24 @@ int hover_global(int i, int kind) {
r = rs_find(hoff);
if (r >= 0 && run.rsrec[r * run.RS_W + 1] == run.RS_SYM && run.rsrec[r * run.RS_W + 3] >= 0 && kind != 2) {
s = run.rsrec[r * run.RS_W + 3];
- if (run.skind[s] != run.K_KW && bs_find(run.sname[s])) { hover_fill(bs_load(bs_k)); return hn; }
+ if (run.skind[s] != run.K_KW && bs_find(run.sname[s])) { bs_hover(); return hn; }
}
if (kind != 2) { kt = kwtext(); if (kt) return kw_put(kt); }
- if (kind == 1) { if (bs_find(hname)) { hover_fill(bs_load(bs_k)); return hn; } hover_blob(i); return hn; }
+ if (kind == 1) { if (bs_find(hname)) { bs_hover(); return hn; } hover_blob(i); return hn; }
if (hover_scan(i)) { hover_fill(i); return hn; }
if (hover_method(i)) return hn;
if (kind == 0 && hover_local(i)) return hn;
- if (kind != 2 && bs_find(hname)) { hover_fill(bs_load(bs_k)); return hn; }
+ if (kind != 2 && bs_find(hname)) { bs_hover(); return hn; }
hover_blob(i);
return hn;
}
+// is symbol s declared in an embedded library source (the text of std or icon, included
+// from tinyvm:std.zc and tinyvm:icon.zc)? such a symbol is a user symbol to the compiler
+// (rs_sym sets 16 for it) but its text is not in the document, so hover looks it up by
+// its qualified name in the library source, as it does for the runtime.
+int hover_embedded_sym(int s) {
+ return s >= 0 && run.sdsrc && run.sdsrc[s] && bs_name(run.sdsrc[s]) && bs_source(run.sdsrc[s]) >= 0;
+}
// the hover at the cursor. the compiler's record of the name decides what it
// is: a local is its declaration, a parameter the header that names it, and a
// symbol is looked up as that kind of symbol. a name with no record is found by
@@ -1950,7 +1963,9 @@ int do_hover(int i) {
else if (k == run.RS_FIELD && v >= 0) hover_named(i, v, 4);
else if (k == run.RS_LOCAL) hover_local(i);
else if (k == run.RS_SYM) {
- if ((v & 15) == run.K_GVAR || (v & 15) == run.K_GARR) {
+ // a symbol from an embedded library source (std, icon) is above the user's symbols but is not in the document: its declaration is found in that source by its qualified name
+ if ((v & 16) && hover_embedded_sym(run.rsrec[r * run.RS_W + 3])) hover_global(i, 1);
+ else if ((v & 15) == run.K_GVAR || (v & 15) == run.K_GARR) {
s = run.rsrec[r * run.RS_W + 3]; q = 0; o = 0 - 1;
while (q < run.rsn) { if (run.rsrec[q * run.RS_W + 3] == s && lt_find(run.rsrec[q * run.RS_W]) >= 0) { o = run.rsrec[q * run.RS_W]; break; } q++; }
if (o >= 0) hover_named(i, o, 3); else if (v & 16) hover_global(i, 2); else hover_global(i, 1);
@@ -2261,16 +2276,29 @@ int bs_method(int dd, char* name, int dot) {
}
return 0 - 1;
}
+// did the last bs_find_in hit find a method of a struct (bs_off is its name), not a top-level declaration
+int bs_meth;
+// the hover of the declaration the last successful bs_find found: a method by its line, anything else
+// by the scan that found it
+int bs_hover() {
+ int dd = bs_load(bs_k), q;
+ if (!bs_meth) return hover_fill(dd);
+ q = bs_off;
+ while (q > 0 && dc(doff[dd], q - 1) != 10) q--;
+ while (q < bs_off && (dc(doff[dd], q) == 32 || dc(doff[dd], q) == 9)) q++;
+ hn = 0; hover_method_at(dd, q);
+ return hn;
+}
// locate a declaration inside one source, after removing its module prefix.
int bs_find_in(int k, char* name) {
int n = 0, dot = -1, o, dd = bs_load(k);
while (name[n]) { if (name[n] == '.' && dot < 0) dot = n; n++; }
if (dot > 0 && name[dot + 1]) {
o = bs_method(dd, name, dot);
- if (o >= 0) { bs_k = k; bs_off = o; return 1; }
+ if (o >= 0) { bs_k = k; bs_off = o; bs_meth = 1; return 1; }
} else if (dot < 0) {
set_hname(name);
- if (hover_scan(dd)) { bs_k = k; bs_off = decl_off(doff[dd], hdl, hdle); return 1; }
+ if (hover_scan(dd)) { bs_k = k; bs_off = decl_off(doff[dd], hdl, hdle); bs_meth = 0; return 1; }
}
return 0;
}
diff --git a/src/runtime/run.zc b/src/runtime/run.zc
index a8f82e2..19a5234 100644
--- a/src/runtime/run.zc
+++ b/src/runtime/run.zc
@@ -8,9 +8,10 @@ include rt;
//
// cat build/run/rt.zc src/runtime/run.zc examples/hello.zc | ./zc0 >build/run/genome.h
-// the program's entry point, written by the program (see start).
+// the program's entry point, written by the program (see start). the prototype is untyped so
+// that main may be declared int, void or untyped, as in any other program: its value is ignored.
namespace;
-int main();
+fn main();
namespace run;
include rt;
diff --git a/tests/drivers/lsp_test.js b/tests/drivers/lsp_test.js
index 0f4abe1..2ad9181 100644
--- a/tests/drivers/lsp_test.js
+++ b/tests/drivers/lsp_test.js
@@ -199,6 +199,18 @@ const tmsrc = 'struct point {\n\tlet x;\n\t// the sum of the parts\n\tint sum(in
hv = hover(tmsrc, 7, at(tmsrc, 7, 'sum'));
check(hv && hv.includes('int sum(int k)') && hv.includes('the sum of the parts'), 'a typed method shows its return type and parameters', hv);
+// members of the embedded standard library hover like the document's own: the group's static
+// function, the group, and a top-level function, each with the comment from std.zc
+const stdsrc = 'include rt, std;\nfn main() { char[24] b; rt.put_s(std.Text.itoa(5, b)); return std.array_contains; }\n';
+hv = hover(stdsrc, 1, at(stdsrc, 1, 'itoa'));
+check(hv && hv.includes('static int itoa(int n, char* dst)') && hv.includes('writes n in decimal into dst'), 'hover on a std group function shows its signature and doc from std.zc', hv);
+hv = hover(stdsrc, 1, at(stdsrc, 1, 'Text'));
+check(hv && hv.includes('struct Text') && hv.includes('helpers for char* strings'), 'hover on a std group shows the comment of the group', hv);
+hv = hover(stdsrc, 1, at(stdsrc, 1, 'array_contains'));
+check(hv && hv.includes('array_contains<T>') && hv.includes('true when an element equals value'), 'hover on a top-level std function shows its doc', hv);
+hv = hover(stdsrc, 1, at(stdsrc, 1, 'put_s'));
+check(hv && hv.includes('int put_s(char* s)'), 'hover on a runtime function still works beside the std ones', hv);
+
// externs are declarations, so they draw no "never reassigned" hint
const ext = 'extern double sqrt(double);\nextern double pow(double x, double y);\nfn main() {\n\treturn 0;\n}\n';
r = run([init(1), open(A, ext)]);
diff --git a/tests/drivers/runtime_image_test.py b/tests/drivers/runtime_image_test.py
index d214613..6c44349 100644
--- a/tests/drivers/runtime_image_test.py
+++ b/tests/drivers/runtime_image_test.py
@@ -18,6 +18,8 @@ cases = [
('enhancements', 'include rt; fn main() { rt.put_n(17 % 5); rt.put_c(32); rt.put_n(17 / 5); }', b'2 3'),
('array algorithms', Path('examples/learn/31-array-algorithms.zc').read_text(), Path('examples/learn/31-array-algorithms.out').read_bytes()),
('owned pop', 'include rt; int*.u take() { int*[?] a = rt.new(int*[?]); int* p = rt.new(int); *p = 8; a.push(p); return a.pop(); } fn main() { let p = take(); rt.put_n(rt.isobj(p)); rt.put_c(32); rt.put_n(*p); }', b'1 8'),
+ # main may be void: run.zc's prototype of main must not fix its type (docs/SPEC.md 5.2.5)
+ ('void main', Path('tests/programs/void_main.zc').read_text(), b'void main\n'),
]
prefix = b''.join(Path(f).read_bytes() + b'\n' for f in ['src/runtime/rt.zc', 'src/core/core.zc', 'src/runtime/run.zc'])
for name, source, expected in cases:
diff --git a/tests/programs/capture_long.zc b/tests/programs/capture_long.zc
new file mode 100644
index 0000000..52736a4
--- /dev/null
+++ b/tests/programs/capture_long.zc
@@ -0,0 +1,26 @@
+namespace;
+include rt, std;
+
+// a capture holds 256 characters and drops the rest. end() must still put its
+// 0 inside the buffer, even when the capture was full: the length is 256 and the
+// last kept character is the 256th one written (index 255, 'a' + 255 % 26 = 'v').
+// a block allocated right after the buffer is checked afterwards: a 0 written one
+// word past the buffer would land in its header and read as a bad or freed block.
+memdebug on; // header checks make a write into a neighbouring block visible
+fn capture_long() {
+ let buf = std.capture.start();
+ int* next = rt.new(8);
+ int i = 0;
+ while (i < 300) { rt.put_c('a' + i % 26); i++; }
+ std.capture.end(buf);
+ next[0] = 7; // stops with "bad heap pointer" if its header was overwritten
+ rt.put_n(next[0]); rt.put_c(10); // 7
+ return buf;
+}
+memdebug off; // the header check only searches 64 words back, so the long buffer is read unchecked
+fn main() {
+ char* buf = capture_long();
+ rt.put_n(rt.len_w(buf)); rt.put_c(10); // 256
+ rt.put_c(buf[255]); rt.put_c(10); // v
+ return 0;
+}
diff --git a/tests/programs/struct_static_call.zc b/tests/programs/struct_static_call.zc
new file mode 100644
index 0000000..73fc0fc
--- /dev/null
+++ b/tests/programs/struct_static_call.zc
@@ -0,0 +1,31 @@
+namespace;
+include rt;
+
+// a static function of a struct can be called by its bare name inside the struct's
+// methods and static functions, as a struct constant can. the struct's own function
+// wins over a top-level one of the same name; outside the struct only S.name reaches it.
+int lim() { return 100; }
+int base() { return 7; }
+
+struct S {
+ int v;
+ static int lim() { return 3; }
+ static int twice(int x) { return x * 2; }
+ static int both() { return lim() + twice(4); }
+ int get() { return lim() + this.v; }
+ int scaled() { return twice(this.v) + later(); }
+ int top() { return base(); }
+ static int later() { return 1000; }
+}
+
+int main() {
+ S s;
+ s.v = 5;
+ rt.put_n(S.lim()); rt.put_c(32); // 3
+ rt.put_n(lim()); rt.put_c(32); // 100, the global one
+ rt.put_n(S.both()); rt.put_c(32); // 3 + 8
+ rt.put_n(s.get()); rt.put_c(32); // 3 + 5
+ rt.put_n(s.scaled()); rt.put_c(32); // 10 + 1000, a static function declared later
+ rt.put_n(s.top()); rt.put_c(10); // 7, a global that the struct has no member of
+ return 0;
+}
diff --git a/tests/programs/void_main.zc b/tests/programs/void_main.zc
new file mode 100644
index 0000000..b4fde70
--- /dev/null
+++ b/tests/programs/void_main.zc
@@ -0,0 +1,8 @@
+namespace;
+include rt;
+
+// main may be declared void: the runtime ignores what main returns (docs/SPEC.md 5.2.5, 8.1.3),
+// so a run-only image, whose run.zc declares int main();, accepts it as zy does.
+void main() {
+ rt.put_s("void main"); rt.put_c(10);
+}
diff --git a/tests/test.sh b/tests/test.sh
index 298c356..c800e8f 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -128,6 +128,7 @@ run(){ # $1 = vm command, $2 = label
chk "$($vm < tests/programs/genmeth.zc | md5sum)" "$(md5sum < tests/programs/genmeth.expected)" "$l methods of generic structs are compiled per type arguments"
chk "$($vm < tests/programs/static.zc)" "14 5 9" "$l static functions of a struct are called as Struct.name(args)"
chk "$($vm < tests/programs/struct_const.zc)" "24 49 24 23 19 55 12 99" "$l constants declared in a struct are reached as Struct.NAME, and by bare name in its methods"
+ chk "$($vm < tests/programs/struct_static_call.zc)" "3 100 11 8 1010 7" "$l a static function of a struct is called by its bare name in its methods, and wins over a global of the same name"
chk "$($vm < tests/programs/methodown.zc)" "7 9" "$l struct methods and functions return owned objects (.u)"
chk "$($vm < tests/programs/typedmethods.zc)" "14 5 13" "$l struct methods declare a return type and typed parameters"
chk "$($vm < tests/programs/enumscope.zc)" "0 5 6 blue" "$l enum members are reached as Enum.MEMBER, also in a case"
@@ -158,6 +159,9 @@ hello world!42
1
1
0" "$l capture.start() collects output and end() hands back the string"
+ chk "$($vm < tests/programs/capture_long.zc)" "7
+256
+v" "$l a full capture is still terminated inside its buffer"
chk "$($vm < tests/programs/sys.zc)" "2 zy time" "$l syscalls: write, read, time"
chk "$($vm < tests/programs/memleak.zc)" $'leak: line 8 3 words\n1' "$l memdebug names a live allocation"
chk "$($vm < tests/programs/uaf.zc)" $'zc: line 10: warning: use after free: this storage\'s lifetime ended on line 9\nzy: use after free from line 7' "$l memdebug: use after free"
@@ -353,6 +357,7 @@ s_runonly() {
runonly wide tests/programs/wide.zc
runonly selfpatch examples/selfpatch.zc
runonly gc tests/programs/gc.zc
+ runonly voidmain tests/programs/void_main.zc
runonly float tests/programs/float.zc
runonly ftruth tests/programs/ftruth.zc
runonly ftext tests/programs/ftext.zc