This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Install OCaml dependencies
opam install . --deps-only
# Build grammar libraries (requires npm)
cd grammars && ./build-grammars.sh && cd ..
# Build the project
dune build
# Run all tests
dune test
# Run specific test group (e.g., just the end-to-end Matcher tests)
dune exec tests/test_runner.exe -- test Matcher
# Run a single named test
dune exec tests/test_runner.exe -- test Matcher "find calls"diffract is an OCaml library and CLI for parsing source files with tree-sitter and pattern matching. Key capabilities:
- Parse source to S-expressions using tree-sitter grammars
- Pattern matching with concrete syntax and metavariables
- Change summaries (
summarize): infer the spatch rules behind a before/after directory pair — rules with per-file sites, tieredafter=rules over the leftovers, and per-file residual diffs for what no rule explains (docs/change-summary.mdfor usage,docs/change-summary-design.mdfor design)
Core Library (lib/)
diffract.ml- Main module, re-exports submodulestree.ml- Pure OCaml tree representation (eliminates FFI overhead during traversal)tree_sitter_bindings.ml- Low-level ctypes FFI bindingstree_sitter_helper.c- C helper layer wrapping TSNode in OCaml custom blocks (libffi can't handle 32-byte structs by value)node.ml- FFI-based tree traversal (internal, used during parsing)languages.ml- Static grammar registry (language name →externalC binding)
Matcher (lib/) — the tokenizer-based matcher (the only matcher):
tokenize.ml- Parse a pattern body with tree-sitter as a lexer, keeping leaves; produces apattern_tokenstream (sigil-free metavars, ellipsis, fragments)cursor.ml- Abstract tree-cursor interface (Cursor.S) the matching engine runs overtree_sitter_cursor.ml-Cursor.Sover a real tree-sitter parsestmatch.ml- The matching engine: strict/partial/field leaf-level matching with backtracking (Makefunctor over aCursor.S)matcher.ml- End-to-end: preamble parse → tokenize → match → transform; the publicfind/transform/debug_tokens/pattern_warningsAPItext_diff.ml- Line-based unified diff (forapply's output)
Diff / change summaries (lib/)
tree_diff.ml- AST-level diff (GumTree-style node mapping); used bydiffand assummarize's change-pair sourceleaf_metric.ml- Token-level edit metric over tree-sitter leaf streams (Myers LCS distance); the summary safety gate's geodesic testchange_summary.ml- Thin facade: re-exports the public surface (summarize,format_summary,load_from_dirs, types). Thesummarizepipeline (propose → evaluate → select, tiered) is split acrosscs_*.mlmodules, one per phase:cs_types.ml- shared types (public API + internal pattern representation)cs_config.ml- tuning constants (one documented home; internal, no CLI)cs_trace.ml- diagnostics gated on theCS_TRACEenv varcs_pattern.ml- tree→pat_node, rendering, anti-unification, coherence predicatescs_propose.ml- change-pair extraction + candidate channels (multi-level, content-extraction, delta-keyed, anchored lattice-descent)cs_cluster.ml- anti-unification dendrogram, orphan coarsening, one-sided clusteringcs_evaluate.ml- the per-site safety gate that defines a rule's meaning (§3.3)cs_fusion.ml- conjunctive multi-section fusion of co-occurring changescs_select.ml- one tier: propose → evaluate → greedy set-cover over changed regionscs_tier.ml- tiered loop, chain-effect accounting, residual emissioncs_io.ml-.summaryformatting and the directory-pair loader- Golden + round-trip tests in
tests/change_summary_cases/. Design:docs/change-summary-design.md(§6 Milestones is historical changelog; §1–§5 describe the current design).
Tree-sitter Integration Flow:
- C helper layer wraps TSNode/TSTree in OCaml custom blocks with finalizers
- ctypes-foreign binds to libtree-sitter and C helpers
languages.mldispatches to grammar language functions statically linked into the binarytree.mlconverts FFI nodes to pure OCaml representation once during parsing
Matching Flow: a pattern's preamble (@@ sections) is parsed, its body is
tokenized into a (text, node_type) leaf stream, and stmatch walks the
source tree (via a Cursor.S) matching those tokens — comparing leaves on
both text and node type. Metavars are sigil-free (a leaf is a metavar iff its
text equals a declared name). See docs/universal-tokenizer.md.
- Add C wrapper and
externalbinding tolib/tree_sitter_helper.candlib/languages.ml:
/* lib/tree_sitter_helper.c */
extern const TSLanguage *tree_sitter_ruby(void);
CAMLprim value tsh_ruby_language(value v_unit) {
CAMLparam1(v_unit); CAMLreturn(caml_copy_nativeint((intnat)tree_sitter_ruby())); }(* lib/languages.ml *)
external ruby_language : unit -> nativeint = "tsh_ruby_language"
(* add to canonical_info: ("ruby", [], ruby_language) *)- Update
grammars/build-grammars.shwith the compilation command:
npm install tree-sitter-ruby
cc -O2 -c -o "$TMPDIR_LOCAL/ruby_parser.o" \
-I node_modules/tree-sitter-ruby/src \
node_modules/tree-sitter-ruby/src/parser.c
cc -O2 -c -o "$TMPDIR_LOCAL/ruby_scanner.o" \
-I node_modules/tree-sitter-ruby/src \
node_modules/tree-sitter-ruby/src/scanner.c
ar rcs lib/libtree-sitter-ruby.a "$TMPDIR_LOCAL/ruby_parser.o" "$TMPDIR_LOCAL/ruby_scanner.o"- Add copy rule and
(foreign_archives ...)entry tolib/dune:
(rule (target libtree-sitter-ruby.a)
(deps ../grammars/lib/libtree-sitter-ruby.a)
(action (copy %{deps} %{target})))
And add tree-sitter-ruby to the (foreign_archives ...) list.
- Rebuild:
cd grammars && ./build-grammars.sh && cd .. && dune build
Patterns use @@ delimiters with a required match mode and optional metavariable declarations:
@@
match: strict
metavar $obj: single
metavar $method: single
@@
$obj.$method()
Types: single (one AST node), sequence (zero or more nodes)
Ellipsis (...) can be used as anonymous sequence matching:
@@
match: strict
@@
<?php
function test() {
...
echo "middle";
...
}
...matches zero or more nodes (like sequence metavars)- Auto-detects context: adds
;in statement position, not in argument position - Does NOT replace
...$var(PHP spread operator is preserved) - Each
...gets a unique binding name (..._0,..._1, etc.) - Sequence metavars (including
...) are not supported withmatch: partial. - In transform bodies
...belongs on context lines: it is rejected anywhere on a+line (a match-side binder has nothing to bind in a replacement) and as a bare-/+line. An inline...within a-line's expression binds normally and the captured run is deleted with it. Rewrite one list element by marking only it between context...lines; delete one by putting it (and its separator) on-lines between them.
Matching modes (required - must specify one):
match: strict- Exact positional matching (no extra children allowed, ordered). Use for function calls, arrays.match: partial- Subset matching (ignores extra children, unordered). Use for object literals, JSX attributes.match: field- Declaration matching that ignores optional fields the pattern omits (decorators, annotations, modifier groups, return types). The pattern's leaf stream is aligned to a subsequence of the declaration node's children: a child the pattern addresses is matched in full, a child it omits is skipped. Use for decorated/annotated definitions. (No per-language config — seedocs/field-mode.md.) Transforms are surgical (all modes): a-/+on a sub-part edits only that part and preserves the rest — context, partial's tolerated extras, field's ignored optional fields, and...-captured source (seedocs/surgical-transforms.md). Marking the whole container in partial/field mode (a body with no context line) still replaces it whole and drops those extras/fields; the CLI warns for that case only.
A pattern file with multiple @@ sections and no on $VAR directives is a
conjunctive rule: every section must find at least one match for any transforms
to fire. If any section finds nothing, the source is returned unchanged.
Metavars of the same name across sections refer to the same binding (threaded
in declaration order); a section without -/+ lines acts as a pure guard
(must match but produces no edits). Matcher.find/Matcher.transform handle
multi-section patterns directly.
When a sequence metavar is referenced inside a + replacement template, its
elements are rendered and substituted in place. Two independent knobs control
this:
join $VAR by "<sep>"(a preamble directive): the string placed between rendered elements.<sep>interprets\n,\t,\\. The default (no directive) is the empty string, so elements are concatenated.foreach $VAR(a following@@section): a per-element transform. The section matches one element of$VARand its-/+lines rewrite it; the rewritten elements are then joined. Without aforeach, elements render as their source text (identity).
The sequence metavar must be declared metavar $VAR: sequence and appear on the
match side. Everything goes through Matcher.transform.
@@
match: strict
metavar $ELEMS: sequence
join $ELEMS by " && "
@@
- all([$ELEMS])
+ ($ELEMS)
all([x, y, z]) becomes (x && y && z).
@@
match: strict
metavar $TAG: single
metavar $PROPS: sequence
@@
- matchExhaustive($TAG, { $PROPS });
+ match($TAG)$PROPS.exhaustive();
@@
match: strict
foreach $PROPS
metavar $KEY: single
metavar $VAL: single
@@
- $KEY: $VAL
+ .with("$KEY", $VAL)
matchExhaustive(tag, { a: f, b: g }); becomes
match(tag).with("a", f).with("b", g).exhaustive(); — the foreach section
rewrites each property, and $PROPS in the outer template is replaced by the
joined results (default empty join here).
A foreach element with an empty replacement (a - line, no +) deletes the
element and cleans up the adjacent separator — e.g. removing a deprecated
property or unused argument from a list.