Repository navigation
Description
Summary
The Semantic Patch v2 preflight builds a source/HIR identity index over declarations and over the contracts and bodies of top-level functions. Its walker has no arms for while statements, method calls, i32 literals, or literal, or-pattern and binding match patterns. In the positions reproduced below, these valid constructs make a rename or rename-member patch fail with SPX-G108 semantic patch source/HIR identity index is inconsistent, even when the construct is unrelated to the renamed declaration. The committed examples/refutable_match.spx and examples/integers_i32.spx fail too. All cases end in the _ => return None fallthroughs of one walker, so they are reported together.
Environment
- Repository: github.com/wavect/semaprax at commit 2d215d3.
- Binary:
target/debug/semaprax, sha25616542ba925d65ead8e1b3c7e070f7e2abac39fd3479dbf78c0786435d287ae72, tested with a clean checkout at the commit above;--versionprintssemaprax 0.9.0 (commit unknown). Every command below used this binary. - Host: macOS 27.2 arm64 (Darwin 27.2.0).
- Toolchain: rustc 1.98.1 (48a229cea 2026-09-01); cargo 1.98.1 (797e8a9bc 2026-08-05).
Fixtures
loop.spx has an unrelated while loop in main:
module patch.member;
@id("patch.box")
record Box {
@id("patch.box.value")
value: i64,
}
@id("app.main")
fn main() -> i64
{
let b = Box { value: 1 };
let mut n = 0;
while n < 1 {
n = n + 1;
n < 1
}
b.value + n
}
control.spx is loop.spx without the let mut n line and the loop, with b.value as the last line of main.
method.spx has a method call in main:
module demo;
@id("demo.counter")
class Counter {
@id("demo.counter.start")
start: i64,
@id("demo.counter.get")
fn get(self: Counter) -> i64
{
41
}
}
@id("demo.helper")
fn helper() -> i64
{
42
}
@id("demo.main")
fn main() -> i64
{
let c = Counter { start: 1 };
c.get() + helper()
}
nocall.spx is method.spx with c.get() + helper() replaced by c.start + helper().
litpat.spx has a literal match pattern in helper:
module t.m;
@id("t.helper")
fn helper(v: i64) -> i64
{
match v { 0 => 0, _ => 1, }
}
@id("t.main")
fn main() -> i64
{
helper(1)
}
Its variants replace only the body of helper. All sources pass fmt --check and check with exit code 0.
Steps and observed output
Each .spatch file has three lines: schema semaprax.semantic-patch.v2, base sha256:<hash> with the hash that semaprax check prints for the source, and the operation. Each patch ran on a fresh copy of the source:
$ cat loop.spatch
schema semaprax.semantic-patch.v2
base sha256:21c8f85d1d6e833955d83a98f04a003a8680a40a3e167c7f7fb2248d2008a6b6
rename-member owner patch.box member patch.box.value to payload
$ semaprax patch loop.spx loop.spatch; echo rc=$?
error[SPX-G108]: semantic patch source/HIR identity index is inconsistent
rc=1
$ semaprax patch control.spx control.spatch; echo rc=$?
applied semantic patch; graph is now sha256:973e7027f8526100c1c74b9e761ce4f6d6569c61e88b970865abe6d1904ff774
rc=0
| Source | Base | Operation | Result |
|---|---|---|---|
control.spx |
28f5d65d2503b9ea441c3c960397f94e0781ccc701614333b866cd18c4ec2064 |
rename-member owner patch.box member patch.box.value to payload |
applied, rc=0 |
loop.spx |
21c8f85d1d6e833955d83a98f04a003a8680a40a3e167c7f7fb2248d2008a6b6 |
same | SPX-G108, rc=1 |
nocall.spx |
2c3bbbaef847fde3b6678a3d45277f9cfe4adbe850724f91f5f3c4e5867dc1b6 |
rename demo.helper to computed |
applied, rc=0 |
method.spx |
b8931ddb57fd255ad3935137facef98ad23399f55893a8d11df616a0c726a12a |
same | SPX-G108, rc=1 |
litpat.spx |
0ba3b8772b94a6f43d2a8cb3120dbde565c123419cdd19998aa624be01c2c40f |
rename t.helper to total |
SPX-G108, rc=1 |
helper body let a = 2i32; then v |
b85e50567ee1660b888adcdb0685427e2f58a9aee57346df906f57a51f1edfe5 |
same | SPX-G108, rc=1 |
helper body match v { 1 | 2 => 0, _ => 1, } |
5912ff3f29be0184e68bdeedf7c9fdba9f66dbb623f3f7b9a79e634d41bdcd44 |
same | SPX-G108, rc=1 |
helper body match v { n => n, } |
a5a31ba32b7ad2b67c2261c399639a0f3c7d545b63f8c5f3227e979f803170a4 |
same | SPX-G108, rc=1 |
helper body v + 1 |
c3ad43d0867c1e8d37c3623cbefb3142b36e99de1034823df44f5256535b47d4 |
same | applied, rc=0 |
examples/refutable_match.spx |
dc987de603a56afdbe1527107b272fea520901dff79ba0b3910cab372b885287 |
rename refutable.sign_class to sign_kind |
SPX-G108, rc=1 |
examples/integers_i32.spx |
643584f9a136582e4613e78411f595f95a8184cbdcde0343528d3cf507e1cbb9 |
rename sum.pair to pair_sum |
SPX-G108, rc=1 |
Every failing patch prints nothing on stdout, writes the error line to stderr, leaves the source byte-identical and creates no files. Every applied patch yields a source that passes check; nocall.spx still prints 43 under run. The examples trigger it at line 6 (literal, or and binding patterns; 2i32).
Each failing program differs from its control only in code the rename does not touch. SPX-G108 also occurs when the while loop or c.get() moves into a separate function that main calls; the c.start twin of the latter applies. A renamed reference inside the construct fails too: while n < b.value { n = n + 1; 0 } in main with the rename-member above gives SPX-G108, rc=1.
Expected behaviour
Each failing case applies like its control, with exit code 0. For loop.spx, the declaration, record-literal label and field read become payload; for method.spx, helper becomes computed at its declaration and call site.
Where in the code
src/patch.rs:1477-1484buildsSemanticSourceIndexand maps aNoneresult to SPX-G108.src/patch/source_index.rs:111-135starts traversal from every top-level function's contracts and body, not only from declarations the patch selects. It does not traverse class method bodies, and the match walker skips arm guards (src/patch/source_index.rs:449-451).src/patch/source_index.rs:347-370: block statements pair onlyLet,AssignandUnsafe; any other statement returnsNone.While(src/ast/statement.rs:42) andFor(line 48) have no arm.src/patch/source_index.rs:506-513: the expression match pairsInt,String,BorrowedUtf8Literal,BoolandVar, then returnsNone.ExprKind::Int32(src/ast.rs:929) andExprKind::MethodCall(src/ast.rs:958) are not named in the file (rg -nw 'MethodCall|Int32|While|For' src/patch/source_index.rsfinds nothing).src/patch/source_index.rs:533-571:pattern()pairs onlyVariant,RecordandWildcardand returnsNoneat line 569.MatchPattern::Literal,OrandBindingare declared atsrc/ast.rs:1063,1070and1075.docs/SEMANTIC-PATCH-V2.md:64lists SPX-G108 for a "source/HIR index mismatch" without naming these constructs.
Suggested fix
One possible fix is to extend SemanticSourceIndex for the demonstrated While, MethodCall, Int32 and literal, or-pattern and binding pattern cases. Method calls lower to Call with the receiver as an implicit first argument (src/hir/resolve_expr.rs:3050-3063), so the pairing must include that argument, and match guards need pairing too. The fail-closed None would stay wherever correspondence cannot be established. Regressions could cover unrelated renames and renamed references inside these constructs. For, ForBorrow and ForOwn probably need a separate audit: their lowering introduces synthesized HIR structure (src/hir/resolve_for.rs:588-665).
Related
- [REF-12][P2] Represent semantic patch operations once and derive planner views #524: refactor of semantic patch operation payloads; touches the same feature, not this walker.
- context fails with SPX-G218 (while statements) on examples/text_analytics.spx although check and graph succeed #39:
contextfailed with SPX-G218 onwhilestatements; the same kind of gap in another subsystem.
Not verified
forloops are not isolated. Afor ownfixture over aVecfails with SPX-G108, but so does its control without the loop.- Other scalar literal kinds and other v2 operations were not tested.
- Only the debug binary on macOS was used.
Written by an AI agent loop, lightly reviewed by a human. All commands were re-run at the commit above.
Activity