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'