Skip to content

feat: Polymorphic properties#1619

Draft
volsa wants to merge 39 commits intomasterfrom
vosa/polymorphism-properties
Draft

feat: Polymorphic properties#1619
volsa wants to merge 39 commits intomasterfrom
vosa/polymorphism-properties

Conversation

@volsa
Copy link
Member

@volsa volsa commented Mar 4, 2026

No description provided.

volsa and others added 30 commits February 16, 2026 12:16
Add a new DataTypeInformation::Interface variant so that interfaces are
tracked as first-class data types. This allows variables, inputs, arrays,
and return types to reference interface types directly.

Because interfaces are now indexed as data types, the dedicated
validate_unique_interfaces check is removed in favor of the existing
data type uniqueness validation.
…ch structure

Move vtable.rs and polymorphism.rs into a new polymorphism/ module tree
organized by concern (table/ for vtable generation, dispatch/ for call
lowering) and by target (pou.rs for classes/function blocks, interface.rs
as empty placeholders for future work).

Introduce PolymorphismLowerer as a single entry point that replaces the
separate VirtualTableGenerator and PolymorphicCallLowerer pipeline
participants, simplifying the pipeline registration to a single participant.
…ispatch

Add InterfaceTableGenerator which produces per-interface itable struct
definitions (one function pointer per method, including inherited methods)
and per-(interface, POU) global instances with initializers pointing to
the POU's concrete method implementations.

Handles deep POU inheritance chains, interface EXTENDS hierarchies,
diamond patterns, method overrides at any level, and multi-unit placement
(struct definitions in the interface's unit, instances in the POU's unit).

Integrated into the existing TableGenerator alongside VirtualTableGenerator.
…e declarations

Replace all interface-typed declarations (variables, params, arrays,
return types) with __FATPOINTER, a shared struct containing data and
table void pointers. The struct is injected on demand into the first
compilation unit. Alias types are resolved via find_effective_type_by_name.
Replace owned Option<Index>/Option<AnnotationMapImpl> with shared
references in PolymorphicCallLowerer and InterfaceDispatchLowerer.
Removes unnecessary take/unwrap patterns and the unused Index return
value from DispatchLowerer::lower.
Expand `ref := instance` into two fat pointer field assignments:
  ref.data  := ADR(instance)
  ref.table := ADR(__itable_<interface>_<POU>_instance)

Both assignments are packed into an ExpressionList node since the
visitor only has access to a single node. The codegen statement
generator already iterates ExpressionList children as individual
statements.

Global itable references are wrapped in ReferenceExpr(Member(...))
so the re-annotation pass can resolve them as global variables.
… dispatch

Implements concrete POU → interface expansion for both assignments and
call arguments (unnamed and named). Assignments expand into fat pointer
field writes (data + table). Call arguments allocate a temporary fat
pointer, initialize it, and substitute the original argument.

Also serializes AllocationStatement in the AST serializer and adds
nesting tests for multi-depth call argument wrapping.
…rect dispatch

Interface method calls lowered to itable dispatch (e.g.
`__itable_IA#(ref.table^).foo^(ref.data)`) require an LLVM function
stub so that `build_indirect_call` can resolve the function type
signature. Previously, interface methods were only registered as POU
declarations — not as implementations — so codegen could not find them.

Register an ImplementationIndexEntry for each interface method during
indexing. This causes LLVM function stubs to be generated alongside
concrete method stubs. Two places in pou_generator assumed every
method's parent class has a struct type, which is not the case for
interfaces (they are abstract):

- collect_parameters_for_implementation: skip the struct-type sanity
  check when the parent is an interface
- debug info parameter list: filter out interface types that have no
  debug type information
… in dispatch

- Override visit_interface in InterfaceDispatchLowerer so interface method
  parameters with interface types get rewritten to __FATPOINTER, matching
  the implementing POUs (fixes signature mismatch error E112)
- Wrap the self-pointer argument in a deref (reference.data^) so codegen
  loads the actual instance pointer instead of passing the address of the
  fat pointer's data slot (fixes Bus error for aggregate return types)
- Add lit tests for interface polymorphism: arguments (named, positional,
  mixed, sequential, implicit), aggregate returns (array, string, struct),
  diamond/linear inheritance, and basic dispatch
…INTER

- Register a forward declaration for internal (no source location) struct
  types in create_struct_type instead of silently returning, so they are
  available for debug info references
- Thread index/types_index through create_function and create_subroutine_type
  to enable lazy debug type registration for types not yet in the map
- Update debug test snapshots
- Add #[allow(clippy::too_many_arguments)] to create_function in debug.rs
- Replace `diagnostics.len() > 0` with `!diagnostics.is_empty()`
- Remove unnecessary `&` on format string arguments
- Remove unnecessary trailing `return`
- Simplify closure to function reference
- Reorder imports alphabetically
- Apply rustfmt line wrapping adjustments
…atch

When a call with interface arguments is the RHS of an assignment
(e.g. `result := ref.foo(instance)`), the fat pointer preamble was
incorrectly nested under the assignment, producing:
  `result := alloca ..., tmp.data := ..., tmp.table := ..., call`
instead of:
  `alloca ..., tmp.data := ..., tmp.table := ..., result := call`

Fix by detecting the enclosing assignment context in visit_call_statement
and routing the preamble through assignment_preamble so the assignment
wraps only the call. Also save/restore assignment_ctx in visit_assignment
to prevent inner named-parameter assignments from clobbering the outer
context.
…to post_annotate

Previously, AggregateTypeLowerer modified POU signatures (inserting a
VAR_IN_OUT parameter for aggregate returns) in post_index. This shifted
parameter positions before the annotation phase, causing the resolver to
assign incorrect type hints to call arguments. In particular, when a
function had both an aggregate return type (e.g. STRING) and an
interface-typed parameter, the InterfaceDispatchLowerer would see the
wrong type hint and fail to wrap the argument in a fat pointer.

By removing the post_index hook and performing all aggregate lowering in
post_annotate, the annotation phase sees the original unmodified POU
signatures with correct parameter positions. The post_annotate handler
now also re-indexes from the modified units (instead of reusing the
stale pre-modification index) so that subsequent pipeline stages and
validation see the updated signatures.
…ateTypeLowerer

When AggregateTypeLowerer's map() encountered an ExpressionList produced
by a previous pass (e.g. InterfaceDispatchLowerer's fat pointer preamble),
it used a single shared scope for all children. This caused the aggregate
alloca and extracted call to be prepended before the entire list, placing
the call before the fat pointer setup it depends on.

Fix by detecting ExpressionList nodes in map() and processing each element
with its own scope, so generated preamble statements are placed directly
before the element that produced them.

Also un-ignores interface_method_call_with_aggregate_return_and_interface_argument
and adds integration tests covering:
- STRING return + interface argument (method dispatch)
- STRUCT return + interface argument (method dispatch)
- STRUCT return + mixed scalar/interface args, unnamed and reordered named
- Free FUNCTION with STRING return + interface argument
Replace drain(..).collect() with std::mem::take() per clippy suggestion.
- Rename existing tests with group prefixes:
  - hierarchy_: single, implicit, linear, diamond
  - type_: array/string/struct arguments and returns
  - combined_: aggregate return + interface argument tests
  - argument_: unchanged (already well-named)

- Add new tests:
  - hierarchy_method_name_collision: two unrelated interfaces with same-named method
  - dispatch_member_variable: interface ref stored as FB member field
  - dispatch_conditional: IF/CASE runtime dispatch
  - dispatch_recursive: chained and self-referential interface dispatch
  - type_array_of_interfaces: array of root interface with hierarchy (IA <- IB <- IC)

- Remove stale Output/ directory (lit artifacts, regenerated on next run)
Fix seven discrepancies between the polymorphism design document
and the actual code:

1. VTable naming case: __vtable_fbA → __vtable_FbA throughout,
   matching the code's format!("__vtable_{}", pou.name) pattern.
2. Doc comment in table/pou.rs: __vtable_instance_A →
   __vtable_A_instance to match get_vtable_instance_name().
3. Initializer placement: move default initializers from the
   global vtable instance onto the struct member definitions,
   matching the code where members carry ADR(...) defaults and
   the instance has no explicit initializer.
4. __body entry: document the __body function pointer that
   function blocks get as the first vtable field, with a note
   that ASCII diagrams omit it for brevity.
5. Instance argument cast: add FbA#(...) wrapping around the
   instance argument in all dispatch examples, matching the
   maybe_cast_instance() behavior in dispatch/pou.rs.
6. THIS calls: add THIS alongside SUPER in the list of calls
   left untouched by dispatch lowering.
7. __itable_ID field order: correct bar,foo,baz,qux to
   foo,bar,baz,qux matching the DFS order the code produces.
The concrete→interface mismatch detection only lived in
visit_reference_expr, which never fires for call expressions.
Functions returning a concrete type (e.g. FbA) assigned to an
interface variable (e.g. IA) were not wrapped in a fat pointer.

Extract the detection logic into a shared maybe_expand_fat_pointer
method and call it from both visit_reference_expr and
visit_call_statement.
Indent list continuation lines to align with the list item text,
fixing clippy doc_lazy_continuation warnings.
Revert the broad forward-declaration registration for all internal
struct types introduced to fix a panic when __FATPOINTER appeared as
a function parameter. Instead, gracefully skip unregistered types:
create_subroutine_type filters out missing parameter types,
get_or_create_debug_type returns Err for unresolvable types so
callers can skip them, and generate_debug_types logs and continues
instead of aborting. This keeps vtables, itables, and fat pointers
out of the DWARF output entirely.
Reformat closure and function call expressions in debug.rs and
data_type_generator.rs to satisfy rustfmt line-length rules.
The alloca alignment for small types (i8, i16) differs between
macOS and Linux CI (1/2 vs 4). Restrict these tests to macOS
until platform-specific snapshots are added for Linux.
volsa and others added 9 commits February 23, 2026 13:02
…-list drain-and-rebuild

Replaces the previous ExpressionList-based 1→N statement expansion hack
with a cleaner architecture using a new `visit_statement_list` visitor
hook. The lowerer now drains each statement list and rebuilds it,
inserting preamble nodes (e.g. fat-pointer allocas) before the current
statement and optionally replacing it entirely (e.g. expanding an
interface assignment into .data + .table field writes).

Key changes:

- Add `visit_statement_list` hook to both `AstVisitor` and
 `AstVisitorMut` traits, routing all control-flow bodies (IF, FOR,
 WHILE, CASE, REPEAT) through it instead of `visit_all_nodes`.

- Implement `Walker[Mut]` for `Interface` and `PropertyBlock` so their
 children (methods, properties, variable blocks) are visited during
 lowering and type replacement.

- Rewrite `InterfaceDispatchLowerer` internals: replace
 `assignment_ctx` stack, `call_depth` counter, and
 `assignment_preamble`/`call_preamble` buffers with `preamble`,
 `replacement`, and `in_call_args` fields that integrate cleanly with
 the drain-and-rebuild loop.

- Remove the `ExpressionList` special-case workaround from
 `AggregateTypeLowerer::map` that was compensating for the old
 expansion strategy.

- Enhance `AstSerializer` with indentation tracking and structured
 formatting for control-flow statements, enabling readable snapshot
 tests for lowered output inside IF/FOR/CASE/WHILE blocks.

- Add comprehensive test suite covering interface dispatch lowering for
 assignments, call argument wrapping (positional, named, nested,
 qualified, array, pointer deref, function/method call results),
 interface method calls, control flow bodies, and parameter directions.
Fixes compilation errors, runtime failures, and snapshot mismatches
caused by merging master's constructor/initializer refactoring
(feat!: Refactor initialisers #1552) into the interface polymorphism
branch.

The merge introduced 5 distinct issues:

1. Missing `linkage` field on `DataType` (compile error)
   Master added a `linkage: LinkageType` field to the `DataType`
   struct. The interface type registration in `src/index/indexer.rs`
   was missing this new field. Fixed by adding
   `linkage: LinkageType::Internal` and the `LinkageType` import.

2. `VirtualTableGenerator::new` signature change (compile error)
   Master added a `generate_external_constructors: bool` parameter to
   control whether constructor functions are emitted for externally-
   linked POUs. `TableGenerator::generate()` still used the old
   one-argument call. Fixed by threading the flag through the full
   call chain: Pipeline -> PolymorphismLowerer (new field) ->
   TableGenerator::generate (new param) -> VirtualTableGenerator::new.

3. Invalid `vtable::VirtualTableGenerator` in pipeline (compile error)
   The merge produced code that used `VirtualTableGenerator` directly
   as a pipeline participant, but: the module path
   `lowering::vtable` doesn't exist (it lives in the private
   `lowering::polymorphism::table::pou` module), and the type doesn't
   implement `PipelineParticipantMut`. This is redundant anyway since
   `PolymorphismLowerer` already calls `VirtualTableGenerator`
   internally via `TableGenerator::generate()`. Fixed by removing the
   standalone participant entry and passing `generate_external_
   constructors` through `PolymorphismLowerer::new()` instead. Also
   fixed the same broken import in `plc_lowering` test code, a
   missing semicolon after `vec![...]`, and a missing return
   expression.

4. `IA__ctor` calls generated for interface types (18 lit test failures)
   Master's new `Initializer` generates `<Type>__ctor()` calls for
   any variable whose type is in the index with non-BuiltIn linkage.
   Interface types registered by this branch (with
   `LinkageType::Internal`) passed this check, causing
   `IA__ctor(reference)` calls to be emitted. But interfaces are
   abstract types that get lowered to `__FATPOINTER` fat pointers —
   no `IA__ctor` function is ever defined, so codegen failed with
   "cannot generate call statement". Fixed by adding
   `&& !dt.is_interface()` to `get_constructor_call()`.

5. Property local variables leaked into FB constructor (1 lit test failure)
   The `PropertyLowerer` converts property blocks into method POUs at
   `pre_index` time, but the original `PropertyBlock` is retained in
   the parent POU. `PropertyBlock::walk()` visits the variable blocks
   inside property implementations. When master's new `Initializer`
   walks a function block POU, it traverses into the retained property
   blocks and encounters local variables (e.g. `one: DINT := 1` from
   a getter). Because the FB is stateful, the initializer generates
   `self.one := 1` in the FB constructor — but `one` is local to the
   getter method, not a member of the FB. At codegen the resolver
   can't find `self.one`, causing "no type hint available". Fixed by
   adding a no-op `visit_property()` override to the `Initializer`.

Snapshot updates reflect constructor naming changes from master's
refactoring (`__init_xxx` -> `Xxx__ctor`, `__user_init_Xxx` removed)
and the removal of spurious `IA__ctor` calls for interface types.
Reorder PropertyLowerer before PolymorphismLowerer in the pipeline
participant list so that property references are lowered to __get_/__set_
method calls before dispatch lowering runs. This single-line change enables
properties accessed through interface variables to dispatch dynamically
through itables, with zero regressions.

Add 8 itable generation tests, 12 dispatch lowering tests, and 6 runtime
lit tests covering getter/setter dispatch, overrides, mixed method+property
interfaces, conditional dispatch, and cross-reference argument passing.
…on chain

When an interface IB extends IA and IA declares a property, accessing that
property through a variable typed as IB failed with 'Could not resolve
reference'. The resolver's property lookup only checked the direct qualifier
and the POU super_class chain via find_method, which does not cover interface
extensions.

Extend resolve_property to walk the interface inheritance chain via
get_derived_interfaces_recursive when the direct lookup fails and the
qualifier is an interface. This mirrors how method resolution already
works for interfaces.

Update stale plc_lowering snapshots for inherited property tests — the
previous commit's pipeline reorder (PropertyLowerer before
PolymorphismLowerer) now correctly routes property calls through vtable
dispatch, changing the expected AST output.

Add property annotation unit test, interface property lit tests (diamond,
deep linear chain with overrides), extended_interface coverage through
child interface, and POU vtable dispatch lit test for properties.
Add lit tests covering dynamic dispatch of properties with selectively
overridden get/set accessors for both vtable (POU-based) and itable
(interface-based) polymorphism.

POU-based (polymorphism/properties/):
- inherited_getter_overridden_setter
- alternating_accessor_overrides (3-level chain)
- method_dispatches_to_overridden_property
- multiple_properties_selective_overrides

Interface-based (interfaces/properties/):
- basic_get_set
- multiple_implementors
- inherited_getter_overridden_setter
- inherited_setter_overridden_getter
- implicit_obligation_through_pou_chain
- deep_chain_alternating_overrides
- mixed_methods_and_properties
- interface_hierarchy
- diamond_hierarchy

Also add itable unit tests for partial property accessor overrides
in interface.rs (overridden set-only, deep alternating overrides,
extended interface hierarchy with property override).
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.

1 participant