Will ChenWill Chen
← Writingsystem design

Choosing a notation for a model to write in

A rich notation for a model to write in, deleted down to JSON with function calls as a primitive type. Syntax is for humans, and the writer here is not one.

Will ChenWill Chen6 min

A notation for a model to write in does not need most of what a notation for a person needs. Working that out deleted three months of syntax design and left function calls and JSON.

Motivation

I was building a system where a person describes an agent in ordinary language and something comes out the other end that a machine can run. That intermediate thing needs a written form, and the form has to be:

  • generated reliably by a language model
  • read back by a program
  • edited by a person when the model gets it wrong
  • checked for errors before anything executes

The obvious candidate is JSON, which models emit reliably and every mainstream language parses. My note on the origin puts the problem plainly: XJSN came out of needing to represent agent behavior and have AI build the agent for you, and JSON was not going to cut it.

JSON is a data format: objects, arrays, strings, numbers and a couple of literals. It has no way to say that a thing is a call, a condition, or a reference to another thing. You can encode all three as nested objects with a field naming what each one is, and then you have invented a language and written it in the least readable available syntax.

First attempt: markup outside, JSON inside

The first answer keeps JSON on the inside and puts a markup layer on the outside, so the model writes the friendlier form and the program reads the stricter one:

I need JSON internal, but XML external repr (superset) such that LLM can easily generate

Markup earns that position on three properties:

  • markup handles nesting and mixed content well
  • models have read enormous quantities of it
  • it has a mature validation story, which matters when the thing generating your program is probabilistic and you want to reject bad output before it runs

The direction got as far as schema validation and a cost estimate for running the validator in the browser, and no property of the markup ever failed a test. What settled it was a question none of the engineering addressed: who the notation is for.

Deleting the syntax

Ergonomics are a cost paid for a reader, so the question is which reader, and my answer that September was that there isn't one:

i deliberately chose function calls because i realized with AI, we don't need the syntactic richness built for human usability. function calls + json are alrady ergonomic we dont need dhall. like function calls are already same semantics as s exprs. the key point is decoupling syntax from semantics

Three claims are stacked in there, and they come apart cleanly. Taken together they say that a notation for a model has no ergonomics budget to spend.

  1. Syntactic richness exists for people. Every convenience in a programming language, the infix operators, the significant whitespace, the shorthand for common shapes, exists because a human being has to type it and read it back later. A model has neither constraint. It does not get tired of typing and it does not lose its place in a nested structure. Designing ergonomics for a reader who has none of the problems ergonomics solve is designing for a user who is not there.
  2. Function calls are already the thing. f(a, b) and (f a b) are the same structure written two ways, one with the operator inside the parentheses and one outside. The Lisp family gets a reputation for strangeness on the strength of that difference, and the difference is punctuation. Whatever expressive power people attribute to symbolic expressions is available in a notation everyone already types.
  3. Syntax and semantics are separable. Once you accept the first two there is nothing left to design in the syntax, so all the design effort moves to the meaning: what operations exist, what they take and return, and how they compose. Those were always the real questions and the notation had been absorbing the attention.

The notation and the parse result

One job is left for the notation, because JSON on its own cannot say that something is a call rather than a value. The package that came out of answering that is XJSN, for eXtensible JavaScript Notation, and its README states the whole design in one line: it is JSON with function calls as a primitive type.

The problem it replaces is what you get if you try to represent a program in plain JSON. Every construct needs a field naming what it is, and the nesting compounds:

{
  "$type": "conditional",
  "$condition": {
    "$type": "function_call",
    "$name": "user_has_permission",
    "$args": [{ "$type": "variable", "$ref": "current_user" }]
  }
}

Four levels deep to say one thing, and a model has to get every bracket right to produce it. In XJSN the call sits where a value would go, and the tagging disappears:

{
  "workflow": checkPermission(currentUser),
  "actions": [sendNotification(), returnResponse("success")]
}

The call is not evaluated. It parses into data, which is the property the whole design rests on:

{ "$type": "call", "$fn": "checkPermission", "$args": [currentUser] }

The verbosity did not go away. It moved: writing the tagged object was the model's job in the first version and is the parser's job in this one.

What that buys is a substrate rather than a language. The parsing stays fixed and each domain supplies its own functions, so a workflow tool and a game both write f(a, b) and mean entirely different things by it, while the tree underneath has one shape.

The family it belongs to already exists:

XJSN is basically Clojure.spec / EDN / Racket in JSON clothing

The shared idea across those three: a program is written in the same form as the data it works on, so one program can read and build another as easily as it can read a list.

XJSN adds two things to it, a way of stating what counts as valid, and a surface form models produce without special instruction.

Being a superset, it cannot borrow a JSON parser. The package carries its own lexer and parser, built on Chevrotain, along with an interpreter, a validator and a schema generator. That is the price of the design: a parser to maintain, in exchange for a notation the model already writes.

The goal underneath predates the project by a year or more: one extensible notation repurposable into any small language, rather than a new toolchain per language. Its practical form is a subset of one language and a superset of another, arranged so that users can attach automatic checking to the parts they care about.

Adjacent work: projectional editors

A projectional editor lets you edit a program's structure directly rather than editing text that gets parsed into structure. The best known one is JetBrains MPS, and this work sits in the same family, differing on four axes:

  • TypeScript rather than Java
  • driven by AI
  • flow based as well as projectional
  • aimed at people who would never install an IDE to get started

The useful question about MPS is what it asks before it gives anything back, and the answer is a Java toolchain and a commitment to building inside its world. That is a steep first step for somebody who wants a small language for one domain, and removing the step is the whole positioning.

What it cost

Three months went into syntax that was deleted. What the deletion bought:

  • a shorter specification
  • a generation path models already handle without special instruction
  • the attention that had been going into syntax now going into the operations

The notation itself is a package in the SDK, @idyllic-labs/xjsn, carrying the lexer, parser, interpreter, validator and schema generator described above.

The part I would carry forward is the question rather than the answer: before designing a representation for a machine to write, work out which of its properties exist for human readers, and check whether your reader is human.