Don't let it crash
Checked failure, capability-gated primitives, and why ordinary Seamless code should not crash.
One of the primary design pillars of Seamless is crash-safety.
Ordinary Seamless code should never crash the program.
If an operation can fail in a recoverable way, the caller should be able to see that in the value it returns.
A common version of this is selecting an item from application data:
function selectedPlanName(plans, selectedId) {
const matches = plansWithId(plans, selectedId);
return matches[0].name;
}Is it safe? Are we really sure that the matches will never be empty?
I’ve seen apps crash too many times because these kinds of invariant assumptions were violated when the code hit real production data. Just because of some missing null checks, out-of-bounds indexes, or other invalid inputs that trigger an unhandled failure path.
In many of these cases the cause and fix is trivial but still becomes an annoying and expensive source of interruption, both for developers and users.
In systems where a function’s fallibility is not explicitly modeled and checked, it inevitably becomes a common source of unexpected failures and crashes.
Let’s see how Seamless handles this. Seamless provides a basic Array as low-level building block for higher-level data structures:
Shell.Main> import Kernel.Array
Shell.Main> Array(1, 2, 3)
#array(1, 2, 3)The Kernel.Array.Primitive.get intrinsic maps to Wasm’s array.get
which traps on out-of-bounds access. So let’s see what happens when we call it:
Shell.Main> Kernel.Array.Primitive.get(Array(1, 2, 3), 3)
CompileError: builtin "Kernel.Array.Primitive.get" requires capability "array.unchecked" and module "Shell.Main" is not granted it at line 1:1 in "Shell.Main"As you can see, we don’t crash. Instead the compiler refuses to compile this code in the first place.
In general, ordinary code is not supposed to use these unsafe operations, and
the compiler enforces that. Regular code should use the safe Kernel.Array API
instead:
Shell.Main> Array.get(Array(1, 2, 3), 0)
(Some 1)
Shell.Main> Array.get(Array(1, 2, 3), 3)
NoneCapabilities
But somewhere, we still need to be able to call the unsafe primitive to implement the safe wrapper.
So let’s see how Kernel.Array implements get:
@forall a
@requires array.unchecked
@type (Array(a), Int) -> Maybe(a)
def get(items, index) {
if index < 0 or index >= length(items) {
None
} else {
Some(Kernel.Array.Primitive.get(items, index))
}
}Note the annotation:
@requires array.uncheckedThis marks the function as requiring the capability array.unchecked.
That capability is attached to the
Kernel.Array.Primitive.get intrinsic, so the compiler knows that any
function calling it must require the capability.
But this is only one side of the story. If we try this:
@requires arithmetic.unchecked
def example() {
Kernel.Array.Primitive.get(Array(1, 2, 3), 0)
}We still get a compile error:
CompileError: builtin "Kernel.Array.Primitive.get" requires capability "array.unchecked" and module "Shell.Main" is not granted it at line 2:48 in "Shell.Main"The crucial part here is that the module, Shell.Main in this case, also must be granted that same capability.
The compiler grants certain capabilities to specific bundled source modules by
default. For example, Kernel.Array is granted array.unchecked.
We cannot remove all unsafe operations from the implementation, but we can keep them below a checked boundary.
Capabilities as Trust Boundaries
The current capability system is a compiler-enforced trust boundary for a handful of primitive operations.
Bundled source modules are granted these capabilities module by module. For
example, as demonstrated, Kernel.Array can use array.unchecked.
That lets the standard library build safe APIs over lower-level Wasm operations without making those lower-level operations available by default to every module.
And the mechanism is not hidden compiler magic reserved for the language implementation either. The compiler can grant the same capabilities to ordinary application modules too.
Most code should never ask for these capabilities. But if a user-land package really wants to build its own checked abstraction over a lower-level primitive, the mechanism is available. It is an explicit trust decision, not a secret privilege.
Why It Matters
The web stack is full of boundary crossings.
Browser to server. Wasm to host. Component to runtime. Generated code to user code. Request to response. Synchronous code to async task. Local state to remote mutation.
In traditional systems, each boundary can smuggle in a different failure convention.
One call throws. Another returns null. Another rejects a promise. Another uses a status field. Another traps below the source language. Another depends on a framework-specific rule about what is allowed to happen where.
That is a hotbed of incidental complexity.
Seamless makes the failures explicit in the substrate. Crucially, checked failure does not come with a casual escape hatch. There is no way to unwrap a checked failure type like Result or Maybe without handling the failure path, and the lower-level operations that can trap are kept behind explicit capability grants.
This makes an important category of failure visible to the compiler, the caller, and eventually the framework.
That is the design I want underneath a web framework: a language where ordinary failure is well-defined and compile-time checked.
Don’t let it crash!