Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specification is too lax regarding the grammar of equations in equation sections #2462

Closed
casella opened this issue Dec 3, 2019 · 15 comments
Labels
discussion Indicates that there's a discussion; not clear if bug, enhancement, or working as intended worksforme

Comments

@casella
Copy link
Collaborator

casella commented Dec 3, 2019

I analyzed this issue together with @fedetft, @albertoleva, @skeru, @silseva, @drblallo, @agosta, and @nikolicmarina, while working on the implementation of an experimental Modelica compiler.

The Modelica Specification, Section 8.3 allows generic component-reference function-call-args non-terminals in equation sections.

However, if we are not mistaken, the semantics is only specified in three very specific cases, namely:

  • reinit() in Section 8.3.6
  • assert() in Section 8.3.7
  • terminate() in Section 8.3.8

Compilers such as OpenModelica and Dymola actually follow the grammar and also accept arbitrary function calls, such as Modelica.Utilities.Streams.print(x), or sin(2*x) which have no definite meaning in the context of equations, that can be reordered arbitrarily, and hence produce a meaningless and implementation-dependent result.

We can see no use for function calls in equations sections, only in algorithm sections they make actual sense. In any case, the semantics of anything beyond the three cases mentioned above is currently unspecified, which is not good.

Our suggestion is to restrict the grammar to indicate that only those three cases are accepted. This makes building a new compiler much easier. If there are other meaningful use cases, their semantics should be clearly defined somewhere in the specification.

@casella casella added the discussion Indicates that there's a discussion; not clear if bug, enhancement, or working as intended label Dec 3, 2019
@casella casella added this to the ModelicaSpec3.5 milestone Dec 3, 2019
@adrpo
Copy link
Collaborator

adrpo commented Dec 3, 2019

I think you're missing some operators, i.e. Connections.branch, Connections.potentialRoot, Connections.root for example. Having printouts in an equation section can help with debugging even if the order doesn't matter, so I wouldn't disallow it.

@adrpo
Copy link
Collaborator

adrpo commented Dec 3, 2019

Also, an equation like component-reference function-call-args surely could be needed in an initial equation section to initialize some external C variables. Yes, you could use an algorithm section as well for this.

@HansOlsson
Copy link
Collaborator

In addition to the previous cases we also have e.g. Modelica.Fluid.Utilities.checkBoundary (and Modelica.Media.R134a.R134a_ph.phaseBoundaryAssert).

As an example the model Modelica.Fluid.Sources.Boundary_ph contains a call of Modelica.Fluid.Utilities.checkBoundary - which is perfectly ok.

That is actually explicitly mentioned in https://specification.modelica.org/master/Ch12.html#pure-modelica-functions saying:

A call to a function with no declared outputs is assumed to have desired side-effects or assertion checks. [A tool shall thus not remove such function calls, with exception of non-triggered assert calls. A pure function, used in an expression or used with a non-empty left hand side, need not be called if the output from the function call do not mathematically influence the simulation result, even if errors would be generated if it were called.]

And finally a when-clause according to the grammar contain equations, and inside a when-clause it is normal to have more complicated "procedure" calls as they are only triggered according to the when-clause (thus the statements about unpredictability are not relevant). We could obviously separate what is allowed inside when-clause from what is allowed at the top - but that will make the grammar more complicated for no obvious gain.

Thus it seems this is based on a misunderstanding and the issue shall be closed.

@casella
Copy link
Collaborator Author

casella commented Dec 4, 2019

Before we close the issue, I would like to get some comments from my colleagues that are actually writing the compiler.

Let me try to summarize what are the possible occurrences of the component-reference function-call-args terminal. The ones in this first group have their semantics described somewhere in the specification

  • operators that affect the simulation flow: assert(), terminate()
  • calls to reinit() (only within when clauses)
  • calls to functions that indirectly can affect the simulation flow, e.g. functions containing calls to ModelicaError() orassert()
  • calls to operators Connection.* that are used to build connection graphs for overconstrained connections

There are also some other potential uses mentioned by @adrpo:

  • calls to print functions for debugging
  • calls to functions to initialize external stuff in initial equation sections

Regarding these two, there is no explicit mention of how those should be handled in the specification. Even if we do not change the grammar, I believe that we should say something about how those are expected to be handled, more specifically how and when they are expected to be called.

@skeru
Copy link

skeru commented Dec 4, 2019

Perhaps it is a misunderstanding.
However, we would like to seek clarification on this statement.

A call to a function with no declared outputs is assumed to have desired side-effects or assertion checks. [A tool shall thus not remove such function calls, with exception of non-triggered assert calls. A pure function, used in an expression or used with a non-empty left hand side, need not be called if the output from the function call do not mathematically influence the simulation result, even if errors would be generated if it were called.]

It seems to me that Modelica language specification allows a model to contain functions calls that the compiler is supposed to erase. This is the case of pure functions in the previously mentioned case.
This is a bit weird. However, there is a defined semantics.

There is the case of assertion checks, which are functions with a defined semantics. However, the lack of specification regarding the timing when those assertions should be verified leaves open the possibility to implement assertion checks before the initialization problem, or after the simulation.
Such possibilities are obviously not useful at all. Yet they are perfectly legal, as it seems to me.
It is certainly possible to leave to the compiler implementation the scheduling decision.
However, we are asking if you do not want to restrict any further this decision with a more detailed specification.

Eventually, there is the case of functions that do not fit any of the previous cases.
Not being such functions clearly stated as pure functions, the compiler can only take two decisions:

  • try to understand whether such function call mathematically influences the simulation result or not. This is a classical theoretical computer science problem, which can only be solved empirically. The compiler shall thus run the simulation with and without the function call to understand whether such function call mathematically influences the simulation result or not. In case it influences the result, the compiler will generate the code with such function.
  • assume they are all not pure functions, and thus behave as they were assertion checks.
    However, we know somehow they influence the simulation results. As there is no specification about their scheduling, they can be scheduled anytime during the simulation. Such function call can be executed an arbitrary number of times for each simulation step. This behavior is highly inefficient, potentially not desired, and, of course, perfectly conforming the specification.

Although the first two cases have a semantic which is a bit confusing (at least for computer science engineers), we believe it is the latter case that needs a little more attention.
Restricting the grammar is one option to solve this issue.
Another option would be to improve the specification and better explain the intended behavior for all the cases covered by the grammar.

@HansOlsson
Copy link
Collaborator

Perhaps it is a misunderstanding.
However, we would like to seek clarification on this statement.

A call to a function with no declared outputs is assumed to have desired side-effects or assertion checks. [A tool shall thus not remove such function calls, with exception of non-triggered assert calls. A pure function, used in an expression or used with a non-empty left hand side, need not be called if the output from the function call do not mathematically influence the simulation result, even if errors would be generated if it were called.]

It seems to me that Modelica language specification allows a model to contain functions calls that the compiler is supposed to erase.

No, the compiler is allowed to erase the call - not supposed to.

There is the case of assertion checks, which are functions with a defined semantics. However, the lack of specification regarding the timing when those assertions should be verified leaves open the possibility to implement assertion checks before the initialization problem, or after the simulation.

Clearly not before the initialization in most cases:

If the code contains y=foo(x); (and no other use of y) the code needs a value for x before computing y and calling foo.
If the code contains assert(x<0, ".."); the code needs a value for x before checking the assertion, and thus checking the assertion before initializing x makes no sense.

And merely checking that x<0 is true for one value of x after the simulation is not sufficient.

Restricting the grammar for equations wouldn't help you, as the same problem you perceive exists for

equation 
  a.foo(x1);
  a.foo(x2);

also occurs for

algorithm
  a.foo(x1);
algorithm
  a.foo(x2);

@HansOlsson HansOlsson removed this from the ModelicaSpec3.5 milestone Dec 4, 2019
@skeru
Copy link

skeru commented Dec 4, 2019

No, the compiler is allowed to erase the call - not supposed to.

This makes more sense. However, it is not what I understood from the statement.
Probably a sentence rewriting in the specification might help.

Clearly not before the initialization in most cases:

If the code contains y=foo(x); (and no other use of y) the code needs a value for x before computing y and calling foo.
If the code contains assert(x<0, ".."); the code needs a value for x before checking the assertion, and thus checking the assertion before initializing x makes no sense.

And merely checking that x<0 is true for one value of x after the simulation is not sufficient.

We understand there are possible schedules that do not make sense.
Could you please formalize all the requirements for the scheduling in the specification?

@fedetft
Copy link

fedetft commented Dec 4, 2019

If the compiler is allowed to elide a call but not required to, assuming this function can raise an error, a benchmark can be made that fails with one compiler and works with another, despite both being standard-conforming.

Also, does the specification say whether asserts (or functions in general) are to be called also for rejected time steps of the solver? In my opinion that's another gray area.

@HansOlsson
Copy link
Collaborator

This is not a general forum for discussing how to implement a Modelica tool.

If the compiler is allowed to elide a call but not required to, assuming this function can raise an error, a benchmark can be made that fails with one compiler and works with another, despite both being standard-conforming.

That is generally what happens when optimizations are allowed. The alternative would be to require that 0*foo(x) must still call foo in order to ensure that foo does not trigger assertions.

Also, does the specification say whether asserts (or functions in general) are to be called also for rejected time steps of the solver? In my opinion that's another gray area.

If an assert triggers for a rejected time step it will just reject it for an additional reason. Note that assert does not stop the simulation, as it effect is instead: "The current evaluation is aborted. The simulation may continue with another evaluation."

@skeru
Copy link

skeru commented Dec 4, 2019

This is not a general forum for discussing how to implement a Modelica tool.

We are simply pointing out parts of the specification that lacks of detail, when such deficiencies might impact on the simulation output.
@HansOlsson you just pointed out that there exists schedules that are not desired, but perfectly compatible with the language specification.

Is this the right place to discuss Modelica specification ambiguities?

@fedetft
Copy link

fedetft commented Dec 4, 2019

This is not a general forum for discussing how to implement a Modelica tool.

Form my understanding, it appears that some part of the Modelica specification are based on a "common sense" shared among current Modelica implementers, but not explicitly written in the specification. The goal of this discussion is to possibly improve and clarify the specification.

In detail, the semantics of when functions are called (whether they are assert or not) appears to be unspecified, to the point that a restriction of the current specification to meaningful cases only is an option to be considered.

For example, how many times shall this program print "test"?

model test
equation
 Modelica.Utilities.Streams.print("test");
end test;

By the way, other languages such as C/C++ would call foo() also in the 0*foo(x) case as they must assume the function could have side effects, so the current "common sense" is not universal.

@casella
Copy link
Collaborator Author

casella commented Dec 4, 2019

For example, how many times shall this program print "test"?

model test
equation
 Modelica.Utilities.Streams.print("test");
end test;

Since Modelica is a declarative, equation-based language, this question may actually be ill posed. For example, the related question 'how many times is the sine function computed by this program'

model foo
  Real x;
equation
  x = sin(time);
end foo;

is not meaningful, since it is commonly accepted (except perhaps by our friend Sébastien Furic, who eventually left the gang) that the Modelica semantics stops at describing a system of equations, but then does not enter into the "trivial" detail of how the solution to those equations is actually computed and stored. Experiment annotations provide some indications, but they are not mandatory and can be overridden at runtime, etc.

The problem I see is that if we mix equations with statements that potentially have side effects such as initializing an external function or printing a value on a screen, the thing becomes murkier.

By the way, other languages such as C/C++ would call foo() also in the 0*foo(x) case as they must assume the function could have side effects, so the current "common sense" is not universal.

We left a lot of these issues dangling in Modelica before we tried to clarify the distinction between pure and impure functions. The situation improved a lot after that, but my impression is that the issue raised by this ticket is still not completely clear.

@casella
Copy link
Collaborator Author

casella commented Dec 4, 2019

By the way, other languages such as C/C++ would call foo() also in the 0*foo(x) case as they must assume the function could have side effects, so the current "common sense" is not universal.

I would expect that a good Modelica tool does not call foo() when simulating B

model A
  constant Real k;
  Real u = 0;
  Real y;
equation
  y = k*foo(u);
end A;

model B
  A a(k = 0, u = time);
end B;

I guess the problem here is that functions have a different semantics when showing up in actual equations (whereby their output value is the only thing that matters), and when showing up alone as component-reference function-call-args terminals (whereby the side effects are the only things that matter).

Maybe this would be worth a brief explanation in the Specification.

@HansOlsson
Copy link
Collaborator

Form my understanding, it appears that some part of the Modelica specification are based on a "common sense" shared among current Modelica implementers, but not explicitly written in the specification. The goal of this discussion is to possibly improve and clarify the specification.

If that were the case there would be something to discuss.

However, that requires that you read the specification and list those cases, or have a supervisor that can help you. Now instead Adrian and I had to make that effort, and that is not the purpose.

I looked back on the original comment (not by you) and could only find misunderstandings based on an incomplete reading of the actual specification.

@casella
Copy link
Collaborator Author

casella commented Dec 5, 2019

For the record, after some discussion with my esteemed colleagues at Politecnico, I took the initiative of starting this discussion. I take full responsibility for that, even though I guess everybody agrees that my main experience and ability is that of a modeller, not of a compiler designer.

I obviously missed some instances of the component-reference function-call-args terminal that were kindly pointed out by @adrpo, and whose semantics is clearly explained in the specification.

I tend to agree that the set of those instances is too varied to be worth the effort of expanding the grammar to fit them, and only them, though my colleagues could disagree, in which case I invite them to provide some concrete proposals that could make the design of a compiler easier. Again, I do not consider myself an expert in compiler design. From this point of view, I agree that this issue as it was originally formulated can be closed.

However, if the grammar gives you complete freedom to put any function-call-like statement in an equation section, I do think that we still have an issue with the Specification regarding the semantics of those cases that are currently not covered anywere in the text, e.g.

  • calls to functions with side effects for the user (e.g. printing stuff on stdout)
  • calls to function with side effects on the result (e.g. initializing external C code)
  • calls to function with no side effects at all (e.g. adding a call to sin(x) to an equation section)

As a modeller, I find the fact that I can add this stuff to an equation section without the foggiest clue about what this actually means rather odd, and I am definitely convinced the specification can be improved from this point of view.

Is it ok if I open a new issue on this specific topic?

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Indicates that there's a discussion; not clear if bug, enhancement, or working as intended worksforme
Projects
None yet
Development

No branches or pull requests

10 participants