Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scopes & Bindings

Scopes are the backbone of milang’s structure. Every indented block and every brace-delimited block creates a new scope with its own bindings.

Basic Bindings

name = expr            -- eager binding
name := expr           -- lazy binding (thunk, evaluated at most once)
name params = expr     -- function binding
x = 42
double x = x * 2
result = double x
build =  {target = c, os = linux, arch = x86_64}
x = 42
double = <closure>
result = 84

Indentation-Sensitive Scoping

Indented lines beneath a binding form a scope. There are two modes depending on whether an explicit result expression appears after =.

Explicit Body

When a binding has an expression directly after =, that expression is the scope’s return value. The indented children are local definitions visible only inside that scope:

compute x = result
  doubled = x * 2
  result = doubled + 1
a = compute 7
build =  {target = c, os = linux, arch = x86_64}
compute = <closure>
a = 15

Here doubled and result are local to compute. The value of compute 7 is the expression after =, which is result (15).

Implicit Record (Scope-as-Record)

When a binding has no expression after = — only indented children — the named bindings are collected into a record and returned automatically:

makeVec x y =
  sum = x + y
  product = x * y
v = makeVec 3 4
build =  {target = c, os = linux, arch = x86_64}
makeVec = <closure>
v =  {sum = 7, product = 12}

makeVec 3 4 returns {sum = 7, product = 12}. This is milang’s lightweight alternative to explicit record construction.

Inner Scopes Shadow Outer Names

A binding in an inner scope shadows any identically-named binding from an enclosing scope. The outer binding is unaffected:

x = 10
f = result
  x = 99
  result = x + 1
outer = x
inner = f
build =  {target = c, os = linux, arch = x86_64}
x = 10
f = 100
outer = 10
inner = 100

Inline Scopes (With Blocks)

Braces create an inline scope on a single line. The expression before the braces is the return value, and the bindings inside are local:

f x = result { doubled = x * 2; result = doubled + 1 }
a = f 7
build =  {target = c, os = linux, arch = x86_64}
f = <closure>
a = 15

Bare Expressions (Effect Statements)

A bare expression in a scope — one not bound to a name — is evaluated for its side effect. Its result is discarded and not included in any implicit record:

main world =
  world.io.println "hello"    -- effect, result discarded
  world.io.println "world"    -- effect, result discarded
  0                           -- explicit body (exit code)

The first two lines run println for their side effects. The final 0 is the return value of main.

The main Function Pattern

A typical main combines all three concepts — local bindings, bare effect expressions, and an explicit result:

main world =
  name = "milang"                      -- local binding
  world.io.println ("Hello, " + name)  -- bare effect
  0                                    -- return value (exit code)

Binding Order

  • Bindings evaluate top-to-bottom (left-to-right in brace scopes).
  • Later bindings may reference earlier ones.
  • The compiler tracks impure (world-tainted) bindings and guarantees they execute in declaration order via an auto-monad spine.
  • Pure bindings can theoretically be reordered by the optimiser.