feat!: Implement error handling for modified variables outside modifiers#1712
Conversation
|
| Branch | na/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block |
| Testbed | Linux |
Click to view all benchmark results
| Benchmark | hugr_bytes | Benchmark Result bytes x 1e3 (Result Δ%) | Upper Boundary bytes x 1e3 (Limit %) | hugr_nodes | Benchmark Result nodes (Result Δ%) | Upper Boundary nodes (Limit %) |
|---|---|---|---|---|---|---|
| tests/benchmarks/test_big_array.py::test_big_array_compile | 📈 view plot 🚷 view threshold | 158.77 x 1e3(0.00%)Baseline: 158.77 x 1e3 | 160.36 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 6,641.00(0.00%)Baseline: 6,641.00 | 6,707.41 (99.01%) |
| tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_compile | 📈 view plot 🚷 view threshold | 27.54 x 1e3(0.00%)Baseline: 27.54 x 1e3 | 27.81 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 1,074.00(0.00%)Baseline: 1,074.00 | 1,084.74 (99.01%) |
| tests/benchmarks/test_queue_push_pop.py::test_queue_push_benchmark_compile | 📈 view plot 🚷 view threshold | 10.91 x 1e3(0.00%)Baseline: 10.91 x 1e3 | 11.02 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 308.00(0.00%)Baseline: 308.00 | 311.08 (99.01%) |
| tests/benchmarks/test_queue_push_pop.py::test_queue_push_pop_benchmark_compile | 📈 view plot 🚷 view threshold | 14.84 x 1e3(0.00%)Baseline: 14.84 x 1e3 | 14.99 x 1e3 (99.01%) | 📈 view plot 🚷 view threshold | 435.00(0.00%)Baseline: 435.00 | 439.35 (99.01%) |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1712 +/- ##
==========================================
- Coverage 93.47% 92.93% -0.54%
==========================================
Files 133 136 +3
Lines 12697 13263 +566
==========================================
+ Hits 11868 12326 +458
- Misses 829 937 +108 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses #1700 by detecting when classical/copyable variables captured by a modifier block are assigned within that block, and raising an error if the stale outer binding is used afterward.
Changes:
- Track captured variables that are assigned inside modifier blocks (
CheckedModifiedBlock.modified_captured), including nested modifier bodies. - Extend linearity checking to mark such variables as unusable after the modifier and emit a dedicated diagnostic with a note pointing to the in-modifier assignment.
- Add new error fixtures covering basic, sequential, nested, multiple-variable, and branching scenarios.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/error/modifier_errors/captured_classical_modified.py | New error fixture: modified captured variable used after a modifier block. |
| tests/error/modifier_errors/captured_classical_modified.err | Expected diagnostic output for the above scenario. |
| tests/error/modifier_errors/captured_classical_modified_sequential.py | New error fixture: sequential modifier blocks reusing a modified captured variable. |
| tests/error/modifier_errors/captured_classical_modified_sequential.err | Expected diagnostic output for sequential modifier case. |
| tests/error/modifier_errors/captured_classical_modified_nested.py | New error fixture: modification occurs inside nested modifiers. |
| tests/error/modifier_errors/captured_classical_modified_nested.err | Expected diagnostic output for nested modifier case. |
| tests/error/modifier_errors/captured_classical_modified_multiple.py | New error fixture: multiple captured variables modified; verifies first reported. |
| tests/error/modifier_errors/captured_classical_modified_multiple.err | Expected diagnostic output for multiple modified captures. |
| tests/error/modifier_errors/captured_classical_modified_branch.py | New error fixture: usage in a branch after a modifier. |
| tests/error/modifier_errors/captured_classical_modified_branch.err | Expected diagnostic output for branch usage case. |
| tests/error/errors_on_usage/branch_in_modifier.py | New error fixture ensuring “not defined” behavior remains for vars only defined inside modifier branches. |
| tests/error/errors_on_usage/branch_in_modifier.err | Expected output for “Variable not defined” regression case. |
| guppylang-internals/src/guppylang_internals/nodes.py | Add modified_captured field to CheckedModifiedBlock for later diagnostics/linearity. |
| guppylang-internals/src/guppylang_internals/checker/stmt_checker.py | Minor rename cleanup: return the checked modified block variable consistently. |
| guppylang-internals/src/guppylang_internals/checker/modifier_checker.py | Compute modified_captured by scanning CFG BB assignments, recursing into nested modifier blocks. |
| guppylang-internals/src/guppylang_internals/checker/linearity_checker.py | Introduce UseKind.DEFINED_IN_MODIFIER and raise ModifiedVariableUsedError on later uses; mark modified copyable captures as stale. |
| guppylang-internals/src/guppylang_internals/checker/errors/linearity.py | Add new ModifiedVariableUsedError diagnostic with note/help. |
| guppylang-internals/src/guppylang_internals/checker/cfg_checker.py | Adds commented-out debug print (should be removed). |
| guppylang-internals/src/guppylang_internals/cfg/cfg.py | Adds cfg_as_string() helper (currently unused except for commented debug line). |
| .gitignore | Ignore AGENTS.md. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
I had a initial look through, but I'm not convinced that a new UseKind is the best way to implement this. Currently, you're marking variables as used that are actually not used in the block, which doesn't feel good. In particular, what we actually care about is assignments, so presumably the assignment should track whether it happened in modifier block, not the use?
Furthermore, I think it would be better to check this before linearity checking and maybe even before type checking. This would be more consistent with how we currently catch undefined variable errors (see check_bb in cfg_checker.py). Any way we can include this logic at that point?
f6fda46 to
8f85e4a
Compare
…ier_block for clarity
| for bb in node.cfg.bbs: | ||
| bb_stat = bb.compute_variable_stats() | ||
| # We update the `assigned_in_modifier_block` field of the stats for | ||
| # each BB. | ||
| for var, ast_node in ( | ||
| # We union the two dicts in this order to prioritize variables assigned | ||
| # in the outer modifier block | ||
| bb_stat.assigned_in_modifier_block | bb_stat.assigned | ||
| ).items(): | ||
| # if already present we keep the first assignment | ||
| if var not in self.stats.assigned_in_modifier_block: | ||
| self.stats.assigned_in_modifier_block[var] = ast_node | ||
| stats[bb] = bb_stat |
There was a problem hiding this comment.
Not sure I see what you're trying to do here? Can't we populate the fields during the visiting without having to do post processing?
There was a problem hiding this comment.
The assigned_in_modifier_block variables are the one assigned in the block bb_stat.assigned plus that are assigned_in_modifier_block in bb_stats, i.e. variables that are assigned in a modifier block inside bb.
We are not going to visit node.cfg.bbs directly, what I can do is trigger the compute_variable_stats and the get the info that I need.
…ty and update related error messages for variable scope in modifier blocks
mark-koch
left a comment
There was a problem hiding this comment.
Nice! This looks a lot cleaner now 👍
Just a couple of questions and maybe opportunities to simplify further
| # We also check that the variables assigned in a modifier block are not | ||
| # in successor blocks | ||
| if x in bb.vars.assigned_in_modifier_block: | ||
| raise GuppyError( | ||
| _assigned_in_modifier_error( | ||
| x, use_bb.vars.used[x], bb.vars.assigned_in_modifier_block[x] | ||
| ) | ||
| ) |
There was a problem hiding this comment.
I think it would make more sense to move this into the if x in cfg.assigned_somewhere branch since it only applies to locals.
In particular, cfg.assigned_somewhere should probably include variables assigned in modifier blocks?
There was a problem hiding this comment.
We should change the Assigment analysis to have the variables assigned in the block registered in cfg.assigned_somewhere
| x: b for x, b in live_after.items() if x not in stats.assigned | ||
| x: b | ||
| for x, b in live_after.items() | ||
| if x not in stats.assigned and x not in stats.assigned_in_modifier_block |
There was a problem hiding this comment.
Isn't assigned_in_modifier_block a subset of assigned?
There was a problem hiding this comment.
no it is not
| if ( | ||
| x not in self.stats.assigned | ||
| and x not in self.stats.used | ||
| and x not in self.stats.assigned_in_modifier_block |
There was a problem hiding this comment.
Same here, assigned_in_modifier_block is a subset of assigned, so no need to check it?
| # We union the two dicts in this order to prioritize variables assigned | ||
| # in the outer modifier block | ||
| bb_stat.assigned_in_modifier_block | bb_stat.assigned |
There was a problem hiding this comment.
Same here, bb_stat.assigned should already contain everything?
There was a problem hiding this comment.
Before this pr, assigned didn't include variables assigned in the block; thus, I defined a new set of variables. In principle, I think we can use bb_stat.assigned for everything? Not sure if this can break how we detect that a variable defined in a modifier block inside a bb is badly used in another bb
There was a problem hiding this comment.
Oh yes I see, sorry! In that case it's fine as is 👍
…improve related error messages
closes #1700
This PR add checks to raise an error when we use variables outside a modifier block when their value was assigned inside that modifier block. Moreover it improve the error rendering for undefined variables when the variables are only defined inside the modifier block.
Variable assigned in modifier blockwhen a previously defined variable is assigned in a modifier and then used outside it.BREAKING CHANGE: Using a variable outside a modifier block after it was assigned inside that modifier block now raises a compilation error.
BEGIN_COMMIT_OVERRIDE
feat!: Error handling for outside use of variables scoped to modifier blocks (#1712)
END_COMMIT_OVERRIDE