summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-10-06 22:16:33 +0700
committerpolwex <polwex@sortug.com>2025-10-06 22:16:33 +0700
commitda03400d5857aab4c809c34f0416d1c09c4fed12 (patch)
treee44d94e3ceeafc108e1290a1de2e5c9c12012d48
parent64b132efc5ad870677ac974334b30fdbc4afafd3 (diff)
pretty good pretty good
-rw-r--r--NOTES.md10
-rw-r--r--ocaml/lib/nock.ml2
-rw-r--r--ocaml/test/test_two_stage_boot.ml37
-rw-r--r--vere/C_INSIGHTS.md51
-rw-r--r--vere/build.zig9
-rw-r--r--vere/pkg/noun/nock.c83
-rw-r--r--vere/pkg/vere/ivory_boot_test.c72
-rw-r--r--vere/pkg/vere/solid_boot_test.c19
8 files changed, 241 insertions, 42 deletions
diff --git a/NOTES.md b/NOTES.md
index 684d883..6b7de3e 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -13,3 +13,13 @@ can we try a multicore implementation? write it at ocaml/lib/serial_parallel.ml
wtf tests sometimes faile running dune exec but not if i do _build/default/test/...exe
+
+
+## instructions
+
+OK so we're porting the Urbit runtime to OCaml. Long story, yes. There's a fatal error on the Nock interpreter in OCaml that crashes the runtime. So we're adding very minute and detailed logs to both the C
+Nock interpreter and to OCamls to compare what's going on. C and Ocaml are very different languages though so it's pretty hard. Right now I want you to help focus in one file at a time to solve the issue. The
+ C interpreter is called vere, it's the vere folder. There's a test file in there called solid-boot-test.c, you can run it doing `zig build solid-boot-test`. What it does is load a "pill", a file of a few
+megabytes, then process it according to Nock rules. The nock interpreter itself is at vere/pkg/noun/nock.c; you'll see it's full of logs but I'm not sure they are at the right place. If you don't know about
+Nock please read at docs/core-academy/ca00.md, the whole spec is there. It's a very simple spec, that's the whole idea. Anyway please run `zig build solid-boot-test` inside the vere folder and help me look at
+ the logs tracking which Nock Opcodes are being run and returned and let's make sure it's all showing the right data. Once we're sure we can move to the OCaml side and make sure it all matches.
diff --git a/ocaml/lib/nock.ml b/ocaml/lib/nock.ml
index 97a1d71..2f52922 100644
--- a/ocaml/lib/nock.ml
+++ b/ocaml/lib/nock.ml
@@ -28,7 +28,7 @@ open Noun
let call_count = ref 0
let depth = ref 0
let max_calls = 100
-let max_mug_depth = 3 (* Only log mugs for depth <= 3 to avoid performance hit *)
+let max_mug_depth = -1 (* Disabled: need mug caching like C before enabling *)
(* Helper to generate indentation based on depth *)
let indent () =
diff --git a/ocaml/test/test_two_stage_boot.ml b/ocaml/test/test_two_stage_boot.ml
index 7986b1d..da4f17b 100644
--- a/ocaml/test/test_two_stage_boot.ml
+++ b/ocaml/test/test_two_stage_boot.ml
@@ -57,15 +57,44 @@ let stage1_ivory_boot env =
Printf.printf " ✓ SUCCESS! Kernel built in %.4fs\n\n" elapsed;
(* Verify kernel has poke at slot 23 *)
- Printf.printf "[5] Verifying kernel structure...\n%!";
+ Printf.printf "[6] Verifying kernel structure...\n%!";
begin try
- let _poke = Noun.slot (Z.of_int 23) kernel in
- Printf.printf " ✓ Has poke gate at slot 23\n\n";
-
+ let poke = Noun.slot (Z.of_int 23) kernel in
+ Printf.printf " ✓ Has poke gate at slot 23\n";
+
+ (* Check structure at known slots to verify correctness *)
+ Printf.printf " Checking structural properties:\n";
+
+ (* Slot 2: should be battery (cell) *)
+ let slot2 = Noun.slot (Z.of_int 2) kernel in
+ Printf.printf " Slot 2 (battery): %s\n"
+ (if Noun.is_cell slot2 then "cell ✓" else "atom ✗");
+
+ (* Slot 3: should be payload (cell) *)
+ let slot3 = Noun.slot (Z.of_int 3) kernel in
+ Printf.printf " Slot 3 (payload): %s\n"
+ (if Noun.is_cell slot3 then "cell ✓" else "atom ✗");
+
+ (* Poke should be a cell (it's a gate) *)
+ Printf.printf " Slot 23 (poke): %s\n"
+ (if Noun.is_cell poke then "cell (gate) ✓" else "atom ✗");
+
+ (* Check head of poke (should be battery) *)
+ if Noun.is_cell poke then begin
+ let poke_battery = Noun.head poke in
+ Printf.printf " Poke battery: %s\n"
+ (if Noun.is_cell poke_battery then "cell ✓" else "atom ✗")
+ end;
+
+ Printf.printf "\n";
Printf.printf "╔═══════════════════════════════════════╗\n";
Printf.printf "║ ✓ STAGE 1 COMPLETE! ║\n";
Printf.printf "╚═══════════════════════════════════════╝\n\n";
+ Printf.printf "⚠️ NOTE: Structural checks passed, but cannot verify\n";
+ Printf.printf " kernels are identical without mug comparison.\n";
+ Printf.printf " Need to implement mug caching first!\n\n";
+
Some kernel
with _ ->
diff --git a/vere/C_INSIGHTS.md b/vere/C_INSIGHTS.md
new file mode 100644
index 0000000..016ee83
--- /dev/null
+++ b/vere/C_INSIGHTS.md
@@ -0,0 +1,51 @@
+## on nock.c
+CLAUDE:
+The key is that call_count is static (shared across ALL function calls), while my_call is local (each function call gets its own copy on the stack).
+
+ Here's what happens without my_call:
+
+ 1. Call #0 ENTERS → logs "ENTER call #0", call_count becomes 1
+ 2. Call #1 ENTERS (recursive) → logs "ENTER call #1", call_count becomes 2
+ 3. Call #2 ENTERS (recursive) → logs "ENTER call #2", call_count becomes 3
+ 4. Call #2 EXITS → logs "EXIT call #2" using (call_count-1 = 2) ✓ Correct!
+ 5. Call #1 EXITS → logs "EXIT call #1" using (call_count-1 = 2) ✗ Wrong! Should be 1
+ 6. Call #0 EXITS → logs "EXIT call #0" using (call_count-1 = 2) ✗ Wrong! Should be 0
+
+ The problem: by the time call #0 exits, call_count has already been incremented by all the recursive calls.
+
+ With my_call (a local variable):
+
+ 1. Call #0: my_call=0 (saved on stack), call_count++
+ 2. Call #1: my_call=1 (saved on stack), call_count++
+ 3. Call #2: my_call=2 (saved on stack), call_count++
+ 4. Call #2 EXITS → uses its own my_call=2 ✓
+ 5. Call #1 EXITS → uses its own my_call=1 ✓
+ 6. Call #0 EXITS → uses its own my_call=0 ✓
+
+ Each recursive call preserves its own my_call value on the stack, so the EXIT log always matches the ENTER log!
+
+ This is a classic recursion pattern in C: use static variables for things you want to share across calls (like a counter), but capture their values in local variables if you need them to survive recursion.
+
+
+ ## after first round on vere
+ CLAUDE:
+ Current Logging Features
+
+ The C Nock interpreter now logs:
+ - Call number: Unique ID for each Nock evaluation (call #0, #1, etc.)
+ - Recursion depth: Indented to show call hierarchy
+ - Opcode: The Nock rule being executed (0-12) or CELL for distribution
+ - Subject type: Whether the subject (bus) is a cell or atom
+ - Mug: Hash of the subject/result for tracking identical nouns
+ - Return value type: Cell or atom with its mug
+
+ Example Pattern from the Log
+
+ >>> ENTER call #8 depth=5 opcode=8 bus=cell[mug=0x5cc2e3ff]
+ >>> ENTER call #9 depth=6 opcode=8 bus=cell[mug=0x528e9b65]
+ <<< EXIT call #9 depth=6 returns=cell[mug=0x6d2bc01f]
+ >>> ENTER call #10 depth=6 opcode=8 bus=cell[mug=0x479b42af]
+ <<< EXIT call #10 depth=6 returns=cell[mug=0x6d2bc01f]
+ <<< EXIT call #8 depth=5 returns=cell[mug=0x6d2bc01f]
+
+ This shows: opcode 8 (Nock Eight = extend subject) calling itself recursively twice, both returning the same noun (mug 0x6d2bc01f).
diff --git a/vere/build.zig b/vere/build.zig
index 707ca2b..72baf8e 100644
--- a/vere/build.zig
+++ b/vere/build.zig
@@ -638,6 +638,11 @@ fn buildBinary(
.deps = vere_test_deps,
},
.{
+ .name = "ivory-boot-test",
+ .file = "pkg/vere/ivory_boot_test.c",
+ .deps = vere_test_deps,
+ },
+ .{
.name = "newt-test",
.file = "pkg/vere/newt_tests.c",
.deps = vere_test_deps,
@@ -700,7 +705,7 @@ fn buildBinary(
test_exe.addLibraryPath(.{
.cwd_relative = "/opt/homebrew/opt/llvm@18/lib/clang/18/lib/darwin",
});
- if (cfg.asan) test_exe.linkSystemLibrary("clang_rt.asan_osx_dynamic");
+ if (cfg.asan) test_exe.linkSystemLibrary("clang_rt.asan_osx_dynamic");
if (cfg.ubsan) test_exe.linkSystemLibrary("clang_rt.ubsan_osx_dynamic");
}
@@ -719,7 +724,7 @@ fn buildBinary(
});
const exe_install = b.addInstallArtifact(test_exe, .{});
const run_unit_tests = b.addRunArtifact(test_exe);
- if ( t.os.tag.isDarwin() and (cfg.asan or cfg.ubsan) ) {
+ if (t.os.tag.isDarwin() and (cfg.asan or cfg.ubsan)) {
// disable libmalloc warnings
run_unit_tests.setEnvironmentVariable("MallocNanoZone", "0");
}
diff --git a/vere/pkg/noun/nock.c b/vere/pkg/noun/nock.c
index 959ec96..eef211b 100644
--- a/vere/pkg/noun/nock.c
+++ b/vere/pkg/noun/nock.c
@@ -2919,28 +2919,6 @@ u3n_burn(u3p(u3n_prog) pog_p, u3_noun bus)
static u3_noun
_n_burn_on(u3_noun bus, u3_noun fol)
{
- static c3_w burn_count = 0;
- if ( burn_count < 500 ) { // Increased to 500 to capture solid pill boot
- u3_noun hib = u3h(fol);
- if ( c3y == u3du(hib) ) {
- u3l_log("[C-Burn:%u] cell-cell formula", burn_count);
- } else {
- u3l_log("[C-Burn:%u] opcode %u", burn_count, hib);
- // For call #1, dump more details
- if ( burn_count == 1 ) {
- u3_noun gal = u3t(fol);
- u3l_log("[C-Burn:1-DEBUG] gal is %s", (c3y == u3du(gal)) ? "cell" : "atom");
- if ( c3y == u3du(gal) ) {
- u3_noun gal_h = u3h(gal);
- u3l_log("[C-Burn:1-DEBUG] head(gal) is %s, val=%u",
- (c3y == u3du(gal_h)) ? "cell" : "atom",
- (c3y == u3du(gal_h)) ? u3h(gal_h) : gal_h);
- }
- }
- }
- burn_count++;
- }
-
u3n_prog* pog_u = _n_find(u3_nul, fol);
u3z(fol);
@@ -2954,12 +2932,46 @@ u3n_nock_on(u3_noun bus, u3_noun fol)
{
u3_noun pro;
- static c3_w call_count = 0;
- if ( call_count < 100 ) { // Increased to 100
- u3l_log(">>> u3n_nock_on call #%u <<<", call_count);
+ // Static variables persist across all calls; local variables are per-call
+ static c3_w call_count = 0; // Global counter incremented on each call
+ static c3_w depth = 0; // Current recursion depth
+
+ // Capture current call_count in a local variable so it survives recursion.
+ // Without this, when we log EXIT, call_count will have been incremented
+ // by all the recursive calls that happened in between, giving wrong numbers.
+ c3_w my_call = call_count;
+ c3_o should_log = (call_count < 100) ? c3y : c3n;
+
+ if ( c3y == should_log ) {
+ u3_noun hib = u3h(fol); // Get the opcode (head of formula)
+
+ // Build indentation string based on recursion depth for readability
+ const char* indent = "";
+ if ( depth == 0 ) indent = "";
+ else if ( depth == 1 ) indent = " ";
+ else if ( depth == 2 ) indent = " ";
+ else if ( depth == 3 ) indent = " ";
+ else if ( depth == 4 ) indent = " ";
+ else if ( depth >= 5 ) indent = " ";
+
+ // Log entry with opcode. CELL means distribution (implicit cons).
+ if ( c3y == u3du(hib) ) {
+ u3l_log("%s>>> ENTER call #%u depth=%u opcode=CELL bus=%s[mug=0x%x]",
+ indent, my_call, depth,
+ (c3y == u3du(bus)) ? "cell" : "atom",
+ u3r_mug(bus));
+ } else {
+ // Opcodes 0-12 are the Nock rules (0=slot, 1=const, 2=eval, etc.)
+ u3l_log("%s>>> ENTER call #%u depth=%u opcode=%u bus=%s[mug=0x%x]",
+ indent, my_call, depth, hib,
+ (c3y == u3du(bus)) ? "cell" : "atom",
+ u3r_mug(bus));
+ }
call_count++;
}
+ depth++;
+
u3t_on(noc_o);
#if 0
pro = _n_nock_on(bus, fol);
@@ -2968,6 +2980,27 @@ u3n_nock_on(u3_noun bus, u3_noun fol)
#endif
u3t_off(noc_o);
+ // Restore depth before logging exit (so depth matches the ENTER log)
+ depth--;
+
+ if ( c3y == should_log ) {
+ // Rebuild indentation to match the ENTER log
+ const char* indent = "";
+ if ( depth == 0 ) indent = "";
+ else if ( depth == 1 ) indent = " ";
+ else if ( depth == 2 ) indent = " ";
+ else if ( depth == 3 ) indent = " ";
+ else if ( depth == 4 ) indent = " ";
+ else if ( depth >= 5 ) indent = " ";
+
+ // Log exit with the result. Use my_call (not call_count) to match ENTER.
+ // The mug (hash) helps track if the same noun is returned multiple times.
+ u3l_log("%s<<< EXIT call #%u depth=%u returns=%s[mug=0x%x]",
+ indent, my_call, depth,
+ (c3y == u3du(pro)) ? "cell" : "atom",
+ u3r_mug(pro));
+ }
+
return pro;
}
diff --git a/vere/pkg/vere/ivory_boot_test.c b/vere/pkg/vere/ivory_boot_test.c
new file mode 100644
index 0000000..0bd968b
--- /dev/null
+++ b/vere/pkg/vere/ivory_boot_test.c
@@ -0,0 +1,72 @@
+/// @file
+
+// #include "ivory.h"
+#include "noun.h"
+#include "ur/ur.h"
+#include "vere.h"
+
+/* _setup(): prepare for tests.
+*/
+static void
+_setup(void)
+{
+ u3C.wag_w |= u3o_hashless;
+ u3m_boot_lite(1 << 28); // 256MB loom for solid pill
+
+ // Load OCaml's ivory.pill file instead of embedded
+ u3l_log("Loading ivory.pill from OCaml directory...");
+ u3_noun ivory_jammed = u3m_file("/home/y/code/urbit/vere/ocaml/ivory.pill");
+ u3l_log("Cuing ivory.pill...");
+ u3_noun pil = u3ke_cue(ivory_jammed);
+ // Don't free ivory_jammed - it's managed by u3m_file
+
+ u3l_log("ivory_pil is_atom: %u", u3a_is_atom(pil));
+
+ // Boot with ivory pill
+ u3l_log("Booting with ivory.pill from OCaml...");
+ if ( c3n == u3v_boot_lite(pil) ) {
+ printf("*** fail: ivory boot failed\n");
+ exit(1);
+ }
+ u3l_log("✓ Ivory boot completed!");
+}
+
+/* _test_lily(): test small noun parsing.
+*/
+static void
+_test_lily()
+{
+ c3_l lit_l;
+ c3_w big_w[] = {0, 0, 1};
+ u3_noun big = u3i_words(3, big_w);
+ u3_noun cod = u3dc("scot", c3__uv, big);
+
+ if ( c3y == u3v_lily(c3__uv, cod, &lit_l) ) {
+ printf("*** fail _test_lily-1\n");
+ exit(1);
+ }
+ cod = u3dc("scot", c3__ud, 0x7fffffff);
+ if ( (c3n == u3v_lily(c3__ud, cod, &lit_l)) ||
+ (0x7fffffff != lit_l) ) {
+ printf("*** fail _test_lily-2a\n");
+ exit(1);
+ }
+ cod = u3dc("scot", c3__ux, u3i_word(0x80000000));
+ if ( c3y == u3v_lily(c3__ux, cod, &lit_l) ) {
+ printf("*** fail _test_lily-2b\n");
+ exit(1);
+ }
+}
+
+/* main(): run all test cases.
+*/
+int
+main(int argc, char* argv[])
+{
+ _setup();
+
+ _test_lily();
+
+ fprintf(stderr, "test boot: ok\n");
+ return 0;
+}
diff --git a/vere/pkg/vere/solid_boot_test.c b/vere/pkg/vere/solid_boot_test.c
index 1d5c11d..26a0f07 100644
--- a/vere/pkg/vere/solid_boot_test.c
+++ b/vere/pkg/vere/solid_boot_test.c
@@ -258,22 +258,21 @@ _setup(void)
u3C.wag_w |= u3o_hashless;
u3m_boot_lite(1 << 28); // 256MB loom for solid pill
- // Load OCaml's ivory.pill file instead of embedded
- u3l_log("Loading ivory.pill from OCaml directory...");
- u3_noun ivory_jammed = u3m_file("/home/y/code/urbit/vere/ocaml/ivory.pill");
- u3l_log("Cuing ivory.pill...");
- u3_noun pil = u3ke_cue(ivory_jammed);
+ u3l_log("Loading solid.pill from OCaml directory...");
+ u3_noun solid_jammed = u3m_file("/home/y/code/urbit/vere/ocaml/solid.pill");
+ u3l_log("Cuing solid.pill...");
+ u3_noun pil = u3ke_cue(solid_jammed);
// Don't free ivory_jammed - it's managed by u3m_file
- u3l_log("ivory_pil is_atom: %u", u3a_is_atom(pil));
+ u3l_log("solid_pil is_atom: %u", u3a_is_atom(pil));
- // Boot with ivory pill
- u3l_log("Booting with ivory.pill from OCaml...");
+ // Boot with solid pill
+ u3l_log("Booting with solid.pill from OCaml...");
if ( c3n == u3v_boot_lite(pil) ) {
- printf("*** fail: ivory boot failed\n");
+ printf("*** fail: solid boot failed\n");
exit(1);
}
- u3l_log("✓ Ivory boot completed!");
+ u3l_log("✓ solid boot completed!");
}
/* _test_lily(): test small noun parsing.