-
Notifications
You must be signed in to change notification settings - Fork 213
specify behavior of fromEnvironment #304
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
Comments
Makes sense! I'll label this issue as 'specification' at first. There might be some elements which would justify a language design perspective, but I suspect (and hope) that we can consider it to be more like a clarification. The decision may end up being part of the language specification or the core library documentation, but that can be decided independently of the actual contents, so nothing is implied at this point. |
We'd like to get closure on this. My proposal is the following:
Yes
compile-time
Consistency is checked at link time. That is, if two separately compiled modules are linked together (whether before runtime, or at runtime) there is a check that they have consistent definitions for fromEnvironment). @lrhn @munificent @eernstg Are we in agreement on this? Issues I'm not seeing? |
@leafpetersen wrote:
Making the choice at compile-time and then checking consistency at link time is what I've suggested we should do whenever this came up, for simplicity. So in that sense I agree. But it is my impression that we are currently using a more complex approach, in particular in the common front end: I believe it has a notion of constant expressions that are unevaluated at compile time. Such expressions are evaluated to such an extent that there is no need for a regular binding environment (so we do not need to look up the declaration of an identifier in order to finish the evaluation), the evaluation can be completed based on nothing more than the values of constant expressions of the form Presumably, this machinery would not be needed if our tools were able to rely on having the values of objects @askeksa-google, can you provide more detail on this? |
Retaining the full environment will increase the files. It will not work well for DDC where you compile files individually and only link them by loading the JS code into the same JS VM. We don't want to do environment checking there. Perhaps they can just retain a sufficiently large hash of the environment and check that (again as long as there is no If we disallow |
Some thoughts about supporting
It seems |
@lrhn I don't really understand your reply, I think maybe we're talking past each other? The key questions as I understand it are the following. Assume the compiler is run with -DPARAM=0 in module A, and the VM is run on the resulting kernel file with -DPARAM=2
My answers are as follow:
What are your answers?
I'm not sure I see this. Why is the environment large? Most invocations don't pass any -D arguments at all? @vsmenon Do you have DDC implementation concerns here? |
@leafpetersen
For modular compilation, any environment will be in each module. I also expect |
OK, as far as I can tell we have consensus.
|
I'm not sure I think this is a good idea. What does it mean for a value to be used? Mentioned? Evaluated? If the latter, do you mean even at runtime (using "new")? What about short-circuiting in "const"? If the former, can we always tell which ones are mentioned? Certainly doesn't seem like that for "new". Even ignoring the issue of defining this well, this feels fragile to me. I have a library that's working "fine" in all of my tests (because coverage doesn't hit the path that evaluates a particular fromEnvironment), but as soon as someone uses something that hits the code path with the fromEnvironment they start getting link time errors. I'd rather know up front that something has inconsistent defines. |
It's true that if we allow I guess I'm saying that I only want to make a declaration conflict into a compile-time error if the conflicting value actually makes a (potential) difference. If a compilation artifact only remembers the values that it has actually depended on, then anything that is entirely compilation environment independent can be reused freely (and it doesn't need to keep the extra data around). |
I would amend this to say that "using new or const takes the value that was provided at the point that a value was provided". We can leave it up to the tools to decide how an environment is provided and to make their own performance and usability tradeoffs. If that formulation is too vague we could try something like "using new or const takes the value that was provided at the point that the code was processed for the purpose of evaluating calls to |
@kmillikin I read both of your formulations as permitting the current VM behavior, which I don't think we want. In particular, I think we want:
I don't understand the _unspecified _ name bit here, can you elaborate? |
[EDIT this is wrong, see correction in next comment] To address the question @lrhn raises, would the following work?
This should allow modules that don't depend on FOO in any way to be compiled once and linked into any program that provides an otherwise consistent definition of FOO. |
I think my suggestion in the previous comment was wrong, it should be as follows: A symbol may be in the following states in the map:
|
Ok, here's my attempt to unify this based on discussion. Every compilation unit is associated with an environment. An environment is a mapping from strings to environment entries. An environment entry is either a value (a string) or a special value UNDEFINED Any tool that processes a compilation unit is allowed to add new entries to the environment. No tool may change an existing environment entry. When a fromEnvironment constructor is evaluated (whether const or new), the string argument is looked up in the environment. If it is present, the value is used. If it is not, then an environment entry of UNDEFINED is added for that string. (This last step can be elided when evaluating "new fromEnvironment" constructors if otherwise no further modifications of the environment are permitted at runtime - the presence of the UNDEFINED sentinel is only used to check that inconsistent defines are not used). When two or more compilation units are linked (whether as part of a compilation step, or at runtime), their environments are checked for consistency, and it is an error if they are not consistent. Two environments E1 and E2 are consistent iff for every key S in the intersection of their domains E1 and E2 map S to the same environment entry. Linking is only defined for consistent environments. The result of linking two compilation units with consistent environments E1 and E2 has an environment E3 where the domain of E3 is the union of the domains of E1 and E2, and where E3 maps each key S in its domain to the value given for that key in E1 if present, else the value given in E2. |
If a library uses I guess that actually applies to (As usual, we can choose to allow conflicts that are not detectable because one of the libraries does not depend on the value - there exists a single consistent definition environment that the entire program could be compiled in to get the same semantics). |
I think the unification of the constant environments is good, but it does not address So, maybe: An environment is a mapping from key strings, or a special key ALL, to environment entries. An environment entry is either a value (a string) or a special value UNDEFINED Any tool that processes a compilation unit is allowed to add new entries to the environment if no ALL key is present. No tool may change an existing environment entry. When a Whena When two or more compilation units are linked (whether as part of a compilation step, or at runtime), their environments are checked for consistency, and it is an error if they are not consistent. Two environments E1 and E2 are consistent iff
Linking is only defined for consistent environments. The result of linking two compilation units with consistent environments E1 and E2 has an environment E3 where the domain of E3 is the union of the domains of E1 and E2, and where E3 maps each key S in its domain to the value given for that key in E1 if present, else the value given in E2. I guess this could be expressed as having two kinds of environments: open and closed. |
I don't understand this at all. What are you trying to achieve here? I am trying to achieve the following properties:
Any new fromEnvironment invocations don't affect the compilation artifacts at all: they are evaluated at runtime. So to summarize: I claim that my proposal guarantees that at runtime there is a single global environment, and that the result of evaluating any new fromEnvironment call will produce the same result that any evaluation of a const fromEnvironment call did during compilation of any of the modules. Do you think my claim is wrong (and if so, what is the counter-example)? Or are you trying to ensure some other property than what I describe above, and if so what is it? |
I want to achieve two things on top of the property you list:
The latter may just be too permissive and friendly, and not needed. It would allow compilation artifacts to only retain environment declarations that are actually (potentially) used, instead of retaining the entire environment. |
@lrhn Note that nothing stops a library from calling a function in a different library which does the I'm fine with just disallowing any differences in the environments - @sigmundch and @kmillikin both seemed to feel that our ability to share compilation artifacts would be low anyway. If we change our mind later, it's a non-breaking change to relax the restrictions. |
Ok, per offline discussion, we will go with the restrictive version that requires environments to be the same. If in the future it proves important to share compilation artifacts, we can consider relaxing this as a non-breaking change. The short version of the spec then is that environments have to be the same when linked. If we're happy requiring that all defines be given before constant evaluation happens, then I think that's enough. The language below allows for tools to add more defines at any point, so long as they are done consistently across all modules, and so long as they are observed to be the same at every invocation. If no defines are allowed to be added after the stage of compilation in which constant evaluation is done, then the UNDEFINED mechanism can be ignored, and environment consistency degenerates to equality. Every compilation unit is associated with an environment. An environment is a mapping from strings to environment entries. An environment entry is either a value (a string) or a special value UNDEFINED. Any tool that processes a compilation unit is allowed to add new entries to the environment. No tool may change an existing environment entry. When a fromEnvironment constructor is evaluated (whether const or new), the string argument is looked up in the environment. If it is present, the value is used. If it is not, then an environment entry of UNDEFINED is added for that string. (This last step can be elided if no further modifications to the environment area allowed, such as at runtime - the presence of the UNDEFINED sentinel is only used to check that a define for a given string S is not added after a const fromEnvironment constructor with argument S has been evaluated). When two or more compilation units are linked (whether as part of a compilation step, or at runtime), their environments are checked for consistency, and it is an error if they are not consistent. Two environments E1 and E2 are consistent iff for every key S in the union of their domains:
Linking is only defined for consistent environments. The result of linking two compilation units with consistent environments E1 and E2 has an environment E3 such that E3 maps every key S in the union of the domains of E1 and E2 to the value given by E1 if any, and otherwise to that given by E2. |
Conditional imports are based on the environment too, so the part of the environment controlling conditional imports must be available before imports can be resolved. We used to allow such names to be overridden by user defines, but we should probably stop doing that and reserve all names starting with |
Shouldn't the consistency rules rather be (to fulfill item 3 in #304 (comment)):
That is, if there is no entry for a key S in one environment (i.e. it is neither defined nor evaluated), it OK for S to map to anything in the other environment(s)? |
No. My final version here is explicitly giving up on item 3 from the comment you reference. So the proposed version essentially requires that the environments are exactly the same. The only reason it's not actually specified as exact equality is that I'm still accounting for the possibility that tools may provide additional defines at different points in the compilation pipeline, and I want the spec to enforce that the resulting view is consistent. Hence the UNDEFINED mechanism for recording the fact that a given string key has been observed to be in the unset state, and can not be set to something by a later compilation stage. |
@eernstg has this been incorporated into the spec yet? |
I'm not sure this belongs in the language specification since it talks about the compilation process. The language specification does not mention compilation at all, it just gives run-time semantics, and some compile-time errors. I guess it could be possible to say that it's a compile-time error if the string associated with a key in the "Dart environment" is not the same between two calls to a Let me try to write that. |
No.
One part that we usually have in the language specification is whether or not an expression is constant, but The other property that we'd need to specify in the language specification is if it is an error (run-time or compile-time) to invoke these constructors at run time. But the consensus seems to be that this is not an error. So it seems to fit with current practice that this is specified in the system library documentation. |
Possible documentation update for the methods: https://dart-review.googlesource.com/c/sdk/+/184467 |
…ve consistently. AAll calls to those constructors must behave as if there is a single consistent environment backing them. This will force compilers to reject programs where that wouldn't be the case, which is possible if doing modular compilation with different settings. If a library doesn't check the environment, it doesn't matter whether the environment was declared differently, the requirement is only that when actually checking, the values must be consistent. Bug: dart-lang/language#304 Change-Id: Ie52ecc3ea49ed87297fab92e5e7bb6d9f96a495d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/184467 Commit-Queue: Lasse R.H. Nielsen <[email protected]> Reviewed-by: Sigmund Cherem <[email protected]> Reviewed-by: Leaf Petersen <[email protected]> Reviewed-by: Erik Ernst <[email protected]>
Can we consider this done? |
I think so. @lrhn, do you have any further plans in this area? |
I think the current text defines the behavior we want: We don't care how you compile, but the combined program must behave as if there is one consistent environment, both compile-time and run-time (which might not match how the VM currently works). |
We'd like to have a source of truth to define what is required for
fromEnvironment
constructors and what is the expected behavior we will support.Since modular compilation was not even in the picture at the time the feature was introduced, we are now finding a lot of inconsistencies across the tools. The recent changes that move constant evaluation to the CFE also highlight the need to define a coherent story going forward.
Some questions that have recently come up:
const
andnew
supported?The text was updated successfully, but these errors were encountered: