commit d0d9ec33dc17c2c446a8727ab09149cad0997fcc parent 1857b6a6348084d906fc93dfec79329ffd4cd439 Author: Ryan Sepassi <rsepassi@gmail.com> Date: Sun, 3 May 2026 14:54:18 -0700 M1pp test updates Diffstat:
34 files changed, 408 insertions(+), 432 deletions(-)
diff --git a/M1pp/M1pp.P1 b/M1pp/M1pp.P1 @@ -599,6 +599,24 @@ DEFINE OFF_local_lookup_scratch 0052850000000000 # if (c == '\0') finish (unterminated; keep what we have) la_br &lex_string_finish beqz_a0 + # if (c == '\\' && lex_ptr[1] != '\0') skip both bytes as a unit so + # `\"` and `\\` don't accidentally terminate the string. Decoding + # the escape's *meaning* (e.g. for %bytes) happens later — here we + # only care about token boundaries. + li_a1 %92 %0 + la_br &lex_string_check_backslash + beq_a0,a1 + la_br &lex_string_no_escape + b +:lex_string_check_backslash + addi_a1,t0,1 + lb_a1,a1,0 + la_br &lex_string_no_escape + beqz_a1 + addi_t0,t0,2 + la_br &lex_string_scan + b +:lex_string_no_escape # if (c == quote) consume closing quote and finish la_a1 &lex_quote ld_a1,a1,0 @@ -5935,7 +5953,10 @@ DEFINE OFF_local_lookup_scratch 0052850000000000 ## TOK_STRING, len >= 2, ptr[0] == '"'. For \xNN, the next two source ## bytes must both be valid hex digits (0-9, a-f, A-F). :ebc_bytes_handler - enter_0 + # No enter_0: ebc_bytes_handler is jumped to as a continuation of + # expand_builtin_call's frame (matching the convention used by + # :ebc_str, :ebc_select, etc.). The terminating `eret` at + # :ebc_b_done unwinds expand_builtin_call's frame. # require arg_count == 1 la_a0 &arg_count diff --git a/M1pp/M1pp.c b/M1pp/M1pp.c @@ -325,6 +325,19 @@ static int lex_source(const char *src) start = i; i++; while (src[i] != '\0' && src[i] != quote) { + if (src[i] == '\\' && src[i + 1] != '\0') { + /* Skip backslash + next char as a unit so the + * close-quote test doesn't fire on `\"`, and so + * `\\` doesn't leave the trailing `\` to start a + * spurious escape. The escape's *meaning* is + * decoded later (e.g. by %bytes); the lexer only + * cares about token boundaries. */ + if (src[i + 1] == '\n') { + line++; + } + i += 2; + continue; + } if (src[i] == '\n') { line++; } @@ -1701,18 +1714,18 @@ static int expand_builtin_call(struct Stream *s, const struct Token *tok) } if (token_text_eq(tok, "%bytes")) { - /* Emit the raw bytes of a "..." string as one WORD of contiguous - * hex digits. Recognised escapes: \n \t \r \0 \\ \" and \xNN. - * No NUL is appended; the caller writes one explicitly if needed. */ + /* Emit the raw bytes of a "..." string as a sequence of one + * `TOK_WORD` per byte (each token's text is the two hex digits + * for that byte). Recognised escapes: \n \t \r \0 \\ \" and + * \xNN. No NUL is appended; the caller writes one explicitly + * if needed. hex2pp's parse_byte_stream coalesces hex digits + * across whitespace, so the emitted tokens reassemble into a + * contiguous byte sequence at link time. */ struct Token *arg_tok; struct Token *end_pos; - struct Token out_tok; - static const char hex[] = "0123456789ABCDEF"; - char *text_ptr; const char *src; int src_len; int src_i; - int hex_len; if (arg_count != 1) { return fail("bad builtin"); @@ -1726,14 +1739,11 @@ static int expand_builtin_call(struct Stream *s, const struct Token *tok) return fail("bad builtin"); } end_pos = call_end_pos; + s->pos = end_pos; + s->line_start = 0; src = arg_tok->text.ptr + 1; src_len = arg_tok->text.len - 2; - if (text_used + 2 * src_len + 1 > MAX_TEXT) { - return fail("text overflow"); - } - text_ptr = text_buf + text_used; - hex_len = 0; src_i = 0; while (src_i < src_len) { unsigned int b; @@ -1773,23 +1783,11 @@ static int expand_builtin_call(struct Stream *s, const struct Token *tok) } else { b = (unsigned char)c; } - text_buf[text_used++] = hex[(b >> 4) & 0xF]; - text_buf[text_used++] = hex[b & 0xF]; - hex_len += 2; - } - text_buf[text_used++] = '\0'; - - out_tok.kind = TOK_WORD; - out_tok.tight = 0; - out_tok.line = current_line; - out_tok.text.ptr = text_ptr; - out_tok.text.len = hex_len; - s->pos = end_pos; - s->line_start = 0; - if (hex_len == 0) { - return 1; + if (!emit_hex_value((unsigned long long)b, 1)) { + return 0; + } } - return emit_token(&out_tok); + return 1; } if (token_text_eq(tok, "%str")) { diff --git a/docs/M1PP.md b/docs/M1PP.md @@ -51,7 +51,12 @@ is written whole from another (`MAX_OUTPUT` bytes). The lexer produces a flat token array. Token kinds: - `WORD` — any run of non-special characters -- `STRING` — `"..."` or `'...'` (quotes included in the token text) +- `STRING` — `"..."` or `'...'` (quotes included in the token text). + Inside a string, a backslash plus the next character is consumed as + one unit, so `\"` and `\\` do not end the literal. The escape's + *meaning* is decoded later by whoever interprets the bytes (e.g. + `%bytes` decodes `\n`, `\xNN`, etc.); the lexer only uses the + backslash to find the right closing quote. - `NEWLINE` — a single `\n` - `LPAREN`, `RPAREN`, `COMMA`, `LBRACE`, `RBRACE` - `PASTE` — the `##` marker @@ -181,16 +186,18 @@ Stringifies a single `WORD` token into a double-quoted string literal: ### `%bytes(STRING)` -Emits the raw bytes of a `"..."`-quoted string as a single contiguous -hex word (consumed as bytes by `hex2++`). No NUL terminator is appended; -write `00` explicitly if you need one. Recognised escapes inside the -string are: +Emits the raw bytes of a `"..."`-quoted string as a sequence of +two-hex-digit `WORD` tokens — one per byte. `hex2++` coalesces hex +digits across whitespace, so the result reassembles into a contiguous +byte sequence at link time. No NUL terminator is appended; write `00` +explicitly if you need one. Recognised escapes inside the string are: \n 0x0A \t 0x09 \r 0x0D \0 0x00 \\ 0x5C \" 0x22 \xNN byte NN (two hex digits) Any other backslash escape is an error. The argument must be exactly one -`STRING` token quoted with `"`. Example: `%bytes("hi\n")` emits `68690A`. +`STRING` token quoted with `"`. Example: `%bytes("hi\n")` emits +`68 69 0A`. ### `%local(NAME)` diff --git a/tests/M1pp/03-builtins.expected b/tests/M1pp/03-builtins.expected @@ -4,12 +4,12 @@ -'7F' -'FF' -'3412' -'FFFF' -'78563412' -'FFFFFFFF' -'8877665544332211' -'0000000000000080' +7F +FF +3412 +FFFF +78563412 +FFFFFFFF +8877665544332211 +0000000000000080 END diff --git a/tests/M1pp/04-expr-ops.expected b/tests/M1pp/04-expr-ops.expected @@ -1,43 +1,43 @@ -'0F00000000000000' -'1800000000000000' -'0700000000000000' -'7000000000000000' -'0000000000000000' +0F00000000000000 +1800000000000000 +0700000000000000 +7000000000000000 +0000000000000000 -'F9FFFFFFFFFFFFFF' -'2800000000000000' +F9FFFFFFFFFFFFFF +2800000000000000 -'8E00000000000000' -'0600000000000000' +8E00000000000000 +0600000000000000 -'0000010000000000' -'0000000800000000' +0000010000000000 +0000000800000000 -'FFFFFFFFFFFFFFFF' +FFFFFFFFFFFFFFFF -'0100000000000000' -'0000000000000000' -'0100000000000000' -'0100000000000000' -'0100000000000000' -'0100000000000000' -'0000000000000000' +0100000000000000 +0000000000000000 +0100000000000000 +0100000000000000 +0100000000000000 +0100000000000000 +0000000000000000 -'0D00000000000000' +0D00000000000000 -'05000000' -'06000000' -'01' -'00000000' +05000000 +06000000 +01 +00000000 END diff --git a/tests/M1pp/05-int-atoms.expected b/tests/M1pp/05-int-atoms.expected @@ -2,12 +2,12 @@ -'0000000000000000' -'2A00000000000000' -'F9FFFFFFFFFFFFFF' -'40420F0000000000' -'FF00000000000000' -'FF00000000000000' -'EFBEADDE00000000' -'0000000000000080' +0000000000000000 +2A00000000000000 +F9FFFFFFFFFFFFFF +40420F0000000000 +FF00000000000000 +FF00000000000000 +EFBEADDE00000000 +0000000000000080 END diff --git a/tests/M1pp/07-rescan.expected b/tests/M1pp/07-rescan.expected @@ -21,6 +21,6 @@ -'0001000000000000' -'1500000000000000' +0001000000000000 +1500000000000000 END diff --git a/tests/M1pp/10-full-parity.expected b/tests/M1pp/10-full-parity.expected @@ -9,8 +9,8 @@ HELLO_WORLD recursed -'01020000' -'2211330000000000' +01020000 +2211330000000000 selected_true selected_else diff --git a/tests/M1pp/13-parenless-control.expected b/tests/M1pp/13-parenless-control.expected @@ -12,8 +12,8 @@ -'18000000' -'18000000' +18000000 +18000000 %foo @@ -22,6 +22,6 @@ %add1 -'2A000000' +2A000000 END diff --git a/tests/M1pp/13-parenless.expected b/tests/M1pp/13-parenless.expected @@ -14,8 +14,8 @@ -'18000000' -'18000000' +18000000 +18000000 %foo @@ -24,6 +24,6 @@ %add1 -'2A000000' +2A000000 END diff --git a/tests/M1pp/15-struct.expected b/tests/M1pp/15-struct.expected @@ -18,19 +18,19 @@ 16 -'74000000' +74000000 -'10000000' +10000000 -'18000000' +18000000 -'20000000' +20000000 -'28000000' +28000000 diff --git a/tests/M1pp/16-enum.expected b/tests/M1pp/16-enum.expected @@ -20,10 +20,10 @@ 7 -'01000000' +01000000 -'01000000' +01000000 0 diff --git a/tests/M1pp/17-scopes.M1pp b/tests/M1pp/17-scopes.M1pp @@ -1,93 +0,0 @@ -# Lexical scopes: `::name` / `&::name` rewrite at emit time against the -# current scope stack, joined by `__`. Resolution is anaphoric: a `::foo` -# token inside a macro body resolves against the caller's scope, not the -# macro's expansion id. -# -# Covers: -# - empty-stack pass-through: `::foo` -> `:foo`, `&::bar` -> `&bar` -# - basic scope: `::start` -> `:parse_number__start` -# - nested scopes: `::a` under [outer, inner] -> `:outer__inner__a` -# - scope name from a macro argument (loop_scoped pattern) -# - anaphoric macros: %break / %continue reach the caller's innermost scope -# - nested scope-introducing macros: innermost scope wins for %break -# - hygienic `:@` and anaphoric `::` coexist in one macro - -%macro loop_scoped(name, body) -%scope name -::top -body -LA_BR &::top -B -::end -%endscope -%endm - -%macro break() -LA_BR &::end -B -%endm - -%macro continue() -LA_BR &::top -B -%endm - -%macro while_scoped_nez(name, ra, body) -%scope name -LA_BR &@top -B -:@body -body -:@top -LA_BR &@body -BNEZ ra -::end -%endscope -%endm - -# Empty-stack pass-through. -::foo -&::bar - -# Basic scope. -%scope parse_number -::start -LA_BR &::done -BEQZ a0 -::done -ERET -%endscope - -# Nested scopes. -%scope outer -::before -%scope inner -::a -LA_BR &::a -B -%endscope -::after -%endscope - -# Scope name from macro arg; anaphoric %break / %continue. -%loop_scoped(scan, { -LI t0, 0 -%break() -%continue() -}) - -# Nested scope-introducing macro intercepts inner %break. -%loop_scoped(outer, { -%loop_scoped(inner, { -%break() -}) -%break() -}) - -# Hygienic `@` and anaphoric `::` together. -%while_scoped_nez(retry, a0, { -LI t1, 1 -%break() -}) - -END diff --git a/tests/M1pp/17-scopes.expected b/tests/M1pp/17-scopes.expected @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - -:foo -&bar - - -:parse_number__start -LA_BR &parse_number__done -BEQZ a0 -:parse_number__done -ERET - - -:outer__before -:outer__inner__a -LA_BR &outer__inner__a -B -:outer__after - - -:scan__top - -LI t0 , 0 -LA_BR &scan__end -B - -LA_BR &scan__top -B - - -LA_BR &scan__top -B -:scan__end - - - -:outer__top - -:outer__inner__top - -LA_BR &outer__inner__end -B - - -LA_BR &outer__inner__top -B -:outer__inner__end - -LA_BR &outer__end -B - - -LA_BR &outer__top -B -:outer__end - - - -LA_BR &top__8 -B -:body__8 - -LI t1 , 1 -LA_BR &retry__end -B - - -:top__8 -LA_BR &body__8 -BNEZ a0 -:retry__end - - -END diff --git a/tests/M1pp/23-one-line-scope.M1pp b/tests/M1pp/23-one-line-scope.M1pp @@ -1,2 +0,0 @@ -%scope S ::start %endscope -done diff --git a/tests/M1pp/23-one-line-scope.expected b/tests/M1pp/23-one-line-scope.expected @@ -1,2 +0,0 @@ -:S__start -done diff --git a/tests/M1pp/25-frame-locals.M1pp b/tests/M1pp/25-frame-locals.M1pp @@ -1,12 +1,13 @@ -# %frame / %endframe + %local(name): frame state separate from the -# %scope stack. %frame sets a single-slot "current frame"; %local(name) -# looks up <frame>_FRAME.<name> (a macro typically synthesized by -# %struct) and emits its body. +# %frame / %endframe + %local(name): set a single-slot "current frame"; +# %local(name) looks up <frame>_FRAME.<name> (typically a %struct- +# generated zero-arg macro) and emits its body. # -# A nested %scope does NOT affect %local resolution. Locals belong to -# the enclosing frame, not the lexical scope, so a function body can -# open inner control-flow scopes without the local namespace shifting -# underneath it. +# Frames are independent of (now-removed) scope handling. They drive +# only the %local lookup. A %frame must be closed by %endframe before +# end-of-input; nesting is forbidden. +# +# Also exercises %local as an expression atom: composes inside %( ... ) +# arithmetic. %struct foo_FRAME { a b c } @@ -15,13 +16,16 @@ %local(b) %local(c) -%scope inner -::label -%local(b) -%endscope - +# %local inside an arithmetic expression — yields offset+100. %((+ %local(b) 100)) %endframe +# A second frame after the first is closed — confirms that frame state +# is properly reset and a fresh frame can be opened. +%struct bar_FRAME { x y } +%frame bar +%local(y) +%endframe + END diff --git a/tests/M1pp/25-frame-locals.expected b/tests/M1pp/25-frame-locals.expected @@ -9,14 +9,17 @@ + 0 8 16 -:inner__label -8 -'6C000000' +6C000000 + + +8 + END diff --git a/tests/M1pp/26-fn2-pattern.M1pp b/tests/M1pp/26-fn2-pattern.M1pp @@ -1,79 +0,0 @@ -# %fn2 / %stl / %ldl — the intended P1pp-side ergonomic pattern built -# on %struct + %frame + %endframe + %local. -# -# %fn2(name, locals, body) synthesizes <name>_FRAME from `locals`, -# emits the function label, opens %scope+%frame around the body, and -# brackets it with %enter sized from <name>_FRAME.SIZE plus %eret. -# -# %stl(reg, local) / %ldl(reg, local) store/load a named local via -# %local(name), which resolves against the currently active frame. -# -# A nested %scope inside the body affects ::label resolution but does -# NOT affect %local — locals stay bound to the function's frame. -# -# %enter, %eret, %st, %ld, %b, %beqz are stubbed as plain text-emitting -# macros so the test isolates the macro-composition behavior rather -# than depending on real P1 encodings. - -%macro enter(size) -ENTER size -%endm - -%macro eret() -ERET -%endm - -%macro st(rs, base, off) -ST rs base off -%endm - -%macro ld(rd, base, off) -LD rd base off -%endm - -%macro b(target) -B target -%endm - -%macro beqz(rs, target) -BEQZ rs target -%endm - -%macro fn2(name, locals, body) -%struct name ## _FRAME { locals } -LABEL name -%scope name -%frame name -%enter(% ## name ## _FRAME.SIZE) -body -%eret -%endframe -%endscope -%endm - -%macro stl(reg, local) -%st(reg, sp, %local(local)) -%endm - -%macro ldl(reg, local) -%ld(reg, sp, %local(local)) -%endm - -# A function with three named locals. Demonstrates: -# - %enter sized from the synthesized print_FRAME.SIZE = 24 -# - %stl/%ldl resolving names a/b/c to offsets 0/8/16 -# - a nested %scope (loop) for ::label resolution coexisting with the -# outer %frame, so %ldl(s0, a) inside the loop still hits offset 0 -# (print_FRAME.a) rather than anything tied to the loop scope. -%fn2(print, {a b c}, { -%stl(s0, a) -%stl(s1, b) -%stl(s2, c) -%scope loop -::top -%ldl(s0, a) -%beqz(s0, &::top) -%endscope -}) - -END diff --git a/tests/M1pp/26-fn2-pattern.expected b/tests/M1pp/26-fn2-pattern.expected @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -LABEL print -ENTER 24 - - -ST s0 sp 0 - - -ST s1 sp 8 - - -ST s2 sp 16 - - -:print__loop__top -LD s0 sp 0 - - -BEQZ s0 &print__loop__top - - -ERET - - - -END diff --git a/tests/M1pp/27-bytes.M1pp b/tests/M1pp/27-bytes.M1pp @@ -0,0 +1,17 @@ +# %bytes(STRING): emit raw bytes of a "..." literal as contiguous hex. +# Recognised escapes: \n \t \r \0 \\ \" and \xNN. + +# Plain ASCII. +%bytes("hi") + +# Empty string emits nothing. +%bytes("") + +# Each supported single-char escape, plus \xNN. +%bytes("a\nb\tc\rd\0e\\f\"g\x7Fh") + +# Followed by trailing literal hex, to confirm hex2pp's byte-stream +# coalescing handles the boundary. +%bytes("X") 90 + +END diff --git a/tests/M1pp/27-bytes.expected b/tests/M1pp/27-bytes.expected @@ -0,0 +1,17 @@ + + + + +68 69 + + + + + +61 0A 62 09 63 0D 64 00 65 5C 66 22 67 7F 68 + + + +58 90 + +END diff --git a/tests/M1pp/28-select-cond-from-macro.M1pp b/tests/M1pp/28-select-cond-from-macro.M1pp @@ -0,0 +1,28 @@ +# %select cond produced by a macro call. +# - The cond argument is itself a %macro that expands to an integer +# expression. parse_args walks the call's tokens raw, but the cond +# is then evaluated as an expression which expands zero-arg macros +# via the same atom path used by %(...) arithmetic. +# - Both truthy and falsy macro-produced conds are exercised. + +%macro ONE() +1 +%endm + +%macro ZERO() +0 +%endm + +%macro EQ_EXPR() +(= 4 4) +%endm + +%macro NE_EXPR() +(!= 4 4) +%endm + +%select(%ONE, picked_one, skipped) +%select(%ZERO, skipped, picked_zero) +%select(%EQ_EXPR, picked_eq, skipped) +%select(%NE_EXPR, skipped, picked_ne) +END diff --git a/tests/M1pp/28-select-cond-from-macro.expected b/tests/M1pp/28-select-cond-from-macro.expected @@ -0,0 +1,16 @@ + + + + + + + + + + + +picked_one +picked_zero +picked_eq +picked_ne +END diff --git a/tests/M1pp/29-string-escapes.M1pp b/tests/M1pp/29-string-escapes.M1pp @@ -0,0 +1,23 @@ +# Lexer-level string escape preservation outside %bytes. +# - In a plain STRING token the lexer treats `\X` as one unit when +# finding the closing quote, so `\"` and `\\` do NOT terminate the +# literal. The escape's *meaning* is left to the consumer; the +# emitter writes the bytes back verbatim. +# - Both quote styles ("..." and '...') participate. +# - %str composition: the ## paste compactor sees the STRING as one +# token and passes it through alongside a paste-built WORD. + +"plain" +"with \"quoted\" inside" +"trailing backslash pair: \\" +"mixed \\ and \" together" +'single \"quoted\" inside' +'single \\ pair' + +%macro emit(s) +s +%endm + +%emit("arg with \"escaped\" quotes") +%emit("arg with \\ backslash pair") +END diff --git a/tests/M1pp/29-string-escapes.expected b/tests/M1pp/29-string-escapes.expected @@ -0,0 +1,22 @@ + + + + + + + + + +"plain" +"with \"quoted\" inside" +"trailing backslash pair: \\" +"mixed \\ and \" together" +'single \"quoted\" inside' +'single \\ pair' + + +"arg with \"escaped\" quotes" + +"arg with \\ backslash pair" + +END diff --git a/tests/M1pp/30-struct-comma-fields.M1pp b/tests/M1pp/30-struct-comma-fields.M1pp @@ -0,0 +1,31 @@ +# %struct / %enum field separators: doc says fields may be separated by +# whitespace, commas, or newlines. Other fixtures cover whitespace and +# newlines; this one exercises the comma-separated form, plus the mixed +# "commas and whitespace and newlines all at once" case. + +%struct comma_only { a, b, c, d } +%comma_only.a +%comma_only.b +%comma_only.c +%comma_only.d +%comma_only.SIZE + +%enum comma_enum { red, green, blue } +%comma_enum.red +%comma_enum.green +%comma_enum.blue +%comma_enum.COUNT + +# Mixed separators — commas adjacent to whitespace and newlines. +%struct mixed { + f0, f1 + f2,f3, + f4 +} +%mixed.f0 +%mixed.f1 +%mixed.f2 +%mixed.f3 +%mixed.f4 +%mixed.SIZE +END diff --git a/tests/M1pp/30-struct-comma-fields.expected b/tests/M1pp/30-struct-comma-fields.expected @@ -0,0 +1,24 @@ + + + + + +0 +8 +16 +24 +32 + +0 +1 +2 +3 + + +0 +8 +16 +24 +32 +40 +END diff --git a/tests/M1pp/31-bytes-via-macro.M1pp b/tests/M1pp/31-bytes-via-macro.M1pp @@ -0,0 +1,27 @@ +# %bytes appearing inside a macro body, so the builtin runs against +# rescanned/expanded input rather than top-level source. The string +# argument may be a literal or a parameter-substituted STRING token. + +%macro EMIT_BYTES(s) +%bytes(s) +%endm + +%macro PREFIXED(s) +AA +%bytes(s) +BB +%endm + +# Literal string passed through a macro arg. +%EMIT_BYTES("hi") + +# Surrounded by literal hex inside the body, to confirm hex2pp's +# byte-stream coalescing works across the rescan boundary. +%PREFIXED("ok") + +# Each escape exercised again, but via macro substitution. +%EMIT_BYTES("a\nb") + +# Empty string substituted in. +%EMIT_BYTES("") +END diff --git a/tests/M1pp/31-bytes-via-macro.expected b/tests/M1pp/31-bytes-via-macro.expected @@ -0,0 +1,25 @@ + + + + + + + +68 69 + + + + +AA +6F 6B +BB + + + +61 0A 62 + + + + + +END diff --git a/tests/M1pp/_27-bytes-bad-escape.M1pp b/tests/M1pp/_27-bytes-bad-escape.M1pp @@ -0,0 +1,10 @@ +# %bytes: unknown backslash escape must be rejected. +# +# Recognised escapes: \n \t \r \0 \\ \" and \xNN. Anything else +# (here `\q`) is an error. Expected outcome: m1pp exits non-zero +# with "bad escape". +# +# (Underscore-prefixed filename so test.sh skips this fixture.) + +%bytes("oops\q") +END diff --git a/tests/M1pp/_27-bytes-bad-hex.M1pp b/tests/M1pp/_27-bytes-bad-hex.M1pp @@ -0,0 +1,9 @@ +# %bytes: \xNN requires exactly two hex digits. A non-hex char in +# either position is an error. +# +# Expected outcome: m1pp exits non-zero with "bad escape". +# +# (Underscore-prefixed filename so test.sh skips this fixture.) + +%bytes("oops\xZZ") +END diff --git a/tests/M1pp/_27-bytes-not-string.M1pp b/tests/M1pp/_27-bytes-not-string.M1pp @@ -0,0 +1,9 @@ +# %bytes argument must be a single TOK_STRING token quoted with `"`. +# A WORD argument must be rejected. +# +# Expected outcome: m1pp exits non-zero with "bad builtin". +# +# (Underscore-prefixed filename so test.sh skips this fixture.) + +%bytes(notastring) +END diff --git a/tests/M1pp/_27-bytes-truncated-hex.M1pp b/tests/M1pp/_27-bytes-truncated-hex.M1pp @@ -0,0 +1,9 @@ +# %bytes: \xNN requires exactly two hex digits. A single trailing hex +# digit at the close-quote position must be rejected. +# +# Expected outcome: m1pp exits non-zero with "bad escape". +# +# (Underscore-prefixed filename so test.sh skips this fixture.) + +%bytes("oops\x7") +END