What's new in v3.7
Every feature on this page is implemented in the interpreter and covered by the
automated test suite — the examples below are executed by
tests/lang_features_test.sh and tests/http_client_test.sh
on every make test run.
Function arity is safe: RUNTIME010
Calling a function with the wrong number of arguments used to read past the end of the argument array (AddressSanitizer measured an 8-byte heap-buffer-overflow) and could crash the interpreter. It is now a controlled diagnostic that names the function, both counts, and the call site:
fn add(a, b, c) {
return a + b + c
}
print(add(1, 2))
error[RUNTIME010]: wrong number of arguments
--> example.mz:4:11
4 | print(add(1, 2))
| ^
|
reason: function 'add' expects 3 arguments, got 2
docs: https://crossberry-in.github.io/mozhi-doc/errors/RUNTIME010
The same validation covers nested and recursive calls, anonymous
fn expressions, constructors, and module-member functions. Too many
arguments are rejected too (expects at most 2 arguments, got 4).
Default parameter values
fn greet(name, greeting = "Hello") {
return greeting + " " + name
}
print(greet("Mozhi")) # Hello Mozhi
print(greet("Mozhi", "Welcome")) # Welcome Mozhi
Semantics, verified by tests:
- Defaults are evaluated once per invocation, in parameter order, inside the
call environment — so a default may reference earlier parameters:
fn f(a, b = a * 2)withf(5)returns15. - Array, map and call-expression defaults work:
fn collect(vals = [1, 2], cfg = { mode: "fast" }). - An omitted argument is distinct from an explicit
null—g("M")uses the default,g("M", null)bindsnull. - A required parameter after a defaulted parameter is a parse error.
- Missing required arguments remain a RUNTIME010 error.
Variadic parameters: ...rest
fn log(level, ...items) {
print(level)
for item in items {
print(item)
}
}
log("INFO", "started", 10, true)
log("EMPTY") # zero collected arguments is fine
The rest parameter collects surplus arguments into a standard Mozhi array, must be
the last parameter, and only one is allowed. It composes with default values:
fn f(a, b = 2, ...rest).
Safe navigation ?. and null coalescing ??
let task = req_opts?.task
let model = config?.provider?.model ?? "default-model"
Defined semantics (all covered by regression tests):
a?.breturnsnullwhenaisnull; otherwise it behaves exactly likea.b.- A missing map key yields
null(consistent with map indexing), som?.missingisnullandcfg?.provider?.modelchains safely. left ?? rightreturnsleftunless it isnull—false,0and""are not null and are returned as-is.- The right-hand side is lazy: it is evaluated only when the left side is
null(verified with a side-effecting function). ??binds at the same precedence asor, left associative;?.binds like.at call precedence. No existing operator precedence changed.- Plain
.onnullstill raises RUNTIME004 — safe access is opt-in. - A lone
?produces a targeted illegal-character hint naming?.and??.
String method aliases
Backward-compatible aliases dispatch to the same implementation, so behavior and error messages are identical:
print("ABC".to_lower()) # abc (alias of lower())
print("abc".to_uppercase()) # ABC (alias of upper())
print("hello".length()) # 5 (alias of len())
| Canonical | Accepted aliases |
|---|---|
lower() | to_lower(), to_lowercase() |
upper() | to_upper(), to_uppercase() |
len() | length() |
Casing is byte-wise (ASCII), matching the existing upper()/lower()
implementations; multi-byte UTF-8 letters pass through unchanged. Calling an alias on a
non-string receiver reports the same type error as the canonical method.
64-bit integers and numeric semantics
Integers are now 64-bit signed. The classic accumulator sums are exact:
total = 0
i = 1
while i <= 1000000 {
total = total + i
i = i + 1
}
print(total) # 500000500000 (was 2147483647 on 32-bit builds)
Rules (documented in the CHANGELOG and covered by tests):
- Integer arithmetic is exact within the signed 64-bit range (−9,223,372,036,854,775,808 … 9,223,372,036,854,775,807).
+,-and*results that exceed the range promote to floating point (no silent wraparound).- Integer division by zero remains
RUNTIME002; the corner case
INT64_MIN / -1also promotes to float instead of trapping. - Integer literals beyond the range are a parse error; write
-9223372036854775807 - 1for the minimum. - Integral floats print with an explicit
.0(500000500000.0, Python-style) instead of5e+11; non-integral floats keep the compact%gform.
Recoverable HTTP client
Network failures no longer abort the program. Every
http.get / http.post / http.request returns
one consistent result shape:
import http
let res = http.request({
url: "http://127.0.0.1:8089",
timeout: 5, # total request timeout (seconds, default 15)
connect_timeout: 3 # TCP connect timeout (default 10)
})
if res.ok {
print(res.body)
} else {
print(res.error) # e.g. "Connection refused (connecting to 127.0.0.1:8089)"
}
print("still running") # a failed request never stops execution
The result map:
| key | success | transport failure | HTTP 404/500/… |
|---|---|---|---|
ok | true (status 200–299) | false | false |
status | HTTP status | 0 | HTTP status |
error | "" | failure reason | "" |
body | response body | "" | response body |
headers | response headers map | empty map | response headers map |
DNS failure, refused connection, timeouts, unreachable hosts and TLS errors all
flow through this schema; a failed request does not affect later independent
requests. Non-2xx statuses are responses, not exceptions — status,
body and headers are filled. method is now
optional and defaults to "GET". Responses are capped at 64 MB.
{ ok: false, ... } result. The downloadable release binaries are built
with zig and have no TLS stack — there https:// returns a
recoverable error telling you to use http:// or a TLS-enabled build.
No curl subprocess fallback is used.
Package registry freshness
mz search and mz install fetch the registry with
Cache-Control: no-cache plus a unique nocache query
parameter, so packages added to the registry are visible immediately instead of
waiting for CDN caches to expire. Registry fetches keep their 8-second timeout and
the remote → last-good-cache → bundled-snapshot fallback. This only affects
metadata freshness — package downloads and integrity checks are unchanged.
Performance
The evaluator was optimized with measurable, behavior-preserving changes
(object shell pooling, immortal null/true/false
singletons, reference-shared immutable scalars, interned environment names). The
numbers below come from the shipped bench/ suite — median of 7 runs,
warm cache, output verified before timing (x86_64 Linux, same algorithm in both
languages, Python 3.12.15 as reference):
| workload | 3.4 baseline | 3.7.0 | speedup | vs python3 |
|---|---|---|---|---|
| float loop, 1M iterations | 252.2 ms | 122.4 ms | 2.1× | 0.79× (faster) |
| integer sum, 1M iterations | 218.7 ms | 109.4 ms | 2.0× | 0.69× (faster) |
| counter loop, 1M iterations | 215.2 ms | 106.2 ms | 2.0× | 0.84× (faster) |
| function calls, 200k | 89.8 ms | 45.2 ms | 2.0× | 1.02× (level) |
| recursion, fib(20) | 9.9 ms | 6.1 ms | 1.6× | 0.48× (faster) |
| arrays, 100k push+sum | 43.2 ms | 23.2 ms | 1.9× | 0.67× (faster) |
| string concat, 20k | 24.6 ms | 14.2 ms | 1.7× | 0.85× (faster) |
| hello world (startup) | 2.9 ms | 2.8 ms | 1.0× | 0.22× (faster) |
Against the 1056 ms one-million-iteration float-loop baseline measured at the
start of this release cycle, the same benchmark now runs in about 122 ms — roughly
8.6× faster. (After-numbers are the final 3.7.0 tree, which also
carries the ecosystem work — reference-counted environments and by-reference
returns.) Reproduce with bash bench/run_bench.sh from the source
repository.
Known limitations
**evaluates through double-precision math; the result stays an integer while it fits in int64 and promotes to float beyond that.- String casing methods are byte-wise (ASCII); multi-byte UTF-8 letters are passed through unchanged (unchanged behavior, now covered by tests).
- DNS resolution has no portable timeout; a hung resolver can outlast the
configured
timeout(TCP connect, TLS handshake, and reads all do honor it). - Release (zig-built) binaries have no TLS;
https://requests there return a recoverable error. Source builds with OpenSSL have native TLS. - There is no bytecode VM yet — the interpreter remains a tree-walking evaluator, now with allocation and lookup costs reduced. Startup, parse, and evaluation remain on one execution path that is validated by the same test suite as before.