SCUA

News

SCUA 0.17.0 is out

August 21, 2026

Two days after 0.16.0, and denser than that gap suggests. This release is mostly about two things: you can finally ask the person at the keyboard a question, and a set of bugs that were returning wrong answers rather than failing.

#You can read from the terminal

sys.readline() reads a line. sys.readpassword() reads one without echoing it. Both sit behind a new --allow-tty grant, so a script cannot quietly start prompting unless you let it.

The password path suppresses echo before the prompt is drawn and discards anything typed ahead, because doing it in the other order means a fast typist or a paste can put the secret on screen before the suppression lands. Echo is restored on the way out, including when the program is interrupted.

#The JIT was returning wrong answers

The optimising tier could clobber a value that stayed live across two calls. A loop body reading an outer variable and then calling functions twice could, once it ran hot enough to reach that tier, use a leftover internal value in place of the variable, while printing the variable still showed the right thing. Wrong numbers, silently, in the default configuration.

The interpreter and the baseline tier were never affected. The fix carries a regression test that was checked by breaking the fix again and confirming the test fails.

#The JIT had never run in a released macOS build

Separately, and worse: the signed macOS binaries were built with the hardened runtime and no entitlements, which is the configuration macOS uses to forbid the executable memory a JIT needs. Every macOS release ran interpreter-only, --jit-stats reported compiled=0, and nothing said why. Unsigned development builds JIT fine, which is why it lasted so long.

If you have measured SCUA's performance on macOS from a downloaded release, those were interpreter numbers.

The release script now grants the entitlement and then verifies it landed rather than trusting the flag, because the first attempt at the entitlements file failed the parser, codesign exited non-zero, and the build carried on regardless. This release is the first where the check is part of cutting it.

#Dates stopped being quietly wrong

Three separate defects at the edges of the calendar, all of the same shape: a wrong answer rather than an error.

A year past the end of the calendar wrapped. time.of(300000000, 1, 1).year() returned -284554050, a plausible-looking date hundreds of millions of years from the one you asked for. In an unoptimised build the same call aborted the process instead, which was at least loud but could not be caught. Every way of building or moving a datetime now checks the range and faults with the limit in the message.

Dates before year 1 were a day out. The leap rule was applied to an already-adjusted era on the negative side, so every year below 1 kept a 29 February it does not have.

And time.parse refused the dates SCUA itself prints for those years. A pre-year-1 date formats with a leading minus, the parser stopped at the minus and called its own output malformed, so a date written out and read back was lost.

#Encoding a value that contains itself

json.encode and toml.encode on a cyclic value used to crash the process with no message and no line number, taking any buffered output with it. A parent pointer or a cache referencing its owner was enough. It is now a catchable fault. json.decode had the mirror-image problem on deeply nested input and now returns an error value instead.

#Language and library

  • A module you import is now type-checked. The checker used to run on the file you ran and nothing else, so every published package compiled with the type system effectively off. Adding a dependency can now surface its type errors, reported against the module's own file and line.
  • A module can export a type. alias.Enum.Variant works in patterns and as a constructor, with exhaustiveness across the boundary.
  • delete(t, k) removes a table key. Assigning nil never did: the key survived len, iteration and json.encode.
  • sys.write(s) writes without a newline.
  • A catchable stack overflow. pcall/try can turn "this input nests too deeply" into an error, and a runaway recursion in one actor no longer kills the others.
  • Unique ids: ids.uuid4, uuid7, ulid, drawn from host entropy rather than the seeded RNG.
  • Full TOML 1.0, plus toml.encode.
  • --allow-file and --allow-all for capability grants, and --env-file with a pure dotenv.parse.
  • A runtime fault names the file it actually happened in, and so does every line of a backtrace.
  • Regex offsets are 0-based, Ok(x) == Ok(x) compares structurally, and datetime orders with <.

#A five-week red nightly, explained

The nightly CI lane had failed every run since it was created on 12 July. Both causes were real and both are fixed: the JIT bug above, and a fuzz divergence in persist.move.

The second was not what it looked like. move was corrupting nothing. The test harness read a promoted numeric array's packed storage as though it held boxed values, walked off the end into the next object's header, and hashed that. Compaction legitimately moves objects, so the "value" changed between runs. The arena bet holds; the checksum was wrong.

The intermittent cluster failure that made the lane unreadable is fixed too. Its fixed round counts were secretly wall-clock budgets racing the kernel's loopback delivery, which optimised builds won often enough to matter. Measured at 14% of runs before, and 0 in 1600 after.

#One note on the package manager

scua-pkg 0.3.1 supports scua 0.14 to 0.16, so scua add and the other package verbs refuse against 0.17 until a scua-pkg release catches up. It says so plainly rather than emitting something subtly wrong. Running programs never needs it.

The changelog has the full accounting.