A partition now works through several messages in a turn, so one partition collecting from twenty others handles a million messages in about 0.4 seconds. The JIT now compiles loops over number arrays that branch and flip signs, and network connections can give up after a time limit. A record type you declare is checked at every level, whichever file declared it. A few changes are worth reading before you upgrade.
#Partitions now handle several messages per turn
Twenty partitions each send 50,000 messages to one collector, which checks that every sender's messages arrive in order:
partition Collector
state last = []
state got = 0
state bad = 0
on Start(n) last = fill(n, 0) end
on Item(s, k)
if k != last[s] + 1 then bad = bad + 1 end
last[s] = k
got = got + 1
if got == 1000000 then print(`{got} messages, {bad} out of order`) end
end
end
partition Sender
on Run(c, s)
for k in 1:50001 do tell c.Item(s, k) end
end
end
let c = Collector()
tell c.Start(20)
for s in 0:20 do
let p = Sender()
tell p.Run(c, s)
end
1000000 messages, 0 out of order
The whole program takes about 0.4 seconds on an M4 Max laptop. A partition with mail waiting keeps taking messages until it has worked through a batch or a handler has to wait for something, and only then gives the next partition a turn. That is the shape of a busy server, where one partition hears from many others. Messages from one sender still arrive in the order they were sent.
#The JIT now speeds up loops over number arrays
This loop moves 100,000 particles for 200 steps and bounces any that leave the box:
let n = 100000
let w = { x = [], v = [] }
for i in 0:n do
w.x.push(i * 0.5)
w.v.push(((i * 7919) % 200 - 100) * 0.01)
end
for step in 0:200 do
for i in 0:n do
let v = w.v[i]
if w.x[i] > 50000.0 or w.x[i] < 0.0 then v = -v end
w.v[i] = v
w.x[i] = w.x[i] + v
end
end
let total = 0.0
for i in 0:n do total = total + w.x[i] end
print(`average position {total / n}`)
average position 24998.755952
With the JIT on it runs in 0.02 seconds on the same laptop, about 25
times faster than with the JIT off. The arrays can sit in a table field,
as they do here and as a partition's state arrays do, and
the loop body can be mostly one if. The JIT compiles the
loop even when the branch inside has not run yet, which is the usual
case for a bounce or an error check.
#Network connections now have time limits
Connecting gives up after 30 seconds by default, and that includes
the TLS handshake. timeout_ms sets a different limit for
one call, and 0 asks for none. Reads and writes wait for as
long as it takes, because some connections are meant to stay idle for a
long time, until you set a limit on the connection with
net.set_timeout:
import net
fn fetch(host, port)
let conn = net.dial(host, port, { timeout_ms = 2s })?
net.set_timeout(conn, 500ms)?
net.write(conn, "PING\r\n")?
return net.read(conn, 1024)
end
match fetch("127.0.0.1", 47123)
Ok(reply) -> print(`reply: {reply}`)
Error(why) -> print(`gave up: {why}`)
end
Against a server that accepts the connection and never answers:
gave up: net.read: timed out after 500ms
Running out of time is an ordinary error you can handle, and a connection whose read timed out can still be used.
#Records now check every level of the types you declared
A module declares an invoice, with a rule on each of its lines:
-- invoice.scua
record Line { sku: string, qty: int where qty > 0 }
record Invoice { id: string, lines: { Line } }
return {}
A program that decodes JSON into invoice.Invoice has
every line checked against that rule:
import invoice
import json
fn load(text)
let inv: invoice.Invoice = json.decode(text)?
return Ok(inv)
end
let good = "{\"id\": \"A-17\", \"lines\": [{\"sku\": \"pen\", \"qty\": 3}]}"
let bad = "{\"id\": \"A-18\", \"lines\": [{\"sku\": \"pen\", \"qty\": 3}, {\"sku\": \"ink\", \"qty\": 0}]}"
for text in [good, bad] do
try
match load(text)
Ok(inv) -> print(`{inv.id}: {len(inv.lines)} line(s)`)
Error(e) -> print(`not JSON: {e}`)
end
rescue e
print(e.message)
end
end
A-17: 1 line(s)
field 'qty' of invoice.Line violates its `where` constraint (got 0)
The check reaches every nested record, whether the value came from
JSON or was written out in code, and whether the types were declared in
this file, in the module you imported, or in a module that module
imports. A module's record can also be given a local name with
type Line = invoice.Line, and an enum case that carries one
of a module's records can be built from any file.
A parameter with a declared type is now checked while the program
runs as well. fn f(x: int) stops at the call if
x arrives holding a float or a string, however it got
there.
#Smaller additions
hash.hmac computes the keyed signature behind two-factor
login codes, JWT and webhook signatures, with SHA-1, SHA-256, SHA-384 or
SHA-512. This is the RFC 6238 test code, computed from its published
secret:
import hash
import bytes
import math
-- The RFC 6238 test secret, 59 seconds after the epoch: counter 1.
let mac = hash.hmac("sha1", "12345678901234567890", bytes.from_array([0, 0, 0, 0, 0, 0, 0, 1]))
let off = mac[19] & 15
let code = ((mac[off] & 127) << 24) | (mac[off + 1] << 16) | (mac[off + 2] << 8) | mac[off + 3]
print(code % 100000000)
print(math.checked_add(9223372036854775807, 1))
94287082
Error(math.checked_add: the result does not fit in an int)
Int arithmetic wraps around at the edge of its range.
math.checked_add, checked_sub and
checked_mul return an error when the result does not fit,
for sizes and offsets that come from a file or the network.
math also has log1p, expm1,
nextafter and float_epsilon.
Slicing and searching a bytes value costs only the part
you ask for, so a large buffer can be read piece by piece, and
bytes.u8 reads a single byte alongside u16le
and the other fixed-width readers.
str.upper, str.lower and the letter tests
now work in every script: str.lower("ÉCOLE") is
"école". For words a protocol defines in ASCII, such as
header names and HTML tags, use the new str.ascii_lower and
str.ascii_upper, which change only A to Z.
#What changes when you upgrade
- A float that holds a whole number now seeds
rand.seedexactly like the int, sorand.seed(1.0)gives the same numbers asrand.seed(1). It used to give an unrelated sequence. A fraction, NaN or infinity is now an error. str.upper,str.lower,str.titleand the letter tests used to change and recognise English letters only. They now handle every script, and a few letters fold onto plain ASCII ones:İlowercases toi. Compare protocol words withstr.ascii_lower.- A parameter's declared type used to be checked only at calls the
compiler could see. It is now checked while the program runs. Caught
with
try, the error is a record, so reade.message. - A call to a function that a module re-exports used to compile with
arguments missing, and the missing ones were
nil. It is now checked like any other call. To keep short calls working, give the trailing parameters defaults:fn text(s, status = nil). - A partition saved with
actors.freezeor a hibernation snapshot by 0.28.0 cannot be loaded by 0.29.0, and the attempt says so. Durable saves are not affected. - Inside
filterandwith, a variable of your own that is also a column name used to be read as the variable. It is now an error that names both spellings; writerow.regionfor the column. - Declaring the same
statename twice in one partition used to compile, and the second replaced the first. It is now an error. - A program holding nested data its own record types say is invalid
used to run. It now stops with the
whereerror. - The order in which output from different partitions interleaves may differ from 0.28.0, which never promised one.
#What's next
Partitions share nothing and talk only by message, and that is what lets them run on several cores at once. Making that the default is next, once this release has had some real use.
The full changelog has the rest.