Skip to content

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(...) and patch path merge expr as 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 ​

  1. No loops — for, while, do do not exist
  2. No user-defined functions — function, arrow functions do not exist
  3. No variables — let, const, var do not exist
  4. No reduce/fold/scan — User-defined accumulation forbidden
  5. No method calls — str.trim() is invalid, use trim(str)
  6. No template literals — Use concat("Hello, ", name)
  7. $ is reserved — Never use $ in user identifiers

Guard Rules ​

  1. All patch must be inside when, once, or onceIntent
  2. All effect must be inside when, once, or onceIntent
  3. All fail must be inside when, once, or onceIntent
  4. All stop must be inside when, once, or onceIntent
  5. once(marker) owns the marker write; do not author that patch manually
  6. Prefer onceIntent for per-intent idempotency when no explicit guard field is needed

Type Rules ​

  1. when condition must be boolean — no truthy/falsy
  2. eq/neq only compare primitives (null, boolean, number, string)
  3. len() works on strings, arrays, and records/objects
  4. keys()/values()/entries() work as expression builtins for object decomposition. record.* effects are for writing results to state in action flows.
  5. sum()/min()/max() with single arg = array aggregation
  6. min(a,b)/max(a,b) with multiple args = value comparison
  7. match() is function-form only: match(key, [k1, v1], ..., defaultValue)
  8. argmax() / argmin() only accept inline [label, eligible, score] candidates plus a literal "first" or "last" tie-break
  9. absDiff, clamp, idiv, and streak are lowering-only sugar over existing arithmetic and conditional builtins
  10. Complex object types in state must be named (type X = { ... })
  11. Object-literal spread { ...obj, key: value } is the sole parser-level shorthand in current MEL
  12. 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
  13. Optional fields produced by spread or spread-equivalent merge() reads are observed as T | null and usually require coalesce(...) before a non-null sink

Domain Structure ​

mel
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 ​

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

mel
// 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() vs patch merge vs spread-set: merge(a, b) is a pure expression returning a new object. patch path merge expr is a patch operation. patch path = { ...obj, key: value } is still a set patch whose value lowers through merge(...).

Forbidden in computed:

mel
// ❌ 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?.name

Action ​

State transitions with guards.

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

mel
// ✅ 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 boolean

once ​

Per-intent idempotency. The compiler inserts the marker patch.

mel
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 ​

mel
// 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:.

mel
// 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:

mel
// ❌ 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 ​

mel
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) ​

mel
// 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:

mel
// ❌ Wrong semantics
stop "Waiting for approval"
stop "Pending review"

// ✅ Correct semantics
stop "already_processed"
stop "skipped_no_action_needed"

Builtin Functions ​

Arithmetic ​

FunctionDescription
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 ​

FunctionDescription
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 ​

FunctionDescription
and(a, b)a && b
or(a, b)a || b
not(a)!a

Null ​

FunctionDescription
isNull(x)x === null
isNotNull(x)x !== null
coalesce(a, b)a ?? b

Array ​

FunctionDescription
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 ​

FunctionDescription
at(rec, k)Value for key or null

Record Effects ​

EffectDescription
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 ​

FunctionDescription
strlen(s)String length
trim(s)Remove whitespace
lower(s)Lowercase
upper(s)Uppercase
concat(...)Join strings

Object ​

FunctionDescription
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 ​

FunctionDescription
min(a, b, ...)Minimum of values
max(a, b, ...)Maximum of values

Common Patterns ​

Fetch and Process ​

mel
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 ​

mel
// 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.

mel
// Allowed in actions (resolved from ADR-027 Context.runtime)
patch id = $runtime.random.uuid
patch createdAt = $runtime.time.timestamp
patch random = $runtime.random.seed

Forbidden:

mel
computed now = $runtime.time.timestamp   // Runtime values not allowed in computed
state { id: string = $runtime.random.uuid } // Not allowed in state defaults

CRUD with Validation ​

mel
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 ​

mel
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:

  1. [ ] All patches inside when, once, or onceIntent?
  2. [ ] All effects inside when, once, or onceIntent?
  3. [ ] once() uses a unique marker field and lets the compiler write it?
  4. [ ] when conditions are boolean expressions?
  5. [ ] No method calls (use function calls)?
  6. [ ] No $ in user identifiers?
  7. [ ] No loops, variables, or function definitions?
  8. [ ] Effects are sequential, not nested?
  9. [ ] Using eq(len(arr), 0) not eq(arr, [])?
  10. [ ] Using isNotNull() not truthy check?
  11. [ ] Record keys/values via record.* effects (not keys() / values())?
  12. [ ] $runtime.* / $context.* only in action bodies (never in computed/state init)?

End of MEL LLM Context