1241 lines
51 KiB
Zig
1241 lines
51 KiB
Zig
//! Reads a C struct definition into a `struct_layout.Definition` (FR-3.1).
|
|
//!
|
|
//! The point is pasting a struct from real code, so the input is C rather than a
|
|
//! language of our own. It is a defined subset, and anything outside it is an error
|
|
//! that says where, never a guess:
|
|
//!
|
|
//! - `struct [tag] { ... }` and `typedef struct [tag] { ... } Name;`, optionally with
|
|
//! a trailing `;`.
|
|
//! - Members of any scalar type, in C spellings (`unsigned long long`, `uint32_t`,
|
|
//! `size_t`, `wchar_t`, `__int128`, `enum color`) or short ones (`u32`, `f64`,
|
|
//! `ptr`, `usize`), and pointers to anything (`struct node *`, `void (*)(int)`).
|
|
//! - Several declarators per declaration (`int a, *b, c[4];`), arrays of any rank, and
|
|
//! a flexible array member (`char data[];`).
|
|
//! - `alignas(N)`, `_Alignas(N)`, `__attribute__((aligned(N)))` and
|
|
//! `__attribute__((packed))`, on members and on the struct, and
|
|
//! `#pragma pack(N | push | push, N | pop | )` before it.
|
|
//! - `//` and `/* */` comments.
|
|
//!
|
|
//! What is refused, each with its own error: nested structs and unions by value,
|
|
//! bitfields, any other preprocessor directive, names it cannot resolve (a typedef or
|
|
//! a macro from elsewhere), and the types whose layout this module does not model yet
|
|
//! (`long double`, `_Complex`, `_Atomic`).
|
|
//!
|
|
//! It has its own lexer. The expression tokenizer has no braces or brackets, and its
|
|
//! number rules contradict C's: `1,000` is a grouped number there, `010` is ten, and a
|
|
//! hex literal continues across a space.
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const struct_layout = @import("struct_layout.zig");
|
|
const Definition = struct_layout.Definition;
|
|
const Member = struct_layout.Member;
|
|
const Primitive = struct_layout.Primitive;
|
|
const Span = struct_layout.Span;
|
|
|
|
/// The file is the parser, as `Parser.zig` is for expressions.
|
|
const CStructParser = @This();
|
|
|
|
/// What parsing can fail with. `UnexpectedToken`, `UnexpectedEnd` and `InvalidNumber`
|
|
/// are the expression parser's names, so they share its wording.
|
|
pub const Error = error{
|
|
UnexpectedToken,
|
|
UnexpectedEnd,
|
|
InvalidNumber,
|
|
/// `/*` with no `*/`.
|
|
UnterminatedComment,
|
|
/// A name that is not a type this parser knows: usually a typedef or a macro
|
|
/// defined somewhere the input does not include.
|
|
UnknownType,
|
|
/// A C type whose layout is not modelled: `long double`, `_Complex`, `_Atomic`,
|
|
/// or a top-level `union`.
|
|
UnsupportedType,
|
|
/// A `void` member.
|
|
IncompleteType,
|
|
/// An attribute that could change the layout in a way this parser does not
|
|
/// model (`mode`, `vector_size`, a bare `aligned`).
|
|
UnsupportedAttribute,
|
|
BitfieldNotSupported,
|
|
/// A struct, union or enum defined inside the struct, or a struct or union
|
|
/// member by value.
|
|
NestedTypeNotSupported,
|
|
/// A preprocessor directive other than `#pragma pack`.
|
|
PreprocessorNotSupported,
|
|
/// More than `max_source_bytes` of input.
|
|
DefinitionTooLong,
|
|
/// An array dimension of zero.
|
|
InvalidArrayLength,
|
|
/// An alignment or pack value that is not a power of two, or a pack value C
|
|
/// compilers refuse.
|
|
InvalidAlignment,
|
|
TooManyMembers,
|
|
OutOfMemory,
|
|
};
|
|
|
|
/// Ceiling on input, so untrusted input cannot ask for unbounded work (NFR-8). A
|
|
/// struct of a thousand members fits with room to spare.
|
|
pub const max_source_bytes: usize = 64 * 1024;
|
|
|
|
/// Where a parse failed: the byte range of the offending text in the source.
|
|
pub const Diagnostic = struct {
|
|
span: Span = .{ .start = 0, .len = 0 },
|
|
};
|
|
|
|
/// A position a person can find: both 1-based, columns counted in bytes.
|
|
pub const Location = struct {
|
|
line: usize,
|
|
column: usize,
|
|
};
|
|
|
|
/// The line and column of a byte offset in `source`.
|
|
pub fn locate(source: []const u8, offset: usize) Location {
|
|
const before = source[0..@min(offset, source.len)];
|
|
const line_start = if (std.mem.lastIndexOfScalar(u8, before, '\n')) |nl| nl + 1 else 0;
|
|
return .{
|
|
.line = std.mem.count(u8, before, "\n") + 1,
|
|
.column = before.len - line_start + 1,
|
|
};
|
|
}
|
|
|
|
/// A parsed definition and the memory it lives in. Self-contained: every name is
|
|
/// copied into the arena, so the source can go away.
|
|
pub const Parsed = struct {
|
|
arena: std.heap.ArenaAllocator,
|
|
definition: Definition,
|
|
|
|
pub fn deinit(self: *Parsed) void {
|
|
self.arena.deinit();
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
/// Parse one struct definition.
|
|
///
|
|
/// On failure, `diag` (if given) holds the span of the text the error is about.
|
|
pub fn parse(allocator: Allocator, source: []const u8, diag: ?*Diagnostic) Error!Parsed {
|
|
if (source.len > max_source_bytes) {
|
|
if (diag) |d| d.span = .{ .start = max_source_bytes, .len = source.len - max_source_bytes };
|
|
return error.DefinitionTooLong;
|
|
}
|
|
var arena: std.heap.ArenaAllocator = .init(allocator);
|
|
errdefer arena.deinit();
|
|
|
|
var parser: CStructParser = .{
|
|
.source = source,
|
|
.lexer = .{ .source = source },
|
|
// SAFETY: `advance` below sets it before anything reads it.
|
|
.current = undefined,
|
|
.arena = arena.allocator(),
|
|
.diag = diag,
|
|
.pack_stack = .empty,
|
|
};
|
|
try parser.advance();
|
|
const definition = try parser.parseInput();
|
|
return .{ .arena = arena, .definition = definition };
|
|
}
|
|
|
|
// -- Lexer --
|
|
|
|
const TokenKind = enum {
|
|
identifier,
|
|
number,
|
|
l_brace,
|
|
r_brace,
|
|
l_bracket,
|
|
r_bracket,
|
|
l_paren,
|
|
r_paren,
|
|
semicolon,
|
|
comma,
|
|
star,
|
|
colon,
|
|
/// A whole preprocessor line, `#` to the end of the line.
|
|
directive,
|
|
eof,
|
|
invalid,
|
|
};
|
|
|
|
const Token = struct {
|
|
kind: TokenKind,
|
|
start: usize,
|
|
len: usize,
|
|
|
|
fn span(self: Token) Span {
|
|
return .{ .start = self.start, .len = self.len };
|
|
}
|
|
};
|
|
|
|
/// A position in a string. Copying it is a lookahead.
|
|
const Lexer = struct {
|
|
source: []const u8,
|
|
pos: usize = 0,
|
|
|
|
/// The next token, or the span of a comment that never ends.
|
|
fn next(self: *Lexer) union(enum) { token: Token, unterminated: Span } {
|
|
const src = self.source;
|
|
while (self.pos < src.len) {
|
|
const c = src[self.pos];
|
|
if (c == ' ' or c == '\t' or c == '\r' or c == '\n' or c == '\x0b' or c == '\x0c') {
|
|
self.pos += 1;
|
|
} else if (std.mem.startsWith(u8, src[self.pos..], "//")) {
|
|
self.pos = std.mem.indexOfScalarPos(u8, src, self.pos, '\n') orelse src.len;
|
|
} else if (std.mem.startsWith(u8, src[self.pos..], "/*")) {
|
|
const end = std.mem.indexOfPos(u8, src, self.pos + 2, "*/") orelse {
|
|
const start = self.pos;
|
|
self.pos = src.len;
|
|
return .{ .unterminated = .{ .start = start, .len = 2 } };
|
|
};
|
|
self.pos = end + 2;
|
|
} else break;
|
|
}
|
|
|
|
const start = self.pos;
|
|
if (start >= src.len) return .{ .token = .{ .kind = .eof, .start = src.len, .len = 0 } };
|
|
|
|
const c = src[start];
|
|
const kind: TokenKind = switch (c) {
|
|
'a'...'z', 'A'...'Z', '_' => blk: {
|
|
while (self.pos < src.len and isIdentChar(src[self.pos])) self.pos += 1;
|
|
break :blk .identifier;
|
|
},
|
|
'0'...'9' => blk: {
|
|
// The whole pp-number, suffix and all; the parser decides whether it
|
|
// is a valid literal.
|
|
while (self.pos < src.len and isIdentChar(src[self.pos])) self.pos += 1;
|
|
break :blk .number;
|
|
},
|
|
'#' => blk: {
|
|
self.pos = std.mem.indexOfScalarPos(u8, src, self.pos, '\n') orelse src.len;
|
|
break :blk .directive;
|
|
},
|
|
else => blk: {
|
|
self.pos += 1;
|
|
break :blk switch (c) {
|
|
'{' => .l_brace,
|
|
'}' => .r_brace,
|
|
'[' => .l_bracket,
|
|
']' => .r_bracket,
|
|
'(' => .l_paren,
|
|
')' => .r_paren,
|
|
';' => .semicolon,
|
|
',' => .comma,
|
|
'*' => .star,
|
|
':' => .colon,
|
|
else => .invalid,
|
|
};
|
|
},
|
|
};
|
|
return .{ .token = .{ .kind = kind, .start = start, .len = self.pos - start } };
|
|
}
|
|
};
|
|
|
|
fn isIdentChar(c: u8) bool {
|
|
return std.ascii.isAlphanumeric(c) or c == '_';
|
|
}
|
|
|
|
// -- Parser state --
|
|
|
|
source: []const u8,
|
|
lexer: Lexer,
|
|
current: Token,
|
|
arena: Allocator,
|
|
diag: ?*Diagnostic,
|
|
/// `#pragma pack` in effect, and what `push` saved.
|
|
pack: ?u32 = null,
|
|
pack_stack: std.ArrayList(?u32),
|
|
|
|
fn text(self: *const CStructParser, tok: Token) []const u8 {
|
|
return self.source[tok.start..][0..tok.len];
|
|
}
|
|
|
|
/// Record where an error is and return it.
|
|
fn fail(self: *CStructParser, err: Error, span: Span) Error {
|
|
if (self.diag) |d| d.span = span;
|
|
return err;
|
|
}
|
|
|
|
/// The error for the current token not being what the grammar needs here.
|
|
fn unexpected(self: *CStructParser) Error {
|
|
const err: Error = if (self.current.kind == .eof) error.UnexpectedEnd else error.UnexpectedToken;
|
|
return self.fail(err, self.current.span());
|
|
}
|
|
|
|
fn advance(self: *CStructParser) Error!void {
|
|
switch (self.lexer.next()) {
|
|
.token => |tok| self.current = tok,
|
|
.unterminated => |span| return self.fail(error.UnterminatedComment, span),
|
|
}
|
|
}
|
|
|
|
fn expect(self: *CStructParser, kind: TokenKind) Error!Token {
|
|
if (self.current.kind != kind) return self.unexpected();
|
|
const tok = self.current;
|
|
try self.advance();
|
|
return tok;
|
|
}
|
|
|
|
fn isWord(self: *const CStructParser, word: []const u8) bool {
|
|
return self.current.kind == .identifier and std.mem.eql(u8, self.text(self.current), word);
|
|
}
|
|
|
|
fn eatWord(self: *CStructParser, word: []const u8) Error!bool {
|
|
if (!self.isWord(word)) return false;
|
|
try self.advance();
|
|
return true;
|
|
}
|
|
|
|
// -- Grammar --
|
|
|
|
/// Attributes that apply to whatever they are attached to.
|
|
const Attributes = struct {
|
|
aligned: ?u32 = null,
|
|
@"packed": bool = false,
|
|
|
|
fn merge(self: *Attributes, other: Attributes) void {
|
|
if (other.aligned) |n| self.aligned = @max(self.aligned orelse 0, n);
|
|
if (other.@"packed") self.@"packed" = true;
|
|
}
|
|
};
|
|
|
|
/// The whole input: directives, one struct, directives.
|
|
fn parseInput(self: *CStructParser) Error!Definition {
|
|
try self.parseDirectives();
|
|
const definition = try self.parseTop();
|
|
try self.parseDirectives();
|
|
if (self.current.kind != .eof) return self.unexpected();
|
|
return definition;
|
|
}
|
|
|
|
fn parseDirectives(self: *CStructParser) Error!void {
|
|
while (self.current.kind == .directive) {
|
|
try self.parseDirective(self.current);
|
|
try self.advance();
|
|
}
|
|
}
|
|
|
|
/// `#pragma pack(...)`, the one directive that matters to a layout. Everything else a
|
|
/// header might hold (`#include`, `#define`, `#if`) is refused rather than skipped,
|
|
/// because skipping `#if 0` would count the members inside it.
|
|
fn parseDirective(self: *CStructParser, line: Token) Error!void {
|
|
var sub: CStructParser = .{
|
|
.source = self.source,
|
|
.lexer = .{ .source = self.source[0 .. line.start + line.len], .pos = line.start + 1 },
|
|
// SAFETY: the first `advance` below sets it before anything reads it.
|
|
.current = undefined,
|
|
.arena = self.arena,
|
|
.diag = self.diag,
|
|
.pack_stack = self.pack_stack,
|
|
};
|
|
defer self.pack_stack = sub.pack_stack;
|
|
sub.pack = self.pack;
|
|
defer self.pack = sub.pack;
|
|
|
|
try sub.advance();
|
|
if (!try sub.eatWord("pragma") or !try sub.eatWord("pack")) {
|
|
return self.fail(error.PreprocessorNotSupported, line.span());
|
|
}
|
|
_ = try sub.expect(.l_paren);
|
|
|
|
if (try sub.eatWord("push")) {
|
|
try sub.pack_stack.append(sub.arena, sub.pack);
|
|
if (sub.current.kind == .comma) {
|
|
try sub.advance();
|
|
sub.pack = try sub.parsePackValue();
|
|
}
|
|
} else if (sub.isWord("pop")) {
|
|
try sub.advance();
|
|
sub.pack = sub.pack_stack.pop() orelse null;
|
|
} else if (sub.current.kind == .number) {
|
|
sub.pack = try sub.parsePackValue();
|
|
} else if (sub.current.kind != .r_paren) {
|
|
return sub.unexpected();
|
|
} else {
|
|
// `#pragma pack()` restores the default.
|
|
sub.pack = null;
|
|
}
|
|
_ = try sub.expect(.r_paren);
|
|
if (sub.current.kind != .eof) return sub.unexpected();
|
|
}
|
|
|
|
/// The values clang and GCC accept for `#pragma pack`.
|
|
fn parsePackValue(self: *CStructParser) Error!u32 {
|
|
const tok = self.current;
|
|
const n = try self.parseInteger();
|
|
return switch (n) {
|
|
1, 2, 4, 8, 16 => @intCast(n),
|
|
else => return self.fail(error.InvalidAlignment, tok.span()),
|
|
};
|
|
}
|
|
|
|
/// `struct ...;` or `typedef struct ... Name;`.
|
|
fn parseTop(self: *CStructParser) Error!Definition {
|
|
const is_typedef = try self.eatWord("typedef");
|
|
if (self.isWord("union")) return self.fail(error.UnsupportedType, self.current.span());
|
|
if (!self.isWord("struct")) return self.unexpected();
|
|
try self.advance();
|
|
|
|
var attrs: Attributes = .{};
|
|
attrs.merge(try self.parseAttributes());
|
|
|
|
var tag: ?[]const u8 = null;
|
|
if (self.current.kind == .identifier and !isAttributeWord(self.text(self.current))) {
|
|
tag = try self.arena.dupe(u8, self.text(self.current));
|
|
try self.advance();
|
|
}
|
|
attrs.merge(try self.parseAttributes());
|
|
|
|
// The pack in effect where the struct is defined is the one it uses.
|
|
const pack = self.pack;
|
|
const members = try self.parseBody();
|
|
attrs.merge(try self.parseAttributes());
|
|
|
|
var name = tag;
|
|
var name_kind: Definition.NameKind = .tag;
|
|
if (is_typedef) {
|
|
const name_tok = try self.expect(.identifier);
|
|
name = try self.arena.dupe(u8, self.text(name_tok));
|
|
name_kind = .typedef;
|
|
attrs.merge(try self.parseAttributes());
|
|
_ = try self.expect(.semicolon);
|
|
} else if (self.current.kind == .semicolon) {
|
|
try self.advance();
|
|
}
|
|
|
|
return .{
|
|
.name = name,
|
|
.name_kind = name_kind,
|
|
.members = members,
|
|
.@"packed" = attrs.@"packed",
|
|
.pack = pack,
|
|
.aligned = attrs.aligned,
|
|
};
|
|
}
|
|
|
|
/// `{ declaration* }`.
|
|
fn parseBody(self: *CStructParser) Error![]const Member {
|
|
_ = try self.expect(.l_brace);
|
|
var members: std.ArrayList(Member) = .empty;
|
|
while (self.current.kind != .r_brace) {
|
|
switch (self.current.kind) {
|
|
.eof => return self.unexpected(),
|
|
.directive => return self.fail(error.PreprocessorNotSupported, self.current.span()),
|
|
// A stray `;` is an empty declaration, which C compilers accept with a
|
|
// warning at most.
|
|
.semicolon => try self.advance(),
|
|
else => try self.parseDeclaration(&members),
|
|
}
|
|
}
|
|
try self.advance();
|
|
return members.items;
|
|
}
|
|
|
|
/// The type part of a declaration, shared by every declarator in it.
|
|
const Specifiers = struct {
|
|
/// What the specifiers name, before any pointer in a declarator.
|
|
base: Base,
|
|
/// The words as written, joined by single spaces, for display.
|
|
words: []const u8,
|
|
attrs: Attributes,
|
|
span: Span,
|
|
|
|
const Base = union(enum) {
|
|
primitive: Primitive,
|
|
/// `void`, which only a pointer may be.
|
|
void,
|
|
/// `struct X` or `union X`, which only a pointer may be here.
|
|
aggregate,
|
|
};
|
|
};
|
|
|
|
/// `specifiers declarator (, declarator)* ;`
|
|
fn parseDeclaration(self: *CStructParser, members: *std.ArrayList(Member)) Error!void {
|
|
const specs = try self.parseSpecifiers();
|
|
while (true) {
|
|
if (members.items.len >= struct_layout.max_members) {
|
|
return self.fail(error.TooManyMembers, self.current.span());
|
|
}
|
|
try members.append(self.arena, try self.parseDeclarator(specs));
|
|
if (self.current.kind == .colon) return self.fail(error.BitfieldNotSupported, self.current.span());
|
|
if (self.current.kind != .comma) break;
|
|
try self.advance();
|
|
}
|
|
_ = try self.expect(.semicolon);
|
|
}
|
|
|
|
/// Which word a type keyword is, for counting.
|
|
const Keyword = enum { signed, unsigned, char, short, int, long, float, double, void, bool, int128 };
|
|
|
|
const keywords = std.StaticStringMap(Keyword).initComptime(.{
|
|
.{ "signed", .signed },
|
|
.{ "__signed__", .signed },
|
|
.{ "unsigned", .unsigned },
|
|
.{ "char", .char },
|
|
.{ "short", .short },
|
|
.{ "int", .int },
|
|
.{ "long", .long },
|
|
.{ "float", .float },
|
|
.{ "double", .double },
|
|
.{ "void", .void },
|
|
.{ "_Bool", .bool },
|
|
.{ "bool", .bool },
|
|
.{ "__int128", .int128 },
|
|
});
|
|
|
|
/// Type names that stand alone. The C ones are the standard library's typedefs whose
|
|
/// layout every profile agrees on by kind; the short ones are this calculator's.
|
|
const named_types = std.StaticStringMap(Primitive).initComptime(.{
|
|
.{ "int8_t", .char }, .{ "uint8_t", .char }, .{ "int16_t", .short },
|
|
.{ "uint16_t", .short }, .{ "int32_t", .int }, .{ "uint32_t", .int },
|
|
.{ "int64_t", .long_long }, .{ "uint64_t", .long_long }, .{ "__int128_t", .int128 },
|
|
.{ "__uint128_t", .int128 }, .{ "intptr_t", .pointer }, .{ "uintptr_t", .pointer },
|
|
.{ "size_t", .pointer }, .{ "ssize_t", .pointer }, .{ "ptrdiff_t", .pointer },
|
|
.{ "wchar_t", .wchar }, .{ "char16_t", .short }, .{ "char32_t", .int },
|
|
.{ "u8", .char }, .{ "i8", .char }, .{ "u16", .short },
|
|
.{ "i16", .short }, .{ "u32", .int }, .{ "i32", .int },
|
|
.{ "u64", .long_long }, .{ "i64", .long_long }, .{ "u128", .int128 },
|
|
.{ "i128", .int128 }, .{ "f32", .float }, .{ "f64", .double },
|
|
.{ "usize", .pointer }, .{ "isize", .pointer }, .{ "ptr", .pointer },
|
|
});
|
|
|
|
/// Words that change nothing about a layout.
|
|
const qualifiers = std.StaticStringMap(void).initComptime(.{
|
|
.{"const"}, .{"volatile"}, .{"restrict"}, .{"__restrict"},
|
|
.{"__restrict__"}, .{"__const"}, .{"__volatile__"}, .{"__extension__"},
|
|
});
|
|
|
|
/// C types that exist and are not modelled. Recognised so the error can say so,
|
|
/// rather than calling `long double`'s second word an unknown type.
|
|
const unsupported_types = std.StaticStringMap(void).initComptime(.{
|
|
.{"_Complex"}, .{"_Imaginary"}, .{"_Atomic"}, .{"_Float16"},
|
|
.{"__fp16"}, .{"__bf16"}, .{"_Float128"}, .{"__float128"},
|
|
.{"_Decimal32"}, .{"_Decimal64"}, .{"_Decimal128"}, .{"_BitInt"},
|
|
});
|
|
|
|
fn isAttributeWord(word: []const u8) bool {
|
|
return std.mem.eql(u8, word, "__attribute__") or std.mem.eql(u8, word, "__attribute") or
|
|
std.mem.eql(u8, word, "alignas") or std.mem.eql(u8, word, "_Alignas");
|
|
}
|
|
|
|
fn parseSpecifiers(self: *CStructParser) Error!Specifiers {
|
|
const first = self.current;
|
|
var words: std.ArrayList(u8) = .empty;
|
|
var attrs: Attributes = .{};
|
|
var counts: std.EnumArray(Keyword, u8) = .initFill(0);
|
|
var named: ?Primitive = null;
|
|
var base: ?Specifiers.Base = null;
|
|
var last_end = first.start;
|
|
|
|
while (self.current.kind == .identifier) {
|
|
const tok = self.current;
|
|
const word = self.text(tok);
|
|
if (isAttributeWord(word)) {
|
|
attrs.merge(try self.parseAttributes());
|
|
continue;
|
|
}
|
|
if (std.mem.eql(u8, word, "struct") or std.mem.eql(u8, word, "union") or std.mem.eql(u8, word, "enum")) {
|
|
if (base != null or named != null or hasKeywords(counts)) return self.unexpected();
|
|
try self.advance();
|
|
const tag = self.current;
|
|
if (tag.kind == .l_brace) return self.fail(error.NestedTypeNotSupported, tok.span());
|
|
_ = try self.expect(.identifier);
|
|
if (self.current.kind == .l_brace) return self.fail(error.NestedTypeNotSupported, tok.span());
|
|
base = if (word[0] == 'e') .{ .primitive = .enumeration } else .aggregate;
|
|
try appendWord(self.arena, &words, word);
|
|
try appendWord(self.arena, &words, self.text(tag));
|
|
last_end = tag.start + tag.len;
|
|
continue;
|
|
}
|
|
if (qualifiers.has(word)) {
|
|
try appendWord(self.arena, &words, word);
|
|
} else if (unsupported_types.has(word)) {
|
|
return self.fail(error.UnsupportedType, tok.span());
|
|
} else if (keywords.get(word)) |kw| {
|
|
if (base != null or named != null) return self.unexpected();
|
|
counts.set(kw, counts.get(kw) + 1);
|
|
try appendWord(self.arena, &words, word);
|
|
} else if (base != null or named != null or hasKeywords(counts)) {
|
|
// A type has been named, so this is the declarator's name.
|
|
break;
|
|
} else if (named_types.get(word)) |p| {
|
|
named = p;
|
|
try appendWord(self.arena, &words, word);
|
|
} else {
|
|
return self.fail(error.UnknownType, tok.span());
|
|
}
|
|
last_end = tok.start + tok.len;
|
|
try self.advance();
|
|
}
|
|
|
|
const span: Span = .{ .start = first.start, .len = last_end - first.start };
|
|
const resolved: Specifiers.Base = if (base) |b|
|
|
b
|
|
else if (named) |p|
|
|
.{ .primitive = p }
|
|
else if (hasKeywords(counts))
|
|
try self.resolveKeywords(counts, span)
|
|
else
|
|
return self.unexpected();
|
|
|
|
return .{ .base = resolved, .words = words.items, .attrs = attrs, .span = span };
|
|
}
|
|
|
|
fn hasKeywords(counts: std.EnumArray(Keyword, u8)) bool {
|
|
for (counts.values) |n| if (n > 0) return true;
|
|
return false;
|
|
}
|
|
|
|
fn appendWord(arena: Allocator, words: *std.ArrayList(u8), word: []const u8) Error!void {
|
|
if (words.items.len > 0) try words.append(arena, ' ');
|
|
try words.appendSlice(arena, word);
|
|
}
|
|
|
|
/// What a combination of type keywords means, C's way: `unsigned` alone is
|
|
/// `unsigned int`, `long long int` is `long long`, and `long double` exists but is
|
|
/// not modelled.
|
|
fn resolveKeywords(self: *CStructParser, counts: std.EnumArray(Keyword, u8), span: Span) Error!Specifiers.Base {
|
|
// `long double` is a real type, so it gets its own error rather than being
|
|
// called an invalid combination.
|
|
if (counts.get(.double) == 1 and counts.get(.long) == 1 and
|
|
counts.get(.signed) + counts.get(.unsigned) + counts.get(.short) + counts.get(.int) == 0)
|
|
{
|
|
return self.fail(error.UnsupportedType, span);
|
|
}
|
|
return combine(counts) orelse self.fail(error.UnexpectedToken, span);
|
|
}
|
|
|
|
/// The type a set of keywords names, or null for a combination C rejects.
|
|
fn combine(counts: std.EnumArray(Keyword, u8)) ?Specifiers.Base {
|
|
// No keyword may repeat except `long`, once.
|
|
for (std.enums.values(Keyword)) |kw| {
|
|
const limit: u8 = if (kw == .long) 2 else 1;
|
|
if (counts.get(kw) > limit) return null;
|
|
}
|
|
const signs = counts.get(.signed) + counts.get(.unsigned);
|
|
if (signs > 1) return null;
|
|
const short = counts.get(.short);
|
|
const long = counts.get(.long);
|
|
const int = counts.get(.int);
|
|
const others = counts.get(.char) + counts.get(.float) + counts.get(.double) +
|
|
counts.get(.void) + counts.get(.bool) + counts.get(.int128);
|
|
if (others > 1) return null;
|
|
|
|
// These take no sign and no size.
|
|
const bare = signs + short + long + int == 0;
|
|
if (counts.get(.void) == 1) return if (bare) .void else null;
|
|
if (counts.get(.bool) == 1) return if (bare) .{ .primitive = .bool } else null;
|
|
if (counts.get(.float) == 1) return if (bare) .{ .primitive = .float } else null;
|
|
if (counts.get(.double) == 1) return if (bare) .{ .primitive = .double } else null;
|
|
// These take a sign and no size.
|
|
if (counts.get(.char) == 1) return if (short + long + int == 0) .{ .primitive = .char } else null;
|
|
if (counts.get(.int128) == 1) return if (short + long + int == 0) .{ .primitive = .int128 } else null;
|
|
if (short == 1) return if (long == 0) .{ .primitive = .short } else null;
|
|
return switch (long) {
|
|
0 => .{ .primitive = .int },
|
|
1 => .{ .primitive = .long },
|
|
else => .{ .primitive = .long_long },
|
|
};
|
|
}
|
|
|
|
/// `__attribute__((...))`, `alignas(N)` and `_Alignas(N)`, as many as are written.
|
|
fn parseAttributes(self: *CStructParser) Error!Attributes {
|
|
var attrs: Attributes = .{};
|
|
while (self.current.kind == .identifier) {
|
|
const word = self.text(self.current);
|
|
if (std.mem.eql(u8, word, "alignas") or std.mem.eql(u8, word, "_Alignas")) {
|
|
try self.advance();
|
|
_ = try self.expect(.l_paren);
|
|
const tok = self.current;
|
|
if (tok.kind != .number) return self.unexpected();
|
|
const n = try self.parseInteger();
|
|
// C11: an alignment of zero has no effect.
|
|
if (n != 0) attrs.merge(.{ .aligned = try self.alignmentValue(n, tok) });
|
|
_ = try self.expect(.r_paren);
|
|
} else if (std.mem.eql(u8, word, "__attribute__") or std.mem.eql(u8, word, "__attribute")) {
|
|
try self.advance();
|
|
_ = try self.expect(.l_paren);
|
|
_ = try self.expect(.l_paren);
|
|
while (self.current.kind != .r_paren) {
|
|
attrs.merge(try self.parseAttribute());
|
|
if (self.current.kind != .comma) break;
|
|
try self.advance();
|
|
}
|
|
_ = try self.expect(.r_paren);
|
|
_ = try self.expect(.r_paren);
|
|
} else break;
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
/// One entry of an `__attribute__` list.
|
|
fn parseAttribute(self: *CStructParser) Error!Attributes {
|
|
const tok = try self.expect(.identifier);
|
|
const name = trimUnderscores(self.text(tok));
|
|
if (std.mem.eql(u8, name, "packed")) return .{ .@"packed" = true };
|
|
if (std.mem.eql(u8, name, "aligned")) {
|
|
// A bare `aligned` means "the largest alignment the target uses", which
|
|
// differs by target in a way the profiles do not record.
|
|
if (self.current.kind != .l_paren) return self.fail(error.UnsupportedAttribute, tok.span());
|
|
try self.advance();
|
|
const value = self.current;
|
|
if (value.kind != .number) return self.unexpected();
|
|
const n = try self.parseInteger();
|
|
_ = try self.expect(.r_paren);
|
|
return .{ .aligned = try self.alignmentValue(n, value) };
|
|
}
|
|
// Attributes that mean nothing to a layout are accepted, with any arguments.
|
|
for ([_][]const u8{ "unused", "used", "deprecated", "may_alias", "nonstring" }) |harmless| {
|
|
if (std.mem.eql(u8, name, harmless)) {
|
|
if (self.current.kind == .l_paren) try self.skipBalanced();
|
|
return .{};
|
|
}
|
|
}
|
|
// Anything else (`mode`, `vector_size`, `designated_init`, ...) might change the
|
|
// type or the layout, and a guess would be a wrong answer.
|
|
return self.fail(error.UnsupportedAttribute, tok.span());
|
|
}
|
|
|
|
/// `__packed__` is `packed`: GCC accepts every attribute with or without the
|
|
/// surrounding double underscores.
|
|
fn trimUnderscores(name: []const u8) []const u8 {
|
|
if (name.len > 4 and std.mem.startsWith(u8, name, "__") and std.mem.endsWith(u8, name, "__")) {
|
|
return name[2 .. name.len - 2];
|
|
}
|
|
return name;
|
|
}
|
|
|
|
fn alignmentValue(self: *CStructParser, n: u64, tok: Token) Error!u32 {
|
|
if (n == 0 or n > 1 << 28 or !std.math.isPowerOfTwo(n)) return self.fail(error.InvalidAlignment, tok.span());
|
|
return @intCast(n);
|
|
}
|
|
|
|
/// Skip a parenthesised group, nested groups included.
|
|
fn skipBalanced(self: *CStructParser) Error!void {
|
|
var depth: usize = 0;
|
|
while (true) {
|
|
switch (self.current.kind) {
|
|
.l_paren => depth += 1,
|
|
.r_paren => depth -= 1,
|
|
.eof => return self.unexpected(),
|
|
else => {},
|
|
}
|
|
try self.advance();
|
|
if (depth == 0) return;
|
|
}
|
|
}
|
|
|
|
/// One declarator: `name`, `*name`, `name[2][3]`, `name[]`, `(*name)(...)`, plus any
|
|
/// attributes after it.
|
|
fn parseDeclarator(self: *CStructParser, specs: Specifiers) Error!Member {
|
|
var attrs = specs.attrs;
|
|
attrs.merge(try self.parseAttributes());
|
|
|
|
var stars: usize = 0;
|
|
while (self.current.kind == .star) {
|
|
stars += 1;
|
|
try self.advance();
|
|
// `char *const p`: a qualified pointer is still a pointer.
|
|
while (self.current.kind == .identifier and qualifiers.has(self.text(self.current))) try self.advance();
|
|
}
|
|
|
|
// SAFETY: both branches below assign it before it is read.
|
|
var name_tok: Token = undefined;
|
|
var dims: []const u64 = &.{};
|
|
var flexible = false;
|
|
var function_pointer = false;
|
|
var array_pointer = false;
|
|
|
|
if (self.current.kind == .l_paren) {
|
|
// `(*name)(params)` or `(*name)[N]`: a pointer whatever the outside says,
|
|
// and possibly an array of them, `(*handlers[4])(int)`.
|
|
try self.advance();
|
|
if (self.current.kind != .star) return self.unexpected();
|
|
while (self.current.kind == .star) {
|
|
try self.advance();
|
|
while (self.current.kind == .identifier and qualifiers.has(self.text(self.current))) try self.advance();
|
|
}
|
|
name_tok = try self.expect(.identifier);
|
|
const inner = try self.parseDimensions();
|
|
dims = inner.dims;
|
|
flexible = inner.flexible;
|
|
_ = try self.expect(.r_paren);
|
|
switch (self.current.kind) {
|
|
.l_paren => {
|
|
try self.skipBalanced();
|
|
function_pointer = true;
|
|
},
|
|
.l_bracket => {
|
|
// The pointee's dimensions are not part of this member's size.
|
|
_ = try self.parseDimensions();
|
|
array_pointer = true;
|
|
},
|
|
else => {},
|
|
}
|
|
stars += 1;
|
|
} else {
|
|
name_tok = try self.expect(.identifier);
|
|
if (self.current.kind == .l_paren) {
|
|
// `int f(void);` declares a function, which a struct cannot hold.
|
|
return self.unexpected();
|
|
}
|
|
const outer = try self.parseDimensions();
|
|
dims = outer.dims;
|
|
flexible = outer.flexible;
|
|
}
|
|
attrs.merge(try self.parseAttributes());
|
|
|
|
const primitive: Primitive = if (stars > 0) .pointer else switch (specs.base) {
|
|
.primitive => |p| p,
|
|
.void => return self.fail(error.IncompleteType, specs.span),
|
|
.aggregate => return self.fail(error.NestedTypeNotSupported, specs.span),
|
|
};
|
|
|
|
var type_name: std.ArrayList(u8) = .empty;
|
|
try type_name.appendSlice(self.arena, specs.words);
|
|
if (function_pointer or array_pointer) {
|
|
try type_name.append(self.arena, ' ');
|
|
try type_name.appendNTimes(self.arena, '*', stars - 1);
|
|
try type_name.appendSlice(self.arena, if (function_pointer) "(*)()" else "(*)[]");
|
|
} else if (stars > 0) {
|
|
try type_name.append(self.arena, ' ');
|
|
try type_name.appendNTimes(self.arena, '*', stars);
|
|
}
|
|
|
|
return .{
|
|
.name = try self.arena.dupe(u8, self.text(name_tok)),
|
|
.type_name = type_name.items,
|
|
.primitive = primitive,
|
|
.dims = dims,
|
|
.flexible = flexible,
|
|
.aligned = attrs.aligned,
|
|
.@"packed" = attrs.@"packed",
|
|
.span = name_tok.span(),
|
|
};
|
|
}
|
|
|
|
/// `[N][M]...`, where only the first may be empty (a flexible array member).
|
|
fn parseDimensions(self: *CStructParser) Error!struct { dims: []const u64, flexible: bool } {
|
|
var dims: std.ArrayList(u64) = .empty;
|
|
var flexible = false;
|
|
var first = true;
|
|
while (self.current.kind == .l_bracket) {
|
|
try self.advance();
|
|
if (self.current.kind == .r_bracket and first) {
|
|
flexible = true;
|
|
} else {
|
|
const tok = self.current;
|
|
if (tok.kind != .number) return self.unexpected();
|
|
const n = try self.parseInteger();
|
|
if (n == 0) return self.fail(error.InvalidArrayLength, tok.span());
|
|
try dims.append(self.arena, n);
|
|
}
|
|
_ = try self.expect(.r_bracket);
|
|
first = false;
|
|
}
|
|
return .{ .dims = dims.items, .flexible = flexible };
|
|
}
|
|
|
|
/// A C integer literal: decimal, `0x` hex, `0b` binary, or octal with a leading
|
|
/// zero, with any `u`/`l`/`ll` suffix. `010` is eight, because this is C.
|
|
fn parseInteger(self: *CStructParser) Error!u64 {
|
|
const tok = try self.expect(.number);
|
|
return integerValue(self.text(tok)) orelse self.fail(error.InvalidNumber, tok.span());
|
|
}
|
|
|
|
/// The suffixes C allows on an integer literal. `u` takes either case on its own;
|
|
/// the two letters of `ll` must match each other.
|
|
const integer_suffixes = std.StaticStringMap(void).initComptime(.{
|
|
.{""}, .{"u"}, .{"l"}, .{"L"}, .{"ll"}, .{"LL"}, .{"ul"},
|
|
.{"uL"}, .{"lu"}, .{"Lu"}, .{"ull"}, .{"uLL"}, .{"llu"}, .{"LLu"},
|
|
});
|
|
|
|
fn integerValue(literal: []const u8) ?u64 {
|
|
var suffix_start = literal.len;
|
|
while (suffix_start > 0 and std.mem.indexOfScalar(u8, "uUlL", literal[suffix_start - 1]) != null) {
|
|
suffix_start -= 1;
|
|
}
|
|
var suffix_buf: [8]u8 = undefined;
|
|
const suffix = literal[suffix_start..];
|
|
if (suffix.len > suffix_buf.len) return null;
|
|
for (suffix, suffix_buf[0..suffix.len]) |c, *out| out.* = if (c == 'U') 'u' else c;
|
|
if (!integer_suffixes.has(suffix_buf[0..suffix.len])) return null;
|
|
|
|
var digits = literal[0..suffix_start];
|
|
var radix: u8 = 10;
|
|
if (digits.len > 2 and digits[0] == '0' and (digits[1] == 'x' or digits[1] == 'X')) {
|
|
radix = 16;
|
|
digits = digits[2..];
|
|
} else if (digits.len > 2 and digits[0] == '0' and (digits[1] == 'b' or digits[1] == 'B')) {
|
|
radix = 2;
|
|
digits = digits[2..];
|
|
} else if (digits.len > 1 and digits[0] == '0') {
|
|
radix = 8;
|
|
digits = digits[1..];
|
|
}
|
|
if (digits.len == 0) return null;
|
|
return std.fmt.parseInt(u64, digits, radix) catch null;
|
|
}
|
|
|
|
// -- Tests --
|
|
|
|
const testing = std.testing;
|
|
|
|
fn expectParseError(expected: Error, source: []const u8, needle: []const u8) !void {
|
|
var diag: Diagnostic = .{};
|
|
try testing.expectError(expected, parse(testing.allocator, source, &diag));
|
|
const got = source[diag.span.start..][0..diag.span.len];
|
|
// The span is the point of the diagnostic, so it is asserted exactly.
|
|
try testing.expectEqualStrings(needle, got);
|
|
}
|
|
|
|
fn expectMember(member: Member, name: []const u8, type_name: []const u8, primitive: Primitive) !void {
|
|
try testing.expectEqualStrings(name, member.name);
|
|
try testing.expectEqualStrings(type_name, member.type_name);
|
|
try testing.expectEqual(primitive, member.primitive);
|
|
}
|
|
|
|
test "the design document's example" {
|
|
var parsed = try parse(testing.allocator, "struct { u32 x; u8 flags; u16 id; u64 ts; }", null);
|
|
defer parsed.deinit();
|
|
const def = parsed.definition;
|
|
try testing.expect(def.name == null);
|
|
try testing.expectEqual(@as(usize, 4), def.members.len);
|
|
try expectMember(def.members[0], "x", "u32", .int);
|
|
try expectMember(def.members[1], "flags", "u8", .char);
|
|
try expectMember(def.members[2], "id", "u16", .short);
|
|
try expectMember(def.members[3], "ts", "u64", .long_long);
|
|
|
|
var layout = try struct_layout.compute(testing.allocator, def, struct_layout.default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 16), layout.size);
|
|
}
|
|
|
|
test "a struct pasted from C, comments and all" {
|
|
const source =
|
|
\\/* A file header. */
|
|
\\struct file_header {
|
|
\\ uint32_t magic; // 'TALY'
|
|
\\ unsigned short version;
|
|
\\ const char *name, **aliases;
|
|
\\ long long int offsets[2][3];
|
|
\\ size_t len;
|
|
\\ enum kind k;
|
|
\\ struct file_header *next;
|
|
\\ void (*on_close)(struct file_header *, int);
|
|
\\ unsigned char data[];
|
|
\\};
|
|
;
|
|
var parsed = try parse(testing.allocator, source, null);
|
|
defer parsed.deinit();
|
|
const def = parsed.definition;
|
|
try testing.expectEqualStrings("file_header", def.name.?);
|
|
try testing.expectEqual(Definition.NameKind.tag, def.name_kind);
|
|
|
|
const ms = def.members;
|
|
try testing.expectEqual(@as(usize, 10), ms.len);
|
|
try expectMember(ms[0], "magic", "uint32_t", .int);
|
|
try expectMember(ms[1], "version", "unsigned short", .short);
|
|
try expectMember(ms[2], "name", "const char *", .pointer);
|
|
try expectMember(ms[3], "aliases", "const char **", .pointer);
|
|
try expectMember(ms[4], "offsets", "long long int", .long_long);
|
|
try testing.expectEqualSlices(u64, &.{ 2, 3 }, ms[4].dims);
|
|
try expectMember(ms[5], "len", "size_t", .pointer);
|
|
try expectMember(ms[6], "k", "enum kind", .enumeration);
|
|
try expectMember(ms[7], "next", "struct file_header *", .pointer);
|
|
try expectMember(ms[8], "on_close", "void (*)()", .pointer);
|
|
try expectMember(ms[9], "data", "unsigned char", .char);
|
|
try testing.expect(ms[9].flexible);
|
|
try testing.expect(!ms[8].flexible);
|
|
|
|
// Spans point at the member's name, for layout errors to report.
|
|
const span = ms[7].span.?;
|
|
try testing.expectEqualStrings("next", source[span.start..][0..span.len]);
|
|
}
|
|
|
|
test "a typedef names the struct by its typedef" {
|
|
var parsed = try parse(testing.allocator, "typedef struct node { int v; } node_t;", null);
|
|
defer parsed.deinit();
|
|
try testing.expectEqualStrings("node_t", parsed.definition.name.?);
|
|
try testing.expectEqual(Definition.NameKind.typedef, parsed.definition.name_kind);
|
|
|
|
var anon = try parse(testing.allocator, "typedef struct { int v; } point;", null);
|
|
defer anon.deinit();
|
|
try testing.expectEqualStrings("point", anon.definition.name.?);
|
|
}
|
|
|
|
test "C's keyword combinations mean what C says" {
|
|
const Case = struct { source: []const u8, primitive: Primitive };
|
|
for ([_]Case{
|
|
.{ .source = "char", .primitive = .char },
|
|
.{ .source = "signed char", .primitive = .char },
|
|
.{ .source = "unsigned char", .primitive = .char },
|
|
.{ .source = "short", .primitive = .short },
|
|
.{ .source = "unsigned short int", .primitive = .short },
|
|
.{ .source = "int", .primitive = .int },
|
|
.{ .source = "unsigned", .primitive = .int },
|
|
.{ .source = "signed", .primitive = .int },
|
|
.{ .source = "long", .primitive = .long },
|
|
.{ .source = "unsigned long", .primitive = .long },
|
|
.{ .source = "long int", .primitive = .long },
|
|
.{ .source = "long long", .primitive = .long_long },
|
|
.{ .source = "unsigned long long int", .primitive = .long_long },
|
|
.{ .source = "int long long unsigned", .primitive = .long_long },
|
|
.{ .source = "float", .primitive = .float },
|
|
.{ .source = "double", .primitive = .double },
|
|
.{ .source = "_Bool", .primitive = .bool },
|
|
.{ .source = "bool", .primitive = .bool },
|
|
.{ .source = "__int128", .primitive = .int128 },
|
|
.{ .source = "unsigned __int128", .primitive = .int128 },
|
|
.{ .source = "volatile const unsigned", .primitive = .int },
|
|
.{ .source = "wchar_t", .primitive = .wchar },
|
|
.{ .source = "char16_t", .primitive = .short },
|
|
.{ .source = "ptrdiff_t", .primitive = .pointer },
|
|
.{ .source = "__uint128_t", .primitive = .int128 },
|
|
.{ .source = "i128", .primitive = .int128 },
|
|
.{ .source = "f32", .primitive = .float },
|
|
.{ .source = "usize", .primitive = .pointer },
|
|
.{ .source = "ptr", .primitive = .pointer },
|
|
}) |case| {
|
|
var buf: [96]u8 = undefined;
|
|
const source = try std.fmt.bufPrint(&buf, "struct {{ {s} x; }}", .{case.source});
|
|
var parsed = parse(testing.allocator, source, null) catch |err| {
|
|
std.debug.print("\"{s}\": {t}\n", .{ case.source, err });
|
|
return err;
|
|
};
|
|
defer parsed.deinit();
|
|
try testing.expectEqual(case.primitive, parsed.definition.members[0].primitive);
|
|
try testing.expectEqualStrings(case.source, parsed.definition.members[0].type_name);
|
|
}
|
|
}
|
|
|
|
test "keyword combinations C rejects are rejected" {
|
|
for ([_][]const u8{
|
|
"short char", "long short", "long long long", "signed unsigned",
|
|
"unsigned float", "short double", "long long double", "int int",
|
|
"char char", "unsigned bool", "long __int128", "void int",
|
|
"unsigned uint32_t x", "uint32_t int",
|
|
}) |spec| {
|
|
var buf: [96]u8 = undefined;
|
|
const source = try std.fmt.bufPrint(&buf, "struct {{ {s} x; }}", .{spec});
|
|
if (parse(testing.allocator, source, null)) |parsed| {
|
|
var p = parsed;
|
|
p.deinit();
|
|
std.debug.print("\"{s}\" parsed\n", .{spec});
|
|
return error.TestUnexpectedResult;
|
|
} else |err| {
|
|
try testing.expect(err == error.UnexpectedToken or err == error.UnknownType);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "several declarators share one set of specifiers" {
|
|
var parsed = try parse(testing.allocator, "struct { int a, *b, c[4], **d; }", null);
|
|
defer parsed.deinit();
|
|
const ms = parsed.definition.members;
|
|
try expectMember(ms[0], "a", "int", .int);
|
|
try expectMember(ms[1], "b", "int *", .pointer);
|
|
try expectMember(ms[2], "c", "int", .int);
|
|
try testing.expectEqualSlices(u64, &.{4}, ms[2].dims);
|
|
try expectMember(ms[3], "d", "int **", .pointer);
|
|
}
|
|
|
|
test "pointers in parentheses are pointers, and arrays of them count" {
|
|
var parsed = try parse(testing.allocator,
|
|
\\struct {
|
|
\\ int (*cb)(void);
|
|
\\ void (*handlers[4])(int, char *);
|
|
\\ char (*row)[16];
|
|
\\ int (*plain);
|
|
\\ char *const *const pp;
|
|
\\}
|
|
, null);
|
|
defer parsed.deinit();
|
|
const ms = parsed.definition.members;
|
|
try expectMember(ms[0], "cb", "int (*)()", .pointer);
|
|
try expectMember(ms[1], "handlers", "void (*)()", .pointer);
|
|
try testing.expectEqualSlices(u64, &.{4}, ms[1].dims);
|
|
try expectMember(ms[2], "row", "char (*)[]", .pointer);
|
|
try testing.expectEqual(@as(usize, 0), ms[2].dims.len);
|
|
try expectMember(ms[3], "plain", "int *", .pointer);
|
|
try expectMember(ms[4], "pp", "char **", .pointer);
|
|
}
|
|
|
|
test "integer literals are read as C reads them" {
|
|
const Case = struct { literal: []const u8, value: u64 };
|
|
for ([_]Case{
|
|
.{ .literal = "16", .value = 16 },
|
|
.{ .literal = "0x10", .value = 16 },
|
|
.{ .literal = "0X1f", .value = 31 },
|
|
.{ .literal = "010", .value = 8 },
|
|
.{ .literal = "0b101", .value = 5 },
|
|
.{ .literal = "4u", .value = 4 },
|
|
.{ .literal = "4UL", .value = 4 },
|
|
.{ .literal = "4lu", .value = 4 },
|
|
.{ .literal = "4ull", .value = 4 },
|
|
.{ .literal = "4LLU", .value = 4 },
|
|
.{ .literal = "7", .value = 7 },
|
|
}) |case| {
|
|
var buf: [64]u8 = undefined;
|
|
const source = try std.fmt.bufPrint(&buf, "struct {{ char x[{s}]; }}", .{case.literal});
|
|
var parsed = try parse(testing.allocator, source, null);
|
|
defer parsed.deinit();
|
|
try testing.expectEqualSlices(u64, &.{case.value}, parsed.definition.members[0].dims);
|
|
}
|
|
for ([_][]const u8{ "08", "0x", "0b2", "4lL", "4uu", "4lul", "1e3", "99999999999999999999999", "0xg" }) |literal| {
|
|
var buf: [64]u8 = undefined;
|
|
const source = try std.fmt.bufPrint(&buf, "struct {{ char x[{s}]; }}", .{literal});
|
|
try expectParseError(error.InvalidNumber, source, literal);
|
|
}
|
|
}
|
|
|
|
test "attributes and alignas reach the members and the struct" {
|
|
var parsed = try parse(testing.allocator,
|
|
\\struct __attribute__((packed)) wire {
|
|
\\ char tag;
|
|
\\ alignas(8) int a, b;
|
|
\\ int c __attribute__((aligned(16)));
|
|
\\ _Alignas(0) short d;
|
|
\\ int e __attribute__((__packed__, unused));
|
|
\\ __attribute__((aligned(4), deprecated("old"))) char f;
|
|
\\} __attribute__((aligned(32)));
|
|
, null);
|
|
defer parsed.deinit();
|
|
const def = parsed.definition;
|
|
try testing.expectEqualStrings("wire", def.name.?);
|
|
try testing.expect(def.@"packed");
|
|
try testing.expectEqual(@as(?u32, 32), def.aligned);
|
|
|
|
const ms = def.members;
|
|
try testing.expectEqual(@as(?u32, null), ms[0].aligned);
|
|
try testing.expectEqual(@as(?u32, 8), ms[1].aligned);
|
|
try testing.expectEqual(@as(?u32, 8), ms[2].aligned);
|
|
try testing.expectEqual(@as(?u32, 16), ms[3].aligned);
|
|
try testing.expectEqual(@as(?u32, null), ms[4].aligned);
|
|
try testing.expect(ms[5].@"packed");
|
|
try testing.expect(!ms[4].@"packed");
|
|
try testing.expectEqual(@as(?u32, 4), ms[6].aligned);
|
|
}
|
|
|
|
test "pragma pack is tracked through push and pop" {
|
|
const Case = struct { source: []const u8, pack: ?u32 };
|
|
for ([_]Case{
|
|
.{ .source = "#pragma pack(1)\nstruct { int a; };", .pack = 1 },
|
|
.{ .source = "#pragma pack(push, 2)\nstruct { int a; };\n#pragma pack(pop)", .pack = 2 },
|
|
.{ .source = "#pragma pack(4)\n#pragma pack()\nstruct { int a; };", .pack = null },
|
|
.{ .source = "#pragma pack(8)\n#pragma pack(push)\n#pragma pack(1)\n#pragma pack(pop)\nstruct { int a; };", .pack = 8 },
|
|
.{ .source = "#pragma pack(pop)\nstruct { int a; };", .pack = null },
|
|
.{ .source = " #pragma pack(16) // wide\nstruct { int a; };", .pack = 16 },
|
|
.{ .source = "struct { int a; };", .pack = null },
|
|
}) |case| {
|
|
var parsed = try parse(testing.allocator, case.source, null);
|
|
defer parsed.deinit();
|
|
try testing.expectEqual(case.pack, parsed.definition.pack);
|
|
}
|
|
}
|
|
|
|
test "the layout of a parsed struct" {
|
|
// Every piece together: a packed header with an aligned trailer.
|
|
var parsed = try parse(testing.allocator,
|
|
\\#pragma pack(push, 2)
|
|
\\typedef struct { char c; double d; uint16_t s; } pair;
|
|
\\#pragma pack(pop)
|
|
, null);
|
|
defer parsed.deinit();
|
|
var layout = try struct_layout.compute(testing.allocator, parsed.definition, struct_layout.default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 12), layout.size);
|
|
try testing.expectEqual(@as(u64, 2), layout.placements[1].offset);
|
|
}
|
|
|
|
test "errors say what and where" {
|
|
try expectParseError(error.UnknownType, "struct { my_u32 x; }", "my_u32");
|
|
try expectParseError(error.UnknownType, "struct {\n int a;\n uint33_t b;\n}", "uint33_t");
|
|
try expectParseError(error.UnsupportedType, "struct { long double x; }", "long double");
|
|
try expectParseError(error.UnsupportedType, "struct { _Atomic int x; }", "_Atomic");
|
|
try expectParseError(error.UnsupportedType, "struct { double _Complex z; }", "_Complex");
|
|
try expectParseError(error.UnsupportedType, "union u { int a; float b; };", "union");
|
|
try expectParseError(error.IncompleteType, "struct { void v; }", "void");
|
|
try expectParseError(error.BitfieldNotSupported, "struct { unsigned a : 3; }", ":");
|
|
try expectParseError(error.NestedTypeNotSupported, "struct { struct point p; }", "struct point");
|
|
try expectParseError(error.NestedTypeNotSupported, "struct { union { int a; } u; }", "union");
|
|
try expectParseError(error.NestedTypeNotSupported, "struct { struct inner { int a; } i; }", "struct");
|
|
try expectParseError(error.NestedTypeNotSupported, "struct { enum { A, B } e; }", "enum");
|
|
try expectParseError(error.PreprocessorNotSupported, "#include <stdint.h>\nstruct { int a; }", "#include <stdint.h>");
|
|
try expectParseError(error.PreprocessorNotSupported, "struct {\n#if 0\n int a;\n#endif\n}", "#if 0");
|
|
try expectParseError(error.PreprocessorNotSupported, "#pragma once\nstruct { int a; }", "#pragma once");
|
|
try expectParseError(error.InvalidAlignment, "#pragma pack(3)\nstruct { int a; }", "3");
|
|
try expectParseError(error.InvalidAlignment, "#pragma pack(32)\nstruct { int a; }", "32");
|
|
try expectParseError(error.InvalidAlignment, "struct { alignas(6) int a; }", "6");
|
|
try expectParseError(error.InvalidAlignment, "struct { int a __attribute__((aligned(0))); }", "0");
|
|
try expectParseError(error.UnsupportedAttribute, "struct { int a __attribute__((aligned)); }", "aligned");
|
|
try expectParseError(error.UnsupportedAttribute, "struct { int a __attribute__((mode(DI))); }", "mode");
|
|
try expectParseError(error.InvalidArrayLength, "struct { char a[0]; }", "0");
|
|
try expectParseError(error.UnterminatedComment, "struct { int a; /* oops }", "/*");
|
|
try expectParseError(error.UnexpectedEnd, "struct { int a;", "");
|
|
try expectParseError(error.UnexpectedEnd, "struct { int", "");
|
|
try expectParseError(error.UnexpectedEnd, "struct { void (*cb)(int, char *", "");
|
|
try expectParseError(error.UnexpectedToken, "struct { int a }", "}");
|
|
try expectParseError(error.UnexpectedToken, "struct { int a; } extra", "extra");
|
|
try expectParseError(error.UnexpectedToken, "struct { int f(void); }", "(");
|
|
try expectParseError(error.UnexpectedToken, "struct { int a; } struct { int b; }", "struct");
|
|
try expectParseError(error.UnexpectedToken, "struct { int a[]x; }", "x");
|
|
try expectParseError(error.UnexpectedToken, "struct { int a[2][]; }", "]");
|
|
try expectParseError(error.UnexpectedToken, "struct { int a @ b; }", "@");
|
|
try expectParseError(error.UnexpectedToken, "int a;", "int");
|
|
try expectParseError(error.UnexpectedToken, "typedef struct { int a; };", ";");
|
|
try expectParseError(error.UnexpectedToken, "#pragma pack(push, 2, 3)\nstruct { int a; }", ",");
|
|
try expectParseError(error.UnexpectedToken, "#pragma pack(show)\nstruct { int a; }", "show");
|
|
try expectParseError(error.UnexpectedEnd, "", "");
|
|
}
|
|
|
|
test "an empty body parses, and laying it out is what refuses it" {
|
|
// The parser's job is syntax; `compute` owns the rule that a struct has members,
|
|
// so every front end gets it.
|
|
var parsed = try parse(testing.allocator, "struct empty { ; };", null);
|
|
defer parsed.deinit();
|
|
try testing.expectError(error.EmptyStruct, struct_layout.compute(testing.allocator, parsed.definition, struct_layout.default_abi, null));
|
|
}
|
|
|
|
test "input past the size limit is refused before it is read" {
|
|
const big = try testing.allocator.alloc(u8, max_source_bytes + 10);
|
|
defer testing.allocator.free(big);
|
|
@memset(big, ' ');
|
|
var diag: Diagnostic = .{};
|
|
try testing.expectError(error.DefinitionTooLong, parse(testing.allocator, big, &diag));
|
|
try testing.expectEqual(max_source_bytes, diag.span.start);
|
|
}
|
|
|
|
test "too many members is refused at the one past the limit" {
|
|
var source: std.ArrayList(u8) = .empty;
|
|
defer source.deinit(testing.allocator);
|
|
try source.appendSlice(testing.allocator, "struct { char ");
|
|
for (0..struct_layout.max_members + 1) |i| {
|
|
if (i > 0) try source.appendSlice(testing.allocator, ",");
|
|
try source.print(testing.allocator, "m{d}", .{i});
|
|
}
|
|
try source.appendSlice(testing.allocator, "; }");
|
|
|
|
var diag: Diagnostic = .{};
|
|
try testing.expectError(error.TooManyMembers, parse(testing.allocator, source.items, &diag));
|
|
try testing.expect(std.mem.startsWith(u8, source.items[diag.span.start..], "m1024"));
|
|
}
|
|
|
|
test "locate turns an offset into a line and column" {
|
|
const source = "struct {\n int a;\n bad b;\n}";
|
|
const at = std.mem.indexOf(u8, source, "bad").?;
|
|
try testing.expectEqual(Location{ .line = 3, .column = 3 }, locate(source, at));
|
|
try testing.expectEqual(Location{ .line = 1, .column = 1 }, locate(source, 0));
|
|
try testing.expectEqual(Location{ .line = 4, .column = 2 }, locate(source, source.len));
|
|
}
|
|
|
|
test "parse releases everything on any allocation failure" {
|
|
try testing.checkAllAllocationFailures(testing.allocator, struct {
|
|
fn run(a: Allocator) !void {
|
|
var parsed = try parse(a,
|
|
\\#pragma pack(push, 4)
|
|
\\typedef struct tag { const char *name, **more; int grid[2][3]; double d; } t;
|
|
\\#pragma pack(pop)
|
|
, null);
|
|
parsed.deinit();
|
|
}
|
|
}.run, .{});
|
|
}
|