std/expression

Expression and ExpressionNode, the typed tree a quoted parameter hands over, plus assert and nameOf.

The generated reference of std/expression has every public construct with its signature, its doc comment and its examples.

std/expression is quoted expressions: if a parameter or a binding has the type Expression<Value>, the compiler type checks the argument as an ordinary Value and then passes it together with its expression tree. Expression values can only be created by the compiler; ExpressionNode trees are plain data and can be built, matched and transformed by anybody. Every name below is already in scope through the prelude.

Import#

use Expression, ExpressionNode, nameOf, assert from "std/expression"
fn double(value: Expression<Int>): Int {
  print nameOf(value)
  value.value() * 2
}

const width = 3
print double(width)

Declarations#

Expression#

native type Expression<Value> {
  tree: ExpressionNode
  source: String
  location: SourceLocation
  native fn value(): Value
  native fn captures(): List<EncodedValue>
}

tree is the static data built at compile time, source is the source text of the quoted expression ("_.age >= minAge"), and value() is the ordinary value - for a function type, the closure itself - evaluated at most once. captures() answers the values of the captured variables, in the order of their Captured.index, so a provider can render "limit = 5" next to the tree without walking the caller's scope.

SourceLocation and TypeReference#

type SourceLocation with Show {
  file: String
  line: Int
  column: Int
  fn show(): String
}
type TypeReference with Show {
  name: String
  arguments: List<Self> = []
  fn show(): String
}

SourceLocation shows as "{file}:{line}:{column}". TypeReference is a description of a type as data, not reflection - there is no way back from a TypeReference to the type it names.

ExpressionNode#

type ExpressionNode {
  case Literal(value: EncodedValue, of: TypeReference)
  case Parameter(index: Int, name: String, of: TypeReference)
  case Captured(index: Int, name: String, of: TypeReference)
  case Field(target: Self, name: String, of: TypeReference)
  case Call(target: Self?, owner: TypeReference, method: String, arguments: List<Self>, of: TypeReference)
  case Construct(arguments: List<Self>, of: TypeReference)
  case Unary(operator: UnaryOperator, operand: Self, of: TypeReference)
  case Binary(operator: BinaryOperator, left: Self, right: Self, of: TypeReference)
  case Conditional(condition: Self, then: Self, else: Self, of: TypeReference)
  case Lambda(parameters: List<String>, body: Self, of: TypeReference)
  case Items(items: List<Self>, of: TypeReference)
  case Interpolation(parts: List<Self>)
  fn capturedNodes(): List<Self>
}

An ordinary algebraic data type: names are already resolved and everything is typed, so an implicit _, a named closure parameter, a receiver and an implicit self all show up as explicit Parameter, Field and Call nodes. UnaryOperator (Negate, Not) and BinaryOperator (Add, Equal, Less, And, ...) are the two small enums the Unary and Binary cases carry. A provider matches on the cases it understands and ends with _ => Fail(...), because new node kinds arrive with new versions of the language. capturedNodes() collects every Captured node below one node, which is what assert uses to render each captured value next to its name.

nameOf#

fn nameOf<Value>(expression: Expression<Value>): String

The name of what was written, never its value: nameOf(user.email) is "email", nameOf(limit) is "limit". For a type there is the compile-time function typeName<User>() instead.

assert#

fn assert(condition: Expression<Bool>)

Panics if condition is false, with the source text and every captured value in the message:

Assertion failed: adults.length() > limit   (adults = [...], limit = 5)   at main.trb:12:1

There are no matchers: assert is the one function std/test needs, because the expression tree already carries enough to explain a failure.

Esc