numeric_expr チュートリアル
このチュートリアルでは、計算を numeric_expr の木として一度だけ記述し、リテラルを読むコールバックと演算を実行するコールバックの 2 つを与えることで、任意の数値型に対して実行する方法を説明します。最後には、Int 上や IEEE フラグ付きの BinFloat 上で式を評価し、失敗をそのソース行で報告できるようになります。これは、適合性フロントエンド(frontend/gda_expr など)がテストコーパスの実行に用いている仕組みです。
クイックスタート
moon.pkg にパッケージを追加します。
import {
"Luna-Flow/floating/numeric_expr",
}
1 + 2 を構築し、Int 上で評価します。
///|
test "quick start" {
let one_plus_two = @numeric_expr.Expr::invoke(
@numeric_expr.Operation::new("add"),
[
@numeric_expr.Expr::literal(@numeric_expr.Literal::new("1")),
@numeric_expr.Expr::literal(@numeric_expr.Literal::new("2")),
],
)
let result : Result[Int, @numeric_expr.EvalError[String]] = @numeric_expr.evaluate(
one_plus_two,
literal => if literal.raw() == "1" { Ok(1) } else { Ok(2) },
(_, arguments) => Ok(arguments[0] + arguments[1]),
)
inspect(result is Ok(3), content="true")
}
evaluate は両方のリテラルをデコードし、デコードされた引数 [1, 2] で演算コールバックを呼び出します。
日常的なタスク
小さなインタプリタを書く
多くの呼び出し側は、演算名と引数配列に対してパターンマッチします。配列パターンのマッチはアリティの検査も兼ねますが、numeric_expr 自身はこれを行いません。
///|
fn int_literal(literal : @numeric_expr.Literal) -> Result[Int, String] {
match literal.raw() {
"0" => Ok(0)
"1" => Ok(1)
"2" => Ok(2)
"3" => Ok(3)
"10" => Ok(10)
other => Err("not a small integer: " + other)
}
}
///|
fn int_operation(
operation : @numeric_expr.Operation,
arguments : Array[Int],
) -> Result[Int, String] {
match (operation.name(), arguments) {
("add", [a, b]) => Ok(a + b)
("mul", [a, b]) => Ok(a * b)
("neg", [a]) => Ok(-a)
("sum", values) => Ok(values.fold(init=0, (acc, x) => acc + x))
(name, values) =>
Err(name + " does not take " + values.length().to_string() + " arguments")
}
}
///|
fn lit(raw : String) -> @numeric_expr.Expr {
@numeric_expr.Expr::literal(@numeric_expr.Literal::new(raw))
}
///|
fn call(name : String, arguments : Array[@numeric_expr.Expr]) -> @numeric_expr.Expr {
@numeric_expr.Expr::invoke(@numeric_expr.Operation::new(name), arguments)
}
///|
test "small interpreter" {
// -(2 * 3) + sum(1, 2, 3, 10)
let expression = call("add", [
call("neg", [call("mul", [lit("2"), lit("3")])]),
call("sum", [lit("1"), lit("2"), lit("3"), lit("10")]),
])
let result = @numeric_expr.evaluate(expression, int_literal, int_operation)
inspect(result is Ok(10), content="true")
}
sum は、演算が任意個の引数を取りうることを示しています。それを決めるのはコールバックです。
BinFloat 上で評価し、IEEE フラグを収集する
値の型 V は数値以上のものを保持できます。ここでは各値が BinFloat とそれまでに発生した IEEE フラグの組であり、各演算は引数のフラグと自身のフラグを統合します。
///|
fn binary_literal(
literal : @numeric_expr.Literal,
) -> Result[(@bin_float.BinFloat, @bin_float.BinaryFlags), String] {
match
@bin_float.BinFloat::from_string_ctx(
literal.raw(),
@bin_float.BinaryContext::binary64(),
) {
Ok(pair) => Ok(pair)
Err(_) => Err("not a number: " + literal.raw())
}
}
///|
fn binary_operation(
operation : @numeric_expr.Operation,
arguments : Array[(@bin_float.BinFloat, @bin_float.BinaryFlags)],
) -> Result[(@bin_float.BinFloat, @bin_float.BinaryFlags), String] {
let context = @bin_float.BinaryContext::binary64()
match (operation.name(), arguments) {
("add", [(a, fa), (b, fb)]) => {
let (value, flags) = a.add_ctx(b, context)
Ok((value, fa.combine(fb).combine(flags)))
}
("div", [(a, fa), (b, fb)]) => {
let (value, flags) = a.div_ctx(b, context)
Ok((value, fa.combine(fb).combine(flags)))
}
_ => Err("unsupported " + operation.name())
}
}
///|
test "binary64 evaluation with flags" {
// 1/3 + 1/3 in binary64
let third = call("div", [lit("1"), lit("3")])
let result = @numeric_expr.evaluate(
call("add", [third, third]),
binary_literal,
binary_operation,
)
match result {
Ok((value, flags)) => {
inspect(value.to_hex(), content="0x15555555555555p-53")
inspect(flags.inexact(), content="true")
}
Err(_) => fail("expected a value")
}
}
2 つのコールバックを差し替えるだけで、同じ木を @decimal_gda.Decimal 上で評価することもできます。式の中に数値型に依存する部分は何もありません。
エラーをそのソース位置で報告する
各ノードに、それが由来するテキストのスパンを与えます。コールバックが失敗すると、EvalError は失敗したノードを返すので、スパンによってエラーの位置を特定できます。
///|
test "locate a bad operand" {
let span = @numeric_expr.SourceSpan::new("rows.txt", line=42, column=9)
let expression = @numeric_expr.Expr::invoke(
@numeric_expr.Operation::new("add", span~),
[
@numeric_expr.Expr::literal(@numeric_expr.Literal::new("2", span~)),
@numeric_expr.Expr::literal(@numeric_expr.Literal::new("two", span~)),
],
)
let message = match
@numeric_expr.evaluate(expression, int_literal, int_operation) {
Ok(_) => "ok"
Err(@numeric_expr.LiteralFailure(literal, error)) =>
literal.span().source() +
":" +
literal.span().line().to_string() +
": " +
error
Err(@numeric_expr.OperationFailure(operation, error)) =>
operation.name() + ": " + error
Err(@numeric_expr.UnsupportedExpression(_)) => "unsupported"
}
inspect(message, content="rows.txt:42: not a small integer: two")
}
評価順序を確かめる
コールバックは左から右への後行順で実行され、評価は最初の失敗で停止します。トレースを取るとこれがわかります。
///|
test "post-order with early stop" {
let trace : Array[String] = []
let expression = call("add", [
call("mul", [lit("2"), lit("3")]),
call("add", [lit("bad"), lit("1")]),
])
let _ = @numeric_expr.evaluate(
expression,
literal => {
trace.push(literal.raw())
int_literal(literal)
},
(operation, arguments) => {
trace.push(operation.name())
int_operation(operation, arguments)
},
)
inspect(trace.join(" "), content="2 3 mul bad")
}
bad が先に失敗するため、リテラル 1 と 2 つの add ノードはいずれも訪問されません。
さらに進んで
- コーパスのフロントエンド。
frontend/gda_exprは、.decTestの各行id operation operands -> expected conditionsをExpr::invoke(Operation::new(op), operands.map(Expr::literal))に変換し、@decimal_gda上のコールバックで評価します。このパターンの完全な例については gda_expr を参照してください。 - より豊かな値。 演算が異なる種類の結果(数値、真偽値、文字列)を返す場合、
Vを列挙型にできます。gda_exprはcompare、class、isnanについてそうしています。 - 型付きのエラー。
EはStringの代わりに独自の列挙型にできます。EvalError[E]はそれをそのまま保持します。 - checked なパイプライン。 ドメインエラーで評価を止めずに値として残したい場合は、
Vを@bin_float_checked.BinFloatResultのような checked 型にし、コールバックが常にOkを返すようにします。
よくある落とし穴
- アリティ検査はありません。
Expr::invokeは任意個の引数を受け付けます。invokeでアリティを検査し(配列パターンで検査できます)、一致しなければErrを返してください。 - 早期停止。 最初の失敗の後、それ以上コールバックは実行されません。失敗したノードより右にあるノードのコールバックの副作用に依存しないでください。
- 深い木。 評価は再帰的で、スタックの深さは木の高さに等しくなります。非常に深い左偏りの木(例えば入れ子の
addで構築した 100 万項の和)はスタックを使い果たす可能性があります。代わりに平坦なsumノードを構築してください。 - 既定のスパン。 明示的なスパンがない場合、ノードは
SourceSpan::new("")を報告し、その行と列は0です。 - ここではリテラルを解析しません。
Literalは生のテキストを保持します。変換の丸め、精度、フラグはすべてdecodeコールバックが決定します。
次のステップ
- numeric_expr API はすべての項目を列挙しています。
- numeric_expr 設計 は評価の意味論とその不変条件を述べています。
- gda_expr チュートリアル は、このパッケージを通して実際の 10 進テスト行を実行します。