Skip to content

v2026.5.1 – :script Tier 2 Lands

Release date: 2026-05-01 Profiles affected: :script (Tier 2 functionally complete), :core (escalate sugar applies) Status: Standard release (6-month support)

The April release (v2026.4.8) introduced the :script profile as a syntactic surface. This release lands Tier 2: the desugar pipeline that makes a .jans file mechanically equivalent to a hand-written :core program. Top-level statements lift, ten stdlib modules auto-import, the escalate sugar drops do/end for single-expression bodies, and janus validate --promotable proves the Script Law on demand.


A .jans file from this release forward looks the way you’d hope:

hello.jans
let name = "World"
println("Hello, {name}!")
Terminal window
$ janus run hello.jans
Hello, World!

No func main. No use std.io. The compiler does that work for you, additively, before sema runs.

You can prove the Script Law mechanically:

Terminal window
$ janus validate --promotable hello.jans
validate --promotable: hello.jans is promotable to :core
$ echo $?
0

If the file fails the check, you get a pointed diagnostic and exit 1.


Pass 1 of the :script desugar walks the parsed AST, partitions top-level decls (functions, structs, traits, etc.) from top-level statements, and lifts the statements into a synthesized func main. The synthesized function is tagged SugarKind.script_main internally so diagnostics name it accurately and downstream consumers can distinguish user-written from synthesized.

Replace-in-place strategy: the source_file root keeps its original index in the AST; only its child_lo/child_hi are updated to point at a new edge range that includes the synthesized main. Anyone who scans unit.nodes by kind discovers everything correctly.

2. Ten fixed auto-imports (SPEC-045 §3.4)

Section titled “2. Ten fixed auto-imports (SPEC-045 §3.4)”

Pass 1.5 prepends the canonical SPEC-045 §3.4 import set to every .jans source file:

std.io std.fs std.process
std.text.rx std.text.peg std.text.stream std.text.search
std.collections std.fmt std.env

The list is fixed in the spec. It cannot be extended per-project, per-user, or per-file. Future additions require a SPEC amendment. If you import one of these by hand the compiler emits E3113_AUTO_IMPORT_SHADOWED as a warning so promotion to :core produces no surprises – resolve by removing the explicit use or by aliasing it (use std.io as stdio).

3. Single-expression escalate (SPEC-044 §3.2.1)

Section titled “3. Single-expression escalate (SPEC-044 §3.2.1)”

When an escalate body is exactly one expression, do/end is now optional:

// Block form (multi-statement bodies, unchanged)
let r = escalate :compute do
let scratch = allocate_buffer(1024)
matmul(a, b, scratch)
end
// Single-expression sugar
let r = escalate :compute matmul(a, b)

Both forms produce identical AST shapes (the sugar wraps the expression in expr_stmt to match the block form). The disambiguation is mechanical because do is reserved and cannot head an expression. grep -nE 'escalate :[a-z]+' over your source still finds every escalation site in either form. The Anti-Gadget Law (SPEC-044 §9.5) holds.

This sugar is universal to SPEC-044 – any profile that can host escalate can use it. It’s especially load-bearing for :script because the One Escalate Law (SPEC-045 §0) judges the profile on how light a single capability bridge feels on the page.

Bug fix shipped alongside: the parser’s convertTokenType was mapping the .escalate_ keyword token to the AST node kind .escalate_stmt (which happens to also exist in the TokenKind enum). As a result every parser.match(.escalate_) check returned false and parseEscalateBlock was dead code – the escalate keyword has never actually been parsed by the compiler. Caught while implementing §3.2.1; both forms now work for the first time.

4. janus validate --promotable (SPEC-045 §7.6 / §7.7)

Section titled “4. janus validate --promotable (SPEC-045 §7.6 / §7.7)”

The Script Law’s falsifiability check shipped as a CLI command:

Terminal window
janus validate --promotable file.jans

Runs parse → top-level desugar → auto-import injection → sema, stops there, reports the verdict. Faster than a full compile (no QTJIR, no LLVM, no linking). If the file passes, promotion to :core is guaranteed to round-trip without loss.

Use it in CI before shipping any .jans that you intend to promote later.


SPEC-045 §10 has been reconciled with the implementation. Six new error codes plus two new warnings, all in numeric order:

CodeSeverityWhen it fires
E3100_EMPTY_SCRIPTerrorA .jans file is empty (no decls, no statements).
E3102_USER_MAIN_WITH_TOP_LEVEL_STMTSerrorThe file declares its own func main AND has top-level statements. Pick one.
E3104_RETURN_AT_MODULE_SCOPEerrorA return appeared at module scope. (Currently dormant – the parser does not yet produce top-level return_stmt.)
E3106_RESERVED_SCRIPT_MAIN_ATTRIBUTEerrorUser code applied #[script_main]. The attribute is reserved for synthesized functions. (Defensive – cannot fire today.)
E3108_AUTO_IMPORT_RESOLUTION_FAILEDerrorA SPEC-045 §3.4 auto-import (e.g. std.text.rx) could not be resolved. Replaces the generic E4100 for script_auto_import-tagged use_jan nodes.
E3112_SERVICE_CONSTRUCT_AT_SCRIPT_TOP_LEVELerrorA :service-only construct (spawn, await, select, nursery, receive) appears at :script top level. Promote to :service, or wrap the single call in escalate :service .... Detected during pass 2; the walker stops at closure boundaries.
E3113_AUTO_IMPORT_SHADOWEDwarningA user use declaration shadows an auto-import without explicit as alias. Compile succeeds.
E3114_SYNTHESIZED_MAIN_ENDS_IN_PANICwarningThe synthesized main’s last statement is a literal panic(...). Fires only on a literal panic callee; divergent while true loops are correctly suppressed. The warning exists because a script that ends in a panic almost always means the author meant to return a status code.

.jan files are unaffected. .jans files that compiled before still compile. The new shadowing warning (E3113) only fires on duplicate user use declarations against the auto-import list – fix it once, ship.


What’s NOT in this release (honest about the runway)

Section titled “What’s NOT in this release (honest about the runway)”
  • Comment preservation through janus desugar – the lowered AST has no comment nodes; the printer cannot emit what is not there. Scheduled with the Phase 1 sugar sprint backlog.
  • Some auto-import targets (std.text.{rx,stream,search}) resolve only as the underlying modules ship. The compiler’s auto_import_known_broken filter prevents E3108 from firing on the modules whose lowering defects are tracked in SI-1/SI-2/SI-3. They auto-emit when those issues close, with no spec or doc change required.
  • A REPL for :script (Tier 4 ambition). janus run + shebang scripts cover the Tier 1 / 3 dev loops. A live REPL is a separate sprint.

Updated 2026-05-03. The original publication of this post (2026-05-01) said pass-2 implicit-try injection and janus desugar text output were not yet shipped, and that E3112/E3114 were reserved namespace slots awaiting pass 2. All four items landed on unstable later the same day. The diagnostics table and this section are amended to reflect ship-state. The :script Tier 2 deep dive carries the canonical reference; if it disagrees with this release post, the deep dive wins.