Seamless Notes

An apple pie from scratch

Why this web-stack experiment starts with a programming language.

If you wish to make an apple pie from scratch, you must first invent the universe.

— Carl Sagan

That quote kept coming back to me because the deeper I pushed on the web-stack problem, the more the “reasonable” solution started to look like inventing the universe.

The component model always felt like the right direction for UI: local state, composition, data flowing into views, and interfaces described from application state instead of manually coordinated DOM updates.

When React came out, it was a game-changer. It made complex UIs easier to build because it moved a large part of the complexity into a better abstraction.

Less manual coordination. Less scattered UI state. A clearer way to compose interface behavior.

That was a real productivity jump.

Years later, Phoenix LiveView gave me a similar feeling on the full-stack side.

For a large class of interactions, the REST or GraphQL layer simply disappeared. No duplicated request and response types. No hand-written resolver glue. No client-side fetching state machine for every ordinary server interaction. Server state changed, the UI updated, and the framework handled the mechanical client/server coordination.

The feeling of relief when that coordination burden gets lifted is hard to overstate.

The client/server communication layer is an enormous tax, and LiveView showed what it feels like when much of that tax is gone.

But the truth is, the system has not escaped taxation. It merely changed the currency.

Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.

— Greenspun’s tenth rule

LiveView is excellent when the server-owned interaction model fits. When it does not, the client-side story becomes a second world: JavaScript hooks, pushed events, client-local state, and code that has to cooperate with the Elixir/HEEx/LiveView world through framework-specific escape hatches.

React has its own version of the same pattern. The component model remains powerful, but the surrounding stack keeps growing rules, compilers, server/client conventions, build-time transforms, special exports, framework-specific APIs, and more layers of machinery around JavaScript.

Greenspun’s rule may be a joke, but the pattern is real. Frameworks keep growing private semantic layers because they need to express things the underlying language does not understand.

Those layers solve real problems. They also create a huge amount of incidental complexity.

That is not an argument that React or LiveView are bad.

It is almost the opposite.

Good abstractions reveal where the next layer of pain lives.

Death by a thousand cuts

There is an old joke in software: every problem can be solved with another layer of indirection, except the problem of too many layers of indirection.

That is increasingly how the modern web stack feels to me.

Every layer solves a real problem. Every layer looks reasonable on its own. Together, they become death by a thousand cuts.

At that point, the question becomes:

What should the layer underneath the framework look like?

That is where this project starts.

A full-stack framework wants to know things that ordinary libraries usually cannot express cleanly:

  • what runs on the server
  • what runs in the browser
  • which data crosses the boundary
  • which effects are required
  • which values can mutate
  • which resources need cleanup
  • which code was generated
  • which host capabilities are available
  • which callbacks can be retained
  • which errors are recoverable

Today, those facts are often spread across TypeScript types, build plugins, generated files, framework-specific compilers, runtime protocols, naming conventions, and documentation.

Seamless is an experiment in moving more of that structure into the language substrate instead.

Why not build on an existing language?

I considered that seriously.

Clojure showed me how powerful a small, regular, data-oriented language can be. S‑expressions are beautiful in their own way: a language made from a few core primitives instead of a pile of arbitrary special cases. It showed me that a language can be powerful precisely because it is small and regular.

But raw Lisp syntax is a hard sell for broad application development, and I want static types to be central rather than optional, external, or bolted on later.

Elixir showed me something equally important: a Lisp-like core with a friendlier surface syntax can work. It also showed that a language can impose a little syntactic discipline and still feel pleasant. Surface syntax does not have to be arbitrary chaos. It can lower into something regular underneath.

Haskell, Rust, and Swift all have pieces I admire: algebraic data modeling, strong static structure, explicit failure, controlled mutation, predictable tooling, and careful attention to semantics.

But none of them are the exact substrate I would design for this problem.

The language I wanted needed to be Wasm-first, not Wasm-as-an-afterthought. It needed a structured macro system, a small core representation, static inference, region-scoped resources, controlled local mutation, and a path toward full-stack UI without treating the browser/client side as a second-class target.

At some point, the cleanest design was no longer:

pick a language and work around it

It was:

start from the substrate I want

Seamless

Seamless is the working title for this experiment.

It is a programming language prototype built around:

  • WebAssembly as a first-class target
  • a small structured core underneath friendlier surface syntax
  • static types, algebraic data types, and explicit Maybe / Result
  • immutable-by-default data
  • explicit local mutation through regions
  • region-scoped resources and async tasks
  • structured macro expansion

A safety bias

Seamless has a simple safety bias: ordinary code should not crash from ordinary failure.

If an operation can fail in normal use, that failure should be part of the type. Missing values use Maybe. Recoverable failures use Result. Async work carries its error channel through Task(r, a, e).

That applies broadly, not only to host interop.

Indexing can fail. Integer division can fail. Parsing can fail. Resource acquisition can fail. Host calls can fail. These cases should not be hidden behind ambient traps or unchecked runtime behavior in ordinary code.

The language can still have sharp operations underneath. It needs them for efficient kernels, low-level runtime code, optimized implementations, and host boundaries. But sharp operations should live in the substrate, not in the default programming model.

The public surface should be boring in the best way: explicit values, explicit failure, explicit lifetimes.

One small slice

The current prototype already has enough of this substrate to run real Wasm code through Node and browser-oriented hosts.

Here is a representative example. The syntax is still moving; the exact spelling is not the point.

module Example

import Std.Host.Http
import Std.Host.IO
import Std.Host.Task

@region r
@type () -> Task(r, Unit, String)
def main() {
  text <- Http.get_text("https://example.com/data")

  IO.println(text)
}

At the surface, this is just an HTTP request followed by a print.

Underneath, the call has this shape:

Http.get_text : String -> Task(r, String, Http.Error)

A Task(r, a, e) is async work owned by region r, eventually producing either a value a or an error e.

The region matters. The task, the callback, and the cancellation handle all belong to the same lifetime. When the region is cleaned up, the active operation can be cancelled.

That same region model is used for controlled local mutation and resource-bearing values. Mutable cells, builders, file handles, timers, HTTP requests, and retained callbacks should not each have unrelated lifecycle rules. They should belong to explicit lifetime boundaries the compiler can see.

This is the kind of structure I want the language to know.

Facing Greenspun

Web frameworks are a prime example of Greenspun’s rule.

A fixed set of language features cannot anticipate every framework abstraction. The answer is to make language extension principled.

If the system wants to grow a language, give it a real representation. Code as data. Macros over structure. Expansion as a defined phase, not a pile of string rewriting and build-tool duct-tape.

Lisp isn’t a language, it’s a building material

— Alan Kay

That is why Seamless lowers its surface language into a smaller structured core. The surface language should be pleasant to write. The core should be regular enough to inspect, transform, quote, expand, and lower.

For example, a surface declaration such as:

record User {
  name: String
  age: Int
}

has a regular core shape underneath:

(module User
  (record
    (name String)
    (age Int)))

The point is to give the compiler and macro system a stable representation for language extension.

A test declaration can look like a language feature:

test "adds two numbers" {
  expect_equal(add(1, 2), 3)
}

Underneath, it expands into an ordinary helper function plus compile-retained metadata. A test runner discovers those cases and builds a typed suite with direct function references.

That is the shape I want for future framework features as well.

Routes, commands, actions, validators, component declarations, generated host bindings, and client/server entry points should not require stringly codegen or hidden runtime reflection. They should lower into ordinary code and pass through the same type, region, capability, and host-boundary checks as everything else.

Macro expansion happens before typed AST elaboration, but it does not bypass the type system. The expanded result still has to become an ordinary typed program.

That is the balance Seamless is trying to strike: structured expansion without token hacking, static types without giving up metaprogramming, local mutation without making mutation the default, and host interop without pretending the host is safe by magic.

Why Wasm

Wasm gives the language a low-level execution substrate that can run in browsers, servers, and other hosts without making JavaScript the semantic foundation of the language.

Instead of designing around JavaScript semantics and recovering safety through framework conventions, Seamless starts with a typed language and lowers into Wasm plus explicit host contracts.

That also keeps the design aligned with the direction of the WebAssembly Component Model: typed interfaces, structured boundary values, and better interoperability between separately compiled Wasm modules and hosts.

Browser integration

The browser is a primary host, but I do not want the UI model to become a direct one-to-one wrapper over DOM APIs.

There will likely be a small typed browser host surface for runners, adapters, tests, experiments, and framework internals. Application code should eventually live at a higher level: declarative components, typed state transitions, scoped subscriptions, async tasks, and framework-owned rendering/update strategies.

The foundation needed for that is already visible. The focus right now is making sure the substrate can support the right abstraction when that work becomes central.

LLMs Make Language Design Matter More

It is tempting to think language design matters less now because LLMs can paper over awkward APIs, boilerplate, and framework conventions.

I think the opposite.

The same properties that make code easier for humans to reason about also make it easier for tools and language models to reason about: explicit types, immutable values, small regular syntax, visible phase boundaries, structured expansion, machine-readable diagnostics, and minimal ambient magic.

If the language is explicit, regular, inspectable, and easy to refactor, that should help both humans and machines.

For now

The compiler is private for now because the design is still moving quickly. A public release would create support, compatibility, and documentation expectations before the language has earned them.

But the intended end state is open. If Seamless reaches the point where I think it is good enough to release, the language, compiler, and framework should be developed under a permissive open-source license.

This is not meant to become a proprietary framework product or hosted-platform lock-in play. The goal, if it gets that far, is broad availability of the technology itself.

Self-hosting comes first

Before the UI layer becomes the center of the project, Seamless needs to become good enough to implement more of its own compiler, standard library, macro system, tests, package tooling, host-integration machinery, diagnostics, and build output.

A framework built too early would put pressure on the most complicated layer before the language substrate has hardened. Self-hosting puts pressure on the right pieces first: syntax, data structures, module boundaries, compile-time expansion, diagnostics, performance, host effects, and everyday ergonomics.

The framework direction remains the reason the substrate exists.

Self-hosting is how the substrate gets tested.

Conclusion

At some point, the next abstraction should not be another wrapper around the old substrate.

It should be a better substrate.

An apple pie from scratch.

That is where Seamless begins.