SCUA 0.31.0 makes a tool you wrote easy to hand to someone else. One
command packs your scripts and the packages they use into a single file,
and scua runs that file directly on macOS, Linux and
Windows. This release also adds AES-192 and crypto.pbkdf2,
lets a project say which scua it needs, and brings the compiler's first
warnings. A few changes are worth reading before you upgrade.
#Packaging your SCUA code
In many languages, sending someone a script together with its dependencies means a separate bundling tool, and often a build for each operating system. In SCUA it is one command and one file.
Here is a small tool, spell, that points out the words
in a file that aren't in your word list and suggests the closest one
that is. It has a module of its own, lib/words.scua, and
uses one package, @scua/fuzzy, added with
scua-pkg add:
-- spell: point out words that aren't in your word list, and the closest one that is.
import fs
import sys
import fuzzy
import lib/words
let args = sys.args()
if args.len() != 2 then
print("usage: spell WORDLIST FILE")
sys.exit(1)
end
fn read(path)
match fs.read_text(path)
Ok(text) -> return text
Error(e) -> sys.fail(`spell: {path}: {e}`)
end
end
let known = words.words(read(args[0]))
let seen = {}
for w in known do seen[w] = true end
for w in words.words(read(args[1])) do
if not seen[w] then
match fuzzy.best(w, known, { cutoff = 0.7 })
Ok(m) -> print(`{w} -> {m.value}?`)
Error(_) -> print(`{w} -> ?`)
end
end
end
In the project folder, one command builds the bundle:
$ scua-pkg bundle --app main.scua
bundled app `spell` (entry main.scua): 2 app file(s), 1 package(s) with 11 file(s) into spell.scuapp (67819 bytes)
needs: fs — grant them when you run it: `scua-pkg run [--allow-…] spell.scuapp`
spell.scuapp is the whole tool, about 68 KB. Copy it to
another machine that has scua installed and run it there
like a script, with the tool's own arguments after it:
$ scua --allow-fs=. spell.scuapp words.txt note.txt
quik -> quick?
recieve -> receive?
seperate -> separate?
parcel -> ?
We ran that same file, byte for byte, on a Mac with Apple silicon, on
x86-64 Linux and on x86-64 Windows, and got the same four lines on each.
The other machine needs scua and nothing else: no project
folder, no deps/ and no package manager. The packages
travel inside the bundle. A package that calls a native library through
ffi still needs that library on the machine.
Before anything compiles, scua checks the whole file
against its digest and every file inside against its own, and runs
nothing if one doesn't match. A bundle grants itself nothing.
Capabilities come from your command line, as they do for a
.scua file, and when one is missing, the message says which
flag to pass:
$ scua spell.scuapp words.txt note.txt
scua: spell.scuapp:main.scua:2: this app needs capabilities this run was not granted: fs. Grant them before the file: `scua --allow-fs spell.scuapp` (or narrower: --allow-fs=DIR)
Building a bundle takes scua-pkg 0.3.20 or later. Ship a tool as one
file has the details.
#crypto now supports AES-192 and PBKDF2
Every AES call now does AES-192. Counter mode, GCM and key wrap take a 24-byte key as well as a 16- or 32-byte one, and CBC with HMAC takes a 48-byte key for JWE's A192CBC-HS384.
crypto.pbkdf2 turns a password into a key, as encrypted
zip, PDF and most password stores do. Here it makes a 24-byte key for
AES-192:
import crypto
import bytes
fn seal(password, text)
let salt = crypto.random_bytes(16)
let key = crypto.pbkdf2("sha256", password, salt, 600000, 24)?
let nonce = crypto.random_bytes(12)
return Ok({ salt = salt, nonce = nonce, sealed = crypto.aes_gcm_encrypt(key, nonce, text) })
end
fn open(password, box)
let key = crypto.pbkdf2("sha256", password, box.salt, 600000, 24)?
let plain = crypto.aes_gcm_decrypt(key, box.nonce, box.sealed)?
return bytes.to_string(plain)
end
fn try_password(password, box)
match open(password, box)
Ok(text) -> print(`opened: {text}`)
Error(e) -> print(`refused: {e}`)
end
end
let box = match seal("correct horse battery staple", "the vault code is 7741")
Ok(b) -> b
Error(e) -> error(e)
end
try_password("correct horse battery staple", box)
try_password("Correct horse battery staple", box)
opened: the vault code is 7741
refused: the data does not authenticate: it was changed, or the key, nonce or extra data is not the one it was encrypted with
pbkdf2 prepares the HMAC key once per call, which makes
it about twice as fast as a textbook version that prepares it on every
iteration. 600,000 iterations of SHA-256 take about 30 milliseconds on
an Apple M4 Max.
It returns Ok(key) or an Error, and so does
crypto.aes_ctr. A file you decrypt sets its own iteration
count, key size and counter block, so when one of those is wrong you get
an Error to match on and can go on to the next file.
#Projects can now say which scua they need
Put scua-version under [package] in
scua.toml:
[package]
name = "report"
scua-version = "0.32"
A scua older than that stops before it reads any of your code, and exits with status 3:
$ scua main.scua
scua: this project needs scua 0.32 or later (scua-version in its scua.toml); this is 0.31.0. Upgrade scua.
Packages declare it the same way, and scua-pkg 0.3.20
copies their requirements into your lock, so a package that needs a
newer scua is named in the message. A script with no
scua.toml is never checked.
#The compiler can now warn
A warning prints on stderr and the program still runs. The first one
catches tonumber given a table, a record, an array or a
bool, which always gives nil:
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
The fix is tonumber(row.qty). A run now lists every
compile error and warning it finds, so you can fix a file in one pass.
For CI, --deny-warnings makes a warning stop the run before
anything starts:
$ scua --deny-warnings 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
scua: nothing ran: --deny-warnings makes a warning fail the run
Warnings from the packages you depend on are hidden unless you pass
--dep-warnings, since only an upgrade can fix them.
#Smaller additions
- A misspelt call to a built-in module is caught before the program
runs, with a suggestion:
scua: typo.scua:2: type error: `str` has no function `uper` (did you mean `str.upper`?). json.decodesays where the input went wrong:Error(invalid JSON at line 2, column 7).- An
http.servehandler that uses?can end withreturn Ok({ status = 200, body = … }), and a proxy can return whathttp.requestgave it. - A
{…}hole in a backtick string can hold any string or template, braces and all:`{str.join(names, "}, {")}`givesana}, {bo. - Zed and VS Code highlight strings, escapes and template holes the way the compiler reads them.
- When you can't run the debugger,
--debug-valuesputs the value anOkheld back into fault messages.
#What changes when you upgrade
crypto.aes_ctrreturnsOk(bytes)orError(reason), where it used to return the bytes. Inside a function, add?. Elsewhere,matchthe result.- An
askthat times out givesError({ kind = "timeout", message = "ask timeout" }), where it used to giveError("ask timeout"). Checke.kind == "timeout". - A handler or
actor(fn, state)function that ends with anErrornow keeps the old state. To store anErroras the state, wrap it:return { last = Error(e) }. - A save frozen while an actor held an
Erroras its state may not load. Revive from an earlier save. - Fault messages say
got an Okwhere they used to print what theOkheld.scua test, the debugger and--debug-valuesstill show it. - A call to a function a built-in module doesn't have is a compile error, even on a line that never runs.
- A Result used as a condition is now caught before the program runs
when the function comes from another module. Test it with
matches Ok, ormatchit. json.decode'sErrortext gains a position. Test it withstr.starts_with(e, "invalid JSON").--max-opscounts every call as well as every loop iteration. A script that finished just under a tight budget may now reach it.
#What's next
More warnings are on the way, along with an option to turn compile warnings off, so a tool or an agent can keep its log to what it needs. After that, the compiler will tell you before a program runs when the Result a call returns is never looked at.
The full changelog has the rest.