diff options
| author | 2026-10-14 15:52:01 +0330 | |
|---|---|---|
| committer | 2026-10-14 15:52:01 +0330 | |
| commit | 7c54e48bca711a6596be4bb17fcaa4330b9928e3 (patch) | |
| tree | fd426755976847e368fe298db1c7c757f58799b9 /src | |
| parent | todo: record each agent's state at the end of the session and the merge order (diff) | |
line tables and backtraces: port agent/line-tables onto main, op 56 on the x86-64 jit, docs, measurements, tests; default genome reservation 0x68000
Diffstat (limited to 'src')
| -rw-r--r-- | src/compiler/base.zc | 89 | ||||
| -rw-r--r-- | src/compiler/builtins.zc | 11 | ||||
| -rw-r--r-- | src/compiler/decl.zc | 13 | ||||
| -rw-r--r-- | src/compiler/expr.zc | 4 | ||||
| -rw-r--r-- | src/compiler/image.zc | 23 | ||||
| -rw-r--r-- | src/compiler/lex.zc | 29 | ||||
| -rw-r--r-- | src/compiler/setup.zc | 1 | ||||
| -rw-r--r-- | src/compiler/stmt.zc | 2 | ||||
| -rw-r--r-- | src/core/core.c | 5 | ||||
| -rw-r--r-- | src/js/tinyvm.ts | 2 | ||||
| -rw-r--r-- | src/lsp/server.zc | 3 | ||||
| -rw-r--r-- | src/micro/micro.c | 3 | ||||
| -rw-r--r-- | src/runtime/rt.zc | 119 | ||||
| -rwxr-xr-x | src/tools/automap.sh | 14 | ||||
| -rw-r--r-- | src/tools/build.zc | 3 | ||||
| -rw-r--r-- | src/tools/genmap.c | 2 | ||||
| -rwxr-xr-x | src/tools/parity.sh | 4 | ||||
| -rw-r--r-- | src/tools/zc0.c | 65 |
18 files changed, 365 insertions, 27 deletions
diff --git a/src/compiler/base.zc b/src/compiler/base.zc index d83dc9d..2f802b9 100644 --- a/src/compiler/base.zc +++ b/src/compiler/base.zc @@ -224,6 +224,16 @@ char[1100] tbuf_initial; char* tbuf = tbuf_initial; int tbufcap = 1100; // 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 +// line tables: lines on; (the default) records, for each function compiled, its entry, its end and the +// source line of each statement, as a line block among the code that rt.line_blocks chains (see +// rt.zc). lt_buf holds the pairs of the functions being compiled, innermost last, each as a frame +// [outer frame, entry, name, file, 0, offset << 32 | line ...]; lt_base is the innermost frame. +// lt_head is the newest block of an image being built, whose own rt.line_blocks it will become. +int lines_on = 1; +// //:lines NAME (see mark_line): the markers the lexer has met, [lines before the file, NAME] each, and the +// one in force, which a top-level item applies when it is reached +word lt_marks; int lt_nmarks, lt_markcap, lt_next_mark; char* lt_mark_file; int lt_mark_base; +word lt_buf, lt_head, lt_file_at; int lt_top, lt_cap, lt_base = 0 - 1, lt_file_img; char* lt_file; 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; int pk_tk, pk_tv, pq_n; word[4] pq_tk; word[4] pq_tv; word[4] pq_gl; word[4] pq_ln; // up to 4 tokens of lookahead @@ -299,7 +309,7 @@ word tdstk; int ntd, tdcap; word readonly_types; int dty, dtag; // result of dtype() int s_typedef; -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_lt, s_gt, s_shr, s_le, s_ge, s_eqeq, s_ne, s_plus, s_struct, s_type, s_typecheck, s_docs, s_lines, 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 ---- @@ -1222,6 +1232,83 @@ int hole() { lit(rt.MW - 1); return a; } +// ---- line tables ---- + +// make room for n more words of pairs. +int lt_room(int n) { + int cap = lt_cap; + if (lt_top + n <= cap) return 0; + if (cap < 256) cap = 256; + while (lt_top + n > cap) cap = cap * 2; + lt_buf = chk_resize(lt_buf, lt_top, cap); lt_cap = cap; + return 0; +} +// start recording the function whose code begins at entry, named name, with its header on line. +// returns the frame to give lt_end, or -1 when no table is being made (lines off, a dry run, +// check mode). +int lt_begin(int entry, char* name, int line) { + int b; + if (!lines_on || dry || cmode) return 0 - 1; + lt_room(8); + b = lt_top; + lt_buf[b] = lt_base; lt_buf[b + 1] = entry; lt_buf[b + 2] = name; lt_buf[b + 3] = curinc; lt_buf[b + 4] = 0; + if (!curinc) { lt_buf[b + 3] = lt_mark_file; lt_buf[b + 4] = lt_mark_base; } // the main source may be marked as a file of its own + lt_top = b + 5; lt_base = b; + lt_buf[lt_top] = line - lt_buf[b + 4]; lt_top++; // the code before the first statement belongs to the header's line + return b; +} +// a statement begins at the current token, and its code at here. +int lt_stmt() { + int off, n; + if (lt_base < 0 || dry) return 0; + off = here - lt_buf[lt_base + 1]; + if (off < 0) return 0; + lt_room(2); + if (lt_top > lt_base + 5 && (lt_buf[lt_top - 1] >> 32) == off) lt_top--; // same place: the later statement is the one + n = tline - lt_buf[lt_base + 4]; + if (lt_top > lt_base + 5 && (lt_buf[lt_top - 1] & 4294967295) == n) return 0; + lt_buf[lt_top] = off * 4294967296 + n; lt_top++; + return 0; +} +// the file name in a block: the include's path, or <stdin> for the main source. one copy of the +// bytes is shared by the blocks that follow it. +int lt_file_data(char* f) { + if (!f) f = "<stdin>"; + if (lt_file_at && lt_file_img == image_active && rt.eq_w(lt_file, f)) return lt_file_at; + lt_file_at = bstrdata(f); lt_file_img = image_active; + lt_file = chk_resize(0, 0, rt.len_w(f) + 1); rt.copy_w(lt_file, f, rt.len_w(f) + 1); + return lt_file_at; +} +// finish the function begun at frame b, whose code ends at end: put its line block among the code +// and chain it. a block is [next, entry, end, name, file, count, pairs...]. +int lt_end(int b, int end) { + int entry, count, i = 0, name, file, blk, need; word head; char* nm; char* fl; + if (b < 0) return 0; + entry = lt_buf[b + 1]; nm = lt_buf[b + 2]; fl = lt_buf[b + 3]; count = lt_top - (b + 5); + if (!nm) nm = "<fn>"; + if (!fl) fl = "<stdin>"; + if (end >= entry && !dry) { + need = 8 + count + ((rt.len_w(nm) + 7) >> 3) + 2 + ((rt.len_w(fl) + 7) >> 3) + 2; + reserve_code(need); + name = bstrdata(nm); file = lt_file_data(fl); + if (image_active) head = lt_head; else head = rt.line_blocks; + blk = here; + emit(head); emit(entry); emit(end); emit(name); emit(file); emit(count); + while (i < count) { emit(lt_buf[b + 5 + i]); i++; } + if (image_active) lt_head = blk; else rt.line_blocks = blk; + } + lt_top = b; lt_base = lt_buf[b]; + return 0; +} +// at the top level, a marker that comes before the item takes effect: lines are on, in its file. +int lt_apply_marks() { + if (incdepth || ginst) return 0; + while (lt_next_mark < lt_nmarks && lt_marks[lt_next_mark * 2] < tline) { + lt_mark_base = lt_marks[lt_next_mark * 2]; lt_mark_file = lt_marks[lt_next_mark * 2 + 1]; + lines_on = 1; lt_next_mark++; + } + return 0; +} // forward references. calling a function that is not defined yet creates a // symbol with F_DEF clear whose value is the place of its first use (for the // "never defined" error). each call site emits a placeholder literal and adds diff --git a/src/compiler/builtins.zc b/src/compiler/builtins.zc index 53d8597..5d7bc9c 100644 --- a/src/compiler/builtins.zc +++ b/src/compiler/builtins.zc @@ -507,6 +507,15 @@ int kw_docs() { next(); expect(s_semi); return 0; } +// lines on; records line tables for the functions that follow, lines off; stops (see lt_begin) +int kw_lines() { + next(); + if (is_id(s_on)) { + lines_on = 1; + } else if (is_id(s_off)) lines_on = 0; else die("lines on or lines off"); + next(); expect(s_semi); + return 0; +} // hints on; / hints small; / hints off; from here to the end of the document (or the next such line) int kw_hints() { int level = 2, at = toff; @@ -1167,7 +1176,7 @@ int types_init() { s_le = intern("<="); s_ge = intern(">="); s_eqeq = intern("=="); s_ne = intern("!="); s_plus = intern("+"); - s_struct = intern("struct"); s_type = intern("type"); s_typedef = intern("typedef"); s_typecheck = intern("typecheck"); s_docs = intern("docs"); s_std = intern("std"); s_icon = intern("icon"); s_small = intern("small"); + s_struct = intern("struct"); s_type = intern("type"); s_typedef = intern("typedef"); s_typecheck = intern("typecheck"); s_docs = intern("docs"); s_lines = intern("lines"); s_std = intern("std"); s_icon = intern("icon"); s_small = intern("small"); s_on = intern("on"); s_off = intern("off"); s_new = intern("rt.new"); diff --git a/src/compiler/decl.zc b/src/compiler/decl.zc index 19b1811..cc84603 100644 --- a/src/compiler/decl.zc +++ b/src/compiler/decl.zc @@ -280,7 +280,7 @@ int body() { // into stype, checked against any earlier prototype or prescan result, and // where the o3 region for an o3 function is handed to the core) int fndef(int chk, int rty, int ret, int n) { - int s, np = 0, tags = 0, t, sig, i; char*[24] pn; word[24] pt; word[24] pty; int typed = 0, ft, explicit_rty = !!rty, o3f = o3nxt, lo, hi, pk; + int s, np = 0, tags = 0, t, sig, i; char*[24] pn; word[24] pt; word[24] pty; int typed = 0, ft, explicit_rty = !!rty, o3f = o3nxt, lo, hi, pk, lt, lend, lfl = tline; o3nxt = 0; if (rty || ntp) typed = 1; if (!dnm) dnm = n; @@ -365,6 +365,7 @@ int fndef(int chk, int rty, int ret, int n) { sflag[s] = sflag[s] | F_DEF; fixall(sfix[s], here); sfix[s] = 0; fn_begin(chk, ret); + lt = lt_begin(sval[s], sname[s], lfl); finsym = s; finfer = !ret; effect_current = effect_record(s); rt.fill_w(effect_current, 0, 50); effect_current[0] = 1; if (!chk || stubgen) effect_current[0] = 2; effect_current[1] = np; i = 0; while (i < np) { if (pt[i] == 2) { effect_current[2 + i] = EF_CONSUME; effect_current[26 + i] = EF_CONSUME; } i++; } @@ -397,6 +398,7 @@ int fndef(int chk, int rty, int ret, int n) { } hi = here; epilogue(); + lend = here; if (o3f) { o3on = 0; o3fs = fslots; @@ -405,6 +407,7 @@ int fndef(int chk, int rty, int ret, int n) { o3region(lo, hi, o3sl, o3rg, no3sl); } o3flush(); + lt_end(lt, lend); ntp = 0; ifcur = 0; fn_remember(s, n); return s; @@ -567,7 +570,7 @@ int topsync() { if (is_op(s_lb)) depth++; else if (is_op(s_rb)) { depth--; if (depth < 0) depth = 0; } else if (!depth && tk == T_ID && (tv == s_fn || tv == s_var || tv == s_const || tv == s_struct || tv == s_type || tv == s_typedef - || tv == s_enhance || tv == s_typecheck || tv == s_docs || tv == s_unchecked || tv == s_immediate || tv == s_o3 + || tv == s_enhance || tv == s_typecheck || tv == s_docs || tv == s_lines || tv == s_unchecked || tv == s_immediate || tv == s_o3 || tv == s_memdebug || tv == s_enum || tv == s_include)) break; next(); } @@ -628,6 +631,7 @@ int fndone(int s, int imm) { // ( or < means function, otherwise a global. int topitem0() { int r, i; + lt_apply_marks(); dsn = 0; dnm = 0; if (docs_on && dn && dline == tline && !ginst) { if (dn > docsnapcap) { docsnapcap = dn; dsnap = chk_resize(dsnap, 0, docsnapcap); } @@ -679,6 +683,7 @@ int topitem1() { if (typedef_ahead()) return kw_typedef(); if (is_id(s_typecheck)) return kw_typecheck(); if (is_id(s_docs)) return kw_docs(); + if (is_id(s_lines)) { peek(); if (pk_tk == T_ID && (pk_tv == s_on || pk_tv == s_off)) return kw_lines(); } if (is_id(s_memdebug)) return kw_memdebug(); if (is_id(s_enum)) return kw_enum(); if (is_id(s_include)) return kw_include(); @@ -1334,7 +1339,7 @@ int unit_end(int from) { // way is emitted behind a jump when another function is being compiled, so // compile() works inside a parse word.) int compile_list_context(int q, int top, char* module, word imports) { - int hop = -1, entry = 0, from = nsym; + int hop = -1, entry = 0, from = nsym, lt = 0 - 1; ctx_push(); ifcur = 0; ifcall = -1; effect_floor = from; module_name = module; module_imports = imports; if (infn) { hop = hole(); op(O_JMP); } if (top) prescan(1, q); @@ -1344,12 +1349,14 @@ int compile_list_context(int q, int top, char* module, word imports) { else { entry = here; fn_begin(1, 0); + lt = lt_begin(entry, "<eval>", tline); prologue(); borrow_prologue(); borrow_begin(); while (tk != T_EOF) stmt(); defer_run(1, 1); unused_check(lbase); if (fchk && live) exit_vars(1, 1); epilogue(); + lt_end(lt, here); } src_pop(); unit_end(from); diff --git a/src/compiler/expr.zc b/src/compiler/expr.zc index 40de6ef..f41109e 100644 --- a/src/compiler/expr.zc +++ b/src/compiler/expr.zc @@ -647,7 +647,7 @@ word closure_layout(int closure_index, int count) { // variable in. ctx_push / ctx_pop save and restore the enclosing function's // compiler state around the body. int closure() { - int np = 0; char*[20] pn; word[20] pty; int hop, entry, i, t, envl, ncap, c, result, declared = 0, tags = 0, tag; word[20] ptags; + int np = 0; char*[20] pn; word[20] pty; int hop, entry, i, t, envl, ncap, c, result, declared = 0, tags = 0, tag, lt, lfl = tline; word[20] ptags; word expected = closure_expected; closure_expected = 0; reserve_closures(nclo + 1); if (o3on) serr("closures can't be made inside an o3 region"); @@ -679,6 +679,7 @@ int closure() { hop = hole(); op(O_JMP); entry = here; fn_begin(1, 0); + lt = lt_begin(entry, "<closure>", lfl); if (declared) fty = declared; prologue(); borrow_prologue(); i = 0; @@ -699,6 +700,7 @@ int closure() { if (result <= 0 || live) result = tword; if (declared) result = declared; epilogue(); + lt_end(lt, here); nclo--; c = nclo; ncap = clo_n[c]; diff --git a/src/compiler/image.zc b/src/compiler/image.zc index dfe5f77..45cd928 100644 --- a/src/compiler/image.zc +++ b/src/compiler/image.zc @@ -62,25 +62,48 @@ int blob_load(word p, int hide) { } return 0; } +// the name of the function whose code holds address a, for the frames of a backtrace that have +// no line table (rt.frame_namer is set to this when the compiler starts). the nearest function +// at or below a, or 0. +char* frame_symbol(int a) { + int i = 0, best = 0 - 1; + while (i < nsym) { + if (skind[i] == K_FN && (sflag[i] & F_DEF) && sval[i] <= a && sval[i] > 0 && (best < 0 || sval[i] > sval[best])) best = i; + i++; + } + if (best < 0) return 0; + return sname[best]; +} +// an image built with line tables gets the head of its chain in its own rt.line_blocks, the +// global the runtime reads, as that global's initial value. +int lt_image_head() { + int s = lookup(intern("rt.line_blocks")); + if (lt_head && s >= 0 && skind[s] == K_GVAR) *(sval[s] + hdelta) = lt_head; + return 0; +} // compile the rest of stdin for org into a growing output buffer. the image's // addresses remain logical addresses at org while its temporary storage grows. // image_words and image_size expose the finished image; initial is its first // buffer reservation. the matching host layout must still fit the finished image. int build_image_grow(int org, int initial) { int sh = here, sd = hdelta, sl = hlimit, sub = ubase, svl = vislo, sns = nsym, sen = nenh, n; + word saved_head = lt_head; char* saved_file = lt_file; word saved_file_at = lt_file_at; char* saved_mark = lt_mark_file; int saved_base = lt_mark_base, saved_nm = lt_next_mark, saved_nmarks = lt_nmarks; word saved_buffer = image_buffer; int saved_active = image_active, saved_org = image_org, saved_capacity = image_capacity; if (org < 0 || initial < 7 || initial > 0x7fffffff - org) fatal("bad image reservation"); image_buffer = chk_resize(0, 0, initial); image_active = 1; image_org = org; image_capacity = initial; ubase = nsym; vislo = nsym; nenh = 0; here = org; hdelta = image_buffer - org; hlimit = org + initial; + lt_head = 0; lt_file_at = 0; lt_mark_file = 0; lt_mark_base = 0; lt_next_mark = 0; lt_nmarks = 0; lit(rt.FTOP); lit(rt.ZFP); op(O_ST); fnref(fwdfn(s_start)); op(O_JMP); emit(0); emit(0); unit_stdin(); + lt_image_head(); n = blob_emit(ubase); image_buffer[5] = n; image_buffer[6] = here; n = here - org; image_words = image_buffer; image_size = n; popsyms(sns); ubase = sub; vislo = svl; here = sh; hdelta = sd; hlimit = sl; nenh = sen; + lt_head = saved_head; lt_file = saved_file; lt_file_at = saved_file_at; lt_mark_file = saved_mark; lt_mark_base = saved_base; lt_next_mark = saved_nm; lt_nmarks = saved_nmarks; image_buffer = saved_buffer; image_active = saved_active; image_org = saved_org; image_capacity = saved_capacity; return n; } diff --git a/src/compiler/lex.zc b/src/compiler/lex.zc index 0b509e4..0348cae 100644 --- a/src/compiler/lex.zc +++ b/src/compiler/lex.zc @@ -366,10 +366,31 @@ int fieldscan(int n) { // a // comment, with ch on its second slash. one that starts a line joins the // doc comment being collected, if the line above was part of it, or starts a // new one; a comment after code on the same line is not documentation. +// a comment //:lines NAME (ch is its colon) says that what follows is the file NAME, with its lines counted +// from the next one, and turns line tables on from the next top-level item. src/tools/automap.sh writes one +// before a program it joins to the runtime. the rest of the line is read here and the marker kept: the +// source is read once to find the signatures and again to compile, and it counts only when compiling reaches it. +int mark_line() { + char[300] b; int n = 0, i, base; word name; + while (ch >= 0 && ch != 10) { if (n < 298) { b[n] = ch; n++; } nextch(); } + while (n && (b[n - 1] == 13 || b[n - 1] == 32)) n--; + b[n] = 0; + if (n > 7 && b[1] == 'l' && b[2] == 'i' && b[3] == 'n' && b[4] == 'e' && b[5] == 's' && b[6] == 32) { + base = line; + if (ch == 10) base = line - 1; + if (!lt_nmarks || base > lt_marks[lt_nmarks * 2 - 2]) { + if (lt_nmarks * 2 + 2 > lt_markcap) { lt_marks = chk_resize(lt_marks, lt_nmarks * 2, lt_markcap * 2 + 16); lt_markcap = lt_markcap * 2 + 16; } + name = chk_resize(0, 0, n); i = 7; + while (i <= n) { name[i - 7] = b[i]; i++; } + lt_marks[lt_nmarks * 2] = base; lt_marks[lt_nmarks * 2 + 1] = name; lt_nmarks++; + } + } + return 0; +} int doccomment(int nl) { int cl = line, start; nextch(); - if (ch == ':') { while (ch >= 0 && ch != 10) nextch(); dn = 0; dsect = 1; dlast = cl; docpending = 1; return 0; } + if (ch == ':') { mark_line(); dn = 0; dsect = 1; dlast = cl; docpending = 1; return 0; } if (!nl) { while (ch >= 0 && ch != 10) nextch(); return 0; } if (dlast != cl - 1) { dn = 0; dsect = 0; } if (ch == 32) nextch(); @@ -451,7 +472,11 @@ int lex1() { if (ch != '/') break; nextch(); if (ch == '/') { - if (docs_on) doccomment(nl); else while (ch >= 0 && ch != 10) nextch(); + if (docs_on) doccomment(nl); + else { + nextch(); + if (ch == ':') mark_line(); else while (ch >= 0 && ch != 10) nextch(); + } tglue = 0; continue; } if (ch == '*') { diff --git a/src/compiler/setup.zc b/src/compiler/setup.zc index 1d8ce6f..9ed938b 100644 --- a/src/compiler/setup.zc +++ b/src/compiler/setup.zc @@ -168,6 +168,7 @@ int start() { blob_load(rt.GORG[5], 0); zc_tables(); types_init(); + rt.frame_namer = sval[lookup(intern("run.frame_symbol"))]; here = 0; hdelta = 0; hlimit = rt.CODE_END; ptop = 0; ubase = nsym; vislo = 0; // a genome that carries the language server runs it instead of compiling stdin diff --git a/src/compiler/stmt.zc b/src/compiler/stmt.zc index 21d5801..4dafe4f 100644 --- a/src/compiler/stmt.zc +++ b/src/compiler/stmt.zc @@ -13,7 +13,7 @@ int resync() { } // compile one statement. int stmt() { - word before = borrow_slots; int result; emit_statement_hooks(); result = stmt_inner(); borrow_cleanup_since(before); return result; + word before = borrow_slots; int result; lt_stmt(); emit_statement_hooks(); result = stmt_inner(); borrow_cleanup_since(before); return result; } int stmt_inner() { int s, c; diff --git a/src/core/core.c b/src/core/core.c index b571981..2a0c467 100644 --- a/src/core/core.c +++ b/src/core/core.c @@ -293,6 +293,7 @@ static Cell hostcall(Cell op, Cell a, Cell b, Cell c) #else return -1; #endif + case 56: return cb_r; /* the return stack pointer as of the gate store: the word after the top return address */ case 1: if (zcstr(a, path, 1024) < 0) return -1; if (b == 1) return open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644); @@ -3608,7 +3609,9 @@ static void syncicache(JW *a, JW *b) #endif } -/* store hook for words that are covered by a translation */ +/* store hook for words that are covered by a translation. both jits reach it with s and r + published in S[1] and S[2] (the x86-64 jit's emit_store calls publish_stacks first), so a + gate store sees the exact stacks: cb_r is what op 56 returns to rt.trace() */ static int hook(Cell a, Cell v) { int j, hit = 0; diff --git a/src/js/tinyvm.ts b/src/js/tinyvm.ts index f779487..f52d4af 100644 --- a/src/js/tinyvm.ts +++ b/src/js/tinyvm.ts @@ -466,6 +466,8 @@ export class TinyVM { } return -1n; } + case 56: // the return stack pointer: the word after the top return address + return BigInt(this.rr); case 42: // poll: input is waiting return this.inputPos < this.inputBuffer.length ? 1n : 0n; case 41: { // shortest float text into a word buffer diff --git a/src/lsp/server.zc b/src/lsp/server.zc index 372724f..c3f9f4f 100644 --- a/src/lsp/server.zc +++ b/src/lsp/server.zc @@ -1704,6 +1704,7 @@ char* kwtext() { if (hname_is("typecheck")) return "typecheck on; / typecheck off;~turns on type checks for the code that follows, enums included."; if (hname_is("hints")) return "hints on; / hints small; / hints off;~controls subsequent hint diagnostics: informational ranges by default, compact markers with small, or no hints with off."; if (hname_is("docs")) return "docs on; / docs off;~keeps the doc comments above declarations in the image, so the language server can show them on hover."; + if (hname_is("lines")) return "lines on; / lines off;~records, for the functions that follow, the source line of each statement, so a run-time fault prints a backtrace (at file:line in function) and rt.trace() can print the call stack. on by default for a program. the language server never builds the tables."; if (hname_is("micro")) return "enhance name(args) micro \"recipe\";~the body of an enhancement, written as micro-ops on registers r0 to r15. the inputs arrive in r0 and up, and the outputs leave from r0."; if (hname_is("vm")) return "enhance name(args) vm { ... };~the body of an enhancement, written as stack machine asm. it becomes one opcode when the core lifts it."; if (hname_is("enhance")) return "enhance name(args) micro \"...\" or vm { ... };~installs a custom opcode at compile time, with up to 8 inputs and 8 outputs. an enhancement with one output can be called like a function."; @@ -2575,7 +2576,7 @@ int hover_source_decl(int i) { if (!init && prev >= 0 && hs_word(i, prev) && prev > head) { candidate = prev; if (first < 0) first = prev; // directives and type aliases are not variable declarations. - if (!fm_is(i, head, "include") && !fm_is(i, head, "docs") && !fm_is(i, head, "hints") && !fm_is(i, head, "typecheck") && !fm_is(i, head, "type") && !fm_is(i, head, "enhance")) matched = hs_match(i, candidate); + if (!fm_is(i, head, "include") && !fm_is(i, head, "docs") && !fm_is(i, head, "lines") && !fm_is(i, head, "hints") && !fm_is(i, head, "typecheck") && !fm_is(i, head, "type") && !fm_is(i, head, "enhance")) matched = hs_match(i, candidate); } if (hs_equal(i, t)) init = 1; else { diff --git a/src/micro/micro.c b/src/micro/micro.c index f598c80..23994dd 100644 --- a/src/micro/micro.c +++ b/src/micro/micro.c @@ -530,6 +530,9 @@ static int run_from(int p, int s, int r) Cell dst = m[a + 2], cap = m[a + 3]; if (dst < 0 || cap <= n || (UCell)dst > mw || (UCell)cap > mw - (UCell)dst) result = -1; else { for (i = 0; i < n; i++) m[dst + i] = (unsigned char)text[i]; m[dst + n] = 0; result = n; } + } else if (b == 56) { + /* the return stack pointer: the word after the top return address */ + result = r; } else if (b == 42) { /* poll: a streaming port suspends once, so its host can deliver input that is waiting */ result = micro_syscall(m, b, 0, 0, 0); diff --git a/src/runtime/rt.zc b/src/runtime/rt.zc index ef5a5c6..4e52cb5 100644 --- a/src/runtime/rt.zc +++ b/src/runtime/rt.zc @@ -34,20 +34,20 @@ include run; // across targets and image sizes. // @map-rt -const MW = 0x1D5000; +const MW = 0x1DD000; const CODE_END = 0x80000; -const HEAP_LO = 0x1D5000; -const HEAP_HI = 0x235000; +const HEAP_LO = 0x1DD000; +const HEAP_HI = 0x23D000; const HEAP_GROW = 0x1; const ZFP = 0x90000; const GORG = 0x114100; -const FBOT = 0x174100; -const FTOP = 0x1B4100; -const RS0 = 0x1B4100; -const MARK0 = 0x1C4100; +const FBOT = 0x17C100; +const FTOP = 0x1BC100; +const RS0 = 0x1BC100; +const MARK0 = 0x1CC100; const MARKN = 0x8000; -const DSLO = 0x1CC100; -const DSHI = 0x1D4FF0; +const DSLO = 0x1D4100; +const DSHI = 0x1DCFF0; const MAGIC = 0x2B7E1516; // @end-map @@ -670,6 +670,7 @@ int scan_ptr(int p, int n) { // pointer (so this can run at all) and stops int frame_overflow() { put_s("zy: frame stack overflow\n"); + trace_at(1); halt(); return 0; } @@ -714,6 +715,7 @@ word frame_leave() { int oom() { outmode = 0; outgrow = 0; put_s("zc: out of heap memory\n"); + trace_at(1); halt(); return 0; } @@ -825,6 +827,7 @@ unchecked int free(.d p) { if (isobj(p) && (p[-4] & F_SHARE)) return 0; if ((dbg & 1) && quar(p)) { put_s("zy: double free from line "); put_n(site_line_of(p)); put_c(10); + trace_at(1); return 0; } if (isobj(p)) { @@ -840,6 +843,7 @@ unchecked int recycle(.d p) { word b; int n, i = 0; word w; if ((dbg & 1) && quar(p)) { put_s("zy: double free from line "); put_n(site_line_of(p)); put_c(10); + trace_at(1); return 0; } if (!isobj(p) || (p[-4] & F_SHARE)) return 0; @@ -986,6 +990,99 @@ unchecked int dgrow(word h, int need) { free(old); return 0; } +// ---- line tables and backtraces ---- + +// with lines on (the default for a program) the compiler records, for every +// function it compiles, a line block among the code: [next, entry, end, name, +// file, count, then count words of offset << 32 | line]. name and file are +// packed byte strings (length word, then the bytes). each word after the +// count says that the code from entry + offset up to the next word's offset +// came from that source line. blocks are chained through line_blocks, newest +// first. a function nested in another (a closure) has its own block and the +// innermost block holding an address is the one that names it. the compiler +// fills line_blocks when it compiles in the same memory as the program; the +// compiler of an image writes the head into the image's own line_blocks. +// frame_namer, when the compiler is resident, is a function that returns the +// name (a zero-terminated character string) of the function holding an +// address, for frames in code without lines. +word line_blocks, frame_namer; +// op 56 of the syscall gate: the return stack pointer, the word after the top return address. +const int SYS_RSP = 56; +// a trace shows this many of the innermost frames, then skips to the last few +const int TRACE_HEAD = 24, TRACE_TAIL = 4; +// the line block of the innermost function whose code holds address a, or 0 +word line_block_of(int a) { + word b = line_blocks, best = 0; + while (b) { + if (b[1] <= a && a < b[2] && (!best || b[1] > best[1])) best = b; + b = b[0]; + } + return best; +} +// the source line of address a within its line block b: the last recorded line at or before it +int line_of(word b, int a) { + int i = 0, off = a - b[1], n = b[5], line = 0; word w; + while (i < n) { + w = b[6 + i]; + if ((w >> 32) > off) break; + line = w & 0xFFFFFFFF; + i++; + } + return line; +} +// write the hexadecimal form of a number +int put_hex(int n) { + char[20] b; int i = 0, d; + put_s("0x"); + if (!n) { put_c('0'); return 0; } + while (n && i < 16) { d = n & 15; if (d < 10) b[i++] = '0' + d; else b[i++] = 'a' + d - 10; n = n >> 4; } + while (i) put_c(b[--i]); + return 0; +} +// write the line for one frame, given the return address a that is on the +// return stack: its function, file and line when the compiler recorded them, +// else the address and the nearest name. code of the runtime and the compiler +// that has no lines is left out, as are words that are not return addresses. +// returns 1 when a line was written. +int trace_frame(int a) { + word b; char* name = 0; + if (a <= 0) return 0; + b = line_block_of(a - 1); + if (b) { + put_s(" at "); put_bs(b[4]); put_c(':'); put_n(line_of(b, a - 1)); put_s(" in "); put_bs(b[3]); put_c(10); + return 1; + } + if (a >= GORG && a < GORG[6]) return 0; + if (!callable_code(a)) return 0; + if (frame_namer) name = frame_namer(a); + put_s(" at "); put_hex(a); + if (name) { put_s(" in "); put_s(name); } + put_c(10); + return 1; +} +// print the call stack, innermost call first, one line per frame, from the +// return stack. skip is how many words at the top of the stack belong to the +// runtime's own calls and are left out: a function that calls trace_at passes 1 +// to leave out the return into itself. this reads the pointer with the gate +// store inline, since a call to syscall would add a frame of its own. +int trace_at(int skip) { + word g = ZFP + 2; int r, lo, shown = 0, left; word rs; + g[1] = 0; g[2] = 0; g[3] = 0; g[0] = SYS_RSP; + r = g[0]; + lo = return_low; + if (r <= lo || r - lo > 0x7FFFFFF) return 0; + r = r - skip; + while (r > lo && shown < TRACE_HEAD) { r--; rs = r; if (trace_frame(rs[0])) shown++; } + left = r - lo; + if (left > TRACE_TAIL + 8) { + put_s(" ... "); put_n(left - TRACE_TAIL); put_s(" more frames\n"); + r = lo + TRACE_TAIL; + } + while (r > lo) { r--; rs = r; trace_frame(rs[0]); } + return 0; +} +// print the current call stack without stopping +int trace() { trace_at(1); return 0; } // the out-of-range report for T[?] indexing (dat, dpop) int oob(int i, int n) { put_s("zy: index "); @@ -993,6 +1090,7 @@ int oob(int i, int n) { put_s(" out of range (length "); put_n(n); put_s(")\n"); + trace_at(1); halt(); return 0; } @@ -1276,9 +1374,10 @@ int checkptr(int w) { if (!(dbg & 1) || !w) return 0; if (w < HEAP_LO || w >= hstop) return 0; p = obj_base(w); - if (!p) { put_s("zy: bad heap pointer\n"); halt(); return 0; } + if (!p) { put_s("zy: bad heap pointer\n"); trace_at(1); halt(); return 0; } if (!(p[-4] & 1)) { put_s("zy: use after free from line "); put_n(site_line_of(p)); put_c(10); + trace_at(1); halt(); } return 0; diff --git a/src/tools/automap.sh b/src/tools/automap.sh index ff559ac..b4aceb7 100755 --- a/src/tools/automap.sh +++ b/src/tools/automap.sh @@ -35,8 +35,18 @@ lay() { # $1 = genome slot, or empty for the preset's own $CC -O1 -w -fwrapv src/tools/zc0.c -I"$O/map" -Isrc/core -o "$ZC0" $CC -O1 -w -fwrapv src/tools/mkgenome.c -I"$O/map" -o "$O/.mkgenome" } +# the files after the map's own. a run-only image gets line tables for its program (so a fault +# prints a backtrace): zc0 reads the comment //:lines NAME as "from here on, file NAME, count +# its lines from the next one". the files under src/ are the runtime's and have none. +feed() { + local f + for f in "$@"; do + if [ "$P" = run ]; then case "$f" in src/*) ;; *) printf '\n//:lines %s\n' "$f" ;; esac; fi + cat "$f" + done +} build() { - if [ -f "$O/map/compiler.zc" ]; then cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc" "$@"; else cat "$O/map/rt.zc" src/core/core.zc "$@"; fi | "$ZC0" > "$O/image.bin" + if [ -f "$O/map/compiler.zc" ]; then { cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc"; feed "$@"; }; else { cat "$O/map/rt.zc" src/core/core.zc; feed "$@"; }; fi | "$ZC0" > "$O/image.bin" "$O/.mkgenome" < "$O/image.bin" > "$O/genome.bin" } # with DOCS=1 the genome is made again with zc0 built for docs (ZC0DOCS), so the @@ -44,7 +54,7 @@ build() { # docofs(). the image is the same code either way; the docs are stored after it. docs() { $CC -O1 -w -fwrapv -DZC0DOCS src/tools/zc0.c -I"$O/map" -Isrc/core -o "$O/.zc0docs" - if [ -f "$O/map/compiler.zc" ]; then cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc" "$@"; else cat "$O/map/rt.zc" src/core/core.zc "$@"; fi | "$O/.zc0docs" > "$O/image.bin" + if [ -f "$O/map/compiler.zc" ]; then { cat "$O/map/rt.zc" src/core/core.zc "$O/map/compiler.zc"; feed "$@"; }; else { cat "$O/map/rt.zc" src/core/core.zc; feed "$@"; }; fi | "$O/.zc0docs" > "$O/image.bin" "$O/.mkgenome" < "$O/image.bin" > "$O/genome.bin" } len() { echo $(( $(wc -c < "$1") / 8 )); } diff --git a/src/tools/build.zc b/src/tools/build.zc index b0e3da0..b628a33 100644 --- a/src/tools/build.zc +++ b/src/tools/build.zc @@ -6,6 +6,9 @@ include rt, run; docs on; +// the genome has no line tables: its code is the runtime's and the compiler's, and a program +// built into an image turns them on for itself (lines on;) +lines off; // writes the compiled genome image to stdout as little-endian 64-bit words. int main() { int n = run.build_image_grow(rt.GORG, 4096), i = 0, w; diff --git a/src/tools/genmap.c b/src/tools/genmap.c index d515778..6d8db55 100644 --- a/src/tools/genmap.c +++ b/src/tools/genmap.c @@ -242,7 +242,7 @@ static void preset_default(Spec *s) s->compiler = 1; s->heap_top = 1; s->code_end = 0x80000; s->types = 65536; s->heap = 0x60000; s->total = 0x8000000; - s->genome = 0x60000; s->frame = 0x40000; s->ret = 0x10000; s->mark = 0x8000; + s->genome = 0x68000; s->frame = 0x40000; s->ret = 0x10000; s->mark = 0x8000; s->data = 0x7FF0; tables_big(s); } diff --git a/src/tools/parity.sh b/src/tools/parity.sh index 00245a8..2b9b870 100755 --- a/src/tools/parity.sh +++ b/src/tools/parity.sh @@ -63,7 +63,9 @@ KNOWN=tests/parity.known mkdir -p "$W" # what every image starts with, and how many lines that is (zc0 counts from the top) SRCS="src/runtime/rt.zc src/core/core.zc src/runtime/run.zc" -{ cat $SRCS; echo 'namespace;'; } > "$W/prefix.new" +# the marker turns zc0's line tables on for the program, named <stdin> as under zy, so faults +# and rt.trace() print the same backtrace on both sides (src/tools/automap.sh does the same) +{ cat $SRCS; echo 'namespace;'; echo '//:lines <stdin>'; } > "$W/prefix.new" if cmp -s "$W/prefix.new" "$W/prefix.zc"; then rm "$W/prefix.new"; else mv "$W/prefix.new" "$W/prefix.zc"; fi nprefix=$(wc -l < "$W/prefix.zc") # what the compiled image depends on besides the program diff --git a/src/tools/zc0.c b/src/tools/zc0.c index 040a4ea..5846e22 100644 --- a/src/tools/zc0.c +++ b/src/tools/zc0.c @@ -73,6 +73,13 @@ static int esc(void) switch (c) { case 'n': return 10; case 't': return 9; case 'r': return 13; case '0': return 0; case 'e': return 27; } return c; } +/* line tables, as the self-hosted compiler makes them with lines on (see rt.line_blocks in rt.zc). + they are off until a program says lines on; or a marker comment //:lines NAME comes by, which + src/tools/automap.sh puts in front of the program of a run-only image: it turns them on, names + the file, and counts the lines from the next one. */ +static int zl_on, zl_base, zl_head, zl_entry, zl_open, zl_fline, zl_file_at, zl_n, zl_cap; +static char zl_file[256] = "<stdin>", zl_file_done[256], zl_name[64]; +static int64_t *zl_pairs; /* doc comments, as the self-hosted compiler reads them with docs on: the // lines directly above a declaration, up to a blank line or any other text */ static char dbuf[2100], dsnap[2100], dnm[64]; static int dn, dlast, dline, dsn, docpending; @@ -82,7 +89,15 @@ static void doccomment(void) /* pos is on the first slash */ while (k >= 0 && (src[k] == ' ' || src[k] == '\t')) k--; bol = k < 0 || src[k] == '\n'; pos += 2; - if (src[pos] == ':') { dn = 0; dlast = cl; docpending = 0; while (src[pos] && src[pos] != '\n') pos++; return; } + if (src[pos] == ':') { + if (!strncmp(src + pos, ":lines ", 7)) { + int n = 0; + pos += 7; + while (src[pos] && src[pos] != '\n' && src[pos] != '\r' && n < 255) zl_file[n++] = src[pos++]; + zl_file[n] = 0; zl_on = 1; zl_base = cl; + } + dn = 0; dlast = cl; docpending = 0; while (src[pos] && src[pos] != '\n') pos++; return; + } if (!bol) { while (src[pos] && src[pos] != '\n') pos++; return; } if (dn && dlast != cl - 1) dn = 0; if (src[pos] == ' ') pos++; @@ -500,6 +515,42 @@ static int bstrwords(const unsigned char *b, long n) if ((n & 7) == 0) emit(0); return a; } +/* line tables. zl_begin starts recording the function whose code begins at entry, zl_stmt notes + that a statement starts at the current line, and zl_end puts the function's line block among + the code, as lt_end does in the resident compiler: [next, entry, end, name, file, count, then + count words of offset << 32 | line]. */ +static void zl_begin(int entry, const char *name, int fline) +{ + if (!zl_on) { zl_open = 0; return; } + zl_open = 1; zl_entry = entry; zl_n = 0; zl_fline = fline - zl_base; + strncpy(zl_name, name, 63); zl_name[63] = 0; + if (zl_n == zl_cap) { zl_cap = zl_cap ? zl_cap * 2 : 1024; zl_pairs = realloc(zl_pairs, (size_t)zl_cap * sizeof *zl_pairs); } + zl_pairs[zl_n++] = zl_fline; +} +static void zl_stmt(void) +{ + int off = here - zl_entry; + if (!zl_open) return; + if (zl_n == zl_cap) { zl_cap *= 2; zl_pairs = realloc(zl_pairs, (size_t)zl_cap * sizeof *zl_pairs); } + if (zl_n && (zl_pairs[zl_n - 1] >> 32) == off) zl_n--; + if (zl_n && (zl_pairs[zl_n - 1] & 0xffffffffLL) == line - zl_base) return; + zl_pairs[zl_n++] = (int64_t)off * 4294967296LL + (line - zl_base); +} +static void zl_end(void) +{ + int end = here, name, file, blk, i; + if (!zl_open) return; + zl_open = 0; + name = bstrwords((const unsigned char *)zl_name, (long)strlen(zl_name)); + if (!zl_file_at || strcmp(zl_file, zl_file_done)) { + zl_file_at = bstrwords((const unsigned char *)zl_file, (long)strlen(zl_file)); strcpy(zl_file_done, zl_file); + } + file = zl_file_at; + blk = here; + emit(zl_head); emit(zl_entry); emit(end); emit(name); emit(file); emit(zl_n); + for (i = 0; i < zl_n; i++) emit(zl_pairs[i]); + zl_head = blk; +} /* b"...": the words inline, jumped over, then the address pushed */ static void bstrlit(void) { @@ -691,6 +742,7 @@ static void patchbreaks(void) { int i; for (i = 0; i < nbrk[nloop]; i++) setlit( static void stmt(void) { int a, b, c, d; + zl_stmt(); if (accept(";")) return; if (is("{")) { block(); return; } { @@ -759,7 +811,7 @@ static void stmt(void) /* ---- top level ---- */ static void fndef(int ret, int rstr) /* rstr: the return type is bstr */ { - char *n = ident(); Sym *s; int np = 0, tags = 0, t, frame, i, helper, busy, done, check, node; + char *n = ident(); int zfl = line; Sym *s; int np = 0, tags = 0, t, frame, i, helper, busy, done, check, node; if (dsn && !dnm[0]) strcpy(dnm, n); char pn[16][64]; int pstr[16]; char full_name[256]; qualify(full_name, sizeof full_name, n); s = find_raw(full_name); @@ -794,6 +846,7 @@ static void fndef(int ret, int rstr) /* rstr: the return type is bstr */ } #endif nlo = 0; nslot = 0; nepi = 0; infn = 1; + zl_begin(here, s->name, zfl); for (i = 0; i < np; i++) { strcpy(lo[nlo].name, pn[i]); lo[nlo].arr = 0; lo[nlo].flt = 0; lo[nlo].dstr = lo[nlo].str = pstr[i & 15]; lo[nlo++].slot = nslot++; } /* retain the caller's pointer until the frame has its final segment. */ helper = !find("frame_busy") || !strcmp(s->name, "rt.frame_grow") || !strcmp(s->name, "rt.frame_leave"); @@ -823,6 +876,7 @@ static void fndef(int ret, int rstr) /* rstr: the return type is bstr */ op(O_RET); setlit(frame, nslot); infn = 0; + zl_end(); } /* enhance name(params) [-> n] micro "recipe"; or ... vm { asm }; emits the install descriptor; the booted genome installs it */ @@ -953,6 +1007,11 @@ static void top1(void) expect(";"); return; } if (accept("include")) { do { ident(); } while (accept(",")); expect(";"); return; } + if (is("lines")) { /* lines on; and lines off; (anything else is a name) */ + Lex L; lexsave(&L); next(); + if (is("on") || is("off")) { zl_on = is("on"); next(); expect(";"); return; } + lexrest(&L); + } if (accept("embed")) { embeddecl(0); return; } { if (accept("unchecked")) { top1(); return; } @@ -1088,6 +1147,8 @@ int main(int argc, char **argv) setlit(fx[i].at, s->val); } if (!(st = find("start")) || !st->defd) { fprintf(stderr, "zc0: no start\n"); return 1; } + /* the line tables are reached through rt.line_blocks, which starts out as the newest block */ + if (zl_head && (st = find_raw("rt.line_blocks"))) img[st->val - GORG] = zl_head; /* symbol blob: kind, value, sig, name chars, 0 ... then a 0 kind */ img[5] = here; for (i = 0; i < nsy; i++) { |
