Skip to content

Polymorphic dispatch missing for VAR_IN_OUT FB parameter calls across compilation boundaries #1743

@ghaith

Description

@ghaith

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)

FB_B body
FB_B body

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions