diff options
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | src/compiler/compiler.zc | 185 | ||||
| -rw-r--r-- | src/lsp/server.zc | 75 | ||||
| -rw-r--r-- | tests/drivers/hint_fixes_test.js | 16 | ||||
| -rw-r--r-- | tests/langerrors/const_local_no_init.zc | 8 | ||||
| -rw-r--r-- | tests/langerrors/const_local_write.zc | 9 | ||||
| -rw-r--r-- | tests/programs/const_local.zc | 11 | ||||
| -rw-r--r-- | tests/programs/precedence.zc | 13 | ||||
| -rwxr-xr-x | tests/test.sh | 2 |
9 files changed, 277 insertions, 46 deletions
@@ -97,7 +97,7 @@ check mode records errors instead of printing them and continues after an error, - **diagnostics** on open, change and save, with an id echoed on every reply, `MethodNotFound` for unknown requests and `-32002` before `initialize`. - **definition and references** (`textDocument/definition`, `textDocument/references`): a local, parameter or field goes to its declaration by the offset the compiler recorded, and a function, global, constant, struct or type goes to where the compiler saw it defined, in an included file too (the file is found from the document's directory upward, then as written in the `include`). references lists the uses in the document, with the declaration when asked. built-in names navigate to their embedded source when one is available. - **module completions**: after an imported namespace and a dot, completion lists that module's exported members with signatures and documentation; it does not add unrelated local or module names. module hovers and completions cover `rt`, `run`, `core`, `lsp`, `std` and `icon`. -- **import quick fixes**: compiler diagnostics for missing imports or unqualified exports offer a workspace edit. the edit adds the needed `include` without duplicating an existing import, qualifies bare exports, and preserves namespace declarations, comments, and crlf line endings. the editor client applies only edits for the current document version, while keeping protocol version epochs separate from editor document versions. +- **import quick fixes**: compiler diagnostics for missing imports or unqualified exports offer a workspace edit. a bare name is looked up in every module: the runtime and compiler modules, loaded files, and the libraries under `src/lib` read from source even when they are not included, including static groups (`itoa` becomes `std.Text.itoa` with `include std;`). a misspelled name gets the closest defined name or module export (`putc` becomes `rt.put_c`). the edit adds the needed `include` without duplicating an existing import, qualifies bare exports, and preserves namespace declarations, comments, and crlf line endings. the editor client applies only edits for the current document version, while keeping protocol version epochs separate from editor document versions. - **semantic tokens** (`textDocument/semanticTokens/full`) in 12 types: keyword, type, function, variable, string, number, operator, comment, parameter, property, macro, enumMember. token kinds come from the compiler's symbol tables and declarations. check mode records each resolved name and source location, following the compiler's lookup order: locals and parameters precede globals, and built-ins are checked last. a local named `cap` is a variable wherever it is in scope, and the built-in `cap` only applies to uses outside that scope. - **formatting** (`textDocument/formatting`, and `zclsp --format < file.zc` on the command line): a pretty-printer in the spirit of clang-format with `ColumnLimit: 0`. it keeps the author's line breaks and sets the indent of every line (from braces, open parentheses and unfinished statements; `case` labels are indented one level inside their `switch` by default), the spaces after commas and around `=`, `==`, `&&` and the like, the blank lines between lines, and the final line break. it never moves a token to another line, and it leaves strings, comments, `asm` and `vm` blocks and everything after a backtick as written, so the tokens of a program are the same before and after. options come from a `.zcformat` file in the document's directory or the nearest directory above it, one `Key: value` per line (`#` starts a comment), then from the editor's tab size and `insertSpaces`, then from the defaults: `UseTab: true`, `IndentWidth: 4` (the spaces of a level when `UseTab` is false), `MaxEmptyLines: 1`, `ContinuationIndent: 1`, `NormalizeSpacing: true`, `CaseIndent: 1`, `FinalNewline: true`. `CaseIndent: 0` aligns each `case` label with its `switch`. `tests/drivers/format_test.js` formats every `.zc` file in the repository and checks that the tokens are unchanged and that formatting twice gives the same text. - **streaming and cancelling**: a check of a document with 100 or more top-level items sends the diagnostics found so far each time the item count doubles, so errors near the top appear before a large file is done. before each of those points the server queries the host (`core.syscall(42)`) for pending input. if input is waiting, the check is dropped without publishing because newer text may replace the current document. the check publishes once no newer input is waiting. smaller documents are checked in one go. @@ -167,7 +167,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. statements are declarations, `if`/`else`, `while`, `do`/`while`, `for`, `switch`, `break`, `continue`, `return`, `{ }` blocks, `asm { }` and expressions. `do` body `while (` cond `);` runs the body once before the test, and `continue` jumps to the test. `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. statements are declarations, `if`/`else`, `while`, `do`/`while`, `for`, `switch`, `break`, `continue`, `return`, `{ }` blocks, `asm { }` and expressions. `do` body `while (` cond `);` runs the body once before the test, and `continue` jumps to the test. `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. functions are declared like C, return type first: `int f(int x)`, `node*.u make()`, `.u grab()` (a tag on its own), `void log(char* s)`. `fn f(a, b)` starts without explicit parameter or return types; constraints from calls and returned values propagate through callers and callees until inference reaches a fixed point, including recursive calls anchored by typed callees. conflicting argument types or an unknown reassignment leave the affected type unresolved instead of choosing a type. the learning example [32](examples/learn/32-inferred-calls.zc) shows this propagation and an owned raw-list return. a generic function lists its type parameters after its name, `T first<T>(*list<T> l)`, the same place a call gives explicit ones, `first<int>(l)`. the return type can use them before they're declared: while it's read, an unknown name is taken as a type parameter, and the list after the name has to confirm it. function types are written the same way, `int(int, *char)`, and `fn(int)` for one that returns a word. the earlier `fn f(...) -> T` spelling is gone. diff --git a/src/compiler/compiler.zc b/src/compiler/compiler.zc index 40d83cf..9c24af9 100644 --- a/src/compiler/compiler.zc +++ b/src/compiler/compiler.zc @@ -2128,7 +2128,8 @@ int add_binop(char* nm, int prec, char* code) { int p = intern(nm), i = binfind(p), a, n; if (i < 0) { reserve_binary(nbin + 1); i = nbin; nbin++; } bname[i] = p; bprec[i] = prec; - if (prec < 0) { bprec[i] = 0 - prec; blen[i] = prec; bseq[i] = 0; addkop(p); return i; } + // && (-1) and || (-2) short-circuit: they bind below |, and && above ||, as in c + if (prec < 0) { bprec[i] = 3 + prec; blen[i] = prec; bseq[i] = 0; addkop(p); return i; } bseq[i] = seqasm(code); blen[i] = seqtop - bseq[i]; addkop(p); // and its compound assignment, x op= y @@ -6861,6 +6862,11 @@ int primary() { } if (s >= 0 && skind[s] == K_GARR) { lit(sval[s]); if (stype[s]) xty = stype[s]; return C_PLAIN; } if (s >= 0 && skind[s] == K_ENH) return enhcall(s); + if (s < 0 && is_op(s_dot)) { + // Text.itoa with no std: a library's static function, reported as one missing name + peekat(0); + if (pk_tk == T_ID && export_owner(methname(n, pk_tv))) { next(); n = methname(n, tv); next(); } + } if (s < 0) s = fwdfn_at(n, callpos); if (is_op(s_lt) && (sflag[s] & F_GEN)) { // f<int, char*>(...) next(); ngbind = 0; @@ -7696,6 +7702,7 @@ int stmt_inner() { s = lookup(tv); if (s >= 0 && skind[s] == K_KW && (sflag[s] & F_STMT)) { kwcall(s, 0); return 0; } } + if (untyped_const()) { next(); localdecl(2); return 0; } if (tstart()) { localdecl(1); return 0; } if (tk == T_OP) { if (tv == s_semi) { next(); return 0; } @@ -7776,8 +7783,16 @@ int lt_note(int o, int t) { return 0; } // compile a local declaration, typed or let-style. +// does `const name =` start here? a const with no type takes the initializer's type. +int untyped_const() { + if (tk != T_ID || tv != s_const) return 0; + peekat(0); if (pk_tk != T_ID || istypename(pk_tv)) return 0; + peekat(1); return pk_tk == T_OP && (pk_tv == s_eq || pk_tv == s_semi || pk_tv == s_comma); +} +// typed is 1 after a type, 0 after let, and 2 after an untyped const. int localdecl(int typed) { - int n, i, c, k, t = 0, tg = 0, pp, kc, kp = ifkp, gty = 0, gok = !typed && ifon && !dry, o, vr; word iq, it; + int n, i, c, k, t = 0, tg = 0, pp, kc, kp = ifkp, gty = 0, gok = !typed && ifon && !dry, o, vr, ro = typed == 2; word iq, it; + if (ro) typed = 0; if (typed) { dtype(); t = dty; tg = dtag; if (t == tvoid) serr("a variable can't be void"); } if ((o3on || o3one) && !typed) serr("o3 variables need a type"); while (1) { @@ -7823,9 +7838,11 @@ int localdecl(int typed) { else ltype[i] = xty; } assigncheck(LV_LOCAL, i, c); + if (ro) { k = ltype[i]; if (!k) { k = tword; if (isnum(xty)) k = xty; } ltype[i] = tconst(k); } if (gok) { k = xty; if (isagg(k)) k = ltype[i]; if (!ifpure(k) || (gty && gty != k)) gok = 0; else gty = k; } } - } else if (gok && !larr[i]) gok = 0; // no initializer: nothing says what it is + } else if (ro) serr("a const needs an initializer"); + else if (gok && !larr[i]) gok = 0; // no initializer: nothing says what it is if (larr[i] && ltype[i][0] == TY_ARR && ltype[i][3] < 0) serr("T[] needs an initializer to determine its length"); if (larr[i]) { k = 1; if (ltype[i][0] == TY_ARR) k = ltype[i][3]; lafact[i] = af_new(k, 1, pp); laaddr[i] = lafact[i]; if (lafact[i]) lafact[i][5] = i; } af_declaration(o, lafact[i]); os_at(o, i); @@ -9508,10 +9525,27 @@ word editdist(word a, word b) { } return row[lb]; } +// the part of a symbol a bare name is compared with: put_c for rt.put_c, the whole name otherwise +char* similar_tail(char* name) { + int k = 0; char* tail = name; + while (name[k] && name[k] != '.') k++; + if (name[k] && module_named(name, k)) tail = name + k + 1; + return tail; +} +// is name[0..k) a module the compiler knows? +int module_named(char* name, int k) { + char* prefix = chk_resize(0, 0, k + 1); int known; + rt.copy_w(prefix, name, k); prefix[k] = 0; + known = module_known(prefix); + rt.free(prefix); + return known; +} // the defined function, global or constant whose name is closest to n, if it -// is close enough to be a likely typo; 0 when none is +// is close enough to be a likely typo; 0 when none is. a module's export is +// compared without its prefix, so putc finds rt.put_c int similar(int n) { int j = 0, best = 0, bd = 99, dist, k, lim = rt.len_w(n) / 3; + if (rt.len_w(n) < 3) return 0; // one edit turns g into gc: too short to guess from if (lim < 1) lim = 1; if (lim > 3) lim = 3; while (j < nsym) { @@ -9519,7 +9553,7 @@ int similar(int n) { if (k == K_FN || k == K_GVAR || k == K_GARR || k == K_CONST) { if (sflag[j] & F_DEF) { if (sname[j] != n) { - dist = editdist(n, sname[j]); + dist = editdist(n, similar_tail(sname[j])); if (dist < bd) { bd = dist; best = sname[j]; } } } @@ -9529,24 +9563,137 @@ int similar(int n) { if (bd > lim) return 0; return best; } -// end a compile unit from token from onward, and report what it defined. -// the module that exports a bare name, when one does: put_c is rt's. the compiler's -// own modules come first, then the libraries this unit has loaded. 0 when none does. -char* export_owner(char* name) { - int k = 0; word p = included_modules; +// a copy of s[a..e) as a string +char* lib_copy(char* s, int a, int e) { + char* b = chk_resize(0, 0, e - a + 1); rt.copy_w(b, s + a, e - a); b[e - a] = 0; + return b; +} +// group.name, as a fresh string: the lists below outlive the name table of a check +char* lib_join(char* group, char* name) { + int g = rt.len_w(group), n = rt.len_w(name); char* b = chk_resize(0, 0, g + n + 2); + rt.copy_w(b, group, g); b[g] = '.'; rt.copy_w(b + g + 1, name, n); b[g + n + 1] = 0; + return b; +} +// the declarations of a library, read from its source text, as a list of [next, tail, path]: +// a top-level name's path is the name itself, a static method's is Group.name. the scan +// only looks at shapes: a name before ( or a declarator's = ; , at the top, or inside a +// struct's braces before (. qualified calls (run.statement_hook(...)) are not declarations. +word lib_scan(char* s) { + int i = 0, depth = 0, paren = 0, init = 0, la = -1, le = 0, dotted = 0, after_dot = 0, words = 0, skip = 0, gpend = 0, gdepth = -1, c, a, k; + char* group = 0; char* path; word list = 0, entry, back = 0; + while (s[i]) { + c = s[i]; + if (c == '/' && s[i + 1] == '/') { while (s[i] && s[i] != 10) i++; continue; } + if (c == '/' && s[i + 1] == '*') { i = i + 2; while (s[i] && !(s[i] == '*' && s[i + 1] == '/')) i++; if (s[i]) i = i + 2; continue; } + if (c == 34 || c == 39) { i++; while (s[i] && s[i] != c) { if (s[i] == 92 && s[i + 1]) i++; i++; } if (s[i]) i++; la = -1; continue; } + if (c == 32 || c == 9 || c == 10 || c == 13) { i++; continue; } + if (isdig(c)) { while (isidc(s[i])) i++; la = -1; continue; } + if (isal(c)) { + a = i; while (isidc(s[i])) i++; + if (gpend) { group = lib_copy(s, a, i); gpend = 0; entry = chk_resize(0, 0, 3); entry[0] = list; entry[1] = group; entry[2] = group; list = entry; } + if (depth == 0 && paren == 0 && !init) { + if (words == 0 && ((i - a == 9 && rt.eq_w(lib_copy(s, a, i), "namespace")) || (i - a == 7 && rt.eq_w(lib_copy(s, a, i), "include")))) skip = 1; + if (i - a == 6 && rt.eq_w(lib_copy(s, a, i), "struct")) { gpend = 1; group = 0; } + words++; + } + la = a; le = i; dotted = after_dot; after_dot = 0; + continue; + } + after_dot = c == '.'; + if (c == '<' && s[i + 1] != '<' && s[i + 1] != '=' && paren == 0 && !init && (depth == 0 || (depth == 1 && group))) { + // generic parameters: int find<T>(...) is still find + k = 1; i++; while (s[i] && k) { if (s[i] == '<') k++; if (s[i] == '>') k--; i++; } + continue; + } + if (c == '(' && paren == 0 && !init && la >= 0 && !dotted && !skip) { + if (depth == 0 || (depth == 1 && group && gdepth == 0)) { + path = lib_copy(s, la, le); + entry = chk_resize(0, 0, 3); entry[0] = list; entry[1] = path; entry[2] = path; + if (depth == 1) entry[2] = lib_join(group, path); + list = entry; + } + } + if (((c == '=' && s[i + 1] != '=') || c == ';' || c == ',' || c == '[') && depth == 0 && paren == 0 && !init && la >= 0 && !dotted && !skip && words > 1) { + entry = chk_resize(0, 0, 3); entry[0] = list; entry[1] = lib_copy(s, la, le); entry[2] = entry[1]; list = entry; + } + if (c == '=' && s[i + 1] != '=' && depth == 0 && paren == 0) init = 1; + if (c == '(' || c == '[') paren++; + if ((c == ')' || c == ']') && paren > 0) paren--; + if (c == '{') { if (depth == 0 && paren == 0 && group) gdepth = 0; else if (depth == 0) group = 0; depth++; } + if (c == '}' && depth > 0) { depth--; if (depth == 0) { group = 0; gdepth = -1; words = 0; skip = 0; init = 0; } } + if (c == ';' && depth == 0 && paren == 0) { init = 0; words = 0; skip = 0; group = 0; } + if (c == ',' && depth == 0 && paren == 0) init = 0; + la = -1; i++; + } + // first declared first: Text.itoa comes before Bytes.itoa + while (list) { entry = list[0]; list[0] = back; back = list; list = entry; } + return back; +} +// the libraries under src/lib, scanned once: std and icon are embedded, actor is read like its include +word std_names, icon_names, actor_names; int names_read; +word lib_names(char* module) { + char* save = incres; + if (!names_read) { + names_read = 1; + std_names = lib_scan(std_text()); icon_names = lib_scan(icon_text()); + char* text = inc_open("src/lib/actor.zc"); + if (text) actor_names = lib_scan(text); + rt.free(text); + incres = save; + } + if (rt.eq_w(module, "std")) return std_names; + if (rt.eq_w(module, "icon")) return icon_names; + return actor_names; +} +// where name is in a library's list: Text.itoa is itself, a bare itoa is a top-level +// declaration first, then a static method of some group. 0 when it is not there. +char* lib_path(word list, char* name) { + word p = list; int k = 0; + while (p) { if (rt.eq_w(p[2], name)) return p[2]; p = p[0]; } while (name[k]) { if (name[k] == '.') return 0; k++; } - if (export_in("rt", name)) return "rt"; - if (export_in("core", name)) return "core"; - if (export_in("run", name)) return "run"; - while (p) { if (export_in(p[1], name)) return p[1]; p = p[0]; } - return 0; + p = list; + while (p) { if (rt.eq_w(p[1], name)) return p[2]; p = p[0]; } + return 0; +} +// the module that exports a bare name, and the path after its prefix: put_c is rt's put_c, +// itoa is std's Text.itoa. every owner, best first, is a list of [next, module, path]: the +// compiler's own modules, then the modules this unit loaded, then the libraries, which are +// read from their source whether this unit loaded them or not. +word export_list, export_tail; char* export_path; +int export_add(char* module, char* path) { + word p = export_list; + if (!path) return 0; + while (p) { if (rt.eq_w(p[1], module)) return 0; p = p[0]; } + p = chk_resize(0, 0, 3); p[1] = module; p[2] = path; + if (export_tail) export_tail[0] = p; else export_list = p; + export_tail = p; + return 0; +} +word export_owners(char* name) { + word p = included_modules; + export_list = 0; export_tail = 0; + export_add("rt", export_in("rt", name)); + export_add("core", export_in("core", name)); + export_add("run", export_in("run", name)); + while (p) { export_add(p[1], export_in(p[1], name)); p = p[0]; } + export_add("std", lib_path(lib_names("std"), name)); + export_add("icon", lib_path(lib_names("icon"), name)); + export_add("actor", lib_path(lib_names("actor"), name)); + return export_list; +} +char* export_owner(char* name) { + word p = export_owners(name); + if (!p) return 0; + export_path = p[2]; return p[1]; } -// does module define name: a function, a global, a constant or a type of it. -int export_in(char* module, char* name) { +// name, when module defines it: a function, a global, a constant or a type of it. 0 otherwise. +char* export_in(char* module, char* name) { int s = lookup_raw(methname(module, name)); if (s < 0) return 0; - return skind[s] != K_DOC && (skind[s] != K_FN || (sflag[s] & F_DEF)); + if (skind[s] != K_DOC && (skind[s] != K_FN || (sflag[s] & F_DEF))) return name; + return 0; } +// end a compile unit from token from onward, and report what it defined. int unit_end(int from) { int i = from, best; char* owner; effect_finish(from); @@ -9564,7 +9711,7 @@ int unit_end(int from) { if (owner) { rt.put_s("; did you mean to "); if (!module_imported(owner)) { rt.put_s("include "); rt.put_s(owner); rt.put_s(" and "); } - rt.put_s("use "); rt.put_s(owner); rt.put_c('.'); rt.put_s(sname[i]); rt.put_s("?"); + rt.put_s("use "); rt.put_s(owner); rt.put_c('.'); rt.put_s(export_path); rt.put_s("?"); } else if (best) { rt.put_s("; did you mean '"); rt.put_s(best); rt.put_s("'?"); } errend(); diff --git a/src/lsp/server.zc b/src/lsp/server.zc index 70f9efd..2587c3b 100644 --- a/src/lsp/server.zc +++ b/src/lsp/server.zc @@ -3149,10 +3149,21 @@ int do_completion(int i) { // import fixes are derived from current compiler errors and qualified exports. // each edit is tied to the document version that was checked. word ca_list; int ca_i, ca_start, ca_end, ca_insert, ca_crlf; -int ca_add(char*.d owner, int qualify) { - word p = ca_list; +// one fix: include owner, and when path is set, replace from..ca_end with owner.path. +// path belongs to the compiler's export list and lives until the next check. +int ca_add(char* owner, char* path, int from) { + word p = ca_list; int n = rt.len_w(owner); char* copy; while (p) { if (rt.eq_w(p[1], owner)) return 0; p = p[0]; } - p = rt.new(3); p[0] = ca_list; p[1] = owner; p[2] = qualify; ca_list = p; + copy = rt.new(n + 1); rt.copy_w(copy, owner, n + 1); + p = rt.new(4); p[0] = ca_list; p[1] = copy; p[2] = path; p[3] = from; ca_list = p; + return 0; +} +// the fixes for every module that exports name, best first +int ca_owners(char* name, int from) { + word p = run.export_owners(name), q = 0, next; + // ca_add prepends, so the list goes in backwards and comes out best first + while (p) { next = rt.new(2); next[0] = q; next[1] = p; q = next; p = p[0]; } + while (q) { p = q[1]; ca_add(p[1], p[2], from); next = q[0]; rt.free(q); q = next; } return 0; } int ca_import_point(int i) { @@ -3176,7 +3187,7 @@ int ca_import_point(int i) { int ca_newline() { if (ca_crlf) hov_put(13); hov_put(10); return 0; } // quick fixes for hints. each record is one edit of the document: it replaces start..end // with text built from its kind. the hint's own range and words say what to change. -const int HF_RENAME = 1, HF_CONST = 2, HF_SHIFT = 3, HF_STACK = 4; +const int HF_RENAME = 1, HF_CONST = 2, HF_SHIFT = 3, HF_STACK = 4, HF_TYPO = 5; word hf_list; // [next, kind, start, end, a, b] int hf_add(int kind, int start, int end, int a, int b) { word p = lsp_grow(0, 0, 6); @@ -3241,7 +3252,7 @@ int hf_operand(int i, int op) { } if (o == op || depth) return 0 - 1; b = o; while (b > 0 && (dc(doff[i], b - 1) == 32 || dc(doff[i], b - 1) == 9)) b--; - if (b > 0) { c = dc(doff[i], b - 1); if (c == '*' || c == '/' || c == '%' || c == '.' || c == '-' && b > 1 && dc(doff[i], b - 2) == '-') return 0 - 1; } + if (b > 0) { c = dc(doff[i], b - 1); if (c == '*' || c == '/' || c == '%' || c == '.' || (c == '-' && b > 1 && dc(doff[i], b - 2) == '-')) return 0 - 1; } return o; } // is the text at o the word w, ending there @@ -3294,6 +3305,7 @@ int hf_title(word p) { if (p[1] == HF_RENAME) { hov_text("rename `"); while (k < p[3]) { hov_put(dc(doff[ca_i], k)); k++; } hov_text("` to _"); } else if (p[1] == HF_CONST) { hov_text("make `"); k = p[3]; while (k < p[4]) { hov_put(dc(doff[ca_i], k)); k++; } hov_text("` a const"); } else if (p[1] == HF_SHIFT) { hov_text("write << "); hov_number(p[4]); } + else if (p[1] == HF_TYPO) { hov_text("change to `"); k = p[4]; while (k < p[5]) { hov_put(dgb[k]); k++; } hov_put('`'); } else { hov_text("use a stack buffer"); } return 0; } @@ -3302,9 +3314,9 @@ int hf_text(word p) { hn = 0; if (p[1] == HF_RENAME) hov_put('_'); else if (p[1] == HF_CONST) { + // let becomes a bare const, which takes the initializer's type hov_text("const "); - if (hf_word(ca_i, p[2], "let")) hov_text("int "); - else { k = p[2]; while (k < p[3]) { hov_put(dc(doff[ca_i], k)); k++; } } + if (!hf_word(ca_i, p[2], "let")) { k = p[2]; while (k < p[3]) { hov_put(dc(doff[ca_i], k)); k++; } } } else if (p[1] == HF_SHIFT) { // (operand << k): the operand's text runs from the record's start to the *, without trailing blanks @@ -3313,6 +3325,7 @@ int hf_text(word p) { while (k < c) { hov_put(dc(doff[ca_i], k)); k++; } hov_text(" << "); hov_number(p[4]); hov_put(')'); } + else if (p[1] == HF_TYPO) { k = p[4]; while (k < p[5]) { hov_put(dgb[k]); k++; } } else { hov_put('['); k = p[4]; while (k < p[5]) { hov_put(dc(doff[ca_i], k)); k++; } hov_put(']'); } return 0; } @@ -3335,7 +3348,7 @@ int gen_code_actions() { if (!first) wc(','); first = 0; imported = run.module_imported(p[1]); hn = 0; if (!imported) { hov_text("include "); hov_text(p[1]); } - if (p[2]) { if (!imported) hov_text(" and "); hov_text("use "); hov_text(p[1]); hov_put('.'); hov_text(hname); } + if (p[2]) { if (!imported) hov_text(" and "); hov_text("use "); hov_text(p[1]); hov_put('.'); hov_text(p[2]); } ws("{\"title\":"); wjs(hov, hn); ws(",\"kind\":\"quickfix\","); if (single) ws("\"isPreferred\":true,"); ws("\"edit\":{\"documentChanges\":[{\"textDocument\":{\"uri\":"); @@ -3348,13 +3361,33 @@ int gen_code_actions() { } if (p[2]) { if (!imported) wc(','); - ws("{\"range\":{\"start\":"); wpos(ca_start); ws(",\"end\":"); wpos(ca_end); ws("},\"newText\":"); - hn = 0; hov_text(p[1]); hov_put('.'); hov_text(hname); wjs(hov, hn); wc('}'); + ws("{\"range\":{\"start\":"); wpos(p[3]); ws(",\"end\":"); wpos(ca_end); ws("},\"newText\":"); + hn = 0; hov_text(p[1]); hov_put('.'); hov_text(p[2]); wjs(hov, hn); wc('}'); } ws("]}]}}"); p = p[0]; } ws("]}"); return 0; } +// "did you mean 'name'?" under the cursor: the name replaces the word. a module's export +// (rt.put_c) also gets its include when the document lacks it. +char[64] ca_owner; char[64] ca_path; +int ca_typo(int at) { + int n = dgb[at + 4], e = n - 2, b, dot, k; + if (!dg_ends(at, "'?")) return 0; + b = e; while (b > 0 && dgb[at + 5 + b - 1] != 39) b--; + if (b == 0 || e - b >= 63) return 0; + dot = b; while (dot < e && dgb[at + 5 + dot] != '.') dot++; + if (dot < e) { + k = 0; while (b + k < dot) { ca_owner[k] = dgb[at + 5 + b + k]; k++; } ca_owner[k] = 0; + if (run.module_known(ca_owner)) { + k = 0; while (dot + 1 + k < e) { ca_path[k] = dgb[at + 6 + dot + k]; k++; } ca_path[k] = 0; + ca_add(ca_owner, ca_path, ca_start); + return 0; + } + } + hf_add(HF_TYPO, ca_start, ca_end, at + 5 + b, at + 5 + e); + return 0; +} int do_code_actions(int i) { int at = 0, error = -1, s, dot, k, n, base = doff[i], start, end, cursor, limit, sl = hline, sc = hchar; ca_list = 0; hf_list = 0; ca_i = i; @@ -3375,22 +3408,18 @@ int do_code_actions(int i) { while (start > 0 && (hid(dc(base, start - 1)) || dc(base, start - 1) == '.')) start--; dot = start; while (dot < ca_end && dc(base, dot) != '.') dot++; if (dot < ca_end) { + // rt.put_c without include rt needs only the include; Text.itoa without std is std.Text.itoa char* owner = rt.new(dot - start + 1); k = start; n = 0; while (k < dot) { owner[n] = dc(base, k); n++; k++; } owner[n] = 0; - if (run.module_known(owner) && !run.module_imported(owner)) ca_add(d*owner, 0); else rt.free(owner); - } else { - s = 0; - while (s < run.nsym) { - if (run.skind[s] != run.K_DOC) { - char* symbol = run.sname[s]; dot = 0; while (symbol[dot] && symbol[dot] != '.') dot++; - if (symbol[dot] == '.' && rt.eq_w(symbol + dot + 1, hname)) { - char* owner = rt.new(dot + 1); rt.copy_w(owner, symbol, dot); owner[dot] = 0; - if (run.module_known(owner)) ca_add(d*owner, 1); else rt.free(owner); - } - } - s++; + if (run.module_known(owner) || run.module_loaded(owner)) { if (!run.module_imported(owner)) ca_add(owner, 0, 0); } + else { + char* dotted = rt.new(ca_end - start + 1); k = start; n = 0; + while (k < ca_end) { dotted[n] = dc(base, k); n++; k++; } dotted[n] = 0; + ca_owners(dotted, start); rt.free(dotted); } - } + rt.free(owner); + } else ca_owners(hname, ca_start); + if (!ca_list) ca_typo(error); send(gen_code_actions); return 0; } diff --git a/tests/drivers/hint_fixes_test.js b/tests/drivers/hint_fixes_test.js index 9513a85..fffcbf5 100644 --- a/tests/drivers/hint_fixes_test.js +++ b/tests/drivers/hint_fixes_test.js @@ -48,8 +48,8 @@ fixes('a local that is never read is renamed to _', head + '\tlet unused = rt.ge none('a local written later is not renamed', head + '\tlet later = 1;\n\tlater = 2;\n\treturn 0;\n}\n', 'later', 0); fixes('a typed constant local becomes const', head + '\tint x = 5;\n\trt.put_n(x);\n\treturn 0;\n}\n', 'x = 5', 0, head + '\tconst int x = 5;\n\trt.put_n(x);\n\treturn 0;\n}\n', 'make `x` a const'); -fixes('an untyped constant local becomes const int', head + '\tlet y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'y = 7', 0, - head + '\tconst int y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'make `y` a const'); +fixes('an untyped constant local becomes an untyped const', head + '\tlet y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'y = 7', 0, + head + '\tconst y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'make `y` a const'); none('one of several declarators is left alone', head + '\tint x = 5, z = rt.get_c();\n\trt.put_n(x + z);\n\treturn 0;\n}\n', 'x = 5', 0); fixes('a power-of-two multiply becomes a parenthesized shift', head + '\tlet y = rt.get_c();\n\tlet q[4];\n\tq[0] = y;\n\trt.put_n(q[0] * 16 + y);\n\treturn 0;\n}\n', '* 16', 0, head + '\tlet y = rt.get_c();\n\tlet q[4];\n\tq[0] = y;\n\trt.put_n((q[0] << 4) + y);\n\treturn 0;\n}\n', 'write << 4'); @@ -62,5 +62,17 @@ fixes('a compiler name is imported from run', 'include rt;\nfn main() {\n\trt.pu 'include rt;\ninclude run;\nfn main() {\n\trt.put_n(run.evals("return 1;"));\n\treturn 0;\n}\n', 'include run and use run.evals'); fixes('an imported module only needs the qualifier', 'include rt;\nfn main() {\n\tput_c(65);\n\treturn 0;\n}\n', 'put_c', 0, 'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'use rt.put_c'); +fixes('a library function is found in its static group', 'include rt;\nfn main() {\n\trt.put_s(itoa(5));\n\treturn 0;\n}\n', 'itoa', 0, + 'include rt;\ninclude std;\nfn main() {\n\trt.put_s(std.Text.itoa(5));\n\treturn 0;\n}\n', 'include std and use std.Text.itoa'); +fixes('a group-qualified name gets its module', 'include rt;\nfn main() {\n\trt.put_s(Text.itoa(5));\n\treturn 0;\n}\n', 'Text', 0, + 'include rt;\ninclude std;\nfn main() {\n\trt.put_s(std.Text.itoa(5));\n\treturn 0;\n}\n', 'include std and use std.Text'); +fixes('a top-level library function is imported', 'include rt;\nfn main() {\n\treturn validate(0);\n}\n', 'validate', 0, + 'include rt;\ninclude icon;\nfn main() {\n\treturn icon.validate(0);\n}\n', 'include icon and use icon.validate'); +fixes('a library read from src/lib is imported', 'include rt;\nfn main() {\n\treturn vector();\n}\n', 'vector', 0, + 'include rt;\ninclude actor;\nfn main() {\n\treturn actor.vector();\n}\n', 'include actor and use actor.vector'); +fixes('a misspelled export is corrected and imported', 'fn main() {\n\tputc(65);\n\treturn 0;\n}\n', 'putc', 0, + 'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'include rt and use rt.put_c'); +fixes('a misspelled local function is corrected', 'int count_items() { return 1; }\nfn main() {\n\treturn count_item();\n}\n', 'count_item(', 0, + 'int count_items() { return 1; }\nfn main() {\n\treturn count_items();\n}\n', 'change to `count_items`'); console.log(ok + ' hint fix checks passed, ' + bad + ' failed'); process.exit(bad ? 1 : 0); diff --git a/tests/langerrors/const_local_no_init.zc b/tests/langerrors/const_local_no_init.zc new file mode 100644 index 0000000..15832a1 --- /dev/null +++ b/tests/langerrors/const_local_no_init.zc @@ -0,0 +1,8 @@ +namespace; +include rt; + +// expect: a const needs an initializer +fn main() { + const k; + return 0; +} diff --git a/tests/langerrors/const_local_write.zc b/tests/langerrors/const_local_write.zc new file mode 100644 index 0000000..31220be --- /dev/null +++ b/tests/langerrors/const_local_write.zc @@ -0,0 +1,9 @@ +namespace; +include rt; + +// expect: const storage cannot be modified +fn main() { + const k = 5; + k = 6; + rt.put_n(k); +} diff --git a/tests/programs/const_local.zc b/tests/programs/const_local.zc new file mode 100644 index 0000000..83c423c --- /dev/null +++ b/tests/programs/const_local.zc @@ -0,0 +1,11 @@ +// a local const without a type takes its initializer's type and can't be written. +include rt; +fn main() { + const k = 6, j = k * 7; + const half = 2.5; + const name = "zc"; + int typed = j; + rt.put_n(k); rt.put_c(32); rt.put_n(typed); rt.put_c(32); + if (half > 2.0) rt.put_s(name); + rt.put_c(10); +} diff --git a/tests/programs/precedence.zc b/tests/programs/precedence.zc new file mode 100644 index 0000000..18b323d --- /dev/null +++ b/tests/programs/precedence.zc @@ -0,0 +1,13 @@ +// && binds tighter than ||, and both bind below the comparisons and |, as in c. +include rt; +int calls; +int seen(int v) { calls++; return v; } +fn main() { + int t = 1, f = 0; + rt.put_n(f && t || t); rt.put_c(32); // (f && t) || t is 1; f && (t || t) would be 0 + rt.put_n(t || t && f); rt.put_c(32); // t || (t && f) is 1 + rt.put_n(f && f || t && t); rt.put_c(32); + rt.put_n(1 | 0 && 2 == 2); rt.put_c(32); + calls = 0; int r = seen(1) || seen(0) && seen(1); + rt.put_n(r); rt.put_c(32); rt.put_n(calls); rt.put_c(10); // || stops after the first call +} diff --git a/tests/test.sh b/tests/test.sh index 449da87..031d6d9 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -109,6 +109,8 @@ run(){ # $1 = vm command, $2 = label chk "$($vm < tests/programs/typeok.zc)" "14" "$l a program the typechecker accepts" chk "$($vm < tests/programs/typeoff.zc)" "6" "$l the typechecker is off by default" chk "$($vm < tests/programs/enhance.zc)" "100 0 343 3,2 5 -2 0" "$l virtual enhancements" + chk "$($vm < tests/programs/precedence.zc)" "1 1 1 1 1 1" "$l && binds tighter than ||, as in c" + chk "$($vm < tests/programs/const_local.zc)" "6 42 zc" "$l an untyped local const takes its initializer's type" chk "$($vm < tests/programs/joins.zc)" "0 0 5 80 8 3 9 0" "$l joins free what one path gave away; switch, ?:, no prototypes" chk "$($vm < tests/programs/buddy.zc)" "131072 262144 1 0" "$l buddy allocator merges freed blocks" chk "$($vm < tests/programs/growheap.zc)" "144 1 7 1" "$l the heap grows past its initial size and a bigger block fits after the frees" |
