diff options
| author | 2026-10-14 08:09:40 +0000 | |
|---|---|---|
| committer | 2026-10-14 08:09:40 +0000 | |
| commit | a3e9e53e64a1aa5692bca2b60bcbff6a0fcfdba1 (patch) | |
| tree | 2598c50e36e488935211c2e304b720a58315bacb | |
| parent | embed: tests, editor support and docs; js vm file access; test fixes (diff) | |
lsp: quick fixes for hints and bare library names; comment cleanup
an undefined bare name that a module exports now says which one:
"put_c is used but never defined; did you mean to include rt and use
rt.put_c?", and the import quick fix is titled the same way and marked
preferred when it is the only one, so the editor's auto-fix applies it.
hints gain quick fixes that keep the program's meaning: an unused
parameter, or a local with no other use, is renamed to _; a constant
local with one declarator becomes const; a power-of-two multiply becomes
a parenthesized shift (not when a * / or % binds its left operand); and
let p = rt.new(n) with a small constant becomes let p[n]. the hints no
longer fire for _ discards or for locals already declared const.
whole-line /* */ comments in zc sources are // comments now, and //: is
gone from the maintained sources: the notes it marked were mostly the
documentation of the declaration below, which hovers now show. section
headers are separated from the declarations after them.
| -rw-r--r-- | docs/LEARN.md | 4 | ||||
| -rw-r--r-- | examples/learn/29-block-comments.zc | 2 | ||||
| -rw-r--r-- | src/compiler/compiler.zc | 562 | ||||
| -rw-r--r-- | src/lib/std.zc | 32 | ||||
| -rw-r--r-- | src/lsp/server.zc | 394 | ||||
| -rw-r--r-- | src/runtime/rt.zc | 218 | ||||
| -rw-r--r-- | tests/drivers/codeactions_client_test.js | 2 | ||||
| -rw-r--r-- | tests/drivers/hint_fixes_test.js | 66 | ||||
| -rw-r--r-- | tests/drivers/toolchain_lsp_contract_test.js | 4 | ||||
| -rwxr-xr-x | tests/test.sh | 3 |
10 files changed, 780 insertions, 507 deletions
diff --git a/docs/LEARN.md b/docs/LEARN.md index 8b0e1e6..b649b4f 100644 --- a/docs/LEARN.md +++ b/docs/LEARN.md @@ -915,7 +915,7 @@ output: the language server reports informational hints for likely mistakes. by default, `hints on;` shows blue informational squiggles. `hints small;` keeps the same hints but displays compact dots; it does not reduce their number or kinds. `hints off;` suppresses hints after the directive. each setting applies from its source position. -an inline block comment immediately before a numeric literal marks that literal as intentional and suppresses its magic-number hint. a comment before another operand does not suppress the hint. a block comment immediately above a declaration provides hover documentation; `//:` marks an implementation note instead. +an inline block comment immediately before a numeric literal marks that literal as intentional and suppresses its magic-number hint. a comment before another operand does not suppress the hint. block comments are for this kind of place, between code on one line; a comment on lines of its own is written with `//`. the `//` comment immediately above a declaration is its hover documentation. [example source](../examples/learn/29-block-comments.zc): @@ -923,7 +923,7 @@ an inline block comment immediately before a numeric literal marks that literal namespace; include rt; -/* adds a memory offset to an address. */ +// adds a memory offset to an address. int offset_address(int address) { return address + /* offset in memory */ 0x40ff; } diff --git a/examples/learn/29-block-comments.zc b/examples/learn/29-block-comments.zc index a5088eb..9468cc5 100644 --- a/examples/learn/29-block-comments.zc +++ b/examples/learn/29-block-comments.zc @@ -1,7 +1,7 @@ namespace; include rt; -/* adds a memory offset to an address. */ +// adds a memory offset to an address. int offset_address(int address) { return address + /* offset in memory */ 0x40ff; } diff --git a/src/compiler/compiler.zc b/src/compiler/compiler.zc index c6e1ca4..40d83cf 100644 --- a/src/compiler/compiler.zc +++ b/src/compiler/compiler.zc @@ -1,59 +1,59 @@ namespace run; include rt; -//: ============================================================================ -//: compiler.zc: the resident zc compiler. it lives in the genome next to the -//: runtime, compiles source straight to vm words in memory, and is visible to -//: the programs it compiles: every function and global here can be called or -//: patched by name, and parse words (immediate fns) run inside it. -//: ============================================================================ +// ============================================================================ +// compiler.zc: the resident zc compiler. it lives in the genome next to the +// runtime, compiles source straight to vm words in memory, and is visible to +// the programs it compiles: every function and global here can be called or +// patched by name, and parse words (immediate fns) run inside it. +// ============================================================================ -//: how this file is organized, in the order a source file travels through it: -//: -//: 1 lexer bytes -> tokens (nextch, lex1, next/peek). a token is a -//: kind (tk), a value (tv: number or interned name) and a -//: place (line, offset, length). sources nest: stdin, a -//: token list for macros, a string for eval. -//: 2 symbols every name is interned once in a string pool; symbols -//: (functions, globals, constants, keywords, types) hang -//: off the interned name and shadow one another. -//: 3 types records in the type area, hash-consed, so comparing two -//: types is comparing two words. typing is optional: an -//: untyped program is all "word". -//: 4 analyzer the ownership/lifetime pass. it runs while the parser -//: emits code, tracking for every local whether it owns an -//: object in this block (m), moves one up and out (u), or -//: moves one down into a lower scope (d). borrowing is -//: tracked separately. the pass inserts frees, and joins -//: merge that knowledge at -//: if/else, loops and switches. -//: 5 expressions precedence climbing over the operator tables, which a -//: program can extend at compile time. -//: 6 statements blocks, control flow, switch, asm blocks, quote/grab. -//: 7 functions signatures, prologues, closures, methods, generics, -//: fattening (typed new) and the top level. -//: 8 setup the keyword table, built-in names, entry point. -//: 9 check mode the same compiler run over an in-memory document for the -//: language server: errors and hints become records. -//: -//: the compiler emits vm words directly into memory at "here" (there is no -//: intermediate tree). a forward reference emits a placeholder literal and -//: remembers the site on a patch chain; defining the name patches the chain. -//: the compiler is itself written in zc and lives in the genome next to rt.zc, -//: so a program being compiled can call any function in this file, and -//: immediate fns (parse words) run in the middle of compilation and can read -//: tokens, emit code, or add operators. a few conventions that recur: -//: - "plain word": most values are untyped 64-bit words. a pointer is just a -//: word that happens to be a heap address. -//: - a code word is lit(v) = 2v+1 (odd) or op(k) = 2k (even). see lit and op. -//: - names ending in an s_ prefix are interned spellings the parser compares -//: against by pointer, which is why intern exists. -//: - serr reports and carries on; die reports and skips input to the next -//: ; or }. nothing in here raises or unwinds. -//: - a backtick outside a comment or a literal ends the source (srcend). zc0, -//: the c bootstrap, reads the same way, so a backtick in a comment is fine. -//: -//: ---- compiler data region (@map-cc) ---- +// how this file is organized, in the order a source file travels through it: +// +// 1 lexer bytes -> tokens (nextch, lex1, next/peek). a token is a +// kind (tk), a value (tv: number or interned name) and a +// place (line, offset, length). sources nest: stdin, a +// token list for macros, a string for eval. +// 2 symbols every name is interned once in a string pool; symbols +// (functions, globals, constants, keywords, types) hang +// off the interned name and shadow one another. +// 3 types records in the type area, hash-consed, so comparing two +// types is comparing two words. typing is optional: an +// untyped program is all "word". +// 4 analyzer the ownership/lifetime pass. it runs while the parser +// emits code, tracking for every local whether it owns an +// object in this block (m), moves one up and out (u), or +// moves one down into a lower scope (d). borrowing is +// tracked separately. the pass inserts frees, and joins +// merge that knowledge at +// if/else, loops and switches. +// 5 expressions precedence climbing over the operator tables, which a +// program can extend at compile time. +// 6 statements blocks, control flow, switch, asm blocks, quote/grab. +// 7 functions signatures, prologues, closures, methods, generics, +// fattening (typed new) and the top level. +// 8 setup the keyword table, built-in names, entry point. +// 9 check mode the same compiler run over an in-memory document for the +// language server: errors and hints become records. +// +// the compiler emits vm words directly into memory at "here" (there is no +// intermediate tree). a forward reference emits a placeholder literal and +// remembers the site on a patch chain; defining the name patches the chain. +// the compiler is itself written in zc and lives in the genome next to rt.zc, +// so a program being compiled can call any function in this file, and +// immediate fns (parse words) run in the middle of compilation and can read +// tokens, emit code, or add operators. a few conventions that recur: +// - "plain word": most values are untyped 64-bit words. a pointer is just a +// word that happens to be a heap address. +// - a code word is lit(v) = 2v+1 (odd) or op(k) = 2k (even). see lit and op. +// - names ending in an s_ prefix are interned spellings the parser compares +// against by pointer, which is why intern exists. +// - serr reports and carries on; die reports and skips input to the next +// ; or }. nothing in here raises or unwinds. +// - a backtick outside a comment or a literal ends the source (srcend). zc0, +// the c bootstrap, reads the same way, so a backtick in a comment is fine. +// +// ---- compiler data region (@map-cc) ---- // src/tools/genmap computes these addresses from the compiler table sizes for each // build. later regions (the type area, local types, exit records) are computed @@ -115,56 +115,56 @@ const JOIN_PATHS = 0x40; const O3_REGIONS = 0xA; // @end-map -//: table bases start in the generated map; growth copies columns without changing indices. +// table bases start in the generated map; growth copies columns without changing indices. -/* interned spelling of each symbol, indexed by symbol id; use lookup(name) to find an id. */ +// interned spelling of each symbol, indexed by symbol id; use lookup(name) to find an id. char** sname = sname_BASE; -/* kind of each symbol: K_FN, K_GVAR, K_GARR, K_CONST, K_KW, K_ENH, K_TYPE or K_DOC. */ +// kind of each symbol: K_FN, K_GVAR, K_GARR, K_CONST, K_KW, K_ENH, K_TYPE or K_DOC. word skind = skind_BASE; -/* value of each symbol, indexed by symbol id: a function code address, variable storage address, constant value, enhancement slot or type record, according to skind. */ +// value of each symbol, indexed by symbol id: a function code address, variable storage address, constant value, enhancement slot or type record, according to skind. word sval = sval_BASE; -/* packed function signature for each symbol; includes the owned-return bit, parameter count and parameter lifetime tags. */ +// packed function signature for each symbol; includes the owned-return bit, parameter count and parameter lifetime tags. word ssig = ssig_BASE; -/* definition and compilation flags for each symbol, such as F_DEF, F_DECL, F_GEN and F_STATIC. */ +// definition and compilation flags for each symbol, such as F_DEF, F_DECL, F_GEN and F_STATIC. word sflag = sflag_BASE; -/* head of unresolved code references for each symbol; references are patched when the symbol is defined. */ +// head of unresolved code references for each symbol; references are patched when the symbol is defined. word sfix = sfix_BASE; -/* previous symbol with the same interned name, stored as id plus one; zero ends its shadow chain. */ +// previous symbol with the same interned name, stored as id plus one; zero ends its shadow chain. word snext = snext_BASE; // retained source metadata for functions that can be transformed at runtime. word fnmeta; int syntax_host_floor, syntax_host_from; word syntax_host_symbols; -/* interned name of each currently active local variable or parameter. */ +// interned name of each currently active local variable or parameter. char** lname = lname_BASE; -/* frame slot of each local; laddr(lslot[i]) emits its address relative to the current frame. */ +// frame slot of each local; laddr(lslot[i]) emits its address relative to the current frame. word lslot = lslot_BASE; -/* scope depth where each local was declared; the lifetime analyzer uses it when values move between blocks. */ +// scope depth where each local was declared; the lifetime analyzer uses it when values move between blocks. word lhome = lhome_BASE; -/* ownership state of each local: plain, m, u, d, dead, child or borrowed. */ +// ownership state of each local: plain, m, u, d, dead, child or borrowed. word lstate = lstate_BASE; -/* owner local of a borrowed or container-owned local; lroot(i) follows this chain. */ +// owner local of a borrowed or container-owned local; lroot(i) follows this chain. word lowner = lowner_BASE; -/* source line of each local's last tracked use; zero means it has not been used. */ +// source line of each local's last tracked use; zero means it has not been used. word lused = lused_BASE; -/* nonzero for locals stored in place as fixed arrays or structs; their expression value is their address. */ +// nonzero for locals stored in place as fixed arrays or structs; their expression value is their address. word larr = larr_BASE; -/* nonzero when a local is responsible for releasing its owned object. */ +// nonzero when a local is responsible for releasing its owned object. word lown = lown_BASE; -/* code location to patch for each unresolved reference record. */ +// code location to patch for each unresolved reference record. word fsite = fsite_BASE; -/* next unresolved reference record, stored as index plus one; zero ends a patch chain. */ +// next unresolved reference record, stored as index plus one; zero ends a patch chain. word fnext = fnext_BASE; int symcap = NS, loccap = NL, fixcap = NF; const word R_TY = rt.ZFP - 16384, R_TYEND = rt.ZFP; -//: interned text and type records never move: only their index vectors grow. +// interned text and type records never move: only their index vectors grow. char** namehash = R_IH; word poolend = R_POOLEND; word poolblocks, typeblocks; int hashcap = IHN, namecount, poolchunk = R_POOLEND - R_POOL; word typelist = R_TY, tyend = R_TYEND; int typecap = 1024; -//: growing scratch regions retain offsets used by nested parser frames. +// growing scratch regions retain offsets used by nested parser frames. word R_SNAP = R_SNAP_BASE; int R_SNAP_cap = R_QBUF_BASE - R_SNAP_BASE; word R_QBUF = R_QBUF_BASE; int R_QBUF_cap = R_SRC_BASE - R_QBUF_BASE; word R_SRC = R_SRC_BASE; int R_SRC_cap = R_SEQ_BASE - R_SRC_BASE; @@ -173,7 +173,7 @@ word R_CTX = R_CTX_BASE; int R_CTX_cap = R_LOOP_BASE - R_CTX_BASE; word R_LOOP = R_LOOP_BASE; int R_LOOP_cap = R_ASM - R_LOOP_BASE; word[60] lpfact_initial; word lpfact = lpfact_initial; int loopfacts_cap = 60; -//: operator columns grow independently of their initial map reservations. +// operator columns grow independently of their initial map reservations. char** bname = bname_BASE; word bprec = bprec_BASE; word bseq = bseq_BASE; @@ -185,18 +185,18 @@ char** aname = aname_BASE; word abin = abin_BASE; word kops = kops_BASE; int bincap = bprec_BASE - bname_BASE, uncap = useq_BASE - uname_BASE, asgcap = abin_BASE - aname_BASE, kopcap = R_CTX_BASE - kops_BASE; -//: ---- token kinds, symbol kinds and flags ---- +// ---- token kinds, symbol kinds and flags ---- const int T_EOF = 0, T_NUM = 1, T_ID = 2, T_STR = 3, T_OP = 4, T_FLT = 5, T_BSTR = 6; const int K_FN = 1, K_GVAR = 2, K_GARR = 3, K_CONST = 4, K_KW = 5, K_ENH = 6, K_TYPE = 7, K_DOC = 8; const int F_DEF = 1, F_SIG = 2, F_UNTYPED = 4, F_STMT = 8, F_EXPR = 16, F_TOP = 32, F_FALLBACK = 64, F_GEN = 128, F_PRE = 256, F_O3 = 512, F_EXT = 1024, F_TPL = 2048, F_STATIC = 4096, F_DECL = 8192, F_RETOWN = 16384, F_EARLY = 32768; -//: variable states for the lifetime analyzer +// variable states for the lifetime analyzer const int S_PLAIN = 0, S_M = 1, S_U = 2, S_D = 3, S_DEAD = 4, S_CHILD = 5, S_BORROW = 6; -//: expression classes: plain word, a new object, or a bare variable (C_VAR + index) +// expression classes: plain word, a new object, or a bare variable (C_VAR + index) const int C_PLAIN = 0, C_FRESH = 1, C_VAR = 2; -//: lvalue kinds +// lvalue kinds const int LV_NONE = 0, LV_LOCAL = 1, LV_GLOBAL = 2, LV_ELEM = 3, LV_OTHER = 4; -//: opcodes +// opcodes const int O_Q = 0, O_LD = 1, O_ST = 2, O_PICK = 3, O_DROP = 4, O_SWAP = 5, O_CALL = 6, O_JMP = 7; const int O_JZ = 8, O_RET = 9, O_ADD = 13, O_SUB = 14, O_LT = 20; const int XOP0 = 22; // first enhancement opcode @@ -205,7 +205,7 @@ const int OP_FEQ = 605, OP_FLT = 606, OP_FLE = 607, OP_I2F = 608, OP_F2I = 609; const int SEQWORD = -4; // in an operator sequence: the next word is a full-width literal const int SEQREF = -2; // in an operator sequence: the next word names a symbol -//: ---- state ---- +// ---- state ---- int chk_heap; int nenh; word pooltop; int nsym, ubase, vislo, nfixu, fixfree; @@ -214,15 +214,15 @@ int nbin, nun, nasg, nkop, seqtop; char* mnem = "q@!$_~cjzr,?.+-*&|^><%"; int check_on = 1; // set to 0 to switch the analyzer off for what follows int tk; word tv; int tglue, tline, line = 1, ch, chkind; word chptr, chbase; int qdone, isp = -1, pendot; -//: where the current token sits in its source (chars from the start, -1 for a token list), how long it is, and where the one before it ended +// where the current token sits in its source (chars from the start, -1 for a token list), how long it is, and where the one before it ended int toff = -1, toklen, tpend = -1, tpline; int ntok; // tokens taken so far, so a parse word that reads nothing can be caught int qst, qesc, qp; // srcend's reading state: normal, line comment, block comment, string, char literal char[1100] tbuf_initial; char* tbuf = tbuf_initial; int tbufcap = 1100; -//: doc comments: the comment lines directly above a declaration (no blank line -//: between them or before the declaration) are its documentation. the lexer -//: collects them in dbuf, dlast is the line of the last one, and dline the -//: line of the token that follows them. only when docs are on. +// doc comments: the comment lines directly above a declaration (no blank line +// between them or before the declaration) are its documentation. the lexer +// collects them in dbuf, dlast is the line of the last one, and dline the +// line of the token that follows them. only when docs are on. int docs_on; // docs on; keeps them: a symbol "doc name" holds a byte string char[2100] dbuf_initial; char* dbuf = dbuf_initial; int docbufcap = 2100; int dn, dlast, dline, docpending, dsect; char[2100] dsnap_initial; char* dsnap = dsnap_initial; int docsnapcap = 2100; int dsn, dnm; char[300] dnmb_initial; char* dnmb = dnmb_initial; int docnamecap = 300; @@ -231,13 +231,13 @@ int here, hdelta, hlimit, ptop; int code_chunk = rt.CODE_END; word image_buffer, image_words; int image_active, image_org, image_capacity, image_size; int nloc, lbase, fdepth, fslots, fchk, fret, live, bxdepth, epichain, fframe, infn; -//: a global that borrows a local owner through g = m*x. each entry is the global's -//: symbol, the local, and the block depth of the store; the lend ends when the same -//: global is assigned again in that block, and is an error if the block can end first. +// a global that borrows a local owner through g = m*x. each entry is the global's +// symbol, the local, and the block depth of the store; the lend ends when the same +// global is assigned again in that block, and is an error if the block can end first. word lendrec; int nlend, lendcap, xlend = -1; int finsym, finfer; // the function being compiled, and whether its owned return is still to be inferred (no .u) int fn_return_type; // a consistent return type inferred from the function body -//: effects come from the real compiler's uses, stores, returns and call edges; unknown bodies stay conservative. +// effects come from the real compiler's uses, stores, returns and call edges; unknown bodies stay conservative. const int EF_READ = 1, EF_WRITE = 2, EF_RETAIN = 4, EF_CONSUME = 8, EF_UNKNOWN = 16, EF_ALIAS = 32, EF_IDENTITY = 64; word effect_symbols, effect_edges, effect_requirements, effect_current; int effect_floor, effect_argument_base; word local_origin, local_origin_mask, local_cell, local_captured, local_ended, local_end_info; word effect_value, argorigin_mask; int capture_enabled; word[1] capture_dummy; @@ -249,7 +249,7 @@ int xbor = -1; // local a pointer value is borrowed from, or -1 int mdbg, mdbg_flags; // memdebug: compile checks in, and the flags start() turns on int lsp, loopbase, ctxsp, snaptop; int lv, lvk, lvi, kw_expr, xcls, fty; -//: interned spellings the parser compares against +// interned spellings the parser compares against int s_lp, s_rp, s_lb, s_rb, s_lk, s_rk, s_comma, s_semi, s_eq, s_arrow, s_inc, s_dec; int s_star, s_slash, s_amp, s_colon, s_hash, s_minus, s_andand, s_oror; @@ -259,23 +259,23 @@ int s_frame_floor, s_frame_top, s_frame_node, s_frame_busy, s_frame_guard, s_fra int s_copy_w; int s_do, s_while, s_enum, s_include, s_memdebug, s_scrub, s_alloc, s_check, s_memset, s_static; -//: ---- types: kinds, tables and parser state (see the types section) ---- +// ---- types: kinds, tables and parser state (see the types section) ---- const int TY_WORD = 0, TY_INT = 1, TY_CHAR = 2, TY_BOOL = 3, TY_VOID = 4, TY_PTR = 5, TY_ARR = 6; -//: dyn: T[?], a = element, n = 1 if fixed +// dyn: T[?], a = element, n = 1 if fixed const int TY_FN = 7, TY_STRUCT = 8, TY_VAR = 9, TY_GEN = 10, TY_DYN = 11, TY_FLOAT = 12, TY_BSTR = 13; const int NTL = 1024; // the first NTL words list every record -//: type per local, declared tag per local, type per symbol +// type per local, declared tag per local, type per symbol const word LTYPE_BASE = R_ASM + ASM_WORDS, LTAG_BASE = LTYPE_BASE + NL, STYPE_BASE = LTAG_BASE + NL; word ltype = LTYPE_BASE, ltag = LTAG_BASE, stype = STYPE_BASE; int tytop, ntl; word tword, tint, tchar, tbool, tvoid, tfloat, tf32, tbstr; int ckst; char* ckn; // extern: the zc struct a by-value parameter names (ckind), and its name int tc_on; // the optional typechecker, off unless a program says typecheck on; -//: type, literal 0, float, byte string, dry runs (typeof), unique giveaway +// type, literal 0, float, byte string, dry runs (typeof), unique giveaway word xty; int xzero, xfloat, xstr, dry, xgive; -//: the type facts of annotate mode (see type facts below): the tables, and where the declaration being read starts +// the type facts of annotate mode (see type facts below): the tables, and where the declaration being read starts const int NIF = 431, IFP = 24, IFW = 80, IFN = 4096, IFT = 32768; int ifon; word ifs, ifr; int nif; word ifx; int nifx; word ift; int nift; word ifcur; int ifkp, ifkk, ifcall, ifgk, ifstrict; @@ -283,31 +283,31 @@ word if_edges, if_value, if_local; int ifrcap = NIF, ifxcap = IFN, iftcap = IFT; int fldst, fldi; // the one struct a bare address's field name resolved to char*[8] tpn; int ntp; // type parameters in scope -//: reading a return type: unknown names are type parameters declared after the function's name +// reading a return type: unknown names are type parameters declared after the function's name int tvauto; word readonly_types; int dty, dtag; // result of dtype() int s_lt, s_gt, s_shr, s_le, s_ge, s_eqeq, s_ne, s_plus, s_struct, s_type, s_typecheck, s_docs, s_small, s_std, s_icon, s_on, s_off, s_new, s_new_t; int s_o3, s_lfa, s_outside, s_new_raw, s_new_oz, s_oob; int s_ld8, s_st8, s_ld16, s_st16, s_ld32, s_st32; -//: ---- o3 region state ---- +// ---- o3 region state ---- -//: each promoted slot maps to a register; pending regions use 67-word records and a descriptor buffer. +// each promoted slot maps to a register; pending regions use 67-word records and a descriptor buffer. int o3on, o3nxt, o3one; word[32] o3sl; word[32] o3rg; int no3sl; word[O3_REGIONS * 67] o3pend_initial; word o3pend = o3pend_initial; int o3pendcap = O3_REGIONS, no3pend; word[80] o3dsc; int o3fs, o3na, o3fn0; -//: o3free holds registers released by closed scopes; o3cache holds the items pointer and length slots for fixed arrays. +// o3free holds registers released by closed scopes; o3cache holds the items pointer and length slots for fixed arrays. word[8] o3free; int no3free, o3nreg; word[NL] o3cache_initial; word o3cache = o3cache_initial; int argloc, s_hitems, s_hlen, s_fatten; -//: operator names used by numeric type checks. +// operator names used by numeric type checks. word[12] numops; int nnumops, s_fixed, s_dlen, s_dcap, s_dnew, s_dat, s_dpush, s_dpop, s_dresize, xconst, xcval; int xlenloc, arrloc, fortest, forstep, ft_iv, ft_bd, ft_ir, ft_nops, fs_ok, xstamp; -//: local lower bound / len(array); one proof record per live loop +// local lower bound / len(array); one proof record per live loop int nfact, factcap = RANGE_FACTS; word[RANGE_FACTS] floc_initial, flo_initial, flen_initial; word floc = floc_initial, flo = flo_initial, flen = flen_initial; int s_len_s, s_at_s, s_slice_s, s_cat_s, s_eq_s, s_lt_s, s_le_s, s_put_bs, s_put_s, s_n_to_s, s_new_s; int mdg; word[8] mdn; // the type parameters of the generic struct whose methods are being compiled word[8] gbind; int ngbind; // explicit type arguments for the next call: f<int>(...) -//: generic functions. the template (type parameters stay variables) is compiled once, as before, -//: and its tokens are kept; a call whose type arguments are all known gets an instance, the same -//: tokens compiled again with the parameters bound to those types, so new(T) and sizeof(T) -//: inside it mean the real type. instances are compiled after the item that asked for them. +// generic functions. the template (type parameters stay variables) is compiled once, as before, +// and its tokens are kept; a call whose type arguments are all known gets an instance, the same +// tokens compiled again with the parameters bound to those types, so new(T) and sizeof(T) +// inside it mean the real type. instances are compiled after the item that asked for them. int grec; word gbuf; int gn, gcap; // the tokens of the top-level item being read int gnew, gnew_np; word[8] gnew_nm; // a generic function was just defined int fn_method_compile, fnpatchseq; @@ -318,10 +318,11 @@ int s_this; const int NCLO = 8; /* initial closure nesting capacity; grows when more closures are compiled */ const int NCAP = 32; /* initial capture capacity per closure; grows when more variables are captured */ -//: ---- closure compilation state ---- -//: closures being compiled, innermost last: the enclosing function's locals -//: (lo .. hi), how many variables the body has captured so far (names and the -//: enclosing local each came from), and the body's own hidden env parameter +// ---- closure compilation state ---- + +// closures being compiled, innermost last: the enclosing function's locals +// (lo .. hi), how many variables the body has captured so far (names and the +// enclosing local each came from), and the body's own hidden env parameter int nclo, closure_cap = NCLO, capture_width = NCAP; word[NCLO] clo_lo_initial; word clo_lo = clo_lo_initial; @@ -334,43 +335,43 @@ word[256] clo_loc_initial; word clo_loc = clo_loc_initial; word[256] clo_via_initial; word clo_via = clo_via_initial; -//: ---- errors ---- +// ---- errors ---- -//: the compiler reports up to 20 errors before it stops. a syntax error -//: (die) skips the token it happened on and sets panic, which skips ahead to -//: the next ; or } (or the next top-level item) and keeps quiet until then. -//: other errors (serr) just report: the parse is still in step. -//: -//: check mode (cmode) is for the language server, which runs the real compiler -//: over a document it holds in memory. nothing is printed and nothing halts: -//: each error is written into dgbuf as a record [severity, line, offset, -//: length, chars...] and giveup raises chkabort, which makes the lexer return -//: end of file so every parsing loop winds down by itself. +// the compiler reports up to 20 errors before it stops. a syntax error +// (die) skips the token it happened on and sets panic, which skips ahead to +// the next ; or } (or the next top-level item) and keeps quiet until then. +// other errors (serr) just report: the parse is still in step. +// +// check mode (cmode) is for the language server, which runs the real compiler +// over a document it holds in memory. nothing is printed and nothing halts: +// each error is written into dgbuf as a record [severity, line, offset, +// length, chars...] and giveup raises chkabort, which makes the lexer return +// end of file so every parsing loop winds down by itself. int nerr, panic; -//: bits kept in a local's ltag for the hints: assigned after its declaration, -//: read, declared with let, initialized with a literal; the size of the object -//: a new gave it (bits 16 to 39); and how many times it was used (from bit 56) +// bits kept in a local's ltag for the hints: assigned after its declaration, +// read, declared with let, initialized with a literal; the size of the object +// a new gave it (bits 16 to 39); and how many times it was used (from bit 56) const int LASG = 0x1000, LREAD = 0x2000, LDECL = 0x4000, LCONST = 0x8000, LCNT1 = 0x100000000000000; const int NEWTYPED = 0x800000; word[NL] lpos_initial; word lpos = lpos_initial; word[NL] lreadpos_initial; word lreadpos = lreadpos_initial; word[NL] lnsz_initial; word lnsz = lnsz_initial; word[NL] lleaf_initial; word lleaf = lleaf_initial; word[NL] lrsoff_initial; word lrsoff = lrsoff_initial; word[NL] lrskind_initial; word lrskind = lrskind_initial; int newsz, newleaf, nomagic, callpos; word[NL] lptr_initial; word lptr = lptr_initial; // annotate mode: the local was indexed or dereferenced, so it holds an address -//: storage facts are shared by aliases. they describe one allocation, not a -//: variable slot: [length, lifetime, kind, allocation place, end place, owner]. -//: lifetime is 0 unknown, 1 live, 2 ended; kind is 1 stack, 2 heap, 3 array handle. +// storage facts are shared by aliases. they describe one allocation, not a +// variable slot: [length, lifetime, kind, allocation place, end place, owner]. +// lifetime is 0 unknown, 1 live, 2 ended; kind is 1 stack, 2 heap, 3 array handle. word[NL] lafact_initial; word lafact = lafact_initial; word[NL] laaddr_initial; word laaddr = laaddr_initial; word xaf, argfact; -//: value previews are separate from optimization facts. each reference keeps -//: the value proved at that position: 1 integer, 2 float bits, 3 string, 4 bytes. +// value previews are separate from optimization facts. each reference keeps +// the value proved at that position: 1 integer, 2 float bits, 3 string, 4 bytes. word lvkind, lvvalue, lvescaped, svkind, svvalue; int xvk; word xvv; -//: local name lookup keeps a head for each spelling and a shadow link per slot. +// local name lookup keeps a head for each spelling and a shadow link per slot. word[NL] lprev_initial; word lprev = lprev_initial; word[256] lkeys_initial; word lkeys = lkeys_initial; word[256] lheads_initial; word lheads = lheads_initial; int lkeycap = 256, lkeyn; int s_new_k, s_recycle_leaf; -//: dgprev: the next record sits at the end of the previous token -//: chk_hook, when set, is called after each top-level item of the document in check mode: the language -//: server streams diagnostics and drops a check whose source has been replaced +// dgprev: the next record sits at the end of the previous token +// chk_hook, when set, is called after each top-level item of the document in check mode: the language +// server streams diagnostics and drops a check whose source has been replaced word chk_hook; int cmode, chkabort, chkfuel; word dgbuf, dggrow; int dgcap, dgpos, dgn, dgopen, dgprev, dgmode; // expand a diagnostic in progress, keeping its output offset within the buffer. @@ -379,15 +380,15 @@ int dg_expand() { dgcap = dgcap * 2; rt.outbuf = dgbuf + dgpos + 5; rt.outcap = dgcap - dgpos - 5; return 0; } -//: diagnostic records for check mode. dg_open starts a record [severity, line, -//: offset, length, text length, text...] at the current token (or just past the -//: previous one when dgprev is set) and redirects the error text into it with -//: outmode 2; dg_close seals it with the length of the text and restores output. -//: severity 1 is an error, 2 a warning, and 4 a hint. -//: completion: the language server asks what is in scope at a place in the document. probe_off is that place -//: (a byte offset; -1 when nobody asks). the check keeps a copy of the locals at the last statement that starts at or -//: before it, with whether the lifetime analysis has used each one up, and, when the place is a member name after -//: a dot, what is before the dot. the check stops as soon as it has what was asked for +// diagnostic records for check mode. dg_open starts a record [severity, line, +// offset, length, text length, text...] at the current token (or just past the +// previous one when dgprev is set) and redirects the error text into it with +// outmode 2; dg_close seals it with the length of the text and restores output. +// severity 1 is an error, 2 a warning, and 4 a hint. +// completion: the language server asks what is in scope at a place in the document. probe_off is that place +// (a byte offset; -1 when nobody asks). the check keeps a copy of the locals at the last statement that starts at or +// before it, with whether the lifetime analysis has used each one up, and, when the place is a member name after +// a dot, what is before the dot. the check stops as soon as it has what was asked for int probe_off = -1, probe_has, probe_n, probe_cap, probe_static; char** probe_name; word probe_kind, probe_state, probe_type, probe_decl, probe_mem; char* curinc; int incdepth, dgdrop; int incplace; // how many includes deep the compile is; a record to throw away; the place of the outermost include @@ -460,10 +461,10 @@ int dg_dup() { while (dgseen[i]) { if (dgseen[i] == key) return 1; i = (i + 1) & (dgseen_cap - 1); } dgseen[i] = key; dgseen_n++; return 0; } -//: hints on; hints small; hints off; set how many hints the code after it gets: -//: on uses informational ranges, small uses compact hint markers, and off -//: suppresses hints. each directive is kept with its byte -//: offset, so a hint is judged by where its code is, not where it was reported. +// hints on; hints small; hints off; set how many hints the code after it gets: +// on uses informational ranges, small uses compact hint markers, and off +// suppresses hints. each directive is kept with its byte +// offset, so a hint is judged by where its code is, not where it was reported. const int HINT_SMALL = 1, HINT_ALL = 2; word hl_off, hl_lev; int hl_n, hl_cap = 16; int hint_set(int level, int at) { @@ -588,8 +589,8 @@ int dien(char* name, char* msg) { next(); return 0; } -//: four classification bits live next to each interned spelling. this works -//: for names in later pool chunks too, without indexing by a fixed pool offset. +// four classification bits live next to each interned spelling. this works +// for names in later pool chunks too, without indexing by a fixed pool offset. const int LSW = 1; word lsets; int lsmark(int set, int n) { @@ -600,13 +601,13 @@ word lsget(int set, int n) { if (!lsets) return 0; return (n[-2] >> set) & 1; } -//: name resolution for the language server. while a check runs over the document -//: at rsbase, every name a use or a declaration resolves to is written into rsrec -//: as a record: the byte offset, kind (RS_LOCAL, RS_PARAM, RS_SYM or RS_FIELD), -//: declaration or symbol, and storage facts at that position. a local names its declaration by offset -//: (-1 when it has none), a symbol names its kind and flags (see rs_sym). the -//: compiler's own scopes decide, so the highlighter and hover see locals and -//: parameters first, then globals and built-ins, the way the compiler does. +// name resolution for the language server. while a check runs over the document +// at rsbase, every name a use or a declaration resolves to is written into rsrec +// as a record: the byte offset, kind (RS_LOCAL, RS_PARAM, RS_SYM or RS_FIELD), +// declaration or symbol, and storage facts at that position. a local names its declaration by offset +// (-1 when it has none), a symbol names its kind and flags (see rs_sym). the +// compiler's own scopes decide, so the highlighter and hover see locals and +// parameters first, then globals and built-ins, the way the compiler does. const int RS_LOCAL = 1, RS_PARAM = 2, RS_SYM = 3, RS_FIELD = 4, RS_CAP = 32768, RS_W = 13; int lt_on, lt_n, ltcap = 256, ltxtcap = 2048; word lt_off, lt_at, lt_len, lt_type, ltxt; int ltxn; @@ -628,18 +629,18 @@ int field_decl(word t, char* n) { while (k < fieldn) { if (fieldtypes[k] == t && fieldnames[k] == n) return fieldoffs[k]; k++; } return 0 - 1; } -//: where each symbol was defined, for go to definition: sdoff is the byte offset of its name + 1 -//: (0 when unknown, as for built-ins) and sdsrc the include path whose text it is in (0 for the document) +// where each symbol was defined, for go to definition: sdoff is the byte offset of its name + 1 +// (0 when unknown, as for built-ins) and sdsrc the include path whose text it is in (0 for the document) word sdoff, sdsrc; -//: includes: incdir is the directory of the file being compiled (0 for the program on stdin), increoot the -//: workspace folder an editor passes in, inc_hook a function that gives the text of a file the editor -//: sent (a host with no files of its own), incres the path an include was found at +// includes: incdir is the directory of the file being compiled (0 for the program on stdin), increoot the +// workspace folder an editor passes in, inc_hook a function that gives the text of a file the editor +// sent (a host with no files of its own), incres the path an include was found at char* incdir, increoot, incres, std_buf, icon_buf; char*(char*.m) inc_hook; int std_done, icon_done; word rsrec, rstmp; word[32] pnoffs; word[32] pnplaces; int rsn, rscap, rsbase, nameoff; -//: ownership at a reference: record word 11 holds the local's state (S_* + 1, plus 16 when -//: it owns its object) and word 12 the declaration offset (+ 1) of the owner it borrows from. -//: transfers are logged as events, EV_W words each: the local's declaration offset, what -//: happened (EV_*), its line, and the target's declaration offset (+ 1) when there is one. +// ownership at a reference: record word 11 holds the local's state (S_* + 1, plus 16 when +// it owns its object) and word 12 the declaration offset (+ 1) of the owner it borrows from. +// transfers are logged as events, EV_W words each: the local's declaration offset, what +// happened (EV_*), its line, and the target's declaration offset (+ 1) when there is one. const int EV_RETURN = 1, EV_VALUE = 2, EV_UP = 3, EV_DOWN = 4, EV_STORE = 5, EV_GLOBAL = 6, EV_FREE = 7, EV_CAPTURE = 8, EV_MOVE = 9, EV_W = 4; word evrec; int evn, evcap = 64; // resize compiler tables. these buffers are collector-managed: m* relinquishes @@ -933,7 +934,7 @@ int cerrn(int i, char* msg) { return 0; } -//: ---- interning ---- +// ---- interning ---- // the pool: an open addressing hash table (R_IH) of pointers into a string // pool (R_POOL). each spelling has two spare words: its symbol-chain head at @@ -985,7 +986,7 @@ char* intern(char* s) { return p; } -//: ---- symbols ---- +// ---- symbols ---- // each interned name keeps the index (+1) of its newest symbol in p[-1], and // each symbol links to the one it shadows, so lookup is one step. @@ -1127,7 +1128,7 @@ int fixall(int head, int v) { return 0; } -//: ---- code emission ---- +// ---- code emission ---- // code goes straight into memory at "here". hdelta shifts where it is written // (an image build emits at one address and stores at another, see nested @@ -1266,9 +1267,9 @@ word vaddr(int i) { } laddr(lslot[i]); if (local_cell && local_cell[i]) op(O_LD); return 0; } -//: registers for an o3 region's variables: the cpu has 7. a variable takes the -//: one its region freed last (when a scope closed), or the next unused one -//: ---- o3: the optimizing region compiler's bookkeeping ---- +// registers for an o3 region's variables: the cpu has 7. a variable takes the +// one its region freed last (when a scope closed), or the next unused one +// ---- o3: the optimizing region compiler's bookkeeping ---- // code inside an o3 region (or a function marked o3) keeps hot locals in the // cpu's registers. the compiler does not change what it emits for them; it @@ -1376,7 +1377,7 @@ int strlit(char* s) { return 0; } -//: ---- input sources ---- +// ---- input sources ---- // frame: 0 kind (0 stdin, 1 token list, 2 string) 1 object 2 index 3 end // 4 transparent 5..7 saved token 8..10 saved char state 11 owned 12 saved line @@ -1599,7 +1600,7 @@ int bstrlit(char* s) { int j = hole(), a; op(O_JMP); a = bstrdata(s); setlit(j, here); lit(a); return 0; } -//: ---- float literals and float code ---- +// ---- float literals and float code ---- // a float is one word holding the ieee bits. the lexer collects the literal's // text in tbuf and fbits asks the core (opcode 611) to convert it, so the @@ -2028,7 +2029,7 @@ int name() { return n; } -//: ---- operator tables ---- +// ---- operator tables ---- // a sequence is written like asm: one-char mnemonics, numbers, and names // (resolved when the operator is used, so image builds stay self-contained) @@ -2245,7 +2246,8 @@ int emit_statement_hooks() { return 0; } -//: ---- function effects and temporary borrows ---- +// ---- function effects and temporary borrows ---- + word effect_record(int symbol) { if (symbol < 0) return 0; if (!effect_symbols) effect_symbols = chk_resize(0, 0, symcap); @@ -2389,7 +2391,7 @@ int borrow_condition() { word before = borrow_slots; int c = expr(); rval(); plainuse(c); ftest(); borrow_cleanup_since(before); return c; } -//: ---- locals and the analyzer's view of them ---- +// ---- locals and the analyzer's view of them ---- // value tracking is for check mode only; it never changes emitted code. int vf_symbols() { @@ -2910,9 +2912,9 @@ int local_hints(int from) { if (hint_begin(lpos[i], HINT_SMALL)) { rt.put_s("unused parameter `"); rt.put_s(lname[i]); rt.put_s("`"); dg_close(); } - } else if ((f & LDECL) && !larr[i]) { + } else if ((f & LDECL) && !larr[i] && !rt.eq_w(lname[i], "_")) { // _ discards its value if (!(f & LREAD)) hint_name(i, " is never read", HINT_SMALL); - else if ((f & LCONST) && !(f & LASG)) hint_name(i, " is never reassigned; make it a const", HINT_ALL); + else if ((f & LCONST) && !(f & LASG) && !(ltype[i] && treadonly(ltype[i]))) hint_name(i, " is never reassigned; make it a const", HINT_ALL); else if (fchk && lstate[i] == S_M && lown[i] && lnsz[i] > 0 && lnsz[i] <= 64) hint_name(i, " never leaves this function; a stack buffer (let name[n]) avoids the heap", HINT_SMALL); else if (fchk && lstate[i] == S_M && lown[i] && lused[i] && tline - lused[i] >= 4) hint_name(i, " is last used several lines above the end of its block; close the block sooner so it is freed sooner", HINT_ALL); } @@ -3029,20 +3031,20 @@ int merge_err(int i, int sa, int what) { return 0; } -//: ---- snapshots and comparing paths ---- +// ---- snapshots and comparing paths ---- -//: to analyze an if, the compiler snapshots the locals' states before the -//: branch, compiles the then path, restores the snapshot for the else path, and -//: then compares or merges the two. a snapshot is [lbase, nloc] followed by -//: state, owner and home for each local; the stack of them is at R_SNAP. -//: ---- joins ---- +// to analyze an if, the compiler snapshots the locals' states before the +// branch, compiles the then path, restores the snapshot for the else path, and +// then compares or merges the two. a snapshot is [lbase, nloc] followed by +// state, owner and home for each local; the stack of them is at R_SNAP. +// ---- joins ---- -//: where control flow comes back together (after an if, a loop, a switch or -//: ?:) every variable has to agree across the paths that reach the join. one -//: exception: a variable that became d, or was given away, on some path is dead -//: after the join, and the paths where it's still alive free it on the way in -//: (fixups). u objects and borrows don't get this treatment: a u object still -//: has to leave, and a borrow never changes. +// where control flow comes back together (after an if, a loop, a switch or +// ?:) every variable has to agree across the paths that reach the join. one +// exception: a variable that became d, or was given away, on some path is dead +// after the join, and the paths where it's still alive free it on the way in +// (fixups). u objects and borrows don't get this treatment: a u object still +// has to leave, and a borrow never changes. int extop; word[JOIN_PATHS] jps_initial; word jps = jps_initial; int jpscap = JOIN_PATHS, njps; // the paths being joined: [lbase, nloc, triples] // accessors on a path record p (a snapshot or an exit record): the state, @@ -3171,13 +3173,13 @@ int join2(int sa, int livea, int ja, int livebv, int n0, int what) { return 0; } -//: the rules below are the heart of the analyzer. each is a function the parser -//: calls at one kind of place where a value moves: recast (u*x), argument -//: passing, assignment, return, block values, and discarding. the argument -//: c they take is an expression class: C_PLAIN (an ordinary word), C_FRESH (a -//: new object nobody owns yet) or C_VAR + n (a bare local, number n), which is -//: how the analyzer knows the thing being moved is a variable it tracks. -//: ---- analyzer rules for the places values flow ---- +// the rules below are the heart of the analyzer. each is a function the parser +// calls at one kind of place where a value moves: recast (u*x), argument +// passing, assignment, return, block values, and discarding. the argument +// c they take is an expression class: C_PLAIN (an ordinary word), C_FRESH (a +// new object nobody owns yet) or C_VAR + n (a bare local, number n), which is +// how the analyzer knows the thing being moved is a variable it tracks. +// ---- analyzer rules for the places values flow ---- // recast: x as u / x as d / x as m int recast(int c, int t) { @@ -3431,9 +3433,10 @@ int discard(int c) { return 0; } -//: ---- completion ---- -//: copy local i (kind: 1 local, 2 parameter, 3 captured from the function around a closure) into the probe, -//: unless a nearer local has its name +// ---- completion ---- + +// copy local i (kind: 1 local, 2 parameter, 3 captured from the function around a closure) into the probe, +// unless a nearer local has its name int probe_add(int i, int kind) { int k = 0; while (k < probe_n) { if (probe_name[k] == lname[i]) return 0; k++; } @@ -3478,7 +3481,7 @@ int probe_member(word t) { return 0; } -//: ---- types ---- +// ---- types ---- // types are a compiler primitive: records in the type area, hash-consed so // the same type is always the same pointer and comparing types is comparing @@ -3839,9 +3842,9 @@ int closeangle() { expect(s_gt); return 0; } -//: a type, then any number of (params) after it: int(int, int) is a function -//: returning int, fn(int) one returning a plain word -//: ---- parsing types ---- +// a type, then any number of (params) after it: int(int, int) is a function +// returning int, fn(int) one returning a plain word +// ---- parsing types ---- // grammar, outermost first: // ptype a type, then any number of (params): int(int, int) is a function type @@ -4097,7 +4100,7 @@ int kw_struct() { mdg = 0; return s; } -//: ---- methods ---- +// ---- methods ---- // a method is an ordinary function named Struct.name whose first parameter is // this, a pointer to the struct. e.method(a) calls Struct.name(e, a). the @@ -4358,9 +4361,9 @@ int caststart() { } return 0; } -//: memdebug on; checks and interior pinning. memdebug scrub; also poisons frees. -//: memdebug off; stops both. at the top level, start() applies it before main. -//: ---- statements that only configure the compiler ---- +// memdebug on; checks and interior pinning. memdebug scrub; also poisons frees. +// memdebug off; stops both. at the top level, start() applies it before main. +// ---- statements that only configure the compiler ---- int kw_memdebug() { int f = 0; @@ -4382,10 +4385,10 @@ int kw_memdebug() { word tenum(char* n) { word t = tnew(TY_INT, 1, n, 0); t[6] = 16; return t; } // true for a type made by an enum declaration. word isen(word t) { return t[0] == TY_INT && (t[6] & 16); } -//: enum name { A, B = 5 }: each member is a constant, plain (A) and scoped (name.A) -//: member records for tagged enums and for plain enums (pm_en is -1 for a plain -//: member). a member's payload fields are at most 8. -//: the text a tagged enum or an extern stub is generated into +// enum name { A, B = 5 }: each member is a constant, plain (A) and scoped (name.A) +// member records for tagged enums and for plain enums (pm_en is -1 for a plain +// member). a member's payload fields are at most 8. +// the text a tagged enum or an extern stub is generated into char[GENERATED_WORDS] ebuf_initial; char* ebuf = ebuf_initial; int ebcap = GENERATED_WORDS, ebn; char*[ENUM_MEMBERS] pm_name_initial, pm_en_initial; word[ENUM_MEMBERS] pm_tag_initial, pm_nf_initial, pm_val_initial, pm_exp_initial; char** pm_name = pm_name_initial, pm_en = pm_en_initial; word pm_tag = pm_tag_initial, pm_nf = pm_nf_initial, pm_val = pm_val_initial, pm_exp = pm_exp_initial; int pmcap = ENUM_MEMBERS, npm; @@ -4549,9 +4552,10 @@ int cover_warn(int e, int cov, int sl) { if (!first) rt.put_c(10); return 0; } -//: ---- include ---- -//: the text of an embedded library, one character to a word and ending in 0, as readfile gives a file. -//: it is unpacked from the byte string the first time it is asked for. +// ---- include ---- + +// the text of an embedded library, one character to a word and ending in 0, as readfile gives a file. +// it is unpacked from the byte string the first time it is asked for. char* bstr_text(bstr p) { int n = rt.len_s(p), i = 0; char* b = m*rt.new(n + 2); while (i < n) { b[i] = rt.at_s(p, i); i++; } @@ -4946,11 +4950,11 @@ int doc_attach(char* n) { dsn = 0; return 0; } -//: the signature of a function as source text, "int put_c(int ch)", kept in the -//: image as the symbol "sig name" when docs are on, so the language server can -//: show it for any genome function, with or without a doc comment. the text is -//: printed through put_c with the output sent to sgb (the same redirect the -//: check uses for a parse word that prints) +// the signature of a function as source text, "int put_c(int ch)", kept in the +// image as the symbol "sig name" when docs are on, so the language server can +// show it for any genome function, with or without a doc comment. the text is +// printed through put_c with the output sent to sgb (the same redirect the +// check uses for a parse word that prints) char[400] sgb_initial; char* sgb = sgb_initial; int sigcap = 400; int sig_print(char* n, int rty, int np, char** pn, word pty, int typed) { int i = 0; @@ -4982,8 +4986,8 @@ int sig_attach(char* n, int rty, int np, char** pn, word pty, int typed) { addsym(intern(dnmb), K_DOC, b); return 0; } -//: the field of a struct type, or -1 -//: ---- built-ins that look like functions ---- +// the field of a struct type, or -1 +// ---- built-ins that look like functions ---- // sizeof and typeof take a type or an expression; the expression form is // compiled with dry set so it contributes only its type. len, cap, push, pop @@ -5200,7 +5204,7 @@ int kw_typeof() { xty = tword; return 0; } -//: ---- c abi: extern and cfunc ---- +// ---- c abi: extern and cfunc ---- // extern double pow(double, double); or with parameter names, and an // optional "= "cname"" to give the zc function another name than the c symbol. @@ -5623,7 +5627,7 @@ word bintype(int b, word lt, word rhs_type) { return tword; // operators a program added } -//: ---- reflection ---- +// ---- reflection ---- // for programs (and parse words) looking at types at runtime word tkind(word t) { return t[0]; } @@ -5635,7 +5639,7 @@ char* tname(word t) { return t[2]; } word tlen(word t) { return t[3]; } // the number of fields of a struct type. word tnfields(word t) { tresolve(t); return t[3]; } -//: ---- fattening ---- +// ---- fattening ---- // typed new stamps the header, in o3 the same as outside it. fatten is for a // value that still has new_raw's blank type word when it reaches code that @@ -5760,8 +5764,8 @@ word tnargs(word t) { } // type argument i of a generic struct instance. word targ(word t, int i) { return t[6][1 + i]; } -//: the type of a function, by address. untyped functions get one made from their signature -//: ---- function types by address ---- +// the type of a function, by address. untyped functions get one made from their signature +// ---- function types by address ---- word sigof(int f) { int i = nsym, s = -1; word[24] ws; int n, k; @@ -5779,7 +5783,7 @@ word sigofs(char* nm) { return sigof(sval[s]); } -//: ---- expressions ---- +// ---- expressions ---- // every expression leaves its value on the stack, returns its analyzer class, // and sets xty to its type. lv means the stack holds an address that hasn't @@ -5974,7 +5978,7 @@ int byteop(int s) { if (sname[s] == s_st32) return 617; return 0; } -//: ---- type facts ---- +// ---- type facts ---- // annotate mode (the language server asks for it, ifon) collects the types the // untyped declarations of a document could be given, as edits: a place, how @@ -6141,7 +6145,7 @@ int if_emit() { } return 0; } -//: ---- hints ---- +// ---- hints ---- // findings that are not errors. each is written only in check mode, through // hint_begin, and none of them changes the code the compiler emits. @@ -6306,7 +6310,7 @@ int enhcall(int s) { if (nout > 1) serrn(sname[s], "leaves several values; use it from asm"); return C_PLAIN; } -//: ---- closures ---- +// ---- closures ---- // fn(a, b) { ... } is a function whose last, hidden parameter is a pointer to // its environment. the closure value is a heap block: word 0 is the body's @@ -6517,9 +6521,10 @@ int closure() { return C_FRESH; } -//: ---- array initializer lists and compound literals ---- -//: keep original token places while measuring and replaying an initializer. measurement never evaluates -//: value expressions: values run once, in source order, after zero filling the storage. +// ---- array initializer lists and compound literals ---- + +// keep original token places while measuring and replaying an initializer. measurement never evaluates +// value expressions: values run once, in source order, after zero filling the storage. word array_tokens() { word q = chk_resize(0, 0, 97); int cap = 32, n = 0, depth = 0; while (tk != T_EOF) { @@ -6716,7 +6721,7 @@ int brace_primary() { src_push(1, q, 0, 0); next(); c = blockexpr(); src_pop(); rt.free(q); return c; } -//: ---- the expression grammar ---- +// ---- the expression grammar ---- // precedence, loosest to tightest: // expr (assignment, ?:) -> bin (binary operators by precedence) -> @@ -7462,7 +7467,7 @@ int bin(int minp) { } return c; } -//: ---- assignment, conditionals and blocks as expressions ---- +// ---- assignment, conditionals and blocks as expressions ---- int storekeep(int t) { if (mdbg && !dry && !ispk(t)) { lit(1); op(O_PICK); callname(s_check); op(O_DROP); } @@ -7617,7 +7622,7 @@ int blockexpr() { return c; } -//: ---- constant expressions ---- +// ---- constant expressions ---- // constant expressions, evaluated while compiling: numbers, named constants, // addresses of globals and functions, unary minus and ~, parentheses, and the @@ -7666,7 +7671,7 @@ int cexpr() { return v; } -//: ---- statements ---- +// ---- statements ---- // after a syntax error: skip to the end of the statement // statements. stmt is the dispatcher: a keyword statement (if, while, return, @@ -8280,13 +8285,13 @@ int kw_unchecked() { return 0; } -//: ---- asm blocks: raw vm words, one per token ---- +// ---- asm blocks: raw vm words, one per token ---- -//: numbers push, -n pushes a negative number, #n emits a raw word, one-char -//: tokens from the mnemonic table are opcodes, names push the address of a -//: variable or function, :name defines a label for this block. -//: (labels are local to the block: :name defines one, a later name refers to it, -//: and references to labels not yet defined are resolved when the block ends) +// numbers push, -n pushes a negative number, #n emits a raw word, one-char +// tokens from the mnemonic table are opcodes, names push the address of a +// variable or function, :name defines a label for this block. +// (labels are local to the block: :name defines one, a later name refers to it, +// and references to labels not yet defined are resolved when the block ends) word asm_names = R_ASM, asm_sites = R_ASM + ASM_LABELS; word asm_refs = R_ASM + ASM_LABELS + ASM_LABELS, asm_fixes = R_ASM + ASM_LABELS + ASM_LABELS + ASM_REFS; int asm_labelcap = ASM_LABELS, asm_refcap = ASM_REFS; @@ -8395,7 +8400,7 @@ int kw_asm() { return 0; } -//: ---- quote: code as data ---- +// ---- quote: code as data ---- // a token list is an object: [n] then kind, value, line for each token. names, // strings and operators are interned pointers. @@ -8470,7 +8475,7 @@ int feed(.d q) { return 0; } -//: ---- reading a unit and scanning it for signatures ---- +// ---- reading a unit and scanning it for signatures ---- // a unit from stdin is read whole first (4 chars to a word), then scanned // once for function signatures, so a function's tags are known before its first @@ -8616,15 +8621,15 @@ int unit_stdin() { return 0; } -//: ---- nested compiles ---- +// ---- nested compiles ---- -//: compile() and load() can run while another function is being compiled (from -//: a parse word). the function context is saved, the new code is emitted in -//: place with a jump around it, and then everything is put back. -//: the context frame is 34 words: everything about the function being compiled -//: that a nested compile would otherwise clobber (locals, depth, frame size, -//: loop stack position, the expression-state globals). closures use the same -//: pair. +// compile() and load() can run while another function is being compiled (from +// a parse word). the function context is saved, the new code is emitted in +// place with a jump around it, and then everything is put back. +// the context frame is 34 words: everything about the function being compiled +// that a nested compile would otherwise clobber (locals, depth, frame size, +// loop stack position, the expression-state globals). closures use the same +// pair. const int CTX_WORDS = 48; int ctx_push() { af_forget(); @@ -8661,9 +8666,9 @@ int ctx_pop() { return 0; } -//: ---- functions and the top level ---- +// ---- functions and the top level ---- -//: ---- a function, start to finish ---- +// ---- a function, start to finish ---- // fndef reads the header, then: fn_begin resets the per-function state, // prologue emits the frame setup, the parameters are declared and stored from @@ -8877,8 +8882,8 @@ int fndef(int chk, int rty, int ret, int n) { fn_remember(s, n); return s; } -//: let g = 1, buf[10]; int g = 3; int[10] buf; point origin; -//: ---- globals, constants, enhancements ---- +// let g = 1, buf[10]; int g = 3; int[10] buf; point origin; +// ---- globals, constants, enhancements ---- // a global's storage is emitted in place among the code (one word, or the // whole array), and the symbol's value is its address. a string initializer @@ -9175,7 +9180,7 @@ int topitem1() { die("expected a declaration"); return 0; } -//: ---- generic functions: templates and instances ---- +// ---- generic functions: templates and instances ---- // which type parameter does name n stand for while an instance is compiled int gfind(int n) { @@ -9525,8 +9530,25 @@ int similar(int n) { 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; + 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; +} +// does module define name: a function, a global, a constant or a type of it. +int 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)); +} int unit_end(int from) { - int i = from, best; + int i = from, best; char* owner; effect_finish(from); panic = 0; while (i < nsym) { @@ -9534,11 +9556,17 @@ int unit_end(int from) { // elsewhere is another file of the same build, which this document can't see if (skind[i] == K_FN && !(sflag[i] & F_DEF) && sfix[i] && !(cmode && (sflag[i] & F_DECL)) && !missing_import_name(sname[i])) { tunpack(sval[i]); - best = similar(sname[i]); + owner = export_owner(sname[i]); + best = 0; if (!owner) best = similar(sname[i]); if (!dry) { errat(); rt.put_s(sname[i]); rt.put_s(" is used but never defined"); - if (best) { rt.put_s("; did you mean '"); rt.put_s(best); rt.put_s("'?"); } + 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("?"); + } + else if (best) { rt.put_s("; did you mean '"); rt.put_s(best); rt.put_s("'?"); } errend(); } } @@ -9635,7 +9663,7 @@ int evals(char* s) { return r; } -//: ---- symbol blob ---- +// ---- symbol blob ---- // an image ends with its own symbol table so the genome knows its names: // kind, value, signature, name chars, 0 ... then a 0 kind. @@ -9730,7 +9758,7 @@ int build_image(int org, int dst) { return n; } -//: ---- setup ---- +// ---- setup ---- // the collector's roots for the compiler's half of the memory map: the program // area (globals live between functions), the genome's globals, the input source @@ -9858,11 +9886,11 @@ int zc_tables() { add_kw("resize", kw_resize, F_EXPR); return 0; } -//: the default map's type records sit in the last 16384 words of the heap. -//: the type list is the first words of that range, so it cannot be kept aside -//: as a buddy block: types_init would overwrite the header and the allocator -//: would hand the range out again. cut the heap off at R_TY until the unit -//: has been compiled, then put the tail back for the program. +// the default map's type records sit in the last 16384 words of the heap. +// the type list is the first words of that range, so it cannot be kept aside +// as a buddy block: types_init would overwrite the header and the allocator +// would hand the range out again. cut the heap off at R_TY until the unit +// has been compiled, then put the tail back for the program. int heldk; // the size class of the type area, for the memory map. int type_class() { @@ -9921,7 +9949,7 @@ int start() { return 0; } -//: ---- check mode: the language server ---- +// ---- check mode: the language server ---- // the server runs this compiler, unchanged, over a document it holds in // memory. chk_unit compiles it as a whole unit, in a machine set up the way diff --git a/src/lib/std.zc b/src/lib/std.zc index f232422..dfa9841 100644 --- a/src/lib/std.zc +++ b/src/lib/std.zc @@ -13,12 +13,12 @@ include rt, run; // the constructors arena(n) and bnew(n) and the argv helpers are plain // functions, because an owned result has to carry its .u tag. -//: ---- the command line and the environment ---- +// ---- the command line and the environment ---- -//: argc and argv, envc and envp, as passed to main by c. a zc string is -//: one character per word, so an array of them is char*[?]. libinit() fills -//: these and the runtime calls it before main, if the program has one (an -//: image built by build_image, or a check-mode document, does not). +// argc and argv, envc and envp, as passed to main by c. a zc string is +// one character per word, so an array of them is char*[?]. libinit() fills +// these and the runtime calls it before main, if the program has one (an +// image built by build_image, or a check-mode document, does not). int argc; char*[?] argv; @@ -59,7 +59,7 @@ int libinit() { return 0; } -//: ---- capturing output ---- +// ---- capturing output ---- // capture.start() redirects everything the program writes into a string and // returns it. writes go to the buffer instead of the terminal until end() @@ -90,7 +90,7 @@ struct capture { } } -//: ---- character strings ---- +// ---- character strings ---- struct Text { // the number written in decimal at the start of s. a leading '-' makes it @@ -246,7 +246,7 @@ struct Text { } } -//: ---- numbers ---- +// ---- numbers ---- // plain integer helpers. none of them can fail: an out of range result wraps // the same way the arithmetic does. @@ -331,9 +331,9 @@ struct Num { } } -//: ---- the host ---- +// ---- the host ---- -//: the argument block for Host.raw: the number, then its six arguments. +// the argument block for Host.raw: the number, then its six arguments. int rawblk[7]; struct Host { @@ -378,7 +378,7 @@ struct Host { } } -//: ---- byte strings ---- +// ---- byte strings ---- // the same helpers for packed byte strings. character-string helpers above are unchanged. // a packed byte string has type bstr; len_s reads its stored byte length. @@ -468,7 +468,7 @@ struct Bytes { } } -//: ---- arenas ---- +// ---- arenas ---- // a region that is freed all at once. // n: its capacity in words; values below 1 allocate one word @@ -489,7 +489,7 @@ struct Arena { static int reset(word a) { return rt.arena_reset(a); } } -//: ---- growable byte buffers ---- +// ---- growable byte buffers ---- // a byte buffer stores its length and capacity in two words, followed by the // bytes, packed. @@ -537,9 +537,9 @@ struct Buf { } } -//: array algorithms borrow their input and visit elements in index order. -//: map and filter return independent handles. reference elements are shared -//: with the collector, so freeing the input does not invalidate the result. +// array algorithms borrow their input and visit elements in index order. +// map and filter return independent handles. reference elements are shared +// with the collector, so freeing the input does not invalidate the result. // the first index equal to value, or -1 when no element matches. int array_find<T>(fixed T[?].m source, T value) { diff --git a/src/lsp/server.zc b/src/lsp/server.zc index b578bd1..70f9efd 100644 --- a/src/lsp/server.zc +++ b/src/lsp/server.zc @@ -1,20 +1,20 @@ namespace lsp; include rt, run; -//: ============================================================================ -//: server.zc: the language server. it is appended to the genome after the -//: compiler (cat src/runtime/rt.zc src/compiler/compiler.zc src/lsp/server.zc | zc0), so it does not -//: analyze zc with a second implementation of the grammar: it runs the -//: compiler itself over the document, in check mode (see chk_unit in -//: compiler.zc), and reports what the compiler said. a parse word, an -//: operator or a type the document defines changes how the rest of the -//: document is read, for the checker and for the highlighter, because both are -//: the compiler. it speaks json-rpc on stdin and stdout, and when stdin is not -//: a protocol stream but a source file it prints the diagnostics as text. -//: ============================================================================ +// ============================================================================ +// server.zc: the language server. it is appended to the genome after the +// compiler (cat src/runtime/rt.zc src/compiler/compiler.zc src/lsp/server.zc | zc0), so it does not +// analyze zc with a second implementation of the grammar: it runs the +// compiler itself over the document, in check mode (see chk_unit in +// compiler.zc), and reports what the compiler said. a parse word, an +// operator or a type the document defines changes how the rest of the +// document is read, for the checker and for the highlighter, because both are +// the compiler. it speaks json-rpc on stdin and stdout, and when stdin is not +// a protocol stream but a source file it prints the diagnostics as text. +// ============================================================================ -//: the first constant the server declares: the blob cut for a document starts -//: here (blob_load, compiler.zc), so this name must stay the first one of the server. +// the first constant the server declares: the blob cut for a document starts +// here (blob_load, compiler.zc), so this name must stay the first one of the server. const int LSP_SERVER = 1; // the compiler's own functions, declared here so this file checks on its own @@ -24,11 +24,12 @@ int run.chk_unit(int src, int buf, int cap, int sets, int lim); int run.hl_begin(int src); int run.hl_end(); -//: ---- memory ---- -//: nothing here has a fixed address. a document is a heap block of its text, the -//: diagnostics and the check's name sets are blocks too, and a check collects -//: what the last one left behind (chk_unit). a document compiles into the -//: program region from 0 up to CODE_END, which is the map's own. +// ---- memory ---- + +// nothing here has a fixed address. a document is a heap block of its text, the +// diagnostics and the check's name sets are blocks too, and a check collects +// what the last one left behind (chk_unit). a document compiles into the +// program region from 0 up to CODE_END, which is the map's own. const int DGCAP = 12000; word dgb, sets; int jsize, tcap; @@ -36,11 +37,12 @@ int dgbcap = DGCAP; const int MAXDOC = 16, URIW = 160; -//: ---- output ---- -//: every message is written twice: once to count its bytes for the header, -//: once for real. wc is the only thing that touches the terminal. +// ---- output ---- + +// every message is written twice: once to count its bytes for the header, +// once for real. wc is the only thing that touches the terminal. int wmode, wcnt; -//: bounded cache of completed analysis replies; document generations and include epochs are part of every key. +// bounded cache of completed analysis replies; document generations and include epochs are part of every key. const int RC_LIMIT = 64, RC_W = 8; word[RC_LIMIT * RC_W] rc_meta; word[RC_LIMIT] rc_payload; int rc_pending = -1, rc_now = -1, rc_epoch, rc_tick, rc_hits, rc_misses, analysis_checks; @@ -110,10 +112,11 @@ int send(int gen) { return 0; } -//: ---- documents ---- -//: a document is its text, four bytes to a word, in a heap block of its own -//: (doff), with a zero word after the last byte so the lexer sees the end. a -//: replaced text is a new block, and the old one is garbage for the collector. +// ---- documents ---- + +// a document is its text, four bytes to a word, in a heap block of its own +// (doff), with a zero word after the last byte so the lexer sees the end. a +// replaced text is a new block, and the old one is garbage for the collector. int nodoc, doccap = MAXDOC; char** duri; word dulen, doff, dsize, dver, dgen, dlive; @@ -167,7 +170,8 @@ int docnew(char* p, int n) { } // the text, its terminator and a spare zero word int dwords(int n) { return (n >> 2) + 2; } -//: ---- the check ---- +// ---- the check ---- + int chk_i, chk_gen; // the document the compiler's tables describe, and its generation int chk_stream, chk_stale, chk_items, chk_next, streamed, unpub = 0 - 1; // see check_progress // checks document i with the compiler in check mode. the diagnostics are left in @@ -201,9 +205,10 @@ int ensure_checked(int i) { return 0; } -//: ---- positions ---- -//: a cursor over the document that turns byte offsets into the line and the -//: utf-16 column the protocol wants. it only moves forward, or starts over. +// ---- positions ---- + +// a cursor over the document that turns byte offsets into the line and the +// utf-16 column the protocol wants. it only moves forward, or starts over. int pbase, plen, poff, pline, pcol; // starts the position cursor at the top of document i. int pc_reset(int i) { @@ -237,7 +242,8 @@ int line_off(int l) { return o; } -//: ---- diagnostics ---- +// ---- diagnostics ---- + int gd; // the document being reported // writes a json position, {line, character}, for byte offset off. int wpos(int off) { @@ -325,9 +331,10 @@ int check_publish(int i) { return 1; } -//: ---- semantic tokens ---- -//: the legend: keyword type function variable string number operator comment -//: parameter property macro enumMember +// ---- semantic tokens ---- + +// the legend: keyword type function variable string number operator comment +// parameter property macro enumMember const int TT_KW = 0, TT_TYPE = 1, TT_FN = 2, TT_VAR = 3, TT_STR = 4, TT_NUM = 5, TT_OP = 6, TT_COM = 7, TT_PARAM = 8, TT_PROP = 9, TT_MACRO = 10, TT_ENUM = 11; int tfirst, tprevl, tprevc; // one token on one line @@ -407,8 +414,8 @@ int rs_find(int o) { } return 0 - 1; } -//: rs_cur is the cursor of the highlighter's pass over the records, which move -//: forward with the tokens. +// rs_cur is the cursor of the highlighter's pass over the records, which move +// forward with the tokens. int rs_cur; // the class of an identifier at byte offset o: what the compiler resolved it to // when it checked the document, so a local named like a built-in is a variable. @@ -505,10 +512,11 @@ int gen_tokens() { } word tbase; int tn, thave, tover, tuseen; -//: ---- files and paths ---- -//: the compiler looks an include up next to the file that has it, then in the workspace folder. a client with no -//: way for the server to read files (the extension's wasm and vm servers) sends the text of every file a document -//: includes (zc/file), keyed by its full path; the compiler asks for them through inc_hook before it reads a file. +// ---- files and paths ---- + +// the compiler looks an include up next to the file that has it, then in the workspace folder. a client with no +// way for the server to read files (the extension's wasm and vm servers) sends the text of every file a document +// includes (zc/file), keyed by its full path; the compiler asks for them through inc_hook before it reads a file. char* rootbuf; int rootcap = 256, rootn, rootset; // the workspace folder: params.rootUri of initialize char* pathbuf; int pathcap = 256, pathn; // the path of a file the client sends (zc/file) char** vf_path; char** vf_text; int vf_n, vf_cap; @@ -583,10 +591,11 @@ int inc_setup(int i) { return 0; } -//: ---- json in ---- -//: the body of a message is read straight off stdin a char at a time and -//: never held whole: the fields the server cares about are picked out as they -//: go by, and a document's text is decoded into the store as it arrives. +// ---- json in ---- + +// the body of a message is read straight off stdin a char at a time and +// never held whole: the fields the server cares about are picked out as they +// go by, and a document's text is decoded into the store as it arrives. int jleft, jpk; int fm_edit_tab, fm_edit_width, fm_edit_set; // the editor's tab settings, from a formatting request int rdecl = 1, hline, hchar, hn, hoff, hcursor, ca_endline = -1, ca_endchar; char* hov; int hovcap = 2300; char* htb; int htbc = 80, htn, ann_name_at; char* hname; int hnamecap = 80; int hdl, hdle, hcs; @@ -819,7 +828,8 @@ word text_start() { return tbase; } -//: ---- messages in ---- +// ---- messages in ---- + char* lbuf; int lcap = 160; int rfirst = 0 - 1; // one header line, without the line end. returns its length, or -1 at the end of the input int rl() { @@ -873,7 +883,8 @@ int read_message() { return 1; } -//: ---- messages out ---- +// ---- messages out ---- + int wid() { if (idtype == 2) { wjs(idbuf, idn); @@ -896,7 +907,7 @@ int gen_null() { ws(",\"result\":null}"); return 0; } -//: the error code the next error reply carries. +// the error code the next error reply carries. int errcode; // an error reply with the code errcode and a matching message. int gen_error() { @@ -926,10 +937,11 @@ int reply_error(int code) { int hs_name = -1, hs_first = -1, hs_const; int bs_k, bs_off; // the embedded source and name offset found by bs_find -//: ---- hover ---- -//: the documentation of the name under the cursor: the comment lines directly -//: above its declaration in the document, or, for what the compiler and the -//: standard library define, the docs stored in the image +// ---- hover ---- + +// the documentation of the name under the cursor: the comment lines directly +// above its declaration in the document, or, for what the compiler and the +// standard library define, the docs stored in the image // reports whether a character can occur in an identifier. int hid(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; } @@ -1013,7 +1025,7 @@ int hov_put(int c) { hov[hn] = c; hn++; return 0; } -//: comment ranges are scanned once per document version, outside strings and chars. +// comment ranges are scanned once per document version, outside strings and chars. int hc_i = -1, hc_gen = -1, hc_n, hc_cap = 64; word hc_start, hc_end, hc_kind; int hover_comments(int i) { @@ -1366,11 +1378,11 @@ word abase, aerr; int nbase, aerrcap; word afx, aft, amask; int anf, afcap, atcap; word awp; int awn; word ann_half; -//: the inferred types of the untyped declarations. annotate_doc runs the annotate -//: rounds on a document (in doc slot 0, which is put back afterwards), and the -//: text it ends with is kept in ann_text. ann_type then finds, for a name the -//: document declares untyped, the type annotate gave that name: the text between -//: the token before it in the annotated text and the name, after the last ( , { ; or =. +// the inferred types of the untyped declarations. annotate_doc runs the annotate +// rounds on a document (in doc slot 0, which is put back afterwards), and the +// text it ends with is kept in ann_text. ann_type then finds, for a name the +// document declares untyped, the type annotate gave that name: the text between +// the token before it in the annotated text and the name, after the last ( , { ; or =. int ann_i = -1, ann_gen = -1, ai_end; // runs the annotate rounds on document i. the rounds run in doc slot 0, and the // slots they use are put back afterwards. the text they end with is kept in @@ -1943,11 +1955,12 @@ int do_hover(int i) { } -//: ---- definition and references ---- -//: the compiler's records say what the name under the cursor is: a local, a -//: parameter or a field names its declaration by offset, a symbol names itself -//: and the compiler kept where it was defined (sdoff, and sdsrc for a name from -//: an include). definitions and references come from those, not from text. +// ---- definition and references ---- + +// the compiler's records say what the name under the cursor is: a local, a +// parameter or a field names its declaration by offset, a symbol names itself +// and the compiler kept where it was defined (sdoff, and sdsrc for a name from +// an include). definitions and references come from those, not from text. int bs_doc_now; word bs_idx; int nav_i, nav_decl, nav_sym, nav_kind, nav_refs, nav_bdoc; char* nav_file_buf; int nav_file_n; @@ -2094,10 +2107,11 @@ int do_nav(int i, int refs) { return 0; } -//: ---- the sources of this image ---- -//: the image carries the text of rt.zc, compiler.zc, server.zc, std.zc, icon.zc and core.zc, embedded -//: when the image is built, so the definition of a built-in has somewhere to go. each source is a document -//: of its own at tinyvm:/NAME.zc, which the editor shows read only and can ask for with zc/source +// ---- the sources of this image ---- + +// the image carries the text of rt.zc, compiler.zc, server.zc, std.zc, icon.zc and core.zc, embedded +// when the image is built, so the definition of a built-in has somewhere to go. each source is a document +// of its own at tinyvm:/NAME.zc, which the editor shows read only and can ask for with zc/source const int BS_COUNT = 6; embed "src/runtime/rt.zc" as bstr rt_embedded; embed "src/compiler/compiler.zc" as bstr compiler_embedded; @@ -2266,21 +2280,22 @@ int bs_named(char* p, int n) { return 0 - 1; } -//: ---- formatting ---- -//: a pretty-printer that keeps the line breaks the author chose and sets everything else: the indent of -//: each line from the braces, parentheses and open statements above it, the spaces between the tokens of a -//: line, the blank lines between lines and the end of the file. it moves no token to another line, so -//: formatting never changes what a program means. the options are the keys of a .zcformat file found from -//: the document's directory upward (one "Key: value" per line), then the editor's tab settings, then the -//: defaults: -//: UseTab: true|false indent with tabs (default true) -//: IndentWidth: n the spaces of a level when UseTab is false (default 4) -//: MaxEmptyLines: n blank lines kept in a row (default 1) -//: ContinuationIndent: n levels added to the lines of an unfinished statement or parenthesis (default 1) -//: NormalizeSpacing: bool spaces after commas and around = == && ... (default true) -//: CaseIndent: n levels a case label is set in from its switch (default 1) -//: FinalNewline: bool end the file with one line break (default true) -//: strings, comments, asm and vm blocks and what follows a backtick keep their text. +// ---- formatting ---- + +// a pretty-printer that keeps the line breaks the author chose and sets everything else: the indent of +// each line from the braces, parentheses and open statements above it, the spaces between the tokens of a +// line, the blank lines between lines and the end of the file. it moves no token to another line, so +// formatting never changes what a program means. the options are the keys of a .zcformat file found from +// the document's directory upward (one "Key: value" per line), then the editor's tab settings, then the +// defaults: +// UseTab: true|false indent with tabs (default true) +// IndentWidth: n the spaces of a level when UseTab is false (default 4) +// MaxEmptyLines: n blank lines kept in a row (default 1) +// ContinuationIndent: n levels added to the lines of an unfinished statement or parenthesis (default 1) +// NormalizeSpacing: bool spaces after commas and around = == && ... (default true) +// CaseIndent: n levels a case label is set in from its switch (default 1) +// FinalNewline: bool end the file with one line break (default true) +// strings, comments, asm and vm blocks and what follows a backtick keep their text. int fm_tab = 1, fm_width = 4, fm_blank = 1, fm_cont = 1, fm_norm = 1, fm_casein = 1, fm_final = 1; char* fm_out; int fm_n, fm_cap; word fm_s, fm_e, fm_k, fm_o, fm_g, fm_l, fm_el, fm_sw; int fm_nt, fm_tcap, fm_swcap; @@ -2318,7 +2333,7 @@ int fm_is(int i, int t, char* w) { while (w[k]) { if (o + k >= fm_e[t] || dc(base, o + k) != w[k]) return 0; k++; } return o + k == fm_e[t]; } -//: the symbol at o: its length, and its code in fm_sym +// the symbol at o: its length, and its code in fm_sym int fm_sym; int fm_symbol(int base, int o, int size) { int c = dc(base, o), c1 = 0, c2 = 0, n = 1; @@ -2392,9 +2407,10 @@ int fm_lex(int i) { } return 0; } -//: ---- source-only hover lookup ---- -//: embedded documents have no compiler records. parse declaration boundaries rather than assuming -//: there is only one declaration, or one name, on a line. never interpret initializer uses as declarations. +// ---- source-only hover lookup ---- + +// embedded documents have no compiler records. parse declaration boundaries rather than assuming +// there is only one declaration, or one name, on a line. never interpret initializer uses as declarations. int hs_word(int i, int t) { return fm_k[t] == 1 && !run.isdig(dc(doff[i], fm_s[t])); } int hs_equal(int i, int t) { return fm_k[t] == 6 && fm_e[t] == fm_s[t] + 1 && dc(doff[i], fm_s[t]) == '='; } int hs_match(int i, int t) { return fm_is(i, t, hname); } @@ -2761,7 +2777,7 @@ int fm_comment(int i, int t, int levels) { } return 0; } -//: the state after token x: braces, parentheses, switch bodies and asm blocks +// the state after token x: braces, parentheses, switch bodies and asm blocks int fm_bd, fm_pd, fm_raw, fm_rawd, fm_pendsw, fm_pendraw, fm_lastk, fm_lasto; int fm_step(int i, int x) { int k = fm_k[x], o = fm_o[x]; @@ -2904,8 +2920,9 @@ int format_cli(int first) { } -//: ---- completion ---- -//: candidates own their text: another compiler check can replace all symbol and type tables. +// ---- completion ---- + +// candidates own their text: another compiler check can replace all symbol and type tables. char** cp_name, cp_detail, cp_docs; word cp_kind, cp_dead, cp_rank, cp_order, cp_tmp; int cp_n, cp_cap, cp_i, cp_start, cp_end, cp_cursor; char* cp_prefix; char* cp_copy(char* text) { @@ -3157,15 +3174,171 @@ int ca_import_point(int i) { return at; } 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; +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); + p[0] = hf_list; p[1] = kind; p[2] = start; p[3] = end; p[4] = a; p[5] = b; hf_list = p; + return 0; +} +// does the message of the record at at start with s, or end with s +int dg_starts(int at, char* s) { + int k = 0, n = dgb[at + 4]; + while (s[k]) { if (k >= n || dgb[at + 5 + k] != s[k]) return 0; k++; } + return 1; +} +int dg_ends(int at, char* s) { + int k = rt.len_w(s), n = dgb[at + 4]; + if (k > n) return 0; + while (k > 0) { k--; n--; if (dgb[at + 5 + n] != s[k]) return 0; } + return 1; +} +// the uses of the local declared at off, from the compiler's reference records +int local_uses(int off) { + int q = 0, n = 0; + while (q < run.rsn) { + if (run.rsrec[q * run.RS_W + 1] == run.RS_LOCAL && run.rsrec[q * run.RS_W + 2] == off && run.rsrec[q * run.RS_W] != off) n++; + q++; + } + return n; +} +int hf_space(int i, int o) { while (o < dsize[i] && (dc(doff[i], o) == 32 || dc(doff[i], o) == 9)) o++; return o; } +// the start of the statement whose declared name starts at name: past the previous ; { } or line start +int hf_head(int i, int name) { + int o = name, c; + while (o > 0) { c = dc(doff[i], o - 1); if (c == ';' || c == '{' || c == '}' || c == 10) break; o--; } + return hf_space(i, o); +} +// a single declarator: from the end of the name to ; with no comma outside brackets +int hf_single(int i, int o) { + int depth = 0, c; + while (o < dsize[i]) { + c = dc(doff[i], o); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if (c == ',' && !depth) return 0; + else if (c == ';' && !depth) return 1; + else if (c == 34 || c == 39) return 0; + o++; + } + return 0; +} +// the start of the left operand of the * at op: a name, number, call or index chain. -1 when +// the operand is not that simple, or when a * / or % binds it to something further left, since +// the rewrite has to keep the meaning: (operand << k) replaces operand * 2^k +int hf_operand(int i, int op) { + int o = op, depth = 0, c, b; + while (o > 0 && (dc(doff[i], o - 1) == 32 || dc(doff[i], o - 1) == 9)) o--; + while (o > 0) { + c = dc(doff[i], o - 1); + if (c == ')' || c == ']') depth++; + else if (c == '(' || c == '[') { if (!depth) break; depth--; } + else if (!depth && !hid(c) && c != '.') break; + else if (depth && (c == 10 || c == ';' || c == '{' || c == '}')) return 0 - 1; + o--; + } + 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; } + return o; +} +// is the text at o the word w, ending there +int hf_word(int i, int o, char* w) { + int k = 0; + while (w[k]) { if (o + k >= dsize[i] || dc(doff[i], o + k) != w[k]) return 0; k++; } + return o + k >= dsize[i] || !hid(dc(doff[i], o + k)); +} +// the hint records over the cursor, turned into edits where the change is certain +int hint_fixes(int i, int cursor, int limit) { + int at = 0, start, end, o, digits, n, v, k; + while (at < run.dgpos) { + start = dgb[at + 2]; end = start + dgb[at + 3]; + if ((dgb[at] == 3 || dgb[at] == 4) && start >= 0 && cursor >= 0 && start <= limit && end > cursor) { + if (dg_starts(at, "unused parameter `")) hf_add(HF_RENAME, start, end, 0, 0); + else if (dg_ends(at, " is never read") && !local_uses(start)) hf_add(HF_RENAME, start, end, 0, 0); + else if (dg_ends(at, " is never reassigned; make it a const") && hf_single(i, end)) hf_add(HF_CONST, hf_head(i, start), start, end, 0); + else if (dg_starts(at, "multiplying by ")) { + // the hint sits on the *; the literal follows it + o = start; if (dc(doff[i], o) == '*') o = hf_space(i, o + 1); else o = 0 - 1; + v = 0; digits = o; + while (o >= 0 && digits < dsize[i] && dc(doff[i], digits) >= '0' && dc(doff[i], digits) <= '9') { v = v * 10 + dc(doff[i], digits) - '0'; digits++; } + k = 0; while ((1 << k) < v) k++; + n = hf_operand(i, start); + if (n >= 0 && o >= 0 && digits > o && v > 1 && (1 << k) == v && !hid(dc(doff[i], digits))) hf_add(HF_SHIFT, n, digits, k, start); + } + else if (dg_ends(at, " never leaves this function; a stack buffer (let name[n]) avoids the heap") && hf_word(i, hf_head(i, start), "let")) { + // let name = rt.new(n); becomes let name[n]; + o = hf_space(i, end); + if (dc(doff[i], o) == '=') { + o = hf_space(i, o + 1); + n = o; k = 0; while (k < 7 && dc(doff[i], n + k) == "rt.new("[k]) k++; + if (k == 7) { + n = hf_space(i, n + 7); digits = n; + while (digits < dsize[i] && dc(doff[i], digits) >= '0' && dc(doff[i], digits) <= '9') digits++; + v = hf_space(i, digits); + if (digits > n && dc(doff[i], v) == ')' && dc(doff[i], hf_space(i, v + 1)) == ';') hf_add(HF_STACK, end, hf_space(i, v + 1), n, digits); + } + } + } + } + at += 5 + dgb[at + 4]; + } + return 0; +} +// the title and the replacement text of hint fix p, into hov +int hf_title(word p) { + int k = p[2]; + hn = 0; + 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 { hov_text("use a stack buffer"); } + return 0; +} +int hf_text(word p) { + int k, c; + hn = 0; + if (p[1] == HF_RENAME) hov_put('_'); + else if (p[1] == HF_CONST) { + 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++; } } + } + else if (p[1] == HF_SHIFT) { + // (operand << k): the operand's text runs from the record's start to the *, without trailing blanks + hov_put('('); k = p[2]; c = p[5]; + while (c > k && (dc(doff[ca_i], c - 1) == 32 || dc(doff[ca_i], c - 1) == 9)) c--; + while (k < c) { hov_put(dc(doff[ca_i], k)); k++; } + hov_text(" << "); hov_number(p[4]); hov_put(')'); + } + else { hov_put('['); k = p[4]; while (k < p[5]) { hov_put(dc(doff[ca_i], k)); k++; } hov_put(']'); } + return 0; +} int gen_code_actions() { - word p = ca_list; int first = 1, imported; + word p = ca_list; int first = 1, imported, single = ca_list && !ca_list[0]; ws("{\"jsonrpc\":\"2.0\",\"id\":"); wid(); ws(",\"result\":["); + p = hf_list; + while (p) { + if (!first) wc(','); first = 0; + hf_title(p); ws("{\"title\":"); wjs(hov, hn); + ws(",\"kind\":\"quickfix\",\"isPreferred\":true,\"edit\":{\"documentChanges\":[{\"textDocument\":{\"uri\":"); + wjs(duri[ca_i], dulen[ca_i]); ws(",\"version\":"); wn(dver[ca_i]); ws("},\"edits\":["); + pc_reset(ca_i); + ws("{\"range\":{\"start\":"); wpos(p[2]); ws(",\"end\":"); wpos(p[3]); ws("},\"newText\":"); + hf_text(p); wjs(hov, hn); ws("}]}]}}"); + p = p[0]; + } + p = ca_list; while (p) { if (!first) wc(','); first = 0; imported = run.module_imported(p[1]); hn = 0; - if (!imported) { hov_text("include "); hov_text(p[1]); hov_put(';'); } - if (p[2]) { if (!imported) hov_text(" and "); hov_text("qualify as "); hov_text(p[1]); hov_put('.'); hov_text(hname); } - ws("{\"title\":"); wjs(hov, hn); ws(",\"kind\":\"quickfix\",\"edit\":{\"documentChanges\":[{\"textDocument\":{\"uri\":"); + 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); } + ws("{\"title\":"); wjs(hov, hn); ws(",\"kind\":\"quickfix\","); + if (single) ws("\"isPreferred\":true,"); + ws("\"edit\":{\"documentChanges\":[{\"textDocument\":{\"uri\":"); wjs(duri[ca_i], dulen[ca_i]); ws(",\"version\":"); wn(dver[ca_i]); ws("},\"edits\":["); pc_reset(ca_i); if (!imported) { @@ -3184,7 +3357,7 @@ int gen_code_actions() { } 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; ca_i = i; + ca_list = 0; hf_list = 0; ca_i = i; ensure_checked(i); hover_word(i); cursor = hcursor; limit = cursor; if (ca_endline >= 0) { hline = ca_endline; hchar = ca_endchar; hover_word(i); limit = hcursor; hline = sl; hchar = sc; } @@ -3193,7 +3366,7 @@ int do_code_actions(int i) { if (dgb[at] == 1 && start >= 0 && cursor >= 0 && start <= limit && end > cursor) { error = at; break; } at += 5 + dgb[at + 4]; } - if (error < 0) { send(gen_code_actions); return 0; } + if (error < 0) { hint_fixes(i, cursor, limit); send(gen_code_actions); return 0; } pc_reset(i); pc_to(dgb[error + 2] + dgb[error + 3] - 1); hline = pline; hchar = pcol; if (!hover_word(i)) { send(gen_code_actions); return 0; } ca_start = hoff; ca_end = hoff + rt.len_w(hname); @@ -3271,8 +3444,9 @@ int gen_cache_stats() { ws(",\"misses\":"); wn(rc_misses); ws(",\"checks\":"); wn(analysis_checks); ws("}}"); return 0; } -//: ---- dispatch ---- -//: the text just read belongs to the document named by the uri +// ---- dispatch ---- + +// the text just read belongs to the document named by the uri int commit_text() { int i; if (!thave || !tuseen) return 0 - 1; @@ -3363,10 +3537,11 @@ int handle() { return 0; } -//: ---- command line ---- -//: a source file on stdin instead of a protocol stream: print what the -//: compiler found, one line each. with --annotate, print the file with the -//: types the compiler can see written in +// ---- command line ---- + +// a source file on stdin instead of a protocol stream: print what the +// compiler found, one line each. with --annotate, print the file with the +// types the compiler can see written in int load_stdin(int first) { int n = 0, c = first, cap = 4096; word nb; int k; tbase = rt.new(cap); @@ -3407,15 +3582,16 @@ int cli(int first) { return 0; } -//: ---- annotate ---- -//: zclsp --annotate < file.zc prints the file with the types the compiler can -//: see written in: locals from their initializers, parameters from what every -//: call passes them or from being used as an address, returns from what the -//: function returns (the type facts of compiler.zc). the typechecker is on for -//: it, and every edit has to leave the file with no error it didn't have, so -//: what comes out compiles wherever the input did. when a batch of edits -//: fails, the batch is cut in halves until the bad edits are found and left -//: out. run it again and the types it just wrote let the next round see more. +// ---- annotate ---- + +// zclsp --annotate < file.zc prints the file with the types the compiler can +// see written in: locals from their initializers, parameters from what every +// call passes them or from being used as an address, returns from what the +// function returns (the type facts of compiler.zc). the typechecker is on for +// it, and every edit has to leave the file with no error it didn't have, so +// what comes out compiles wherever the input did. when a batch of edits +// fails, the batch is cut in halves until the bad edits are found and left +// out. run it again and the types it just wrote let the next round see more. int aw_start(int p) { awp = p; awn = 0; return 0; } // appends byte c to the output being built at awp (awn bytes so far). int aw(int c) { diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc index a54d88c..ef5a5c6 100644 --- a/src/runtime/rt.zc +++ b/src/runtime/rt.zc @@ -1,33 +1,33 @@ namespace rt; include run; -//: ============================================================================ -//: rt.zc: the runtime half of the genome. memory map, i/o, arithmetic helpers, -//: the heap (size-class free lists) and the mark-sweep collector. -//: ============================================================================ +// ============================================================================ +// rt.zc: the runtime half of the genome. memory map, i/o, arithmetic helpers, +// the heap (size-class free lists) and the mark-sweep collector. +// ============================================================================ -//: how the runtime is laid out, for a reader coming in cold: -//: -//: memory map generated constants name the regions used by a build. the -//: compiler and runtime use the same names and values. -//: i/o, strings thin wrappers over the cpu's i/o opcodes, plus the two -//: string forms (char* strings and packed byte strings). -//: heap a buddy allocator with a 5 word header on every block. -//: new/free/recycle are built on it. every heap object has at -//: most one owner (the container that holds it), and freeing -//: an owner frees what it owns. -//: growable arrays T[?] handles, built on the heap. -//: mark-sweep a conservative collector. it is the safety net behind -//: free: anything the compiler's ownership analysis could not -//: free gets reclaimed here. -//: memory debug optional checks (use after free, double free, leaks). -//: syscalls file and environment access through one gate word. -//: -//: everything is a 64-bit word, and an address is a word index, not a byte -//: index. the cpu is a stack machine, so "asm { a @ . }" pushes a, fetches the -//: word at that address with @, then outputs it with . (see the vm reference). -//: -//: ---- memory map (words) ---- +// how the runtime is laid out, for a reader coming in cold: +// +// memory map generated constants name the regions used by a build. the +// compiler and runtime use the same names and values. +// i/o, strings thin wrappers over the cpu's i/o opcodes, plus the two +// string forms (char* strings and packed byte strings). +// heap a buddy allocator with a 5 word header on every block. +// new/free/recycle are built on it. every heap object has at +// most one owner (the container that holds it), and freeing +// an owner frees what it owns. +// growable arrays T[?] handles, built on the heap. +// mark-sweep a conservative collector. it is the safety net behind +// free: anything the compiler's ownership analysis could not +// free gets reclaimed here. +// memory debug optional checks (use after free, double free, leaks). +// syscalls file and environment access through one gate word. +// +// everything is a 64-bit word, and an address is a word index, not a byte +// index. the cpu is a stack machine, so "asm { a @ . }" pushes a, fetches the +// word at that address with @, then outputs it with . (see the vm reference). +// +// ---- memory map (words) ---- // src/tools/genmap writes the map values for each build between these markers. // keep the names and order stable so programs can use the same map interface @@ -51,7 +51,7 @@ const DSHI = 0x1D4FF0; const MAGIC = 0x2B7E1516; // @end-map -//: frames grow in stable segments so pointers to a caller's locals stay valid. +// frames grow in stable segments so pointers to a caller's locals stay valid. word frame_floor = FBOT, frame_top = FTOP, frame_node; int frame_chunk = FTOP - FBOT, frame_guard = (FTOP - FBOT) >> 2, frame_busy, gc_active; @@ -61,7 +61,7 @@ word return_low = RS0, return_top = MARK0 - 8, return_node; const int F_STACK = 1024; const int F_INTERNAL = 512; -//: ---- virtual enhancements ---- +// ---- virtual enhancements ---- // install(desc) passes a descriptor to the core through the % opcode. the core // reads it and installs the region it describes in the cpu, so the compiler can replace @@ -84,7 +84,7 @@ enhance ishl(a, b) micro "a = shl a b"; enhance lfa(k) micro "r1 = ld 589824; k = add r1 k"; // 1 when i is outside 0..n-1 (the unsigned compare also catches negative i), else 0. enhance outside(i, n) micro "i = ltu i n; i = xor i 1"; -//: ---- bit operations and float conversions ---- +// ---- bit operations and float conversions ---- // these use single machine instructions where the cpu has them. @@ -114,7 +114,7 @@ enhance zf2s(a) micro "a = f2s a"; // s2f, the software form used where the cpu has none. enhance zs2f(a) micro "a = s2f a"; -//: runtime code segments retain the addresses of earlier functions and globals. +// runtime code segments retain the addresses of earlier functions and globals. word code_blocks; // distinguish a raw function address from a heap closure's environment block. int callable_code(word f) { @@ -124,13 +124,13 @@ int callable_code(word f) { return 0; } -//: ---- i/o ---- +// ---- i/o ---- -//: output can be redirected. mode 1 drops what is written and mode 2 appends -//: it to a buffer; mode 3 counts characters without storing them (the server -//: uses these modes while it checks a document, -//: so a parse word that prints can't write into the protocol stream). -//: raw_c always reaches the terminal. +// output can be redirected. mode 1 drops what is written and mode 2 appends +// it to a buffer; mode 3 counts characters without storing them (the server +// uses these modes while it checks a document, +// so a parse word that prints can't write into the protocol stream). +// raw_c always reaches the terminal. int outmode; char* outbuf; int outn, outcap; word outgrow; // write one character to the terminal with no translation. @@ -185,12 +185,12 @@ int eq_w(char* a, char* b) { while (a[i] && a[i] == b[i]) i++; return a[i] == b[i]; } -//: ---- word helpers ---- +// ---- word helpers ---- -//: copy and fill by word. copy_w copies forward, so overlapping ranges are only -//: valid when dst is below src. a long run above the program area goes to the -//: host in one call (ops 18 and 19 of the syscall gate); a host that answers -1 -//: (a board, the js vm, a range that overlaps itself) gets the plain loop. +// copy and fill by word. copy_w copies forward, so overlapping ranges are only +// valid when dst is below src. a long run above the program area goes to the +// host in one call (ops 18 and 19 of the syscall gate); a host that answers -1 +// (a board, the js vm, a range that overlaps itself) gets the plain loop. const int SYS_FILL = 18, SYS_COPY = 19; // copy n words from src to dst. @@ -342,7 +342,7 @@ bstr n_to_s(int n) { return p; } -//: ---- arithmetic the hardware doesn't have ---- +// ---- arithmetic the hardware doesn't have ---- word[64] pow2; int zrem; @@ -380,42 +380,42 @@ int zmod(int a, int b) { // a shifted left by b bits, for b under 64. int zshl(int a, int b) { return a * pow2[b & 63]; } -//: ---- heap ---- +// ---- heap ---- -//: a buddy allocator. the heap is cut into aligned power-of-two chunks, every -//: block is a power of two words, and a freed block merges with its buddy -//: (the other half of the block they were split from) whenever that one is -//: free too, so freed space can be reused at any size. every block has a 5 -//: word header before the pointer the program sees: -//: p[-5] block size -//: p[-4] flags in the low 16 bits, size class in the next 5 -//: 0 free (check word cleared) or quarantined (check word still p ^ MAGIC) -//: 1 live 2 releasing 3 marked 4 quarantined but a word still points at it -//: bit 8 (value 8) is orthogonal: the object is shared. free is a no-op, -//: own and release do not follow it, and the sweep still collects it when -//: nothing points at it. bits 0..2 keep the meanings above. -//: p[-3] p ^ MAGIC p[-2] owner (the container that owns it, or 0) -//: p[-1] its type (a type record from new(T), 0 from new(n), -(n + 1) from o3's new_raw) -//: free blocks sit on a doubly linked list per class, linked through p[0], p[1]. -//: free quarantines: the block stays intact until a sweep finds nothing pointing -//: at it. recycle is the fast path for a pointer the program has already -//: dropped: it pushes the block onto its size-class list and does not walk the -//: heap. a two-word node is still 8 words (the smallest block); a three-word -//: node fits in 8 instead of 16. +// a buddy allocator. the heap is cut into aligned power-of-two chunks, every +// block is a power of two words, and a freed block merges with its buddy +// (the other half of the block they were split from) whenever that one is +// free too, so freed space can be reused at any size. every block has a 5 +// word header before the pointer the program sees: +// p[-5] block size +// p[-4] flags in the low 16 bits, size class in the next 5 +// 0 free (check word cleared) or quarantined (check word still p ^ MAGIC) +// 1 live 2 releasing 3 marked 4 quarantined but a word still points at it +// bit 8 (value 8) is orthogonal: the object is shared. free is a no-op, +// own and release do not follow it, and the sweep still collects it when +// nothing points at it. bits 0..2 keep the meanings above. +// p[-3] p ^ MAGIC p[-2] owner (the container that owns it, or 0) +// p[-1] its type (a type record from new(T), 0 from new(n), -(n + 1) from o3's new_raw) +// free blocks sit on a doubly linked list per class, linked through p[0], p[1]. +// free quarantines: the block stays intact until a sweep finds nothing pointing +// at it. recycle is the fast path for a pointer the program has already +// dropped: it pushes the block onto its size-class list and does not walk the +// heap. a two-word node is still 8 words (the smallest block); a three-word +// node fits in 8 instead of 16. const int HDR = 5; -//: p[-4] bit, orthogonal to live / mark / releasing. +// p[-4] bit, orthogonal to live / mark / releasing. const int F_SHARE = 8; -//: flags live in these bits; the size class is above them. +// flags live in these bits; the size class is above them. const int F_LOW = 65535; int hbump; word[32] freel; int gc_runs, gc_freed, gc_auto = 1; -//: block walks and new chunks stop here. +// block walks and new chunks stop here. int hstop; -//: while nonzero, balloc will not return a block overlapping this range. +// while nonzero, balloc will not return a block overlapping this range. int hold_lo, hold_hi; -//: words of memory the host has (0: the heap cannot grow); hcut: the heap was cut, so it must not grow. +// words of memory the host has (0: the heap cannot grow); hcut: the heap was cut, so it must not grow. int hmax, hcut; -//: memdebug bits, and the line the compiler stamps before new. +// memdebug bits, and the line the compiler stamps before new. int dbg, alloc_line; // the size of the top-level chunk holding heap offset off, within [0, hstop) @@ -563,7 +563,7 @@ word balloc(int k) { } return 0; } -//: ---- a heap that grows ---- +// ---- a heap that grows ---- // the heap is [HEAP_LO, hstop). on a map with HEAP_GROW the memory above // HEAP_HI belongs to the host (a big zeroed array nobody has touched yet), and @@ -613,7 +613,7 @@ int hfree() { } return n; } -//: end of the block bfree last made, for the sweep. +// end of the block bfree last made, for the sweep. int fend; // put block b back, merging it with its buddy as long as it can int bfree(word b) { @@ -650,10 +650,10 @@ int bdrop(word b) { // quarantined)? the check word p[-3] == p ^ MAGIC validates the pointer before // checking it against block metadata; a random value would have to match by chance. word isobj(word x) { return x >= HEAP_LO + HDR && x < hbump && x[-3] == (x ^ MAGIC) && (x[-4] & 1); } -//: the number of words to skip in p[0..n) before one that could be a pointer -//: to a block (see op 20 in core.c); n when none can be. a long payload of -//: plain numbers costs one host call instead of an isobj per word. a host that -//: does not have the op (-1) is asked once and then never again. +// the number of words to skip in p[0..n) before one that could be a pointer +// to a block (see op 20 in core.c); n when none can be. a long payload of +// plain numbers costs one host call instead of an isobj per word. a host that +// does not have the op (-1) is asked once and then never again. const int SYS_SCAN = 20; int scan_off; // walk the n words at p and mark every heap object they point to. @@ -956,7 +956,7 @@ word tget(word p, int i) { return p[i]; } -//: ---- growable arrays ---- +// ---- growable arrays ---- // a T[?] is a handle: [len, cap, items, element size]. items is an object the // handle owns; growing swaps it for a bigger one and moves its children over. @@ -1085,12 +1085,12 @@ int dlen(word h) { return h[0]; } // the capacity of a growable array: how many elements it can hold before it grows. int dcap(word h) { return h[1]; } -//: ---- mark-sweep ---- +// ---- mark-sweep ---- -//: conservative and non-moving. roots: the program area (globals live -//: between functions), the genome's globals, the compiler's input stack, -//: the live part of the frame stack and the live part of the data stack. -//: opcode 619 pushes that index. a word below it is not a root. +// conservative and non-moving. roots: the program area (globals live +// between functions), the genome's globals, the compiler's input stack, +// the live part of the frame stack and the live part of the data stack. +// opcode 619 pushes that index. a word below it is not a root. int ms_top, ms_over, gc_lo, gc_hi; word ms_buf = MARK0; int markcap = MARKN; @@ -1213,12 +1213,12 @@ int scrub() { return 0; } -//: ---- memory debug ---- +// ---- memory debug ---- -//: dbg bits: 1 header checks and allocation sites, 2 poison the payload on free -//: (the word 57005, which is 0xDEAD), 4 interior pointers keep a quarantined -//: block (the mark walks back at most 64 words to a real header). off unless -//: a program requests. sites are p ^ MAGIC so the collector does not treat them as pointers. +// dbg bits: 1 header checks and allocation sites, 2 poison the payload on free +// (the word 57005, which is 0xDEAD), 4 interior pointers keep a quarantined +// block (the mark walks back at most 64 words to a real header). off unless +// a program requests. sites are p ^ MAGIC so the collector does not treat them as pointers. const int NSITE = 64; const int POISON = 57005; @@ -1301,28 +1301,28 @@ int memreport() { return n; } -//: ---- host syscalls ---- +// ---- host syscalls ---- -//: a store of the op number to SYSGATE runs it. the three words after the gate -//: are the arguments, and the store leaves the result in the gate. -//: 1 open(path, 0 read | 1 write-create) 2 close(fd) -//: 3 read(fd, buf, n) 4 write(fd, buf, n) bytes, one per word -//: 5 getenv(name, buf, cap) 6 time() unix seconds -//: 21 argc() 22 argv(i, buf, cap) 23 envc() 24 envp(i, buf, cap) -//: 40 raw(block) any linux syscall: the block holds the number and six arguments. -//: the result is the kernel's, or -errno. see Host.raw in src/lib/std.zc +// a store of the op number to SYSGATE runs it. the three words after the gate +// are the arguments, and the store leaves the result in the gate. +// 1 open(path, 0 read | 1 write-create) 2 close(fd) +// 3 read(fd, buf, n) 4 write(fd, buf, n) bytes, one per word +// 5 getenv(name, buf, cap) 6 time() unix seconds +// 21 argc() 22 argv(i, buf, cap) 23 envc() 24 envp(i, buf, cap) +// 40 raw(block) any linux syscall: the block holds the number and six arguments. +// the result is the kernel's, or -errno. see Host.raw in src/lib/std.zc const int SYSGATE = ZFP + 2; const int SYS_OPEN = 1, SYS_CLOSE = 2, SYS_READ = 3, SYS_WRITE = 4, SYS_GETENV = 5, SYS_TIME = 6; -/* terminates the host process with the exit status supplied in argument a. */ +// terminates the host process with the exit status supplied in argument a. const int SYS_EXIT = 7; const int SYS_BOPEN = 10, SYS_BREAD = 11, SYS_BWRITE = 12, SYS_BGETENV = 13; const int SYS_RAW = 40; -//: the command line and the environment. argv holds the program path, which is -//: <eval> when the source came from stdin, so argc is 1 even with no arguments. -//: the string ops take an index and copy one entry across, one character per -//: word like SYS_GETENV: a zero buffer requests the length alone, so the caller -//: can size the string before it fetches it. src/lib/std.zc builds the arrays. +// the command line and the environment. argv holds the program path, which is +// <eval> when the source came from stdin, so argc is 1 even with no arguments. +// the string ops take an index and copy one entry across, one character per +// word like SYS_GETENV: a zero buffer requests the length alone, so the caller +// can size the string before it fetches it. src/lib/std.zc builds the arrays. const int SYS_ARGC = 21, SYS_ARG = 22, SYS_ENVC = 23, SYS_ENV = 24; // byte-string paths. the address is the first byte (addr_b of the word after // the length), not the string object. char* string syscalls are unchanged. @@ -1333,15 +1333,15 @@ int bread(int fd, bstr s, int n) { return syscall(SYS_BREAD, fd, addr_b(bstr_sto int bwrite(int fd, bstr s, int n) { return syscall(SYS_BWRITE, fd, addr_b(bstr_storage(s) + 1), n); } // copy the value of the environment variable name into dst, returning its length. int bgetenv(bstr name, bstr dst, int n) { return syscall(SYS_BGETENV, addr_b(bstr_storage(name) + 1), addr_b(bstr_storage(dst) + 1), n); } -//: ---- c abi ---- +// ---- c abi ---- -//: ops 14..17 reach native code (see the c abi section of core.c and extern in -//: compiler.zc). cfind(name) is 1 if a native symbol resolves (name is a byte -//: string), cload(path) opens a library so its symbols resolve, and -//: cbmake(entry, sig) is a native function pointer that runs the zc function at -//: entry (cfunc(f) in the compiler builds the signature). a native pointer -//: from cbmake only works while a native call is in progress, and not on the -//: arm64 jit, where it is 0. +// ops 14..17 reach native code (see the c abi section of core.c and extern in +// compiler.zc). cfind(name) is 1 if a native symbol resolves (name is a byte +// string), cload(path) opens a library so its symbols resolve, and +// cbmake(entry, sig) is a native function pointer that runs the zc function at +// entry (cfunc(f) in the compiler builds the signature). a native pointer +// from cbmake only works while a native call is in progress, and not on the +// arm64 jit, where it is 0. const int SYS_CCALL = 14, SYS_CFIND = 15, SYS_CBMAKE = 16, SYS_CLOAD = 17; // the address of the native symbol name in the loaded libraries, or 0 when it is not diff --git a/tests/drivers/codeactions_client_test.js b/tests/drivers/codeactions_client_test.js index f1936e7..6f956b9 100644 --- a/tests/drivers/codeactions_client_test.js +++ b/tests/drivers/codeactions_client_test.js @@ -9,7 +9,7 @@ class CodeAction { constructor(title, kind) { this.title = title; this.kind = ki const vscode = { Range, WorkspaceEdit, CodeAction, CodeActionKind: { QuickFix: 'quickfix' }, Uri: { parse: s => s } }; const uri = 'file:///test.zc', doc = { uri: { toString: () => uri }, version: 3 }; const edit = { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: 'include rt;\n' }; -const source = { title: 'include rt;', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } }; +const source = { title: 'include rt', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } }; const actions = convertCodeActions(vscode, [source], doc); assert.strictEqual(actions.length, 1); assert.strictEqual(actions[0].kind, 'quickfix'); diff --git a/tests/drivers/hint_fixes_test.js b/tests/drivers/hint_fixes_test.js new file mode 100644 index 0000000..9513a85 --- /dev/null +++ b/tests/drivers/hint_fixes_test.js @@ -0,0 +1,66 @@ +// quick fixes for hints and for library names used without their module: each fix is the +// edit the hint describes, offered only where the change keeps the program's meaning. +// usage: node tests/drivers/hint_fixes_test.js path/to/zclsp +// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; +const cp = require('child_process'); +const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); }; +let ok = 0, bad = 0; +function check(cond, label, extra) { + if (cond) { ok++; console.log('ok ' + label); } + else { bad++; console.log('FAIL ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 400))); } +} +// the actions offered at the first occurrence of needle (plus offset) in text +function actions(text, needle, offset = 0) { + const at = text.indexOf(needle) + offset, before = text.slice(0, at); + const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 }; + const uri = 'file:///hint-fixes.zc'; + const messages = [{ id: 1, method: 'initialize', params: {} }, + { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }, + { id: 2, method: 'textDocument/codeAction', params: { textDocument: { uri }, range: { start: position, end: position }, context: { diagnostics: [] } } }, + { method: 'exit' }]; + const r = cp.spawnSync(process.argv[2], [], { input: Buffer.concat(messages.map(m => frame({ jsonrpc: '2.0', ...m }))), timeout: 30000 * TIMEOUT_SCALE }); + const out = r.stdout.toString(), i = out.indexOf('{"jsonrpc":"2.0","id":2'); + return i < 0 ? null : JSON.parse(out.slice(i, out.indexOf('Content-Length', i) < 0 ? undefined : out.indexOf('Content-Length', i))).result; +} +// apply the first action's edits to text +function apply(text, action) { + const lines = text.split('\n'); + const offset = p => lines.slice(0, p.line).reduce((n, l) => n + l.length + 1, 0) + p.character; + const edits = action.edit.documentChanges[0].edits.map(e => [offset(e.range.start), offset(e.range.end), e.newText]).sort((a, b) => b[0] - a[0]); + for (const [s, e, t] of edits) text = text.slice(0, s) + t + text.slice(e); + return text; +} +function fixes(label, text, needle, offset, expected, title) { + const result = actions(text, needle, offset); + const first = result && result[0]; + check(first && first.title === title && first.isPreferred === true && apply(text, first) === expected, label, result); +} +function none(label, text, needle, offset) { + const result = actions(text, needle, offset); + check(Array.isArray(result) && result.length === 0, label, result); +} +const head = 'include rt;\nfn main() {\n'; +fixes('an unused parameter is renamed to _', 'include rt;\nint g(int a, int b) { return a; }\nfn main() { return g(1, 2); }\n', 'int b', 4, + 'include rt;\nint g(int a, int _) { return a; }\nfn main() { return g(1, 2); }\n', 'rename `b` to _'); +fixes('a local that is never read is renamed to _', head + '\tlet unused = rt.get_c();\n\treturn 0;\n}\n', 'unused', 0, + head + '\tlet _ = rt.get_c();\n\treturn 0;\n}\n', 'rename `unused` to _'); +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'); +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'); +none('a multiply bound to a division on its left is left alone', head + '\tlet y = rt.get_c();\n\trt.put_n(y / 2 * 16);\n\treturn 0;\n}\n', '* 16', 0); +fixes('a small heap buffer becomes a stack buffer', head + '\tlet p = rt.new(8);\n\tp[0] = rt.get_c();\n\trt.put_n(p[0]);\n\treturn 0;\n}\n', 'p = rt', 0, + head + '\tlet p[8];\n\tp[0] = rt.get_c();\n\trt.put_n(p[0]);\n\treturn 0;\n}\n', 'use a stack buffer'); +fixes('a runtime name without its module is imported and qualified', 'fn 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', 'include rt and use rt.put_c'); +fixes('a compiler name is imported from run', 'include rt;\nfn main() {\n\trt.put_n(evals("return 1;"));\n\treturn 0;\n}\n', 'evals', 0, + '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'); +console.log(ok + ' hint fix checks passed, ' + bad + ' failed'); +process.exit(bad ? 1 : 0); diff --git a/tests/drivers/toolchain_lsp_contract_test.js b/tests/drivers/toolchain_lsp_contract_test.js index 2872282..59d5a4c 100644 --- a/tests/drivers/toolchain_lsp_contract_test.js +++ b/tests/drivers/toolchain_lsp_contract_test.js @@ -284,12 +284,12 @@ test('inference', 'typed fields do not constrain the containing parameter to a s test('quickfix', 'an entire-line request finds the compiler error inside the range', () => { const source = 'fn main() { rt.put_n(7); }'; const r = run(source, [{ needle: 'fn', method: 'textDocument/codeAction', params: { range: { start: { line: 0, character: 0 }, end: { line: 0, character: source.length } }, context: { diagnostics: [{ range: { start: { line: 99, character: 99 }, end: { line: 99, character: 99 } }, message: 'stale' }] } } }]); - assert.ok(r.result(2)?.some(a => a.title === 'include rt;'), JSON.stringify(r.result(2))); + assert.ok(r.result(2)?.some(a => a.title === 'include rt'), JSON.stringify(r.result(2))); }); test('quickfix', 'an owner-name cursor can import a qualified module', () => { const source = 'fn main() { rt.put_n(7); }', start = { line: 0, character: source.indexOf('rt.') }; const r = run(source, [{ needle: 'rt.', method: 'textDocument/codeAction', params: { range: { start, end: start }, context: { diagnostics: [] } } }]); - assert.ok(r.result(2)?.some(a => a.title === 'include rt;')); + assert.ok(r.result(2)?.some(a => a.title === 'include rt')); }); test('modulehover', 'source library overview is available without a built-in module entry', () => { const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-module-hover-')); diff --git a/tests/test.sh b/tests/test.sh index 811a09a..449da87 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -550,6 +550,9 @@ s_lsp() { grep -v '^ok' $T/contracts-$group.out chk $r 0 "lsp: $group contracts" done + node tests/drivers/hint_fixes_test.js $lsp > $T/hintfixes.out 2>&1; r=$? + grep -v '^ok' $T/hintfixes.out + chk $r 0 "lsp: hint and import quick fixes keep the program's meaning" node tests/drivers/codeactions_client_test.js > $T/codeactions-client.out 2>&1; r=$? grep -v '^ok' $T/codeactions-client.out chk $r 0 "lsp: versioned import quick-fix edits" |
