OCaml kernel changes:
sx_types.ml:
- Add l_call_count : int field to lambda type — counts how many times
a named lambda has been invoked through the VM dispatch path.
- Add module-level refs jit_threshold (default 4), jit_compiled_count,
jit_skipped_count, jit_threshold_skipped_count for stats.
Refs live here (not sx_vm) so sx_primitives can read them without
creating a sx_primitives → sx_vm dependency cycle.
sx_vm.ml:
- In the Lambda case of cek_call_or_suspend, before triggering the JIT,
increment l.l_call_count. Only call jit_compile_ref if count >= the
runtime-tunable threshold. Below threshold, fall through to the
existing cek_call_or_suspend path (interpreter-style).
sx_primitives.ml:
- Register jit-stats — returns dict {threshold, compiled, compile-failed,
below-threshold}.
- Register jit-set-threshold! N — change threshold at runtime.
- Register jit-reset-counters! — zero the stats counters.
bin/run_tests.ml:
- Add l_call_count = 0 to the test-fixture lambda construction.
Effect: lambdas only get JIT-compiled after the 4th invocation. One-shot
lambdas (test harness wrappers, eval-hs throwaways, REPL inputs) never enter
the JIT cache, eliminating the cumulative slowdown that the batched runner
currently works around. Hot paths (component renders, event handlers) cross
the threshold within a handful of calls and get the full JIT speed.
Phase 2 (LRU eviction) and Phase 3 (jit-reset! / jit-clear-cold!) follow.
Verified: 4771 passed, 1111 failed in OCaml run_tests.exe — identical to
baseline before this change. No regressions; tiered logic is correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes for Iverson's dfn
{1≥≢⍵:⍵ ⋄ p←⍵⌷⍨?≢⍵ ⋄ (∇⍵⌿⍨⍵<p),(p=⍵)/⍵,∇⍵⌿⍨⍵>p}:
1. parser: standalone op-glyph branch (/ ⌿ \ ⍀) now consumes a
following ⍨ or ¨ and emits :derived-fn — `⍵⌿⍨⍵<p` parses
as compress-commute (was previously dropping ⍨)
2. tokenizer: `name←...` (no spaces) now tokenizes as separate
:name + :assign instead of eating ← into the name. ⎕← still
stays one token for the output op
3. inline p←⍵⌷⍨?≢⍵ mid-dfn now works via existing :assign-expr
Full suite 585/585. Phase 10 complete (all 7 items ticked).
Remaining gaps for a future phase: heterogeneous-strand inner
product is the only unfinished part — life works after dropping ⊃,
quicksort works directly.
Side-quest emerged from adding roman.ml baseline (Roman numeral greedy
encoding): top-level 'let () = expr' was unsupported because
ocaml-parse-program's parse-decl-let consumed an ident strictly. Now
parse-decl-let recognises a leading '()' as a unit binding and
synthesises a __unit_NN name (matching how parse-let already handles
inner-let unit patterns).
roman.ml exercises:
* tuple list literal [(int * string); ...]
* recursive pattern match on tuple-cons
* String.length + List.fold_left
* the new top-level let () support (sanity in a comment, even though
the program ends with a bare expression for the test harness)
Bumped lib/ocaml/test.sh server timeout 180->360s — the recent surge in
test count plus a CPU-contended host was crowding out the sole epoch
reaching the deeper smarts.
Parser hk-parse-parens gains a `::` arm after the first inner expression:
consume `::`, parse a type via the existing hk-parse-type, expect `)`,
emit (:type-ann EXPR TYPE). Sections, tuples, parenthesised expressions
and unit `()` are unchanged.
Desugar drops the annotation — :type-ann E _ → (hk-desugar E) — since
the existing eval path has no type-directed dispatch. Phase 20 will
extend infer.sx to consume the annotation and unify against the
inferred type.
tests/parse-extras.sx (12/12) covers literal, arithmetic, function arg,
string, bool, tuple, nested annotation, function-typed annotation, and
no-regression checks for plain parens / 3-tuples / left+right sections.
eval (66/0), exceptions (14/0), typecheck (15/0), records (14/0), ioref
(13/0) all still clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bytecode + sx_browser.bc.{js,wasm.js} regenerated from sources updated
by the hs-f merge (e8246340). No semantic change — these are build
outputs catching up to their inputs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- apl-partition: new partition where M[i]>M[i-1] (init prev=0);
continue where M[i]≤prev∧M[i]≠0; drop cells where M[i]=0
- Returns apl-vector of apl-vector parts
- pipeline 140/140
Tcl tokenizer treats $::g-name as $::g + literal -name, so the var
lookup fails. Renamed test vars to ::gname / ::nval (no hyphens).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- apl-unique: dedup keeping first-occurrence order
- apl-union: dedup'd A then B-elements-not-in-A
- apl-intersect: A elements that are in B, preserves left order
- ∪ wired both monadic and dyadic; ∩ wired dyadic
- pipeline 121/121
Five "typed ok: …" tests in tests/typecheck.sx compared an unforced thunk
against an integer/list. The untyped-path convention is hk-deep-force on
the result; hk-run-typed follows the same shape but the tests omitted
that wrap. Added hk-deep-force around hk-run-typed in those five tests.
typecheck.sx now 15/15; infer.sx still 75/75.
Plan adds three phases capturing the remaining type-system work:
- Phase 20: Algorithm W gaps (case, do, record accessors, expression
annotations).
- Phase 21: type classes with qualified types ([Eq a] => …) and
constraint propagation, integrated with the existing dict-passing
evaluator.
- Phase 22: typecheck-then-run as the default conformance path, with a
≥ 30/36 typechecking threshold before swap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more arities of the naive memoization wrapper:
table-1: predicate (1-arg) tabling. Cache entry is :ok / :no.
Demonstrated with a tabled membero-as-predicate.
table-3: 3-arg (i1 i2 output) tabling. Cache key joins the two
inputs; cache value is the output value list.
Canonical demo: tabled Ackermann.
(ack-o 0 0 q) -> 1
(ack-o 2 3 q) -> 9
(ack-o 3 3 q) -> 61
A(3,3) executes A(2,..) many times, A(1,..) more, A(0,..) most. With
table-3 each (m, n) pair is computed once.
6 new tests, 644/644 cumulative.
`table-2` wraps a 2-arg (input, output) relation. On a ground input
walk, looks up the (string-encoded) cache key; on miss, runs the
relation, drains the answer stream, extracts walk*-output values from
each subst, stores them, and replays. On hit, replays the cached
values directly — no recomputation.
Cache lifetime: a single global mk-tab-cache (mutated via set!).
mk-tab-clear! resets between independent queries.
Canonical demo: tabled fib(25) = 75025 in ~5 seconds; the same naive
fib-o times out at 60s. Memoization collapses the exponential redundant
recomputation in the binary recursion.
Limitations (deferred to future SLG work): cyclic recursive calls with
the same ground key still diverge — naive memoization populates the
cache only AFTER computation completes, so a recursive call inside its
own computation can't see the in-progress entry. The brief's "tabled
patho on cyclic graphs" use case requires producer/consumer
scheduling and is left for a future iteration.
12 new tests, fib(0..20) + ground-term predicate + cache-replay
verification. 638/638 cumulative.
In parse-atom-postfix, after consuming '.', if the next token is '(',
parse the inner expression and emit (:let-open M EXPR) instead of
:field. Cleanly composes with the existing :let-open evaluator and
loops to allow chained dot postfixes.
List.(length [1;2;3]) = 3
List.(map (fun x -> x + 1) [1;2;3]) = [2;3;4]
Option.(map (fun x -> x * 10) (Some 4)) = Some 40
Parser detects 'let open' as a separate let-form, parses M as a path
(Ctor(.Ctor)*) directly via inline AST construction (no source slicing
since cur-pos is only available in ocaml-parse-program), and emits
(:let-open PATH BODY).
Eval resolves the path to a module dict and merges its bindings into
the env for body evaluation. Now:
let open List in map (fun x -> x * 2) [1;2;3] = [2;4;6]
let open Option in map (fun x -> x + 1) (Some 5) = Some 6
ocaml-eval-module now handles :def-mut and :def-rec-mut decls so
'module M = struct let rec a n = ... and b n = ... end' works. The
def-rec-mut version uses cell-based mutual recursion exactly as the
top-level version.
Graph BFS using Queue + Hashtbl visited-set + List.assoc_opt + List.iter.
Returns 6 for a graph where A reaches B/C/D/E/F. Demonstrates 4 stdlib
modules (Queue, Hashtbl, List) cooperating in a real algorithm.
let NAME [PARAMS] : T = expr and (expr : T) parse and skip the type
source. Runtime no-op since SX is dynamic. Works in inline let,
top-level let, and parenthesised expressions:
let x : int = 5 ;; x + 1 -> 6
let f (x : int) : int = x + 1 in f 41 -> 42
(5 : int) -> 5
((1 + 2) : int) * 3 -> 9
Parser: in parse-decl-type, dispatch on the post-= token:
'|' or Ctor -> sum type
'{' -> record type
otherwise -> type alias (skip to boundary)
AST (:type-alias NAME PARAMS) with body discarded. Runtime no-op since
SX has no nominal types.
poly_stack.ml baseline exercises:
module type ELEMENT = sig type t val show : t -> string end
module IntElem = struct type t = int let show x = ... end
module Make (E : ELEMENT) = struct ... use E.show ... end
module IntStack = Make(IntElem)
Demonstrates the substrate handles signature decls + abstract types +
functor parameter with sig constraint.
parse-try now consumes optional 'when GUARD-EXPR' before -> and emits
(:case-when PAT GUARD BODY). Eval try clause loop dispatches on case /
case-when and falls through on guard false — same semantics as match.
Examples:
try raise (E 5) with | E n when n > 0 -> n | _ -> 0 = 5
try raise (E (-3)) with | E n when n > 0 -> n | _ -> 0 = 0
try raise (E 5) with | E n when n > 100 -> n | E n -> n + 1000 = 1005
parse-function now consumes optional 'when GUARD-EXPR' before -> and
emits (:case-when PAT GUARD BODY) — same handling as match clauses.
function-style sign extraction now works:
(function | n when n > 0 -> 1 | n when n < 0 -> -1 | _ -> 0)
Group anagrams by canonical (sorted-chars) key using Hashtbl +
List.sort. Demonstrates char-by-char traversal via String.get + for-loop +
ref accumulator + Hashtbl as a multi-valued counter.
Untyped lambda calculus interpreter inside OCaml-on-SX:
type term = Var | Abs of string * term | App | Num of int
type value = VNum of int | VClos of string * term * env
let rec eval env t = match t with ...
(\x.\y.x) 7 99 = 7. The substrate handles two ADTs, recursive eval,
closure-based env, and pattern matching all written as a single
self-contained OCaml program — strong validation.
ocaml-type-of-program now handles :def-mut (sequential generalize) and
:def-rec-mut (pre-bind tvs, infer rhs, unify, generalize all, infer
body — same algorithm as the inline let-rec-mut version).
Mutual top-level recursion now type-checks:
let rec even n = ... and odd n = ...;; even 10 : Bool
let rec map f xs = ... and length lst = ...;; map :
('a -> 'b) -> 'a list -> 'b list
Memoized fibonacci using Hashtbl.find_opt + Hashtbl.add.
fib(25) = 75025. Demonstrates mutable Hashtbl through the OCaml
stdlib API in real recursive code.
4-queens via recursive backtracking + List.fold_left. Returns 2 (the
two solutions of 4-queens). Per-program timeout in run.sh bumped to
240s — the tree-walking interpreter is slow on heavy recursion but
correct.
The substrate handles full backtracking + safe-check recursion +
list-driven candidate enumeration end-to-end.
Counter-style record with two mutable fields. Validates the new
r.f <- v field mutation end-to-end through type decl + record literal
+ field access + field assignment + sequence operator.
type counter = { mutable count : int; mutable last : int }
let bump c = c.count <- c.count + 1 ; c.last <- c.count
After 5 bumps: count=5, last=5, sum=10.
<- added to op-table at level 1 (same as :=). Eval short-circuits on
<- to mutate the lhs's field via host SX dict-set!. The lhs must be a
:field expression; otherwise raises.
Tested:
let r = { x = 1; y = 2 } in r.x <- 5; r.x (5)
let r = { x = 0 } in for i = 1 to 5 do r.x <- r.x + i done; r.x (15)
let r = { name = ...; age = 30 } in r.name <- "Alice"; r.name
The 'mutable' keyword in record type decls is parsed-and-discarded;
runtime semantics: every field is mutable. Phase 2 closes this gap
without changing the dict-based record representation.
type r = { x : int; mutable y : string } parses to
(:type-def-record NAME PARAMS FIELDS) with FIELDS each (NAME) or
(:mutable NAME). Parser dispatches on { after = to parse field list.
Field-type sources are skipped (HM registration TBD). Runtime no-op
since records already work as dynamic dicts.