Skip to content

Comments

feat: Interface-based Polymorphism#1588

Draft
volsa wants to merge 31 commits intomasterfrom
vosa/ipolymorphism
Draft

feat: Interface-based Polymorphism#1588
volsa wants to merge 31 commits intomasterfrom
vosa/ipolymorphism

Conversation

@volsa
Copy link
Member

@volsa volsa commented Feb 9, 2026

First working version of interface based polymorphism. Some things still need to be tackled in follow-up PRs, most importantly:

  • Properties
  • Debug Locations (and thus debug DX)
  • Validations
  • Embedded type information for runtime checks like instanceof
  • Interface to interface assignments (e.g. refA := refB)
  • ...and some other stuff I have noted for myself

That being said, interface based polymorphism with this PR should work for most use cases.

I've included a draft of a technical document which I want to add to our book once polymorphism is feature complete. For now it should serve as a good starting point to understand the concept and get started with reviewing this PR. In general my recommendation is: Read the draft followed by the files located in the lowering/polymorphism folder followed by exploring the remaining changes. Alternatively start with the draft then tests and then the lowering/polymorphism, up to you guys. Also note, the draft is a mix of human writing and LLM touch-ups, it should be correct however. Again, I will refine it once polymorphism is feature complete.

volsa and others added 12 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
@volsa volsa force-pushed the vosa/ipolymorphism branch from 7c104e0 to 5ffed38 Compare February 18, 2026 17:54
volsa added 13 commits February 19, 2026 11:22
- 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.
@codecov
Copy link

codecov bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 96.95305% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.73%. Comparing base (54406fb) to head (625a6f8).
⚠️ Report is 97 commits behind head on master.

Files with missing lines Patch % Lines
src/lowering/polymorphism/dispatch/interface.rs 96.80% 44 Missing ⚠️
src/codegen/debug.rs 70.83% 7 Missing ⚠️
src/lowering/polymorphism/table/interface.rs 98.65% 6 Missing ⚠️
src/codegen/generators/data_type_generator.rs 60.00% 2 Missing ⚠️
src/index.rs 95.12% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1588      +/-   ##
==========================================
- Coverage   94.95%   94.73%   -0.22%     
==========================================
  Files         174      190      +16     
  Lines       56307    60323    +4016     
==========================================
+ Hits        53464    57150    +3686     
- Misses       2843     3173     +330     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
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