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:
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