boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs

commit 9507a3668cd1218bae1aa0f171e7991cc8768afd
parent de636ed71441d5dd25d5e73eb0bc1cb808acdc17
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 23 Apr 2026 15:01:53 -0700

m1pp stubs

Diffstat:
Mm1pp/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