Polymorphic dispatch missing for VAR_IN_OUT FB parameter calls across compilation boundaries
Filed by Claude (Opus 4.7) via gh issue create. Posted under @ghaith account; reviewer judgement applies.
Summary
Calling an FB through a VAR_IN_OUT FB_X parameter (bare-member form,
e.g. target()) bypasses vtable dispatch. The polymorphism participant
emits a direct call to FB_X__body regardless of the runtime type of
the actual argument. When the lib is compiled separately and a
downstream consumer passes an instance of FB_X_DERIVED EXTENDS FB_X,
the derived body never runs — the base body executes instead.
The same shape works correctly when the parameter is declared as
POINTER TO FB_X (called as target^()) or REFERENCE TO FB_X
(called as target() with auto-deref). Only the VAR_IN_OUT FB_X case
is broken.
Reproduction (clean master, commit at the time of filing)
lib.st:
FUNCTION_BLOCK FB_B
printf('FB_B body$N');
END_FUNCTION_BLOCK
FUNCTION_BLOCK FB_A
VAR_IN_OUT
target : FB_B;
END_VAR
target();
END_FUNCTION_BLOCK
app.st:
{external}
FUNCTION_BLOCK FB_B
END_FUNCTION_BLOCK
{external}
FUNCTION_BLOCK FB_A
VAR_IN_OUT
target : FB_B;
END_VAR
END_FUNCTION_BLOCK
FUNCTION_BLOCK FB_B_DERIVED EXTENDS FB_B
printf('FB_B_DERIVED body$N');
END_FUNCTION_BLOCK
FUNCTION main
VAR
a : FB_A;
base : FB_B;
derived : FB_B_DERIVED;
END_VAR
a(target := base);
a(target := derived);
END_FUNCTION
Commands (compile lib in isolation, then app linking against the .so;
mirrors the extern_st_polymorphism lit test):
STDLIBLOC=<path-to-stdlib>
PRINTF=<repo>/tests/lit/util/printf.pli
plc --shared -o /tmp/libvarinout.so \
-liec61131std -L$STDLIBLOC/lib -i "$STDLIBLOC/include/*.st" -i "$PRINTF" \
--linker=cc lib.st
plc -o /tmp/app.out \
-liec61131std -L$STDLIBLOC/lib -i "$STDLIBLOC/include/*.st" -i "$PRINTF" \
-L/tmp -lvarinout --linker=cc app.st
LD_LIBRARY_PATH="$STDLIBLOC/lib:/tmp" /tmp/app.out
Expected output
FB_B body
FB_B_DERIVED body
Actual output (master)
The second invocation should reach FB_B_DERIVED__body because the
runtime instance passed via VAR_IN_OUT is of type FB_B_DERIVED.
Instead the lib's pre-compiled FB_A.body invokes FB_B__body
directly.
Working cases (for contrast)
Replacing the parameter declaration with either of the following
produces the expected FB_B body / FB_B_DERIVED body sequence:
VAR target : POINTER TO FB_B; END_VAR with the call site as
target^().
VAR target : REFERENCE TO FB_B; END_VAR with the call site as
target() (auto-deref).
Both are handled by PolymorphicCallLowerer::is_polymorphic_call_candidate
in src/lowering/polymorphism/dispatch/pou.rs:
(ReferenceAccess::Deref, Some(base)) — handles ptr^() for any
pointer whose inner type is an FB / class.
(ReferenceAccess::Member(member), None) — handles bare-member
calls when the member is annotated as is_reference_to.
VAR_IN_OUT FB_B falls through both. The argument is ByRef(InOut) in
the index (an implicit reference) but its annotated type is the bare
FB type, not a POINTER TO FB or a REFERENCE TO FB. There is no
candidate branch that recognises this shape.
Suspected fix location
PolymorphicCallLowerer::is_polymorphic_call_candidate at
src/lowering/polymorphism/dispatch/pou.rs:239.
A new candidate branch is needed for "bare-member call whose target is
a parameter declared as argument_type.is_by_ref() AND whose type is
an FB / class". The rewrite that follows (patch_instance_argument,
patch_vtable_access, patch_vtable_cast, patch_method_call_deref)
already handles the indirected shape — the by-ref parameter behaves
identically to a REFERENCE TO FB once recognised. Adapting the
candidate check is likely the only change needed.
Regression test
A two-compilation lit test for this exact shape lives at
tests/lit/multi/var_in_out_fb_in_pre_oop_lib_dispatches/ on the
incremental/pr-b-precheck-gates branch, currently marked XFAIL so
the gap doesn't get forgotten. Removing the XFAIL directive once
this is fixed will turn it into a passing regression test.
Impact
A pre-OOP library that takes FB parameters by reference (a common
shape for legacy oscat-style code) cannot be safely extended by
downstream consumers — the lib's calls always reach the base body
regardless of the type the consumer passes. POINTER TO and REFERENCE TO
parameters do not have this issue, but porting an existing API away
from VAR_IN_OUT is a breaking change for every caller.
Polymorphic dispatch missing for
VAR_IN_OUT FBparameter calls across compilation boundariesSummary
Calling an FB through a
VAR_IN_OUT FB_Xparameter (bare-member form,e.g.
target()) bypasses vtable dispatch. The polymorphism participantemits a direct call to
FB_X__bodyregardless of the runtime type ofthe actual argument. When the lib is compiled separately and a
downstream consumer passes an instance of
FB_X_DERIVED EXTENDS FB_X,the derived body never runs — the base body executes instead.
The same shape works correctly when the parameter is declared as
POINTER TO FB_X(called astarget^()) orREFERENCE TO FB_X(called as
target()with auto-deref). Only theVAR_IN_OUT FB_Xcaseis broken.
Reproduction (clean
master, commit at the time of filing)lib.st:app.st:Commands (compile lib in isolation, then app linking against the .so;
mirrors the
extern_st_polymorphismlit test):Expected output
Actual output (master)
The second invocation should reach
FB_B_DERIVED__bodybecause theruntime instance passed via
VAR_IN_OUTis of typeFB_B_DERIVED.Instead the lib's pre-compiled
FB_A.bodyinvokesFB_B__bodydirectly.
Working cases (for contrast)
Replacing the parameter declaration with either of the following
produces the expected
FB_B body/FB_B_DERIVED bodysequence:VAR target : POINTER TO FB_B; END_VARwith the call site astarget^().VAR target : REFERENCE TO FB_B; END_VARwith the call site astarget()(auto-deref).Both are handled by
PolymorphicCallLowerer::is_polymorphic_call_candidatein
src/lowering/polymorphism/dispatch/pou.rs:(ReferenceAccess::Deref, Some(base))— handlesptr^()for anypointer whose inner type is an FB / class.
(ReferenceAccess::Member(member), None)— handles bare-membercalls when the member is annotated as
is_reference_to.VAR_IN_OUT FB_Bfalls through both. The argument isByRef(InOut)inthe index (an implicit reference) but its annotated type is the bare
FB type, not a
POINTER TO FBor aREFERENCE TO FB. There is nocandidate branch that recognises this shape.
Suspected fix location
PolymorphicCallLowerer::is_polymorphic_call_candidateatsrc/lowering/polymorphism/dispatch/pou.rs:239.A new candidate branch is needed for "bare-member call whose target is
a parameter declared as
argument_type.is_by_ref()AND whose type isan FB / class". The rewrite that follows (
patch_instance_argument,patch_vtable_access,patch_vtable_cast,patch_method_call_deref)already handles the indirected shape — the by-ref parameter behaves
identically to a
REFERENCE TO FBonce recognised. Adapting thecandidate check is likely the only change needed.
Regression test
A two-compilation lit test for this exact shape lives at
tests/lit/multi/var_in_out_fb_in_pre_oop_lib_dispatches/on theincremental/pr-b-precheck-gatesbranch, currently markedXFAILsothe gap doesn't get forgotten. Removing the
XFAILdirective oncethis is fixed will turn it into a passing regression test.
Impact
A pre-OOP library that takes FB parameters by reference (a common
shape for legacy oscat-style code) cannot be safely extended by
downstream consumers — the lib's calls always reach the base body
regardless of the type the consumer passes. POINTER TO and REFERENCE TO
parameters do not have this issue, but porting an existing API away
from
VAR_IN_OUTis a breaking change for every caller.