Will ChenWill Chen
← writing

Semantic blocksa pidgin between English and code

system design8 min

Semantic blocks are a vocabulary I sketched for writing on a page: English with a small set of added constructs, for the parts of a thought that plain English leaves loose. I described the result as a pidgin between English and a programming language.

The hard part was not choosing constructs. A construct earns its place by changing what happens after it is written, and nothing downstream of a page of English reads one, so the sketch had no way to separate a construct doing work from a construct that only looked like it was doing work.

The same question came back three more times in the same week, aimed at a ledger schema, at a type system, and at a dependency I was about to take on. The type system is where it gets a usable answer, because a type system already has something downstream that reads what you wrote.

The page is a wider channel than speech, and English is fitted to speech

English got its grammar from speech: a linear channel that rewards fast encoding, immediate decoding, redundancy and tolerance for ambiguity. Writing uses a wider medium in which a reader can take in more context at once, revisit earlier clauses and inspect structure directly.

The two channels have almost nothing in common:

speechthe page
deliveryone word at a time, in real timea whole paragraph at once
directionforward onlyglance back, skim forward at will
where structure is heldthe listener's working memoryin front of your eyes
decodingone pass, at the speed of productionas many passes as you want
so the grammar isloose, fuzzy and redundant, to survive being generated liveunconstrained by any of it

A grammar shaped by the left column is being used to write the right one. Speech has to be redundant enough to survive a listener who lost a clause and cannot ask for it again. The page removes that constraint and English spends none of the slack.

So the design goal is a notation that uses the width the page offers: constructs for the parts of a thought that a linear channel forced you to leave implicit, and that a reader taking in a paragraph at once could take in too.

Naming even one of those constructs requires naming what it is for, and English as a whole is too large a thing to aim at.

Augmenting a language and replacing it are different design problems

The first candidate is to stop using English. A formal notation carries three properties speech could never have given: it is unambiguous, it nests, and a machine can read it without guessing.

It also gives up the one property I wanted most, which is carrying a meaning nobody has finished defining yet. Most of what I want to write down is at that stage, so replacing English loses more than it buys.

So the notation has to augment English rather than substitute for it. I called the result a pidgin between English and a programming language: a small borrowed vocabulary for ideas natural language leaves loose.

Pidgin is the accurate word, and it names three design constraints:

  • it borrows from both languages
  • it is simpler than either
  • it exists for a job rather than for a culture

Those rule out the two nearby things I did not want, a formal language wearing English syntax and English with annotations bolted on.

Augmentation costs the enforcement. A formal notation gets its strength from a reader that rejects what it cannot parse. A pidgin has no compiler, so a semantic block is only as strong as whatever consumes it, and a page of English is consumed by a person who will forgive anything.

None of the constructs got named, because the design goal had nowhere to land. A notation for the page has to say what reads a block before it can hold a single construct.

A representation earns its structure by changing what happens

If the complaint is about English specifically, it should not show up in representations with no English in them. It shows up in two.

Accounting software works in codified low-level objects such as ledger entries, transactions and accounts. People reason in broader concepts such as “business expense,” which covers a loose collection of entities, rules and operations.

what the software stores   ledger entries, transactions, accounts
what a person says         "business expense"

The design goal there is an interface whose nouns are the concepts a person reasons in, and the sketch takes the consequence seriously: a concept-oriented API would expose operations that are themselves loosely defined, the way two people hand work to each other.

A loosely defined operation cannot be checked against a signature, so something has to interpret each call, and that interpreter is a component the design does not have. The idea got a name, an API for Human Concepts, and stopped there.

Both representations fail the same way. A representation should be load-bearing. A structure that does not constrain, produce, or otherwise affect what actually happens is decoration, paid for in attention and returning nothing.

Erased types are the same defect with a runtime already attached

TypeScript is the case where the structure exists, is enforced, and is deliberately disconnected from runtime behavior. Its types guide the checker and editor but do not influence the code that runs.

Compilation deletes them:

function f(x: Uint8): Uint8    // what you wrote and what the checker read
function f(x)                  // what runs

Which makes the transaction lopsided. You pay a strict and verbose type checker and fix everything it complains about, you get editor support, and the compiled JavaScript carries none of the guarantee.

The erasure is deliberate. Dropping the types is what lets TypeScript compile to JavaScript any runtime already accepts, and that compatibility is most of why anyone adopted it.

The design goal is to buy the guarantee back without giving up the syntax people already write, which is the trade Mojo makes against Python: familiar surface, low-level machinery underneath. Koala is the TypeScript superset that does it, stated as three changes:

  • Types are enforced at runtime and introspectable before compilation, so logic can depend on them.
  • Decorators work as they do in Python, rewriting at the AST level rather than annotating.
  • Pattern matching happens over type expressions, with consequences in the logic rather than in the checker.

What that buys is the sized types and sum types erasure rules out:

function addTwoNumbers(a: Uint8, b: Uint8): Uint8 {
  return a.checkedAdd(b);
}

Uint8 has a width, so checkedAdd has something to check against. Erase the type and the method has nothing to overflow.

enum Result<T, E> {
  Ok  { res: T; },
  Err { err: E; }
}

function divideBy(a: Uint8, b: Uint8): Result<Uint8, String> {
  if (b === 0) {
    return Result.Err { err: "cannot divide by zero" };
  } else {
    return Result.Ok { ok: a / b };
  }
}

Result is a choice between two shapes rather than a union the compiler forgets, so a caller cannot read err off a value carrying res.

Three consequences follow from the one decision, and they are the reason the design is a language rather than a library. The runtime has to carry type information the compiler currently discards, so the output stops being ordinary JavaScript. Introspection before compilation makes the compiler an API a program can call. The implementation must then choose between a native runtime, a TypeScript target that recreates the checks, or a lower-level target such as Rust.

All three are one choice priced three ways: keep the guarantee and give up the free ride on every JavaScript runtime, or keep the ride and go back to a checker whose findings evaporate. The systems-language branch went at it directly, through LLVM bindings under the name chickenscript.

Removing the tool is the test of whether the tool was load-bearing

The last case is a dependency rather than a notation, and it is cheap enough to settle before writing code.

The candidate was a framework for building editor extensions around a custom React renderer. A renderer decides what a description of an interface actually turns into, so a custom one would let you describe an editor panel in React and have it come out as something the editor understood.

The design goal is to answer the question on paper, because a proof of concept commits you to the answer while you are still deciding it. Written out, it settles itself:

React gives         JSX, reactivity, state
Pays off for        markup (HTML, PDF), interfaces (React Native), tree data (an AST)
My strongest case   web views
Already served by   React-DOM

WebViews were the strongest use case, and React DOM already served them. A custom renderer added no capability the design required.

The rule generalises: if removing the tool costs nothing, the tool was never load-bearing. The renderer was an interesting angle rather than a requirement, and asking in writing made the reversal cheap.

Structure exists only when something reads it

The hard decision in all four is the same, and it is never about the syntax: what reads the structure.

  • A semantic block is read by nobody, so it can only ever be a convention between a writer and himself.
  • A ledger row is read by the accounting software, which is why its structure is real and also why it is the wrong structure.
  • A TypeScript type is read by the checker and then discarded, so the guarantee stops at the compiler boundary.
  • A Koala type is read at runtime, which is the entire change and the entire cost.

Turning the load-bearing test on the documents themselves sorts them the same way. The medium argument constrains what any notation for the page has to carry, before a line of code exists. The blocks constrain nothing, because nothing downstream of them exists to be constrained.

A design document sits on the reading side of the gap between feeling that code is understood and writing enough code to test that understanding. It can feel complete while constraining nothing downstream.

The principles that came out of the week:

  • Aim a notation at a representation small enough to name, never at a whole language.
  • Ask what reads the structure before deciding what the structure is.
  • A construct that changes nothing downstream is a convention, and a convention is free to break.
  • Buying back a guarantee costs whatever property made the original adoptable, so name that property before spending it.
  • Remove the dependency on paper and see what breaks.

The question underneath, what unit carries meaning between a person and a machine, came back twice more with better answers. Semantic objects gave the unit an interpreter, so a model could hold a name for something and call methods on it. A notation for a model to write in gave it a parser, so the structure a model produced could be checked before anything ran. Each of those answers is the 2023 sketch with a reader attached.