4. Program structure
An EigenQL program has two parts: zero or more DEFINE rules, followed by a single top-level query. The grammar is authoritatively specified in D2 §3; this chapter focuses on semantics and the resulting Program AST.
Program ::= Definition* QueryThe entry points in kernel/src/query/parser.rs are parse_program() (the full file), parse_define() (one rule), and parse_query() (the query).
4.1. The Program and Query AST
pub struct Program { pub definitions: Vec<RuleDefinition>, pub query: Query,}
pub struct Query { pub body: MatchPart, pub group_by: Vec<Expression>, pub result_classes: Vec<Name>, pub result: Vec<ReturnItem>, pub order_by: Vec<OrderItem>, pub limit: Option<usize>, pub offset: Option<usize>, pub distinct: bool, pub top: Option<usize>, // D43 §3.3 — ranked truncation}The query’s evaluation order is: body → group_by → top → result → distinct → order_by → offset → limit. Each stage consumes the output of the previous one. TOP ranks and truncates before RETURN shaping, because shaping projects away the subject binding the similarity score is keyed by.
4.2. USING — class imports, and USING NAMESPACE — short-name scope
UsingClause ::= 'USING' StringLit ( ',' StringLit )*UsingNamespace ::= 'USING' 'NAMESPACE' StringLit ( ',' StringLit )*These two do different jobs, and it is the second one that makes short names work.
A USING clause asserts that an IRI names a class. It is checked and otherwise does nothing: every USING IRI must resolve in the layer chain and the resolved resource must have is_a: Class, or the typechecker raises using_unresolved / using_not_class (type_check.rs). It does not bring a short name into scope.
A USING NAMESPACE clause imports an IRI-string prefix. A bare short name in a pattern (MATCH Dog(?d)) resolves against the resources whose IRI starts with the core namespace — always implicitly in scope, it is the platform prelude — or with any prefix imported here, filtered to the right metaclass (core:Class for a pattern class, core:Property for a property, institution:QueryClass for a FIBER query class), and matched on short_name. Resolution lives in kernel/src/query/resolve.rs.
USING NAMESPACE "urn:eigenius:example:"
MATCH Class(?c) { ... } // core:Class — core is always in scopeMATCH Dog(?d) { ... } // urn:eigenius:example:Dog, via the import aboveWithout the USING NAMESPACE, MATCH Dog(?d) fails with pattern class 'Dog' does not resolve to a Class in the core namespace or any USING NAMESPACE; write the full IRI as a quoted string instead — MATCH "urn:eigenius:example:Dog"(?d) { ... }.
Two consequences worth knowing:
- Ambiguity is an error, not a first-wins pick. A short name matching resources in two imported namespaces raises
ambiguous_short_nameand names the candidates. - Resolution cost is proportional to the imported vocabulary, not to chain size: candidates are filtered by prefix before any body is resolved.
4.3. USING INSTITUTION — fiber aliases
UsingInstitution ::= 'USING' 'INSTITUTION' StringLit 'AS' identDeclares a short alias for an institution’s IRI, to be used in subsequent FIBER clauses in the same MatchPart.
USING INSTITUTION "urn:eigenius:institutions:docking" AS dockUSING INSTITUTION "urn:eigenius:institutions:assay" AS assayAliases are scoped to their MatchPart — a DEFINE body and the main query have separate scopes. Aliases must be unique within a scope.
The parsed AST:
pub struct InstitutionAlias { pub iri: Iri, pub alias: String,}Type-check rules:
duplicate_using_institution_aliasif an alias is reusedundeclared_institution_aliasif aFIBERclause uses an alias that wasn’t declared
4.4. DEFINE — derived relations
Definition ::= 'DEFINE' ident '(' Variable (',' Variable)* ')' 'FROM' MatchPartA DEFINE rule introduces a new named relation with a tuple of head variables. The body is a MatchPart (same shape as a query body, minus FIBER) that produces bindings; those bindings, projected onto the head variables, populate the relation.
DEFINE Ancestor(?x, ?z) FROM MATCH ?x { "urn:eigenius:test:reports_to": ?z }Ancestor is now a relation with two arguments. Other rules and the main query can match against it:
Ancestor(?y) { "urn:eigenius:test:reports_to": ?z }Multiple rules may define the same relation. Their bindings union. The ancestor example in chapter 2 §2.4 uses two rules — a base case and a recursive step.
Recursion is allowed through positive dependencies only. If a DEFINE body negates a relation that transitively depends on the rule’s head, stratification fails. See chapter 10.
Fiber restriction: DEFINE bodies cannot contain FIBER clauses. The rule-fixpoint evaluator has no overlay context, so institution dispatch is disallowed by both the parser (parse_match_part(allow_fiber=false)) and the evaluator (defensive check in evaluate_match_part).
4.5. MATCH — structural patterns
Clause ::= Pattern | FiberClauseMatchClause ::= 'MATCH' Pattern (',' Pattern)*MATCH is the primary pattern clause. It’s followed by one or more comma-separated Patterns, each of which binds variables by unifying with resources in the layer.
MATCH Dog(?d) { breed: ?breed, name: ?name}The full treatment of patterns (subjects, class tags, property patterns, negation) is in chapter 5.
Multiple MATCH clauses compose by equi-join on shared variables:
MATCH Dog(?d) { owner: ?o }MATCH Person(?o) { name: ?owner_name }Here the second MATCH refines bindings from the first by requiring ?o to also be a Person with a name.
4.6. FIBER — institution dispatch
FiberClause ::= 'FIBER' inst '.' QueryClass '{' Params '}' 'AS' VariableA FIBER clause dispatches an OnDemand QueryClass to its institution and binds the response. Param values may use comorphism coercion (comorphism_iri(source)) to translate across an institution boundary inline.
FIBER assay:validate_prediction { candidate: dock_to_assay(?d)} AS ?checkWhat happens at evaluation (D14 §9):
- Resolve the QueryClass in the
InstitutionIndex; confirmOnDemandis in itsdispatch_roleset. - Build an input resource of class
QueryClass.query_classwith each param’s value filled in. Comorphism-coerced param values run the four-step extract → transform → reify pipeline (D14 §10.3). - Look up the QueryClass’s
institution_refin theInstitutionRuntimeand callInstitution::query(query_handler, input, ctx). - Stamp the response with a deterministic transient IRI and attach it to the
FiberOverlay. - Extend the binding with the AS-named variable → the response IRI (as a
Value::String).
Subsequent MATCH clauses can pattern-match against the response through the overlay exactly as if it were a layer resource, scoped to this query only.
Type-check rules (D2 §5.8):
fiber_query_class_not_classif the query class doesn’t resolve to aClassfiber_query_class_not_on_demandif the QueryClass is declared but itsdispatch_roleset excludesOnDemandfiber_institution_mismatchif the QueryClass’sinstitution_refdoesn’t match the alias / inline IRI in the FIBER clausefiber_param_short_name_unresolvedif a param’s short name isn’t a declaredrequires/recommendsproperty of the query classfiber_missing_required_paramif arequiresproperty of the query class isn’t suppliedcomorphism_coercion_class_mismatchif a comorphism coercion’sto_classdoesn’t satisfy the FIBER input class for that propertycomorphism_io_not_supported_in_v1if a comorphism’s transformation Component requiresIOcapability
Full FIBER semantics: chapter 8.
4.7. WHERE — condition filter
WhereClause ::= 'WHERE' ExpressionWHERE filters bindings by evaluating its expression against each one. A binding is retained if the expression evaluates to Value::Boolean(true); dropped otherwise (including on type errors and Undecidable institution results).
WHERE ?age > 18 AND ?country = "DE"The expression grammar is a full operator precedence hierarchy (see chapter 7). Aggregates are not permitted in WHERE — the type checker rejects them with aggregate_in_where.
Decidable QueryClass predicates (D14):
WHERE cap:within_tolerance(?delta, 0.1) HOLDSQualified-name function calls dispatch to Institution::query for the matching Decidable QueryClass and return a Verdict. The postfix HOLDS (or FAILS / UNDECIDABLE) projects the Verdict to a Boolean — only Holds survives the ... HOLDS filter. A bare Verdict-typed expression in Boolean position is a static type error (bare_verdict_in_boolean_position); the conversion is always explicit.
4.8. RETURN — result shaping
ReturnClause ::= 'RETURN' '[' ClassList? ']' '{' ReturnItem (',' ReturnItem)* '}'ReturnItem ::= Name ':' ExpressionRETURN turns each surviving binding into a result resource.
RETURN [Prediction, AssayResult] { compound: ?cpd, predicted_affinity: ?pred, concentration: 10 * ?mw}Behaviour:
- The class list (between
[and]) becomes theis_aarray on each result row. - Each
ReturnItemis a name-expression pair. The expression is evaluated against the binding to produce a value. - Name resolution: a
FullIriis used verbatim as the property IRI; aShortNameis mapped to a synthesized per-query property IRI viaQueryFingerprint::row_property_iri. This keeps every query’s result shape self-describing (see chapter 11). - Aggregate expressions (see §4.9) are only permitted when
GROUP BYis present or when the entire query has no non-aggregate return items.
Empty return (RETURN [] {}) produces a guard-style result — one empty row per binding. Useful for counting or existence checks.
4.9. GROUP BY — aggregation
GroupByClause ::= 'GROUP' 'BY' Expression (',' Expression)*Partitions bindings by the values of the group-by expressions. Each partition becomes one output row, with aggregates computed per partition.
GROUP BY ?breed, ?countryRETURN [] { breed: ?breed, country: ?country, count: COUNT(?d), avg_age: AVG(?age)}Type-check rule (aggregate_without_group_by):
- Every non-aggregate expression in
RETURNmust appear verbatim inGROUP BY. - If
RETURNcontains only aggregates (and class tags),GROUP BYmay be empty — the entire binding set is one group.
4.10. ORDER BY, LIMIT, TOP, OFFSET, DISTINCT
OrderByClause ::= 'ORDER' 'BY' OrderItem (',' OrderItem)*OrderItem ::= Expression ('ASC' | 'DESC')?LimitClause ::= 'LIMIT' integerTopClause ::= 'TOP' integerOffsetClause ::= 'OFFSET' integerDISTINCT(just the keyword, no argument) deduplicates result rows afterRETURNshaping.ORDER BYsorts the result resources by the given expressions; default direction isASC. MixedASC/DESCper column supported.OFFSET nskips the firstnrows.LIMIT ntruncates tonrows — un-ranked structural cutoff.TOP ntruncates tonrows by similarity ranking when the query contains~operators (D43). Mutually exclusive withLIMITandORDER BY; requires at least one~inWHERE. See chapter 6 §6.5 for the full surface and the typecheck rules.
Evaluation order: RETURN shape → DISTINCT → ORDER BY → OFFSET → LIMIT. The TOP reordering happens earlier — between GROUP BY and RETURN shaping — because it sorts by the per-binding similarity score before the binding-to-resource projection drops the subject IRI needed for the score lookup.
4.11. Typical clause order
// DEFINE rules (0+)DEFINE ...
// QueryUSING ...USING INSTITUTION ... AS ...
MATCH ... // one or more clauses, interleaved with FIBERFIBER ...MATCH ...
WHERE ... // optional, may contain `~` similarity operators (chapter 13)
GROUP BY ... // optionalRETURN [...] { ... }
ORDER BY ... // mutually exclusive with TOPLIMIT n // mutually exclusive with TOPTOP n // mutually exclusive with LIMIT + ORDER BY; requires ~ in WHEREOFFSET mDISTINCTThe parser accepts the clauses in this order; it will error on unexpected sequencing.
Next: 5. Pattern matching →