Tags: compiler, interlang, policy, composition
Compile a complete Interlang policy, then combine it with independent policy constraints without merging their private helpers. An account policy and an operation policy can each select records through arbitrary local relationships; their composed operand permits only the selections both contribute.
Lace-120 defines Interlang. This specification defines its compilation boundary and structural composition of complete canonical policies. 030 and 040 remain authoritative for Datalog execution and exchange.
Compilation accepts one self-contained 120 contribution containing
friendly helpers, effect blocks, and inline datalog: rules.
Friendly and advanced rules share one local helper namespace and union
when their inferred signatures agree. Every referenced lowercase helper
MUST be locally defined.
The Rust surface is:
pub struct CompilationLimits {
pub max_source_bytes: usize,
}
pub fn compile(source: &str) -> Result<InterlaceModule, CompilationError>;
pub fn compile_with_limits(
source: &str,
limits: CompilationLimits,
) -> Result<InterlaceModule, CompilationError>;
CompilationLimits::default() sets
max_source_bytes to 1 MiB (1,048,576 bytes).
compile(source) is equivalent to
compile_with_limits with those defaults. The bound counts
the complete supplied UTF-8 source before removing comments or
whitespace. Exceeding it MUST fail preparation without partial
output.
A failure is a CompilationError whose kind,
Policy or LimitExceeded, is what callers
branch on; its message is for the author. An error in the source text
gives one message line per diagnostic,
LINE:COLUMN: description, with lines and columns (Unicode
scalars) counted from 1 at the offending clause or token. An error found
only in the complete program, such as the source-byte limit, a
helper-only contribution (policy has no executable facets),
or a fixed preparation bound, has no position. For example, a selection
calling an undefined helper:
converge:
include Note:
Note['#Notes'] == 'note'
reviewed(Note)
fails with:
4:5: unknown relation `reviewed/1`
Bindings raise this message unchanged under the same category, here
Python LacePolicyError and a JavaScript
InterlaceError with category policy.
Compilation returns one immutable, independently owned
InterlaceModule with canonical ordinary 030/040 Datalog and
its validated parsed representation. It MUST contain at least one rule
for a standard 040 facet. Empty and helper-only bodies may satisfy the
120 grammar, but compilation as an exchange policy MUST reject them. A
veto-only contribution is a valid policy.
Source has no module wrapper, imports, relation parameters, or
qualified helper calls. Implementations MUST reject module,
input, export, and use
declarations, raw @ providers, and qualified calls in every
relation position, including inline Datalog, negation, cardinality, and
TopK. There is no public partial-program, authoring-AST, or
external-relation compilation entry point. Interlang compilation has no
finite helper-output target; generic finite Datalog preparation and
evaluation remain the separate 030 surface.
Compilation MUST NOT perform IO, acquire records, invoke application resolvers, read hidden time, or evaluate policy. Applications supply complete source bytes. A source error is a preparation failure, not a request for more input. A prepared operand does not watch files or change when application code changes.
The compiler MUST validate every authored rule and its dependencies before any execution pruning, including unused rules and paths through zero-bound TopK. Checks include kinds, safety, reserved predicates, stratification, and the complete 040 request-only advertisement dependency restriction. An undefined helper is invalid, not an empty relation.
Local friendly-helper discovery specialization and checked-support by-hash proofs follow 120. A helper with any advanced Datalog defining rule remains opaque to those transformations. Compilation MUST NOT turn checked supporting records into advertisement claims or weaken grounding to accommodate an opaque helper. Multiple outputs retain 120’s separate unary rules and independent per-output guards.
Finalized rules use the existing 030 validator, canonicalizer, and evaluator. Canonical APIs and ILTP operand validation MUST reject authoring syntax without requiring the authoring compiler. No source form extends canonical predicate syntax or changes the fixed exchange profile.
A composition has one nonempty authoring collection of complete policies, zero or more guest policies, and zero or more field exclusions. Facets from both policy classes constrain the result. Guest rules additionally resolve record facts through the authoring inputs’ exposure view, so rules the authoring side did not write inspect only records it exposes, exactly as 020 evaluates peer-origin rules in the local side’s peer exposure view. Exclusions remove matching records from every composed facet and from that view.
The compiler-independent Rust surface is:
pub struct FieldExclusion {
pub name: Option<String>,
pub index: Option<String>,
pub value: Option<String>,
}
pub fn prepare_facet_constraints(
authoring: &[InterlaceModule],
guests: &[InterlaceModule],
exclusions: &[FieldExclusion],
) -> Result<InterlaceModule, PreparationError>;
The function borrows complete immutable policies and exclusions and returns one independently owned canonical operand. Each input in either class MUST have at least one standard 040 facet rule, even when other inputs have facets. Helper-only canonical programs are invalid composition inputs; veto-only policies are valid. Canonical Datalog validation alone need not establish this stronger policy precondition.
For each of SelectStoredRecord/1,
SelectAdvertisedCandidate/1, and
ExposeRecord/1, define its contributors as the inputs in
either class containing at least one rule for that facet. Composition
intersects that facet’s relations across its contributors. If no input
contributes the facet, it remains absent/empty.
TxDisabled/0 and RxDisabled/0 instead union
across all inputs.
An absent facet contributes no local constraint on that facet. A present facet whose rules derive no rows constrains the result to empty. These cases MUST NOT be conflated. Facet presence is determined from rules, not the current fact view. Within one contribution, alternative rules or selections still union.
Exposure is composed independently of stored selection. The composer MUST NOT reconstruct exposure from selected records, add an absent facet, or widen a contributor’s exposure output. A veto-only input constrains direction without constraining selection or exposure.
Composition MUST structurally isolate every input’s private helper symbols, including references under negation, cardinality, recursion, and TopK. Helpers with equal spelling in different inputs do not merge or become callable across input boundaries. Base predicates keep their ordinary meaning. Predicate renaming MUST operate on parsed structure, not substitute source strings.
The resulting operand contains ordinary rules only. Composition MUST validate the complete inputs and composed program before execution pruning. It does not execute rules, fetch data, bind intermediate relations, or activate callbacks. The containing 040 operand determines every composed rule’s origin; guest confinement narrows the fact view inside that origin. Source distribution grants no separate authority.
The guest view is the intersection of the authoring inputs’
ExposeRecord relations, with presence determined from
rules. If no authoring input defines ExposeRecord, the
guest view is empty and the operand remains valid, matching 020’s
default for a peer with no active local exposure rules. A guest’s own
ExposeRecord constrains the composed exposure facet but
never contributes to its view.
In guest rules, every Have, Field,
RecordLink, and ContainsRecord occurrence,
whether positive, negated, counted, or a TopK candidate,
every AdmitOrder operand, and every TopK
ranking over AdmitOrder resolves through that view under
020’s peer-exposure-view rules: the whole-record gate applies to the
record whose projection supplies each row; a visible row discloses its
link targets and contained hashes as values without exposing those
records; a candidate without visible Have has no
admission-order value and is excluded before partition construction.
Advertisement facts, Here/1,
PeerConfidential/1, builtins, and request-only validation
are unchanged, and the plan schema still includes guest request
dependencies.
Holding the authoring policies fixed, guest rules MUST influence a
side’s observable exchange behavior, including evaluation work,
resource-limit failures, and timing, only through the authoring exposure
view, the standard runtime facts Here/1 and
PeerConfidential/1 when present, peer advertisement facts,
and intrinsic protocol facts. Records outside that view MUST NOT
contribute candidate work, partitions, ranking, or limit failures to
guest rules. Guests only narrow: they add no selected record, exposure,
or visible fact.
Confinement is local rewriting inside the one composed operand. It adds no view kind, plan input, profile, transcript, wire, or plan-identity change. The composed operand is side-agnostic like any other: in the peer slot its complete rule set, guests included, is evaluated in that side’s peer exposure view.
An exclusion is a Field occurrence pattern whose name,
index and value are each a constant or _
(None). The composer appends every exclusion negated to
each composed facet rule:
not Field(Record,name,index,value) to
SelectStoredRecord and ExposeRecord, and
not AdvertisedField(Record,name,index,value) to
SelectAdvertisedCandidate. The guest view is the authoring
exposure with the same not Field atoms. A record carrying a
matching occurrence is therefore neither selected for sending,
requested, nor exposed by the operand, and guest rules cannot inspect
it. Identical exclusions count once, their order does not affect
canonical output, and an empty collection leaves the composition
unchanged.
The advertised atom puts the excluded field in the plan’s
advertisement schema: an exact name requires that field, _
requires all fields. Under 050 transferred bytes must match their
advertised projection, so a peer cannot withhold the field to get a
matching record requested.
Exclusions subtract only from facets some input defines: they add no
facet and leave vetoes unchanged. Selection and exposure remain separate
facets; a selected but unexposed record is not sent under 040. Only
Field patterns are exclusions, because other record facts
have no advertisement counterpart with which a receiver could decline a
record before requesting it. Each negated atom tests a candidate the
intersected facet atoms already bound, so evaluation work does not grow
with excluded or unrelated records.
Compile these three independent contributions:
converge:
include Record:
Record['#Notes'] == 'note'
converge:
include Record:
Record.State == 'published'
datalog:
TxDisabled() :- true.
Passing their three prepared policies together as authoring inputs to
prepare_facet_constraints selects and exposes records
satisfying both field conditions, selects request candidates satisfying
both advertisement conditions, and vetoes transmission from the side
assigned this operand. The veto adds no selection or exposure
constraint. The peer’s operand and ordinary exchange gates still
apply.
Now compose the first policy as the only authoring input with this guest:
datalog:
SelectStoredRecord(Record) :- Have(Record), Have(Flag), Field(Flag,'Flag','0','on').
The guest’s Have(Flag) and Field atoms
range over the authoring exposure view, which contains only
#Notes records. A stored Flag: on record
without that field is outside the view, so it cannot unlock the
selection, and it contributes no evaluation work. A #Notes
record carrying Flag: on can.
The result can be composed again with another complete policy, in either class. For successful evaluations over the same supplied fact view, constraint composition within a class is commutative, associative, and idempotent at the level of facet results, and nested composition preserves a guest’s confinement and an inner composition’s exclusions. These laws do not promise equal work, limit failures, or canonical bytes across differently grouped calls. Nested composition may retain extra wrappers and consume more preparation or evaluation allowance. One n-ary call avoids those additional intermediate compositions.
There is no general union, complement, helper-export, or relation-binding operation over policies, and no subtraction other than field exclusions. Alternatives and intermediate joins belong inside a self-contained contribution. Unary policy outputs cannot recover intermediate multi-column relations after projection.
Canonical Datalog bytes are the executable identity. Compilation MUST preserve meaningful constants, rule contents, argument order, and partition order; remove comments and duplicate ordinary rules; and normalize output-list order under 120. Generated helpers MUST have unique ordinary lowercase names.
For one compiler implementation, the same normalized input produces identical canonical output. Ordinary helper and variable spelling may remain content; no general logical-equivalence or arbitrary alpha-equivalence search is required. Different compiler implementations need not allocate the same private names. Peers execute supplied canonical operands rather than reconstructing them from source. Applications can retain source files or the canonical artifact; the compiler retains no source-selection or replay graph.
040’s fixed rule, arity, and static TopK bounds apply to each complete compiled contribution and to the complete composed operand. Interlang per-output rules, advertised specializations, and local composition wrappers, including the coverage and record-fact wrappers emitted for guest inputs, count toward those bounds. Only wrappers a guest references are emitted. Exclusions are inline atoms; they add the composed facet rules a singleton input would otherwise not need. Duplicate canonical contributions and duplicate ordinary rules do not consume additional slots. Limits MUST fail rather than truncate source, outputs, or dependencies. Source-byte limits are authoring inputs, not ILTP options.
Generated 040 exchange-plan lowering retains its separate accounting. Runtime evaluation retains the ordinary derived-fact, TopK, iteration, and work bounds; TopK does not bound candidate derivation. Generic configurable Datalog limits are not exchange compilation options.
Bindings project the same compiler and composer through their existing prepared-policy values:
JavaScript:
compile(source, options = {}) -> Promise<PreparedPolicy>
composePolicies(policies, { guests?, exclusions?, wasm?, wasmUrl? } = {}) -> Promise<PreparedPolicy>
Python:
compile(source, *, max_source_bytes=1 << 20) -> PreparedPolicy
compose_policies(policies, *, guests=(), exclusions=()) -> PreparedPolicy
JavaScript’s compilation option is maxSourceBytes, with
the same 1 MiB default; WASM-loading options do not change compilation
or composition semantics. JavaScript and Python compilation accept one
source string. Composition accepts the authoring collection of prepared
policies plus optional guest and exclusion collections, not source
fragments. JavaScript exclusions are
{ name?, index?, value? } objects and Python’s are
FieldExclusion values; an omitted component is
_. C supplies the guest and exclusion arrays to the same
single composition call.
Every binding MUST submit all complete input collections to one n-ary composition call rather than fold a binary composer. For the same inputs it MUST preserve Rust’s canonical bytes and preparation-error categories, including duplicates, permutations, empty authoring input, a module in both classes, exclusions, and final rule-limit failures. Foreign owner errors are checked at the binding boundary before calling the composer. Inputs remain caller-owned and success creates an independently owned result.
Preparation failures distinguish policy errors from configured source-byte or fixed preparation-limit failures. Compilation and composition finish before participant claiming or replacement staging. Failure leaves a running operation untouched; applying a prepared operand follows 050/100 hard replacement.
Implementations MUST cover:
AdmitOrder comparison, and
TopK ranging only over the authoring exposure view;ExposeRecord
giving an empty guest view and a valid operand;