gotchas — what you will guess wrong on your first try
Mix has almost no presence in any model's training data, so a new session extrapolates from bash, Python and JavaScript — and several of those guesses are silently wrong: they run, produce a value, and mean something else. This page is the list of them.
Read it before writing Mix. It is the one page that pays for itself in a
single session. mix man syntax is the mental model; this is the errata.
Every row below is executed by cargo test -p system --test man_gotchas,
which runs the probe column through mix -c and compares the prints
column. A wrong row fails the build, so the page cannot rot into confident
fiction.
The table
A prints cell starting with ! means the probe fails, and that text
must appear in the error.
| you will guess | Mix is | probe | prints |
|---|---|---|---|
| a bare name reads the variable | a bare word is a string literal — $ is not optional, and omitting it is silent | $x = 5; print(x) | x |
null (or None, undefined) is null | it is a bare word, so it is the string "null"; Mix's null is nil (mix lint warns, MIX-W2312, and --fix rewrites it) | print(json_encode({a: null})) | {"a":"null"} |
let i = 0 declares a variable | Mix has no let, var or const: write $i = 0 (and $i = $i + 1, never a bare i = …) | let i = 0 | !$i = |
"hi $name" interpolates | only ${...} interpolates; a bare $name in a string is literal text | $n = "world"; print("hi $n") | hi $n |
| — so use braces | ${name} — and it is the same rule everywhere a string is built, heredocs included | $n = "world"; print("hi ${n}") | hi world |
elseif | elif | if 1 == 2 then print("a") elif 1 == 1 then print("b") end | b |
str(x) | to_string(x) | print(to_string(5)) | 5 |
print(a, b) | print is a statement, not a function, so (a, b) is read as an expression and fails | print("a", "b") | !expected RParen |
| — so how do I print two things | print a, b — no parens, joined by a space; print(x) only works because (x) is parenthesised | print "a", "b" | a b |
run() returns a status | run() raises on a non-zero exit | print(run("false")) | !failed (rc=1) |
| — so how do I get the code | run_rc() returns {rc, stdout, stderr, …} | print(run_rc("false").rc) | 1 |
read_file() returns nil when missing | it raises; test with is_file() first | print(read_file("/no/such/file")) | !No such file or directory |
push mutates any list | push writes through the slot; on a nested list it is a silent no-op | $m = {a: [1]}; push($m.a, 2); print($m) | {a: [1]} |
| — so how | read it out, push, store it back | $m = {a: [1]}; $i = $m.a; push($i, 2); $m["a"] = $i; print($m) | {a: [1, 2]} |
a declared fn name is a value | a lambda is a real value, but a declared fn's bare name is the string "f" | fn f($x) return $x end; $g = f; print($g) | f |
| — so how do I pass one | assign a lambda: $f = fn($x) = …, which is a function and works in map/filter | $f = fn($x) = $x + 1; print(map([1, 2], $f)) | [2, 3] |
a fn can write an outer variable | assignment inside fn binds a local; return the value instead | $n = 1; fn bump() $n = 99 end; bump(); print($n) | 1 |
catch $e gives an error object | $e is the message string; use catch $msg, $err for $err.code | try raise("E_X", "boom") catch $m, $e print($e.code) end | E_X |
raise("boom") | raise(CODE, MESSAGE) — one argument is an arity error | try raise("E_X", "boom") catch $m print($m) end | boom |
json_decode() | json_parse() (and json_encode() the other way) | print(json_parse("{\"a\":1}").a) | 1 |
== compares two maps or lists | it raises rather than answer a useless false | print([1, 2] == [1, 2]) | !would always answer false |
| — so how do I compare them | deep_eq(a, b) | print(deep_eq([1, 2], [1, 2])) | true |
re_replace(pattern, s, ...) | subject first: re_replace(s, pattern, replacement) | print(re_replace("a1b", "[0-9]", "#")) | a#b |
sort_by takes a direction | ascending only, with a lambda; reverse() for descending | print(sort_by([3, 1, 2], fn($x) = $x)) | [1, 2, 3] |
'~/x' expands | a single-quoted string is raw — no ~, no ${...} | print('~/x') | ~/x |
— and "~/x"? | a double-quoted one expands ~ to $HOME | print(starts_with("~/x", "/")) | true |
send svc-name verb needs quoting | it does not — a tight-hyphenated bare target is read whole, like the quoted and $var forms | send zz-no-such-svc ping timeout=1; print(type($result)) | string |
— but send a - b verb? | a space makes it subtraction again, and a bareword is a string, so that is a type error | send a - b ping timeout=1 | !as number |
replace() tells you it missed | it returns the input unchanged, silently — check the result, or use mix edit from a prompt | print(replace("abc", "zz", "!")) | abc |
replace() replaces the first | it replaces all of them | print(replace("a a a", "a", "b")) | b b b |
mix -c 'print(x)' needs escaping gymnastics | it does not; a probe is one call and the binary is the oracle | print(mix_version() != "") | true |
a provable -c arity/dead-mutation snippet runs | the D1 lint gate REFUSES it with exit 2 before any line runs — --no-lint overrides (0.103.4) | mix -c '$m = {}; remove($m, "k")' | !refusing to run |
is_reload_candidate() is true while a --serve RELOAD is pending | it is true only while the replacement's init body executes — false in a plain script, at initial boot, and in every committed generation; the candidate must stay passive (no starts, no persisted writes) and commit-time work belongs in on lifecycle.commit | print(is_reload_candidate()) | false |
on $svc .. ".call" registers a verb under the --serve name | on takes a static name (that is a parse error); write on @.call — the unquoted @ becomes the serve name when the handler registers, and outside --serve it raises SERVE_PREFIX_OUTSIDE_SERVE | try on @.call end catch $m, $e print($e.code) end | SERVE_PREFIX_OUTSIDE_SERVE |
Three rules that are not a syntax trap
These cost more than any row above, and no probe can catch them.
- Never
sed -i, never Python. A one-line edit ismix edit FILE OLD NEW— exact match, refuses an absent or ambiguous needle (see the mix command). A script is a.mixfile. If Mix genuinely cannot do something, that is a reportable gap in Mix, not a licence to reach for another language. - Never
ssh host 'bash -c "…"'. Usessh_mix(host, source, {bindings: {…}})— it ships the source over ssh stdin intomix -, bypassing all shell quoting, andbindingspasses values in as$nameassignments instead of string-interpolating them. Quoting a remote loop by hand fails in ways that look remote and are local. See remote execution. - Probe, do not extrapolate.
mix -c '<code>'answers in under a second and the binary outranks every document, including this one.mix builtins NAMEgives the exact signature,mix what NAMEthe one-liner,mix lint FILEthe call-contract check before you run anything non-trivial.
See also
- syntax & the classifier — the mental model these are errata to.
- the mix command —
mix edit,mix lint,mix what,mix man. - errors & exit handling — the full
try/catch/raisemodel. - collections — why
pushwrites through a slot.