aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar zinc <zinc@downsta.ir> 2026-10-14 15:54:04 +0330
committerGravatar zinc <zinc@downsta.ir> 2026-10-14 15:54:04 +0330
commit0879d0a42d137054057cd2a1e37437e6be78ed0b (patch)
treefcb16d3185c1f80fae8d54cd9062bd68d6c0c88d /src
parentmerge spec conformance suite (diff)
parentmerge main into work/line-tables (diff)
merge line tables
Diffstat (limited to 'src')
-rw-r--r--src/compiler/base.zc89
-rw-r--r--src/compiler/builtins.zc11
-rw-r--r--src/compiler/decl.zc13
-rw-r--r--src/compiler/expr.zc4
-rw-r--r--src/compiler/image.zc23
-rw-r--r--src/compiler/lex.zc29
-rw-r--r--src/compiler/setup.zc1
-rw-r--r--src/compiler/stmt.zc2
-rw-r--r--src/core/core.c5
-rw-r--r--src/js/tinyvm.ts2
-rw-r--r--src/lsp/server.zc3
-rw-r--r--src/micro/micro.c3
-rw-r--r--src/runtime/rt.zc119
-rwxr-xr-xsrc/tools/automap.sh14
-rw-r--r--src/tools/build.zc3
-rw-r--r--src/tools/genmap.c2
-rwxr-xr-xsrc/tools/parity.sh4
-rw-r--r--src/tools/zc0.c65
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++) {