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 guessMix isprobeprints
a bare name reads the variablea bare word is a string literal — $ is not optional, and omitting it is silent$x = 5; print(x)x
null (or None, undefined) is nullit 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 variableMix has no let, var or const: write $i = 0 (and $i = $i + 1, never a bare i = …)let i = 0!$i =
"hi $name" interpolatesonly ${...} 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
elseifelifif 1 == 2 then print("a") elif 1 == 1 then print("b") endb
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 failsprint("a", "b")!expected RParen
— so how do I print two thingsprint a, b — no parens, joined by a space; print(x) only works because (x) is parenthesisedprint "a", "b"a b
run() returns a statusrun() raises on a non-zero exitprint(run("false"))!failed (rc=1)
— so how do I get the coderun_rc() returns {rc, stdout, stderr, …}print(run_rc("false").rc)1
read_file() returns nil when missingit raises; test with is_file() firstprint(read_file("/no/such/file"))!No such file or directory
push mutates any listpush 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 howread 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 valuea 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 oneassign 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 variableassignment 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.codetry raise("E_X", "boom") catch $m, $e print($e.code) endE_X
raise("boom")raise(CODE, MESSAGE) — one argument is an arity errortry raise("E_X", "boom") catch $m print($m) endboom
json_decode()json_parse() (and json_encode() the other way)print(json_parse("{\"a\":1}").a)1
== compares two maps or listsit raises rather than answer a useless falseprint([1, 2] == [1, 2])!would always answer false
— so how do I compare themdeep_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 directionascending only, with a lambda; reverse() for descendingprint(sort_by([3, 1, 2], fn($x) = $x))[1, 2, 3]
'~/x' expandsa single-quoted string is raw — no ~, no ${...}print('~/x')~/x
— and "~/x"?a double-quoted one expands ~ to $HOMEprint(starts_with("~/x", "/"))true
send svc-name verb needs quotingit does not — a tight-hyphenated bare target is read whole, like the quoted and $var formssend 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 errorsend a - b ping timeout=1!as number
replace() tells you it missedit returns the input unchanged, silently — check the result, or use mix edit from a promptprint(replace("abc", "zz", "!"))abc
replace() replaces the firstit replaces all of themprint(replace("a a a", "a", "b"))b b b
mix -c 'print(x)' needs escaping gymnasticsit does not; a probe is one call and the binary is the oracleprint(mix_version() != "")true
a provable -c arity/dead-mutation snippet runsthe 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 pendingit 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.commitprint(is_reload_candidate())false
on $svc .. ".call" registers a verb under the --serve nameon 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_SERVEtry on @.call end catch $m, $e print($e.code) endSERVE_PREFIX_OUTSIDE_SERVE

Three rules that are not a syntax trap

These cost more than any row above, and no probe can catch them.

  1. Never sed -i, never Python. A one-line edit is mix edit FILE OLD NEW — exact match, refuses an absent or ambiguous needle (see the mix command). A script is a .mix file. If Mix genuinely cannot do something, that is a reportable gap in Mix, not a licence to reach for another language.
  2. Never ssh host 'bash -c "…"'. Use ssh_mix(host, source, {bindings: {…}}) — it ships the source over ssh stdin into mix -, bypassing all shell quoting, and bindings passes values in as $name assignments instead of string-interpolating them. Quoting a remote loop by hand fails in ways that look remote and are local. See remote execution.
  3. Probe, do not extrapolate. mix -c '<code>' answers in under a second and the binary outranks every document, including this one. mix builtins NAME gives the exact signature, mix what NAME the one-liner, mix lint FILE the call-contract check before you run anything non-trivial.

See also

This page as Markdown · This page describes the current main branch of MixOS.