aboutsummaryrefslogtreecommitdiff
path: root/tests/CLAUDE SUGGESTIONS.org
diff options
context:
space:
mode:
Diffstat (limited to 'tests/CLAUDE SUGGESTIONS.org')
-rw-r--r--tests/CLAUDE SUGGESTIONS.org179
1 files changed, 179 insertions, 0 deletions
diff --git a/tests/CLAUDE SUGGESTIONS.org b/tests/CLAUDE SUGGESTIONS.org
new file mode 100644
index 0000000..f37c7f9
--- /dev/null
+++ b/tests/CLAUDE SUGGESTIONS.org
@@ -0,0 +1,179 @@
+#+TITLE: Claude's suggestions on forest construction
+#+DATE: 2026-10-12
+#+STARTUP: overview
+
+Written by Claude Code at the user's request, for later reference.
+Nothing here has been applied to the source. The only code written is
+=tests/spike.rs=, which holds exploratory probes.
+
+Status labels used below:
+- MEASURED: observed by running the probes in =tests/spike.rs=.
+- INFERRED: concluded from reading code, not directly observed.
+- OPINION: a judgement about design, not verified.
+
+* Where we are
+** Harness facts
+- A root-crate test can use =chain=, =grammar=, =graph=, =forest=, =nfa=.
+ (=nfa= and the =test-helper= feature were added to the root
+ =[dev-dependencies]= by the user.)
+- Grammars are built with =Terminal::new=, =Nonterminal::new=,
+ =Rule::new= and =DefaultRegParser= + =scan_tnt=. Names in the rule
+ strings are letters only, e.g. =Ta=, =NS=. An empty alternative
+ (="... | "=) is epsilon.
+- The parser's debug dumps write to a relative =output/= directory.
+ The tests change into =$TMPDIR/rep-tests= (with =output/= inside) once,
+ so nothing is written to the source tree.
+- Call sequence: =DefaultAtom::from_grammar= -> =DefaultChain::unit= ->
+ =chain(tok, pos, false)= per token -> =end_of_input(n, last_tok)=.
+- The END_OF_INPUT token is NOT a convention of this project.
+- Empty input is deferred to the epsilon layer: the =ter= argument of
+ =end_of_input= selects virtual fragments keyed by (nt, ter)
+ (=chain/src/atom/default.rs:489-549=, =chain/src/item/genins.rs:730=), so
+ an arbitrary terminal is not safe for an empty input.
+
+** First failure found (MEASURED)
+Smallest failing case: =S -> S a | a= on =aa=. =chain()= returns
+=Err(CannotPlant)= at the second token (pos 1). Case 1
+(=S -> S S | a=) fails the same way on =aa=.
+
+| Grammar | Forest building on | Recognition only |
+|------------------------------------+-----------------------------+------------------|
+| =S -> a S \| a= (right recursion) | n = 1..4 pass | accepted |
+| =S -> a a= | n = 1..4 pass | accepted |
+| =S -> S a \| a= (left recursion) | n >= 2 fails at pos 1 | accepted, n=1..4 |
+| =S -> S S \| a= (Case 1) | n >= 2 fails at pos 1 | accepted, n=1..4 |
+
+So recognition is fine; the failure is in forest construction.
+Left recursion is listed in layer 4 of CLAUDE.md, but it already fails
+at two tokens without ambiguity or epsilon. Suggestion: stop at this
+failure instead of building the oracle first.
+
+** Where the failure surfaces
+=insert_item= in =chain/src/item/genins.rs=. The walk up the parents
+ends with an empty =stack= and returns =Error::CannotPlant= (line 551).
+The dump at the failure shows =parents = []= and
+=forest_source = true_source = Virtual(0, 0, 0)=.
+
+* How =insert_item= works
+Inputs: an NFA =Edge= (with =forest_source= and =true_source=), a small
+=fragment= (a chain of nodes from a nonterminal through a rule node
+down to a terminal leaf), and =atom_child_iter= (the NFA nodes the step
+moves to). A =PaVi= says where the edge's source sits in the forest:
+- =Parent(node, edge, child)=: under a real forest node;
+- =Virtual(nt, t, node)=: the nonterminal =node= is only implied by the
+ collapsed left-closure edge, its expansion down to =t= is not built;
+- =Empty=: at the root.
+
+Steps (line numbers in =genins.rs=):
+1. 306-316: if =true_source= is virtual, close it by planting the stored
+ virtual fragment for (nt, t) (=close_pavi=, line 686).
+2. 318-375: choose the initial =parents= (rule nodes to hang from).
+ - =Parent= gives that node; =Empty= gives the root.
+ - =Virtual=: for each =atom_child=, =atom.trace(nt, t, atom_child)=
+ gives rule numbers; for each rule fetch
+ =generate_virtual_frags(nt, t, Some(rule))=, plant it at the start of
+ =node= and add its second-to-last node as a parent.
+ =parents= is only ever filled here.
+3. 377-395: for a =Parent= source run =reduction()= on the child; if it
+ produced a different node, replace =parents= by its parents.
+4. 403-420: lift each parent from a rule node to the nonterminal nodes
+ above it.
+5. 428-552: for each =atom_child=, =query_reduction= gives the
+ nonterminals this step completes, innermost first. For each, find a
+ stack node with that label, close it (=splone= sets its end to =pos=,
+ splits off clones) and move to the grandparent nonterminals. An
+ empty stack here means "nowhere to plant": =CannotPlant=.
+6. 558-673: =splant= (splone with an open end, then plant the fragment)
+ under each remaining node, and return the =PaVi= of the planted
+ terminal (or a =Virtual= one for a single-terminal fragment).
+
+* Why =parents= is empty (partly INFERRED)
+- MEASURED: =atom.trace(S, a, 7)= is =[5]= for =S -> S a | a=; node 7 is
+ the only target with a trace. Rule positions: 5 is
+ "(NSTa|Ta . )", the end of the second alternative.
+- MEASURED: =query_expansion(0, 5)= for the terminal edge from the
+ nonterminal start is =[(0,2), (0,4), (0,4), (0,4), (0,4)]=.
+- INFERRED: virtual fragments are stored under =line[len-2]=, the first
+ rule of that expansion, which is 2 (=chain/src/atom/default.rs:502,546-549=).
+ Then =generate_virtual_frags(0, 0, Some(5))= finds nothing, nothing is
+ planted, =parents= stays empty and the walk has nothing to pop.
+- To confirm: print the key set of =virtual_frags.get(&VirtualNode(0,0))=
+ just after line 549 of =atom/default.rs=, or make
+ =generate_virtual_frags= temporarily =pub= and call it from the test.
+- The =dbg!= at =genins.rs:538= calls =atom.trace(0, 3, ...)= with
+ hard-coded arguments, so its empty =[]= is meaningless.
+
+* Algorithmic assessment (OPINION; the paper has not been consulted)
+The outline (expand / push / reduce, walk up completed nonterminals,
+plant) is the standard completer and is a reasonable shape. The
+weak points:
+1. The machine discards what the forest needs. =chain/src/item/reduction.rs=
+ header: after the nullable closure a reduction edge looks like a
+ normal push. The left closure collapses =S => S a => ...= into one
+ edge in the same way.
+2. The three fixes tried: complete at the end (failed), record skipped
+ nodes (failed because nodes are split and cloned), "guided
+ brute-force" (perform all possible reductions and rely on =extract=
+ to prune). The third makes correctness depend on exact pruning,
+ with no stated invariant. This could explain missing and extra trees.
+3. =Edge= carries =forest_source= / =true_source= as =PaVi::Parent(node, ...)=
+ node numbers, while nodes are split and cloned. Not checked how these
+ are updated. =query_label(...).expect("the forest was wrongly
+ planted")= (e.g. =genins.rs:361=) finds nodes by label, assuming labels
+ are unique.
+4. Three tables must agree: =virtual_traces=, =virtual_frags=, reduction
+ info. They use different notions of "rule" (trace 5 against key 2).
+ Nothing checks this.
+5. Virtual fragments have a fixed depth, but left recursion is
+ unbounded. The four-times repeated =(0,4)= looks unrolled. The
+ fragment for (S, a) should arguably be just the base step =S -> a=,
+ with each later token adding one =S -> S a= level through reduction.
+6. =insert_item= places virtual expansions, reduces, walks up and
+ plants in one function on a live mutable graph where =None= ends mean
+ "in progress". Failures are hard to localise.
+7. Two different lookups serve the same virtual source: =close_pavi=
+ asks for the accepting fragments (=None= rule) at step 1, while step 2
+ asks by trace rule. Whether they plant twice or collide is unchecked.
+8. The code flags its own doubts: =REVIEW: is this really correct?=
+ (=genins.rs:707=), =NOTE: ... holds in this case, but not in general=
+ (=:345=), =panic!("assumption fails")= (=:287=, =:509=).
+9. Possible nondeterminism: the edges from a nonterminal start are
+ collected into a =HashMap= (=atom/default.rs:448-452=). The
+ =S -> S S | a= dump showed =atom_child = 11= in one run and =7= in
+ another. Not investigated.
+
+* Suggested next steps
+Ordered; the user decides.
+1. Confirm the trace/key mismatch (see "Why =parents= is empty").
+2. Unit test in the atom: for every (nt, t, target) with a trace,
+ every traced rule has a matching fragment key. Cheap, and it would
+ flag mismatches directly, for left recursion and for other grammars.
+3. Define a mid-parse invariant and assert it in debug builds after each
+ token. For example: every =forest_source= / =true_source= points at a
+ node of the expected kind, and every pending forest node has
+ start <= pos. This is =validate_forest= applied per token.
+4. Check determinism: run =S -> S S | a= on =aa= many times and compare
+ which =atom_child= fails.
+5. Once left recursion and Case 1 get through =chain()=, resume the
+ original plan from CLAUDE.md: oracle, =unpack=, diff harness,
+ =validate_forest=, then the layers in order.
+6. Ask before consulting the paper. Possible question: how does the
+ paper build the forest without splitting nodes destructively? Read
+ only the forest-construction section.
+
+* Known typo in the reference file
+=forest-examples.md= Case 1 says "5 packed alternatives in total".
+The forest listed has 7 (2 + 1 + 1 + 1 + 1 + 1). The user has
+confirmed this is a typo. The file is untouched.
+
+* Probes in =tests/spike.rs=
+| Test | Purpose |
+|--------------------------------+-------------------------------------------------|
+| =locate_failure_case1= | at which token =chain()= first fails, Case 1 |
+| =layer1_sanity= | right recursion, left recursion, fixed length |
+| =recognition_only= | same inputs with forest building off |
+| =probe_atom_left_recursion= | prints traces, expansions and NFA edges |
+| =dump_case1= | planned forest dump; currently fails in =chain()= |
+
+Run, for example: =cargo test --test spike probe -- --nocapture=.
+Probe output is prefixed with =PROBE=, =RESULT=, =LAYER1= or =RECOG=.