SCUA

News

SCUA 0.32.0 catches more bugs before your code runs

October 4, 2026

SCUA 0.32.0 checks how your code uses Ok and Error. When a call can fail and your code uses its answer as if it couldn't, the compiler says so before anything runs and shows the line written the right way. Crypto is safer too: every crypto call now hands back a result you can check. Programs with partitions use several cores by default, and reading files and sending messages take less memory. There is a short guide to upgrading at the end.

#The compiler now checks Results

Many calls in SCUA answer with Ok(value) or Error(reason): decoding JSON, reading a file, an HTTP request, and now signing and encrypting. The value you want is inside the Ok. SCUA 0.32 follows Results through your code, and when one is used where its value is needed, you get a compile error that shows the fix:

import json
import sys

let user = json.decode(sys.args()[0])
print(`hello, {user.name}`)
$ scua greet.scua '{"name": "Ada"}'
scua: greet.scua:5: type error: `user` is a Result (`json.decode` returns Ok or Error), and a Result has no field `name`: the value inside it may. Take the value out first:
  user?.name                               -- an Error stops the script (exit 1)
  match user  Ok(v) -> v.name  Error(e) -> …  end

Add ? after the call to take the value out:

let user = json.decode(sys.args()[0])?
$ scua greet.scua '{"name": "Ada"}'
hello, Ada

At the top level of a script, ? on an Error stops the script with exit status 1 and writes the reason to stderr. Inside a function it hands the Error back to the caller.

$ scua greet.scua '{"name": Ada}'
{"error":"invalid JSON at line 1, column 10"}

The same checks cover a Result put into a template or a string, a match that handles Ok but not Error, and a function that returns Ok on one path and a plain value on another. They also run inside partition handlers. A call whose Result nothing looks at gets a warning, so a failed write doesn't go unnoticed. When you mean to drop a Result, write let _ = save(x) and the warning goes.

#Crypto is safer now

Every crypto call now hands back a result you can check. Signing, encrypting, wrapping a key and drawing random bytes return Ok(value). A bad key, an unknown algorithm name or data that has been changed comes back as an Error, which your code handles like any other value. crypto.verify and hash.equal answer true or false, whatever they are given:

import crypto

let keys = crypto.keypair(crypto.random_bytes(32)?)?
let sig = crypto.sign("pay ana 10", keys.secret)?

print(crypto.verify("pay ana 10", sig, keys.public))
print(crypto.verify("pay ana 99", sig, keys.public))
print(crypto.verify("pay ana 10", b"not a signature", keys.public))
$ scua sign.scua
true
false
false

Every crypto Error holds a kind and a message. The kind is "rejected" when a key, a signature or data from outside didn't check out, and "misuse" when your own program passed something of the wrong shape, such as a nonce of the wrong length, which lets one comparison tell bad input from a mistake in your code. Here a sealed box is opened with the right key and then with a different one:

import crypto

let key = crypto.random_bytes(32)?
let nonce = crypto.random_bytes(12)?
let box = crypto.aes_gcm_encrypt(key, nonce, "the vault code is 7741")?

fn try_open(k)
  match crypto.aes_gcm_decrypt(k, nonce, box)
    Ok(plain) -> print(`opened {len(plain)} bytes`)
    Error(e) -> print(`{e.kind}: {e.message}`)
  end
end

try_open(key)
try_open(crypto.random_bytes(32)?)
$ scua vault.scua
opened 22 bytes
rejected: aes_gcm_decrypt: the data does not authenticate: it was changed, or the key, nonce or extra data is not the one it was encrypted with

The crypto reference lists what each call returns and which kind each problem gives.

#Partitions now use several cores by default

A program with partitions now spreads them across your machine's cores with no flag: one thread per full-speed core, up to eight. In our measurements, a program whose partitions each do heavy computing ran 3.5 times faster on an Apple M4 Max and 3.9 times faster on an eight-core Intel i7-10700 running Linux, with the code unchanged. Partitions that pass many small messages back and forth run within a few per cent of their speed on one core, because SCUA keeps a round of light work on one thread and brings in the other cores once there is enough work to share.

What your code can rely on stays the same. Each partition handles one message at a time, messages from one sender arrive in the order they were sent, and the lines one partition prints stay in order. Lines from different partitions can interleave differently from run to run. When you need the same output every time, pass --workers=1 or set SCUA_WORKERS=1. scua test and scua debug always run that way.

#Files and messages take less memory

fs.read and fs.read_text read a file straight into the value they give you, so loading a 50 MB file takes about 50 MB on top of what your script already uses. That holds for text and bytes, for files of any size, and inside a partition that uses --io=async. A 200 MB fs.read takes about 25 milliseconds on an Apple M4 Max. Both calls also read files that don't report a size, such as those under /proc and /sys on Linux, all the way to the end.

A table you send to another partition is built at its final size when it arrives, so it costs the receiver only its own size: a 5,000-entry table takes about 422 KB there. A new partition starts at about 64 KB. Sending a large table or creating many partitions is 10 to 20 per cent faster, and the receiver sees the table's keys in the same order the sender did.

#Smaller additions

  • --no-warnings keeps compile warnings off stderr, for a tool or an agent that reads every line of it. Errors still print and still stop the run. A project can set it for everyone with warnings = false under [run] in scua.toml, and --warnings shows them again for one run:

    let row = { qty = "3", price = "4.50" }
    let qty = tonumber(row) ?? 0
    print(qty)
    $ scua total.scua
    scua: total.scua:2: warning: `tonumber` is given a table (table {qty, price}), which is never a number, so this always gives nil — pass it a string or a number
    0
    $ scua --no-warnings total.scua
    0
  • A money value has .code() and .amount():

    let price = 19.99 USD
    print(price.code(), price.amount())
    $ scua price.scua
    USD 19.99
  • Reading a field on a datetime or a money value now names the method to call: a datetime has no fields — `year` is a method, so write `.year()` .

  • A literal algorithm name that a crypto call doesn't know is a compile error that suggests the nearest one, so "ES265" gets "did you mean "ES256"?".

#Upgrading to 0.32

Run your code with 0.32 first. Most of what changes shows up as a compile error that points at the line and shows it written the new way.

  • A function that uses ? returns Ok or Error on every path. Wrap its plain returns in Ok, and have its callers take the value out with ? or match. If it reaches its end without a return, it gives Ok(nil).

    import json
    
    fn port(text)
      let config = json.decode(text)?
      return Ok(config.port)        -- was: return config.port
    end
    
    print(port("{\"port\": 8080}"))  -- Ok(8080)
  • ?? on a Result gives the value inside, or the default when there is none:

    import json
    
    let config = json.decode("not json") ?? { port = 80 }
    print(config.port)              -- 80
  • _ throws a value away and can't be read back. To drop a Result on purpose, write let _ = save(x).

  • Crypto calls that returned a value now return Ok(value). Take the value out with ? or match. Where you showed an Error as {e}, show {e.message}:

    import crypto
    
    let id = crypto.random_bytes(16)?   -- was: crypto.random_bytes(16)
    print(len(id))                      -- 16
  • A function whose answer is a deliberate mix, such as true, nil or an Error, keeps it when you declare it -> any:

    fn check(name) -> any
      if name == "" then return Error("no name") end
      if name == "root" then return true end
      return nil
    end
    
    print(check("root"), check("ada"), check(""))  -- true nil Error(no name)
  • If your project needs 0.32, say so in scua.toml, and an older scua asks to be upgraded before it reads your code:

    [package]
    name = "report"
    scua-version = "0.32"

    A package that supports 0.31 and 0.32 at once can follow Supporting scua 0.31 and 0.32 in one package.

  • If you embed SCUA, scua_eval and scua_eval_file return SCUA_ERR_FAILED when the script reports a failure itself, through sys.fail, a non-zero sys.exit or a top-level return Error. The reason is in errbuf.

  • If you save sessions, let parked turns finish before you upgrade. A turn parked inside a function that uses ?, ?? or _ is discarded when 0.32 loads the session, because that function now compiles differently.

  • If you compare a program's output with a saved copy and it has more than one partition, add --workers=1 to that run.

#What's next

Next, your own record types get operators, so a vector or a unit type you write can use + and < the way the built-in ones do. Big savings in memory use are on the way too.

The full changelog has everything in 0.32.0.