tally/engine/test/struct_oracle.zig

290 lines
13 KiB
Zig

//! Holds `struct_layout`'s profiles to a C compiler (NFR-3).
//!
//! For each profile, writes one C file asserting, with `_Static_assert`, what the
//! engine computed: the size and in-struct alignment of every scalar kind, then the
//! size, alignment, and every member's offset and size for each test struct below.
//! `build.zig` compiles each file for every clang target the profile claims to
//! reproduce, so a wrong table entry or a wrong layout rule fails `zig build test`
//! with the struct and the claim in the message. Nothing beyond Zig is needed: clang
//! is the one Zig ships.
//!
//! The test structs are C, read by the engine's own parser, so the compiler and the
//! engine see the same text.
//!
//! Usage: struct-oracle <abi-id> <output.c> [<abi-id> <output.c> ...]
//! Every profile in the engine must be named exactly once, so a profile cannot be
//! added without being checked.
const std = @import("std");
const engine = @import("engine");
const struct_layout = engine.struct_layout;
const CStructParser = engine.CStructParser;
const Primitive = struct_layout.Primitive;
const Case = struct {
/// C that the engine does not read: declarations a test struct refers to.
prelude: []const u8 = "",
/// One struct definition, named, as the engine parses it.
source: []const u8,
};
/// Each one exists to pin a rule. Any `push` a case makes it also pops: clang warns
/// about an unterminated one, and the generator resets the pack after every case
/// with `#pragma pack()` so a plain `pack(N)` cannot leak into the next.
const cases = [_]Case{
// The design document's example: one byte of interior padding.
.{ .source = "struct spec { u32 x; u8 flags; u16 id; u64 ts; };" },
// Tail padding.
.{ .source = "struct tail { uint64_t a; uint8_t b; };" },
// The data model: what `long`, pointers and `wchar_t` are.
.{ .source = "struct model { char c; long l; void *p; wchar_t w; size_t n; char e; };" },
// Where 8-byte types align.
.{ .source = "struct doubles { char c; double d; char e; long long ll; char f; };" },
// Alignment rising and falling through a struct.
.{ .source = "struct mixed { char a; short b; char c; int d; char e; long long f; char g; };" },
// Every spelling the parser accepts maps to the kind the compiler gives it.
.{ .source =
\\struct spellings {
\\ signed char sc; unsigned char uc; char c; _Bool b; bool b2;
\\ short s; unsigned short us; short int si; int i; unsigned u; signed sg;
\\ long l; unsigned long ul; long int li; long long ll; unsigned long long int ull;
\\ float f; double d;
\\ int8_t i8_; uint8_t u8_; int16_t i16_; uint16_t u16_; int32_t i32_; uint32_t u32_;
\\ int64_t i64_; uint64_t u64_;
\\ intptr_t ip; uintptr_t up; size_t sz; ssize_t ssz; ptrdiff_t pd;
\\ wchar_t wc; char16_t c16; char32_t c32;
\\ u8 s1; i8 s2; u16 s3; i16 s4; u32 s5; i32 s6; u64 s7; i64 s8;
\\ f32 s9; f64 s10; usize s11; isize s12; ptr s13;
\\ const volatile int cv;
\\};
},
// Sixteen-byte integers, on the profiles that have them.
.{ .source =
\\struct wide {
\\ char c; __int128 big; char d; unsigned __int128 ubig;
\\ __int128_t t; __uint128_t ut; u128 x; i128 y;
\\};
},
// Arrays, including C's octal and hex lengths.
.{ .source = "struct arrays { char tag[3]; int grid[2][3]; short s[0x4]; char oct[010]; double d[1]; };" },
// A flexible array member: no size, but it aligns, and the struct rounds up.
.{ .source = "struct flex { short len; char c; double data[]; };" },
.{ .source = "struct flex_tail { int n; char c; char data[]; };" },
// Pointers of every shape.
.{ .prelude = "enum color { red, green };", .source =
\\struct ptrs {
\\ char c; const char *name, **more; void (*cb)(int);
\\ void (*handlers[3])(void); char (*row)[16]; struct ptrs *next;
\\ struct elsewhere *opaque; enum color k; char *const *const pp;
\\};
},
// A typedef'd anonymous struct.
.{ .source = "typedef struct { char c; double d; } typedef_s;" },
// `packed` on the struct.
.{ .source = "struct __attribute__((packed)) packed_s { char c; int i; double d; short s; };" },
// ... with a member that asks for alignment anyway.
.{ .source = "struct __attribute__((packed)) packed_aligned { char c; int i __attribute__((aligned(4))); short s; };" },
// `packed` on one member.
.{ .source = "struct packed_member { char c; int i __attribute__((packed)); short s; };" },
// `alignas` and `aligned` on members.
.{ .source = "struct aligned_member { char c; alignas(16) int i; _Alignas(8) char d; };" },
.{ .source = "struct attr_aligned { char c; int i __attribute__((aligned(8))); char d; };" },
// An `aligned` smaller than the natural alignment.
.{ .source = "struct aligned_lower { char c; double d __attribute__((aligned(2))); };" },
// `aligned` on the struct.
.{ .source = "struct __attribute__((aligned(32))) aligned_struct { int a; char b; };" },
.{ .source = "struct trailing_aligned { int a; char b; } __attribute__((aligned(16)));" },
// `#pragma pack`.
.{ .source = "#pragma pack(push, 1)\nstruct pack1 { char c; int i; double d; };\n#pragma pack(pop)" },
.{ .source = "#pragma pack(2)\nstruct pack2 { char c; double d; short s; char e; };" },
.{ .source = "#pragma pack(4)\nstruct pack4 { char c; double d; long long ll; };" },
.{ .source = "#pragma pack(16)\nstruct pack16 { char c; double d; };" },
// How pack meets the attributes: the interactions most worth checking.
.{ .source = "#pragma pack(push, 2)\nstruct pack_attr_aligned { char c; int i __attribute__((aligned(8))); };\n#pragma pack(pop)" },
.{ .source = "#pragma pack(push, 2)\nstruct pack_alignas { char c; alignas(8) int i; };\n#pragma pack(pop)" },
.{ .source = "#pragma pack(push, 4)\nstruct __attribute__((packed)) pack_packed { char c; double d; };\n#pragma pack(pop)" },
.{ .source = "#pragma pack(push, 1)\nstruct __attribute__((aligned(8))) pack_struct_aligned { char c; int i; };\n#pragma pack(pop)" },
.{ .source = "#pragma pack(push, 2)\nstruct pack_flex { char c; double data[]; };\n#pragma pack(pop)" },
};
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
const io = init.io;
const args = try init.minimal.args.toSlice(allocator);
if (args.len < 3 or (args.len - 1) % 2 != 0) {
std.log.err("usage: struct-oracle <abi-id> <output.c> [...]", .{});
return error.BadArguments;
}
var seen = [_]bool{false} ** struct_layout.abis.len;
var i: usize = 1;
while (i < args.len) : (i += 2) {
const index = for (struct_layout.abis, 0..) |abi, n| {
if (std.mem.eql(u8, abi.id, args[i])) break n;
} else {
std.log.err("no profile named '{s}'", .{args[i]});
return error.UnknownAbi;
};
if (seen[index]) {
std.log.err("'{s}' named twice", .{args[i]});
return error.DuplicateAbi;
}
seen[index] = true;
const text = try generate(allocator, &struct_layout.abis[index]);
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = args[i + 1], .data = text });
}
for (struct_layout.abis, seen) |abi, checked| {
if (!checked) {
std.log.err("profile '{s}' has no compiler target in build.zig", .{abi.id});
return error.UncheckedAbi;
}
}
}
/// The C spelling of each kind, for the scalar probes.
const spelling: std.EnumArray(Primitive, []const u8) = .init(.{
.char = "char",
.bool = "_Bool",
.short = "short",
.int = "int",
.long = "long",
.long_long = "long long",
.int128 = "__int128",
.float = "float",
.double = "double",
.pointer = "void *",
.wchar = "wchar_t",
.enumeration = "enum probe_enum",
});
const prelude =
\\#include <stddef.h>
\\#include <stdint.h>
\\#include <stdbool.h>
\\#include <stdalign.h>
\\
\\/* What the engine's parser accepts that C does not define for itself. */
\\typedef __CHAR16_TYPE__ char16_t;
\\typedef __CHAR32_TYPE__ char32_t;
\\typedef __PTRDIFF_TYPE__ ssize_t;
\\typedef uint8_t u8;
\\typedef int8_t i8;
\\typedef uint16_t u16;
\\typedef int16_t i16;
\\typedef uint32_t u32;
\\typedef int32_t i32;
\\typedef uint64_t u64;
\\typedef int64_t i64;
\\typedef float f32;
\\typedef double f64;
\\typedef size_t usize;
\\typedef ptrdiff_t isize;
\\typedef void *ptr;
\\#ifdef __SIZEOF_INT128__
\\typedef unsigned __int128 u128;
\\typedef __int128 i128;
\\#endif
\\
\\enum probe_enum { probe_enum_value };
\\
;
fn generate(allocator: std.mem.Allocator, abi: *const struct_layout.Abi) ![]u8 {
var out: std.Io.Writer.Allocating = .init(allocator);
const w = &out.writer;
try w.print(
\\/* Generated by engine/test/struct_oracle.zig. Every assertion is a claim the
\\ * engine makes about the {s} profile ({s}); a failure here means
\\ * the engine disagrees with the compiler. */
\\
, .{ abi.id, abi.label });
try w.writeAll(prelude);
// Every scalar kind: its size, and its alignment inside a struct (which is not
// `_Alignof`'s answer on i386, where a double aligns to 8 alone and 4 in a struct).
try w.writeAll("/* Scalars */\n");
for (std.enums.values(Primitive)) |p| {
const shape = abi.shapes.get(p) orelse {
if (p != .int128) return error.ProbeMissing;
try w.print(
\\#ifdef __SIZEOF_INT128__
\\#error "{s}: the engine says there is no __int128, and this target has one"
\\#endif
\\
, .{abi.id});
continue;
};
if (p == .int128) try w.print(
\\#ifndef __SIZEOF_INT128__
\\#error "{s}: the engine says there is an __int128, and this target has none"
\\#endif
\\
, .{abi.id});
const c_type = spelling.get(p);
try w.print(
\\struct probe_{t} {{ char c; {s} v; }};
\\_Static_assert(sizeof({s}) == {d}, "{s}: the engine says {s} is {d} bytes");
\\_Static_assert(__builtin_offsetof(struct probe_{t}, v) == {d}, "{s}: the engine says {s} aligns to {d} in a struct");
\\
, .{
p, c_type, c_type, shape.size, abi.id, c_type, shape.size,
p, shape.alignment, abi.id, c_type, shape.alignment,
});
}
for (cases) |case| try writeCase(allocator, w, abi, case);
return out.toOwnedSlice();
}
fn writeCase(allocator: std.mem.Allocator, w: *std.Io.Writer, abi: *const struct_layout.Abi, case: Case) !void {
var diag: CStructParser.Diagnostic = .{};
var parsed = CStructParser.parse(allocator, case.source, &diag) catch |err| {
const at = CStructParser.locate(case.source, diag.span.start);
std.log.err("a test struct does not parse ({t}, line {d}, column {d}):\n{s}", .{
err, at.line, at.column, case.source,
});
return err;
};
defer parsed.deinit();
const def = parsed.definition;
const name = def.name orelse return error.UnnamedCase;
var ref_buf: [64]u8 = undefined;
const ref = switch (def.name_kind) {
.tag => try std.fmt.bufPrint(&ref_buf, "struct {s}", .{name}),
.typedef => name,
};
var layout = struct_layout.compute(allocator, def, abi, null) catch |err| switch (err) {
// Left out entirely, since the compiler would refuse the struct. Whether the
// type really is absent is the scalar probes' assertion.
error.TypeUnavailable => {
try w.print("\n/* {s}: uses a type {s} does not have */\n", .{ name, abi.id });
return;
},
else => {
std.log.err("{s} does not lay out on {s}: {t}", .{ name, abi.id, err });
return err;
},
};
defer layout.deinit(allocator);
try w.print("\n/* {s} */\n{s}\n{s}\n#pragma pack()\n", .{ name, case.prelude, case.source });
try w.print(
\\_Static_assert(sizeof({s}) == {d}, "{s}: the engine says {s} is {d} bytes");
\\_Static_assert(_Alignof({s}) == {d}, "{s}: the engine says {s} aligns to {d}");
\\
, .{ ref, layout.size, abi.id, ref, layout.size, ref, layout.alignment, abi.id, ref, layout.alignment });
for (def.members, layout.placements) |member, placement| {
try w.print(
\\_Static_assert(__builtin_offsetof({s}, {s}) == {d}, "{s}: the engine says {s}.{s} is at {d}");
\\
, .{ ref, member.name, placement.offset, abi.id, ref, member.name, placement.offset });
if (!member.flexible) try w.print(
\\_Static_assert(sizeof((({s} *)0)->{s}) == {d}, "{s}: the engine says {s}.{s} is {d} bytes");
\\
, .{ ref, member.name, placement.size, abi.id, ref, member.name, placement.size });
}
}