cr8script · plate V.II
github ↗
plate V.II · the design defended
i / i

the refusals

each "no" is the feature

cr8script's premise is unfashionable: a language whose refusals are the feature. It refuses Python's silent type coercion, refuses truthy/falsy, refuses null, refuses 0-based lists. Each refusal trades convenience for the property that matters most when a model writes the code — that the script either does what it claims, or fails in a way the model can fix.

i

no truthy/falsy

if requires a real boolean

In Python, if 0 is false, if "" is false, if [] is false. Each is a quiet little surprise the language hopes you'll appreciate. For an LLM writing one-shot scripts, this is exactly the wrong default: a typo that makes the test reach an empty list silently passes the wrong branch and runs to completion with the wrong answer. cr8script makes if 0 then a located error — the model sees the line, sees the value, knows what to fix.

The cost is one extra character in some places: if xs.length is greater than 0 instead of if xs. The benefit is the entire class of "I thought it was a list, it was empty" bugs disappearing.

see also contrast: cr8 vs python shows three truthy/falsy footguns Python silently accepts.
ii

no silent type mixing

"5" + 3 is an error, not 53 and not 8

Languages disagree about "5" + 3. JavaScript says "53". Python says TypeError. cr8script says error, on this line, with this value, with the fix: use to_text(3) or to_number("5"). The explicit cast is two seconds; the bug it prevents is indeterminate — particularly when a number arrives as a string from an HTTP response or a CSV cell.

Crucially this also covers cross-type equality. 5 is "5" isn't false — it's an error. The language refuses to decide whether the string and the number are the same; the writer has to decide.

iii

decimal by default

0.1 + 0.2 is exactly 0.3

Every reader who has ever written a quick "what's the total?" script in Python has seen 0.30000000000000004 appear in a place a human will read. It's float arithmetic; it's correct; it's also wrong for the use case. cr8script's number type is decimal. There is no float. There is no Decimal import. There is just number, and addition does what you wrote.

The cost is a tree-walking evaluator that's slower at scientific computing. cr8script doesn't do scientific computing. The benefit is that finance, billing, tax, and reporting scripts — exactly the kind a model writes that a human reads and trusts directly — produce the number on the page, not a number eight ulps away from it.

iv

1-based lists

xs[0] is an error; xs.first / xs[1] are valid

This is the most opinionated choice and the one most likely to irritate a reader who has indexed from zero their entire career. The argument: people writing one-shot data scripts almost never want the C-array semantics that 0-based indexing was built to support. They want "the first row", "the third item", "the last entry". Most of the time the right access isn't an index at all — it's xs.first, xs.last, or a pipeline.

An LLM trained on a hundred languages will mis-index between them. Making xs[0] a located error rather than a silent off-by-one closes one of the most common silent-bug surfaces in agent-written code. The migration cost is real; the diagnostic when the model gets it wrong is immediate.

v

only nothing for absence

no null, no None, no undefined

Python's None is the C++ pointer's grandchild — a single sentinel that means absence, missing-ness, default, "I forgot", "it didn't apply", "the call failed", "look it up later". JavaScript has both null and undefined with subtly different rules nobody remembers. cr8script collapses absence to one keyword: nothing. Field access on a record requires the field to exist; absence is opt-in via r.get("key"), which returns nothing on miss.

The single sentinel makes "is this missing?" a single check (x is nothing) rather than three different ones across libraries. The required field access prevents a typo in r.cusomer from silently returning None — it's a hard error with a "did you mean customer?" hint.

vi

errors that teach

{ line, message, hint } · structured for agents

Most language error messages were designed for the human at a terminal. cr8script's were designed for the agent in a tool-use loop. Every diagnostic carries three things: the line where the problem is, a message describing what went wrong, and a hint proposing a concrete fix. The static checker emits these as JSON via --check-json — a list of {line, message, hint} objects the model reads and acts on without parsing English error text.

This is the property that justifies the rest. The refusals (truthy/falsy, type mixing, indexing, null) only pay off if the moment of failure is converted into something the writer can fix. cr8script's wager is that a small language with strict semantics and structured diagnostics produces fewer bugs per token than a permissive language with loose semantics and free-form English errors.

see it in action examples/agent_loop ships a deliberately-broken script, the actual JSON the checker emits, and the corrected version.
read next agents (integrator playbook) ↗ cookbook (recipes) ↗ cr8 vs python ↗ play (in-browser) ↗