MEL LLM Context
Purpose: Minimal context for LLMs generating MEL code. Usage: Include this document in LLM system prompts when generating MEL.
What is MEL?
MEL (Manifesto Expression Language) is a declarative domain language for defining state machines.
MEL is NOT:
- A programming language
- Turing-complete
- A subset of JavaScript
MEL is not a subset of JavaScript. Object-literal spread is admitted exclusively as an authoring-ergonomics sugar that lowers to
merge(). Array spread, rest destructuring, computed keys, optional chaining, method-call syntax, template literals, and truthiness coercion remain unsupported.Compatibility note: If you need code that also compiles on older pre-v1.2 compiler branches, prefer
merge(...)andpatch path merge expras the fallback forms. On current v1.2 compilers, object-literal spread is part of the normal surface.
MEL expresses:
- Facts (computed)
- Conditions (when, available when)
- Decisions (fail, stop)
- Declared effects (effect ... into ...)
- Explicit state transitions (patch)
Core Constraints
Absolute Rules
- No loops —
for,while,dodo not exist - No user-defined functions —
function, arrow functions do not exist - No variables —
let,const,vardo not exist - No reduce/fold/scan — User-defined accumulation forbidden
- No method calls —
str.trim()is invalid, usetrim(str) - No template literals — Use
concat("Hello, ", name) - $ is reserved — Never use
$in user identifiers
Guard Rules
- All
patchmust be insidewhen,once, oronceIntent - All
effectmust be insidewhen,once, oronceIntent - All
failmust be insidewhen,once, oronceIntent - All
stopmust be insidewhen,once, oronceIntent once(marker)owns the marker write; do not author that patch manually- Prefer
onceIntentfor per-intent idempotency when no explicit guard field is needed
Type Rules
whencondition must be boolean — no truthy/falsyeq/neqonly compare primitives (null, boolean, number, string)len()works on strings, arrays, and records/objectskeys()/values()/entries()work as expression builtins for object decomposition.record.*effects are for writing results to state in action flows.sum()/min()/max()with single arg = array aggregationmin(a,b)/max(a,b)with multiple args = value comparisonmatch()is function-form only:match(key, [k1, v1], ..., defaultValue)argmax()/argmin()only accept inline[label, eligible, score]candidates plus a literal"first"or"last"tie-breakabsDiff,clamp,idiv, andstreakare lowering-only sugar over existing arithmetic and conditional builtins- Complex object types in state must be named (
type X = { ... }) - Object-literal spread
{ ...obj, key: value }is the sole parser-level shorthand in current MEL - Spread operands must be object-shaped or
T | null; arrays, records, computed keys, rest patterns, and object-only multi-branch unions are not part of the current contract - Optional fields produced by spread or spread-equivalent
merge()reads are observed asT | nulland usually requirecoalesce(...)before a non-null sink
Domain Structure
domain DomainName {
state {
field: Type = defaultValue
}
computed name = expression
action name(param: Type) available when condition {
when condition {
patch field = expression
effect type({ args, into: target })
fail "CODE" with "message"
stop "reason"
}
}
}State Declaration
type User = { name: string, age: number }
type Task = { id: string, done: boolean }
state {
// Primitives
count: number = 0
name: string = ""
active: boolean = false
// Nullable
selectedId: string | null = null
// Union (enum)
status: "idle" | "loading" | "done" = "idle"
// Collections
items: Array<string> = []
tasks: Record<string, Task> = {}
// Objects
user: User = { name: "", age: 0 }
}Computed
Pure expressions. No effects. Recalculated on access.
// Arithmetic
computed doubled = mul(count, 2)
computed total = add(a, b)
// Boolean
computed isEmpty = eq(len(items), 0)
computed canSubmit = and(isNotNull(email), gt(len(items), 0))
// Null handling
computed displayName = coalesce(user.name, "Anonymous")
// Ternary
computed label = gt(count, 0) ? "Positive" : "Zero or negative"
// Aggregation
computed sum = sum(prices) // Array<number> → number
computed min = min(values) // Array<T> → T | null
computed max = max(values) // Array<T> → T | null
computed length = len(items) // Array<T> → number
// Bounded sugar
computed error = absDiff(observed, predicted)
computed bounded = clamp(score, 0, 100)
computed buckets = idiv(total, bucketSize)
computed missStreak = streak(previousMissStreak, eq(kind, "miss"))
// Finite branch / fixed candidate selection
computed label = match(status, ["open", "Open"], ["closed", "Closed"], "Unknown")
computed bestKind = argmax(
["coarse", coarseOk, coarseDelta],
["repair", repairOk, repairDelta],
"first"
)
// Object functions
computed withDefaults = { theme: "light", ...config } // Spread-first composition
computed effectiveConfig = { theme: "light", locale: "en", ...userPrefs }
computed mergedConfig = merge(defaults, config) // Direct merge() still available
computed taskIds = keys(tasks) // Object keys
computed taskList = values(tasks) // Object values
computed taskPairs = entries(tasks) // Key-value pairs
merge()vspatch mergevs spread-set:merge(a, b)is a pure expression returning a new object.patch path merge expris a patch operation.patch path = { ...obj, key: value }is still a set patch whose value lowers throughmerge(...).
Forbidden in computed:
// ❌ effect in computed
computed filtered = effect array.filter(...)
// ❌ runtime context in computed
computed now = $runtime.time.timestamp
// ❌ Nested aggregation
computed total = sum(filter(prices))
// ❌ Arrow-arm match syntax is not part of the current contract
computed label = match(status, "open" => "Open", _ => "Unknown")
// ❌ Runtime-array argmax/argmin forms are not supported
computed best = argmax(candidates, "first")
// ❌ Adjacent JS forms remain unsupported
computed copy = [...items]
computed keyed = { [name]: value }
computed maybeName = profile?.nameAction
State transitions with guards.
action increment() {
when true {
patch count = add(count, 1)
}
}
action addAmount(amount: number) {
when gt(amount, 0) {
patch count = add(count, amount)
}
}
// With precondition
action decrement() available when gt(count, 0) {
when true {
patch count = sub(count, 1)
}
}Input access: $input.field is an explicit alias for action parameters.
Note: $input is not allowed in available when.
Guards
when
Conditional execution. Must have boolean condition.
// ✅ Correct
when eq(status, "idle") { ... }
when gt(len(items), 0) { ... }
when isNotNull(selectedId) { ... }
// ❌ Wrong - not boolean
when items { ... } // Array not boolean
when count { ... } // Number not boolean
when user.name { ... } // String not booleanonce
Per-intent idempotency. The compiler inserts the marker patch.
action submit() {
once(submitIntent) {
patch status = "loading"
effect api.submit({ data: form, into: result })
}
}
// With condition
once(step2) when isNotNull(step1Result) {
effect process({ data: step1Result, into: step2Result })
}Patch Operations
// Set value
patch count = add(count, 1)
patch user.name = "Alice"
patch items[id] = newItem
// Remove key from Record
patch tasks[id] unset
// Shallow merge
patch settings merge { theme: "dark" }
// Set patch with object spread sugar
patch draft = {
...draft,
customerId: customerId,
appliedCouponId: null,
submissionState: "submitted"
}Effects
Declare requirements. Host executes. Results go to into:.
// API
effect api.fetch({ url: "/data", method: "GET", into: data })
effect api.post({ url: "/items", body: payload, into: result })
// Array operations - use $item for current element
effect array.filter({
source: items,
where: eq($item.active, true),
into: activeItems
})
effect array.map({
source: items,
select: { id: $item.id, name: upper($item.name) },
into: transformed
})
effect array.sort({
source: items,
by: $item.createdAt,
order: "desc",
into: sorted
})
effect array.flatMap({
source: teams,
select: $item.members,
into: allMembers
})
// Record operations
effect record.keys({ source: tasks, into: taskIds })
effect record.values({ source: tasks, into: taskList })Effects must be sequential, not nested:
// ❌ Wrong - nested effect
effect array.map({
source: items,
select: effect array.filter(...) // NESTED - forbidden
})
// ✅ Correct - sequential
once(step1) {
effect array.flatMap({ source: items, select: $item.children, into: allChildren })
}
once(step2) when isNotNull(allChildren) {
effect array.filter({ source: allChildren, where: $item.active, into: activeChildren })
}Error Handling
fail — Error termination
when eq(trim(email), "") {
fail "MISSING_EMAIL"
}
when not(isValidEmail(email)) {
fail "INVALID_EMAIL" with "Email format is invalid"
}
when isNotNull(at(users, email)) {
fail "DUPLICATE" with concat("Already exists: ", email)
}stop — Early exit (success, no-op)
// Already in desired state
when eq(task.completed, true) {
stop "already_completed"
}
// Nothing to do
when eq(len(items), 0) {
stop "no_items_to_process"
}stop is NOT waiting:
// ❌ Wrong semantics
stop "Waiting for approval"
stop "Pending review"
// ✅ Correct semantics
stop "already_processed"
stop "skipped_no_action_needed"Builtin Functions
Arithmetic
| Function | Description |
|---|---|
add(a, b) | a + b |
sub(a, b) | a - b |
mul(a, b) | a * b |
div(a, b) | a / b (null if b=0) |
mod(a, b) | a % b |
neg(a) | -a |
Comparison
| Function | Description |
|---|---|
eq(a, b) | a == b (primitives only) |
neq(a, b) | a != b (primitives only) |
lt(a, b) | a < b |
lte(a, b) | a <= b |
gt(a, b) | a > b |
gte(a, b) | a >= b |
Logic
| Function | Description |
|---|---|
and(a, b) | a && b |
or(a, b) | a || b |
not(a) | !a |
Null
| Function | Description |
|---|---|
isNull(x) | x === null |
isNotNull(x) | x !== null |
coalesce(a, b) | a ?? b |
Array
| Function | Description |
|---|---|
len(arr) | Array length |
first(arr) | First element or null |
last(arr) | Last element or null |
at(arr, i) | Element at index or null |
sum(arr) | Sum of numeric array |
min(arr) | Minimum or null |
max(arr) | Maximum or null |
Record
| Function | Description |
|---|---|
at(rec, k) | Value for key or null |
Record Effects
| Effect | Description |
|---|---|
effect record.keys({ source, into }) | Extract record keys |
effect record.values({ source, into }) | Extract record values |
effect record.entries({ source, into }) | Extract record entries |
String
| Function | Description |
|---|---|
strlen(s) | String length |
trim(s) | Remove whitespace |
lower(s) | Lowercase |
upper(s) | Uppercase |
concat(...) | Join strings |
Object
| Function | Description |
|---|---|
merge(a, b, ...) | Shallow merge objects (later wins) |
keys(obj) | Object keys as array |
values(obj) | Object values as array |
entries(obj) | Object entries as [key, value] array |
Value Comparison
| Function | Description |
|---|---|
min(a, b, ...) | Minimum of values |
max(a, b, ...) | Maximum of values |
Common Patterns
Fetch and Process
action loadData() {
once(loading) {
patch status = "loading"
effect api.fetch({ url: "/items", into: rawItems })
}
once(filtering) when isNotNull(rawItems) {
effect array.filter({
source: rawItems,
where: eq($item.active, true),
into: items
})
}
when and(isNotNull(rawItems), isNotNull(items)) {
patch status = "done"
}
}Object Composition to Enrich/Override Fields
// In computed — spread-first override
computed enriched = { ...item, status: "active", source: "api" }
// In map — override fields without enumerating all
effect array.map({
source: items,
select: { ...$item, processed: true },
into: processedItems
})Runtime Values
Runtime values come from ADR-027 Context and are only allowed inside action bodies.
// Allowed in actions (resolved from ADR-027 Context.runtime)
patch id = $runtime.random.uuid
patch createdAt = $runtime.time.timestamp
patch random = $runtime.random.seedForbidden:
computed now = $runtime.time.timestamp // Runtime values not allowed in computed
state { id: string = $runtime.random.uuid } // Not allowed in state defaultsCRUD with Validation
action create(title: string) {
when eq(trim(title), "") {
fail "MISSING_TITLE"
}
once(creating) when neq(trim(title), "") {
patch items[$runtime.intent.id] = {
id: $runtime.intent.id,
title: trim(title),
done: false
}
}
}
action delete(id: string) {
when eq(at(items, id), null) {
fail "NOT_FOUND" with concat("Item not found: ", id)
}
when isNotNull(at(items, id)) {
patch items[id] unset
}
}Toggle with Idempotency
action toggle(id: string) {
when eq(at(items, id), null) {
fail "NOT_FOUND"
}
once(toggling) when isNotNull(at(items, id)) {
patch items[id].done = not(at(items, id).done)
}
}Generation Checklist
When generating MEL:
- [ ] All patches inside
when,once, oronceIntent? - [ ] All effects inside
when,once, oronceIntent? - [ ]
once()uses a unique marker field and lets the compiler write it? - [ ]
whenconditions are boolean expressions? - [ ] No method calls (use function calls)?
- [ ] No
$in user identifiers? - [ ] No loops, variables, or function definitions?
- [ ] Effects are sequential, not nested?
- [ ] Using
eq(len(arr), 0)noteq(arr, [])? - [ ] Using
isNotNull()not truthy check? - [ ] Record keys/values via
record.*effects (notkeys()/values())? - [ ]
$runtime.*/$context.*only in action bodies (never in computed/state init)?
End of MEL LLM Context