numeric_expr API

numeric_expr は数値テストコーパスとツールのための小さな式言語です。Expr は木であり、葉はリテラル(生のソーステキスト)、内部ノードは名前付き演算を引数式に適用します。このパッケージは数値型を一切知りません。evaluate は呼び出し側が与える 2 つのコールバック(リテラルを復号するものと演算を実行するもの)で木を畳み込みます。各ノードは SourceSpan を持つため、失敗をソースの行で報告できます。チュートリアルでは式を段階的に構築・評価し、設計ページでは評価の意味論を述べ、その不変条件を証明します。

moon.pkg でパッケージをインポートします:

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

ソース位置

SourceSpan

SourceSpan は構文ノードの出所を記録します。ソース名(通常はファイルパス)、行、列です。

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

フィールドは非公開かつ不変です。2 つのスパンは、ソース、行、列がすべて等しいときに等しくなります。

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

これらのアクセサはスパンの 3 つの構成要素を返します。

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 コールバックが決めるため、同じ木を 2 進値、10 進値、区間値のいずれとしても読めます。

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
}

表現は非公開です。式を作成できるのは以下の 2 つのコンストラクタだけなので、すべての 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) は 1 つのリテラルからなる葉の式です。

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

Expr::invoke

Expr::invoke(operation, arguments) は operation を引数式に順に適用します。

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

配列は木にコピーされるため、後で arguments を変更しても式は変わりません。空配列も許され、0 項演算になります。

///|
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) は 2 つのコールバックを用いて 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)) になる。

したがってコールバックは後行順(子を左から右へ、次に親)で、各ノードにつき高々 1 回呼ばれ、評価は最初の失敗で停止します。成功時には decode はリテラルの葉ごとに 1 回、invoke は呼び出しノードごとに 1 回実行されます。パッケージはそれ以外の作用を一切行いません。再帰の深さは木の高さに等しくなります。これらの性質は設計ページで証明されています。

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):すべての引数の評価が成功した後、invoke が operation に対して 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")
  }
}

トレイト実装

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

テキストが同じでもスパンが異なる 2 つのリテラルは異なります。

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