December 17, 2025
A language with authenticate, charge_card, tensor, sprite, interrupt, and pivot_table in its core would be worse than the languages it replaces. It would encode one committee's snapshot of software into grammar.
The solution is not to predict every domain programmers will touch. It is to standardize how a domain becomes legible to the compiler and runtime.
Part 1 argued that human-oriented source languages are not obviously the right canonical output of a probabilistic programmer. That claim is cheap until there is an alternative concrete enough to criticize. The alternative proposed here has three layers: a small semantic core, typed domain extensions, and adapters to existing ecosystems.
The core is not a universal ontology. It knows about types, contracts, effects, capabilities, resources, state, failure, time, observation, and provenance. It does not know what Stripe, CUDA, Excel, or Vulkan is.
The weak version of this proposal looks like deployment configuration:
service checkout {
runtime = node
database = postgres
}
That may be useful, but it is not a programming language. It cannot express how an order total is calculated, what happens when a customer is missing, how a subscription changes state, which operation may be retried, or what the user should see while an external event is delayed.
A generation language must express computation: records and variants, branching, data transformation, functions, state transitions, concurrency where it matters, typed failure, external effects, and user-visible outcomes. It also needs contracts strong enough for a compiler to reject a program that is locally well-formed but systemically incomplete.
Here is the Storefront operation from Part 1 with a little more context. The syntax remains provisional.
system Storefront {
resource commerce: Database<CommerceSchema>
capability customers: Authentication<User>
capability billing: PaymentProcessor
operation begin_checkout(
request: BeginCheckout
) -> CheckoutRedirect
requires [
customers.authenticated(request.actor),
request.cart.non_empty
]
effects [
commerce.write(PendingOrder),
billing.create_checkout
]
idempotent_by request.id
ensures result.destination.is_https
{
customer = commerce.Customer.require_identity(
request.actor.identity
)
checkout = billing.checkout.create {
purchaser = customer
product = AnnualPlan
success_url = web.route("/account?checkout=success")
cancel_url = web.route("/pricing")
}
commerce.PendingOrder.insert {
user = request.actor.id
checkout = checkout.id
}
return checkout.redirect
}
}
This is application code. It performs a lookup, constructs a request, invokes an external capability, writes state, and returns a result. The declarations around the body are not comments. They constrain which callers are legal, what authority is needed, whether a retry is valid, and what the result must satisfy.
A "small core plus extensions" sounds responsible because every ambitious language proposal eventually says it. It can also be a way of hiding the ambition.
If extensions can define types, effects, lifecycle, simulators, lowerings, observations, verifiers, editors, and package metadata, we have recreated a compiler framework, package manager, build system, infrastructure planner, runtime protocol, and ontology project. If the core is too small, extensions cannot agree on anything useful. If it is too rich, the core becomes the universal language of software by another name.
That objection should constrain the prototype, not merely appear in a limitations section.
The first implementation should define only the extension contract required by one vertical slice. Unknown libraries enter through foreign interfaces. A concept earns semantic elevation only when doing so enables checking, authority control, planning, simulation, observation, verification, cross-backend lowering, or safer repair. Portability is a gradient: some operations may have several verified adapters, some one partial adapter, and some only an opaque escape hatch.
The goal is not to model everything. It is to stop pretending we understand things we do not.
The core should contain mechanisms that recur even when the domain nouns change:
This list is still too large for a first parser. It is an architectural boundary, not a sprint backlog.
Effects deserve special attention because they turn hidden behavior into an interface. A capability answers who has authority? An effect answers what externally visible class of action may happen? A precondition answers when is it legal? A postcondition answers what must be true afterward?
Languages such as Koka demonstrate the narrower proposition that effects can appear in function types and support static reasoning. This proposal uses that idea more coarsely. billing.create_checkout is not necessarily an algebraic effect handler. It is a semantic fact the planner, sandbox, auditor, and retry checker can all consume.
Canonical form matters here. The language should not offer five error models, three module systems, two resource-ownership schemes, and arbitrary serialization choices. Canonical does not mean terse. It means equivalent programs tend to converge on the same structure, so generation and repair do not spend probability mass on irrelevant variation.
The graph behind begin_checkout connects several kinds of fact:
This representation does not prove that the payment provider is correct or that the chosen identity is globally unique. It gives deterministic tooling something better than pattern matching over arbitrary application code.
Resources also need stable identity. A database row, cloud bucket, model checkpoint, device buffer, and payment subscription have different lifecycles, but all can be created, observed, updated, transferred, or destroyed. If generated code changes filenames and line numbers on every repair, semantic resource identities give traces and diagnostics a stable address.
Probabilistic programmers are good at filling blanks. A language should not force every blank to be filled.
authentication.provider = unresolved(
"Choose a provider compatible with the customer identity policy"
)
Or, if the allowed set is known:
authentication.provider in [Clerk, Auth0, InternalOIDC]
An unresolved value is neither null nor a confident guess. It is a typed planning blocker. The compiler can propagate it, determine which operations depend on it, and ask a focused question when the answer becomes necessary. A deployment planner can refuse production while still allowing a local simulator.
This is one place where a model-facing language can be more honest than conventional source code. A placeholder string that happens to type-check is often worse than a compile error.
Payments do not belong in the universal core. A payment extension can still tell the system why a provider call is special:
domain commerce.payments {
interface PaymentProcessor {
resource Checkout {
identity: CheckoutId
status: CheckoutStatus
}
effect CreateCheckout {
class = external_financial_mutation
retry = idempotency_required
audit = required
}
operation checkout.create(
request: CheckoutRequest
) -> Checkout
effects [CreateCheckout]
observation events: Stream<PaymentEvent>
simulator sandbox
verifier signed_events
}
}
A real extension format may look nothing like this. The contract is the point. It defines resources, operations, effect classes, retry rules, observations, a simulator, and a verifier. A Stripe adapter can implement that contract; a fake adapter can implement it for local tests. Stripe does not become syntax.
There should be three honest levels of integration:
The third level is essential. A language that cannot call an npm package until a standards committee models it will not survive contact with a real repository. But the escape hatch should leave a visible hole in the guarantee surface rather than laundering unknown code into a trusted capability.
The canonical representation need not be the best interface for every person or domain.
A model may prefer a redundant textual form. A reviewer may want a compact effect and resource summary. A compiler emits TypeScript. An ML researcher works in a notebook. A financial analyst edits a spreadsheet grid. A graphics engineer manipulates a scene. A debugger follows a causal trace.
Those can be projections over one semantic graph rather than files that drift independently. This is difficult: round-tripping visual edits without destructive serialization is an open problem. The alternative is already difficult. Today the notebook, generated script, deployment settings, and spreadsheet often disagree with no mechanism to say which one owns the truth.
The architecture is not general merely because it has an extension mechanism. It should survive one contrast.
experiment ImageClassifier {
resource samples: Dataset<Image, Label> = datasets.cifar10()
resource model: Model<Image, Label> = ml.resnet18()
capability compute: Accelerator = accelerators.available_gpu()
run training = ml.train {
model = model
dataset = samples
optimizer = adam(learning_rate = 0.001)
epochs = 20
checkpoint = every(5 epochs)
}
observe [
training.loss_curve,
training.validation_accuracy,
training.gradient_statistics,
compute.memory_usage,
training.sample_predictions
]
expect {
training.gradients.finite
training.validation_accuracy >= 0.80
}
}
This does not constitute an ML language. It shows the recurring core: resources, capabilities, effects, time, observations, assertions, and evidence. The ML extension supplies datasets, models, optimizers, checkpoints, and domain-specific observations. A PyTorch adapter supplies execution. A future compiler could choose another backend without pretending all backends are equivalent.
The rule is simple: do not standardize every noun. Standardize how a new noun acquires types, effects, lifecycle, observations, verification, and a path to execution.
That still leaves the program cut off at the repository boundary. The deployed Storefront depends on provider instances, secrets, domains, webhooks, certificates, and settings that currently live in dashboards.
Previous: What Should Coding Agents Compile To?. Next: The Browser Is the Uncompiled Part of Your Program.