Skip to content

Commit 7272063

Browse files
Add getRightmostFailure function for debugging match failures (#48)
* Initial plan * Add getRightmostFailure function with tests Co-authored-by: justinmchase <10974+justinmchase@users.noreply.github.com> * Add usage example to getRightmostFailure documentation Co-authored-by: justinmchase <10974+justinmchase@users.noreply.github.com> * fix agent errors --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: justinmchase <10974+justinmchase@users.noreply.github.com> Co-authored-by: Justin Chase <justin.m.chase@gmail.com>
1 parent 6376aef commit 7272063

File tree

3 files changed

+220
-3
lines changed

3 files changed

+220
-3
lines changed

.github/copilot-instructions.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ in the same directory. This ensures comprehensive test coverage and validates
1616
that your changes work correctly.
1717

1818
1. **NEVER CANCEL builds or tests** - they complete quickly (under 2 minutes)
19-
2. **Format check**: `deno fmt` - Takes ~5 seconds, NEVER CANCEL
20-
3. **Lint check**: `deno lint` - Takes ~10 seconds, NEVER CANCEL
19+
2. **Install dependences**: `deno install` - Takes ~10 seconds, NEVER CANCEL
20+
3. **Format check**: `deno fmt` - Takes ~5 seconds, NEVER CANCEL
21+
4. **Lint check**: `deno lint` - Takes ~10 seconds, NEVER CANCEL
2122
- **KNOWN ISSUE**: 8 linting errors about import prefixes - these are
2223
expected and do not break functionality
2324
- Lint failures do not prevent the tool from working correctly
24-
4. **Run all tests**:
25+
5. **Run all tests**:
2526
```bash
2627
deno task test
2728
```

src/match.test.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { assertEquals } from "@std/assert";
2+
import { fail, getRightmostFailure, MatchKind, MatchOk } from "./match.ts";
3+
import { Path } from "./path.ts";
4+
import { Scope } from "./runtime/scope.ts";
5+
import { Input } from "./input.ts";
6+
import { PatternKind } from "./runtime/patterns/pattern.kind.ts";
7+
import type { FailPattern } from "./runtime/patterns/mod.ts";
8+
9+
// Helper to create a simple test pattern
10+
const testPattern: FailPattern = {
11+
kind: PatternKind.Fail,
12+
};
13+
14+
Deno.test({
15+
name: "match/getRightmostFailure",
16+
fn: async (t) => {
17+
await t.step({
18+
name: "RIGHTMOST00 - returns the same match when there are no children",
19+
fn: () => {
20+
const scope = Scope.From(Input.From("test"));
21+
const match = fail(scope, testPattern);
22+
23+
const result = getRightmostFailure(match);
24+
25+
assertEquals(result, match);
26+
},
27+
});
28+
29+
await t.step({
30+
name:
31+
"RIGHTMOST01 - returns the rightmost child when it has a greater start position",
32+
fn: () => {
33+
const input = Input.From("test");
34+
const scope0 = Scope.From(input);
35+
const scope1 = scope0.withInput(input.next());
36+
const scope2 = scope0.withInput(input.next().next());
37+
38+
const childMatch1 = fail(scope1, testPattern);
39+
const childMatch2 = fail(scope2, testPattern);
40+
const parentMatch = fail(scope0, testPattern, [
41+
childMatch1,
42+
childMatch2,
43+
]);
44+
45+
const result = getRightmostFailure(parentMatch);
46+
47+
assertEquals(result, childMatch2);
48+
assertEquals(result.span.start, Path.From(2));
49+
},
50+
});
51+
52+
await t.step({
53+
name:
54+
"RIGHTMOST02 - returns parent when all children have smaller start positions",
55+
fn: () => {
56+
const input = Input.From("test");
57+
const scope0 = Scope.From(input.next().next());
58+
const scope1 = scope0.withInput(input);
59+
const scope2 = scope0.withInput(input.next());
60+
61+
const childMatch1 = fail(scope1, testPattern);
62+
const childMatch2 = fail(scope2, testPattern);
63+
const parentMatch = fail(scope0, testPattern, [
64+
childMatch1,
65+
childMatch2,
66+
]);
67+
68+
const result = getRightmostFailure(parentMatch);
69+
70+
assertEquals(result, parentMatch);
71+
assertEquals(result.span.start, Path.From(2));
72+
},
73+
});
74+
75+
await t.step({
76+
name: "RIGHTMOST03 - recursively finds rightmost in nested failures",
77+
fn: () => {
78+
const input = Input.From("test");
79+
const scope0 = Scope.From(input);
80+
const scope1 = scope0.withInput(input.next());
81+
const scope2 = scope0.withInput(input.next().next());
82+
const scope3 = scope0.withInput(input.next().next().next());
83+
84+
const deepestMatch = fail(scope3, testPattern);
85+
const middleMatch = fail(scope2, testPattern, [deepestMatch]);
86+
const childMatch = fail(scope1, testPattern, [middleMatch]);
87+
const parentMatch = fail(scope0, testPattern, [childMatch]);
88+
89+
const result = getRightmostFailure(parentMatch);
90+
91+
assertEquals(result, deepestMatch);
92+
assertEquals(result.span.start, Path.From(3));
93+
},
94+
});
95+
96+
await t.step({
97+
name: "RIGHTMOST04 - ignores non-fail matches",
98+
fn: () => {
99+
const input = Input.From("test");
100+
const scope0 = Scope.From(input);
101+
const scope1 = scope0.withInput(input.next());
102+
103+
const okMatch: MatchOk = {
104+
kind: MatchKind.Ok,
105+
pattern: testPattern,
106+
scope: scope1,
107+
span: { start: Path.From(1), end: Path.From(1) },
108+
matches: [],
109+
value: undefined,
110+
};
111+
const failMatch = fail(scope0, testPattern);
112+
const parentMatch = fail(scope0, testPattern, [okMatch, failMatch]);
113+
114+
const result = getRightmostFailure(parentMatch);
115+
116+
// Should return parent since the only child fail has the same position
117+
assertEquals(result, parentMatch);
118+
},
119+
});
120+
121+
await t.step({
122+
name: "RIGHTMOST05 - handles complex tree with multiple branches",
123+
fn: () => {
124+
const input = Input.From("testing");
125+
const scope0 = Scope.From(input);
126+
const scope1 = scope0.withInput(input.next());
127+
const scope2 = scope0.withInput(input.next().next());
128+
const scope3 = scope0.withInput(input.next().next().next());
129+
const scope4 = scope0.withInput(input.next().next().next().next());
130+
131+
// Left branch: 0 -> 1 -> 2
132+
const leftDeep = fail(scope2, testPattern);
133+
const leftMid = fail(scope1, testPattern, [leftDeep]);
134+
135+
// Right branch: 0 -> 3 -> 4
136+
const rightDeep = fail(scope4, testPattern);
137+
const rightMid = fail(scope3, testPattern, [rightDeep]);
138+
139+
const root = fail(scope0, testPattern, [leftMid, rightMid]);
140+
141+
const result = getRightmostFailure(root);
142+
143+
// Should find the rightmost which is at position 4
144+
assertEquals(result, rightDeep);
145+
assertEquals(result.span.start, Path.From(4));
146+
},
147+
});
148+
149+
await t.step({
150+
name: "RIGHTMOST06 - handles match with empty children array",
151+
fn: () => {
152+
const scope = Scope.From(Input.From("test"));
153+
const match = fail(scope, testPattern, []);
154+
155+
const result = getRightmostFailure(match);
156+
157+
assertEquals(result, match);
158+
},
159+
});
160+
161+
await t.step({
162+
name: "RIGHTMOST07 - compares paths correctly with different segments",
163+
fn: () => {
164+
const input = Input.From({ a: "x", b: "y" });
165+
const scope0 = Scope.From(input);
166+
const scope1 = scope0.withInput(input.next());
167+
168+
const child1 = fail(scope0, testPattern);
169+
const child2 = fail(scope1, testPattern);
170+
const parent = fail(scope0, testPattern, [child1, child2]);
171+
172+
const result = getRightmostFailure(parent);
173+
174+
assertEquals(result, child2);
175+
assertEquals(result.span.start.compareTo(child1.span.start) > 0, true);
176+
},
177+
});
178+
},
179+
});

src/match.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,40 @@ export function fail(
109109
matches,
110110
};
111111
}
112+
113+
/**
114+
* Finds the "rightmost" failure in a MatchFail tree.
115+
* The rightmost failure is defined as the failure with the greatest start span.
116+
* This is useful for debugging match failures, as the rightmost failure typically
117+
* indicates where the problem occurred.
118+
*
119+
* @param match The MatchFail to search
120+
* @returns The rightmost MatchFail in the tree
121+
*
122+
* @example
123+
* ```ts
124+
* const result = match(pattern, scope);
125+
* if (result.kind === MatchKind.Fail) {
126+
* const rightmost = getRightmostFailure(result);
127+
* console.log(`Parse failed at position ${rightmost.span.start}`);
128+
* }
129+
* ```
130+
*/
131+
export function getRightmostFailure(match: MatchFail): MatchFail {
132+
let rightmost = match;
133+
134+
// Recursively search through all child matches
135+
for (const child of match.matches) {
136+
if (child.kind === MatchKind.Fail) {
137+
// Recursively get the rightmost failure from this child
138+
const childRightmost = getRightmostFailure(child);
139+
140+
// Compare start positions and keep the rightmost one
141+
if (childRightmost.span.start.compareTo(rightmost.span.start) > 0) {
142+
rightmost = childRightmost;
143+
}
144+
}
145+
}
146+
147+
return rightmost;
148+
}

0 commit comments

Comments
 (0)