Object orientation: the proposal
Ed.3 6.6.5 to 6.6.8 (Tables 48 to 53) adds classes, methods,
interfaces, inheritance and polymorphism, for a new POU, CLASS, and
for function blocks. iecf parses none of it today: CLASS,
INTERFACE, METHOD, EXTENDS and IMPLEMENTS are skipped with
E0009. This proposes how to build them, in three steps (§4), and asks
for the decisions the standard leaves open or gets wrong (§6). DECIDED
2026-11-10 (florin): all twelve of §6 as recommended. Steps A, B and C
BUILT 2026-11-10, as §4 says; what a program gets is in the book’s
Classes and methods page.
1. What Ed.3 says
Classes (6.6.5.1 to 6.6.5.3, Tables 48 and 49)
CLASS [FINAL | ABSTRACT] name [EXTENDS base] [IMPLEMENTS i1, i2], thenVARandVAR_EXTERNALsections, then methods, thenEND_CLASS. A class has no body, no VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT or VAR_TEMP; it is never called, only its methods are.- Its variables may be RETAIN or NON_RETAIN, initialized, and
AT %I*. Each VAR section has an access specifier,VAR PUBLIC: PUBLIC, PRIVATE, INTERNAL or PROTECTED, PROTECTED by default (Table 48 10a-10d). A PUBLIC variable is read from outside as an output is,inst.v. - An instance is declared as a structured variable is, its PUBLIC
variables initialized by a list:
c2 : CCounter := (m_iUpperLimit := 20000, ...)(Table 49). Instances of classes and function blocks may be declared in a class’s VAR and VAR_EXTERNAL.
Methods (6.6.5.4)
METHOD [access] [FINAL | ABSTRACT] [OVERRIDE] name [: result], its VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR and VAR_TEMP (VAR and VAR_TEMP mean the same: temporary), a body,END_METHOD. Listed after a class’s variables; in a function block, between its declarations and its body.- Every method variable and the result are temporary, “like the
variables of a function”: nothing is kept from one call to the next.
Reading a method’s output outside its call (
y := inst.outm1) or setting its input (inst.inm1 := a) is an error (6.6.5.4.7). - A method reads and writes its instance’s variables and externals; in a function block, its inputs (not R_EDGE or F_EDGE ones), outputs, VARs and externals, but not its VAR_TEMP or VAR_IN_OUT (6.6.7.2.3). A method’s variable may not have a name the class already uses.
- A method with a result is called in an expression, one without in a statement only; formal or not, EN/ENO as a function’s (Table 50). Overloading is out of scope.
- A method of the same instance is called
THIS.m(...); of another,inst.m(...)(6.6.5.4.4). - Access: PUBLIC anywhere the class can be used; PRIVATE only in the class itself; PROTECTED in the class and its derivations; INTERNAL in its namespace. PROTECTED by default (6.6.5.4.3 rule 5, 6.6.5.9). “All improper uses shall be treated as errors.”
Inheritance (6.6.5.5 to 6.6.5.8)
EXTENDS: one base only. A class extends a class; a function block extends a function block or a class; a class never extends a function block (Table 48 row 6, Table 53 rows 7a, 7b).- The derived type inherits every variable and every method but the PRIVATE ones (and INTERNAL ones outside the namespace). Declaring a variable its base has (unless PRIVATE there), a method named as a base variable, or deriving from itself is an error.
OVERRIDEreplaces a base method: same signature (name, result, every parameter’s name, type and order), same access; FINAL may be added. OVERRIDE where no base method is is an error.FINALon a method: not overridden; on a type: not a base.ABSTRACTon a type: never instantiated, holds at least one abstract method; an abstract method is implemented by every non-abstract derived type. Not with OVERRIDE; only in an abstract type. An abstract type may be the type of an input or in-out.- Dynamic binding (6.6.5.6): a call through
THIS, an in-out, a reference or an interface runs the method of the instance’s actual type. 6.6.5.6’s example: CIRCLE’s CF callsTHIS.PI(); in a CIRCLE2, which overrides PI, CF gives the more accurate value. SUPER.m()calls the base’s m, bound statically. Notinst.SUPER.m, notSUPER.SUPER.m.THISis a reference to the own instance; it may be given to an interface variable (ctrl(RM := THIS)).
Function blocks (6.6.7)
- A function block may have methods, a body, or both; with methods only, “this function block can also be declared as a class”.
- The body is not inherited: a derived block’s body is its own,
empty if it has none.
SUPER()in it runs the base’s body, “once … and shall not be in a loop”, never in a method (6.6.7.2.9). - A call of a block through a base-typed in-out or reference runs the derived block’s body: “the call of the function block shall be bound dynamically”.
- Inputs and outputs are always PUBLIC, outputs read-only from outside; VAR_EXTERNAL is always PROTECTED; a block’s VAR is PROTECTED by default, as a class’s.
Interfaces (6.6.6, Tables 51 and 52)
INTERFACE name [EXTENDS i1, i2], method prototypes (a name, a result, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT; no body, no access specifier: always PUBLIC),END_INTERFACE. An interface extends any number of interfaces; redeclaring a base’s prototype, or deriving from itself, is an error.- A class or block
IMPLEMENTSone or more interfaces, and implements each prototype with the same signature, PUBLIC or INTERNAL; one that leaves any out is ABSTRACT. - An interface is the type of a variable: a reference to an
instance of a type implementing it, or NULL, its initial value.
It may be given an instance, an instance of a derived type, another
interface variable of the same or a derived interface, or NULL;
compared with
=to another, equal when the same instance or both NULL. Calling a method of a NULL one “a runtime error will occur”. Not an in-out (6.6.6.2). - Assignment attempt
a ?= b(6.6.6.7):agetsbif whatbreferences is of a typeacan hold, NULL otherwise. Between interfaces, from an interface to a reference to a type, and from a reference to a base type to a reference to a derived one (downcast).
Polymorphism (6.6.8): through an interface, a VAR_IN_OUT, a reference, and THIS. A derived block passed to a base-typed in-out or reference must add no in-out of its own (it would be unset when the base’s call runs its body); whether it may add inputs or outputs is “Implementer specific” (6.6.8.3, 6.6.8.4; Table 53 rows 12a-12d).
Grammar (Annex A): Class_Decl, Method_Decl, Interface_Decl,
Method_Prototype, Access_Spec; THIS.x in Symbolic_Variable;
Invocation takes m(...), THIS(...), THIS.m(...) and
a.b.m(...); SUPER() a statement; Assignment_Attempt;
Interface_Spec_Init.
2. What the vendors do
| CLASS | THIS, SUPER | method access, none written | OVERRIDE | NULL interface | downcast | |
|---|---|---|---|---|---|---|
| CODESYS V3.5 | none: methods, EXTENDS and IMPLEMENTS on function blocks | THIS^.x, SUPER^.m() | PUBLIC | none found | 0, “methods can be called only after” assignment | __QUERYINTERFACE, __QUERYPOINTER |
| TwinCAT 3 | none, as CODESYS | THIS^, SUPER^; SUPER^() “Call of FB-body of base class” | PUBLIC (“equivalent to specifying no access modifier”) | none found | 0; check “<> 0” first | __QUERYINTERFACE |
| Siemens SIMATIC AX | yes, CLASS ABSTRACT, IMPLEMENTS a list | THIS.x, as Ed.3 | not documented (pages not readable) | METHOD PUBLIC OVERRIDE, FINAL OVERRIDE | compared with NULL | not found |
| PLCnext Engineer | none: methods of function blocks only, no inheritance or interfaces | THIS.m() | none: “an access specifier must be specified”, Public or Private only | - | - | - |
| RuSTy | yes; no body, THIS rejected in one | THIS^, SUPER^ | specifiers parsed and ignored | optional | fat pointer, NULL unchecked (a crash) | none |
| Siemens S7-1500 SCL | no OO documented by Siemens |
- Dynamic binding. CODESYS and TwinCAT dispatch a call through a
pointer, a reference, an interface, a base-typed VAR_IN_OUT, and a
method’s call of a method of its own block. RuSTy the same, through
a vtable;
SUPER^.m()and a call on a named instance are direct. - A method’s variables are reinitialized at every call (CODESYS,
TwinCAT, RuSTy), as Ed.3. CODESYS and TwinCAT add
VAR_INST, a method variable kept in the instance. PLCnext keeps a method’s array or structure result between calls, which Ed.3 does not. - A derived block’s body: RuSTy runs its own only,
SUPER^();for the base’s, as Ed.3. TwinCAT hasSUPER^(); neither it nor CODESYS says whether the base’s body runs without one. - ABSTRACT on function blocks and their methods: CODESYS and TwinCAT (build 4024); RuSTy parses it and an abstract method “returns the default value”. FINAL on blocks and methods: TwinCAT.
- Interfaces extend several interfaces (CODESYS, TwinCAT, RuSTy), hold function block instances (CODESYS, TwinCAT; they have no classes) or both (RuSTy). RuSTy refuses an interface as VAR_IN_OUT (E126), as Ed.3.
- Initialization methods FB_init, FB_reinit, FB_exit (CODESYS, TwinCAT; RuSTy a parameterless FB_INIT). Ed.3 has none: an instance starts at its declared initial values.
- What none documents: Ed.3’s PROTECTED default,
?=, and what calling a method through a NULL interface does.
Sources: content.helpme-codesys.com (CODESYS Development System:
_cds_obj_method, _cds_obj_interface, _cds_extending_function_block,
_cds_implementing_interface, _cds_method_call, _cds_pointer_this,
_cds_pointer_super, _cds_vartypes_var_inst, _cds_operator_queryinterface,
_cds_operator_querypointer, _cds_method_fb_init_fb_reinit);
infosys.beckhoff.com (tc3_plc_intro: methods 2530307467, calls
2527348875, SUPER 2528837771, function block specifiers 2530268811,
interface extension 3537333899, ABSTRACT 6413748235, interface pointer
4256428299 and 5680748299, __QUERYINTERFACE 2529176459, FB_init
5482088715); engineer.plcnext.help (2026.6, POU_Method) and plcnext.help
(PLCnext Engineer features); Siemens’ open-source SIMATIC AX library
github.com/simatic-ax/statemachine (the AX documentation is behind a
login); RuSTy at 11e8107 (book/user/language/inheritance.md,
methods-and-properties.md, book/technical/participants/03-polymorphism.md,
tests/lit/single/polymorphism). Siemens’ and PLCnext’s OO for classic
SCL was not found.
3. Where Ed.3 contradicts itself or is silent
- Is an access specifier required?
Method_Declrequires one ('METHOD' Access_Spec ...); 6.6.5.4.3 says the declaration “shall contain one” and in the next sentence that without one it is PROTECTED; 6.6.5.9 says PROTECTED “may be omitted”. - The order of the specifiers. The grammar has access, then FINAL
or ABSTRACT, then OVERRIDE; the examples write
METHOD OVERRIDE mbandMETHOD PUBLIC OVERRIDE DAYTIME, and 6.6.5.10 lets a VAR section’s specifiers come “in any order”. - A method of the own instance without THIS. 6.6.5.4.3 rule 13
and 6.6.5.4.4 say it “shall be called using the THIS keyword”;
Invocationtakes a bareMethod_Name(...). A barem()could also be a FUNCTION of that name (Table 48 row 14 only says “should not”). THIS.xorTHIS^.x. THIS is “a reference to the own class instance”, and a reference is dereferenced with^(6.4.4.10); yet every example writesTHIS.x,THIS.m(), neverTHIS^.SUPER.m()is not in the grammar.InvocationhasTHIS.and instance names,Subprog_Ctrl_StmtonlySUPER(); the text and its examples useSUPER.DAYTIME().- A method’s result in an expression is not in the grammar.
Primary_ExprhasFunc_Call(a function’s name only); the text says a method with a result “shall be called as an operand of an expression” (VALUE := CT.UP(INC := 5)). - A method of the base’s name without OVERRIDE. Only OVERRIDE without a base method is listed as an error; a derived method that has a base method’s name and no OVERRIDE is not addressed (an error, a hidden base method, or an override anyway).
- What a function block’s body is when the derived one has none. 6.6.7.2.9: “not automatically inherited … It is empty by default”; Figure 21’s right column shows each derived block as if it had the base’s variables written again, which rule 3 (“names … shall be unique”) forbids.
- Additional inputs and outputs of a derived block behind a base in-out or reference: “Implementer specific” (6.6.8.3, 6.6.8.4), Table 53 offering both.
- An interface as an in-out. 6.6.6.2: “Interfaces shall not be used as in-out variables”; 6.6.6.5.3’s explanation passes an instance to one “as function block input, output, in-out, or result”.
- What an interface references. 6.6.6.2 and 6.6.6.5: “instances of classes”; Table 51 row 4c and Table 53 row 6c: “a function block instance”; 6.6.6.7: both.
- An interface method implemented INTERNAL (6.6.6.4.2 error 3 allows PUBLIC or INTERNAL), though a prototype is “always PUBLIC”: called through the interface from outside the namespace, an INTERNAL method is reached where its specifier forbids it.
- What the runtime error is, for a NULL interface’s method call,
and for
?=there is none (it gives NULL). 6.6.6.5.2 suggests a “dummy” method as one way. - Copying an instance,
a := bof a class or an OO block, is not addressed; nor whether an instance may be an input (by value) or a function’s variable. - 6.6.8.4’s only example shows no dynamic binding: its
ROOM_CTRL sets
RM^.DAYTIMEand never calls the blockRMreferences, so neither body runs. - An abstract type as an input. 6.6.5.8.2: “An abstract class may be used as a type of an input or in-out parameter”; an input is passed by value, and an abstract class “cannot be instantiated”.
- Mistakes in the examples: Table 49’s initialization list
separates with
;(Struct_Init uses,); Table 50 callsCOUNTER.UPon the type and passes aSTARTUP does not have; 6.6.6.7.3 Example 3 declarespinstBase2and usesrinstBase2, and uses anITF3never declared; 6.6.8.3 Example 2 and 6.6.8.4 Example 1 writeVAR_INPUT: DAYTIME; aSUPER.NIGHTTIME()without its;;END_VAR;andEND_PROGRAM;. None changes a rule.
4. What iecf builds
In three steps, each a commit with its tests and book page, each usable alone. What Ed.3 states plainly is built as written; where it does not, as §6 decides.
Step A: methods and classes, bound statically.
- Parser: a CLASS is a POU of kind
Class, each method a POU of kindMethodin its type’smethods(between a block’s declarations and its body); access specifiers on a method and on a VAR section (VAR PUBLIC,VAR PRIVATE RETAIN), FINAL, ABSTRACT and OVERRIDE, in any order (D2). Each is a word only where a name follows it, so a variable may still be namedfinal; THIS and SUPER are reserved (E0018).THIS.x,THIS.m(...)andinst.m(...)are member expressions and calls as before. - Checker: a class’s sections (only VAR and VAR_EXTERNAL); a
method’s signature, its result only in an expression and a method
without one only as a statement (6.6.5.4.5); a method’s output read
or its input set outside its call (6.6.5.4.7); a method’s names
apart from its instance’s (rule 10); in a function block, no VAR_TEMP
or VAR_IN_OUT of the block in a method (6.6.7.2.3); access, for
methods and variables, from where the use is: the type, a derived
type, the namespace; a class instance never called. A method is
known by
TYPE__METHOD, as a FUNCTION by its name, and checked as one, its type’s variables in scope; each call is recorded for the generator. New errors: E0030 (access), E0031 (a part where Ed.3 does not allow it, an instance copied or made an input, D11). - C: a class is a function block without a body,
B_<NAME>_tandB_<NAME>_init, so a block may extend one. A method is a C functionM_<TYPE>__<METHOD>(ctx, B_<TYPE>_t *self, ...), its parameters as a FUNCTION’s (inputs by value, outputs and in-outs by pointer, EN/ENO as a FUNCTION’s), its VAR and VAR_TEMP C locals set at each call, its result returned.inst.m()andTHIS.m()call it directly: in step A no type has a derived one.THIS.xis generated asx. Setting an instance’s input or PUBLIC variable from outside,f.i := 3, which the generator had refused for any block, is built with it. - Symbols: a class instance’s variables, as a block’s.
Step B: inheritance and dynamic binding.
- Checker: EXTENDS (one base; a class from a class, a block from a
block or a class), the inherited variables and methods but PRIVATE
ones, OVERRIDE’s signature and access, FINAL, ABSTRACT (never
instantiated, its abstract methods all implemented below), SUPER,
SUPER()once in a block’s body and not in a loop, the errors of 6.6.5.5.5; a derived instance given to a base-typed VAR_IN_OUT or REF_TO (§6 D7). - C: a derived type’s structure starts with its base’s,
B_<BASE>_t base;, so a pointer to it is a pointer to its base (C99 6.7.2.1); a base’s variable isself->base.V_X. The root of a hierarchy starts withconst iecf_otype_t *type, which each_initsets to its ownTY_<NAME>: the instance knows what it is, for the whole run. Aniecf_otype_t(runtime/iecf.h;iecf_type_tis the symbol table’s) holds its base, its name and its table of methods,VT_<NAME>; step C adds its interfaces’ tables. The table is a C structure of function pointers, the body first for a block, then each method that can be overridden (not PRIVATE, not FINAL); a derived type’s starts with its base’s. Each slot takes the declaring type’sself; an override is reached through a thunk,M_<TYPE>__<METHOD>__vt, which converts it back. BUILT 2026-11-10: harness caseinherit. - Which call is bound how: through
THIS, a VAR_IN_OUT, a reference, or a block’s call through either, from the instance’s table (6.6.5.6, 6.6.7.2.9 rule 4);SUPER.m(),SUPER()and a call on a named instance directly, its type known. A FINAL method, or one no type overrides, directly too (6.6.8.5 NOTE). - A NULL reference is checked before the call, as
r^is: a fault NULL, the call not made, the statement stopped (§6 D8).
Step C: interfaces.
- Checker: INTERFACE and its EXTENDS (any number, no prototype
declared twice, no cycle), IMPLEMENTS (every prototype, same
signature, PUBLIC or INTERNAL, or the type ABSTRACT), an interface
variable’s values (6.6.6.5.1) and
=/<>,?=(6.6.6.7). - C: an interface variable is
iecf_itf_t { void *self; const void *table; }, NULL whenselfis. Given a named instance, its table is known (&I_<ITF>__<TYPE>); given THIS, an in-out, what a reference references or another interface, it is found at run time in the instance’s type (iecf_itf_of). Each type that implements an interface, itself or through a base, carries its type, as a type in a hierarchy does, and has its own table for each interface, its bases’ and theirs: a derived type’s override is in its own, so the type’s list is searched alone, not its bases’.?=looks the same way:iecf_itf_asfor an interface,iecf_itf_reffor a reference from an interface,iecf_ref_as(byiecf_otype_is) for a reference to a derived type. Interfaces never reach the process image nor the symbol table; instances never reach the image. - A method called through a NULL interface: as a NULL reference
(D8). BUILT 2026-11-10: harness case
interfaces. An interface as an output, a result, a global, in an ARRAY or STRUCT, and with an instance as its initial value, BUILT 2026-11-11:itfplaces. A PROGRAM’s input or output, connected, BUILT 2026-11-11:itfprog; a RETAIN one is E0027, as a RETAIN reference (docs/dependencies.md, decided florin).
Tests, each step: a test for each rule and each error
(crates/iecf-sema/tests/objects.rs); the book page
behaviour/objects.md, without checked rows, which evaluate an
expression and cannot declare a class; harness
cases with traces worked by hand: methods (A), inherit (B: CIRCLE,
LIGHT2ROOM with SUPER, Figure 21’s SUPER() chain, a base-typed in-out
and reference), interfaces (C: ROOM_CTRL, ?= as 6.6.6.7’s
examples). The reference compiler has no OO, so all are iecf’s only
(reference-skip).
Prokop needs nothing changed: a project without OO compiles as today, and the Workbench gets the new errors live. The cost is the largest yet: step A about a namespace’s (a parser, checker and generator each touched), B and C each about as much again, B the most (the tables, the initializers, the dispatch).
5. Not in this step
- CODESYS’s spelling:
THIS^,SUPER^, overriding without OVERRIDE, PUBLIC when no specifier is written. CODESYS, TwinCAT and RuSTy write OO so; Ed.3 does not. As an opt-in extension later (docs/extensions.md), if Prokop users bring CODESYS code. - VAR_INST (CODESYS, TwinCAT): a method variable kept in the instance. Not Ed.3.
- FB_init, FB_reinit, FB_exit (CODESYS, TwinCAT) and
__QUERYINTERFACE,__QUERYPOINTER: not Ed.3;?=does the second’s work. - Properties (
PROPERTY, GET and SET): Ed.4, with the step 4 additions. - Overloading of methods: “not in the scope” of Ed.3 (6.6.5.4.3 NOTE 1).
- Methods in LD or FBD: iecf compiles ST and SFC.
6. Decisions
All twelve DECIDED 2026-11-10 (florin: “All as recommended!”).
D1. A method or VAR section without an access specifier is
PROTECTED, as Ed.3 says three times (6.6.5.4.3 rule 5, 6.6.5.9,
Table 48 10d). CODESYS and TwinCAT make it PUBLIC; PLCnext requires
one; no vendor documents PROTECTED. Calling a PROTECTED method from
outside is an error whose message says to write METHOD PUBLIC. The
other way: PUBLIC, as CODESYS, which would make code written for
CODESYS compile unchanged and code written for Ed.3 more open than its
author meant.
D2. The specifiers in any order, each at most once: METHOD OVERRIDE PUBLIC m and METHOD PUBLIC OVERRIDE m both, as the examples
write them and as 6.6.5.10 allows for a VAR section, though the grammar
gives one order.
D3. A method of the own instance is called THIS.m(); a bare
m() inside a method or body is a FUNCTION’s call, an error if there
is none, the message saying to write THIS.m(). As 6.6.5.4.3 rule 13
and 6.6.5.4.4 say, though Invocation allows the bare form; it keeps
a method and a FUNCTION of one name apart (Table 48 row 14). The other
way: a bare m() is the method if the type has one, as RuSTy and (by
their examples) CODESYS and TwinCAT.
D4. THIS.x and SUPER.m(), as Ed.3 and SIMATIC AX write them,
though THIS is called a reference; THIS^ refused, until the CODESYS
spelling (§5).
D5. SUPER.m() and a method’s result in an expression, though the
grammar has neither: the text and every example use them, and
without them SUPER and results have no use.
D6. A method with a base method’s name and no OVERRIDE is an error, the message saying to write OVERRIDE: Ed.3 asks OVERRIDE of every override, and without the error the method would override silently or hide its base’s, whichever iecf chose. CODESYS has no OVERRIDE; RuSTy does not require it.
D7. A derived block behind a base-typed in-out or reference may add inputs and outputs, not in-outs. The added inputs keep the values they have (the call through the base sets only the base’s); an added in-out is an error where the instance is given or its reference taken (6.6.8.3, 6.6.8.4, which leave the first “Implementer specific”, the second an error). Table 53 rows 12b and 12d.
D8. A method or block called through a NULL reference or interface is
a fault NULL: not called, the statement stopped, as r^ of a NULL
reference (decided 2026-11-08). Ed.3 says “a runtime error”; CODESYS
and TwinCAT say only to check first; RuSTy crashes.
D9. An interface is not an in-out, and references class and block instances both: 6.6.6.2 forbids the in-out, as RuSTy (E126); 6.6.6.7 and Table 51 row 4c take block instances, as CODESYS and TwinCAT, whose interfaces hold nothing else.
D10. An interface’s method may be implemented INTERNAL, as 6.6.6.4.2 allows, and is then reachable through the interface from anywhere: the interface’s prototypes are PUBLIC. The other way: PUBLIC only.
D11. An instance of a class, or of a block with methods, a base or
an interface, is not copied with :=, not an input and not a
FUNCTION’s or method’s variable. A copy into a base-typed variable
would cut the derived part and keep the derived type’s table; an
input is a copy. An abstract type is then an in-out, a reference or an
interface only, which reads 6.6.5.8.2’s “input” as the in-out it can
only be. A plain block keeps what it has today. An array or a
structure that holds such an instance, anywhere in it, is refused the
same way, as it would be copied with it (applied 2026-11-10, when
instances in arrays and structures were built).
D12. An ABSTRACT method has no statements: its body would never run. RuSTy runs one, giving the result’s default.