numeric_expr API

numeric_expr 是一种面向数值测试语料和工具的小型表达式语言。Expr 是一棵树,其叶子是字面量(原始源文本),内部节点将具名操作应用于参数表达式。该包不了解任何数值类型:evaluate 借助调用者提供的两个回调折叠这棵树,一个解码字面量,一个执行操作。每个节点都携带一个 SourceSpan,以便在其源代码行处报告失败。教程逐步构造并求值表达式,设计页给出求值语义并证明其不变量。

在 moon.pkg 中导入该包:

import {
  "Luna-Flow/floating/numeric_expr",
}

源位置

SourceSpan

SourceSpan 记录语法节点的来源:源名称(通常是文件路径)、行号和列号。

pub struct SourceSpan {
  // private fields
} derive(Eq, @debug.Debug)

这些字段是私有且不可变的。当源、行号和列号都相等时,两个区段相等。

SourceSpan::new

SourceSpan::new(source, line?, column?) 创建一个区段。

pub fn SourceSpan::new(String, line? : Int, column? : Int) -> Self

line 和 column 默认为 0,表示“未知”。值按原样存储,不做范围检查。

SourceSpan::source, SourceSpan::line, SourceSpan::column

这些访问器返回区段的三个分量。

pub fn SourceSpan::source(Self) -> String
pub fn SourceSpan::line(Self) -> Int
pub fn SourceSpan::column(Self) -> Int
///|
test "source span accessors" {
  let span = @numeric_expr.SourceSpan::new("add.decTest", line=12, column=1)
  inspect(span.source(), content="add.decTest")
  inspect(span.line(), content="12")
  let unknown = @numeric_expr.SourceSpan::new("")
  inspect(unknown.line(), content="0")
  inspect(unknown.column(), content="0")
}

语法节点

Literal

Literal 是表达式的叶子:操作数的原始文本(例如 "1.20"、"-Inf" 或 "#7C00016E")及其区段。

pub struct Literal {
  // private fields
} derive(Eq, @debug.Debug)

构造字面量时不会解析文本。其含义由 evaluate 的 decode 回调决定,因此同一棵树可以被读作二进制值、十进制值或区间值。

Literal::new, Literal::raw, Literal::span

Literal::new(raw, span?) 构造一个字面量;raw 和 span 将其读回。

pub fn Literal::new(String, span? : SourceSpan) -> Self
pub fn Literal::raw(Self) -> String
pub fn Literal::span(Self) -> SourceSpan

默认区段为 SourceSpan::new("")。

Operation

Operation 命名内部节点的操作(例如 "add" 或 "squareroot"),并带有其区段。

pub struct Operation {
  // private fields
} derive(Eq, @debug.Debug)

该包不赋予名称任何含义,也不记录元数。由 evaluate 的 invoke 回调决定存在哪些名称以及每个名称接受多少个参数。

Operation::new, Operation::name, Operation::span

Operation::new(name, span?) 构造一个操作;name 和 span 将其读回。

pub fn Operation::new(String, span? : SourceSpan) -> Self
pub fn Operation::name(Self) -> String
pub fn Operation::span(Self) -> SourceSpan

默认区段为 SourceSpan::new("")。

表达式

Expr

Expr 是不可变的表达式树。

pub struct Expr {
  // private fields
}

其表示是私有的。只有下面两个构造器能创建表达式,因此每个 Expr 都具有如下形状

e  ::=  lit(ℓ)  ∣  op(o)(e1,…,en),n≥0.e \;::=\; \mathsf{lit}(\ell) \;\mid\; \mathsf{op}(o)(e_1, \dots, e_n), \qquad n \ge 0 .

Expr 没有相等性、Debug 或 Show 实现;要检查一个表达式,请对其求值。

Expr::literal

Expr::literal(literal) 是对应单个字面量的叶子表达式。

pub fn Expr::literal(Literal) -> Self

Expr::invoke

Expr::invoke(operation, arguments) 将 operation 按顺序应用于各参数表达式。

pub fn Expr::invoke(Operation, Array[Self]) -> Self

数组会被复制到树中,因此之后对 arguments 的修改不会改变该表达式。允许空数组,此时得到零元操作。

///|
test "build a nested expression" {
  // (2 + 3) * 4
  let sum = @numeric_expr.Expr::invoke(@numeric_expr.Operation::new("add"), [
    @numeric_expr.Expr::literal(@numeric_expr.Literal::new("2")),
    @numeric_expr.Expr::literal(@numeric_expr.Literal::new("3")),
  ])
  let product = @numeric_expr.Expr::invoke(
    @numeric_expr.Operation::new("mul"),
    [sum, @numeric_expr.Expr::literal(@numeric_expr.Literal::new("4"))],
  )
  let result : Result[Int, @numeric_expr.EvalError[String]] = @numeric_expr.evaluate(
    product,
    literal => {
      match literal.raw() {
        "2" => Ok(2)
        "3" => Ok(3)
        "4" => Ok(4)
        _ => Err("bad literal")
      }
    },
    (operation, arguments) => {
      match (operation.name(), arguments) {
        ("add", [a, b]) => Ok(a + b)
        ("mul", [a, b]) => Ok(a * b)
        _ => Err("unknown operation")
      }
    },
  )
  inspect(result is Ok(20), content="true")
}

求值

evaluate

evaluate(expression, decode, invoke) 借助这两个回调自底向上计算 expression 的值。

pub fn[V, E] evaluate(Expr, (Literal) -> Result[V, E], (Operation, Array[V]) -> Result[V, E]) -> Result[V, EvalError[E]]

V 是回调的值类型,E 是其错误类型。规则如下:

  • 字面量 ℓ\ell 求值为 decode(ℓ);Err(e) 变为 Err(LiteralFailure(ℓ, e));
  • 调用 o(e1,…,en)o(e_1, \dots, e_n) 先从左到右求值 e1,…,ene_1, \dots, e_n。第一个失败的参数会终止求值,其错误原样返回。当所有参数都成功并得到值 v1,…,vnv_1, \dots, v_n 时,结果为 invoke(o, [v1, ..., vn]),Err(e) 变为 Err(OperationFailure(o, e))。

因此,回调按后序调用(先从左到右处理子节点,再处理父节点),每个节点至多一次,且求值在第一次失败时停止。成功时,decode 对每个字面量叶子运行一次,invoke 对每个调用节点运行一次。该包不产生任何其他副作用。递归深度等于树的高度。设计页证明了这些性质。

evaluate 自身从不中止;任何中止都来自回调。

EvalError

EvalError[E] 指出哪个节点失败,并携带回调的错误。

pub(all) enum EvalError[E] {
  LiteralFailure(Literal, E)
  OperationFailure(Operation, E)
  UnsupportedExpression(SourceSpan)
}
  • LiteralFailure(literal, error):decode(literal) 返回了 Err(error)。literal.span() 定位该操作数。
  • OperationFailure(operation, error):在 operation 的所有参数都成功求值之后,invoke 对它返回了 Err(error)。
  • UnsupportedExpression(span):树中包含求值器不处理的形式。用 Expr::literal 和 Expr::invoke 构造的树永远不会产生它;该构造器是为私有表示能够容纳、但公共构造器尚无法构造的形式(例如变量和绑定子)预留的。
///|
test "evaluation reports the failing node" {
  let span = @numeric_expr.SourceSpan::new("row.txt", line=3, column=7)
  let expression = @numeric_expr.Expr::invoke(
    @numeric_expr.Operation::new("div", span~),
    [
      @numeric_expr.Expr::literal(@numeric_expr.Literal::new("1", span~)),
      @numeric_expr.Expr::literal(@numeric_expr.Literal::new("0", span~)),
    ],
  )
  let result : Result[Int, @numeric_expr.EvalError[String]] = @numeric_expr.evaluate(
    expression,
    literal => if literal.raw() == "1" { Ok(1) } else { Ok(0) },
    (_, arguments) => {
      if arguments[1] == 0 {
        Err("division by zero")
      } else {
        Ok(arguments[0] / arguments[1])
      }
    },
  )
  match result {
    Err(@numeric_expr.OperationFailure(operation, message)) => {
      inspect(operation.name(), content="div")
      inspect(operation.span().line(), content="3")
      inspect(message, content="division by zero")
    }
    _ => fail("expected an operation failure")
  }
}

trait 实现

Literal::equal、Operation::equal、SourceSpan::equal 与 not_equal

这些方法比较所有存储的分量(文本或名称,以及区段)。新代码请使用 == 和 !=。

pub fn Literal::equal(Self, Self) -> Bool
pub fn Literal::not_equal(Self, Self) -> Bool
pub fn Operation::equal(Self, Self) -> Bool
pub fn Operation::not_equal(Self, Self) -> Bool
pub fn SourceSpan::equal(Self, Self) -> Bool
pub fn SourceSpan::not_equal(Self, Self) -> Bool

文本相同但区段不同的两个字面量是不同的。

Literal::to_repr, Operation::to_repr, SourceSpan::to_repr

这些方法为 debug_inspect 及其他 Debug 使用者渲染值。

pub fn Literal::to_repr(Self) -> @debug.Repr
pub fn Operation::to_repr(Self) -> @debug.Repr
pub fn SourceSpan::to_repr(Self) -> @debug.Repr
///|
test "syntax nodes compare by text and span" {
  let a = @numeric_expr.Literal::new("1.0")
  let b = @numeric_expr.Literal::new(
    "1.0",
    span=@numeric_expr.SourceSpan::new("x", line=1),
  )
  inspect(a == @numeric_expr.Literal::new("1.0"), content="true")
  inspect(a != b, content="true")
}

完整公共接口

此快照是该包生成的 pkg.generated.mbti。当正文与接口不一致时,以接口为准。

// Generated using `moon info`, DON'T EDIT IT
package "Luna-Flow/floating/numeric_expr"

import {
  "moonbitlang/core/debug",
}

// Values
pub fn[V, E] evaluate(Expr, (Literal) -> Result[V, E], (Operation, Array[V]) -> Result[V, E]) -> Result[V, EvalError[E]]

// Errors

// Types and methods
pub(all) enum EvalError[E] {
  LiteralFailure(Literal, E)
  OperationFailure(Operation, E)
  UnsupportedExpression(SourceSpan)
}

pub struct Expr {
  // private fields
}
pub fn Expr::invoke(Operation, Array[Self]) -> Self
pub fn Expr::literal(Literal) -> Self

pub struct Literal {
  // private fields
} derive(Eq, @debug.Debug)
pub fn Literal::equal(Self, Self) -> Bool
pub fn Literal::new(String, span? : SourceSpan) -> Self
pub fn Literal::not_equal(Self, Self) -> Bool
pub fn Literal::raw(Self) -> String
pub fn Literal::span(Self) -> SourceSpan
pub fn Literal::to_repr(Self) -> @debug.Repr

pub struct Operation {
  // private fields
} derive(Eq, @debug.Debug)
pub fn Operation::equal(Self, Self) -> Bool
pub fn Operation::name(Self) -> String
pub fn Operation::new(String, span? : SourceSpan) -> Self
pub fn Operation::not_equal(Self, Self) -> Bool
pub fn Operation::span(Self) -> SourceSpan
pub fn Operation::to_repr(Self) -> @debug.Repr

pub struct SourceSpan {
  // private fields
} derive(Eq, @debug.Debug)
pub fn SourceSpan::column(Self) -> Int
pub fn SourceSpan::equal(Self, Self) -> Bool
pub fn SourceSpan::line(Self) -> Int
pub fn SourceSpan::new(String, line? : Int, column? : Int) -> Self
pub fn SourceSpan::not_equal(Self, Self) -> Bool
pub fn SourceSpan::source(Self) -> String
pub fn SourceSpan::to_repr(Self) -> @debug.Repr

// Type aliases

// Traits