(↑)

Zipper

Zipper is a small text-replacement programming language. It works the way people think an interpreter works. I evaluates code, and replaces the code with the result.

Its complete program state is one string, the interpreter scans to the right of a visible cursor (▮), finds the next reducible call, replaces it in place, and repeats. The program text can be edited at any point, making it its own debugger.

◆

Loading the Zipper playground…

Open the playground on its own.

Browser calls

Zipper looks up its own definitions first. Any unresolved function is called in the live page's JavaScript: Math.round(...), document.querySelector(...), fetch(...), or another function supplied by the page. Arguments are evaluated left to right, one call per step. Promises are awaited automatically, and returned values can be passed to another call.

seq(
  define(setText, selector, text, q{
    Reflect.set(document.querySelector(selector()), "textContent", text())
  }),
  setText("#heading", "Hello from Zipper"),
  emit(Math.round(3.7))
)

Reflect.get(object, "property") reads a property; Reflect.set(object, "property", value) changes it. The live page supplies call(object, "method", args…) for methods on returned objects, so call(fetch(url), "json") fetches and decodes JSON. Browser objects keep their identity and appear as numbered references on the tape. Use emit(...) to print a result. CORS and browser permissions still apply.

Back restores language state and output, stopping at browser effects. Reset restarts the current source and creates a fresh live page. You can also edit the current tape and continue from its cursor. Programs run only when you press Run or Step; Save .piz downloads your source.

The language

A Zipper program looks like nested function calls:

▮seq(
  define(
    greet,
    who,
    q{
      seq(
        emit("Hello, "),
        who(),
        emit("!\n")
      )
    }
  ),
  greet(emit("Jeremy")),
  emit("Done.\n")
)

It produces:

Hello, Jeremy!
Done.

The interesting part is how it gets there. A simple expression changes directly on the tape:

▮seq(emit("A"), emit("B"))
▮emit("A") seq(emit("B"))
"A"▮ seq(emit("B"))
"A" ▮emit("B")
"A" "B"▮
Output: AB

seq exposes one expression at a time, emit prints a string and leaves it quoted on the tape, and define replaces itself with a stored function definition. Bodies inside q{...} are delayed until called. Function arguments become zero-argument thunks, so they are evaluated only where the body asks for them; definitions capture the definitions already on the tape, which provides lexical closures.

The cursor moves into each new code replacement, or past literal output. The live tape makes function expansion, argument substitution, output, and closure capture visible. This JavaScript version keeps printed strings and quoted blocks inert, validates malformed input, and restores captured definitions for zero-argument functions as well.

The source and example program are in the racketprogs repository.