:script — The Gateway
:script — The Gateway
Section titled “:script — The Gateway”“Explore fast. Ship when ready.”
:script is :core with the training wheels on. It’s designed for the moments when you just want to get something done — explore an idea, prototype a solution, or crunch some data. When you’re ready to ship, one command promotes your script to production-ready :core code.
Status: SPEC-045 v1.4.0 RATIFIED (2026-08-02). The ratified scope is what runs today: top-level statements desugar to a synthesized
main, implicittryon fallible calls, last-expression-is-value semantics,janus desugar/janus validate --promotable, the{.script: sysadmin.}template directive, and AOT-by-default execution. Some sections below describe specified but deferred features (the$-family, shell/path literals) — they are marked where they appear; see What is ratified, what is deferred.
What :script Gives You
Section titled “What :script Gives You”Convenience Features
Section titled “Convenience Features”- Implicit types — The compiler figures it out
- Top-level code — No
main()wrapper required - AOT-first
janus run— Compile through the normal backend, then run the native binary - Auto-imports — Common stdlib modules available by default
- Script arena allocator — No explicit memory management
The $-Family (The Killer Feature)
Section titled “The $-Family (The Killer Feature)”Specified, not yet shipped. The
$-family positional sugar is formally deferred (SPEC-045 §15) and is not in the ratified v1.4.0 scope. The examples in this section show where:scriptis going, not what compiles today.
The $-family is what makes :script the replacement for awk, bash, Ruby, and Python. It’s inspired by awk’s $1, $NF — but with static type safety.
| Form | Meaning |
|---|---|
$_ | Current pipeline element, whole |
$1, $2, … | Positional component of the current element |
$# | Index of the current element in the stream |
$N | Number of positional components |
$_1, $_2, … | Closure arguments by position (multi-arg) |
$@ | Full closure argument tuple |
All resolved at compile time. If $5 doesn’t exist on your element type, you get a compile error — not a runtime crash.
AOT Execution
Section titled “AOT Execution”janus run path/to/script.jans # Compile through AOT, then executejanus ./path/to/script.jans arg1 # Shebang-friendly direct dispatchjanus run --jit path/to/script.jans # Development/debug runnerjanus run uses the same AOT compilation path as janus build, writes a
temporary script binary into a scratch location, forwards script arguments, and
then executes the native binary. --jit is an explicit development escape
hatch; --trace also opts into the JIT runner so trace output stays attached
to the interpreter path.
Known
--jitcaveat: the JIT runner mishandles failing scripts (a pre-existing interpreter bug — infinite recursion instead of a clean error exit). The AOT default path is correct; use it when testing failure behavior.
What Your Exit Code Means (SPEC-045 §3.2, ratified 2026-08-02)
Section titled “What Your Exit Code Means (SPEC-045 §3.2, ratified 2026-08-02)”Under janus run, a script’s last-expression value is discarded at the run boundary. The process exit code signals success/failure only:
- Exit 0 — the script ran to completion, regardless of what value its last expression produced.
- Exit 1 — the script failed. A failing implicit
tryaborts withPANIC: main returned with errorand exit code 1.
func compute() -> i32 do return 42end
println("computed")compute() // value 42 is DISCARDED — the process exits 0The exit code never encodes your computed value. The value is for the screen and the logs; the exit code is for the shell that ran you. (Before this contract landed, a script ending in the expression 42 exited with code 42, and a failing implicit try could silently exit 0. Both are fixed.)
Command Execution
Section titled “Command Execution”Use std.command when a script needs to orchestrate other binaries. The API is
argv-first by default, like Go’s os/exec, so arguments are not interpreted by
a shell unless you explicitly ask for shell execution.
use std.command
var out: [4096]u8 = undefinedlet n = command.output1("printf", "janus", out[0..4096])
var err: [4096]u8 = undefinedlet result = command.capture2("sh", "-c", "printf out; printf err >&2; exit 3", out[0..4096], err[0..4096])
let bounded = command.capture1_timeout_ms("sleep", "2", 100, out[0..4096], err[0..4096])if bounded.timed_out == 1 do eprintln("child exceeded deadline")end
if command.run4("test", "!", "alpha", "=", "beta") != 0 do eprintln("test command failed")end
let child = command.spawn2("sh", "-c", "exit 0")let code = command.wait(child)Use command.shell(...) or command.shell_output(...) only when shell
expansion, pipes, redirects, or shell builtins are intentionally part of the
program.
What :script Excludes
Section titled “What :script Excludes”| Excluded | Available In |
|---|---|
| Not publishable via Hinge | :core (after promotion) |
| No explicit allocator control | :core, :service |
Promotion is one command:
janus desugar script.jans > script.janWhen to Use :script — and When to Leave
Section titled “When to Use :script — and When to Leave”Perfect for:
- Learning Janus interactively
- Prototyping an idea in 10 minutes
- Data exploration and analysis
- One-off automation scripts
- AI agent tasks (short-lived, disposable code)
- Homework and algorithm competitions
The rule: Use :script to explore. Use :core to ship.
When to promote
Section titled “When to promote”:script is single-file, non-publishable, and AOT-cached by design. The moment your program outgrows those bounds, it is telling you it is ready to grow up. The six triggers — wanting to live longer than a session, needing a capability outside the template’s default set, needing tensors/actors/grains, needing a second file, needing hot reload, or writing a library — each mean the same thing: promote. The full list, with the Lua lesson behind it and the mechanical janus validate --promotable test, lives on its own page:
→ When to Promote from :script
Code Examples
Section titled “Code Examples”Hello, World (No Boilerplate)
Section titled “Hello, World (No Boilerplate)”# Just run this - no main() neededprint("Hello from :script!")The Killer Example
Section titled “The Killer Example”Deferred syntax. This example uses the
$-family and<<p"...">>path literals — both specified but not yet shipped (SPEC-045 §15). It shows the design target.
This is the syntax that makes Nexus operators delete their ~/.zshrc:
<<p"access.log">> |> map($_.fields()) |> filter($5.to_int()? >= 500) |> group_by($7) |> map(($_1, $_2.len())) |> sort_by($2, desc) |> take(10) |> for_each(println)What this does:
- Stream lines from
access.log - Split each line into fields (whitespace-separated)
- Filter to status codes >= 500 (errors)
- Group by the 7th field (URL path)
- Count items per group
- Sort by count descending
- Take top 10
- Print each
Compare to the shell equivalent:
cat access.log | awk '{print $5, $7}' | grep -v '^[0-4]' | sort | uniq -c | sort -rn | head -10The Janus version is:
- Type-checked —
$5.to_int()?fails at compile time if not a number - Provenance tracked — every line knows its source file and line number
- Fused — compiles to a single loop, zero intermediate arrays
Pipeline with Regex
Section titled “Pipeline with Regex”Deferred syntax — same status as the example above. Running this today emits a stopgap loud-fail diagnostic and returns empty (see the note in What is ratified, what is deferred).
<<p"server.log">> |> grep(r/ERROR.*connection reset/) |> map($_.split(":").1) |> unique() |> for_each(println)Working with Data
Section titled “Working with Data”# Read JSON, filter, transform, output CSVlet data := read_json("users.json") |> filter($_.age >= 18) |> map($.name) |> sort()
for name in data do println(name)endWhy This Beats the Competition
Section titled “Why This Beats the Competition”| Concern | awk | Bash | Ruby | Python | Janus :script |
|---|---|---|---|---|---|
Positional $N | ✅ | ❌ | ❌ | ❌ | ✅ |
| Pipeline operator | ❌ | ✅ | ✅ | ✅ | ✅ |
| Type-checked positions | ❌ | ❌ | ❌ | ❌ | ✅ |
| Zero-alloc fusion | ✅ | ❌ | ❌ | ❌ | ✅ |
| Sub-10ms cold start | ✅ | ✅ | ❌ | ❌ | ✅ |
| Static binary output | ❌ | ❌ | ❌ | ❌ | ✅ |
| Promotion to production | ❌ | ❌ | ❌ | ❌ | ✅ |
Regularity is the speed floor
Section titled “Regularity is the speed floor”The fusion and cold-start wins in the table above are not backend accidents. They fall out of the language being small and regular. Roberto Ierusalimschy draws this lesson from Lua outperforming Python despite both being dynamic: Lua is faster partly because the VM fits in cache (smallness), and partly because Lua is less dynamic — fewer indirections, fewer “everything can mean something else” surprises, so there is less runtime type dispatch to bail out of. LuaJIT’s trace compiler works at all because Lua is regular; a trace compiler on a language with more dynamic surprises would deoptimize to the interpreter constantly.
The principle is backend-independent, and it is why every :script sugar form desugars mechanically to :core rather than introducing a new runtime path. Pipeline fusion specifically depends on a deeper property Roberto names: type stability through a pipeline is what enables fusion. LuaJIT traces win in the dynamic world when a variable is not reused across types; Janus fusion wins in the static world because every stage’s output type is known at compile time, so the next stage’s input shape is provable without dispatch. This is the load-bearing reason the $-family and TextStream can promise zero-allocation walks — and the reason any future “just make it dynamic” pressure must be resisted.
What is ratified, what is deferred
Section titled “What is ratified, what is deferred”SPEC-045 v1.4.0 (ratified 2026-08-02, RFC-057 scope) covers the language you can run today:
- Top-level statements desugar to a synthesized
main(Pass 1) - Implicit
tryon fallible calls (Pass 2) - Last-expression-is-value semantics + the exit-code contract above
- Optional field default sugar (§3.5.13)
- The
{.script: sysadmin.}template directive, with rejection of unknown template names (E3118) janus desugar+janus validate --promotabletooling and the Script Law round-trip- AOT-by-default execution (
--jitis an explicit dev flag)
Be honest with yourself about the rest. Ratified :script is not yet the sysadmin product. The ten-module auto-import set (std.io, std.os.*, std.text.*, std.collections, std.fmt) is provisional — it works, but it is pending the template split that migrates to public facades. Runnable Artifact Store caching on the default janus run path is now shipped (unstable @ 06a95f8c, gated to import-free scripts until semantic-CID plumbing). And these remain formally deferred (SPEC-045 §15): entry-point allocator injection, the capability sandbox, the REPL, and the $-family positional sugar.
Unbacked-literal note (2026-08-04,
unstable @ e935c354): the path/stream/regex literal syntax (p"...",<<...>>,r/.../) tokenizes and lowers, but the runtime backing is not yet implemented. These literals are a stopgap loud-fail: they emit a diagnostic to stderr naming the feature and SPEC section and return an empty string, rather than silently passing the input through. Note the stopgap does NOT yet force a non-zero exit — a script whose final expression is an unbacked literal can still exit 0 with empty output. True loud failure (non-zero exit) lands with the E3115/E3116/E3117 diagnostics. The shell literal (`...`) IS implemented (viapopen) and works. The$-family resolves at compile time for the implemented positional rules (tuple/Vec), but several forms ($*, out-of-range$k) are stubs — the libjanus worklist (diagnostics, real runtime implementations) is tracked inJANUS/.agents/specs/_HANDOFF/SCRIPT-SUGAR-DIAGNOSTICS-SPEC.md.
Next Steps
Section titled “Next Steps”- When to Promote from
:script– The six triggers that say your.janshas outgrown the bazaar, plus the Lua lesson and the mechanical promotion test. :scriptTier 2 deep dive – How the desugar actually works (top-level lift, auto-imports,escalatesugar,validate --promotable)- Move to :core — When you’re ready to ship
- Profile Overview — See all six profiles
- Generics & Traits — Level up your code
Explore fast. Ship with confidence.