Starting point for this issue is comment !785 (comment 1704170) while discussing trouble in handling function blocks, as they allow both variable access and a function call.
The basic trouble is that the model classes don't always provide enough information to easily code features. As a result, additional special case code is needed around the model framework to handle all features.
By improving the model classes instead, lots of that additional code can be avoided.
Changes to make:
Make PlcDerivedType useful as reference to a type declaration. Currently it only has a name (the same as a type declaration that exists). (!863 (merged))
Add the type of a value to expression classes. Add type to PlcExpression, all derived classes set it to null. This may cause need to temporarily handle null type values further down. (!875 (merged))
Cleanup the enumerations (currently tightly integrated with the type generator). (!875 (merged))
Create an overloaded type class (ANY_INT etc). To avoid getting generic types in expressions, derive this from a new super-type of PlcType. (!875 (merged))
Add types to function definitions, including generic types. (!875 (merged))
Compute result type for function applications (that is, resolve the generic types to concrete types).
Add type to PlcFuncAppl
Finally, remove all temporary null type handling code.
EDIT: Fixed markup of PlcStructLiteral.
EDIT: "integrated with the type generator" rather than "integrated into the type generator".
EDIT: Less space between items (if one of them has an empty line, they all get more spacing).
Further down in the linked discussion is !785 (comment 1748638) where we concluded that adding a function type would be useful for function blocks.
This indeed holds for the S7 output. To have both fncBlk.ET := ... and fncBlk.TON(...) you could consider fncblk to have a tuple (TIME ET, func ... TON(...), ...).
In that way fncblk can have both value fields and a function to call.
However, S7 is the exception here. The standard specifies what the iec target is doing, that is (for button-lamp):
where the main program shows that timer0 has fields, like ET (lines 31 and 49) and is a function (line 29 and 47) at the same time.
I don't see how you can express this in a type system that we have in languages like Chi or Cif, without merging the function type with the struct type.
More traditional PLC code also has structs without a function and functions without struct data. To handle that, we either need to have a pure function type and a pure struct type as well, or be able to express in the combined struct+function that one of them is not available.
It seems useful to give plain functions, function blocks and POUs all the same function type.
That means the function type has
Optional input variables
Optional output variables
A result-type.
The name of the function would be in the FuncAppl expression class then.
It also needs a type name (eg TON). Not sure it belongs here, we could also use a derived type for that after extending it with a type. The above function type is then stored in that type.
Another consideration is whether structure types should be unified with this as well, since they basically are just a bunch of (read+write) variables. The function result type would not exist in that case, and input and output variables are the same. I am somewhat inclined not to do this.
Functions and function blocks can have generic types in their definition, but not in their applications. Give the above merge to the same function type, it's likely feasible to have POUs also allow generic types in their definition. No idea if that is allowed. I also don't see a convincing reason to do that, as CIF cannot express such generic types.
I can't oversee all you suggest in this issue. Probably due to a lack of PLC knowledge, combined with not working on the PLC code generator much myself. Some thoughts though:
Do POUs always have a result type? For instance, the main program is a POU, but it doesn't return anything, right?
I don't get why the name of the function would be in the FuncAppl. Doesn't a function have its own name, with its declaration? How would you link a FuncAppl with just a name of a function (no reference) to the declaration of that function? Doesn't make a lot of sense to me. Maybe we need a function type, a function declaration, and a function application?
What is the type name, like TON. Isn't that just a function named TON with a certain function type? I assume a function declaration has exactly that, a name and a type, and maybe a body/implementation, if it is not a built-in one?
I would indeed keep structures separate. They may look like functions, and share some characteristics, but they are not functions. In general, we could make one object that represents everything, but that doesn't seem like a good idea either. Keep concepts separated.
If we don't need something, like POUs with generic types, then let's not allow that. Could just in the POU constructor, that that gets a function type, check that the type has no generic types? If we don't need it, and we're not sure it is even allowed, and thus have no examples to see whether we handle it correctly, it is best not to allow it.
Do POUs always have a result type? For instance, the main program is a POU, but it doesn't return anything, right?
The main program is also never called in a function-like way from the PLC code, so return type is non-relevant. But no, it doesn't have a return type that you specify. It's probably a POU as that is a highly generic concept, the more precise phrase needs a more specialized classification thus, but no idea what that would be. Perhaps "used-defined function" (and "function-block" I guess).
I don't get why the name of the function would be in the FuncAppl.
The PLC output shows the function name, so it has to be somewhere. Assuming that function-type is basically (param, param ...) -> type, Funcappl is the only logical remaining spot. Note I didn't say anything about the internal structure of FuncAppl. Currently the function description (name, how to call, parameter names) is separate from the FuncAppl, and I like that.
I am far from sure how things are going to work out in the details, in particular for the function blocks.
Maybe we need a function type, a function declaration, and a function application?
Yup, we do, and we already have the latter two (There is a "description" rather than "declaration" with more details, such as allowed ways to call, names of the parameters, priority of the infix notation, and mathematical operation that it performs.)
Function-type for pure functions is not difficult and was not needed until now, since functions as data don't quite exist and expressions don't have types. Function-type for function-blocks is more complicated, but there is too much other stuff not quite right in the type domain to be able to understand what the solution should do.
What is the type name, like TON. Isn't that just a function named TON with a certain function type?
Yes for most PLC systems there is only TON as type, and then you 'call' the variable. Eg timer0: TON; and timer0(IN := curTimer = 0, PT := T#1D);.
S7 however has iec_timer as type (hidden in TIMERS.DB and a special case in the code), and then TON as the specific function to call on the variable, like timer0.TON(IN := "DB".curTimer = 0, PT := T#1D);.
In cif2plc you can just insert random text here as it goes straight from names to a line of text. In plcgen, there are model classes in-between, so they need to store TON as function name to call in some currently unknown way.
In either case, you can also access the input and output parameters with the variable, like timerValue0 := timer0.ET;
So S7 is nicer here in making an explicit difference in calling the function. I don't know what happens if you want to call a user-defined POU for S7. I don't see a way to add a function name then as they did for timers. It doesn't matter much though, since other PLC systems don't have it either so in any case we need to support myvar(...) calls together with myvar.field access.
I would indeed keep structures separate.
Agreed.
If we don't need something, like POUs with generic types, then let's not allow that.
Generic types are already needed for plain functions. SEL(b, v0, v1) only works for boolean v0 and v1 in S7. To select eg INT values, it must be written as SEL_INT(b, v0, v1). This is one of the reasons why we need types in expressions.
Type overloading isn't very complicated. A generic type like ANY_INT must be resolved to exactly one integer type, for all instances in the type definition. Also, afaik you can't have arrays or structs of generic types (or at least I am not aware of a case where it's needed).
In other words, a type is either a normal concrete type, or it is a single ANY_something type (and the standard has more generic types than we support).
Could just in the POU constructor, that that gets a function type, check that the type has no generic types?
That's why I want to add an PlcAbstractType base-class below the current PlcType and derive a generic class from the new base-class. Then you can have generic type declarations using PlcAbstractType in eg function definition descriptions, and PlcType everywhere else. This works because mixing concrete types with generic type (eg array of a generic type) isn't needed as far as I know. Then you don't need to check at runtime, code won't even compile if you try to sneak a generic type in a supposedly concrete type.
Likely that will break things badly if we need to support user-defined POUs with generic types or if we ever need to get mixes of generic types and concrete types, but let's worry about that when it happens :)
Note we currently have no way to express SEL_INT-like cases. If it is needed now, my shortcut solution would be to add a type to the funcAppl object construction method.
We also don't have a way to state when to (or not to) add type suffixes. My first thought was to add them everywhere as that is seemingly allowed. Eventually, likely the target should make such decisions.
Obviously, this is all a non-issue until we have types in the expressions to use for deriving such a suffix.
So, we already have a function description (which is kind of like the declaration) and function applications. But not yet function types. Then I would:
Add function types.
Put/keep the name in the function description.
Function description gets a function type, and some fields of the function description can be removed as they are covered already by the function type.
I don't see why the function application must get the name. That still feels wrong. You indicate it is the only solution, but my idea doesn't require it.
Note that this requires all functions to have a description, also builtin ones, etc, but I don't think that is a problem. I vague remember we have them already.
As for calls to function blocks. We can just make timer variables. Depending on the target (we need to query it), we may need to generate different types. So be it.
For the calls to function blocks, we can also query what the target supports and generate timer0(...) or timer0.TON(...). We need the target to indicate it wants this particular syntax, we query it for the function call/application, and generate it. I don't think we need to store TON as function name. It is always TON for a call to a variable of type iec_timer. We could for instance ask the target for a postfix for a call to a certain variable, giving it the variable. The variable has iec_timer as type, and the S7 target knows the postfix to use in the call.
If we need to support myvar(...) calls together with myvar.field access, then that is just different code generated for certain expression nodes.
In the code generator for those expressions, we query the target and generate working code.
Regarding ANY_INT and SEL, etc. Wouldn't it be easier if we just create one SEL function as we do now if the target supports it with overloading, and multiple SEL_* variants if the target doesn't?
Then we just use the right functions depending in the expression nodes we're generating. No need for generic types. If all targets support the specific typed variants, we don't even need to query the targets and we can indeed just use the specific ones.
I like the idea of type hierarchy that leads to compile errors over runtime checking. But, I'm still not sure I get why we need generic types at all. Can't we just have more variants of the same thing? Like the SEL_* variants.
Maybe what I miss is the overview of all this. This issue has a description with a list of stuff that needs to be done (deltas). I only see change deltas and a lot of discussion.
Maybe we need some trees of classes that we'll get, what fields they'll have, etc (types, descriptions, applications). So what is the intended end situation. With that, we could discuss if that is the desired end situation, rather than only deltas, which I find difficult.