Skip to content

Proposal: guards #416

@yjbanov

Description

@yjbanov

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions