feat(ffi-linkage): extern/export parser+AST plumbing, unconsumed (Phase 0.1)
Add ast.ExternExportModifier { none, extern_, export_ } beside
CallingConvention; FnDecl.extern_export and VarDecl.is_extern/extern_name
fields (all defaulting to absent); and Parser.parseOptionalExternExport()
mirroring parseOptionalCallConv.
None of this is consumed by a decl path yet — no user-facing behavior
change, corpus diff empty. Two inline parser unit tests pin the helper's
keyword mapping and the field defaults. Phase 1.0 wires the helper into
the fn-decl path. lock commit.
This commit is contained in:
@@ -3680,6 +3680,25 @@ pub const Parser = struct {
|
||||
return cc;
|
||||
}
|
||||
|
||||
/// Postfix linkage modifier in the slot after `callconv(...)`:
|
||||
/// `extern` (import) or `export` (define + expose), or `.none` if neither.
|
||||
/// Mirrors `parseOptionalCallConv`. Bare-keyword today; the optional
|
||||
/// `"csym"` symbol-name override lands in Phase 1.2/2.2. Defined here in
|
||||
/// Phase 0.1 but NOT yet called from any decl path (wired in Phase 1.0).
|
||||
fn parseOptionalExternExport(self: *Parser) ast.ExternExportModifier {
|
||||
switch (self.current.tag) {
|
||||
.kw_extern => {
|
||||
self.advance();
|
||||
return .extern_;
|
||||
},
|
||||
.kw_export => {
|
||||
self.advance();
|
||||
return .export_;
|
||||
},
|
||||
else => return .none,
|
||||
}
|
||||
}
|
||||
|
||||
fn isAssignOp(self: *const Parser) bool {
|
||||
return switch (self.current.tag) {
|
||||
.equal, .plus_equal, .minus_equal, .star_equal, .slash_equal, .percent_equal,
|
||||
@@ -3975,6 +3994,48 @@ test "parse minimal main" {
|
||||
try std.testing.expectEqual(@as(i64, 42), body.data.block.stmts[0].data.int_literal.value);
|
||||
}
|
||||
|
||||
test "parseOptionalExternExport recognizes linkage keywords (unconsumed)" {
|
||||
// Phase 0.1 plumbing: the helper exists and maps the keywords, but no
|
||||
// decl path calls it yet (wired in Phase 1.0). Drive it directly.
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
{
|
||||
var parser = Parser.init(arena.allocator(), "extern");
|
||||
try std.testing.expectEqual(ast.ExternExportModifier.extern_, parser.parseOptionalExternExport());
|
||||
}
|
||||
{
|
||||
var parser = Parser.init(arena.allocator(), "export");
|
||||
try std.testing.expectEqual(ast.ExternExportModifier.export_, parser.parseOptionalExternExport());
|
||||
}
|
||||
{
|
||||
var parser = Parser.init(arena.allocator(), "foo");
|
||||
try std.testing.expectEqual(ast.ExternExportModifier.none, parser.parseOptionalExternExport());
|
||||
}
|
||||
}
|
||||
|
||||
test "extern/export AST fields default to absent (unconsumed)" {
|
||||
// FnDecl.extern_export defaults to .none on a normally-parsed function;
|
||||
// the fn-decl path does not consume the modifier until Phase 1.0.
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () {}");
|
||||
const root = try parser.parse();
|
||||
const fd = root.data.root.decls[0].data.fn_decl;
|
||||
try std.testing.expectEqual(ast.ExternExportModifier.none, fd.extern_export);
|
||||
|
||||
// VarDecl.is_extern / extern_name default to absent (no var-decl path
|
||||
// consumes them until Phase 1.2). A struct literal locks field presence +
|
||||
// defaults without depending on a top-level var form.
|
||||
const vd: ast.VarDecl = .{
|
||||
.name = "g",
|
||||
.name_span = .{ .start = 0, .end = 0 },
|
||||
.type_annotation = null,
|
||||
.value = null,
|
||||
};
|
||||
try std.testing.expect(!vd.is_extern);
|
||||
try std.testing.expect(vd.extern_name == null);
|
||||
}
|
||||
|
||||
test "block value: trailing expr without `;` produces a value" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
Reference in New Issue
Block a user