commit aa49b4c0bc19392da1ef6159929122d5e7cdcb77
parent 8fa59182120ef726749e340bca33f554eee98588
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 29 May 2026 19:28:32 -0700
asm: round-trip .L local symbols + complete the unscaled ld/st family
Three gaps surfaced by round-tripping codegen output through cc -S | as:
1. .L local symbols. The assembler rejected .L-prefixed names (string
literals, jump-table bases, static data all reference .Lcfree_* symbols).
Lex a .L-prefixed token — leading and embedded dots included, e.g.
.Lcfree_ro.0 / .L.str — as a single identifier; unambiguous since no
directive starts with .L. The -S symbolizer now emits .L names instead of
falling back to numeric operands (section symbols like .text still defer).
2. Unscaled load/store coverage. The assembler only knew ldur/stur (32/64-bit
GPR); codegen emits sturb/ldurb/sturh/ldurh and the signed ldursb/ldursh/
ldursw. Generalize p_ldur_stur with fixed_size/sign_ext, mirroring
p_ldst_core, and add the mnemonics.
3. Signed unscaled load decode. The disassembler had no rows for the signed
forms (opc=10/11) and keyed register width on size only; add the rows and
key Wt/Xt on opc in print_ldst_simm9.
Regression: new encode case test/asm/encode/aa64_ldur_family. Round-trip
aa64 still 36/0; asm, ISA-unit, and inline-asm corpora green.
Diffstat:
7 files changed, 106 insertions(+), 16 deletions(-)
diff --git a/src/api/asm_emit.c b/src/api/asm_emit.c
@@ -318,8 +318,18 @@ static const char* reloc_modifier(u16 kind, SurgKind* surg) {
}
}
+/* A `.L`-prefixed name is an assembler-local label (e.g. `.Lcfree_ro.0`,
+ * `.Lcfree_jt.0`): the assembler's lexer accepts it as an identifier. Other
+ * `.`-prefixed names (section symbols like `.text`, `.rodata`) are not yet
+ * re-assemblable as operands, so the symbolizer keeps the numeric form. */
+static int sym_is_assemblable(Slice s) {
+ if (s.len == 0) return 0;
+ if (s.s[0] != '.') return 1;
+ return s.len >= 2 && s.s[1] == 'L';
+}
+
/* Build "<mod><sym>[+/-addend]" into buf. Returns length, or -1 if the symbol
- * has no usable name (anonymous, or a `.`-prefixed section/local symbol the
+ * has no usable name (anonymous, or a `.`-prefixed section symbol the
* assembler's expression parser does not accept). */
static int build_symref(char* buf, u32 cap, Compiler* c, const char* mod,
Sym name, i64 addend) {
@@ -327,7 +337,7 @@ static int build_symref(char* buf, u32 cap, Compiler* c, const char* mod,
u32 p = 0, i;
if (!name) return -1;
s = pool_slice(c->global, name);
- if (s.len == 0 || s.s[0] == '.') return -1;
+ if (!sym_is_assemblable(s)) return -1;
for (i = 0; mod[i] && p + 1 < cap; ++i) buf[p++] = mod[i];
for (i = 0; i < s.len && p + 1 < cap; ++i) buf[p++] = s.s[i];
if (addend != 0) {
@@ -493,7 +503,7 @@ static Sym symbol_at(const EmitCtx* x, u32 off) {
for (i = 0; i < x->nlabels; ++i) {
if (x->labels[i].offset == off && x->labels[i].name) {
Slice s = pool_slice(x->c->global, x->labels[i].name);
- if (s.len && s.s[0] != '.') return x->labels[i].name;
+ if (sym_is_assemblable(s)) return x->labels[i].name;
}
}
return (Sym)0;
diff --git a/src/arch/aa64/asm.c b/src/arch/aa64/asm.c
@@ -1229,23 +1229,31 @@ static void p_ldrsb(AsmDriver* d) { p_ldst_core(d, 1, 0, 1); }
static void p_ldrsh(AsmDriver* d) { p_ldst_core(d, 1, 1, 1); }
static void p_ldrsw(AsmDriver* d) { p_ldst_core(d, 1, 2, 1); }
-/* ldur/stur — unscaled signed-imm9. */
-static void p_ldur_stur(AsmDriver* d, int is_load) {
+/* ldur/stur — unscaled signed-imm9. `fixed_size` is the access log2-size
+ * (0=byte..3=dword) for sturb/ldurb/sturh/ldurh/ldursw etc., or -1 to derive
+ * it from the register width (stur/ldur). `sign_ext` selects the signed-load
+ * opc (ldursb/ldursh/ldursw), keyed on the destination register width — the
+ * unscaled mirror of p_ldst_core. */
+static void p_ldur_stur(AsmDriver* d, int is_load, int fixed_size,
+ int sign_ext) {
AA64Reg rt = parse_reg(d);
reject_sp_reg(d, rt, "ldur/stur");
expect_comma(d, "ldur/stur");
AA64Mem m = parse_mem(d);
- u32 size = rt.is64 ? 3u : 2u;
+ u32 size = (fixed_size >= 0) ? (u32)fixed_size : (rt.is64 ? 3u : 2u);
+ u32 opc = !is_load ? AA64_LDST_OPC_STR
+ : !sign_ext ? AA64_LDST_OPC_LDR
+ : rt.is64 ? 2u /* LDURS*, 64-bit dst */
+ : 3u; /* LDURS*, 32-bit dst */
if (m.imm < -256 || m.imm > 255)
asm_driver_panic(d, "asm: ldur/stur: imm9 out of range");
u32 imm9 = (u32)((u64)m.imm & 0x1ffu);
- u32 word = aa64_ldst_simm9_pack(
- (AA64LdStSimm9){.size = size,
- .V = 0,
- .opc = is_load ? AA64_LDST_OPC_LDR : AA64_LDST_OPC_STR,
- .imm9 = imm9,
- .Rn = m.base.num,
- .Rt = rt.num});
+ u32 word = aa64_ldst_simm9_pack((AA64LdStSimm9){.size = size,
+ .V = 0,
+ .opc = opc,
+ .imm9 = imm9,
+ .Rn = m.base.num,
+ .Rt = rt.num});
emit32(d, word);
}
@@ -1505,8 +1513,15 @@ static void p_brk_(AsmDriver* d) { p_except(d, 1); }
static void p_hlt_(AsmDriver* d) { p_except(d, 2); }
static void p_ldr_(AsmDriver* d) { p_ldr_str(d, 1); }
static void p_str_(AsmDriver* d) { p_ldr_str(d, 0); }
-static void p_ldur_(AsmDriver* d) { p_ldur_stur(d, 1); }
-static void p_stur_(AsmDriver* d) { p_ldur_stur(d, 0); }
+static void p_ldur_(AsmDriver* d) { p_ldur_stur(d, 1, -1, 0); }
+static void p_stur_(AsmDriver* d) { p_ldur_stur(d, 0, -1, 0); }
+static void p_ldurb(AsmDriver* d) { p_ldur_stur(d, 1, 0, 0); }
+static void p_sturb(AsmDriver* d) { p_ldur_stur(d, 0, 0, 0); }
+static void p_ldurh(AsmDriver* d) { p_ldur_stur(d, 1, 1, 0); }
+static void p_sturh(AsmDriver* d) { p_ldur_stur(d, 0, 1, 0); }
+static void p_ldursb(AsmDriver* d) { p_ldur_stur(d, 1, 0, 1); }
+static void p_ldursh(AsmDriver* d) { p_ldur_stur(d, 1, 1, 1); }
+static void p_ldursw(AsmDriver* d) { p_ldur_stur(d, 1, 2, 1); }
static void p_ldp_(AsmDriver* d) { p_ldp_stp(d, 1); }
static void p_stp_(AsmDriver* d) { p_ldp_stp(d, 0); }
static void p_adr_(AsmDriver* d) { p_adr(d, 0); }
@@ -1999,6 +2014,13 @@ static const AA64Mn kTable[] = {
{"ldrsw", p_ldrsw, 0},
{"ldur", p_ldur_, 0},
{"stur", p_stur_, 0},
+ {"ldurb", p_ldurb, 0},
+ {"sturb", p_sturb, 0},
+ {"ldurh", p_ldurh, 0},
+ {"sturh", p_sturh, 0},
+ {"ldursb", p_ldursb, 0},
+ {"ldursh", p_ldursh, 0},
+ {"ldursw", p_ldursw, 0},
{"ldp", p_ldp_, 0},
{"stp", p_stp_, 0},
{"adr", p_adr_, 0},
diff --git a/src/arch/aa64/isa.c b/src/arch/aa64/isa.c
@@ -250,6 +250,13 @@ const AA64InsnDesc aa64_insn_table[] = {
{MN("ldurb"), 0x38400000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
{MN("sturh"), 0x78000000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
{MN("ldurh"), 0x78400000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
+ /* Signed unscaled loads (opc=10 → 64-bit Xt, opc=11 → 32-bit Wt). The
+ * printer keys the register width on opc; size selects the mnemonic. */
+ {MN("ldursb"), 0x38800000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
+ {MN("ldursb"), 0x38C00000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
+ {MN("ldursh"), 0x78800000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
+ {MN("ldursh"), 0x78C00000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
+ {MN("ldursw"), 0xB8800000u, 0xFFE00C00u, AA64_FMT_LDST_SIMM9, 0, {0, 0}},
{MN("stur"),
0xB8000000u,
0xFFE00C00u,
@@ -711,7 +718,13 @@ static void print_ldst_simm9(StrBuf* sb, u32 w, const AA64InsnDesc* d) {
u32 sz = f.size & 3u;
(void)d;
if (f.V == 0) {
- emit_reg(sb, f.Rt, /*sf=*/(int)(sz == 3u), 0);
+ /* opc 00/01 = STUR/LDUR (register width from size); opc 10/11 = signed
+ * load LDURS* (width from opc: 10 sign-extends to 64-bit Xt, 11 to
+ * 32-bit Wt). */
+ int sf = (f.opc == 2u) ? 1
+ : (f.opc == 3u) ? 0
+ : (int)(sz == 3u);
+ emit_reg(sb, f.Rt, sf, 0);
} else {
char p = (sz == 0u) ? 'b' : (sz == 1u) ? 'h' : (sz == 2u) ? 's' : 'd';
emit_vreg(sb, f.Rt, p);
diff --git a/src/asm/asm_lex.c b/src/asm/asm_lex.c
@@ -386,6 +386,35 @@ AsmTok asm_lex_next(AsmLexer* l) {
}
}
+ /* Local-label identifier: a `.L`-prefixed symbol name (the universal GNU
+ * convention for assembler-local labels, e.g. `.Lcfree_ro.0`, `.L.str`,
+ * `.LBB0_1`). Lexed as a single ASM_TOK_IDENT — including the leading dot
+ * and any embedded dots — so it flows through the same operand / label /
+ * `.type` paths as an ordinary identifier. This is unambiguous against
+ * directives: no assembler directive begins with `.L`, so `.text`,
+ * `.section`, `.quad` etc. still tokenize as PUNCT('.') + IDENT and reach
+ * the directive dispatcher. Embedded `.` is consumed only when followed by
+ * another symbol char, so `.Lfoo, x` and `.Lfoo+4` stop at the delimiter. */
+ if (ch == '.' && peek(l, 1) == 'L') {
+ bump(l); /* '.' */
+ bump(l); /* 'L' */
+ for (;;) {
+ int c = peek(l, 0);
+ if (is_alnum(c) || c == '$') {
+ bump(l);
+ } else if (c == '.' && (is_alnum(peek(l, 1)) || peek(l, 1) == '$' ||
+ peek(l, 1) == '_')) {
+ bump(l);
+ } else {
+ break;
+ }
+ }
+ t.kind = ASM_TOK_IDENT;
+ t.spelling = intern_spliced(l, start, l->pos);
+ t.v.ident = t.spelling;
+ return t;
+ }
+
/* Identifier (§6.4.2). Encoding-prefix candidates above are matched
* before this since L/u/U followed by a quote is a literal, not an
* identifier. The grammar's identifier-nondigit covers letters, _,
diff --git a/test/asm/encode/aa64_ldur_family.expected.hex b/test/asm/encode/aa64_ldur_family.expected.hex
@@ -0,0 +1 @@
+200010f862f04ff8a44000b8e6c05fb8281100386af15f38ac210078eee15f783032803872d2df38b4428078f6c2df78388380b8
diff --git a/test/asm/encode/aa64_ldur_family.s b/test/asm/encode/aa64_ldur_family.s
@@ -0,0 +1,14 @@
+ .text
+ stur x0, [x1, #-256]
+ ldur x2, [x3, #255]
+ stur w4, [x5, #4]
+ ldur w6, [x7, #-4]
+ sturb w8, [x9, #1]
+ ldurb w10, [x11, #-1]
+ sturh w12, [x13, #2]
+ ldurh w14, [x15, #-2]
+ ldursb x16, [x17, #3]
+ ldursb w18, [x19, #-3]
+ ldursh x20, [x21, #4]
+ ldursh w22, [x23, #-4]
+ ldursw x24, [x25, #8]
diff --git a/test/asm/encode/aa64_ldur_family.targets b/test/asm/encode/aa64_ldur_family.targets
@@ -0,0 +1 @@
+aa64