What is AMP?
AMP — Anvil Messaging Protocol — isn't an OML at all, and it isn't trying to be one. Where AML's whole job is describing structured object data, AMP's job is the opposite: guarantee that a payload can't grow structure, so whatever reads it never has to handle a shape it wasn't built for.
Same grammar as AML, same parser, same shebang-declared-dialect mechanism — just with most of the permissions turned off, enforced at parse time instead of by convention.
A different problem than modeling
An OML's hard problem is impedance mismatch — matching the shapes a host language already has. Messaging has a different hard problem entirely: a message crosses a boundary between two things that don't share a codebase, don't deploy together, and don't agree in advance on exactly what "the schema" is at any given moment. The sender's idea of the shape and the receiver's idea of the shape can drift, quietly, the same way any two independently deployed services drift — and unlike a config file, nobody's reviewing a pull request before the next message goes out.
Most messaging formats solve this by pushing the problem elsewhere. MQTT's payload is
opaque bytes — whatever structure exists is a convention between sender and receiver, the
protocol itself enforces nothing. AMQP frames a message but leaves its body's structure to
whatever content-type says, unchecked by the broker. Protocol Buffers goes the
other way — real structure, real enforcement — but only by compiling a .proto
schema ahead of time; the wire format itself is just bytes a generated decoder happens to
know how to walk. In every case, the actual guarantee that a payload matches its shape
exists outside the format, if it exists at all.
How AMP approaches it
AMP's bet is different: put the guarantee in the grammar, not in a compiled schema
or an unenforced convention. A document that opens with #!amp is restricted
before a single statement is read — the parser rejects the constructs below outright, the
instant it sees them, with a dedicated error code each:
| Construct | Error code |
|---|---|
Objects ({ ... }) anywhere in value position |
AMP_OBJECT_FORBIDDEN |
$ VarRef, in any position |
AMP_VARREF_FORBIDDEN |
Attributes (@[...]), module-level or per-statement |
AMP_ATTRIBUTE_FORBIDDEN |
Inheritance (Child : Base := ...) |
AMP_INHERITANCE_FORBIDDEN |
| A non-scalar element inside an array or tuple — no nesting, ever | AMP_ARRAY_ELEMENT_NOT_SCALAR |
What's left is deliberately small: scalars, flat arrays and tuples of them, tagged and
untagged blobs, and plain identifier := value; assignment. Enough to carry a
real message; not enough to ever quietly grow a nested object three services later.
A look at AMP
#!amp
status := ok;
code := 200;
tags := [alpha, beta, gamma];
point := (10, 20);
raw := `payload bytes`;
And the same restrictions, rejected the instant the parser sees them — not silently dropped, not a warning, a hard parse error:
#!amp
bad := { x := 1; }; // AMP_OBJECT_FORBIDDEN
bad := $foo; // AMP_VARREF_FORBIDDEN
bad := [[1, 2]]; // AMP_ARRAY_ELEMENT_NOT_SCALAR
Why the distinction matters
The dialect is declared once, on the shebang line, and enforced immediately from that point on — not inferred, not mixed, and not a runtime flag layered on top of one generic grammar. A feature added to AML — say, a new collection type — can't accidentally become reachable from AMP just because it shares a code path. This separation is enforced deliberately, not just by convention, the same way every construct in the table above is.