moon_cel

    A pure MoonBit Common Expression Language core with parsing, checking, and deterministic evaluation.

    cel
    expression
    policy
    interpreter
    parser
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    32

    No dependencies

    #MoonCEL

    MoonCEL is an embeddable Common Expression Language core written in MoonBit. It parses source-aware expressions, performs gradual static checking, compiles an opaque reusable program, and evaluates it with deterministic resource limits. The core library checks on every stable MoonBit backend; the CLI is native-only because it uses process arguments and exit codes.

    MoonCEL targets the language described by CEL Spec v0.25.3. Version 0.1 is an intentionally bounded subset, not a claim of full CEL conformance.

    #What works

    • null, bool, int64, uint64, double, string, bytes, list, and map values;
    • identifiers, calls, receiver calls, selection, indexing, unary operators, arithmetic, relations, equality, membership, logical operators, and ternary;
    • source spans and aggregate checker diagnostics;
    • dyn, scalar types, list<T>, and map<K,V>;
    • short-circuit evaluation and deterministic structured errors;
    • has, all, exists, exists_one, map, and filter macros with lexical iteration variables;
    • cost, recursion-depth, and collection-iteration budgets;
    • checked int64/uint64 arithmetic, division-by-zero checks, and conversion failures;
    • JSON activation/type bridges and stable JSON AST output;
    • native parse, check, and eval commands.

    #Embed it

    ///|
    let env = @moon_cel.TypeEnv::new()
    .with_variable("role", @moon_cel.CelType::String)
    .with_variable("amount", @moon_cel.CelType::Int)
    .with_variable("limit", @moon_cel.CelType::Int)

    ///|
    let program = @moon_cel.compile("role == 'admin' || amount <= limit", env)

    ///|
    let activation = @moon_cel.Activation::new()
    .bind("role", @moon_cel.Value::String("reviewer"))
    .bind("amount", @moon_cel.Value::Int(75L))
    .bind("limit", @moon_cel.Value::Int(100L))

    ///|
    let decision = program.eval(
    activation,
    limits=@moon_cel.EvalLimits::new(max_cost=1000),
    )

    The principal APIs are:

    parse(source : StringView) -> ParsedExpr raise ParseError
    check(expr : ParsedExpr, env : TypeEnv) -> CheckResult
    compile(source : StringView, env : TypeEnv) -> Program raise CompileError
    Program::eval(activation : Activation, limits? : EvalLimits) -> Value raise EvalError

    ParsedExpr, CheckedExpr, and Program are opaque. Public models include Expr, Value, CelType, Diagnostic, SourceSpan, TypeEnv, Activation, and EvalLimits.

    #CLI

    Run locally with the native target:

    moon run cmd/main --target native -- parse "1 + 2 * 3" --json moon run cmd/main --target native -- check "amount <= limit" --types-json '{"amount":"int","limit":"int"}' --json moon run cmd/main --target native -- eval "role == 'admin' || amount <= limit" --vars-json '{"role":"reviewer","amount":75,"limit":100}' --max-cost 1000 --json

    Exit status is 0 on success, 1 for parse/check/evaluation failure, and 2 for invalid command usage. JSON inputs support JSON-expressible CEL values. UInt and Bytes remain lossless through the programmatic API and use tagged objects when emitted as JSON.

    The access-control example combines a role, request amount, configured limit, and resource tags:

    moon run examples/access_control --target native

    #Compatibility matrix

    Areav0.1 statusNotes
    Core scalar literalsSupportedNull, Bool, Int64, UInt64, Double, String, Bytes
    Lists and mapsSupportedMap keys follow the checked CEL scalar-key subset
    Selection and indexingSupportedString-keyed maps provide field-style selection
    OperatorsSupportedIncludes lazy &&, ||, in, and ternary
    Gradual type checkingSupporteddyn propagation and aggregate diagnostics
    Standard conversionsSupported subsetint, uint, double, string, bytes, type, size
    String receiver functionsSupported subsetcontains, startsWith, endsWith, matches
    matches regular expressionsBounded subset., *, +, ?, ^, $, escapes; groups/classes/alternation rejected
    Standard macrosSupportedhas, all, exists, exists_one, map, filter
    Resource limitsSupportedNode cost, recursive depth, collection iterations
    JSON bridgeSupportedJSON values, type environments, activations, AST and diagnostics
    Protobuf values/constructionNot supportedExplicit v0.1 exclusion
    Timestamp and durationNot supportedExplicit v0.1 exclusion
    Optional types/extensionsNot supportedExplicit v0.1 exclusion
    CEL Policy YAML layerNot supportedThis project is the expression core only
    Full RE2 semanticsNot supportedThe bounded portable matcher rejects unsupported syntax
    Official full conformance suiteNot claimedOnly supported v0.1 cases are adapted and tested

    #Determinism and safety

    EvalLimits defaults to 10,000 evaluated nodes, depth 64, and 10,000 collection iterations. Every evaluated AST node consumes cost; skipped lazy branches consume none. Comprehension iterations are charged separately. Invalid limits are rejected before evaluation. Integer operations check overflow/underflow before performing the operation.

    matches is deliberately bounded to a portable regular-expression subset. Its matcher uses finite position sets rather than recursive backtracking. This keeps behavior consistent across MoonBit targets and avoids exponential regex execution.

    #Development and verification

    moon update moon fmt --check moon check --target all --deny-warn moon build --target all --deny-warn moon test --target all --deny-warn python scripts/count_effective_moonbit.py --minimum 4000 --maximum 4500 moon info git diff --exit-code moon package --list moon publish --dry-run --frozen

    The line gate counts production MoonBit only. It excludes tests, CLI, examples, generated interfaces, comments, blank lines, and build directories. CI also checks CLI success/failure/usage exit codes.

    moon publish --dry-run --frozen completes local archive extraction and checking, then verifies the authenticated Mooncakes owner. Run that final gate only after logging in as aloyscc; another account must not be used. CI instead builds the frozen archive, extracts it into a clean directory, and runs frozen all-target checking without requiring publishing credentials.

    Upstream version, license, adapted test scope, and explicit skips are recorded in UPSTREAM.md. See CONTRIBUTING.md for the change gate.

    #License

    Apache-2.0. CEL is an open specification maintained by the CEL project; MoonCEL is an independent MoonBit implementation.

    CompileError

    pub(all) suberror CompileError {
    CompileError(diagnostics~ : Array[Diagnostic])
    } derive(Eq,
    Debug
    )

    EvalError

    pub(all) suberror EvalError {
    UnboundVariable(name~ : String, span~ : SourceSpan)
    TypeMismatch(expected~ : String, actual~ : String, span~ : SourceSpan)
    NoSuchField(field~ : String, span~ : SourceSpan)
    NoSuchKey(key~ : String, span~ : SourceSpan)
    IndexOutOfBounds(index~ : Int, length~ : Int, span~ : SourceSpan)
    DivisionByZero(span~ : SourceSpan)
    NumericOverflow(span~ : SourceSpan)
    UnknownFunction(name~ : String, span~ : SourceSpan)
    InvalidArgument(message~ : String, span~ : SourceSpan)
    CostLimitExceeded(limit~ : Int, span~ : SourceSpan)
    DepthLimitExceeded(limit~ : Int, span~ : SourceSpan)
    IterationLimitExceeded(limit~ : Int, span~ : SourceSpan)
    } derive(Eq,
    Debug
    )

    EvalError::code

    fn EvalError::code(self : EvalError) -> String

    EvalError::message

    fn EvalError::message(self : EvalError) -> String

    EvalError::render_with_source

    fn EvalError::render_with_source(self : EvalError, source : String) -> String

    EvalError::span

    fn EvalError::span(self : EvalError) -> SourceSpan

    EvalError::to_diagnostic

    fn EvalError::to_diagnostic(self : EvalError) -> Diagnostic

    JsonBridgeError

    pub(all) suberror JsonBridgeError {
    InvalidJsonValue(message~ : String)
    InvalidTypeName(name~ : String)
    } derive(Eq,
    Debug
    )

    ParseError

    pub(all) suberror ParseError {
    ParseError(code~ : String, message~ : String, span~ : SourceSpan)
    } derive(Eq,
    Debug
    )

    Activation

    pub(all) struct Activation {
    values : Map[String, Value]
    } derive(
    Debug
    )

    Activation::bind

    fn Activation::bind(self : Activation, name : String, value : Value) -> Activation

    Activation::contains

    fn Activation::contains(self : Activation, name : String) -> Bool

    Activation::lookup

    fn Activation::lookup(self : Activation, name : String) -> Value?

    Activation::new

    fn Activation::new() -> Activation

    Activation::type_env

    fn Activation::type_env(self : Activation) -> TypeEnv

    BinaryOp

    pub(all) enum BinaryOp {
    Or
    And
    Equal
    NotEqual
    Less
    LessEqual
    Greater
    GreaterEqual
    In
    Add
    Subtract
    Multiply
    Divide
    Modulo
    } derive(Eq,
    Debug
    )

    BinaryOp::precedence

    fn BinaryOp::precedence(self : BinaryOp) -> Int

    BinaryOp::symbol

    fn BinaryOp::symbol(self : BinaryOp) -> String

    CelType

    pub(all) enum CelType {
    Dyn
    Null
    Bool
    Int
    UInt
    Double
    String
    Bytes
    List(CelType)
    Map(CelType, CelType)
    } derive(Eq,
    Debug
    )

    CelType::accepts

    fn CelType::accepts(self : CelType, actual : CelType) -> Bool

    CelType::common

    fn CelType::common(self : CelType, other : CelType) -> CelType

    CelType::display

    fn CelType::display(self : CelType) -> String

    CelType::is_collection

    fn CelType::is_collection(self : CelType) -> Bool

    CelType::is_numeric

    fn CelType::is_numeric(self : CelType) -> Bool

    CheckResult

    pub(all) struct CheckResult {
    checked : CheckedExpr?
    diagnostics : Array[Diagnostic]
    } derive(
    Debug
    )

    CheckResult::error_count

    fn CheckResult::error_count(self : CheckResult) -> Int

    CheckResult::is_ok

    fn CheckResult::is_ok(self : CheckResult) -> Bool

    CheckResult::warning_count

    fn CheckResult::warning_count(self : CheckResult) -> Int

    CheckedExpr

    pub struct CheckedExpr {
    parsed : ParsedExpr
    root_type : CelType
    inferred : Map[Int, CelType]
    } derive(
    Debug
    )

    CheckedExpr::parsed

    fn CheckedExpr::parsed(self : CheckedExpr) -> ParsedExpr

    CheckedExpr::root_type

    fn CheckedExpr::root_type(self : CheckedExpr) -> CelType

    CheckedExpr::type_of

    fn CheckedExpr::type_of(self : CheckedExpr, expr_id : Int) -> CelType?

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    message : String
    severity : Severity
    span : SourceSpan
    } derive(Eq,
    Debug
    )

    Diagnostic::error

    fn Diagnostic::error(code : String, message : String, span : SourceSpan) -> Diagnostic

    Diagnostic::note

    fn Diagnostic::note(code : String, message : String, span : SourceSpan) -> Diagnostic

    Diagnostic::render

    fn Diagnostic::render(self : Diagnostic) -> String

    Diagnostic::render_with_source

    fn Diagnostic::render_with_source(self : Diagnostic, source : String) -> String

    Diagnostic::warning

    fn Diagnostic::warning(code : String, message : String, span : SourceSpan) -> Diagnostic

    EvalLimits

    pub(all) struct EvalLimits {
    max_cost : Int
    max_depth : Int
    max_iterations : Int
    } derive(Eq,
    Debug
    )

    EvalLimits::default

    fn EvalLimits::default() -> EvalLimits

    EvalLimits::is_valid

    fn EvalLimits::is_valid(self : EvalLimits) -> Bool

    EvalLimits::new

    fn EvalLimits::new(max_cost? : Int, max_depth? : Int, max_iterations? : Int) -> EvalLimits

    Expr

    pub(all) struct Expr {
    id : Int
    span : SourceSpan
    kind : ExprKind
    } derive(Eq,
    Debug
    )

    Expr::binary

    fn Expr::binary(id : Int, span : SourceSpan, op : BinaryOp, left : Expr, right : Expr) -> Expr

    Expr::call

    fn Expr::call(id : Int, span : SourceSpan, name : String, args : Array[Expr]) -> Expr

    Expr::child_count

    fn Expr::child_count(self : Expr) -> Int

    Expr::conditional

    fn Expr::conditional(id : Int, span : SourceSpan, condition : Expr, when_true : Expr, when_false : Expr) -> Expr

    Expr::identifier

    fn Expr::identifier(id : Int, span : SourceSpan, name : String) -> Expr

    Expr::index

    fn Expr::index(id : Int, span : SourceSpan, target : Expr, index : Expr) -> Expr

    Expr::kind_name

    fn Expr::kind_name(self : Expr) -> String

    Expr::list

    fn Expr::list(id : Int, span : SourceSpan, values : Array[Expr]) -> Expr

    Expr::literal

    fn Expr::literal(id : Int, span : SourceSpan, value : Literal) -> Expr

    Expr::map

    fn Expr::map(id : Int, span : SourceSpan, entries : Array[MapExprEntry]) -> Expr

    Expr::node_count

    fn Expr::node_count(self : Expr) -> Int

    Expr::receiver_call

    fn Expr::receiver_call(id : Int, span : SourceSpan, target : Expr, name : String, args : Array[Expr]) -> Expr

    Expr::select

    fn Expr::select(id : Int, span : SourceSpan, target : Expr, field : String) -> Expr

    Expr::to_json

    fn Expr::to_json(self : Expr) -> Json

    Convert an expression and all descendants to a stable JSON AST.

    Expr::unary

    fn Expr::unary(id : Int, span : SourceSpan, op : UnaryOp, operand : Expr) -> Expr

    ExprKind

    pub(all) enum ExprKind {
    Literal(Literal)
    Identifier(String)
    List(Array[Expr])
    Map(Array[MapExprEntry])
    Unary(UnaryOp, Expr)
    Binary(BinaryOp, Expr, Expr)
    Conditional(Expr, Expr, Expr)
    Select(Expr, String)
    Index(Expr, Expr)
    Call(String, Array[Expr])
    ReceiverCall(Expr, String, Array[Expr])
    } derive(Eq,
    Debug
    )

    Literal

    pub(all) enum Literal {
    Null
    Bool(Bool)
    Int(Int64)
    UInt(UInt64)
    Double(Double)
    String(String)
    Bytes(Bytes)
    } derive(Eq,
    Debug
    )

    Literal::kind_name

    fn Literal::kind_name(self : Literal) -> String

    MacroKind

    pub(all) enum MacroKind {
    Has
    All
    Exists
    ExistsOne
    Map
    Filter
    } derive(Eq,
    Debug
    )

    MacroKind::name

    fn MacroKind::name(self : MacroKind) -> String

    MapExprEntry

    pub(all) struct MapExprEntry {
    key : Expr
    value : Expr
    span : SourceSpan
    } derive(Eq,
    Debug
    )

    ParsedExpr

    pub struct ParsedExpr {
    source : String
    root : Expr
    } derive(
    Debug
    )

    ParsedExpr::node_count

    fn ParsedExpr::node_count(self : ParsedExpr) -> Int

    ParsedExpr::root

    fn ParsedExpr::root(self : ParsedExpr) -> Expr

    ParsedExpr::source

    fn ParsedExpr::source(self : ParsedExpr) -> String

    ParsedExpr::to_json

    fn ParsedExpr::to_json(self : ParsedExpr) -> Json

    ParsedExpr::to_json_string

    fn ParsedExpr::to_json_string(self : ParsedExpr, indent? : Int) -> String

    Program

    pub struct Program {
    checked : CheckedExpr
    } derive(
    Debug
    )

    Program::eval

    fn Program::eval(self : Program, activation : Activation, limits? : EvalLimits) -> Value raise EvalError

    Evaluate a compiled expression against an immutable activation.

    Program::node_count

    fn Program::node_count(self : Program) -> Int

    Program::result_type

    fn Program::result_type(self : Program) -> CelType

    Program::source

    fn Program::source(self : Program) -> String

    Severity

    pub(all) enum Severity {
    Error
    Warning
    Note
    } derive(Eq,
    Debug
    )

    Severity::label

    fn Severity::label(self : Severity) -> String

    SourceExcerpt

    pub(all) struct SourceExcerpt {
    line_number : Int
    column : Int
    source_line : String
    marker : String
    } derive(Eq,
    Debug
    )

    SourceExcerpt::render

    fn SourceExcerpt::render(self : SourceExcerpt) -> String

    SourceSpan

    pub(all) struct SourceSpan {
    start : Int
    end : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    A half-open source range measured in UTF-16 code units.

    SourceSpan::contains

    fn SourceSpan::contains(self : SourceSpan, offset : Int) -> Bool

    SourceSpan::empty

    fn SourceSpan::empty() -> SourceSpan

    SourceSpan::format

    fn SourceSpan::format(self : SourceSpan) -> String

    SourceSpan::is_empty

    fn SourceSpan::is_empty(self : SourceSpan) -> Bool

    SourceSpan::length

    fn SourceSpan::length(self : SourceSpan) -> Int

    SourceSpan::merge

    fn SourceSpan::merge(self : SourceSpan, other : SourceSpan) -> SourceSpan

    SourceSpan::new

    fn SourceSpan::new(start~ : Int, end~ : Int, line~ : Int, column~ : Int) -> SourceSpan

    TypeEnv

    pub(all) struct TypeEnv {
    variables : Map[String, CelType]
    } derive(
    Debug
    )

    TypeEnv::contains

    fn TypeEnv::contains(self : TypeEnv, name : String) -> Bool

    TypeEnv::lookup

    fn TypeEnv::lookup(self : TypeEnv, name : String) -> CelType?

    TypeEnv::names

    fn TypeEnv::names(self : TypeEnv) -> Array[String]

    TypeEnv::new

    fn TypeEnv::new() -> TypeEnv

    TypeEnv::with_variable

    fn TypeEnv::with_variable(self : TypeEnv, name : String, cel_type : CelType) -> TypeEnv

    UnaryOp

    pub(all) enum UnaryOp {
    Not
    Negate
    } derive(Eq,
    Debug
    )

    UnaryOp::symbol

    fn UnaryOp::symbol(self : UnaryOp) -> String

    Value

    pub(all) enum Value {
    Null
    Bool(Bool)
    Int(Int64)
    UInt(UInt64)
    Double(Double)
    String(String)
    Bytes(Bytes)
    List(Array[Value])
    Map(Array[ValueEntry])
    } derive(Eq,
    Debug
    )

    Value::cel_type

    fn Value::cel_type(self : Value) -> CelType

    Value::contains

    fn Value::contains(self : Value, needle : Value) -> Bool

    Value::display

    fn Value::display(self : Value) -> String

    Value::kind_name

    fn Value::kind_name(self : Value) -> String

    Value::map_lookup

    fn Value::map_lookup(self : Value, key : Value) -> Value?

    Value::size

    fn Value::size(self : Value) -> Int?

    Value::to_json

    fn Value::to_json(self : Value) -> Json raise JsonBridgeError

    Convert a CEL value to stable JSON. UInt and Bytes use tagged objects.

    Value::to_json_string

    fn Value::to_json_string(self : Value, indent? : Int) -> String raise JsonBridgeError

    ValueEntry

    pub(all) struct ValueEntry {
    key : Value
    value : Value
    } derive(Eq,
    Debug
    )

    activation_from_json

    fn activation_from_json(json : Json) -> Activation raise JsonBridgeError

    check

    fn check(expr : ParsedExpr, env : TypeEnv) -> CheckResult

    Type-check a parsed CEL expression in the supplied variable environment.

    compile

    fn compile(source : StringView, env : TypeEnv) -> Program raise CompileError

    Parse and type-check a CEL expression, producing a reusable program.

    diagnostics_to_json

    fn diagnostics_to_json(diagnostics : Array[Diagnostic]) -> Json

    lex_labels

    fn lex_labels(source : String) -> Array[String] raise ParseError

    parse

    fn parse(source : StringView) -> ParsedExpr raise ParseError

    Parse one CEL expression into a source-aware abstract syntax tree.

    parse_literal

    fn parse_literal(source : StringView) -> Literal raise ParseError

    Parse exactly one CEL literal without parsing a complete expression.

    parse_type_name

    fn parse_type_name(name : String) -> CelType raise JsonBridgeError

    Parse the compact type syntax accepted by the CLI type environment.

    source_excerpt

    fn source_excerpt(source : String, span : SourceSpan) -> SourceExcerpt

    Extract a single-line source frame for a diagnostic span.

    standard_function_names

    fn standard_function_names() -> Array[String]

    Names provided by the v0.1 standard environment.

    type_env_from_json

    fn type_env_from_json(json : Json) -> TypeEnv raise JsonBridgeError

    value_from_json

    fn value_from_json(json : Json) -> Value raise JsonBridgeError

    Convert JSON-expressible data to CEL values.