-
Notifications
You must be signed in to change notification settings - Fork 227
Description
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
and requires
.
The guard
keyword is used as a top-level or static
declaration of a const
or a runtime guard, or as an instance guard of a class. Guards are initialized from boolean expressions:
const guard debugMode(const_bool_expression);
guard inPaintPhase(false);
class Foo {
static const guard isDebugMode(const_boolean_expression);
static guard isInPaintPhase(false);
guard enabled = true;
}
A guard may be in two states:
- "on", initialized from
true
- "off", initialized from
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:
typedef PaintingContextCallback = void Function(PaintingContext context, Offset offset)
requires isInPaintPhase;
class ContainerRenderObjectMixin {
void debugValidateChild(RenderObject child) requires isDebugMode {
// validation logic
}
}
abstract class RenderObject {
void paint(PaintingContext context, Offset offset) requires isInPaintPhase;
}
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 promoting if
blocks, requires
clauses, and data flow analysis. In the following example, the if
block is promoted to "isDebugMode is on" and therefore it is safe to call debugValidateChild
:
if (isDebugMode && child != null) {
debugValidateChild(child);
}
The following example shows how the body of debugValidateChild
is promoted allowing it to call debugValidateIsEmpty
safely without extra checks.
void debugValidateIsEmpty(RenderObject object) requires isDebugMode {
assert(object._children.isEmpty);
}
void debugValidateChild(RenderObject child) requires isDebugMode {
debugValidateIsEmpty(child);
}
The following example shows how the remainder of a function block is promoted using data flow analysis:
void drawFrame() {
build();
layout();
isInPaintPhase.set(true); // code following this line is promoted
paint(); // safe to call without checking due to previous line
isInPaintPhase.set(false);
}
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:
class Button {
guard enabled(true);
void click() requires enabled {
}
}
class AlwaysClickableButton extends Button {
@override
void click() { // ok to not require enabled in the override
}
}
It is legal to call click()
on AlwaysClickableButton
without the enabled
guard. However, it is not legal to call it on Button
even when the runtime type is AlwaysClickableButton
.
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:
void click() requires enabled {
foo(); // guarded
await bar(); // guarded
baz(); // unguarded
}
void foo() required isInPaintPhase {
bar(); // guarded
scheduleMicrotask(() {
baz(); // unguarded
});
qux(); // guarded
}
Future extensions
This proposal limits guards to bool
only. In the future, we might want to support enum
.