commit ba016341a809dc6bc748baa48219eabb8bf8fdea
parent dca693c3073799b8d46cc9f6479839ea4c8d913d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 23 Apr 2026 17:06:38 -0700
m1pp: add %str stringification builtin
Diffstat:
6 files changed, 111 insertions(+), 1 deletion(-)
diff --git a/m1pp/m1pp.c b/m1pp/m1pp.c
@@ -13,6 +13,7 @@
* %(expr) evaluate an integer S-expression, emit LE 32-bit hex
* $(expr) evaluate an integer S-expression, emit LE 64-bit hex
* %select(c,t,e) evaluate condition S-expression; expand t if nonzero else e
+ * %str(IDENT) stringify a single WORD token into a "..."-quoted literal
*
* Expression syntax is intentionally Lisp-shaped:
* atoms: decimal or 0x-prefixed integer literals
@@ -1157,6 +1158,46 @@ static int expand_builtin_call(struct Stream *s, const struct Token *tok)
return push_pool_stream_from_mark(mark);
}
+ if (token_text_eq(tok, "%str")) {
+ struct Token *arg_tok;
+ struct Token *end_pos;
+ struct Token out_tok;
+ char *text_ptr;
+ int orig_len;
+ int out_len;
+
+ if (arg_count != 1) {
+ return fail("bad builtin");
+ }
+ if (arg_ends[0] - arg_starts[0] != 1) {
+ return fail("bad builtin");
+ }
+ arg_tok = arg_starts[0];
+ if (arg_tok->kind != TOK_WORD) {
+ return fail("bad builtin");
+ }
+ end_pos = call_end_pos;
+
+ orig_len = arg_tok->text.len;
+ out_len = orig_len + 2;
+ if (text_used + out_len + 1 > MAX_TEXT) {
+ return fail("text overflow");
+ }
+ text_ptr = text_buf + text_used;
+ text_buf[text_used++] = '"';
+ memcpy(text_buf + text_used, arg_tok->text.ptr, (size_t)orig_len);
+ text_used += orig_len;
+ text_buf[text_used++] = '"';
+ text_buf[text_used++] = '\0';
+
+ out_tok.kind = TOK_STRING;
+ out_tok.text.ptr = text_ptr;
+ out_tok.text.len = out_len;
+ s->pos = end_pos;
+ s->line_start = 0;
+ return emit_token(&out_tok);
+ }
+
return fail("bad builtin");
}
@@ -1220,7 +1261,8 @@ static int process_tokens(void)
token_text_eq(tok, "@") ||
token_text_eq(tok, "%") ||
token_text_eq(tok, "$") ||
- token_text_eq(tok, "%select"))) {
+ token_text_eq(tok, "%select") ||
+ token_text_eq(tok, "%str"))) {
if (!expand_builtin_call(s, tok)) {
return 0;
}
diff --git a/tests/m1pp/14-str-builtin.M1pp b/tests/m1pp/14-str-builtin.M1pp
@@ -0,0 +1,17 @@
+# Phase 14 %str stringification builtin.
+# - %str(IDENT) wraps the identifier text in double quotes
+# - result is a TOK_STRING, byte-identical to a hand-written literal
+
+%macro quoteit(name)
+%str(name)
+%endm
+
+%quoteit(hello)
+%quoteit(foo_bar)
+%quoteit(a)
+
+# Control: hand-written literals must match the macro-generated form.
+"hello"
+"foo_bar"
+"a"
+END
diff --git a/tests/m1pp/14-str-builtin.expected b/tests/m1pp/14-str-builtin.expected
@@ -0,0 +1,17 @@
+
+
+
+
+
+"hello"
+
+"foo_bar"
+
+"a"
+
+
+
+"hello"
+"foo_bar"
+"a"
+END
diff --git a/tests/m1pp/14-str-paste.M1pp b/tests/m1pp/14-str-paste.M1pp
@@ -0,0 +1,13 @@
+# Phase 14 paste + stringify.
+# - `##` joins word fragments: str_##n -> str_quote (TOK_WORD).
+# - `%str(n)` wraps the same identifier in quotes (TOK_STRING).
+# - Complementary operators: paste builds the label, %str builds the literal.
+
+%macro defsym(n)
+:str_ ## n %str(n)
+%endm
+
+%defsym(quote)
+%defsym(if)
+%defsym(begin)
+END
diff --git a/tests/m1pp/14-str-paste.expected b/tests/m1pp/14-str-paste.expected
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+:str_quote "quote"
+
+:str_if "if"
+
+:str_begin "begin"
+
+END
diff --git a/tests/m1pp/_14-str-malformed.M1pp b/tests/m1pp/_14-str-malformed.M1pp
@@ -0,0 +1,8 @@
+# Phase 14 %str malformed input.
+# - Underscore-prefix => skipped by test.sh.
+# - Expected outcome: m1pp exits non-zero.
+# - %str takes exactly one single-token WORD argument. A multi-token
+# argument (`a b`) must be rejected; so must an already-string arg
+# (`"already_string"`). This fixture exercises the multi-token path.
+
+%str(a b)