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-warningskeeps 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 withwarnings = falseunder[run]inscua.toml, and--warningsshows 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 0A money value has
.code()and.amount():let price = 19.99 USD print(price.code(), price.amount())$ scua price.scua USD 19.99Reading 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
?returnsOkorErroron every path. Wrap its plain returns inOk, and have its callers take the value out with?ormatch. If it reaches its end without areturn, it givesOk(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, writelet _ = save(x).Crypto calls that returned a value now return
Ok(value). Take the value out with?ormatch. Where you showed anErroras{e}, show{e.message}:import crypto let id = crypto.random_bytes(16)? -- was: crypto.random_bytes(16) print(len(id)) -- 16A function whose answer is a deliberate mix, such as
true,nilor anError, 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_evalandscua_eval_filereturnSCUA_ERR_FAILEDwhen the script reports a failure itself, throughsys.fail, a non-zerosys.exitor a top-levelreturn Error. The reason is inerrbuf.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=1to 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.