Skip to content

fix(transpiler): @when result assignment silently dropped in exported module#30

Open
xemishra wants to merge 5 commits into
microsoft:mainfrom
xemishra:fix/transpiler-assign-wrapped-in-expr
Open

fix(transpiler): @when result assignment silently dropped in exported module#30
xemishra wants to merge 5 commits into
microsoft:mainfrom
xemishra:fix/transpiler-assign-wrapped-in-expr

Conversation

@xemishra
Copy link
Copy Markdown

@xemishra xemishra commented Jun 6, 2026

Summary

WhenTransformer.visit_FunctionDef in transpiler.py was returning
ast.Expr(ast.Assign(...)) an ast.Assign statement incorrectly
wrapped inside an ast.Expr node.

This caused a silent double failure:

  • ast.Assign is a statement, not an expression, so the wrapping
    violates the Python AST contract.
  • visit_Module filters out every ast.Expr node to strip bare
    expression statements. Because the return value matched that filter,
    the name = whencall(...) assignment was thrown away entirely.

Impact

For every @when-decorated function, the variable that should hold
the result Cown was never assigned in the exported module. Any code
that read .value, checked .exception, or chained behaviors on the
result was operating on None (or whatever the name happened to be
bound to previously), with no error at schedule time.

Root cause

# Before (buggy)
return ast.Expr(ast.Assign([ast.Name(id=node.name)], when_call))

Fix

# After
assign = ast.Assign(
    targets=[ast.Name(id=node.name, ctx=ast.Store())],
    value=when_call,
)
ast.copy_location(assign, node)
ast.fix_missing_locations(assign)
return assign

Returning a bare ast.Assign passes the visit_Module filter
correctly, satisfies the AST node contract, and ensures the result
Cown is bound to the user's chosen name in the exported module.

Testing

Verified by invoking export_module() directly on a minimal source
containing a @when-decorated function and asserting that
name = whencall(...) appears in the unparsed output, which it did
not before this fix.

@matajoh matajoh self-assigned this Jun 8, 2026
@matajoh matajoh added the bug Something isn't working label Jun 8, 2026
@matajoh
Copy link
Copy Markdown
Member

matajoh commented Jun 8, 2026

Hi! Thanks for this contribution. I'm curious as this definitely works currently (i.e., tests pass which use this functionality) but provided all the workflows are green then I see no reason not to merge as is. Is there a specific regression test you could add to test_transpiler.py for this issue?

@matajoh matajoh self-requested a review June 8, 2026 09:38
Comment thread src/bocpy/transpiler.py
@xemishra
Copy link
Copy Markdown
Author

xemishra commented Jun 8, 2026

@microsoft-github-policy-service agree

@xemishra
Copy link
Copy Markdown
Author

xemishra commented Jun 8, 2026

I've added a test for this scenario and updated the PR accordingly.

@matajoh
Copy link
Copy Markdown
Member

matajoh commented Jun 8, 2026

I've added a test for this scenario and updated the PR accordingly.

Awesome, thank you for this as well. I've figured out what it was about this that was confusing me. Because standard BOC practice is for behaviors to be defined within functions, this:

from bocpy import when, Cown, wait

def main():
    x = Cown(0)
    @when(x)
    def add_one(x):
        x.value += 1

    @when(add_one, x)
    def print_x(_, x):
        print(x.value)


if __name__ == "__main__":
    main()
    wait()

works fine, but your case is when the behavior is defined at the module level, which explains why it wasn't caught. Thanks for finding this!

@xemishra
Copy link
Copy Markdown
Author

xemishra commented Jun 8, 2026

I have restored the trailing newline in test_transpiler.py, which was causing the lint PR gate failure. The fix has been pushed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants