commit 9507a3668cd1218bae1aa0f171e7991cc8768afd
parent de636ed71441d5dd25d5e73eb0bc1cb808acdc17
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 23 Apr 2026 15:01:53 -0700
m1pp stubs
Diffstat:
| M | m1pp/m1pp.M1 | | | 468 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
1 file changed, 442 insertions(+), 26 deletions(-)
diff --git a/m1pp/m1pp.M1 b/m1pp/m1pp.M1
@@ -2,10 +2,6 @@
##
## Runtime shape: m1pp input.M1 output.M1
##
-## Phase 2: lexer + pass-through + real %macro storage (header, params,
-## body tokens, body limits). Macros are stored but not yet called, so
-## definition-only inputs match the C oracle byte-for-byte.
-##
## Pipeline:
## _start argv/argc from kernel SP; openat+read into input_buf
## -> call lex_source
@@ -14,7 +10,10 @@
## lex_source input_buf -> source_tokens[] (via append_text + push_source_token)
## process_tokens source_tokens[] -> output_buf (via emit_token / emit_newline,
## branching to define_macro at
-## line-start %macro)
+## line-start %macro). Phase 3
+## rewrites this into a stream-
+## driven loop that dispatches
+## the Phase 4+ stubs.
## define_macro parse %macro header+body; record in macros[] + macro_body_tokens[];
## consume through the %endm line without emitting output
##
@@ -40,6 +39,7 @@ DEFINE O_WRONLY_CREAT_TRUNC 4102000000000000
DEFINE MODE_0644 A401000000000000
DEFINE AT_FDCWD 9CFFFFFFFFFFFFFF
DEFINE ZERO32 '0000000000000000000000000000000000000000000000000000000000000000'
+DEFINE ZERO8 '0000000000000000'
DEFINE TOK_WORD 0000000000000000
DEFINE TOK_STRING 0100000000000000
@@ -49,6 +49,62 @@ DEFINE TOK_RPAREN 0400000000000000
DEFINE TOK_COMMA 0500000000000000
DEFINE TOK_PASTE 0600000000000000
+## Token record stride (kind + text_ptr + text_len). Advance a Token* by this.
+DEFINE M1PP_TOK_SIZE 1800000000000000
+
+## --- Phase 3+ data structure sizes (stubbed functions reference these) -------
+## Stream record: 40 bytes. Fields (each 8 bytes):
+## +0 start Token*
+## +8 end Token* (exclusive)
+## +16 pos Token*
+## +24 line_start u64 (1 at entry, 0 after first non-newline emit)
+## +32 pool_mark i64 (byte offset into expand_pool; -1 for source)
+DEFINE M1PP_STREAM_SIZE 2800000000000000
+DEFINE M1PP_STREAM_END_OFF 0800000000000000
+DEFINE M1PP_STREAM_POS_OFF 1000000000000000
+DEFINE M1PP_STREAM_LS_OFF 1800000000000000
+DEFINE M1PP_STREAM_MARK_OFF 2000000000000000
+
+## Stream stack cap: 16 streams × 40 = 640 bytes.
+DEFINE M1PP_STREAM_STACK_CAP 8002000000000000
+
+## Expansion pool: 256 Token slots × 24 bytes = 6144 bytes.
+DEFINE M1PP_EXPAND_CAP 0018000000000000
+
+## ExprFrame record: 144 bytes. Fields:
+## +0 op_code u64
+## +8 argc u64
+## +16 args i64[16] (16 × 8 = 128 bytes)
+DEFINE M1PP_EXPR_FRAME_SIZE 9000000000000000
+DEFINE M1PP_EXPR_ARGC_OFF 0800000000000000
+DEFINE M1PP_EXPR_ARGS_OFF 1000000000000000
+
+## Expr frame stack cap: 16 frames × 144 = 2304 bytes.
+DEFINE M1PP_EXPR_FRAMES_CAP 0009000000000000
+
+## Common cap used by macro params, call args, and expression args.
+DEFINE M1PP_MAX_PARAMS 1000000000000000
+
+## ExprOp codes (indexed by apply_expr_op).
+DEFINE EXPR_ADD 0000000000000000
+DEFINE EXPR_SUB 0100000000000000
+DEFINE EXPR_MUL 0200000000000000
+DEFINE EXPR_DIV 0300000000000000
+DEFINE EXPR_MOD 0400000000000000
+DEFINE EXPR_SHL 0500000000000000
+DEFINE EXPR_SHR 0600000000000000
+DEFINE EXPR_AND 0700000000000000
+DEFINE EXPR_OR 0800000000000000
+DEFINE EXPR_XOR 0900000000000000
+DEFINE EXPR_NOT 0A00000000000000
+DEFINE EXPR_EQ 0B00000000000000
+DEFINE EXPR_NE 0C00000000000000
+DEFINE EXPR_LT 0D00000000000000
+DEFINE EXPR_LE 0E00000000000000
+DEFINE EXPR_GT 0F00000000000000
+DEFINE EXPR_GE 1000000000000000
+DEFINE EXPR_INVALID 1100000000000000
+
## --- Runtime shell: argv, read input, call pipeline, write output, exit ------
:_start
@@ -1132,6 +1188,275 @@ DEFINE TOK_PASTE 0600000000000000
leave
ret
+## ============================================================================
+## --- Phase 3 STUBS: stream stack + expansion-pool lifetime -------------------
+## ============================================================================
+## Phase 3 isolates lifecycle plumbing with no semantic change: after Phase 3
+## lands, process_tokens is rewritten to be stream-driven but only exercises
+## the pass-through + %macro paths, so Phase 2 parity still holds.
+
+## push_stream_span(a0=start_tok, a1=end_tok, a2=pool_mark) -> void (fatal on overflow)
+## Push Stream { start = pos = a0, end = a1, line_start = 1, pool_mark = a2 }
+## onto streams[]. Bumps stream_top. pool_mark is a byte offset into
+## expand_pool, or -1 for a source-owned stream (pop_stream won't rewind).
+## Reads/writes: streams, stream_top. Oracle: m1pp.c:push_stream_span.
+:push_stream_span
+ la_br &err_not_implemented
+ b
+
+## current_stream() -> a0 = &streams[stream_top-1], or 0 if empty. Leaf.
+## Reads: streams, stream_top. Oracle: m1pp.c:current_stream.
+:current_stream
+ la_br &err_not_implemented
+ b
+
+## pop_stream() -> void. Leaf.
+## Decrement stream_top. If the popped stream's pool_mark >= 0, restore
+## pool_used = pool_mark (reclaim the expansion-pool space it used).
+## Reads/writes: streams, stream_top, pool_used. Oracle: m1pp.c:pop_stream.
+:pop_stream
+ la_br &err_not_implemented
+ b
+
+## copy_span_to_pool(a0=start_tok, a1=end_tok) -> void (fatal on pool overflow)
+## Append each 24-byte Token in [start, end) to expand_pool at pool_used,
+## advancing pool_used accordingly.
+## Reads/writes: expand_pool, pool_used. Oracle: m1pp.c:copy_span_to_pool.
+:copy_span_to_pool
+ la_br &err_not_implemented
+ b
+
+## push_pool_stream_from_mark(a0=mark) -> void (fatal on overflow)
+## If pool_used == mark (empty expansion), do nothing and return.
+## Otherwise push_stream_span(expand_pool+mark, expand_pool+pool_used, mark).
+## Reads/writes: expand_pool, pool_used, streams, stream_top.
+## Oracle: m1pp.c:push_pool_stream_from_mark.
+:push_pool_stream_from_mark
+ la_br &err_not_implemented
+ b
+
+## ============================================================================
+## --- Phase 4 STUB: argument parsing ------------------------------------------
+## ============================================================================
+
+## parse_args(a0=lparen_tok, a1=limit_tok) -> void (fatal on unterminated/overflow)
+## Scan tokens from lparen+1 up to limit, tracking paren depth. At depth 1 each
+## TOK_COMMA ends one arg and starts the next; the matching TOK_RPAREN at
+## depth 0 ends the last arg. An empty `()` is arg_count = 0.
+##
+## Writes globals:
+## arg_starts[i] = first token of arg i
+## arg_ends[i] = one past last token of arg i
+## arg_count = number of args (0..16)
+## call_end_pos = one past the closing RPAREN
+##
+## Fatal on: > 16 args, reaching limit without matching RPAREN.
+## Oracle: m1pp.c:parse_args.
+:parse_args
+ la_br &err_not_implemented
+ b
+
+## ============================================================================
+## --- Phase 5 STUBS: macro lookup + call expansion ----------------------------
+## ============================================================================
+
+## find_macro(a0=tok) -> a0 = Macro* or 0. Leaf.
+## Non-zero only if tok is TOK_WORD, text.len >= 2, text[0] == '%', and
+## (text+1, len-1) equals macros[i].name for some i. First match wins.
+## Reads: macros, macros_end. Oracle: m1pp.c:find_macro.
+:find_macro
+ la_br &err_not_implemented
+ b
+
+## find_param(a0=macro_ptr, a1=tok) -> a0 = (index+1) or 0. Leaf.
+## Linear search over macro->params[0..param_count). Non-WORD tok -> 0, so
+## callers can test the return against zero without pre-filtering.
+## Oracle: m1pp.c:find_param.
+:find_param
+ la_br &err_not_implemented
+ b
+
+## copy_arg_tokens_to_pool(a0=arg_start, a1=arg_end) -> void (fatal if empty)
+## Non-leaf (calls copy_span_to_pool). Empty arg is an error.
+## Oracle: m1pp.c:copy_arg_tokens_to_pool.
+:copy_arg_tokens_to_pool
+ la_br &err_not_implemented
+ b
+
+## copy_paste_arg_to_pool(a0=arg_start, a1=arg_end) -> void (fatal unless len 1)
+## Enforces the single-token-argument rule for params adjacent to ##.
+## Oracle: m1pp.c:copy_paste_arg_to_pool.
+:copy_paste_arg_to_pool
+ la_br &err_not_implemented
+ b
+
+## expand_macro_tokens(a0=call_tok, a1=limit, a2=macro_ptr) -> void (fatal on bad)
+## Requires call_tok+1 is TOK_LPAREN. Runs parse_args(call_tok+1, limit),
+## verifies arg_count == macro->param_count, walks macro body, substituting
+## each param token via copy_arg_tokens_to_pool (or copy_paste_arg_to_pool
+## when adjacent to ##), copying other body tokens as-is, then runs
+## paste_pool_range over the newly-written slice.
+##
+## Outputs via globals (callers must snapshot before any nested call that
+## could overwrite them):
+## emt_after_pos = token one past the matching ')' (= call_end_pos)
+## emt_mark = pool_used as of entry (start of expansion slice)
+##
+## Oracle: m1pp.c:expand_macro_tokens.
+:expand_macro_tokens
+ la_br &err_not_implemented
+ b
+
+## expand_call(a0=stream_ptr, a1=macro_ptr) -> void (fatal on bad call)
+## Calls expand_macro_tokens for the call at stream->pos, sets
+## stream->pos = emt_after_pos, stream->line_start = 0, and
+## push_pool_stream_from_mark(emt_mark) to rescan the expansion.
+## Oracle: m1pp.c:expand_call.
+:expand_call
+ la_br &err_not_implemented
+ b
+
+## ============================================================================
+## --- Phase 6 STUBS: ## token paste compaction --------------------------------
+## ============================================================================
+
+## append_pasted_token(a0=dst_tok, a1=left_tok, a2=right_tok) -> void (fatal)
+## Concatenate left->text and right->text into text_buf via append_text and
+## write *dst = { TOK_WORD, new_span }. Practical length limit: fit in the
+## implementation's working buffer (oracle uses 512 bytes).
+## Oracle: m1pp.c:append_pasted_token.
+:append_pasted_token
+ la_br &err_not_implemented
+ b
+
+## paste_pool_range(a0=mark) -> void (fatal on bad paste)
+## In-place compactor over expand_pool[mark..pool_used). For each TOK_PASTE,
+## paste (prev, next) into prev via append_pasted_token and skip both the
+## PASTE and the next token. Copy other tokens forward. Update pool_used to
+## the new end. Fatal if ## is first, last, or adjacent to NEWLINE/PASTE.
+## Oracle: m1pp.c:paste_pool_range.
+:paste_pool_range
+ la_br &err_not_implemented
+ b
+
+## ============================================================================
+## --- Phase 7 STUBS: integer atoms + S-expression evaluator -------------------
+## ============================================================================
+
+## parse_int_token(a0=tok) -> a0 = i64 (fatal on bad). Leaf.
+## Accepts decimal (optional leading '-') and 0x-prefixed hex. For positive
+## inputs the oracle goes through strtoull and reinterprets as i64, so u64
+## values with the high bit set wrap to negative i64.
+## Oracle: m1pp.c:parse_int_token.
+:parse_int_token
+ la_br &err_not_implemented
+ b
+
+## expr_op_code(a0=tok) -> a0 = EXPR_ADD..EXPR_GE, or EXPR_INVALID. Leaf.
+## Accepts operator tokens: + - * / % << >> & | $ ~ = == !=
+## < <= > >=. Note: the oracle spells XOR as "$" (not "^"). Keep parity
+## with the oracle unless docs/M1M-IMPL.md is updated. Non-WORD tok or
+## unknown operator -> EXPR_INVALID.
+## Oracle: m1pp.c:expr_op_code.
+:expr_op_code
+ la_br &err_not_implemented
+ b
+
+## apply_expr_op(a0=op_code, a1=args_ptr, a2=argc) -> a0 = i64 result
+## Reduce args[0..argc) per op (see docs/M1M-IMPL.md "Operators"):
+## + * & | $ variadic, argc >= 1
+## - argc >= 1 (argc == 1 is negate, else left-assoc subtract)
+## / % binary, div-by-zero fatal
+## << >> binary (>> is arithmetic)
+## ~ unary
+## = == != < <= > >= binary
+## Fatal on wrong argc or EXPR_INVALID. Oracle: m1pp.c:apply_expr_op.
+:apply_expr_op
+ la_br &err_not_implemented
+ b
+
+## skip_expr_newlines(a0=pos, a1=end) -> a0 = new pos. Leaf.
+## Advance pos past consecutive TOK_NEWLINE tokens so expressions may span
+## lines. Oracle: m1pp.c:skip_expr_newlines.
+:skip_expr_newlines
+ la_br &err_not_implemented
+ b
+
+## eval_expr_atom(a0=tok, a1=limit) -> void
+## Outputs via globals:
+## eval_after_pos = token one past the consumed atom (or one past ')' for
+## a macro atom)
+## eval_value = the atom's i64 value
+##
+## If tok is a defined macro followed by TOK_LPAREN: expand_macro_tokens into
+## the pool at mark = pool_used, recursively eval_expr_range over the new
+## slice, require exactly one value (no trailing tokens), restore
+## pool_used = mark, and set eval_after_pos = emt_after_pos. Otherwise
+## parse_int_token(tok) and set eval_after_pos = tok + 24 bytes.
+##
+## CAVEAT: this path can recurse through eval_expr_range. Callers MUST
+## snapshot eval_after_pos / eval_value into local stack slots (via
+## enter_N) before any further call that might overwrite them.
+## Oracle: m1pp.c:eval_expr_atom.
+:eval_expr_atom
+ la_br &err_not_implemented
+ b
+
+## eval_expr_range(a0=start_tok, a1=end_tok) -> a0 = i64 result (fatal on bad)
+## Main S-expression evaluator loop, driven by an explicit ExprFrame stack
+## (expr_frames[], expr_frame_top) — NOT by P1 recursion. See
+## docs/M1M-IMPL.md "Layer 7: expression evaluator" for the step-by-step
+## loop. Enforces exactly one top-level value and no trailing tokens.
+## Fatal on: unmatched parens, > 16 frames deep, > 16 args per frame,
+## bad atom, bad operator.
+## Reads/writes: expr_frames, expr_frame_top.
+## Oracle: m1pp.c:eval_expr_range.
+:eval_expr_range
+ la_br &err_not_implemented
+ b
+
+## ============================================================================
+## --- Phase 8 STUB: hex emit for !@%$ -----------------------------------------
+## ============================================================================
+
+## emit_hex_value(a0=value_u64, a1=byte_count) -> void (fatal on overflow)
+## byte_count must be 1, 2, 4, or 8. Serialize value into (2 * byte_count)
+## uppercase hex chars, little-endian byte order (byte i at char indices
+## 2i, 2i+1). Emit as a synthesized TOK_WORD via append_text + emit_token.
+## Oracle: m1pp.c:emit_hex_value.
+:emit_hex_value
+ la_br &err_not_implemented
+ b
+
+## ============================================================================
+## --- Phase 8-9 STUB: builtin dispatcher ( ! @ % $ %select ) ------------------
+## ============================================================================
+
+## expand_builtin_call(a0=stream_ptr, a1=builtin_tok) -> void (fatal on bad)
+## Requires builtin_tok+1 is TOK_LPAREN. Runs parse_args(lparen, stream->end),
+## then dispatches on builtin_tok->text:
+##
+## "!" "@" "%" "$" [Phase 8]
+## require arg_count == 1
+## eval_expr_range(arg_starts[0], arg_ends[0]) -> value
+## stream->pos = call_end_pos; stream->line_start = 0
+## emit_hex_value(value, 1 / 2 / 4 / 8 respectively)
+##
+## "%select" [Phase 9]
+## require arg_count == 3
+## eval_expr_range(cond_arg) -> value
+## chosen = (value != 0) ? arg1 : arg2
+## stream->pos = call_end_pos; stream->line_start = 0
+## if chosen is empty, return (no stream push)
+## else copy_span_to_pool(chosen) and push_pool_stream_from_mark(mark)
+## The unchosen branch is NOT evaluated, validated, or expanded.
+##
+## Any other text under a builtin slot -> fatal "bad builtin".
+## Oracle: m1pp.c:expand_builtin_call.
+:expand_builtin_call
+ la_br &err_not_implemented
+ b
+
## --- Error paths -------------------------------------------------------------
## Each err_* loads a (msg, len) pair for fatal; fatal writes "m1pp: <msg>\n"
## to stderr and exits 1. Error labels are branched to from range/overflow
@@ -1202,6 +1527,11 @@ DEFINE TOK_PASTE 0600000000000000
li_a1 %19 %0
la_br &fatal
b
+:err_not_implemented
+ la_a0 &msg_not_implemented
+ li_a1 %15 %0
+ la_br &fatal
+ b
## fatal(a0=msg_ptr, a1=msg_len): writes "m1pp: <msg>\n" to stderr, exits 1.
## Saves args across the three syscalls since a0..a3 are caller-saved.
@@ -1265,6 +1595,7 @@ DEFINE TOK_PASTE 0600000000000000
:msg_bad_macro_header "bad macro header"
:msg_too_many_macros "too many macros"
:msg_macro_body_overflow "macro body overflow"
+:msg_not_implemented "not implemented"
## --- BSS ---------------------------------------------------------------------
## Placed before :ELF_end so filesz/memsz (which this ELF header sets equal)
@@ -1277,47 +1608,84 @@ DEFINE TOK_PASTE 0600000000000000
## Scalars (each 8 bytes).
:input_fd
-ZERO32
+ZERO8
:input_len
-ZERO32
+ZERO8
:output_fd
-ZERO32
+ZERO8
:output_used
-ZERO32
+ZERO8
:output_written
-ZERO32
+ZERO8
:output_need_space
-ZERO32
+ZERO8
:output_path
-ZERO32
+ZERO8
:text_used
-ZERO32
+ZERO8
:source_end
-ZERO32
+ZERO8
:lex_ptr
-ZERO32
+ZERO8
:lex_start
-ZERO32
+ZERO8
:lex_quote
-ZERO32
+ZERO8
:proc_pos
-ZERO32
+ZERO8
:proc_line_start
-ZERO32
+ZERO8
:macros_end
-ZERO32
+ZERO8
:macro_body_end
-ZERO32
+ZERO8
:def_m_ptr
-ZERO32
+ZERO8
:def_param_ptr
-ZERO32
+ZERO8
:def_body_line_start
-ZERO32
+ZERO8
:err_saved_msg
-ZERO32
+ZERO8
:err_saved_len
-ZERO32
+ZERO8
+
+## Phase 3+ scalars. Each is one u64 (ZERO8).
+## pool_used — byte offset into expand_pool (i.e. next write slot).
+## stream_top — stream stack depth (0 == empty).
+## arg_count — number of args produced by the most recent parse_args.
+## call_end_pos — Token* one past the ')' of that call.
+## expr_frame_top — ExprFrame stack depth inside eval_expr_range.
+## emt_after_pos, emt_mark — expand_macro_tokens output slots (Token* and
+## byte offset into expand_pool).
+## eval_after_pos, eval_value — eval_expr_atom output slots (Token* and i64).
+## Callers MUST snapshot these before any nested
+## eval_* call that could overwrite them.
+:pool_used
+ZERO8
+:stream_top
+ZERO8
+:arg_count
+ZERO8
+:call_end_pos
+ZERO8
+:expr_frame_top
+ZERO8
+:emt_after_pos
+ZERO8
+:emt_mark
+ZERO8
+:eval_after_pos
+ZERO8
+:eval_value
+ZERO8
+
+## arg_starts[16] / arg_ends[16]: 16 × 8 = 128 bytes each, i.e. 4 ZERO32.
+## Written by parse_args; read by expand_macro_tokens and expand_builtin_call.
+:arg_starts
+ZERO32 ZERO32 ZERO32 ZERO32
+:arg_ends
+ZERO32 ZERO32 ZERO32 ZERO32
## input_buf: 8 KB (M1PP_INPUT_CAP)
:input_buf
@@ -1505,4 +1873,52 @@ ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+## streams: 16 Stream records × 40 bytes = 640 bytes (M1PP_STREAM_STACK_CAP).
+## 20 ZERO32 = 2 lines of 8 + 1 line of 4.
+:streams
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32
+
+## expand_pool: 256 Token slots × 24 bytes = 6144 bytes (M1PP_EXPAND_CAP).
+## 24 lines × 8 ZERO32 = 192 ZERO32.
+:expand_pool
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+
+## expr_frames: 16 × 144 bytes = 2304 bytes (M1PP_EXPR_FRAMES_CAP).
+## 9 lines × 8 ZERO32 = 72 ZERO32.
+:expr_frames
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32 ZERO32
+
:ELF_end