diff --git a/Makefile b/Makefile index 7e7ad5fd..2b4f2ffa 100644 --- a/Makefile +++ b/Makefile @@ -316,6 +316,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_typeeq_fn_ast \ $(BIN)/test_amp_fn_ident \ $(BIN)/test_star_fn_deref \ + $(BIN)/test_star_fn_deref_call \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -618,6 +619,12 @@ $(BIN)/test_star_fn_deref: test/wcc/765_star_fn_deref.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_star_fn_deref_call: test/wcc/766_star_fn_deref_call.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b6a3a091..ac20e233 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -12408,7 +12408,6 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.N_DOT) { nm = callee.str; }; - if (nm.len == 0) { return nil; }; // #56: bare-leaf N_IDENT calls go through scopelookupprefer so // `foo()` inside module M binds to M.foo rather than another // module's same-leaf foo at the head of the flat scope bucket. @@ -12438,31 +12437,41 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // that coexists in the same scope (coexistence-equivalent of // cstage's use_alias; see lib/ww/sym.ww + memory // module_type_name_collision). - let s: *sym = nil; - if (callee.kind == nkind.N_IDENT) { - s = scopelookupprefer(c.cur, c.curmod, nm); - } else { - let ms: *sym = nil; - if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { - ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != skind.SK_USE) { - let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); - if (mu != nil) { ms = mu; }; + // + // #181: a non-named callee (N_UN TK_STAR deref of a *fn local, + // `(*f)(...)`; or any other expression-as-callee shape) has no + // leaf to resolve here — skip the SK_FN name-lookup and let the + // fn-VALUE fallback below peel TPTR / dealias to TFN. Mirrors + // harec check_autodereference at ref/harec/src/check.c:1566. + // cgen post-#180+#185 already lowers the deref-call correctly, + // so lifting the asserttyped bail is silent-SIGSEGV-safe. + if (nm.len > 0) { + let s: *sym = nil; + if (callee.kind == nkind.N_IDENT) { + s = scopelookupprefer(c.cur, c.curmod, nm); + } else { + let ms: *sym = nil; + if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { + ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); + if (ms != nil && ms.skind != skind.SK_USE) { + let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); + if (mu != nil) { ms = mu; }; + }; + }; + if (ms != nil && ms.skind == skind.SK_USE) { + s = scopelookupinmodule(c.cur, callee.lhs.str, nm); + } else { + s = scopelookup(c.cur, nm); }; }; - if (ms != nil && ms.skind == skind.SK_USE) { - s = scopelookupinmodule(c.cur, callee.lhs.str, nm); - } else { - s = scopelookup(c.cur, nm); - }; + if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { + // fn-decl's lhs is the return-type AST node. Mirrors cstage + // cmd/wcc/check.c:984+ regular-CALL `n->type = + // build_fn_type(c, s->decl)->ret` shape. + e.type_ = tinfofornode(c, s.decl.lhs): *void; + return s.decl.lhs; + }; }; }; }; - if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { - // fn-decl's lhs is the return-type AST node. Mirrors cstage - // cmd/wcc/check.c:984+ regular-CALL `n->type = - // build_fn_type(c, s->decl)->ret` shape. - e.type_ = tinfofornode(c, s.decl.lhs): *void; - return s.decl.lhs; - }; }; }; // A callee that is a fn-VALUE — a fn-pointer struct field // (`w.emit(...)`), local, or param — has no free SK_FN entry, so // the name lookup above misses. Read the result off the checked diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index f1b9d929..6d65c35b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2378,7 +2378,6 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.N_DOT) { nm = callee.str; }; - if (nm.len == 0) { return nil; }; // #56: bare-leaf N_IDENT calls go through scopelookupprefer so // `foo()` inside module M binds to M.foo rather than another // module's same-leaf foo at the head of the flat scope bucket. @@ -2408,31 +2407,41 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // that coexists in the same scope (coexistence-equivalent of // cstage's use_alias; see lib/ww/sym.ww + memory // module_type_name_collision). - let s: *sym = nil; - if (callee.kind == nkind.N_IDENT) { - s = scopelookupprefer(c.cur, c.curmod, nm); - } else { - let ms: *sym = nil; - if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { - ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != skind.SK_USE) { - let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); - if (mu != nil) { ms = mu; }; + // + // #181: a non-named callee (N_UN TK_STAR deref of a *fn local, + // `(*f)(...)`; or any other expression-as-callee shape) has no + // leaf to resolve here — skip the SK_FN name-lookup and let the + // fn-VALUE fallback below peel TPTR / dealias to TFN. Mirrors + // harec check_autodereference at ref/harec/src/check.c:1566. + // cgen post-#180+#185 already lowers the deref-call correctly, + // so lifting the asserttyped bail is silent-SIGSEGV-safe. + if (nm.len > 0) { + let s: *sym = nil; + if (callee.kind == nkind.N_IDENT) { + s = scopelookupprefer(c.cur, c.curmod, nm); + } else { + let ms: *sym = nil; + if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { + ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); + if (ms != nil && ms.skind != skind.SK_USE) { + let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); + if (mu != nil) { ms = mu; }; + }; + }; + if (ms != nil && ms.skind == skind.SK_USE) { + s = scopelookupinmodule(c.cur, callee.lhs.str, nm); + } else { + s = scopelookup(c.cur, nm); }; }; - if (ms != nil && ms.skind == skind.SK_USE) { - s = scopelookupinmodule(c.cur, callee.lhs.str, nm); - } else { - s = scopelookup(c.cur, nm); - }; + if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { + // fn-decl's lhs is the return-type AST node. Mirrors cstage + // cmd/wcc/check.c:984+ regular-CALL `n->type = + // build_fn_type(c, s->decl)->ret` shape. + e.type_ = tinfofornode(c, s.decl.lhs): *void; + return s.decl.lhs; + }; }; }; }; - if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { - // fn-decl's lhs is the return-type AST node. Mirrors cstage - // cmd/wcc/check.c:984+ regular-CALL `n->type = - // build_fn_type(c, s->decl)->ret` shape. - e.type_ = tinfofornode(c, s.decl.lhs): *void; - return s.decl.lhs; - }; }; }; // A callee that is a fn-VALUE — a fn-pointer struct field // (`w.emit(...)`), local, or param — has no free SK_FN entry, so // the name lookup above misses. Read the result off the checked diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a549d86e..b60f8818 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -12408,7 +12408,6 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.N_DOT) { nm = callee.str; }; - if (nm.len == 0) { return nil; }; // #56: bare-leaf N_IDENT calls go through scopelookupprefer so // `foo()` inside module M binds to M.foo rather than another // module's same-leaf foo at the head of the flat scope bucket. @@ -12438,31 +12437,41 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // that coexists in the same scope (coexistence-equivalent of // cstage's use_alias; see lib/ww/sym.ww + memory // module_type_name_collision). - let s: *sym = nil; - if (callee.kind == nkind.N_IDENT) { - s = scopelookupprefer(c.cur, c.curmod, nm); - } else { - let ms: *sym = nil; - if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { - ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != skind.SK_USE) { - let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); - if (mu != nil) { ms = mu; }; + // + // #181: a non-named callee (N_UN TK_STAR deref of a *fn local, + // `(*f)(...)`; or any other expression-as-callee shape) has no + // leaf to resolve here — skip the SK_FN name-lookup and let the + // fn-VALUE fallback below peel TPTR / dealias to TFN. Mirrors + // harec check_autodereference at ref/harec/src/check.c:1566. + // cgen post-#180+#185 already lowers the deref-call correctly, + // so lifting the asserttyped bail is silent-SIGSEGV-safe. + if (nm.len > 0) { + let s: *sym = nil; + if (callee.kind == nkind.N_IDENT) { + s = scopelookupprefer(c.cur, c.curmod, nm); + } else { + let ms: *sym = nil; + if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { + ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); + if (ms != nil && ms.skind != skind.SK_USE) { + let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); + if (mu != nil) { ms = mu; }; + }; + }; + if (ms != nil && ms.skind == skind.SK_USE) { + s = scopelookupinmodule(c.cur, callee.lhs.str, nm); + } else { + s = scopelookup(c.cur, nm); }; }; - if (ms != nil && ms.skind == skind.SK_USE) { - s = scopelookupinmodule(c.cur, callee.lhs.str, nm); - } else { - s = scopelookup(c.cur, nm); - }; + if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { + // fn-decl's lhs is the return-type AST node. Mirrors cstage + // cmd/wcc/check.c:984+ regular-CALL `n->type = + // build_fn_type(c, s->decl)->ret` shape. + e.type_ = tinfofornode(c, s.decl.lhs): *void; + return s.decl.lhs; + }; }; }; }; - if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { - // fn-decl's lhs is the return-type AST node. Mirrors cstage - // cmd/wcc/check.c:984+ regular-CALL `n->type = - // build_fn_type(c, s->decl)->ret` shape. - e.type_ = tinfofornode(c, s.decl.lhs): *void; - return s.decl.lhs; - }; }; }; // A callee that is a fn-VALUE — a fn-pointer struct field // (`w.emit(...)`), local, or param — has no free SK_FN entry, so // the name lookup above misses. Read the result off the checked diff --git a/test/wcc/766_star_fn_deref_call.c b/test/wcc/766_star_fn_deref_call.c new file mode 100644 index 00000000..21f305a4 --- /dev/null +++ b/test/wcc/766_star_fn_deref_call.c @@ -0,0 +1,289 @@ +/* + * 766_star_fn_deref_call — root-cause lock for project #181, and the + * live verification of the full deref-call c-cluster (#180 + #185 + + * #181 working together end-to-end). + * + * Pre-#181 wwstage's checker bailed asserttyped on the N_CALL whose + * callee was N_UN TK_STAR over a *fn — selfhost/cmd/wcc/check.ww + * exprtype's N_CALL arm only resolved IDENT/DOT-named callees and + * early-returned nil for any other shape, leaving e.type_ unstamped + * so the post-checker invariant gate fired. With cgen post-#180+#185 + * already correct for the deref-call lowering, the safe fix was to + * lift the asserttyped bail and route the callee through the + * fn-VALUE fallback (autodereference + dealias to TY_FN, mirroring + * harec check_autodereference at ref/harec/src/check.c:1566). + * + * Fix: selfhost/cmd/wcc/check.ww exprtype N_CALL arm — replace the + * `if (nm.len == 0) return nil` early-bail with `if (nm.len > 0) + * { name-lookup }`, so non-named callees fall through to the + * existing fn-VALUE branch (peel TPTR, dealias to TFN, stamp the + * result type). cstage check.c already worked because cexpr + * recurses on the callee — TK_STAR's unop arm returns t->sub which + * is TY_FN directly, no name path needed. + * + * Gate flip from 765: 765 was cstage-only because wwstage bailed + * before reaching cgen. Post-#181 every row gates BOTH stages — + * cstage runtime, wwstage runtime, AND cs.s == ww.s byte-id. This + * is the runtime coverage 765 deferred plus the symmetry gate that + * proves both stages emit identical asm for the deref-call shape. + * + * Coverage (same 5 rows as 765, now with STAGE_CS|STAGE_WW): + * 1. minimal — `let f = &add1; (*f)(7) == 8` + * 2. branched_callee — pick aa or bb by runtime cond + * 3. alias_chain — `let f = &fn; let g = f; (*g)(7)` + * 4. fn_with_args — multiple args, scalar + ptr mix + * 5. fn_tuple_return — (i32, i32) return shape; multi-reg + * return ABI survives the deref-call + * + * Gates per row: + * a. cstage builds + runs, exit == expected (already worked + * post-#180+#185; kept for c-cluster regression coverage). + * b. wwstage builds + runs, exit == expected — the #181 lift + * gate; pre-fix this row's asserttyped bailed before cgen. + * c. cstage .s == wwstage .s byte-identical — rule-10 symmetry, + * proves the checker change doesn't perturb the codegen. + * + * GATE POLARITY: must stay GREEN. A red here means either the + * checker N_CALL fn-VALUE route regressed, the cgen deref-call + * lowering regressed (#180/#185), or stage symmetry drifted. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int expected_exit; + int stage_mask; +}; + +static const struct row rows[] = { + { "minimal", + "fn add1(x: i32) i32 = { return x + 1; };\n" + "export fn main() i32 = {\n" + " let f: *fn(x: i32) i32 = &add1;\n" + " return (*f)(7);\n" + "};\n", + 8, + STAGE_CS | STAGE_WW }, + /* Branched-callee: the if-arm forces a runtime choice between + * two distinct fn addresses, defeating any constant-aliasing + * mask of the deref miscompile (#105 lesson). */ + { "branched_callee", + "fn aa(x: i32) i32 = { return x; };\n" + "fn bb(x: i32) i32 = { return x + 100; };\n" + "export fn main() i32 = {\n" + " let pick: i32 = 1;\n" + " let f: *fn(x: i32) i32 = &aa;\n" + " if (pick != 0) { f = &bb; };\n" + " return (*f)(7);\n" + "};\n", + 107, + STAGE_CS | STAGE_WW }, + { "alias_chain", + "fn add1(x: i32) i32 = { return x + 1; };\n" + "export fn main() i32 = {\n" + " let f: *fn(x: i32) i32 = &add1;\n" + " let g: *fn(x: i32) i32 = f;\n" + " return (*g)(7);\n" + "};\n", + 8, + STAGE_CS | STAGE_WW }, + { "fn_with_args", + "fn many(a: i32, b: i32, p: *i32) i32 = { return a + b + *p; };\n" + "export fn main() i32 = {\n" + " let z: i32 = 5;\n" + " let f: *fn(a: i32, b: i32, p: *i32) i32 = &many;\n" + " return (*f)(3, 7, &z);\n" + "};\n", + 15, + STAGE_CS | STAGE_WW }, + /* Tuple-return: exercises the multi-register return ABI through + * the deref-call. (i32, i32) keeps it simple while still covering + * the multi-reg path. */ + { "fn_tuple_return", + "fn pair(x: i32) (i32, i32) = { return (x, x + 1); };\n" + "export fn main() i32 = {\n" + " let f: *fn(x: i32) (i32, i32) = &pair;\n" + " let a, b = (*f)(7);\n" + " return a + b;\n" + "};\n", + 15, + STAGE_CS | STAGE_WW }, +}; + +static int +write_source(const char *src_path, const char *src) +{ + FILE *f = fopen(src_path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[512]; + snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + rmdir(tmpdir); +} + +static int +build_via_driver(const char *driver, const char *tmpdir, const char *src) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null", + tmpdir, driver, src); + return runwait(cmd); +} + +/* run_row — build + run; returns the binary's exit code (-1 on + * build failure). */ +static int +run_row(const char *driver, const struct row *r, int seq) +{ + char tmpdir[256], src[256], base[64], outbin[512]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsfdc_%d_d_%d", getpid(), seq); + snprintf(src, sizeof src, "%s/main766.ww", tmpdir); + snprintf(base, sizeof base, "main766"); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { + cleanup_tmp(tmpdir, base); + return -1; + } + int rc = -1; + if (build_via_driver(driver, tmpdir, src) == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } + cleanup_tmp(tmpdir, base); + return rc; +} + +/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so + * ww_ww writing intermediates next to the source doesn't clobber the + * cstage .s (filed bug per CLAUDE.md rule 14 phase split). */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, + const struct row *r, int seq) +{ + char src[256], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/wcsfdc_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/wcsfdc_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main766"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/main766.ww", tdc); + if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; } + int rc = -1; + if (build_via_driver(cdrv, tdc, src) != 0) goto out; + snprintf(cs, sizeof cs, "%s/%s.s", tdc, base); + + snprintf(src, sizeof src, "%s/main766.ww", tdw); + if (write_source(src, r->src) != 0) goto out; + if (build_via_driver(wdrv, tdw, src) != 0) goto out; + snprintf(ws, sizeof ws, "%s/%s.s", tdw, base); + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (fc && fw) { + rc = 0; + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); +out: + cleanup_tmp(tdc, base); + cleanup_tmp(tdw, base); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + int wwpresent = (access(wdrv, X_OK) == 0); + int seq = 0; + + for (int i = 0; i < n; i++) { + if (rows[i].stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, &rows[i], seq++); + if (got != rows[i].expected_exit) { + fprintf(stderr, + "star_fn_deref_call[cstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + } + if (wwpresent && (rows[i].stage_mask & STAGE_WW)) { + total++; + int got = run_row(wdrv, &rows[i], seq++); + if (got != rows[i].expected_exit) { + fprintf(stderr, + "star_fn_deref_call[wwstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + total++; + if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) { + fprintf(stderr, + "star_fn_deref_call[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + + if (!wwpresent) + fprintf(stderr, "star_fn_deref_call: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "star_fn_deref_call: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("star_fn_deref_call: %d/%d ok\n", total, total); + return 0; +}