predicate

package
v0.0.0-...-eabb6a4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

Documentation

Overview

Package predicate is a small, sandboxed typed expression-evaluation engine over named values and host-registered functions. Compile retains the strict boolean-predicate profile; CompileValue accepts an explicit scalar profile.

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.

The separate Int type (int64) exists for integer-typed metamodel properties so comparisons are exact and non-lexicographic — there is no Int *literal* syntax, so a number literal compared against an Int-typed attribute is coerced to Int at compile time (rejecting a fractional literal). See the Typed values section.

Typed values (Int, Date) and literal coercion

Beyond the scalar primitives, the engine carries two metamodel-driven typed values: Int (int64) and Date (time.Time, instant-granular, backing both `date` and `datetime` properties). There is no literal syntax for either — an integer is written as a number literal and a date as a string literal. When one operand of a comparison is an Int/Date-typed expression (declared via IntType / DateTypeWithLayout on the Env) and the other is a bare literal, the literal is COERCED to the operand's type at COMPILE time: a number literal becomes an Int (fractional literals are rejected), a string literal is parsed to a Date against the field's declared layout.

Coercion at compile time is deliberate: it keeps Eval a pure function with no parsing and no metamodel access (the Date is already a time.Time by the time Eval runs), which is what lets the engine be used on the read/ACL path. The metamodel->Env mapping (property type -> predicate type, incl. the date layout) lives in internal/predicatefns so this package stays dependency-free.

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.

Target portability

Programs expose their exact static record-field references and whether every node/host function declares SQL-portable semantics. This is classification, not lowering: a host-only function remains valid in a context that permits it and makes only that Program non-portable. Context profiles can restrict the accepted feature set, but must never change the semantics of accepted IR.

Index

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.

func (*Bindings) SetFunc

func (b *Bindings) SetFunc(name string, f Func) error

SetFunc binds an implementation to a host function name.

func (*Bindings) SetVar

func (b *Bindings) SetVar(name string, v Value) error

SetVar binds a value to a variable name. Returns an error on empty name or nil value (the typed Nil value is fine; a Go nil interface is not).

type Bool

type Bool struct {
	// contains filtered or unexported fields
}

Bool is a concrete-typed boolean value.

func NewBool

func NewBool(b bool) Bool

NewBool constructs a Bool value.

func (Bool) Bool

func (b Bool) Bool() bool

Bool returns the underlying Go bool.

func (Bool) Type

func (Bool) Type() Type

type CompileError

type CompileError struct {
	Line   int
	Col    int
	Reason string
}

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 ConstEquality

type ConstEquality struct {
	// Attribute is the field of RecordVar being constrained.
	Attribute string

	// Value is the string literal the attribute must equal. Meaningful
	// only when FromVar is empty.
	Value string

	// FromVar names the field of ConstVar supplying the value. Empty for
	// a literal comparison.
	FromVar string

	// List reports that Attribute is a list of strings and the constraint
	// is MEMBERSHIP — some element equals the value — rather than scalar
	// equality. Only a ConstFuncs call produces it: the language has no
	// list equality operator.
	List bool
}

ConstEquality is one pushable equality reported by Program.ConstEqualities.

Exactly one of Value and FromVar is meaningful: a literal comparison sets Value, and a comparison against the constant record's field sets FromVar (leaving Value empty for the caller to fill in).

type Date

type Date struct {
	// contains filtered or unexported fields
}

Date is a concrete instant value backed by time.Time. It backs both the metamodel `date` and `datetime` property types; comparison is always instant-granular (a bare date is midnight in its parsed location), matching internal/filter's matchDate. The time.Time is parsed at compile or bind time — never at Eval — so the engine keeps its no-I/O-at-eval invariant (see doc.go, RR-A3EZR).

func NewDate

func NewDate(t time.Time) Date

NewDate constructs a Date value from a time.Time.

func (Date) Time

func (d Date) Time() time.Time

Time returns the underlying time.Time.

func (Date) Type

func (Date) Type() Type

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 NewEnv

func NewEnv() *Env

NewEnv constructs an empty Env.

func (*Env) DeclareFunc

func (e *Env) DeclareFunc(name string, sig FuncSig) error

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.)

func (*Env) DeclareVar

func (e *Env) DeclareVar(name string, t Type) error

DeclareVar registers a variable name and its type. Returns an error if name is already declared (as a var or as a func).

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.

func (*EvalError) Error

func (e *EvalError) Error() string

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

type Func interface {
	Call(ctx context.Context, args []Value) (Value, error)
}

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

type FuncFunc func(ctx context.Context, args []Value) (Value, error)

FuncFunc adapts a Go closure into a Func. Use it when the host function has no state of its own.

func (FuncFunc) Call

func (f FuncFunc) Call(ctx context.Context, args []Value) (Value, error)

Call satisfies Func.

type FuncSig

type FuncSig struct {
	Params   []Type
	Variadic Type
	Return   Type
	// SQLPortable reports that this function has target-neutral semantics
	// which a future SQL lowering may reproduce exactly. False is the safe
	// default: host functions must opt in deliberately.
	SQLPortable bool
}

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 Int

type Int struct {
	// contains filtered or unexported fields
}

Int is a concrete integer value backed by int64. It is a SEPARATE variant from Number (which is float64) so integer-typed properties compare exactly — no lossy float64 round-trip past 2^53 and no lexicographic "10" < "9" surprise. A predicate never mixes Int and Number in a comparison: the type checker (checkRelational) requires same-type operands, and literal coercion (walkRelational) retypes a number-literal RHS to Int when the LHS attribute is IntType.

func NewInt

func NewInt(i int64) Int

NewInt constructs an Int value. Bind it to a field declared IntType. Do NOT use NewNumberFromInt for an IntType field — that returns a Number (float64), which fails the runtime type check against an IntType binding at Eval (RR-4189H).

func (Int) Int64

func (i Int) Int64() int64

Int64 returns the underlying int64.

func (Int) Type

func (Int) Type() Type

type Issue

type Issue struct {
	Name string
	Err  error
}

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

func NewList(elems []Value) List

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).

func (List) Elems

func (l List) Elems() []Value

Elems returns the underlying slice. Callers must not mutate.

func (List) Type

func (List) Type() Type

type ListType

type ListType struct{ Elem Type }

ListType is a homogeneous list type descriptor.

type NamedSource

type NamedSource struct {
	Name   string
	Source string
}

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.

func NewNil

func NewNil() Nil

NewNil constructs a Nil value.

func (Nil) Type

func (Nil) Type() Type

Type returns NilType.

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 NewNumber

func NewNumber(f float64) Number

NewNumber constructs a Number value from a float64.

func NewNumberFromInt

func NewNumberFromInt(i int) Number

NewNumberFromInt constructs a Number value from a Go int, with the integer promoted to float64. Use it for a field declared NumberType. For a field declared IntType use NewInt instead — binding a Number to an IntType field fails the runtime type check at Eval (RR-4189H).

func (Number) Float

func (n Number) Float() float64

Float returns the underlying float64.

func (Number) Type

func (Number) Type() Type

type ParseError

type ParseError struct {
	Line int
	Col  int
	Msg  string
}

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 PrefilterSpec

type PrefilterSpec struct {
	// RecordVar is the record whose attributes are being constrained (the
	// entity under test). Required.
	RecordVar string

	// ConstVar is the record whose fields are constant for one evaluation
	// (the request's current_user). Optional: empty considers string
	// literals only.
	ConstVar string

	// ConstFuncs maps a host-function name to the field of ConstVar its
	// single argument is compared against. Listing a function here is the
	// caller's ASSERTION about its semantics, which the engine cannot
	// verify: `f(RecordVar.attr)` must mean exactly `RecordVar.attr ==
	// ConstVar.<field>` when attr is a string, and "some element of
	// RecordVar.attr equals ConstVar.<field>" when attr is a list of
	// strings. The argument's static type decides which reading applies;
	// a call whose argument is anything but a direct attribute of
	// RecordVar is ignored.
	ConstFuncs map[string]string
}

PrefilterSpec names the variables and host functions Program.ConstEqualities recognizes as request-constant comparisons.

The predicate engine knows nothing about entities or users; the caller tells it which record is the row under test, which record is constant for the request, and which of its own host functions are equality sugar over that constant.

type Profile

type Profile struct {
	Expected           Type
	AllowArithmetic    bool
	AllowConcatenation bool
	RequireSQLPortable bool
}

Profile selects the expression features and result contract available to a compilation context. Profiles change availability, never semantics.

func ValueProfile

func ValueProfile(expected Type) Profile

ValueProfile returns the general pure scalar-expression profile used by materialized computations.

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 CompileValue

func CompileValue(env *Env, source string, profile Profile, opts ...CompileOption) (prog *Program, err error)

CompileValue compiles a pure value expression under profile. Unlike Compile, the top-level result need not be bool.

func (*Program) Attributes

func (p *Program) Attributes(recordVar string) []string

Attributes returns the statically referenced fields of recordVar in sorted order. Dynamic attribute access is rejected by the compiler, so this is a complete dependency set for an entity record.

func (*Program) ConstEqualities

func (p *Program) ConstEqualities(spec PrefilterSpec) []ConstEquality

ConstEqualities returns the attributes of the spec's RecordVar that the program constrains, at the TOP LEVEL of an AND-chain, to be equal to a value that is constant for one evaluation — a string literal, a field of ConstVar, or a ConstFuncs call over the attribute.

What it is for

A store can pre-filter rows by an indexed equality far more cheaply than the Go pass can reject them, but only if the comparison holds for EVERY row the authoritative pass would keep. This reports exactly the comparisons for which that is true, so a caller can lower them to a backend predicate (see internal/queryplan) while still running the full program in Go.

Why the shape is so restricted

A pre-filter is sound only if it can never remove a row the authoritative pass would keep, which forces three restrictions:

  • Top-level AND only. Under an `or`, either side may be false while the program is still true, so pushing one branch would drop rows the program accepts. `not` inverts the sense entirely.
  • Equality only. Ordered and inequality comparisons have backend-vs-Go semantics that differ on typed and missing values; the store compares by string form, the Go pass by declared type.
  • Constant right-hand side. A comparison against another entity field varies per row and is not a filter the store can bind.

Values are returned only for comparisons whose other side is a plain string: a literal, or a string-typed field of ConstVar (whose value the caller supplies at bind time and is therefore the same for every row). For a ConstVar field the returned Value is empty and FromVar names the field, because the program does not know the identity — the caller does.

A ConstFuncs call over a LIST attribute is reported with List set: the constraint is membership, not scalar equality, and a caller lowering it must pick a store operation with that meaning.

The result is sorted by attribute name for deterministic output. A program that constrains the same attribute twice reports it once, with the FIRST binding encountered — a caller must treat the result as a subset of the program's constraints, never as the whole of them.

func (*Program) Eval

func (p *Program) Eval(ctx context.Context, b *Bindings, opts ...EvalOption) (Value, error)

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) Functions

func (p *Program) Functions() []string

Functions returns the host functions the program calls, sorted. Like Program.Attributes it is exact: calls are resolved statically, so a caller can decide from this alone whether the program depends on a binding one of them closes over.

func (*Program) References

func (p *Program) References(varName string) bool

References reports whether the program reads the variable at all — bare (passed whole to a host function) or through an attribute. It is the question a caller asks before deciding whether a binding for it is REQUIRED: a program that never names a variable evaluates identically with or without one.

func (*Program) ResultType

func (p *Program) ResultType() Type

ResultType returns the static type of the program's top-level expression.

func (*Program) SQLPortable

func (p *Program) SQLPortable() bool

SQLPortable reports whether every node and host function in the program has declared target-neutral semantics suitable for a future SQL lowering.

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

func NewRecord(fields map[string]Value) Record

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.

func (Record) Get

func (r Record) Get(name string) (Value, bool)

Get returns the field value and a present flag.

func (Record) Type

func (Record) Type() Type

type RecordType

type RecordType map[string]Type

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.

func NewString

func NewString(s string) String

NewString constructs a String value.

func (String) String

func (s String) String() string

String returns the underlying Go string.

func (String) Type

func (String) Type() Type

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"}
	IntType    Type = primitiveType{"int"}
	// DateType is the bare date type (no parse layout). It is
	// equalsType-compatible with any DateTypeWithLayout(...) — see that
	// constructor. Use DateTypeWithLayout at the metamodel->Env adapter
	// so string date literals coerce against the field's real format.
	DateType   Type = dateType{}
	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.

func DateTypeWithLayout

func DateTypeWithLayout(layout string) Type

DateTypeWithLayout returns a date type descriptor that additionally carries the Go time layout used to parse bare string/number date literals against this field at COMPILE time (see walkRelational literal coercion, RR-A3EZR). It is equalsType-compatible with the bare DateType singleton — a value is still a Date, comparisons are still date-ordered — so only the compile-time coercion path reads the layout; eval never does. The metamodel->Env adapter supplies the layout (e.g. "2006-01-02" for date, time.RFC3339 for datetime) so the predicate package need not import metamodel (arch_test forbids it).

An empty layout falls back to the built-in default layouts at coercion time (see coerceDateLiteral).

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL