feat(asm): Phase A.1 — parse asm { … } into AsmExpr; loud lowering bail
`asm volatile? { "tmpl", [name]? "constraint" (-> Type | = expr), …,
clobbers(.…) }` now parses into a flat-operand AsmExpr/AsmOperand (ast.zig +
parser.zig parseAsmExpr, dispatched from parsePrimary on .kw_asm). `volatile`
and `clobbers` are recognized contextually (not reserved). `-> @place`
write-through is rejected with a clear "Phase 2" parse error.
Codegen is not implemented yet (IR op + LLVM emit are Phases C–E), so lowering
bails LOUD + named via an explicit .asm_expr arm in lower/expr.zig (not the
generic unknown_expr else) — emitPlaceholder makes hasErrors() abort the build
on the message.
The new asm_expr tag forced (and got) arms in three exhaustive Node.Data
switches: sema.zig analyzeNode + findNodeAtOffset, semantic_diagnostics.zig
checkBindingNames — each recurses into template + operand payloads.
Design: adopted the operand auto-naming rule (design §II.5) — name auto-derived
from a {reg} pin, explicit [name] only when it differs or for register-class
operands, echo form rejected. Typing-stage rule; parser stores name: ?[]const u8.
Locked with examples/1640-platform-asm-parse.sx (multi-output divmod: named
operands, register pins, clobbers — parses then bails, called from main).
Also files issue 0137 (pre-existing, orthogonal: `sx run` with no `main`
segfaults via an unguarded JIT entry lookup in target.zig — not an asm bug).
zig build test green (648 corpus, 445 unit).
This commit is contained in:
100
src/parser.zig
100
src/parser.zig
@@ -2702,6 +2702,105 @@ pub const Parser = struct {
|
||||
return expr;
|
||||
}
|
||||
|
||||
/// True when the current token is a bare identifier with text `word` — used
|
||||
/// for the contextual keywords `volatile` / `clobbers` that appear only
|
||||
/// inside an `asm { … }` body and are NOT globally reserved.
|
||||
fn isContextualWord(self: *const Parser, word: []const u8) bool {
|
||||
return self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), word);
|
||||
}
|
||||
|
||||
/// Inline assembly expression (ASM stream, design §II.2–II.4):
|
||||
/// `asm volatile? { "tmpl", [name]? "constraint" (-> Type | = expr), …,
|
||||
/// clobbers(.name, …) }`
|
||||
/// A flat, comma-separated brace block: the template first, then operands
|
||||
/// and an optional `clobbers(.…)` clause, source order preserved.
|
||||
fn parseAsmExpr(self: *Parser, start: u32) anyerror!*Node {
|
||||
self.advance(); // consume `asm`
|
||||
var is_volatile = false;
|
||||
if (self.isContextualWord("volatile")) {
|
||||
is_volatile = true;
|
||||
self.advance();
|
||||
}
|
||||
try self.expect(.l_brace);
|
||||
|
||||
// First element: the template (a comptime string — `"..."` or `#string`).
|
||||
const template = try self.parseExpr();
|
||||
|
||||
var operands = std.ArrayList(ast.AsmOperand).empty;
|
||||
var clobbers = std.ArrayList([]const u8).empty;
|
||||
|
||||
while (self.current.tag == .comma) {
|
||||
self.advance(); // consume the separating comma
|
||||
if (self.current.tag == .r_brace) break; // trailing comma
|
||||
|
||||
// `clobbers(.name, .name, …)` clause.
|
||||
if (self.isContextualWord("clobbers")) {
|
||||
self.advance();
|
||||
try self.expect(.l_paren);
|
||||
while (true) {
|
||||
try self.expect(.dot);
|
||||
if (self.current.tag != .identifier)
|
||||
return self.fail("expected a clobber name after '.' in clobbers(...)");
|
||||
try clobbers.append(self.allocator, self.tokenSlice(self.current));
|
||||
self.advance();
|
||||
if (self.current.tag == .comma) {
|
||||
self.advance();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Operand: `[name]? "constraint" (-> Type | = expr)`.
|
||||
var op_name: ?[]const u8 = null;
|
||||
if (self.current.tag == .l_bracket) {
|
||||
self.advance();
|
||||
if (self.current.tag != .identifier)
|
||||
return self.fail("expected an operand name in '[...]'");
|
||||
op_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
try self.expect(.r_bracket);
|
||||
}
|
||||
if (self.current.tag != .string_literal)
|
||||
return self.fail("expected a \"constraint\" string in asm operand");
|
||||
const craw = self.tokenSlice(self.current);
|
||||
const constraint = craw[1 .. craw.len - 1]; // strip quotes
|
||||
self.advance();
|
||||
|
||||
var role: ast.AsmOperand.Role = undefined;
|
||||
var payload: *Node = undefined;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
if (self.current.tag == .at)
|
||||
return self.fail("`-> @place` write-through asm outputs are not supported yet (Phase 2); use a `-> Type` value output");
|
||||
role = .out_value;
|
||||
payload = try self.parseTypeExpr();
|
||||
} else if (self.current.tag == .equal) {
|
||||
self.advance();
|
||||
role = .input;
|
||||
payload = try self.parseExpr();
|
||||
} else {
|
||||
return self.fail("expected '->' (output) or '=' (input) after the asm constraint");
|
||||
}
|
||||
try operands.append(self.allocator, .{
|
||||
.name = op_name,
|
||||
.constraint = constraint,
|
||||
.role = role,
|
||||
.payload = payload,
|
||||
});
|
||||
}
|
||||
|
||||
try self.expect(.r_brace);
|
||||
return try self.createNode(start, .{ .asm_expr = .{
|
||||
.template = template,
|
||||
.is_volatile = is_volatile,
|
||||
.operands = try operands.toOwnedSlice(self.allocator),
|
||||
.clobbers = try clobbers.toOwnedSlice(self.allocator),
|
||||
} });
|
||||
}
|
||||
|
||||
fn parsePrimary(self: *Parser) anyerror!*Node {
|
||||
const start = self.current.loc.start;
|
||||
// Pack references in expression position:
|
||||
@@ -2807,6 +2906,7 @@ pub const Parser = struct {
|
||||
self.advance();
|
||||
return try self.createNode(start, .{ .identifier = .{ .name = name } });
|
||||
},
|
||||
.kw_asm => return self.parseAsmExpr(start),
|
||||
.dot => {
|
||||
self.advance();
|
||||
// Anonymous struct literal: .{ ... }
|
||||
|
||||
Reference in New Issue
Block a user