-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Generated structs exposed in header files, reactions linkable from separate source files, and updated C target preamble visibility #1599
Conversation
This syntax was discussed in our meeting with Magnition this morning:
I think that the reason for wanting to do this syntax change can be articulated more precisely. I claim that port access using dot notation (e.g., Symptom 1The code generator contains "scary code" that generates structs solely for the purpose of allowing dot notation access in C. This is accidental complexity; it is not essential to any interesting problem we are trying to solve, and instead is merely an issue of how we interact with C syntax. And our code quality problem is not merely that we have to maintain more code; it is also that the code we maintain has to pass around information about the structure of the program -- in particular, the dot notation expression that is being used to access a port. This information has to be propagated through every level of the code generator; it cannot be abstracted in any intermediate IR because the final code that falls out has to reflect the form of the expression. This issue will not be completely hidden from end users. In order to understand what prototypes they have to implement, end users will have to read the generated header files, including the types which are referenced in the prototypes of the functions they have to implement. Do we really want to clutter these generated header files with meaningless, "do nothing" structs that are just there to allow this dot access notation? Symptom 2Dot notation syntax stands in the way of refactoring. Suppose we have the following reactor: reactor r {
input in: int
output out: int
reaction(in) {=
lf_set(out, in->value);
=}
} And it gets refactored to this reactor: reactor r {
input in: long
output out: int
transducer = new LongToIntConverter
in -> transducer.in
reaction(transducer.out as in) {=
lf_set(out, in->value);
=}
} Without aliasing using Furthermore, the name The core problemMainstream languages allow most expressions to be treated as reducible in the sense that they can be replaced with their value (in this case, with the port that is being referenced). When expressions are not treated as reducible, they cannot scale without becoming hard to understand. These structs that we are having to generate, for example, reflect the fact that whenever we have a port that is accessed using dot notation, it becomes "its own concept," requiring its own representation in a header file; and furthermore, these extra concepts grow as the number of expressions that you compose grows because you cannot replace the dot-notation expressions with something (a port) that they evaluate to. Symptom 2 is also a consequence of this fundamental error in the sense that because the hierarchical reference is "its own concept," it cannot be used in the place of a regular port. There is no fundamental reason for this incompatibility. |
331e02a
to
b210197
Compare
In response to #1599 (comment): How about we:
This is backward compatible and should not get into anyone's way. |
84d64f6
to
f64c4ba
Compare
c72bbc3
to
24baa72
Compare
This refactoring example is another instance of the round tripping problem. I wonder how many such problems still lurk, waiting to be discovered as we gain experience... |
This PR now involves generating code that is specific to a given reactor constructor into a separate file. This change is motivated by the need to reduce name conflicts, and is part of the effort to expose generated types in generated header files. It is also a long-standing to-do item that I think has the potential to make the generated code easier to work with. If my implementation works, this PR will also allow us to avoid generating code multiple times for reactors that are instantiated multiple times with different names, which can let us modestly reduce code size. This also gives us the opportunity to re-assess how we deal with file-level preambles. The semantics of preambles have been discussed in #1372 and #32, and probably in a few other issues. If we take the stance that they should be for Is everyone OK with this? |
Unfortunately, global variables are sometimes needed. For example, when targeting the nRF52, initialization of hardware interfaces has to happen only once, regardless of how many instances of a reactors use that hardware. See for example Display.lf. |
In that case, we can define |
OK, it would be good to test that idea. It should be tested with inheritance also. I.e., all reactor classes that use some shared hardware should subclass a common base reactor, and the global variables and initialization code should be declared there. |
This sounds good to me! Removing the dot notation syntax would be a great relief and simplify the code generation also in other targets.
I am not sure if I understand how this would work. Are you saying that the file level preamble would be code generated into a header file, while the reactor-level preamble is generated into a |
I am opposed to changing the syntax of LF to avoid a perceived awkwardness under the hood in the C target implementation. Specifically, I am not in favor either of referring to a port x of a contained reactor y as just x, nor am I in favor of renaming it using "as". The cost of this change is high (it will have to be supported in all targets and will complicate the documentation and tutorials). Moreover, it creates yet another round tripping problem. What if I later add a port named x to y? What if I renamed it z and later add a port named z? |
I agree that we would have to change the documentation and tutorials and it could make them more complicated.
For the record, there is a bit more to it than that... For example, Christian seems to have suggested that other targets may have similar problems. There is also a bit more explanation in the comment where I introduced the idea.
Yes, I agree that the proposal could be involved in round tripping problems and could in some cases make them worse. I actually argued in my earlier comment that that it would alleviate round tripping problems rather than making them worse; however, I agree that there are lots of ways to make any two files incompatible with each other, and that port aliases can add more such ways. Overall, I think I understand the concerns and I like Marten's idea to avoid making any backward-incompatible changes. This issue with hierarchical references can be treated as an experimental, undocumented feature that we can implement (for just the C target, and only for the non-inlined reactions), try out, and maybe discuss again at another time. If we do it the way Marten suggested, then the current coding experience in Lingua Franca will continue unchanged, and nearly all the effort involved in this PR will be in non-disruptive refactorings that we need to do anyway. |
The dot notation creates awkwardness in all target generators, precisely because it requires to create a new object just to give the reaction something that it can use to reference the port. This has been a challenge in C++ and was even more difficult to implement in Rust. The code in the generators is complex, difficult to maintain, and after all only there for cosmetics. I would be very happy if we could just get rid of this "feature".
I don't understand why this would be problematic. Let's try to make this more concrete:
In this example, I cannot add a port
This is not a round-tripping problem, as we add something to the reaction signature and this something just needs to fulfil certain constraints. Also, if we now modify
we can do this without problems. We can leave Maybe a less disruptive solution would be to make I must say, though, that I would love to have the renaming option available for all ports and triggers. In programming, names matter and good names are part of the documentation. Maybe a port has a more specific meaning in the context of a specific reaction than it has on the outside interface of a reactor. Maybe also the port of a contained library reactor has an ambiguous name and we want to clarify. So why not have this feature? It should be quite easy to implement (in the C++ target it is trivial), and I also don't see why it would be difficult to document. That said, I agree that we are currently lacking the man power and this is not high priority, so this idea should be added to the backlog. |
See #1599 (comment) for discussion
As a (late) response to earlier comments from @cmnrd:
Yes.
I am in the process of implementing this idea and getting tests/benchmarks to compile with this change. So far I have not run into problems. The way I am doing it right now separates declaration and definition by putting declarations in file-level preambles and definitions in reactor-level preambles. AFAICT this seems to be working OK. A potential downside is that users are forced to put declarations in file-level preambles even when only one reactor needs them. I do not regard this as a problem, however, since in all the languages I know imports are commonly done at file-level. Indeed, even though I think they can be scoped in Python and TypeScript, it is unusual and even confusing to do so.
There may well be weird issues, but I am not yet aware of any in particular. Superusers who want to do creative tricks with their |
I find the approach interesting. At the same time, I find the constraint to place definitions only in reactor level preambles a bit strange. Consider a case where a couple of reactors use a specific data type. We could place the declaration of the data struct as well as the declaration of some API functions for manipulating the struct in the file level preamble. But where should the definitions go? As I understand, we would need to put them in the local preamble of one reactor, but which? Perhaps the only real answer to this question is that preambles shouldn't be used for this and the type and functions should be declared and defined in external header and .c files. |
OK, I understand your concern now. It would be easy to use the This issue is not too complicated and is very easy to change as we find out more about what LF programs will look like in practice. I propose to table the discussion for now. |
See #1599 (comment) for discussion
891bec7
to
f6c0c2f
Compare
See #1599 (comment) for discussion
fb1a4a0
to
ca532c1
Compare
This is to be code generated.
No hierarchical inputs/outputs yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This generally looks very nice, but I left some questions. I'm a little worried about the change that now requires a bunch of includes in the preamble that didn't used to be necessary.
org.lflang/src/org/lflang/generator/c/CReactorHeaderFileGenerator.java
Outdated
Show resolved
Hide resolved
057fa36
to
dc4177c
Compare
I think that in the long run this might not be what we want, but it reduces the impact of this and the accompanying reactor-c PR on existing LF programs.
dc4177c
to
a4dd88c
Compare
The error gets reported with an unexpected line number (not even in the right file) when a mistake is inserted into the type for the list of time values. I do not want to fix this right now (honestly it is the C++ compiler's fault, I do not even know how we would fix this), so instead I will try to cover it up.
I do not know how I accidentally put these changes here but anyway they were wrong and are the reason why the unit tests have been failing.
The unit tests were failing for a while because a few typos were corrected in the validator but not in the validation tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Only nitpicks.
91427df
to
d452c97
Compare
Waiting to pull the trigger on this one until lf-lang/reactor-c#189 has been merged... |
I will change the test so that it passes. This is a slight regression in the quality of the error message due to the change in the grammar that is introduced by this PR. It is a warning sign, but I think we can tolerate it. |
This is opened in response to feedback from Magnition. It allows LF files to be written without requiring target-specific code to be mixed into them.
UPDATE: This PR has come to encompass substantially more than just the small changes that I intended. I explain:
Definitions
Declarations are user-visible if they are intended to be
#include
d in the LF programmer's C code. User-visible generated code can therefore be expected to be viewed frequently as a reference for how C code has to interact with LF code.Declarations are mangled if they are not intended to be user-visible.
The main
.c
file is the one that hasinitializeTriggerObjects
.Change Summary
This PR:
.c
file and into mangled.h
files. This includes mangled self structs, mangled auxiliary structs such as port and action structs, and mangled reaction function declarations..c
file and into mangled.c
files. This includes mangled reaction function definitions..h
files. This includes mangled self structs, mangled port structs of the current reactor, mangled port structs of the contained reactors (non-recursively, down to just one level of hierarchy), and mangled auxiliary structs such as action structs.#include
others in their entirety. It also allows us to present simplified versions of the self structs, without all of the implementation details which are not intended to be user-visible.Rationale
.h
file. However, I do not feel strongly about this and am willing to change it if other people object.Work that will be left for future PRs
master
also has this problem, since it copies data into local structs, and the changes are backwards-compatible, so this is not really a regression.Companion PR in reactor-c: lf-lang/reactor-c#189