Asi lispsyntax-acset
LispSyntax.jl ↔ ACSets.jl bidirectional bridge with OCaml ppx_sexp_conv-style
git clone https://github.com/plurigrid/asi
T=$(mktemp -d) && git clone --depth=1 https://github.com/plurigrid/asi "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/asi/skills/lispsyntax-acset" ~/.claude/skills/plurigrid-asi-lispsyntax-acset-6f101a && rm -rf "$T"
plugins/asi/skills/lispsyntax-acset/SKILL.mdlispsyntax-acset
Bidirectional S-expression ↔ ACSet conversion with Specter-style inline caching
Version: 1.2.0 Trit: 0 (Ergodic - coordinates data serialization) Dynamic Sufficiency: ✅ VERIFIED (2025-12-22)
Overview
This skill bridges LispSyntax.jl with ACSets.jl using patterns from:
- OCaml's
library (bidirectional deriving)ppx_sexp_conv - Clojure Specter's inline caching and CPS (Nathan Marz)
Core Capabilities
1. S-expression Parsing & Serialization
# String → Sexp (like OCaml's Sexp.of_string) sexp = parse_sexp("(define (square x) (* x x))") # Sexp → String (like OCaml's Sexp.to_string) str = to_string(sexp)
2. ACSet Conversion (ppx_sexp_conv pattern)
# ACSet → Sexp sexp = sexp_of_acset(my_graph) # Sexp → ACSet graph = acset_of_sexp(GraphType, sexp)
3. Specter-Style Bidirectional Navigation
Key insight from Specter: Same path expression works for both
select AND transform:
# Same path for selection and transformation path = [ALL, pred(iseven)] # Select: collect matching values select(path, [1,2,3,4,5]) # → [2, 4] # Transform: modify matching values in-place transform(path, x -> x*10, [1,2,3,4,5]) # → [1, 20, 3, 40, 5]
Specter Navigator Protocol
From Marz's talk "Rama on Clojure's Terms":
| Specter (Clojure) | Julia (SpecterACSet) | Purpose |
|---|---|---|
| abstract type | select*/transform* duality |
| | Fast composition (alloc + field sets) |
| macro | Dynamic param caching |
| | Symbol→keypath, fn→pred |
Primitive Navigators
ALL # Navigate to every element FIRST # Navigate to first element LAST # Navigate to last element keypath(k) # Navigate to key in map/dict pred(f) # Filter by predicate
S-expression Navigators (Unique to Julia)
SEXP_HEAD # Navigate to first child (head of list) SEXP_TAIL # Navigate to rest of children SEXP_CHILDREN # Navigate to children as vector SEXP_WALK # Recursive descent (prewalk) sexp_nth(n) # Navigate to nth child ATOM_VALUE # Navigate to atom's string value
ACSet Navigators (Unique to Julia)
acset_field(:E, :src) # Navigate morphism values acset_where(:E, :src, ==(1)) # Filter parts by predicate acset_parts(:V) # Navigate all parts of object
Bidirectional Examples
S-expression Transform
sexp = parse_sexp("(define (square x) (* x x))") # Uppercase all atoms transformed = nav_transform(SEXP_WALK, sexp, s -> s isa Atom ? Atom(uppercase(s.value)) : s) # → (DEFINE (SQUARE X) (* X X)) # Rename function: change 'square' to 'cube' renamed = transform([sexp_nth(2), sexp_nth(1), ATOM_VALUE], _ -> "cube", sexp) # → (define (cube x) (* x x))
ACSet Navigation
g = @acset Graph begin V=4; E=3; src=[1,2,3]; tgt=[2,3,4] end # Select all source vertices select([acset_field(:E, :src)], g) # → [1, 2, 3] # Transform: shift all targets (mod 4) g2 = transform([acset_field(:E, :tgt)], t -> mod1(t+1, 4), g)
Cross-Domain Bridge
# ACSet → Sexp → Navigate → Transform → Sexp → ACSet graph = @acset Graph begin V=4; E=3; src=[1,2,3]; tgt=[2,3,4] end sexp = sexp_of_acset(graph) # Navigate sexp to find all morphism names morphism_names = select([SEXP_CHILDREN, sexp_nth(1), ATOM_VALUE], sexp) # Roundtrip back to ACSet graph2 = acset_of_sexp(Graph, sexp)
Performance: Inline Caching (comp-navs = alloc + field sets)
From Marz's Specter talk: "comp-navs is fast because it's just object allocation + field sets"
What This Means
# Traditional approach: work at composition time compose(a, b, c) → [compile] → [optimize] → CompiledPath # SLOW # Specter approach: zero work at composition comp_navs(a, b, c) → ComposedNav{navs: [a, b, c]} # Just allocate!
The
comp_navs function does exactly two things:
- Allocate a
structComposedNav - Set its
field to the array of navigatorsnavs
No compilation. No interpretation. No tree walking. Just allocation.
Why This Works: CPS (Continuation-Passing Style)
All actual work happens at traversal time via chained continuations:
# When you call select(), it builds a chain: nav_select(first_nav, data, result1 -> nav_select(second_nav, result1, result2 -> nav_select(third_nav, result2, final_result -> collect(final_result))))
Inline Caching at Callsite
# At each callsite, path compiled ONCE: @compiled_select([ALL, pred(iseven)], data) # Internally: let cached = @__MODULE__.CACHE[callsite_id] if cached === nothing cached = comp_navs(ALL, pred(iseven)) # Once! end nav_select(cached, data, identity) end
Result: Near-hand-written performance with full abstraction.
GF(3) Triads
| Triad | Role |
|---|---|
| slime-lisp (-1) ⊗ lispsyntax-acset (0) ⊗ cider-clojure (+1) | Sexp Serialization |
| three-match (-1) ⊗ lispsyntax-acset (0) ⊗ gay-mcp (+1) | Colored Sexp |
| polyglot-spi (-1) ⊗ lispsyntax-acset (0) ⊗ geiser-chicken (+1) | Scheme Bridge |
Catlab API Notes
The
homs(schema) API returns tuples (name, dom, codom):
# Correct usage (post-Catlab update) for hom_tuple in homs(schema) hom_name = hom_tuple[1] # First element is name dom_ob = hom_tuple[2] # Second is domain codom_ob = hom_tuple[3] # Third is codomain end
Files
- Bridge:
lib/lispsyntax_acset_bridge.jl - Specter navigators:
lib/specter_acset.jl - Comparison:
lib/specter_comparison.bb
References
- Specter - Nathan Marz
- ppx_sexp_conv - Jane Street
- LispSyntax.jl
- ACSets.jl
Scientific Skill Interleaving
This skill connects to the K-Dense-AI/claude-scientific-skills ecosystem:
Annotated Data
- anndata [○] via bicomodule
- Hub for annotated matrices
Bibliography References
: 734 citations in bib.duckdbgeneral
Cat# Integration
This skill maps to Cat# = Comod(P) as a bicomodule in the equipment structure:
Trit: 0 (ERGODIC) Home: Prof Poly Op: ⊗ Kan Role: Adj Color: #26D826
GF(3) Naturality
The skill participates in triads satisfying:
(-1) + (0) + (+1) ≡ 0 (mod 3)
This ensures compositional coherence in the Cat# equipment structure.