Documentation
¶
Overview ¶
Package predicate is a small, sandboxed expression-evaluation engine for boolean predicates over named values and host-registered functions.
The package parses a strict subset of Lua expression syntax via gopher-lua's parse package (already vendored for write-path automation), walks the AST against a hard allow-list, translates it to a typed internal IR, and evaluates the IR against caller-supplied bindings.
Lifecycle ¶
env := predicate.NewEnv()
env.DeclareVar("entity", predicate.RecordType{
"status": predicate.StringType,
})
env.DeclareVar("current_user", predicate.RecordType{})
env.DeclareFunc("has_role", predicate.FuncSig{
Params: []predicate.Type{predicate.RecordType{}, predicate.StringType},
Return: predicate.BoolType,
})
prog, err := predicate.Compile(env,
`entity.status == 'review' and has_role(current_user, 'reviewer')`)
if err != nil { ... }
b := predicate.NewBindings()
b.SetVar("entity", predicate.NewRecord(map[string]predicate.Value{
"status": predicate.NewString("review"),
}))
b.SetVar("current_user", predicate.NewRecord(map[string]predicate.Value{}))
b.SetFunc("has_role", predicate.FuncFunc(
func(ctx context.Context, args []predicate.Value) (predicate.Value, error) {
return predicate.NewBool(true), nil
},
))
v, err := prog.Eval(ctx, b)
// v.(predicate.Bool).Bool() == true
Concurrency ¶
A *Program is immutable after Compile and is safe to Eval concurrently from multiple goroutines, each with its own Bindings. The Eval call allocates per-invocation visitor state; no caches or memoization live on *Program.
Equality semantics ¶
Predicates use Lua-flavored equality, not Go-flavored. Comparison across most type pairs is a compile-time error; the few mixed-type pairs that compile follow this table:
a is b is a == b -------- -------- -------------------------- nil nil true nil anything false bool bool Go == number number float64 == (single numeric type, see below) string string byte-equal (incl. null bytes)
Ordered comparisons (<, <=, >, >=) require two numbers or two strings; strings compare lexicographically (byte-wise).
Numeric model ¶
Numbers are a single type backed by float64, matching Lua 5.1 semantics. Integer literals (1, 0xFF), float literals (1.0, 1.5e-3), and exponential forms (1e10) all parse to the same Number type. Bindings of Go int are promoted to float64 at binding time; values outside the 53-bit integer range round per IEEE 754.
Security model ¶
The walker rejects any AST node not on the allow-list (default-reject branch on every switch). Per-field invariants are enforced beyond node type — e.g. AttrGetExpr.Key must be *StringExpr, rejecting computed attribute access entity[expr]. A compile-time depth budget (default 256) defends against stack overflow from adversarially nested expressions; a per-Eval step budget (default 10_000) defends against runtime exhaustion. Neither budget can be disabled, only raised.
The package does no I/O: no file access, no network, no goroutine spawning. It is a pure function from (Program, Bindings) to a Value.
Index ¶
- func CompileAll(env *Env, sources []NamedSource, opts ...CompileOption) ([]*Program, []Issue)
- type Bindings
- type Bool
- type CompileError
- type CompileOption
- type Env
- type EvalError
- type EvalOption
- type Func
- type FuncFunc
- type FuncSig
- type Issue
- type List
- type ListType
- type NamedSource
- type Nil
- type Number
- type ParseError
- type Program
- type Record
- type RecordType
- type String
- type Type
- type Value
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CompileAll ¶
func CompileAll(env *Env, sources []NamedSource, opts ...CompileOption) ([]*Program, []Issue)
CompileAll compiles every source in turn against env and returns (a) the compiled programs in input order, with a nil entry where compile failed, and (b) one Issue per failed source.
Intended for batch checking at policy-load time so a caller (e.g. an ACL loader in a future PR) can fail-fast on bad rules while keeping the successfully compiled programs ready for use — no double-parse needed (RR-LQE9).
Types ¶
type Bindings ¶
type Bindings struct {
// contains filtered or unexported fields
}
Bindings carries the runtime values a predicate evaluates against: concrete Values for each declared variable plus implementations for each declared host function.
Build a Bindings with NewBindings and the SetVar/SetFunc methods; the engine does not expose the underlying maps so callers cannot mutate them mid-evaluation. A *Bindings is safe to reuse across Eval calls but is not safe for concurrent mutation; build once and share the resulting value.
func NewBindings ¶
func NewBindings() *Bindings
NewBindings returns an empty Bindings ready for SetVar / SetFunc.
type Bool ¶
type Bool struct {
// contains filtered or unexported fields
}
Bool is a concrete-typed boolean value.
type CompileError ¶
CompileError reports a failure to translate the parsed AST into the predicate IR: an unsupported AST node, a per-field invariant violation, an unknown symbol, a type mismatch, or a budget overrun.
func (*CompileError) Error ¶
func (e *CompileError) Error() string
type CompileOption ¶
type CompileOption func(*compileOptions)
CompileOption configures Compile.
func WithMaxDepth ¶
func WithMaxDepth(n int) CompileOption
WithMaxDepth overrides the compile-time depth budget. Clamped to >= 1.
type Env ¶
type Env struct {
// contains filtered or unexported fields
}
Env declares the variables and functions a predicate may reference. Build one before calling Compile.
Env is mutable until the first Compile that uses it; callers should finish declarations before any compile. Concurrent declares are not safe; declare then share.
func (*Env) DeclareFunc ¶
DeclareFunc registers a host function name and its signature.
The return type must be a scalar (bool, number, string, nil). Record and list return types are rejected (RR-93UN): the engine's runtime type check does not reach into a returned Record's fields, so a downstream entity.attribute access on a host-returned record could observe a typed field whose runtime type differs from the declared type. Until the type checker is extended to re-validate nested values, host functions return scalars only. (Current use cases — has_role, has_relation, count_relations — all do.)
type EvalError ¶
type EvalError struct {
Reason string
}
EvalError reports a failure during Eval — a missing binding, a host function returning the wrong type, or the per-Eval step budget being exhausted.
type EvalOption ¶
type EvalOption func(*evalOptions)
EvalOption configures a single Eval call. Options stack left-to-right; later options override earlier ones.
func WithStepBudget ¶
func WithStepBudget(n int) EvalOption
WithStepBudget overrides the per-Eval step budget. Must be > 0; values <= 0 are clamped to 1 so a misconfigured caller cannot disable the budget.
type Func ¶
Func is a host-implemented function callable from a predicate. The engine type-checks args against the declared FuncSig at compile time; an implementation receives args in the declared types and must return a Value of the declared return type.
Func is an interface (not a function type) so implementations can carry state and so the engine can pass a context.Context — needed once host functions traverse the store, hit caches, or are cancellable.
type FuncFunc ¶
FuncFunc adapts a Go closure into a Func. Use it when the host function has no state of its own.
type FuncSig ¶
FuncSig declares a host function's parameter and return types. A non-nil Variadic indicates the function accepts zero or more extra arguments of that type after the fixed Params.
type Issue ¶
Issue is a single compile failure: the source it came from and the parse / compile error that produced it. Err is one of *ParseError or *CompileError; use errors.As to inspect (RR-8VKE).
type List ¶
type List struct {
// contains filtered or unexported fields
}
List is an ordered sequence of values, currently unused by the expression grammar (no list literals) but reachable through host functions whose return type is a list. Reserved here so the surface is stable.
func NewList ¶
NewList constructs a List. As with NewRecord, the returned List retains the supplied slice by reference — callers must not mutate it after the call (RR-AJS4).
type NamedSource ¶
NamedSource pairs a predicate source string with a stable name the caller can use to identify the source in lint output.
type Nil ¶
type Nil struct{}
Nil is the predicate engine's nil value. Distinct from Go's nil and from a missing binding.
type Number ¶
type Number struct {
// contains filtered or unexported fields
}
Number is a concrete numeric value, backed by float64. There is no separate integer type — see doc.go ("Numeric model").
func NewNumberFromInt ¶
NewNumberFromInt constructs a Number value from a Go int, with the integer promoted to float64.
type ParseError ¶
ParseError reports a failure inside gopher-lua's parser. Line and Col are best-effort: they reflect the position the parser reported, adjusted for the synthetic "return " prefix we prepend.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
type Program ¶
type Program struct {
// contains filtered or unexported fields
}
Program is a compiled predicate, ready for repeated evaluation.
A Program is immutable after Compile. It carries no mutable state, no caches, and no per-instance memoization. Multiple goroutines may call Eval on the same Program concurrently with their own Bindings.
func Compile ¶
func Compile(env *Env, source string, opts ...CompileOption) (prog *Program, err error)
Compile parses source as a single Lua expression, walks the AST against the predicate engine's allow-list, and type-checks against env. The returned *Program is safe for concurrent evaluation.
A nil env is rejected. The source must be a single expression (statements, multi-return-value, and leading `return` are all rejected with a *CompileError naming the failure mode).
func (*Program) Eval ¶
Eval evaluates the program against bindings and returns the result value or an *EvalError. Safe to call concurrently with distinct bindings (see doc.go).
The context is threaded through to any host function the program invokes. A nil bindings argument is treated as empty; refer to any declared variable or function and Eval returns an *EvalError.
func (*Program) ResultType ¶
ResultType returns the static type of the program's top-level expression.
type Record ¶
type Record struct {
// contains filtered or unexported fields
}
Record is a named-field bundle, the value-form of a Lua table used for entity-shape access (entity.status). Field access happens at eval time through the AttrGet IR op.
func NewRecord ¶
NewRecord constructs a Record from a map. The returned Record retains the supplied map by reference — callers must not mutate it after the call (RR-AJS4). Pass a freshly built map if the caller intends to keep working with one of its own.
type RecordType ¶
Record is a named-field type descriptor. Used both as a static declaration and as a runtime Value (see value.go). The fields map declares attribute name → type for an entity-like structure.
type String ¶
type String struct {
// contains filtered or unexported fields
}
String is a concrete string value. Lua strings are byte-strings; we preserve any bytes the caller binds, including embedded null bytes.
type Type ¶
type Type interface {
// contains filtered or unexported methods
}
Type is the static type a Value can carry. The type system is deliberately tiny: it only needs to discriminate the cases the expression grammar can express.
var ( BoolType Type = primitiveType{"bool"} NumberType Type = primitiveType{"number"} StringType Type = primitiveType{"string"} NilType Type = primitiveType{"nil"} // AnyType only appears in host-function signatures: it accepts // any Value. Use sparingly — it short-circuits the type checker. AnyType Type = primitiveType{"any"} )
Public type descriptors callers use when declaring an env.
type Value ¶
type Value interface {
Type() Type
// contains filtered or unexported methods
}
Value is the sealed sum type the evaluator operates on. The unexported sealedValue method prevents external packages from inventing new variants; everything that round-trips through the engine must be one of the constructors below.