-
Notifications
You must be signed in to change notification settings - Fork 213
Proposal: guards #416
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
var button = Button();
if (button.enabled) {
button.enabled.set(false);
button.click(child); // that's not good
} |
Absolutely brilliant! I love it! Have you thought about the ability to use enums as guards? It would allow for mutual exclusion of guard types, and if extended to arbitrary const values (in the far future), it would allow for some interesting amounts of expressibility (a library defined implementation of Java's This would make asserting states in a state machine a bit easier. Say you have two stages/phases: layout and paint. With enums, you could describe a method that is only available during layout, or a method that is only available during paint. If you know you're in the middle of layout, you definitely aren't in the middle of paint, for example. |
@ds84182 In the "Future extensions" section I propose exactly that, although I didn't explain the motivation behind adding @jonahwilliams pointed out that this enters the dependent type territory. So there may be lots of opportunities, although we should be careful about not over-complicating the language. |
This description seems to include mutex support (single write, multiple read). You can set a guard variable to false to disable readers, and successfully read a true result to disable writing until the guarded block exits. Multiple readers can check that the value is true, and it cannot be set to false until all of them are done. This suggests some of the implementation complexities around the suggested feature. static guard foo = false;
if (foo) { // get read lock if possible.
} // Release read lock. This cannot fail.
foo = false; // throws if any read lock.
foo = true; // always possible (unless we have negative guards, There is no synchronization. There is no way to block until the guard becomes available. There is no way to check whether assigning false will work except trying and catching the error. That might be an issue. The example: isInPaintPhase.set(true); // code following this line is promoted
paint(); // safe to call without checking due to previous line
isInPaintPhase.set(false); suffers from this problem. If Using data-flow based promotion and mutex is clever, but perhaps too clever. How about introducing an actual guarded block syntax: if guard foo {
// read lock on foo.
} Then you won't get into discussion about how far the scope of the test is. I doubt putting the requirements on function types is a good idea. That will complicate function types (I'm not sure what the subtype relation would be, but I guess it's possible that If we use enums (constant values) for guards, then I guess it becomes I guess one use-case is when the guard is constant. Then the compiled can remove any guarded code not enabled by the constant value. Since the function invocations are guarded themselves, we can definitely promise that they are not called when the guard can never become true (or false, if the guard is |
I don't think guards should support async operations out-of-the-box. I think it's OK to leave that to the developer. IOW, if a guarded function creates a microtask, the microtask is executed on its own. If the microtask callback requires a certain guard, it would have to check for it separately. This also means that
I think these would be interesting potential future extensions to this feature, but I think these can be left out for the MVP.
I was thinking that guards should be usable inside boolean expressions so that they compose with normal if (isDebugMode && child != null) {
debugValidateChild(child);
}
It definitely requires some new subtyping rules, but I think this proposal would lose a lot of value if this was left out. Yes, tree-shaking using constant guards is one huge use-case for this. Another big use-case is elimination of reentrant locking of runtime guards (i.e. a guarded function does not need to re-lock when calling another guarded function). |
If we are going to mark functions with bar() {
// bar() is inferred to be `mutates foo`.
foo.set(false);
}
if (foo) {
bar(); // Static error: Can't mutate `foo` inside a block guarded by `foo`.
} In the same way, Both bar() {
// if baz() requires foo, then bar() should also require foo.
baz();
}
bar() {
// If baz() mutates foo, then bar() also mutates foo.
baz();
} |
@mdebbar I think The other issue is that in Dart, by design, type inference does not leak types into the function/method signature (whole world analysis is a different story). This simplifies code analysis (you don't need to look at function bodies), but also makes subtyping saner. For example, what if in your example, An alternative is to make statically inferred guard mutation best effort. However, that makes it very brittle. Seemingly benign code changes would break inference. That would make the system too brittle to rely on. |
Update: added "Asynchrony" section in the issue description. |
Very interesting! This looks like typestate. Is that deliberate? |
No, first time I hear about typestate. Does look similar. |
After migerated to NNBD, we've got a lot boilerplate code to guard usage of nullable field in method class dart {
int? _index;
void foo() {
if (_index == null) return;
final int index = _index;
print('index %index');
}
} Could we get Swift-like private var index: Int?;
func foo() {
guard let index = self.index else { return }
print("index \(index)")
} Or Koltin-like Elvis operator private var index: Int? = null;
fun foo() {
val index = index ?: return
println("index $index")
} |
If we allow "statement expressions" (#1211, expressions which can contain general statements, including control flow) then this would just be: var index = self.index ?? {{return;}}; // |
Typescript has a similar feature called https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates |
This proposes a language feature that allows guarding parts of programs based on conditions such that the analyzer and the compiler can statically verify that those parts are accessed correctly.
This aims to provide a solution for #415.
Declaration
New keywords are introduced in Dart:
guard
andrequires
.The
guard
keyword is used as a top-level orstatic
declaration of aconst
or a runtime guard, or as an instance guard of a class. Guards are initialized from boolean expressions:A guard may be in two states:
true
false
const
guards are evaluated eagerly before dead-code elimination. Variable guards can change their state dynamically.Usage
To change the state of a runtime guard, call its
set
method, e.g.inPaintPhase.set(true)
.Guards are used to modify functions, methods, getters, setters, and typedefs (not sure if it's worth guarding variable access) using a
requires
clause:Members with
requires
clauses are referred to as "guarded". Guarded members may only be accessed if it can be statically proven that the guard is on. Such proofs are provided by promotingif
blocks,requires
clauses, and data flow analysis. In the following example, theif
block is promoted to "isDebugMode is on" and therefore it is safe to calldebugValidateChild
:The following example shows how the body of
debugValidateChild
is promoted allowing it to calldebugValidateIsEmpty
safely without extra checks.The following example shows how the remainder of a function block is promoted using data flow analysis:
Runtime checks
Because runtime guards may change state dynamically we need to ensure guards are not disabled in the middle of a guarded block. This is done by locking the guard when a guarded block is entered and releasing it when the outermost guarded block exits. While locked, a guard's state may not change.
Dynamic dispatch
requires
clause is considered part of the method's name. Dynamic dispatch may never reach a guarded method.Overrides
requires
clauses use the same rules as parameters. Namely, method overrides are allowed to relax requirements, but never tighten them. For example:It is legal to call
click()
onAlwaysClickableButton
without theenabled
guard. However, it is not legal to call it onButton
even when the runtime type isAlwaysClickableButton
.Asynchrony
Guards ignore asynchrony. A microtask scheduled from within a guarded function is not automatically guarded. The body of the microtask must check the guard independently. This applies to
await
, timers, I/O, etc. In particular,await
demotes a previously promoted block back to unguarded. Examples:Future extensions
This proposal limits guards to
bool
only. In the future, we might want to supportenum
.The text was updated successfully, but these errors were encountered: