Skip to content

feat!: Implement error handling for modified variables outside modifiers#1712

Merged
nicolaassolini-qntm merged 27 commits into
mainfrom
na/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block
Jun 8, 2026
Merged

feat!: Implement error handling for modified variables outside modifiers#1712
nicolaassolini-qntm merged 27 commits into
mainfrom
na/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block

Conversation

@nicolaassolini-qntm
Copy link
Copy Markdown
Contributor

@nicolaassolini-qntm nicolaassolini-qntm commented May 1, 2026

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.

  • Track variables assigned inside modifier blocks during CFG analysis.
  • Report Variable assigned in modifier block when a previously defined variable is assigned in a modifier and then used outside it.
  • Add help notes for variables that are defined only inside a modifier block and later used outside it.
  • Preserve valid reassignment behavior when a variable is reassigned after the modifier block before being used.
  • Add regression coverage for nested modifiers, sequential modifiers, branches, captured classical variables, and variables defined only inside modifiers.

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

@nicolaassolini-qntm nicolaassolini-qntm requested a review from a team as a code owner May 1, 2026 15:34
@nicolaassolini-qntm nicolaassolini-qntm changed the title Implement error handling for modified variables outside modifiers feat: Implement error handling for modified variables outside modifiers May 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🐰 Bencher Report

Branchna/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block
TestbedLinux
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
tests/benchmarks/test_big_array.py::test_big_array_check📈 view plot
🚷 view threshold
870,780.05 µs
(-14.09%)Baseline: 1,013,581.58 µs
1,064,260.65 µs
(81.82%)
tests/benchmarks/test_big_array.py::test_big_array_compile📈 view plot
🚷 view threshold
2,121,526.13 µs
(-0.18%)Baseline: 2,125,295.27 µs
2,231,560.03 µs
(95.07%)
tests/benchmarks/test_big_array.py::test_big_array_executable📈 view plot
🚷 view threshold
8,777,533.78 µs
(+1.93%)Baseline: 8,611,724.33 µs
9,042,310.55 µs
(97.07%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_check📈 view plot
🚷 view threshold
109,183.00 µs
(-4.36%)Baseline: 114,159.33 µs
119,867.30 µs
(91.09%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_compile📈 view plot
🚷 view threshold
232,023.20 µs
(-2.35%)Baseline: 237,598.15 µs
249,478.06 µs
(93.00%)
tests/benchmarks/test_ctrl_flow.py::test_many_ctrl_flow_executable📈 view plot
🚷 view threshold
952,387.91 µs
(-27.05%)Baseline: 1,305,526.15 µs
1,370,802.46 µs
(69.48%)
tests/benchmarks/test_prelude.py::test_import_guppy📈 view plot
🚷 view threshold
51.21 µs
(-5.92%)Baseline: 54.44 µs
57.16 µs
(89.60%)
tests/benchmarks/test_queue_push_pop.py::test_queue_push_benchmark📈 view plot
🚷 view threshold
463,308.71 µs
(-2.51%)Baseline: 475,216.79 µs
498,977.63 µs
(92.85%)
tests/benchmarks/test_queue_push_pop.py::test_queue_push_pop_benchmark📈 view plot
🚷 view threshold
619,643.00 µs
(-1.36%)Baseline: 628,161.81 µs
659,569.90 µs
(93.95%)
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🐰 Bencher Report

Branchna/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block
TestbedLinux
Click to view all benchmark results
Benchmarkhugr_bytesBenchmark Result
bytes x 1e3
(Result Δ%)
Upper Boundary
bytes x 1e3
(Limit %)
hugr_nodesBenchmark 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%)
🐰 View full continuous benchmarking report in Bencher

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.93%. Comparing base (561882f) to head (88095f1).
⚠️ Report is 50 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread guppylang-internals/src/guppylang_internals/checker/cfg_checker.py Outdated
Comment thread guppylang-internals/src/guppylang_internals/cfg/cfg.py Outdated
@nicolaassolini-qntm nicolaassolini-qntm changed the title feat: Implement error handling for modified variables outside modifiers feat(guppylang_internals)!: Implement error handling for modified variables outside modifiers May 1, 2026
@nicolaassolini-qntm nicolaassolini-qntm marked this pull request as draft May 1, 2026 16:05
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 1, 2026

Merging this PR will not alter performance

✅ 9 untouched benchmarks


Comparing na/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block (88095f1) with main (06a551d)

Open in CodSpeed

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread guppylang-internals/src/guppylang_internals/nodes.py Outdated
Comment thread guppylang-internals/src/guppylang_internals/checker/linearity_checker.py Outdated
Comment thread guppylang-internals/src/guppylang_internals/checker/errors/linearity.py Outdated

This comment was marked as off-topic.

@nicolaassolini-qntm nicolaassolini-qntm force-pushed the na/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block branch from f6fda46 to 8f85e4a Compare May 26, 2026 14:44

This comment was marked as outdated.

This comment was marked as outdated.

Comment thread guppylang-internals/src/guppylang_internals/checker/cfg_checker.py Outdated
Comment on lines +252 to +264
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

@nicolaassolini-qntm nicolaassolini-qntm Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/error/errors_on_usage/var_in_modifier1.err Outdated
Comment thread tests/error/modifier_errors/captured_classical_modified.err
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.

Comment thread guppylang-internals/src/guppylang_internals/checker/cfg_checker.py
Comment thread guppylang-internals/src/guppylang_internals/checker/cfg_checker.py Outdated
Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This looks a lot cleaner now 👍

Just a couple of questions and maybe opportunities to simplify further

Comment thread guppylang-internals/src/guppylang_internals/cfg/bb.py Outdated
Comment on lines +298 to +305
# 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]
)
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change the Assigment analysis to have the variables assigned in the block registered in cfg.assigned_somewhere

Comment thread guppylang-internals/src/guppylang_internals/cfg/bb.py Outdated
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't assigned_in_modifier_block a subset of assigned?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, assigned_in_modifier_block is a subset of assigned, so no need to check it?

Comment on lines +261 to +263
# 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, bb_stat.assigned should already contain everything?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes I see, sorry! In that case it's fine as is 👍

@nicolaassolini-qntm nicolaassolini-qntm added this pull request to the merge queue Jun 8, 2026
Merged via the queue into main with commit cab8077 Jun 8, 2026
12 checks passed
@nicolaassolini-qntm nicolaassolini-qntm deleted the na/1700-raise-an-error-when-variables-are-used-outside-a-modifier-block branch June 8, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Raise an error when variables are used outside a modifier block

4 participants