dart-lang / dart-lang/language
Proposal: guards
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
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:
```dart
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:
```dart
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`:
```dart
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.
```dart
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:
```dart
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:
```dart
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:
```dart
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`.
Contributor guide
Research direction
Read the proposal's Declaration, Usage, Runtime checks, Dynamic dispatch, Overrides, and Asynchrony sections first. There are no implementation files or tests named in the issue; done would require an agreed, internally consistent language-design specification for guards and requires.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100