is expression, a small set of operators and built-in functions, and user-defined
functions when you want to name and reuse a computation.
Binding a result with is
The is expression evaluates an arithmetic expression and binds the result to a
variable:
Z is W * H as “let Z be W * H”. The variable on the left must be fresh —
unbound at that point in the body. The engine evaluates the right-hand side using
values already bound (here W and H, supplied by rect), then binds the fresh Z.
You know it worked when the computed value shows up bound in the results: with a
rect(3, 4) fact, running this program derives area(3, 4, 12).
Operators and precedence
Five binary operators are available:+, -, *, /, and % (remainder). They
follow ordinary arithmetic precedence — *, /, and % bind tighter than + and -
— so:
Y * Z first, then adds X. Use parentheses to override the default grouping:
Operands must share a type
A binary operator requires both operands to have the same type. You cannot add ani64 to an f64 directly, or a u32 to an i32 — there is no implicit promotion.
When operand types differ, convert one explicitly with cast (below).
The five built-in functions
XLOG provides exactly five built-in functions. There are no others:pow always yields an f64, whatever its operands are, because exponentiation is
inherently a floating-point operation. cast takes a type name as its second argument
and is how you bridge the same-type rule for operators:
Numeric edge cases
Arithmetic on real hardware has boundaries, and XLOG’s are defined rather than undefined:- Division by zero. Integer division by zero yields the largest value of the
operand’s own type — not one fixed constant. The sentinel therefore depends on the
column you divide:
2147483647fori32,9223372036854775807fori64,4294967295foru32, and18446744073709551615foru64. Float division by zero follows IEEE-754:0.0 / 0.0isNaN, and any other numerator over0.0is+Infor-Inf. - Remainder by zero. Integer
X % 0yields0— an ordinary-looking value that you cannot tell apart from a genuine zero remainder. FloatX % 0.0yieldsNaN. - Integer overflow wraps around (two’s-complement), rather than trapping.
0, or an overflowing
product, silently seeds these values into your results. Because the integer
division-by-zero sentinel is type-dependent, do not test for one hard-coded number
downstream. The idiomatic guard is to filter the bad inputs out in the body before
computing:
Float literals must be written with both an integer and a fractional part:
1.0,
not 1.; 0.5, not .5. Scientific notation such as 1e9 is not part of the literal
grammar either. Write the digits out on both sides of the decimal point.User-defined functions
When a calculation or relational lookup recurs, name it. A function declaration isfunc, a name, a parameter list, an optional return type, and a body:
: type annotations, and the return type follows ->. The body
above is a conditional: if cond then A else B chooses between two arithmetic
expressions based on a comparison. Bodies can also be plain arithmetic:
is expressions in ordinary rules and
constraints. At each call site, XLOG inserts the function’s relational body immediately
before the original is binding. Calls within one arithmetic expression expand from
left to right. Every non-parameter result and body-local variable receives a fresh name
for each invocation, so it cannot capture a caller variable or a local from another
call. Nested function calls use the same expansion rules.
The relational body may match zero, one, or many rows. Each match contributes a caller
row, so function notation does not impose scalar uniqueness on a predicate-bodied call.
Ordinary set semantics still deduplicates identical projected caller tuples when
different body witnesses derive the same tuple.
Arguments substituted into predicate-body term positions must already be variables or
numeric literals. Bind a compound arithmetic argument first, then pass the bound variable:
Start + 1 directly where Child becomes a term in parent(Child, Parent) is
rejected. A predicate-bodied call in a conditional result branch is also rejected,
because moving its relational body outside the selected branch would change the rule’s
meaning.
Add the private modifier to keep a function local to its module — it will not be
exported when another file does use:
Expansion limits
Production compilation registers function declarations in source order, then expands only functions reached from calls in ordinary rules or constraints. Unused definitions whose bodies are recursive, call an undefined function, or share a name with a predicate do not block this demand-driven path. Duplicate function declarations are still rejected. Every call-site expansion is bounded by#pragma max_recursion_depth. When expansion
tries to enter another user-defined call while already at that depth, it reports
error[E0504]. Function normalization expands both conditional branches before
execution; it does not evaluate a data-dependent base case while expanding. A reachable
cycle therefore reaches E0504 instead of acting as a runtime loop, whether its source
is wholly unguarded or includes a conditional branch that looks like a base case.
Rust callers that need to audit every declaration can use
FunctionRegistry::from_program. This strict validation walks definitions and their
callees in source order. It rejects a wholly unguarded recursive strongly connected
component (SCC) with error[E0502]; an SCC passes that check when at least one member has
a conditional body. Passing strict validation does not make expansion data-dependent, so
a used guarded cycle can still reach E0504.
Calling a function
Call a function on the right side ofis, where an arithmetic expression is allowed.
The left side must be a fresh variable:
span binds A and B, the function call computes the difference, and is
binds the fresh variable D.
Aggregation
Collapse many rows into one — count, sum, min, max, and logsumexp in a rule head.