boot2

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

commit 968ba690fb1865bd72df6f2486c58db5807445f6
parent dca693c3073799b8d46cc9f6479839ea4c8d913d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 23 Apr 2026 17:05:37 -0700

m1pp: add braced block arguments

Diffstat:
Mm1pp/m1pp.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Atests/m1pp/12-braced-args.M1pp | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/m1pp/12-braced-args.expected | 45+++++++++++++++++++++++++++++++++++++++++++++
Atests/m1pp/_12-braced-malformed.M1pp | 17+++++++++++++++++
4 files changed, 192 insertions(+), 2 deletions(-)

diff --git a/m1pp/m1pp.c b/m1pp/m1pp.c @@ -81,7 +81,9 @@ enum { TOK_LPAREN, TOK_RPAREN, TOK_COMMA, - TOK_PASTE + TOK_PASTE, + TOK_LBRACE, + TOK_RBRACE }; enum ExprOp { @@ -305,6 +307,22 @@ static int lex_source(const char *src) i++; continue; } + if (src[i] == '{') { + if (!push_token(source_tokens, &source_count, MAX_TOKENS, + TOK_LBRACE, (struct TextSpan){src + i, 1})) { + return 0; + } + i++; + continue; + } + if (src[i] == '}') { + if (!push_token(source_tokens, &source_count, MAX_TOKENS, + TOK_RBRACE, (struct TextSpan){src + i, 1})) { + return 0; + } + i++; + continue; + } start = i; while (src[i] != '\0' && @@ -315,6 +333,8 @@ static int lex_source(const char *src) src[i] != '(' && src[i] != ')' && src[i] != ',' && + src[i] != '{' && + src[i] != '}' && !(src[i] == '#' && src[i + 1] == '#')) { i++; } @@ -376,6 +396,9 @@ static int emit_newline(void) static int emit_token(const struct Token *tok) { + if (tok->kind == TOK_LBRACE || tok->kind == TOK_RBRACE) { + return 1; + } if (output_need_space) { if (output_used + 1 >= MAX_OUTPUT) { return fail("output overflow"); @@ -545,6 +568,7 @@ static int parse_args(struct Token *lparen, struct Token *limit) struct Token *tok = lparen + 1; struct Token *arg_start = tok; int depth = 1; + int brace_depth = 0; int arg_index = 0; while (tok < limit) { @@ -556,6 +580,9 @@ static int parse_args(struct Token *lparen, struct Token *limit) if (tok->kind == TOK_RPAREN) { depth--; if (depth == 0) { + if (brace_depth != 0) { + return fail("unbalanced braces"); + } if (arg_start == tok && arg_index == 0) { arg_count = 0; } else { @@ -572,7 +599,20 @@ static int parse_args(struct Token *lparen, struct Token *limit) tok++; continue; } - if (tok->kind == TOK_COMMA && depth == 1) { + if (tok->kind == TOK_LBRACE) { + brace_depth++; + tok++; + continue; + } + if (tok->kind == TOK_RBRACE) { + if (brace_depth <= 0) { + return fail("unbalanced braces"); + } + brace_depth--; + tok++; + continue; + } + if (tok->kind == TOK_COMMA && depth == 1 && brace_depth == 0) { if (arg_index >= MAX_PARAMS) { return fail("too many args"); } @@ -589,16 +629,54 @@ static int parse_args(struct Token *lparen, struct Token *limit) return fail("unterminated macro call"); } +static int arg_is_braced(struct TokenSpan span) +{ + struct Token *tok; + int depth; + + if (span.end - span.start < 2) { + return 0; + } + if (span.start->kind != TOK_LBRACE || + (span.end - 1)->kind != TOK_RBRACE) { + return 0; + } + depth = 0; + for (tok = span.start; tok < span.end; tok++) { + if (tok->kind == TOK_LBRACE) { + depth++; + } else if (tok->kind == TOK_RBRACE) { + depth--; + if (depth == 0 && tok != span.end - 1) { + return 0; + } + } + } + return depth == 0; +} + static int copy_arg_tokens_to_pool(struct TokenSpan span) { if (span.start == span.end) { return fail("bad macro argument"); } + if (arg_is_braced(span)) { + struct TokenSpan inner; + inner.start = span.start + 1; + inner.end = span.end - 1; + if (inner.start == inner.end) { + return 1; + } + return copy_span_to_pool(inner); + } return copy_span_to_pool(span); } static int copy_paste_arg_to_pool(struct TokenSpan span) { + if (arg_is_braced(span)) { + return fail("bad macro argument"); + } if (span.end - span.start != 1) { return fail("bad macro argument"); } diff --git a/tests/m1pp/12-braced-args.M1pp b/tests/m1pp/12-braced-args.M1pp @@ -0,0 +1,50 @@ +# Braced block arguments (§2 of M1PP-EXT): +# - { ... } groups tokens into one arg, protecting commas inside +# - outer { ... } is stripped when the arg span begins with LBRACE and +# ends with its matching RBRACE +# - nesting: { { ... } } — outer is stripped, inner braces pass through +# (emit_token is a no-op on brace kinds, so inner braces never reach +# output either) +# - braces are independent of parens: st(r0, r3, 0) inside a braced arg +# is a single group, its commas are NOT arg separators +# - plain (non-braced) args still work unchanged + +%macro IF_EQ_ELSE(a, b, t, e) +(= a b) t e +%endm + +%macro WHILE_NEZ(r, body) +:loop__ +body +bnez r :loop__ +%endm + +%macro ID(x) +x +%endm + +# body with commas inside a brace — st(r0, r3, 0) carries two commas that +# MUST NOT split the outer call into more than 4 args +%IF_EQ_ELSE(r1, r2, { +li(r0, 5) +st(r0, r3, 0) +}, { +li(r0, 0) +}) + +# nested braces — inner { inner_block } survives outer strip but its +# braces are no-op'd at emit time, so only the tokens appear +%WHILE_NEZ(rx, { +addi(rx, rx, -1) +{ inner_block } +}) + +# plain arg with no braces still works (sanity) +%ID(plain_token) + +# arg that opens with { but does not close at the outer level is NOT +# stripped: { x } tail — the { and } are emitted as nothing (emit_token +# no-op) but the surrounding tokens pass through verbatim +%ID({ x } tail) + +END diff --git a/tests/m1pp/12-braced-args.expected b/tests/m1pp/12-braced-args.expected @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + +( = r1 r2 ) +li ( r0 , 5 ) +st ( r0 , r3 , 0 ) + +li ( r0 , 0 ) + + + + + +:loop__ + +addi ( rx , rx , -1 ) +inner_block + +bnez rx :loop__ + + + +plain_token + + + + + +x tail + + +END diff --git a/tests/m1pp/_12-braced-malformed.M1pp b/tests/m1pp/_12-braced-malformed.M1pp @@ -0,0 +1,17 @@ +# Malformed: unmatched `{` inside a macro call. +# +# Expected behavior: the m1pp expander MUST exit non-zero. parse_args detects +# that the outer RPAREN closes the call while brace_depth is still > 0 and +# reports "unbalanced braces". +# +# No `.expected` file is needed — the leading underscore in the filename +# causes m1pp/test.sh to skip this fixture. It is verified manually via the +# verification block in the §2 implementation notes. + +%macro F(a, b) +a b +%endm + +%F(first, { never_closed ) + +END