8 Expressions — prefix notation and the expr island
What to know first
match must cover every caseLooking back
Why did chapter 3 reject expr a. lt b. lt c. .? What did the diagnostic say to write instead?
A. Chained comparisons read as “a < b and b < c” in mathematics but as (a < b) < c in many languages. Since the same spelling means different things to people and to the machine, it was rejected with E-EXPR-CHAIN. The diagnostic said to write expr (a. lt b.) and (b. lt c.) .. This chapter covers every rule inside that island.
The need for this chapter, and its context
expr island. You need to understand this two-layer structure to read prefix expressions with their runs of stops, and to know what the island does not allow so you do not get lost. As the last chapter of Part II, it gathers the rules of expressions seen piecemeal so far.By the end of this chapter
and and or are parenthesised. You will also see how short-circuiting and and or keep conditions safe, and size_of and comptime, which are computed at translation time.The questions this chapter answers
- The VM shows
in_range(5, 1, 9) = 1. Has theboolbecome a number?
8.1 Nothing to memorise in prefix notation#
In prefix notation the operation’s name comes first and its arguments follow. The name opens a form and a stop closes it. This shape has no precedence; the stops say everything about what is computed first — the inner form closes first.
examples/ch08/island.low
module island .
rem run: score 3 4
rem run: score_prefix 3 4
rem run: grouped 3 4
rem run: in_range 5 1 9
rem run: spread 3 9
fn score input a u32 . input b u32 . output u32 .
requires le a. 1000 . .
requires le b. 1000 . .
do
return expr a. + b. * 2 . .
end .
fn score_prefix input a u32 . input b u32 . output u32 .
requires le a. 1000 . .
requires le b. 1000 . .
do
return add a. mul b. 2 . . .
end .
fn grouped input a u32 . input b u32 . output u32 .
requires le a. 1000 . .
requires le b. 1000 . .
do
return expr (a. + b.) * 2 . .
end .
fn in_range input x u32 . input lo u32 . input hi u32 . output bool .
do
return expr (lo. le x.) and (x. le hi.) . .
end .
fn spread input a u32 . input b u32 . output u32 .
do
return expr (max a. b. .) - (min a. b. .) . .
end .
Output
$ lowentc --run score island.low 3 4
score(3, 4) = 11
$ lowentc --run score_prefix island.low 3 4
score_prefix(3, 4) = 11
$ lowentc --run grouped island.low 3 4
grouped(3, 4) = 14
$ lowentc --run in_range island.low 5 1 9
in_range(5, 1, 9) = 1
$ lowentc --run spread island.low 3 9
spread(3, 9) = 6
add a. mul b. 2 . . in score_prefix and expr a. + b. * 2 . in score are the same expression. The two ops give the same answer, and since the island is translated to prefix, the run-time cost is the same too. The island is a projection of notation, not a different operation.
The reason for words instead of symbols is the same. ^ is exponentiation in some languages and exclusive or in others. bit_xor means one thing wherever it is read. And words can be read aloud.
8.2 The island’s precedence table#
Precedence exists only inside the island, and this table is all of it.
| Level | Operations | Associativity | Prefix equivalent |
|---|---|---|---|
| 5 | ( ) grouping | — | Only marks grouping; not a value |
| 4 | * / | left | mul · div |
| 3 | + - | left | add · sub |
| 2 | eq ne lt le gt ge | cannot chain | the prefix op of the same name |
| 1b | and | left | and (short-circuit) |
| 1a | or | left | or (short-circuit) |
Table 8.1 — Precedence inside an expr island (tightest at the top)
Parentheses change the order, as in grouped’s expr (a. + b.) * 2 .. Operations not in the table — the remainder mod, bitwise operations, min and max — are written prefix even inside the island. spread’s expr (max a. b. .) - (min a. b. .) . has that shape. A prefix form closes with its own stop, so the parentheses there are decoration.
in_range joins two comparisons with and. By the table, comparisons bind tighter than and, so expr lo. le x. and x. le hi. . means the same without parentheses (the “Common mistakes” section of this chapter shows it). Still, when mixing comparisons with and or or, parenthesise each comparison. The reader no longer needs to recall the table either.
Q. The VM shows in_range(5, 1, 9) = 1. Has the bool become a number?
A. No. The VM’s result line merely displays a bool as 0 or 1. Inside a program a bool does not mix with numbers (chapter 4). In the C that native code is emitted as, a bool is also one byte holding 0 or 1.
8.3 What the island cannot do#
An op can be called inside the island too. A call is opened by its name and closed by its own stop, so the stops say where twice ends.
examples/ch08/app.low
module app .
rem run: bump 3
rem run: bump_marked 3
fn twice input a u32 . output u32 .
requires le a. 1000 . .
do
return mul a. 2 . .
end .
fn bump input a u32 . output u32 .
requires le a. 100 . .
do
return expr twice a. . + 1 . .
end .
fn bump_marked input a u32 . output u32 .
requires le a. 100 . .
do
rem the parentheses are decoration --- the same 7
return expr (twice a. .) + 1 . .
end .
Output
$ lowentc --run bump app.low 3
bump(3) = 7
$ lowentc --run bump_marked app.low 3
bump_marked(3) = 7
In expr twice a. . + 1 . the stop after a. closes twice, and the last stop closes the island. To make the call stand out, wrap it in parentheses as bump_marked does — the meaning is the same.
The island is a world of infix operators, so one thing is missing: unary operators. The island has no operator without a left operand.
examples/ch08/unary_bad.low
module unary_bad .
rem expect: E-EXPR-UNARY
fn flip input a i32 . output i32 .
do
return expr - a. . .
end .
Output
$ lowentc --check unary_bad.low
unary_bad.low:6:0 E-EXPR-UNARY: an `expr` island has INFIX operators only — there is no unary form. `expr - 5` has nothing on the left of `-`. Write the prefix op instead (`neg 5`, `not b`, `bit_not x`), or give the left operand. The island exists to read like arithmetic; a unary sign would make `a - 5` and `a (- 5)` two readings of the same letters (SPEC-002 §2.5 · RFC-0091)
To flip a sign, write neg a. . in prefix, or inside the island write expr 0 - a. .. not b. . and bit_not x. . are the same.
A common misconception. The island is small because it is unfinished
8.4 Short-circuiting guards conditions#
and and or do not compute the right-hand side when the left-hand side already decides the answer. Using that, you can put the condition that makes the right-hand side safe on the left.
examples/ch08/shortcircuit.low
module shortcircuit .
rem run: safe_first_is_zero []
rem run: safe_first_is_zero [0,4]
rem run: ratio_ok 0 0
rem run: ratio_ok 10 5
fn safe_first_is_zero input xs slice u8 . output bool .
do
return and gt len xs. . 0 . eq idx xs. 0 . 0 . . .
end .
fn ratio_ok input num u32 . input den u32 . output bool .
do
return and ne den. 0 . ge div num. den. . 2 . . .
end .
Output
$ lowentc --run safe_first_is_zero shortcircuit.low []
safe_first_is_zero([]) = 0
arg0 (written) = []
$ lowentc --run safe_first_is_zero shortcircuit.low [0,4]
safe_first_is_zero([0,4]) = 1
arg0 (written) = [0,4]
$ lowentc --run ratio_ok shortcircuit.low 0 0
ratio_ok(0, 0) = 0
$ lowentc --run ratio_ok shortcircuit.low 10 5
ratio_ok(10, 5) = 1
safe_first_is_zero does not compute idx xs. 0 . on an empty slice, because if gt len xs. . 0 . on the left is false, the answer is already false. ratio_ok likewise skips the division when the denominator is 0. This shape suits conditions too small for a guard. If the condition is a fact about the whole op, guard is better (chapter 7).
8.5 Expressions computed at translation time#
Some expressions have values before the program runs.
examples/ch08/compt.low
module compt .
rem run: table_size
rem run: pick
fn table_size output usize .
do
return mul size_of u32 . 16 . .
end .
fn pick output u32 .
do
match comptime add 2 3 . do
case 0 do return 100 . end .
case 5 do return 500 . end .
case _ do return 999 . end .
end .
end .
Output
$ lowentc --run table_size compt.low
table_size() = 64
$ lowentc --run pick compt.low
pick() = 500
size_of u32 . gives the number of bytes of one value of that type at translation time. It lets code that takes a type as a parameter compute its costs honestly instead of assuming the widest type (chapter 22).
comptime add 2 3 . marks an expression to be computed at translation time. When the value a match splits on is a translation-time constant, the match folds to the one matching arm and the run-time comparisons disappear. The folded-away arms are still type-checked, unlike code removed with C’s #ifdef, which is not even checked. config <option>, which reads the build configuration, is also used as a translation-time constant (chapter 31).
A comptime expression can also read names: module constants, a local let whose value is known at translation time, machine.*, native_lanes <type> and config <option>. A local let’s value is known at translation time when it is written as a literal, one such name, native_lanes <type> or comptime <expr>. A var and an input are not read. A module constant’s value can itself be written as comptime <expr>.
examples/ch08/compt_names.low
module compt_names .
rem run: page_bytes
rem run: lanes_twice
rem a module constant can be written as a `comptime` expression
let KIB u64 1024 .
let PAGE u64 comptime mul 4 KIB. . . .
fn page_bytes output u64 .
do
let extra u64 4 .
rem reads a constant and a `let` whose value is known at compile time
return comptime add PAGE. extra. . . .
end .
fn lanes_twice output usize .
do
rem a value asked of the target takes part too
return comptime mul native_lanes u32 . 2 . . .
end .
Output
$ lowentc --run page_bytes compt_names.low
page_bytes() = 4100
$ lowentc --run lanes_twice compt_names.low
lanes_twice() = 8
The answer is a constant. The place that receives it measures it like a literal, so an answer that does not fit is refused just as return 600 . is.
examples/ch08/compt_width.low
module compt_width .
rem expect: E-TYPE-WIDTH
let K u64 6 .
fn f output u8 .
do
rem ✘ the answer 600 does not fit `u8` — the same as `return 600 .`
return comptime mul K. 100 . . .
end .
Output
$ lowentc --check compt_width.low
compt_width.low:9:0 E-TYPE-WIDTH: value does not fit the declared type (a literal out of range, or a narrowing — use narrow / narrow_wrap / narrow_sat / round_to)
A call to a pure fn is something comptime can compute too. The tool really runs that fn at translation time, and only the answer, one number, is left at run time. For now it runs a fn whose inputs and answer are each one integer or bool. A proc, which may have effects, is not run. If the run overflows, breaks a contract or does not end within a set number of steps, it is refused with E-COMPTIME-NONCONST, and the message says why.
examples/ch08/compt_fn.low
module compt_fn .
rem run: table_len
rem run: small
rem a pure `fn` can run at compile time
fn fact input n u64 . output u64 .
do
var r u64 1 .
var i u64 1 .
while le i. n. . do
set r. mul r. i. . .
set i. add i. 1 . .
end .
return r. .
end .
rem a module constant written as its answer
let PERMS u64 comptime fact 6 . . .
fn table_len output u64 .
do
return PERMS. .
end .
fn small output u8 .
do
rem the answer 120 is a constant, so it fits a `u8` place
return comptime fact 5 . . .
end .
Output
$ lowentc --run table_len compt_fn.low
table_len() = 720
$ lowentc --run small compt_fn.low
small() = 120
A fn that returns an array (chapter 9) runs at translation time too. Its elements are sized numbers. The answer becomes a read-only table baked into the program, so nothing builds the table at run time. Give the answer a name whose length and element type match the fn’s result: let x array <length> <type> comptime f … .. It may be a let in a body or a module constant. The table is read only — writing an element (set · copy · swap) or passing it to a mut input is refused. If you need a copy to change, copy it into a var made with lit array <length> <type> _ ..
examples/ch08/compt_table.low
module compt_table .
rem run: sq7
rem run: cube_sum
rem a `fn` that returns an array runs at compile time too
fn squares output r array 16 u16 .
do
var i usize 0 .
while lt i. 16 . do
let v u16 cast u16 i. . .
set idx r. i. . mul v. v. . .
set i. add i. 1 . .
end .
return r. .
end .
fn cubes input k u32 . output r array 4 u32 .
do
var i usize 0 .
while lt i. 4 . do
let v u32 add k. cast u32 i. . . .
set idx r. i. . mul mul v. v. . v. . .
set i. add i. 1 . .
end .
return r. .
end .
rem a module constant — one read-only table baked into the program
let SQ array 16 u16 comptime squares . . .
fn sq7 output u16 .
do
return idx SQ. 7 . .
end .
fn cube_sum output u32 .
do
rem a `let` in a body works too — 2³+3³+4³+5³
let t array 4 u32 comptime cubes 2 . . .
return add add idx t. 0 . idx t. 1 . . add idx t. 2 . idx t. 3 . . . .
end .
Output
$ lowentc --run sq7 compt_table.low
sq7() = 49
$ lowentc --run cube_sum compt_table.low
cube_sum() = 224
An array’s elements may also be bool or rows (an array of arrays). A fn that returns a slice or a struct runs at translation time too. A slice answer becomes read-only bytes like an array answer, and a struct answer becomes a value of that struct — the same as a lit with the computed value in each field. Besides integers and bool, an input may be a slice whose bytes are fixed at translation time: a quoted string, a text literal, a list of constants, or an array or slice module constant.
examples/ch08/compt_values.low
module compt_values .
rem run: hello_len
rem run: origin_y
rem run: vowels
rem a `fn` that returns a slice — a piece of its input
fn trim input s slice u8 . output slice u8 .
do
var a usize 0 .
var b usize len s. . .
while and lt a. b. . eq idx s. a. . 32 . . do set a. add a. 1 . . end .
while and gt b. a. . eq idx s. sub b. 1 . . 32 . . do set b. sub b. 1 . . end .
return subslice s. a. b. . .
end .
struct pt do
x i32 .
y i32 .
end .
rem a `fn` that returns a struct
fn corner input k i32 . output pt .
requires le k. 1000 . .
requires ge k. -1000 . .
do
return lit pt do x k. . y neg k. . . end . .
end .
rem a `fn` that counts bytes — it takes a `text` literal
fn count_vowels input s slice u8 . output u64 .
do
var n u64 0 .
for c s. do
if or or eq c. 97 . eq c. 101 . . or or eq c. 105 . eq c. 111 . . eq c. 117 . . . do set n. add n. 1 . . end .
end .
return n. .
end .
let HELLO slice u8 comptime trim " hello " . . .
let ORIGIN pt comptime corner 3 . . .
fn hello_len output usize .
do
return len HELLO. . .
end .
fn origin_y output i32 .
do
return field ORIGIN. y . .
end .
fn vowels output u64 .
do
return comptime count_vowels text END
a quiet morning
END
. . .
end .
Output
$ lowentc --run hello_len compt_values.low
hello_len() = 5
$ lowentc --run origin_y compt_values.low
origin_y() = -3
$ lowentc --run vowels compt_values.low
vowels() = 6
A struct can go in too: a lit with a constant in each field, or the struct answer of a fn run at translation time (the struct needs a byte layout — no slice or array fields). A slice of structs can be an input and an answer. A fn run at translation time builds no new memory, so a slice answer is a piece of its input or a constant. A struct answer with a 128-bit field keeps the upper half too.
examples/ch08/compt_structs.low
module compt_structs .
rem run: scaled
rem run: rest_x
struct pt do
x u16 .
y u16 .
end .
rem a `fn` that takes a struct — a struct goes into a compile-time call
fn scale input p pt . input k u16 . output pt .
do
return lit pt do x mul field p. x . k. . . y mul field p. y . k. . . end . .
end .
rem takes a slice of structs and returns a piece of it — no new storage
fn from input ps slice pt . input k usize . output slice pt .
do
return subslice ps. k. len ps. . . .
end .
let P pt comptime scale lit pt do x 3 . y 4 . end . 10 . . .
let REST slice pt comptime from lit slice pt lit pt do x 1 . y 2 . end . lit pt do x 5 . y 6 . end . lit pt do x 7 . y 8 . end . . 1 . . .
rem 30 + 40
fn scaled output u16 .
do
return add field P. x . field P. y . . .
end .
rem the first element of the piece — 5
fn rest_x output u16 .
do
return field idx REST. 0 . x . .
end .
Output
$ lowentc --run scaled compt_structs.low
scaled() = 70
$ lowentc --run rest_x compt_structs.low
rest_x() = 5
A fn run at translation time is pure, so it allocates no new memory. It can still build a table: the place for the answer comes from outside (the place of an array result), and the slice fields in the answer point at pieces of the input text. The tool keeps those pieces as read-only bytes and makes the fields point there. A struct with slice fields can be an element of a list and of an array result (each slice field is two words, an address and a length). If you do not know the number of lines in advance, give generous room and count the filled lines separately. If the room runs out, the write to the next element stops out of bounds, and translation is refused at that constant’s declaration.
examples/ch08/compt_messages.low
module compt_messages .
rem run: lines
rem run: thanks_len
struct msg do
ko slice u8 .
en slice u8 .
end .
rem split each `ko,en` line — the slice fields are pieces of the input text
fn parse input txt slice u8 . output r array 8 msg .
do
var row usize 0 .
var start usize 0 .
var comma usize 0 .
var i usize 0 .
while le i. len txt. . . do
if or eq i. len txt. . . eq idx txt. i. . 10 . . do
if gt i. start. . do
set idx r. row. . lit msg do ko subslice txt. start. comma. . . en subslice txt. add comma. 1 . i. . . end . .
set row. add row. 1 . .
end .
set start. add i. 1 . .
end else do
if eq idx txt. i. . 44 . do set comma. i. . end .
end .
set i. add i. 1 . .
end .
return r. .
end .
fn count_lines input txt slice u8 . output usize .
do
var n usize 0 .
for c txt. do if eq c. 10 . do set n. add n. 1 . . end . end .
return add n. 1 . .
end .
fn same input s slice u8 . output slice u8 .
do
return s. .
end .
let RAW slice u8 comptime same text END
안녕,hello
고마워,thanks
END
. . .
rem room for 8 — how many lines were filled is counted separately
let N usize comptime count_lines RAW. . . .
let MSGS array 8 msg comptime parse RAW. . . .
fn lines output usize .
do
return N. .
end .
fn thanks_len output usize .
do
return len field idx MSGS. 1 . en . . .
end .
Output
$ lowentc --run lines compt_messages.low
lines() = 2
$ lowentc --run thanks_len compt_messages.low
thanks_len() = 6
To keep the source text of a table in a file instead of in the program, use include_text "<path>". When translating, the tool reads that file and hands its bytes (slice u8) to a fn run at translation time. The path is read from the folder of this source file, so the same file is read wherever you build. A path outside that folder (absolute, with .., or reached through a link) is refused. include_text goes only in the input place of a fn run at translation time — anywhere else it would look like reading a file while the program runs, so it is refused with E-INCLUDE-PLACE. When the file changes, the fn runs again instead of using a kept answer. In a build with a lockfile (pkg.lock) the file is pinned by its content like a dependency — lowentc --lock-write records an include:<path> entry, and a later change stops the build with E-DEP-HASH (chapter 31).
examples/ch08/compt_include.low
module compt_include .
rem run: words
rem run: longest
rem counts the lines
fn count_lines input txt slice u8 . output usize .
do
var n usize 0 .
for c txt. do if eq c. 10 . do set n. add n. 1 . . end . end .
return n. .
end .
rem the length of the longest line
fn longest_line input txt slice u8 . output usize .
do
var best usize 0 .
var cur usize 0 .
for c txt. do
if eq c. 10 . do
if gt cur. best. . do set best. cur. . end .
set cur. 0 .
end else do set cur. add cur. 1 . . end .
end .
return best. .
end .
rem reads `compt_words.txt` next to this file when translating
let WORDS usize comptime count_lines include_text "compt_words.txt" . . . .
let LONGEST usize comptime longest_line include_text "compt_words.txt" . . . .
fn words output usize .
do
return WORDS. .
end .
fn longest output usize .
do
return LONGEST. .
end .
Output
$ lowentc --run words compt_include.low
words() = 3
$ lowentc --run longest compt_include.low
longest() = 6
One fn run at translation time must finish within 1,000,000 steps. For a table that takes longer, write build comptime_steps 50000000 . in pkg.low or give --comptime-steps 50000000 on the command line (the command line wins). If the environment variable LOWENT_CTFE_CACHE names a folder, the answers are kept there and used again by the next translation. A kept answer is not used if the fn’s definition or its inputs changed, or if the tool version, the target or the step budget differ.
8.6 Common mistakes#
Comparison (level 2) binds tighter than and (level 1b), so mixing them without parentheses has exactly one meaning.
examples/ch08/mixcmp.low
module mixcmp .
rem run: in_range 3 1 5
rem run: in_range 9 1 5
fn in_range input x u64 . input lo u64 . input hi u64 . output bool .
do
rem comparison binds tighter than `and`, so mixing without parentheses has one meaning
return expr lo. le x. and x. le hi. . .
end .
Output
$ lowentc --run in_range mixcmp.low 3 1 5
in_range(3, 1, 5) = 1
$ lowentc --run in_range mixcmp.low 9 1 5
in_range(9, 1, 5) = 0
Writing the parentheses anyway spares the reader from recalling the table. Below is the same thing written that way.
examples/ch08/mixcmp_fixed.low
module mixcmp_fixed .
rem run: in_range 5 1 9
rem run: in_range 12 1 9
fn in_range input x u64 . input lo u64 . input hi u64 . output bool .
do
return expr (lo. le x.) and (x. le hi.) . .
end .
Output
$ lowentc --run in_range mixcmp_fixed.low 5 1 9
in_range(5, 1, 9) = 1
$ lowentc --run in_range mixcmp_fixed.low 12 1 9
in_range(12, 1, 9) = 0
Counter-example. Writing the remainder as %
examples/ch08/mistake_percent.low
module mistake_percent .
rem expect: E-CHAR
fn last_digit input a u64 . output u64 .
do
rem ✘ `%` is not an island operator --- call `mod` in parentheses for the remainder
return expr a. % 10 . .
end .
Output
$ lowentc --check mistake_percent.low
7:18 E-CHAR: unexpected character
The island has exactly the operators in the table. % is the remainder in C, but languages disagree on its sign for negative numbers, so the same symbol gives different answers. Lowent’s remainder is the one name mod, and its sign follows the divisor (chapter 4). Inside the island it is still called in prefix form.
examples/ch08/percent_fixed.low
module percent_fixed .
rem run: last_digit_plus 1234 1
fn last_digit_plus input a u64 . input k u64 . output u64 .
do
rem operations missing from the table are written prefix inside the island, wrapped in parentheses
return expr (mod a. 10 .) + k. . .
end .
Output
$ lowentc --run last_digit_plus percent_fixed.low 1234 1
last_digit_plus(1234, 1) = 5
Counter-example. Writing powers as ^, or using math.pow on integers
^ is not an island operator (E-CHAR) — it means power in some languages and exclusive or in others, so it was left out. Square an integer with mul a. a. .. And math.pow is the power of floating-point numbers — both inputs are f64.
examples/ch08/mistake_pow.low
module mistake_pow .
rem expect: E-TYPE-ARG
use math .
fn square input a u64 . output u64 .
do
rem ✘ `pow` on an integer; transcendental functions are floating-point only
return math.pow a. 2 . .
end .
Output
$ lowentc --check mistake_pow.low
mistake_pow.low:9:0 E-TYPE-ARG: call argument type does not match parameter
mistake_pow.low:9:0 E-TYPE-ARG: call argument type does not match parameter
mistake_pow.low:9:0 E-TYPE-RETURN: the returned value does not match the op's `output` — expected `u64`, found `f64`
The canon makes pow, sqrt, sin, cos, exp, log and fmod floating-point only (§6.3.9). The ops of math declare their inputs as f64, so an integer is refused with E-TYPE-ARG — the argument type does not match. Until 2026-09-16 it was not: square 5 answered 5 instead of 25, and neither translation nor the run said anything — a quiet wrong answer. Write an integer power as multiplication, and convert with cast f64 n. . to compute in floating point.
examples/ch08/pow_fixed.low
module pow_fixed .
rem run: square_f 1.5
rem run: square_i 5
use math .
fn square_f input a f64 . output f64 .
do
rem `pow` works correctly on floats
return math.pow a. 2.0 . .
end .
fn square_i input a u64 . output u64 .
requires le a. 1000 . .
do
rem an integer power is written as multiplication; the name says what it does
return mul a. a. . .
end .
Output
$ lowentc --run square_f pow_fixed.low 1.5
square_f(1.5) = 2.25
$ lowentc --run square_i pow_fixed.low 5
square_i(5) = 25
A common misconception. Nested calls must always be parenthesised
examples/ch08/noparen.low
module noparen .
rem run: with_parens 3 4
rem run: without_parens 3 4
fn with_parens input a u64 . input b u64 . output u64 .
do
return add 1 (mul a. b. .) . .
end .
fn without_parens input a u64 . input b u64 . output u64 .
do
rem without parentheses, `mul` still closes at its own stop --- the same 13
return add 1 mul a. b. . . .
end .
Output
$ lowentc --run with_parens noparen.low 3 4
with_parens(3, 4) = 13
$ lowentc --run without_parens noparen.low 3 4
without_parens(3, 4) = 13
Parentheses are decoration. Where an inner call ends is decided by that call’s own stop. So add 1 mul a. b. . . and add 1 (mul a. b. .) . are the same expression. Up to edition 1.7 it was the number of arguments each op takes that decided the end, so you had to know an op’s declaration to read an expression. Now the stops are enough — write parentheses only where you want a grouping to stand out in a long expression.
8.7 This chapter’s syntax at a glance#
| Shape | Meaning | Why |
|---|---|---|
add a. mul b. c. . . | prefix notation — no precedence | the stops say the whole order of evaluation |
expr a. + b. * c. . | infix island — * / bind tighter than + - | only what school arithmetic taught goes infix |
expr (a. + b.) * c. . | grouping inside the island | parentheses group; they are not values |
expr (lo. le x.) and (x. le hi.) . | comparisons mixed with logic | parentheses per comparison — no table to recall |
expr twice a. . + 1 . | calling an op inside the island | a call closes with its own stop — parentheses are decoration |
expr mod a. 10 . + k. . | an operation not in the table (mod, bitwise, min) | the island never grows — call it prefix |
expr 0 - a. . · neg a. . | flip the sign | the island has no unary operators (E-EXPR-UNARY) |
and · or | short-circuit — the right side is skipped once the left decides | put the condition that makes the right side safe first |
size_of u32 . | bytes in one value of a type (at compile time) | generic code counts its cost honestly |
comptime add 2 3 . | evaluate at compile time | folded branches are still type-checked |
Table 8.2 — Expression syntax — shape · meaning · why it looks this way
Recap
expr island allows only arithmetic, comparisons, and and or in infix; op calls inside it close with their own stops, and there are no unary operators. When mixing comparisons with and or or, parenthesise each comparison. Short-circuiting and and or can guard the right-hand side, and size_of and comptime are computed at translation time.