Skip to content

Conversation

volsa
Copy link
Member

@volsa volsa commented Jul 17, 2025

This PR is the first of two introducing polymorphism to the compiler. This PR focuses on polymorphism for classes and function blocks whereas the second will focus on introducing interfaces and validations. For reviewing this monster of a PR, best to start reading either the tests or lowering/vtable.rs and lowering/polymorphism.rs before exploring other files.

In short, polymorphism enables dynamic dispatch allowing for user code such as the following to execute

FUNCTION_BLOCK A
    METHOD getName: STRING
        getName := 'A';
    END_METHOD

    METHOD printName
        printf('name = %s$N', ADR(getName()));
    END_METHOD
END_FUNCTION_BLOCK

FUNCTION_BLOCK B EXTENDS A
    METHOD getName: STRING
        getName := 'B';
    END_METHOD

    METHOD persistName
        // Persist name to some file
    END_METHOD
END_FUNCTION_BLOCK

FUNCTION main
    VAR
        result: STRING;
        instanceA: A;
        instanceB: B;

        refInstanceA: POINTER TO A;
    END_VAR

    refInstanceA := ADR(instanceA);
    result := refInstanceA^.getName();   // Calls `A::getName` yielding "name = A"
    refInstanceA^.printName();           // Calls `A::printName` which calls `A::getName` yielding "name = A"

    refInstanceA := ADR(instanceB);
    result := refInstanceA^.getName();   // Calls `B::getName` yielding "name = B"
    refInstanceA^.printName();           // Calls `A::printName` which calls `B::getName` yielding "name = B"
END_FUNCTION

This is achieved by calling methods indirectly through a virtual table. Any class or function block will generate a __vtable_<POU_NAME> struct which will be part of the POU of the following form

TYPE __vtable_<POU_NAME>:
    STRUCT
        // function pointers to inherited, overridden or POU unique methods
    END_STRUCT
END_TYPE

Taking the previous example, the compiler would generate

TYPE __vtable_A:
    STRUCT
        getName: __FPOINTER A.getName := ADR(A.getName);
        printName: __FPOINTER A.printName := ADR(A.printName);
    END_STRUCT
END_TYPE

TYPE __vtable_B:
    STRUCT
        getName: __FPOINTER B.getName := ADR(B.getName);                // Overridden
        printName: __FPOINTER A.printName := ADR(A.printName);          // Inherited
        persistName: __FPOINTER B.persistName := ADR(B.persistName);    // Unique to B
    END_STRUCT
END_TYPE

Calling methods then invoke the virtual table to access the correct method, deciding which method needs to be called at runtime. This happens by transforming the method calls such as ADR(getName()) to __vtable_A#(THIS^.__vtable^).getName^(THIS^)). For child POUs this means upcasting their virtual table to their parent one.

The vtable member fields thereby will be initialized in the corresponding __init function, pointing to some global variable instance. That is, every POU has a global variable instance of form

VAR_GLOBAL
    __vtable_A_instance: __vtable_A;
    __vtable_B_instance: __vtable_B;
END_VAR

Finally, the final form of the previous example as transformed by the compiler would look as follows:

TYPE __vtable_A:
    STRUCT
        getName: __FPOINTER A.getName := ADR(A.getName);
        printName: __FPOINTER B.printName := ADR(A.printName);
    END_STRUCT
END_TYPE

TYPE __vtable_B:
    STRUCT
        getName: __FPOINTER B.getName := ADR(B.getName);                // Overridden
        printName: __FPOINTER A.printName := ADR(A.printName);          // Inherited
        persistName: __FPOINTER B.persistName := ADR(B.persistName);    // Unique to B
    END_STRUCT
END_TYPE

VAR_GLOBAL
    __vtable_A_instance: __vtable_A;
    __vtable_B_instance: __vtable_B;
END_VAR

FUNCTION_BLOCK A
    VAR
        // Note: Only the upper-most base POU, i.e. non-extended POU, will have a `__vtable` entry. Every child POU will access and override it internally.
        __vtable: POINTER TO __VOID := ADR(__vtable_A_instance);
    END_VAR

    METHOD getName: STRING
        getName := 'A';
    END_METHOD

    METHOD printName
        printf('name = %s$N', ADR(__vtable_A#(THIS^.__vtable^).getName^(THIS^)));
    END_METHOD
END_FUNCTION_BLOCK

FUNCTION_BLOCK B EXTENDS A
    VAR
        // Happens internally, accessing the super virtual table overridding it:
        // __A.__vtable := ADR(__vtable_B_instance);
    END_VAR

    METHOD getName: STRING
        getName := 'B';
    END_METHOD

    METHOD persistName
        // Persist name to some file
    END_METHOD
END_FUNCTION_BLOCK

FUNCTION main
    VAR
        result: STRING;
        instanceA: A;
        instanceB: B;

        refInstanceA: POINTER TO A;
    END_VAR

    refInstanceA := ADR(instanceA);
    result := __vtable_A#(refInstanceA^.__vtable^).getName^(refInstanceA^);
    __vtable_A#(refInstanceA^.__vtable^).printName^(refInstanceA^);

    refInstanceA := ADR(instanceB);
    result := __vtable_A#(refInstanceA^.__vtable^).getName^(refInstanceA^);
    __vtable_A#(refInstanceA^.__vtable^).printName^(refInstanceA^);
END_FUNCTION

@volsa volsa mentioned this pull request Jul 17, 2025
9 tasks
@volsa volsa force-pushed the vosa/polymorphism branch 2 times, most recently from f7b7c11 to 04bc60d Compare July 29, 2025 15:44
volsa added 14 commits August 1, 2025 13:15
`POINTER TO <function>` are now indexed in the codegen
Previously when dealing with function pointer calls such as `fnPtr^()`
the resolver would only annotate the operator and any arguments. Some
codegen parts require an annotation on the whole call statement however
(which makes sense), as such this commit fixes the described issue.
Visuallized, assuming `fnPtr` points to a function returning a DINT:
```
fnPtr^();
^^^^^^^^ -> StatementAnnotation::Value { resulting_type: "DINT" }
```
@volsa volsa force-pushed the vosa/polymorphism branch from 04bc60d to e79c95e Compare August 14, 2025 14:30
volsa and others added 7 commits August 26, 2025 09:20
Function pointers can now point to the body of a function block, allowing
for code execution such as
```
FUNCTION_BLOCK A
    VAR
        localState: DINT := 5;
    END_VAR

    METHOD foo
        // ...
    END_METHOD

    printf('localState = %d$N', localState);
END_FUNCTION_BLOCK

FUNCTION main
    VAR
        instanceA: A;
        fnBodyPtr: FNPTR A := ADR(A);
    END_VAR

    fnBodyPtr^(instanceA); // prints "localState = 5"
END_FUNCTION
```
Also inject `__vtable` member variable into "root" POUs
Methods calls which must make use of the virtual table are now
desugared. For example a method call within a method such as `foo(1, 2)`
will now be desugared into `__vtable_{FB_NAME}#(THIS^.__vtable^).foo^(THIS^, 1, 2)`.
Similarly, a variable like `refInstance: POINTER TO FbA` making a method call
such as `refInstance^.foo(1, 2)` will be lowered into some similar form, except
not making use of THIS, rather of the operator name (excluding the method name), i.e.
`__vtable_FbA#(refInstance^.__vtable^).foo(refInstance^, 1, 2)`.
The `__vtable` member field is now initialized with a right hand side of
`ADR(__vtable_<POU_NAME>_instance)` which represents the global instance
variable of a virtual table that is initialized with its function pointers.
@volsa volsa force-pushed the vosa/polymorphism branch from e79c95e to 27a06a5 Compare August 26, 2025 11:22
The `__body` function pointer now points at the function blocks body
method in the virtual table struct. As a result, code execution like
the following is now possible
```
VAR
    instanceA: A;
    instanceB: B; // Child of A

    refInstanceA: POINTER TO A;
END_VAR

refInstanceA := ADR(A);
refInstanceA^(); // Calls body method of A

refInstanceA := ADR(B);
refInstanceA^(); // Calls body method of B
```
@volsa volsa force-pushed the vosa/polymorphism branch from 956a3eb to 213376f Compare August 27, 2025 09:07
@volsa volsa force-pushed the vosa/polymorphism branch 3 times, most recently from b3b3dec to 521ed19 Compare September 1, 2025 15:32
@volsa volsa force-pushed the vosa/polymorphism branch from 521ed19 to 68cdbf7 Compare September 1, 2025 15:55
@volsa volsa requested a review from Copilot September 2, 2025 09:05
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces polymorphism support for structured text by implementing virtual tables and method dispatch. The implementation enables dynamic method calls on function blocks and classes through pointer-based references.

Key changes:

  • Virtual table infrastructure to support polymorphic method calls
  • Method call lowering to use indirect function calls through virtual tables
  • Test cases for various polymorphism scenarios including inheritance chains, method overriding, and reference types

Reviewed Changes

Copilot reviewed 103 out of 107 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/tests.rs Removes classes module reference to fix test organization
tests/lit/single/polymorphism/*.st Comprehensive test suite covering polymorphic scenarios (inheritance, method calls, references)
tests/lit/single/pointer/value_behind_function_block_pointer_is_assigned_to_correctly.st Test for function block pointer assignment behavior
tests/lit/single/oop/*.st Minor formatting improvements for existing OOP tests
tests/lit/multi/extern_C_fb_init/foo.c Updates external C structure to include vtable pointer
tests/lit/correctness/pointers/references/*.st Reference handling tests moved to lit framework
tests/lit/correctness/functions/*.st Function block behavior tests moved to lit framework
tests/lit/correctness/classes/class_reference_in_pou.st Class reference test moved to lit framework
tests/lit/correctness/lit.local.cfg Configuration for lit test framework
tests/integration/snapshots/*.snap Updated snapshots reflecting vtable struct changes
tests/correctness/*.rs Test cases migrated from unit tests to lit framework
src/lowering/vtable.rs New module implementing virtual table generation for polymorphism
src/validation/*.rs Updates to support polymorphic type checking and inheritance validation
src/typesystem.rs Type system extensions for method and function block identification
src/resolver.rs Updates to method resolution for inheritance chains

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@volsa volsa marked this pull request as ready for review September 2, 2025 09:28
%foo7 = getelementptr inbounds %__vtable_A, %__vtable_A* %cast6, i32 0, i32 1
%4 = load i16 (%A*, i32)*, i16 (%A*, i32)** %foo7, align 8
%deref8 = load %A*, %A** %refInstanceA, align 8
%fnptr_call9 = call i16 %4(%A* %deref8, i32 10)
Copy link
Member Author

@volsa volsa Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While writing these tests, I realized that the calls look somewhat "wrong". Here we call call i16 %4(%A* %deref8, i32 10) but the signature in the virtual table of B expects i16 (%B*, i32)*, that is we pass an object of type A to a function pointer that would expect an object of B. Technically wrong, but not really an issue because up- and downcasting for child POUs works. A "much cleaner" version of this however would be consistent function signatures, i.e. inherit the types of the base classes and explicitly type cast at the start of a method. For example

%__vtable_A = type { void (%A*)*, i16 (%A*, i32)* }
-%__vtable_B = type { void (%B*)*, i16 (%B*, i32)* }
+%__vtable_B = type { void (%A*)*, i16 (%A*, i32)* }

and

-define i16 @B__foo(%B* %0, i32 %1) {
+define i16 @B__foo(%A* %0, i32 %1) {
+%this = bitcast %A* %0 to %B*

Not a blocker, just wanted to mention this as a general fyi.

@volsa volsa requested review from ghaith, riederm and mhasel September 2, 2025 11:35
@volsa volsa changed the title feat: Polymorphism feat: Polymorphism (Classes and Function Blocks) Sep 2, 2025
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