prelude

prelude lib/builtins.mu

builtins is the Mu-written layer of utility functions available globally.

Imports

Functions

bool#

fn bool(value)

bool(value) converts a value to a boolean.

Relocated from the host builtin table (see issue #46). This is the whole implementation: truthiness is a LANGUAGE rule, not a host capability, so if already knows it -- including the __ops["bool"] dispatch that makes a zero decimal falsey. The builtin was six hand-written copies of a rule the compiler enforces anyway.

Source lib/builtins.mu:39
fn bool(value) {
  if value {
    return true
  }
  return false
}

exit#

fn exit(args...)

exit exits the process with the given status code.

Source lib/builtins.mu:378
fn exit(args...) {
    status := 0
    if len(args) == 1 {
      if type(args[0]) != "INTEGER" {
        return error("exit status must be an integer")
      }
      status = args[0]
    }
    if status < 0 {
      return error("exit status must be non-negative")
    }
    if env("MU_TEST_NO_EXIT") != nil {
      __mu_test_signal_failure("exit")
      return error("exit")
    }
    return syscall("exit", status)
}

has#

fn has(m, key)

has(m, key) reports whether the map contains the key.

Relocated from the host builtin table (see issue #46). Scanning keys() is what distinguishes an absent key from one whose value is nil -- m[key] alone cannot, because nil is a legal element.

This is O(n) where the builtin was O(1). That is the honest cost of the move; see the note in docs/BuiltinReductionPlan.md.

Source lib/builtins.mu:112
fn has(m, key) {
  if type(m) != "MAP" {
    return error("has expects map as first argument, got " + type(m))
  }
  if !_hashable(key) {
    return error("unusable as hash key: " + type(key))
  }
  // A non-nil value proves the key is present, and indexing a map is a hash
  // lookup on every backend now. Only a nil answer -- absent, or present holding
  // nil -- has to walk the keys, which is the one question indexing cannot
  // settle. Walking is not cheap: keys() builds a list of every key in the map,
  // so has() on a 2,000-key map cost 38 MICROSECONDS per call against about 50
  // nanoseconds for the lookup below.
  if m[key] != nil {
    return true
  }
  ks := keys(m)
  i := 0
  while i < len(ks) {
    if ks[i] == key {
      return true
    }
    i = i + 1
  }
  return false
}

input#

fn input(args...)

input reads a single line from stdin. An optional prompt is written first.

Source lib/builtins.mu:265
fn input(args...) {
  if len(args) > 1 {
    return error("input expects at most one argument")
  }
  if len(args) == 1 {
    if type(args[0]) != "STRING" {
      return error("input prompt must be a string")
    }
    wrote := _stdout_write(args[0])
    if is_error(wrote) {
      return wrote
    }
  }
  return _stdin_read_line()
}

insert#

fn insert(coll, index, value)

insert(coll, index, value) inserts value at index, shifting the tail up.

Relocated from the host builtin table (see issue #46). Expressible because append() MUTATES its target in place for both lists and buffers, so growing by one and shifting is a real in-place insert rather than a rebuilt copy -- which is also why pop() and del() cannot make the same trip: nothing in mu can SHRINK a collection.

The checks are ordered the way the builtin ordered them: index type before collection type, so insert(5, "x", 1) still complains about the index.

Source lib/builtins.mu:181
fn insert(coll, index, value) {
  if type(index) != "INTEGER" {
    return error("insert expects integer index, got " + type(index))
  }
  kind := type(coll)
  if kind != "LIST" && kind != "BUFFER" {
    return error("insert expects list or buffer as first argument, got " + kind)
  }
  length := len(coll)
  if index < 0 || index > length {
    return error("index out of bounds: " + str(index))
  }
  // append does the byte-range validation for buffers, so a bad value is
  // rejected here before anything has shifted.
  grown := append(coll, value)
  if is_error(grown) {
    return grown
  }
  i := length
  while i > index {
    coll[i] = coll[i - 1]
    i = i - 1
  }
  coll[index] = value
  return value
}

int#

fn int(args...)

int(value) coerces a value to an integer.

Relocated from the host builtin table (see issue #46). Accepts no argument (0), an integer, a boolean, nil, or a base-10 numeric string with an optional sign -- matching strconv.ParseInt(s, 10, 64), which is what the builtin used.

Source lib/builtins.mu:144
fn int(args...) {
  if len(args) > 1 {
    return error("wrong number of arguments. got=" + str(len(args)) + ", want=0 or 1")
  }
  if len(args) == 0 {
    return 0
  }
  value := args[0]
  kind := type(value)
  if kind == "INTEGER" {
    return value
  }
  if kind == "BOOLEAN" {
    if value {
      return 1
    }
    return 0
  }
  if kind == "NULL" {
    return 0
  }
  if kind != "STRING" {
    return error("int expects integer, boolean, null, or numeric string, got " + kind)
  }
  return _parse_int10(value)
}

is_error#

fn is_error(value)

is_error reports whether the value is an ERROR value. Errors travel as ordinary values in mu, so this test is the seam of every error-handling path; giving it a global name keeps that path to one short word.

Source lib/builtins.mu:49
fn is_error(value) {
  return type(value) == "ERROR"
}

must#

fn must(value)

must yields the value, or panics when it is an ERROR.

conn := must(sockets.dial(addr))

This is a plain function rather than a macro, and deliberately so. A macro is for arguments that must NOT be evaluated, for control flow that has to land in the caller's frame, or for information that exists only at compile time. must needs none of those: its argument is an ordinary value the caller has already evaluated, and panic ends the process, so there is nothing left to do in the caller's frame that a call cannot do. Written as a macro it would expand to fn() { … }() -- which is a function, spelled the long way.

Use it where a failure is not worth recovering from. Inside a fallible helper, use try below, so the error reaches a caller that can still decide.

Source lib/builtins.mu:410
fn must(value) {
  if is_error(value) {
    panic(value)
  }
  return value
}

panic#

fn panic(args...)

panic writes a panic message and a traceback to stderr and exits (or raises a test failure).

When the panic carries an ERROR, the traceback shown is the one the error captured when it was CREATED, not the stack standing here. Those are rarely the same place: mu errors are values, so an error is typically returned up through several frames before a must or a bare panic decides it is fatal, and by then the frame that produced it is gone. The stack at the panic is usually two frames of prelude; the stack at the error is the answer.

Source lib/builtins.mu:345
fn panic(args...) {
  text := "panic"
  if len(args) > 0 {
    text = inspect(args[0])
  }
  _stderr_write(text)
  _stderr_write("\n")
  _stderr_write(_panic_traceback(args))
  if env("MU_TEST_NO_EXIT") != nil {
    __mu_test_signal_failure(text)
    return error(text)
  }
  return exit(1)
}

print#

fn print(args...)

print writes its arguments to stdout separated by spaces and followed by a newline.

Source lib/builtins.mu:243
fn print(args...) {
  i := 0
  length := len(args)
  while i < length {
    if i > 0 {
      _stdout_write(" ")
    }
    text := inspect(args[i])
    part := _stdout_write(text)
    if is_error(part) {
      return part
    }
    i = i + 1
  }
  newline := _stdout_write("\n")
  if is_error(newline) {
    return newline
  }
  return nil
}

test#

fn test(name, body)

test registers a test case and runs it when selected by MU_TEST_RUN.

Source lib/builtins.mu:361
fn test(name, body) {
  if type(name) != "STRING" {
    return error("test name must be a string")
  }
  if !_is_function(body) {
    return error("test body must be a function")
  }
  __mu_test_register(name)
  target := env("MU_TEST_RUN")
  // Skip tests that do not match when running a single test
  if type(target) == "STRING" && target != name {
    return nil
  }
  return body()
}

values#

fn values(m)

values(m) returns the map's values, in the same order as keys(m).

Relocated from the host builtin table (see issue #46). Building it on keys() is not just shorter, it is MORE CORRECT than the builtin was: the Go VM's values() iterated the pair map in Go's randomised order while keys() iterated it separately, so values(m)[i] was frequently not m[keys(m)[i]] -- 665 mismatches in 1000 (issue #44). One traversal cannot disagree with itself.

Source lib/builtins.mu:90
fn values(m) {
  ks := keys(m)
  if is_error(ks) {
    return error("values expects map, got " + type(m))
  }
  out := []
  i := 0
  while i < len(ks) {
    out = append(out, m[ks[i]])
    i = i + 1
  }
  return out
}

Macros

try#

macro try(name, expr)

try binds name to the value of expr and propagates an ERROR out of the enclosing function.

try(chunk, sockets.read(conn, remaining))

expands to

chunk := sockets.read(conn, remaining)
if is_error(chunk) { return chunk }

This one has to be a macro, where must does not: the return belongs to the CALLER's function, and a call cannot return on its caller's behalf. Binding the name is what makes the guard un-forgettable -- there is no way to reach the value without passing the check, which is the omission this replaces.

Every statement here is ordinary code inside one quote. The binding used to be built with __code_make, because the parser refused unquote name := …; it no longer does (#97). Note the target is NOT a hygiene concern: name arrives through unquote, so it is the caller's own node and carries no quote mark either way. Hygiene would only bite if the macro invented the name itself.

This is the language's only Mu-builtin macro. Its macro-phase registration is ambient, while free names inside the expansion resolve through this module's hidden binding so is_error keeps the scope it had here (#93).

Source lib/builtins.mu:441
macro.define("try", fn(name, expr) {
  return quote {
    unquote name := unquote expr
    if is_error(unquote name) {
      return unquote name
    }
  }
})

Internal helpers

Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.

_hashable(value)_hashable reports whether a value may be used as a map key.
_is_function(value)
_panic_traceback(args)_panic_traceback picks the traceback a panic should report.
_parse_int10(text)_parse_int10 mirrors strconv.ParseInt(text, 10, 64): an optional sign then one or more decimal digits, nothing else -- no whitespace, no underscores.
_stderr_write(text)
_stdin_read_line()
_stdout_write(text)