dart-lang / dart-lang/language

Proposal: guards

Open
#416 13 comments 18 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.