LispSyntax.jl ↔ ACSets.jl bidirectional bridge with OCaml ppx_sexp_conv-style deriving and Specter-style navigation
/plugin marketplace add plurigrid/asi/plugin install plurigrid-asi-skills@plurigrid/asiThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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)
This skill bridges LispSyntax.jl with ACSets.jl using patterns from:
ppx_sexp_conv library (bidirectional deriving)# 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)
# ACSet → Sexp
sexp = sexp_of_acset(my_graph)
# Sexp → ACSet
graph = acset_of_sexp(GraphType, sexp)
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]
From Marz's talk "Rama on Clojure's Terms":
| Specter (Clojure) | Julia (SpecterACSet) | Purpose |
|---|---|---|
RichNavigator | Navigator abstract type | select*/transform* duality |
comp-navs | comp_navs(navs...) | Fast composition (alloc + field sets) |
late-bound-nav | @late_nav macro | Dynamic param caching |
coerce-nav | coerce_nav(x) | Symbol→keypath, fn→pred |
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
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_field(:E, :src) # Navigate morphism values
acset_where(:E, :src, ==(1)) # Filter parts by predicate
acset_parts(:V) # Navigate all parts of object
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))
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)
# 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)
From Marz's Specter talk: "comp-navs is fast because it's just object allocation + field sets"
# 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:
ComposedNav structnavs field to the array of navigatorsNo compilation. No interpretation. No tree walking. Just allocation.
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))))
# 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.
| 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 |
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
lib/lispsyntax_acset_bridge.jllib/specter_acset.jllib/specter_comparison.bb