Skip to content

LNAST

This chapter shows current Pyrope-to-LNAST translation using small programs compiled with lhd. Each Pyrope block is a standalone input; its first comment names the file to save. The excerpts below come from actual dumps, not proposed optimizations. They were checked against the local LiveHD checkout on 2026-09-30.

LNAST is an intermediate representation. Its node shapes, temporary names and rewrites can change without changing Pyrope semantics. See the language chapters for the source-language rules.

Reproducing the dumps

Set LHD to the LiveHD binary you want to inspect, save a Pyrope example, and run it in a fresh work directory:

LHD=/path/to/livehd/bazel-bin/lhd/lhd
"$LHD" compile values.prp \
  --dump parse,lnast \
  --emit-dir lnast-dump:values-dump \
  --workdir values-work \
  > values.result.json 2> values.trace.txt

--dump parse prints the LNAST immediately after the front end; despite the name, this is LNAST, not the parser's concrete syntax tree. --dump lnast prints the post-upass tree. Both go to stderr with stage labels. The lnast-dump: directory contains post-upass textual .lnast files; ln: is the binary interchange form. Check the command's exit status and diagnostics: a parse dump alone does not establish that elaboration succeeded.

An invocation requesting only LNAST outputs need not lower to hardware. To check graph lowering too, add --emit-dir lg:values-lg, or use ordinary lhd compile without the LNAST-only output selection. The runtime examples below were also checked with an lg: output.

For readability, excerpts omit tree-drawing glyphs, source-span annotations, the dump's outer display quotes, and enclosing top/stmts containers. Indentation still shows parent/child relationships. Each excerpt states its stage; any additional omissions are stated locally. const with no payload is printed that way by the dumper; it is not a missing line in the example.

Variable names

The front end uses user names and generated references such as %x_0. Later passes can introduce inlining prefixes and SSA suffixes such as work___ssa_1. The spelling of generated names is not a user-facing contract; LNAST is not guaranteed to keep every user assignment under one unchanged name. Backticked source identifiers remain distinguishable in the dump.

The following program checks declarations, updates, and a backticked name:

// values.prp
const x = 3 + 1
mut z:U8 = 4
z = z + 2
const `foo x` = x + z
cassert(`foo x` == 10)

post-parse excerpt.

plus
  ref %x_0
  const 3
  const 1
declare
  ref x
  prim_type_none
  const
store
  ref x
  ref %x_0
declare
  ref z
  prim_type_int
    const 0xff
    const 0
  const mut
store
  ref z
  const 4
plus
  ref %z_0
  ref z
  const 2
store
  ref z
  ref %z_0

declare records the name, type, and qualifier. In this post-parse dump the immutable qualifier has an empty const payload, while the mutable qualifier is const mut. Initialization is a separate store. prim_type_int stores maximum then minimum, so U8 is represented by 0xff and 0.

The final cassert succeeds. Constant propagation removes the arithmetic and known assertion; the remaining post-upass statements are:

post-upass excerpt.

declare
  ref x
  prim_type_none
  const
declare
  ref z
  prim_type_int
    const 0xff
    const 0
  const mut
declare
  ref `foo x`
  prim_type_none
  const
store
  ref z
  const 6

This illustrates why a post-upass dump is not a transcript of all source operations: known values can live in the compiler's value tables, and folded expressions need not remain as statements.

Registers

Registers carry a different declaration qualifier and retain their reset value. This small module also shows a typed output and wrapping update:

// register.prp
mod counter(enable:Bool) -> (reg count:U4@[0] = 0) {
  if enable { wrap count += 1 }
}

post-upass excerpt — body of register.counter; io omitted.

declare
  ref count
  prim_type_int
    const 15
    const 0
  const reg
  const 0
if
  ref enable
  stmts
    plus
      ref %count_0
      ref count
      const 1
    get_mask
      ref %count_1
      ref %count_0
      const 15
    store
      ref count
      ref %count_1

Here the declaration includes const reg and reset value 0. The wrap update has become arithmetic followed by a mask of 15 before the store. A dump of the whole unit also includes io metadata; this is separate from the body statements.

Tuples

A tuple is all named or all positional. In the parsed tree, named entries are store children of tuple_add; positional entries are value children. tuple_get reads fields or positions, and a field write is a store with a field path between the base and the new value.

// tuples.prp
mut pair = (mut left:U4=2, const right:U4=3)
pair.left = 4
const selected = pair.left
const positional = (1, 2)
const joined = (...positional, 3)
const nested = (const inner=(const value=7))
cassert(selected == 4)
cassert(joined == (1, 2, 3))
cassert(nested.inner.value == 7)

post-parse excerpt.

tuple_add
  ref %pair_0
  store
    ref left
    const 2
  store
    ref right
    const 3
tuple_get
  ref %pair_1
  ref %pair_0
  const left
declare
  ref %pair_1
  prim_type_none
  const mut
type_spec
  ref %pair_1
  prim_type_int
    const 15
    const 0
tuple_get
  ref %pair_2
  ref %pair_0
  const right
type_spec
  ref %pair_2
  prim_type_int
    const 15
    const 0
declare
  ref pair
  prim_type_none
  const mut
store
  ref pair
  ref %pair_0
store
  ref pair
  const left
  const 4

The mut left field is represented by a field reference followed by its mutable declaration; its width and the immutable right field's width are recorded by type_spec. The outer binding's declare and store follow construction of the tuple.

The same input gives these read, splice, and nested-read nodes (intervening bindings and assertions omitted):

post-parse excerpt.

tuple_get
  ref %selected_0
  ref pair
  const left
fcall
  ref %joined_0
  ref __fkind__tuple_spread
  ref positional
tuple_concat
  ref %joined_2
  ref %joined_0
  ref %joined_1
tuple_get
  ref %167467691_0
  ref nested
  const inner
  const value

The splice is initially marked by a call to __fkind__tuple_spread, then combined with the following positional entry by tuple_concat. After elaboration the marker can become a plain copy. A nested field path can be carried by one tuple_get; nested tuple construction itself uses intermediate tuple_add results. The compiler may flatten field paths in later dumps.

Named-field merge and positional append rules are described in Tuples. They are not additional in/cassert statements inserted after every splice.

Attributes

Attribute syntax produces separate attr_set and attr_get statements, not children attached to a ref. For attr_set, the base comes first and the value last, with the attribute path between them. For attr_get, the destination precedes the base and path.

Attributes are compile-time metadata. The unfinished fluid design uses valid and retry for runtime handshake signals and derives fire from them; see Attributes and Fluid.

// attributes.prp
const a::[tag=3, marked] = 1
const tag = a.[tag]
const k = 2
cassert(tag == 3)
cassert(a.[marked])
cassert(k.[`comptime`])
const d::[debug] = 3
const result = d + 100
cassert(result.[debug])

post-parse excerpt.

declare
  ref a
  prim_type_none
  const
attr_set
  ref a
  const tag
  const 3
attr_set
  ref a
  const marked
  const true
store
  ref a
  const 1
attr_get
  ref %tag_0
  ref a
  const tag

An attribute with no explicit value is set to true. k.[`comptime`] tests whether the current value is known at compile time, not whether k was declared with the comptime modifier.

The debug propagation assertion in this program also passes. Attribute propagation is attribute-specific: do not generalize it into a claim that all attributes are sticky, or that arithmetic removes every attribute.

Sticky attributes

This separate test checks a user attribute copied directly, then removed by arithmetic and bit selection. It complements the sticky debug test above:

// attribute_copy.prp
const a::[tag=3] = 4
const copy = a
const arithmetic = a + 0
const bits = a#[..]
cassert(copy.[tag] == 3)
cassert(arithmetic.[tag] == nil)
cassert(bits.[tag] == nil)

All three assertions pass. These are examples of current propagation behavior; the attribute's own rules determine whether it survives a particular operation.

Bit selection

The following runtime function preserves the selected values in the post-upass tree. Its assertions independently check the sum and the patched word for a concrete input:

// bits.prp
comb bits(foo:U8, xx:U4) -> (yy:U4, patched:U8) {
  mut work:U8 = foo
  work#[1..=2] = xx#[0..<2]
  yy = foo#[5] + xx#[1..<4]
  patched = work
}
cassert(bits(foo=32, xx=10).yy == 6)
cassert(bits(foo=32, xx=10).patched == 36)

post-upass excerpt — body of bits.bits; io and type_spec nodes omitted.

declare
  ref work
  prim_type_int
    const 0xff
    const 0
  const mut
store
  ref work
  ref foo
get_mask
  ref %work_0
  ref xx
  const 3
set_mask
  ref %work_1
  ref work
  const 6
  ref %work_0
store
  ref work___ssa_1
  ref %work_1
get_mask
  ref %yy_0
  ref foo
  const 32
get_mask
  ref %yy_1
  ref xx
  const 14
plus
  ref %yy_2
  ref %yy_0
  ref %yy_1
store
  ref yy
  ref %yy_2
store
  ref patched
  ref work___ssa_1

The mask operand in these LNAST nodes is a bit mask, not a bit index:

Source selection Mask in this dump
xx#[0..<2] 3 (0ub11)
work#[1..=2] 6 (0ub110)
foo#[5] 32 (0ub100000)
xx#[1..<4] 14 (0ub1110)

get_mask(dst, value, mask) extracts and right-justifies the selected bits. set_mask(dst, old_value, mask, replacement) creates the patched value; the following store binds it. In this dump SSA renames the updated work to work___ssa_1.

The plus consumes %yy_0 and %yy_1, the two extracted values. It does not add a range object or the mask itself. A single-bit read at index 5 therefore cannot be rewritten as get_mask(..., 5).

This LNAST representation should not be confused with a lower-level helper's API: a helper can receive interval bounds even when the LNAST node carries a mask. Pyrope selectors accept an index or a range; a list of disjoint indices is not an alternate bit-order notation.

Sign extension and reductions

These operations first select the requested bit vector:

// reductions.prp
comb reductions(v:U8) -> (s:S5, any:U1, all:U1, parity:U1, count:U3) {
  s = v#sext[..=4]
  any = v#|[..=4]
  all = v#&[..=4]
  parity = v#^[..=4]
  count = v#+[..=4]
}
cassert(reductions(31).s == -1)
cassert(reductions(31).all == 1)
cassert(reductions(31).count == 5)

post-upass excerpt — signed selection; type_spec omitted.

get_mask
  ref %s_1
  ref v
  const 31
sext
  ref %s_4
  ref %s_1
  const 4
store
  ref s
  ref %s_4

The selected five-bit vector uses mask 31. sext takes the sign-bit position in that selected vector, here 4. The other outputs use the following nodes after their own get_mask:

Pyrope LNAST operation
v#\|[..=4] red_or
v#&[..=4] red_and
v#^[..=4] red_xor
v#+[..=4] popcount

The dump contains separate selections for these outputs; it does not promise that the front end has already shared one selection among all five.

Direct LNAST/Lgraph call

Cell calls such as __sum are normal Pyrope calls with the cell's pin names as arguments. They appear as fcall before folding or lowering. See the call example below for the actual argument layout.

The old LNAST(a=("let", ...)) injection example is not supported by the current front end. A direct compile reports undefined-call for LNAST; it does not inject a let node. To inspect compiler IR, use the dump commands above.

Basic operators

This program exercises the arithmetic, bitwise, comparison, and Boolean operators before they fold:

// operators.prp
const a = 12
const b = 3
const sum = a + b
const difference = a - b
const product = a * b
const quotient = a / b
const remainder = a % b
const band = a & b
const bor = a | b
const bxor = a ^ b
const left = a << 1
const right = a >> 1
const neg = -b
const inv = ~a
const p = true
const q = false
const both = p and q
const either = p or q
const implication = p implies q
const not_implication = not (p implies q)
const nand = ~(a & b)
const eq = a == b
const ne = a != b
const lt = a < b
const le = a <= b
const gt = a > b
const ge = a >= b
cassert(sum == 15 and difference == 9 and product == 36)
cassert(quotient == 4 and remainder == 0)
cassert(left == 24 and right == 6 and neg == -3 and inv == -13)
cassert(not both and either and not implication and not_implication)

The post-parse nodes use a destination followed by their operands:

Pyrope expression Node or node sequence
a + b, a - b, a * b, a / b, a % b plus, minus, mult, div, mod
a & b, a \| b, a ^ b bit_and, bit_or, bit_xor
a << b, a >> b shl, sra
-a minus with operands 0, a
~a bit_not
a == b, a != b eq, ne
a < b, a <= b, a > b, a >= b lt, le, gt, ge
p and q, p or q, not p log_and, log_or, log_not

These are translation shapes, not a guarantee that every operand/type combination lowers to hardware. In particular, hardware modulo has the restrictions described in Basic syntax.

Unary

Elaboration can add type-dependent operands. An unsigned four-bit complement has an explicit flip width in its post-upass bit_not:

// bit_not.prp
comb invert(a:U4) -> (y:U4) { y = ~a }
cassert(invert(3) == 12)

post-upass excerpt — body of bit_not.invert.

bit_not
  ref %y_0
  ref a
  const 4
store
  ref y
  ref %y_0

Complex operators

Logical implication is emitted as log_not followed by log_or. Negating an implication adds a further log_not; the post-parse tree does not substitute the hand-simplified expression p and not q. These are the nodes from the operator test, with the intervening result bindings omitted:

post-parse excerpt.

log_not
  ref %implication_0
  ref p
log_or
  ref %implication_1
  ref %implication_0
  ref q
log_not
  ref %not_implication_0
  ref p
log_or
  ref %not_implication_1
  ref %not_implication_0
  ref q
log_not
  ref %not_implication_2
  ref %not_implication_1

Similarly, ~(a & b) produces bit_and followed by bit_not; the corresponding negated OR and XOR use their own bitwise nodes. Later passes may fold or rewrite these expressions.

Tuple/Set operators

Membership produces an in node. The operand types determine its meaning; a tuple/range membership test is not universally interchangeable with a bitwise AND. Tuple splicing uses the spread marker and tuple_concat shown in Tuples.

Range operator

Ranges use inclusive endpoints in LNAST. The front end subtracts one from an exclusive end, or computes start + count - 1 for a counted range. An explicit positive step is a separate call rather than a fourth child of the range node. Ranges compare directly with unnamed tuples:

// ranges.prp
const r = 1..=3
const exclusive = 1..<4
const counted = 1..+3
const stepped = 0..=6 step 2
const joined = (...r, 4)
cassert(r == (1, 2, 3))
cassert(r == exclusive and r == counted)
cassert(stepped == (0, 2, 4, 6))
cassert(2 in r)
cassert(joined == (1, 2, 3, 4))

post-parse excerpt.

range
  ref %r_0
  const 1
  const 3
minus
  ref %exclusive_0
  const 4
  const 1
range
  ref %exclusive_1
  const 1
  ref %exclusive_0
plus
  ref %counted_0
  const 1
  const 3
minus
  ref %counted_1
  ref %counted_0
  const 1
range
  ref %counted_2
  const 1
  ref %counted_1
range
  ref %stepped_0
  const 0
  const 6
fcall
  ref %stepped_1
  const step
  ref %stepped_0
  const 2

The assertions confirm (1, 2, 3) for all three unstepped spellings and (0, 2, 4, 6) for the stepped one. The old a..<=b by 2 and a to b spellings are not current syntax.

Type operators

Structural operations have dedicated nodes. In particular, the front end emits equals and case directly; it does not expand them into the does/in/log_and diagrams from older versions of this chapter.

// types.prp
const a = (const x=1, const y=2)
const b = (const x=1)
const has_x = a has 'x'
const covers = a does b
const same_type = a equals a
const matches = a case a
cassert(has_x and covers and same_type and matches)

post-parse excerpt.

has
  ref %has_x_0
  ref a
  const 'x'
does
  ref %covers_0
  ref a
  ref b
equals
  ref %same_type_0
  ref a
  ref a
case
  ref %matches_0
  ref a
  ref a

has tests field existence, does checks structural coverage, equals checks type equivalence, and case performs the structural/value match. Their language rules are in Type system; the dedicated node names do not make them ordinary integer comparison operations.

if/unique if

Branch nodes contain condition/body pairs, followed by an optional final stmts for else. An ordinary chain uses if; a unique chain uses uif. This runtime example keeps all three forms visible after elaboration:

// control.prp
comb choose(a:U4) -> (priority:U5, parallel:U5, matched:U5) {
  priority = 0
  if a < 3 { priority = a + 1 }
  elif a > 12 { priority = 20 }
  parallel = 0
  unique if a == 1 { parallel = 10 }
  elif a == 2 { parallel = 20 }
  matched = match a {
    == 1 { 10 }
    == 2 { 20 }
    else { 0 }
  }
}
cassert(choose(1).priority == 2)
cassert(choose(2).parallel == 20)
cassert(choose(3).matched == 0)

post-upass excerpt — priority and unique chains from control.choose; io omitted.

store
  ref priority
  const 0
gt
  ref %2188985937_0
  ref a
  const 12
lt
  ref %3010617009_0
  ref a
  const 3
if
  ref %3010617009_0
  stmts
    plus
      ref %priority_0
      ref a
      const 1
    store
      ref priority
      ref %priority_0
  ref %2188985937_0
  stmts
    store
      ref priority
      const 20
store
  ref parallel
  const 0
eq
  ref %3385895541_0
  ref a
  const 2
eq
  ref %3375548154_0
  ref a
  const 1
uif
  ref %3375548154_0
  stmts
    store
      ref parallel
      const 10
  ref %3385895541_0
  stmts
    store
      ref parallel
      const 20

The printed order of pure condition calculations can differ from source order; the condition/body order within if determines priority. uif records the unique-branch construct. This dump does not contain a synthetic shl/popcount/assume sequence before it. Do not treat that old explanatory sequence as emitted LNAST.

match

The match in the same program also becomes uif. An expression-valued match writes a common temporary in each arm and then assigns the result:

post-upass excerpt — match from control.choose.

eq
  ref %matched_2
  ref a
  const 2
eq
  ref %matched_1
  ref a
  const 1
uif
  ref %matched_1
  stmts
    store
      ref %matched_0
      const 10
  ref %matched_2
  stmts
    store
      ref %matched_0
      const 20
  stmts
    store
      ref %matched_0
      const 0
store
  ref matched
  ref %matched_0

The explicit else { 0 } is preserved as the final stmts arm. The compiler does not replace an explicit catch-all with assert(false). Exhaustiveness and uniqueness rules belong to the source construct; see match.

Scope

An initializer is evaluated before its condition. Its declarations are visible from their declaration through the remaining if/elif/else chain, match, or while, and are out of scope after that construct. Reading or assigning them afterward is a compile error. Lowering an elif initializer before the arm bodies does not make its variables visible to an earlier arm: reading one there is a read-before-declaration error. See Variable scope for the language rules.

// scope.prp
mut total = 0
if const limit=3; limit > 2 {
  total = limit
}
cassert(total == 3)

post-parse excerpt.

if
  const true
  stmts
    declare
      ref limit
      prim_type_none
      const
    store
      ref limit
      const 3
    gt
      ref %2940350924_0
      ref limit
      const 2
    if
      ref %2940350924_0
      stmts
        store
          ref total
          ref limit

The outer if true creates the initializer scope. It uses the normal branch merge to preserve writes to enclosing variables such as total; elaboration folds away this always-taken condition. The assertion checks total == 3. Adding cassert(limit == 3) after the if is an undefined-variable error, even though the initializer and branch condition are known at compile time.

while/loop/for

LNAST has both while and for nodes. It is not limited to while, and elaboration does not always expand every hardware loop into repeated source statements. The following comptime examples do expand and fold:

// loops.prp
mut i = 0
loop {
  i += 1
  if i == 3 { break }
}
cassert(i == 3)
mut sum = 0
while mut j=0; j < 3 {
  sum += j
  j += 1
}
cassert(sum == 3)
mut enumerated = 0
for (index, value) in (2, 4, 6) {
  enumerated += index + value
}
cassert(enumerated == 15)
mut stepped_sum = 0
for value in 0..=6 step 2 { stepped_sum += value }
cassert(stepped_sum == 12)

post-parse excerpt — while loop; initializer scope and initialization omitted.

while
  ref %1721491827_0
  stmts
    lt
      ref %1721491827_1
      ref j
      const 3
    if
      ref %1721491827_1
      stmts
        plus
          ref %sum_0
          ref sum
          ref j
        store
          ref sum
          ref %sum_0
        plus
          ref %j_0
          ref j
          const 1
        store
          ref j
          ref %j_0
      stmts
        break
          ref %1826580722_2

The condition is calculated before the while and again inside it. The inner if executes the body or breaks. An unconditional loop uses the same machinery with an always-true condition. break carries an internal reference in the dump; it is not a returned Pyrope value.

The enumeration loop is represented directly as:

post-parse excerpt — enumeration; tuple construction omitted.

for
  ref value
  ref %3800684521_0
  stmts
    plus
      ref %enumerated_0
      ref index
      ref value
    plus
      ref %enumerated_1
      ref enumerated
      ref %enumerated_0
    store
      ref enumerated
      ref %enumerated_1
  const val
  ref index

The child order is value binding, iterable, body, iteration mode, and optional index/key bindings. The source (index, value) binding therefore is not the same order as the first two children of the for node.

// for_key.prp
const tup = (const left=2, const right=4)
mut sum = 0
for (index, value, key) in tup { sum += value }
cassert(sum == 6)

For this three-name binding, the parsed node ends with const val, ref index, and ref key. The sum assertion passes; named-tuple ordering should not be inferred from this order-independent sum.

// for_ref.prp
mut tup = (1, 2, 3)
for value in ref tup { value += 1 }
cassert(tup == (2, 3, 4))

post-parse excerpt.

for
  ref value
  ref tup
  stmts
    plus
      ref %value_0
      ref value
      const 1
    store
      ref value
      ref %value_0
  const ref

The const ref mode requests write-back to the original tuple. Its post-upass body contains stores to tuple positions 0, 1, and 2, and the assertion confirms (2, 3, 4).

Compact hardware loops

A typed accumulator can keep a hardware loop compact under the default compile.unroll=false policy:

// compact_loop.prp
comb sum_lanes(a:[4]U4) -> (y:U8) {
  mut total:U8 = 0
  for i in 0..<4 { wrap total += a[i] }
  y = total
}
cassert(sum_lanes((1, 2, 3, 4)) == 10)

post-upass excerpt — rolled_for from compact_loop.sum_lanes.

rolled_for
  ref i
  const 0
  const 1
  const 4
  const
  const
  tuple_add
    store
      ref total__carry_in
      const total__carry_out
  stmts
    tuple_get
      ref %total_0
      ref a
      ref i
    plus
      ref %total_1
      ref total
      ref %total_0
    fcall
      ref %total_2
      ref wrap
      store
        ref v
        ref %total_1
      store
        ref type
        ref total
    store
      ref total
      ref %total_2
  stmts
    fcall
      ref %u_loop_0_r
      ref compact_loop.sum_lanes.__loop0
      store
        ref __inst_name
        const u_loop_0
      store
        ref a
        ref a
      store
        ref total__carry_in
        ref total
      store
        ref __valid
        const true
    store
      ref total
      ref %u_loop_0_r

This dump also contains the generated unit compact_loop.sum_lanes.__loop0. The retained loop records the iteration bounds and the accumulator's carry connection. compile.unroll=true requests source expansion; loops that are not eligible for compact lifting can expand under either setting. A finite hardware loop is not necessarily an eagerly unrolled LNAST tree.

To build a tuple with a loop, use an explicit mutable accumulator and splice entries into it, as described in for. The old claim that cont/brk carry comprehension values is not an emitted-node contract.

puts/print

Interpolation is built before the printing call. In the parsed tree below, a String call receives literal fragments and interpolated operands:

// strings.prp
const num = 2
const color = "blue"
const text = "I have {num} {color} potatoes"
cassert(text == "I have 2 blue potatoes")
cputs(text)
puts(text)
print("count={num}")

post-parse excerpt.

fcall
  ref %text_0
  ref String
  const 'I have '
  ref num
  const ' '
  ref color
  const ' potatoes'
fcall
  ref %2917045464_0
  ref cputs
  ref text
fcall
  ref %318168145_0
  ref puts
  ref text
fcall
  ref %3932631745_0
  ref String
  const 'count='
  ref num
fcall
  ref %1055615812_0
  ref print
  ref %3932631745_0

The text assertion passes, and cputs reports I have 2 blue potatoes during compilation. A post-upass tree can fold the string construction and consume compile-time calls; absence of an fcall there is not by itself a simulation trace. The snippet checks translation and constant evaluation, not the execution of runtime puts/print. There is no separate format(...) built-in in this example.

Lambda call

Named arguments are store children of the fcall itself. A bare argument can remain a bare ref in the parsed tree; argument binding is resolved by the compiler, not by wrapping every source call in one prebuilt named tuple.

// calls.prp
comb add(a:U4, b:U4) -> (sum:U5) { sum = a + b }
const a:U4 = 2
const result = add(a, b=3)
const cell = __sum(`as`=(1, 2, 3))
cassert(result == 5)
cassert(cell == 6)

post-parse excerpt.

fcall
  ref %result_0
  ref add
  ref a
  store
    ref b
    const 3
tuple_add
  ref %cell_0
  const 1
  const 2
  const 3
fcall
  ref %cell_1
  ref __sum
  store
    ref as
    ref %cell_0

The add call contains the bare ref a and a named store b; the cell call contains a named store as referring to the positional operand tuple. Both assertions pass.

A post-parse file can contain an fdef marker with __streamed_comb or __streamed_mod and an opaque body identifier. The lambda body is materialized later as its own unit. For example, the post-upass dump includes calls.add, whose body is:

post-upass excerpt — body of calls.add; io omitted.

plus
  ref %sum_0
  ref a
  ref b
store
  ref sum
  ref %sum_0

The surrounding unit can instead contain an inlined call with folded operands. Do not confuse the streamed marker, the specialized/inlined call, and the materialized lambda unit: they are different views of the same source at different stages.

Assertions in the dump

Assertions are not ordinary fcall nodes in the current parsed tree. The node name is cassert, with a marker distinguishing the source operation:

// assertions.prp
const k = 2
cassert(k == 2)
assert(k > 0)
assume(k < 3)

post-parse excerpt.

eq
  ref %3023404487_0
  ref k
  const 2
cassert
  ref %3023404487_0
  const __fkind__cassert
gt
  ref %1078686553_0
  ref k
  const 0
cassert
  ref %1078686553_0
lt
  ref %3061419811_0
  ref k
  const 3
cassert
  ref %3061419811_0
  const __fkind__assume

__fkind__cassert identifies the compile-time assertion; __fkind__assume identifies the assumption; plain assert has no such marker here. Sharing an IR node does not give them identical semantics. All conditions in this input are known and disappear after verification. See Assertions for formal obligations and the TBD runtime fallback for design-body assertions in lhd sim.

Validation scope

The examples above were compiled individually with fresh work directories, and both parse and post-upass dumps were inspected. All their cassert checks passed. The bit-selection, reduction, conditional, register, call, unsigned-complement, and compact-loop examples also completed graph lowering. These checks establish the shown translations and concrete results; they do not establish that every feature or operand combination has a hardware implementation. In particular, this chapter supplies no lowering claim for unfinished fluid support.