nib

Honest limits.

nib is v0.1: a research language with a real type system and several real gaps. This page is written by the people who built it, because the attractive version of each item below would be a lie — and a language that advertises guarantees should be judged by what it says it cannot do.

Where a claim is about a number in the source, check.sh greps the file for it on every build, so this page cannot quietly go out of date.

01There is no string library

Strings can be concatenated with ++, interpolated inside a literal, and compared. That is the whole vocabulary: no slicing, no split, no replace, no formatting — and no String namespace to call. Even this site is assembled by concatenation for exactly that reason.

snippets/strings.nib refused
namespace App

module App where
  -- Concatenation, interpolation and equality exist. A namespace of string
  -- operations does not — v0.1 has no slicing, no split, no replace.
  func main :: -> String =
    String.replace("a", "b", "banana")

wire W where App.main with Io.Out = Io.Stdout
Not "unimplemented" — absent. The message says which namespaces exist
$ nibc --run snippets/strings.nib
Name error at snippets/strings.nib:7:5: namespace 'String' is not known: no loaded file declares it (used: App)

02concurrent is a declaration, not a thread

sequential and concurrent describe whether an instance's state has a lifecycle. Neither one starts a lane: the runtime is single-threaded, nothing schedules a concurrent effector, and no parallelism is inferred from the flags. Hoisting work out of a branch or a failure point would execute effects that were never reached, so independence has to be an explicit, checkable claim by the author — and independent { } is not built yet. What is built is the refusal: a var on a concurrent effector is rejected rather than accepted and quietly wrong (guarantee 07).

03The HTTP server is not hardened

The server is part of the tool, and it is deliberately small: one connection at a time, always Connection: close, no keep-alive, no chunking, no TLS, and no routing table. These are design decisions and they are also limits you have to plan around.

lib/server.ml says the quiet part itself: nothing here is hardened for a hostile network. This page is nonetheless public, and it is served by exactly the loop described above — that is a deliberate trade for a v0.1 site whose whole point is to be served by the compiler it describes, not an oversight. This site terminates TLS in front, as every app here does; if you expose a nib server directly, you are the first line of defence — and you should not.

04A pin is a promise about a revision, and no type keeps it true

pin moves a decision from invisible to visible. It does not make the decision safe: a stored closure still runs the code of the revision that created it, and if that revision's dependencies moved, it may fail in a way nobody can reproduce. The best a language can do is refuse the silence — run the pinned value at its birth revision and say so, or refuse and demand an explicit migration. Anyone expecting pin to be a safety feature should read this paragraph twice.

system/tools/nib/docs/boundary.md §7.5

Read the loan checker as a tool for making intent visible at every site, not as a sandbox. It refuses the cases it can see — storing lent code, returning it, putting it in a container, handing it to a pin parameter from a lent one — and says nothing about what the code does when it finally runs.

05A frame is data; a journal is not built

Durability is protocol-shaped. Writing a run down is not enough: something has to decide that the run should be picked up again — when, by whom, with what retry, against which revision of the program. nib has the first half and none of the second. --park FILE writes the rest of a run to a versioned JSON frame and --resume FILE continues it in a different process:

snippets/park.nib two invocations
namespace Park

-- The same shape as hello.nib, parked instead of finished: --park writes the
-- rest of the run to a file, and a *different* process can pick it up.

interface Greeting where
  hello(name: String) -> String

sequential effector English is Greeting where
  hello(name) = "Hello, " ++ name ++ "!"

module Park where
  func main :: -> String ! Greeting =
    @Greeting.hello("World")

wire W where Park.main with Greeting = Park.English
The parked run prints no answer at all — the compile chatter and the frame's path go to stderr — and the second process finishes the job
$ nibc --run snippets/park.nib --park /tmp/nib-park-demo.json


$ nibc --run snippets/park.nib --resume /tmp/nib-park-demo.json
Hello, World!

What does not exist is anything that owns frames: no journal, no queue, no scheduler, no retry budget, no record of which frames are outstanding. Resuming is a command a person types. So the property you get is that a parked run is not trapped in one process — and the property you do not get is durability, which is a runtime, not a file format. A pin does not change this.

06One binary, three stdlib files, no packages

There is no package manager, no registry, and no dependency graph. The standard library is three nib files — io, http, iter — and they are nib source, compiled by the same front end as your program, which means you can read them, argue with them, and replace them. Anything else you write yourself, and the type checker will make sure the seams are explicit.

Two consequences worth naming: there is no Option type in v0.1, so Map.get raises MissingField instead of returning one; and the compiler gives you no fancier diagnostic than the ones printed on these pages, which is why they are worth reading carefully.

07And this site's own honesty has a hole

check.sh runs every snippet, checks its exit status as well as its words, diffs every quoted block against the file it claims to quote, and greps the facts above — but it cannot check prose. If a sentence here stops being true without a number or a program behind it, nothing will catch it: the first version of this page said --park and --resume moved a frame "within one process", which is false, and no grep noticed. Two smaller caveats, stated plainly: if no nibc can be found, the check fails rather than skipping, so a broken toolchain cannot pass quietly; and ./check.sh --record rewrites the expected transcripts from a live run, which is useful for an honest change and dangerous as a reflex — the diff it produces is the claim you are changing.

Back to the start Write your first program → The guarantees →