task(DOPE-584): Python function blocks reach class parity [phase 5] - #1041
Merged
thiagoralves merged 1 commit intoAug 26, 2026
Merged
Conversation
Same gap the C++ side had, with a harder boundary. A Python block could see its inputs and its outputs; a VAR, a VAR_IN_OUT or a VAR_EXTERNAL declared in the same table simply did not cross, so the block's code referred to a name the editor had accepted and Python raised NameError at runtime. A Python block is a separate process, so nothing can be passed by pointer. Every variable is marshalled: the stub packs one struct on the way in, unpacks another on the way out, and the driver decodes and re-encodes them by `struct` format string. The direction each class travels now follows from what it means. An input goes in, an output comes back, and VAR_IN_OUT travels both ways because that is what it is. A VAR and a VAR_EXTERNAL also travel both ways, for a different reason: the PLC owns the storage, so round-tripping is what makes a VAR the block's own state, keeps it visible to the debugger, and lets it be retained once NODE-94 lands. A block that never assigns one sends back what it received. VAR_TEMP is refused rather than approximated. It means storage that does not survive the invocation, and Python has no such thing here — the block's variables are module globals in a process that outlives every scan. Marshalling it would not make it temporary, only a VAR wearing the wrong name. The refusal names the variable and says to declare it under VAR instead. An external needed care on the C side. It is a `GlobalVar<V>*` — the value plus that global's mutex — so naming it in a copy statement would compile, convert the pointer, and read the wrong memory holding no lock. Each one's copy is now wrapped in `with_lock`, one at a time rather than nested: this stub only moves values, it runs no user code, so a single lock at a time is enough and there is no ordering to reason about. The lambda parameter is deduced, so nothing here names `V`. Four emitters had been filtering by class independently — the two structs, the copy loops, the format strings, and the LSP preamble. That is the same shape of bug the C++ side had, and worse here: a field one side omits does not go missing, it shifts every later field's offset, so the corruption lands on unrelated variables. One selection rule now, in `block-interface`. Widening to VAR immediately proved why the exclusion list matters. `first_run`, `shm_in_ptr` and `shm_out_ptr` are injected as locals, and the first run on hardware swept them into the structs and handed the block its own mapped segment addresses — Python died on every cycle and the loader respawned it dozens of times. They are named and excluded now, as `hasBeenInitialized` is for C++. Two coverage gaps closed on the way. P2 added the WSTRING pack and unpack paths without tests, dropping `injectPythonRuntime` from 100%; they are pinned now, in both directions, including the truncation that must land on a code-unit boundary. And `generatePythonLspPreamble` had never covered STRING or an array of an unmappable element type — the `list[` branch in its literal helper was dead code its own caller already owned, and is gone. Verified on slm-rp4: over 195 Python cycles the VAR accumulated to exactly 195 x 2, the VAR_IN_OUT reached 195 and round-tripped to the calling program, and the VAR_EXTERNAL reached 195 x 2 with the configuration global agreeing. Sampled three times; every counter stayed exact. A VAR_TEMP is refused at compile time with the message above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
thiagoralves
merged commit Aug 26, 2026
bc2c8f4
into
task/DOPE-584-native-block-iec-parity
1 check was pending
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Phase 5 of DOPE-584: a Python function block now sees the same Variables Table an ST block does — the same gap phase 4 closed for C++, across a harder boundary.
Before this, a Python block could see its inputs and its outputs. A
VAR, aVAR_IN_OUTor aVAR_EXTERNALdeclared in the same table did not cross, so the block's code referred to a name the editor had accepted and Python raisedNameError.How each class travels
A Python block is a separate process, so nothing is passed by pointer — every variable is marshalled through two packed structs. The direction each class travels follows from what it means:
inputoutputinOutlocalexternaltempVAR_TEMPmeans storage that does not survive the invocation, and Python has no such thing here: the block's variables are module globals in a process that outlives every scan. Marshalling it would not make it temporary, only aVARwearing the wrong name. The refusal names the variable and says to declare it underVARinstead.One selection rule
Four emitters had been filtering by class independently — the two structs, the copy loops, the format strings, and the LSP preamble. That is worse here than it was for C++: a field one side omits does not go missing, it shifts every later field's offset, so the corruption surfaces on unrelated variables. The rule now lives in
block-interfaceand all four read it.Widening to
VARproved immediately why the exclusion list matters.first_run,shm_in_ptrandshm_out_ptrare injected as locals, and the first hardware run swept them into the structs and handed the block its own mapped segment addresses. Python died every cycle and the loader respawned it dozens of times. They are named and excluded now, ashasBeenInitializedis on the C++ side.Hardware verification
On slm-rp4, one block using all five classes. Over 195 Python cycles:
VARaccumulated to exactly195 x 2VAR_IN_OUTreached 195 and round-tripped to the calling programVAR_EXTERNALreached195 x 2, with the configuration global agreeingSampled three times; every counter stayed exact. A
VAR_TEMPis refused at compile time. The simulator path still stubs Python out and builds at 6% flash / 3% RAM.Coverage gaps closed on the way
injectPythonRuntimefrom 100%. Both directions are pinned now, including the truncation that must land on a code-unit boundary.generatePythonLspPreamblehad never covered STRING or an array of an unmappable element type, and thelist[branch in its literal helper was dead code its own caller already owned. It is gone. This one predates the feature branch —src/frontend/utils/was already below its 100% threshold ondevelopment.Not in scope here
🤖 Generated with Claude Code
https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ