structured messaging

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:

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

Read the full AMP Guide → Download and try it