WalkLang Syntax Guide

This guide is the readable syntax companion to docs/SPEC.md. SPEC.md is the contract when the two disagree.

---

Files

WalkLang files use .walk.

main.walk
calc.walk
tests.walk

Comments use # outside strings.

# full-line comment
var: x = 5 # inline comment

---

Blocks

Blocks are indentation-based.

if: true
    out: 'inside'
out: 'outside'

Tabs are invalid. walk fmt emits 4 spaces for each block level.

---

Commands

Command statements use keyword:.

var: x = 1
const: limit = 10
out: x

Long expressions can use a block after a command.

out:
    +:
        1
        2
        3

Draft effect calls use do:. The expression after do: must be a draft effect function, not an ordinary value expression.

imp: io

do: io.write('Loading')
do: io.write_line('done')
do: io.error_line('warning')

Draft scope cleanup uses defer: with an explicit effect call:

imp: term

do: term.color('red')
defer: do term.reset()

defer: is draft. A deferred cleanup belongs to the lexical block that contains it, runs in last-in, first-out order at block exit, and captures call arguments when the defer: line is reached.

---

Names

Names use letters, digits, and _, and cannot be reserved words.

var: user_name = 'Walker'
var: score2 = 100

Invalid:

var: if = 1
var: 2score = 100

---

Literals

var: i = 10
var: f = 3.14
var: yes = true
var: no = false
var: name = 'Walker'
var: optional_name string? = null

Strings are single-quoted.

var: msg = 'don\'t stop'

Put an expression inside {} when a string should include a value.

imp: string

var: secretWord = 'paddle'
var: secretWordLength = string.len(secretWord)
out: 'the secret word is {secretWordLength} characters long'

Interpolation accepts display values: int, float, bool, string, and nullable string. Use doubled braces for literal braces.

out: '{{literal}}'

Strings can be indexed by zero-based byte position. The result is a one-character string.

out: 'walk'[1]

Use the string module for common string helpers.

imp: string
out: string.contains('walk', 'al')
out: string.slice('walklang', 4, 99)
out: string.prefix('walklang', 4)
out: string.concat('walk', 'lang')
out: string.lower('Hi WALK')
out: string.replace('banana', 'na', 'NA')

---

Variables And Constants

var: creates a mutable binding.

var: count = 0
count = + count 1

const: creates an immutable binding.

const: max = 10

Type annotations go between the name and =.

var: count int = 0
var: price float = 9
var: name string? = null

---

Output

Use out: to print a scalar value.

out: 'hello'
out: + 1 2
out: true

Arrays and function values cannot be printed directly.

The draft io module gives explicit effect calls for output without a newline and for stderr. Import it and call those functions with do:.

imp: io

do: io.write('prefix')
do: io.write_line(' line')
do: io.error_line('error')

---

Input

Use in: to read one required line from stdin.

var: name = in:

in: may include a prompt. The prompt is a string expression written to stdout without a newline, then stdout is flushed before reading.

var: prompt = 'Name? '
var: name = in: prompt

in: strips the final line ending, preserves all other whitespace, returns '' for an empty line, accepts final input without a trailing newline, and runtime-stops if stdin reaches EOF before any text is read.

in: is an expression, so this is valid:

out: in: 'Say something: '

Typed input is separate from in:. Read text first, then parse explicitly when parsing helpers exist.

---

Prefix Math

Math is prefix only.

+ a b
- a b
* a b
/ a b
^ a b

+ and * accept 2 or more operands.

var: total = + a b c
var: product = * a b c

-, /, and ^ accept exactly 2 operands.

var: diff = - a b
var: ratio = / 5 2
var: square = ^ x 2

Negative numeric literals are valid.

var: x = -4

Use subtraction from zero to negate a name.

var: y = - 0 x

Use math.remainder(value, divisor) when integer code needs the remainder of division. WalkLang keeps this as a named helper instead of adding % punctuation.

imp: math
out: math.remainder(35, 11)

---

Grouping

Use parentheses when an expression must be one operand.

var: x = * (+ a b) (- c d)

There is no infix precedence.

---

Operator Blocks

Long prefix expressions may use operator: blocks.

var: total =
    +:
        subtotal
        tax
        fee

---

Comparisons And Boolean Logic

> a b
< a b
>= a b
<= a b
== a b
!= a b
and a b
or a b
not a

Example:

if: and (> age 18) (< age 65)
    out: 'working age'

---

If / Else

if: condition
    statement
else:
    statement

The condition must be bool.

---

Loops

while: loops while a bool condition is true.

var: count = 0

while: < count 3
    out: count
    count = + count 1

repeat: loops an int count.

repeat: 3
    out: 'again'

for: iterates arrays.

var: nums = [1, 2, 3]

for: n in nums
    out: n

break: and continue: are valid only inside loops.

---

Functions

Parameters may have explicit types.

func: add(a int, b int) int
    return: + a b

Obvious local helper functions can omit parameter and return types when the body proves the types clearly.

func: power_four(n)
    return: ^ n 4

This infers n int and an int return. Use an annotation when the obvious type is not the intended type:

func: half(n float)
    return: / n 2

Ambiguous parameters need annotations. Types are not inferred from later call sites.

func: identity(value) # add a type for value
    return: value

Omitting the return type on a function with no value returns makes the function void.

func: say(message string)
    out: message

Non-void functions must return on all paths.

---

Function Values

Named functions can be passed to typed function parameters.

func: inc(x int) int
    return: + x 1

func: apply(f func(int) int, x int) int
    return: f(x)

out: apply(inc, 4)

Anonymous functions and closures are not stable syntax.

---

Arrays

Arrays use brackets and commas.

var: nums = [1, 2, 3]
var: names = ['a', 'b', 'c']

Arrays must be homogeneous. Empty arrays need an explicit array annotation.

nums[1] = 99
out: nums[0]
var: guessed array[string] = []

Stable native element types are int, float, bool, and string. Experimental struct values can also be stored in arrays, indexed, looped over, and appended with array.push.

Use the array module for common helpers. array.push returns a new array; assign it back when you want to keep the appended value.

imp: array
var: guessed array[string] = []
guessed = array.push(guessed, 'w')
out: array.contains(guessed, 'w')

Use random.choice to pick an item from a non-empty stable native array.

imp: random
var: words = ['dog', 'cat']
out: random.choice(words)

Use random.float(min, max) for uniform decimal samples in [min, max).

imp: random
out: random.float(0, 1)

---

Draft Maps

Draft maps currently support map[string]array[string]. Use them for explicit string-keyed tables whose values are string arrays. This is a draft collection, not a stable feature.

imp: array
imp: map

var: table map[string]array[string] = []
table = map.push(table, 'of the', 'people')
table = map.push(table, 'of the', 'walk')

out: map.has(table, 'of the')
out: table['of the'][0]
out: array.len(map.keys(table))

map.set and map.push return a new map value; assign the result back when you want to keep the change.

---

Null

Use nullable string annotations when assigning null.

var: email string? = null
email = 'a@b.com'

if: != email null
    out: email

---

Imports And Exports

Use imp: to import built-in modules, sibling user modules, or current package modules.

imp: math
out: math.sqrt(9)
out: math.exp(1)
out: math.log(1)

User module example:

# calc.walk
func: square(x int) int
    return: * x x

exp: square
# main.walk
imp: calc
out: calc.square(5)

Only names listed with exp: are public through the namespace.

Package module example:

imp: geometry.core
out: geometry.core.double(3)

Draft process, IO, filesystem, JSON, map, terminal, HTTP, and HTML helpers are available in the current compiler, but they are draft features.

imp: process

out: process.arg_count()
out: process.cwd()
do: process.exit(0)

Draft recoverable text input and parse helpers return result structs:

imp: io
imp: parse

var: line = io.read_line()
var: age = parse.int(line.value)
if: age.ok
    out: age.value

Draft terminal helpers are ordinary imported function calls. Terminal mutation helpers are explicit effects:

imp: io
imp: term

if: term.is_tty()
    do: term.color('red')
    do: io.write_line('error')
    do: term.reset()

Draft HTTP and HTML helpers are ordinary imported function calls that return result structs or strings. They are documented in docs/STDLIB.md.

For large generated text, prefer bounded chunks over repeated whole-string concatenation:

imp: array
imp: file

var: chunks array[string] = []
chunks = array.push(chunks, 'header\n')
chunks = array.push(chunks, 'body\n')
do: file.write_chunks('report.txt', chunks)

---

Tests

Use test: and assert: with walk test.

func: add(a int, b int) int
    return: + a b

test: 'add works'
    assert: == add(2, 3) 5

assert: requires a bool expression.

Imported modules may contain top-level test: blocks. Those tests are checked when the module is imported, are not run by the importer, and run when that module file is the walk test entry.

testing.assert(bool) can wrap that bool expression when you want a namespaced stdlib assertion helper.

imp: testing

test: 'wrapped assertion works'
    assert: testing.assert(true)

---

Reserved Words

var const out if else while for repeat break continue
func return imp exp true false null and or not in test assert defer

---

Complete Example

imp: math

func: distance(x1 float, y1 float, x2 float, y2 float) float
    return:
        math.sqrt(
            +:
                ^ (- x2 x1) 2
                ^ (- y2 y1) 2
        )

var: d = distance(0, 0, 3, 4)

if: == d 5
    out: 'distance is 5'
else:
    out: 'distance is not 5'