CLI-only support for struct layout
This commit is contained in:
parent
c22ed56806
commit
5aa70fcc8a
11 changed files with 3779 additions and 201 deletions
|
|
@ -24,6 +24,7 @@
|
|||
| Parser.zig | evaluator.zig | programmer.zig units.zig |
|
||||
| tokenizer | financial.zig | bitwise.zig float_interp|
|
||||
| ast.zig | number/Rational/Integer grouping.zig|
|
||||
| struct_layout.zig CStructParser.zig |
|
||||
+-------------------------------------------------------------+
|
||||
```
|
||||
|
||||
|
|
@ -87,8 +88,10 @@ build.zig
|
|||
| `float_interp.zig` | IEEE 754 bit-level interpretation |
|
||||
| `units.zig` | Unit conversion tables and resolver |
|
||||
| `financial.zig` | CAGR, TVM, compound interest, amortization, and money display (cents are a property of money, not of a screen) |
|
||||
| `struct_layout.zig` | Struct layout (section 3): the ABI profiles as data, a language-neutral `Definition`, and `compute`. Knows no syntax |
|
||||
| `CStructParser.zig` | Reads a C struct definition into a `struct_layout.Definition` (file-as-struct, like `Parser.zig`), with its own lexer and positioned diagnostics |
|
||||
| `message.zig` | Gone: the wording lives in `engine.zig` beside the `Error` union it words |
|
||||
| `engine.zig` | Public API surface (Zig-native): the seven files a frontend imports, nine name aliases, the derived `Error` union, and `phrase` |
|
||||
| `engine.zig` | Public API surface (Zig-native): the nine files a frontend imports, nine name aliases, the derived `Error` union, and `phrase` |
|
||||
| `c_api.zig` | `extern "C"` seam for JNI/FFI consumers. Two of its three exports are stubs; see section 6 |
|
||||
|
||||
Every file declares its own error set; there is no shared one (section 10).
|
||||
|
|
@ -99,7 +102,7 @@ unrelated groups: the error vocabulary, the fixed-width integer model, `Base` (u
|
|||
only by the lexer and the AST), and `Mode` (which the engine stored and never read).
|
||||
`errors.zig` was where the error vocabulary landed, until the same question showed that
|
||||
what needed splitting was the type inside it. Each piece has gone to the file that
|
||||
owns it. `struct_layout.zig` does not exist; section 3 is a design, not a description.
|
||||
owns it.
|
||||
|
||||
A file is named TitleCase when the file *is* the type, with its fields at container
|
||||
level: `Integer.zig`, `Rational.zig` and `Parser.zig`. `number.zig` stays lowercase
|
||||
|
|
@ -758,119 +761,180 @@ upstream, and diagnostic paths inside the tests themselves.
|
|||
|
||||
---
|
||||
|
||||
## 3. Struct Layout Engine (DESIGN ONLY, NOT BUILT)
|
||||
## 3. Struct Layout Engine
|
||||
|
||||
Nothing in this section exists. There is no `struct_layout.zig`, no ABI profile
|
||||
interface, no `tally struct` subcommand and no struct sub-view in the TUI, and the four
|
||||
errors this design needs (`InvalidType`, `InvalidFieldName`, `DuplicateFieldName`,
|
||||
`StructTooLarge`) were deleted from the engine because nothing could produce them.
|
||||
Built as two engine files and a CLI subcommand (FR-3, FR-6.5). The TUI sub-view (8.3) and
|
||||
the Android screen (9.4) are not, and the C ABI has no export for it yet. An earlier
|
||||
version of this section was a proposal: a DSL of its own, an ABI interface of function
|
||||
pointers, and a precomputed byte map. Each of those went a different way once built, and
|
||||
3.6 says why.
|
||||
|
||||
The feature is wanted (FR-3) and is sequenced after the Android app. The design is kept
|
||||
because it is the one part of the plan with a grammar and an algorithm worked out in
|
||||
advance, and because the shape below is still what we would build.
|
||||
The split is between syntax and layout:
|
||||
|
||||
Read what follows as a proposal.
|
||||
- `CStructParser.zig` reads text into a `struct_layout.Definition`. It is the only part
|
||||
that knows C.
|
||||
- `struct_layout.zig` lays a `Definition` out under an `Abi` and knows no syntax at all.
|
||||
A Rust `#[repr(C)]` or Zig `extern struct` front end would be a second parser
|
||||
producing the same `Definition` (FR-3.13).
|
||||
|
||||
### 3.1 Mini-DSL Grammar
|
||||
### 3.1 The input is C
|
||||
|
||||
The point is pasting a struct from real code, so the input is C rather than a language
|
||||
invented for the calculator: a struct copied out of a header should be accepted as it
|
||||
stands, or rejected with a reason and a position. FR-3.1 and FR-3.12 list what the
|
||||
subset accepts and refuses.
|
||||
|
||||
```
|
||||
struct_def := "struct" "{" field_list "}"
|
||||
field_list := (field ";")*
|
||||
field := type_name identifier
|
||||
type_name := "u8" | "u16" | "u32" | "u64"
|
||||
| "i8" | "i16" | "i32" | "i64"
|
||||
| "f32" | "f64" | "bool" | "ptr"
|
||||
| "[" number "]" type_name // arrays
|
||||
identifier := [a-zA-Z_][a-zA-Z0-9_]*
|
||||
input := directive* definition directive*
|
||||
definition := "typedef"? "struct" attrs? tag? attrs? "{" declaration* "}" attrs? name? ";"?
|
||||
declaration := specifiers declarator ("," declarator)* ";"
|
||||
declarator := attrs? ("*" qualifier*)* (identifier | "(" "*"+ identifier dims ")" suffix)
|
||||
dims attrs?
|
||||
dims := ("[" integer? "]")* // only the first may be empty
|
||||
suffix := "(" balanced ")" | dims // a function pointer, or a pointer to an array
|
||||
directive := "#pragma pack(" (N | "push" ("," N)? | "pop" | "") ")"
|
||||
```
|
||||
|
||||
### 3.2 ABI Profile Interface
|
||||
Decisions worth recording:
|
||||
|
||||
- **Its own lexer.** The expression tokenizer was the obvious thing to reuse and is wrong
|
||||
for C in three places that would each give a wrong answer rather than an error: `1,000`
|
||||
is a grouped number there, `010` is ten (C says eight), and a hex literal continues
|
||||
across a space. It also has no braces or brackets.
|
||||
- **Type words are counted, then resolved,** C's way: `unsigned` is `unsigned int`,
|
||||
`int long long unsigned` is `unsigned long long`, and `short char` is an error. A name
|
||||
that is none of the words and no known typedef is `UnknownType`, pointing at the name.
|
||||
- **Unknown is an error, never a skip.** `#if 0` is refused rather than ignored, because
|
||||
ignoring it would count the members inside it. An unknown attribute is refused because
|
||||
`mode(DI)` and `vector_size` change a type. Harmless ones (`unused`, `deprecated`,
|
||||
`may_alias`) are accepted.
|
||||
- **Every error carries a span** (`Diagnostic`), and `locate` turns it into a line and
|
||||
column, so the CLI can say `line 3, column 5: uint33_t`. Layout errors carry the
|
||||
member's index instead, and the member carries the span of its name.
|
||||
- **`Parsed` owns an arena** with every name copied in, so a definition outlives its
|
||||
source. Input is capped at 64 KiB before anything is read (NFR-8).
|
||||
|
||||
### 3.2 An ABI is a table
|
||||
|
||||
```zig
|
||||
pub const AbiProfile = struct {
|
||||
name: []const u8,
|
||||
pointer_size: u8, // bytes
|
||||
max_alignment: u8, // maximum alignment enforced
|
||||
|
||||
/// Given a field type, return its (size, alignment) in bytes
|
||||
field_layout: *const fn (field_type: FieldType) -> struct { size: u8, alignment: u8 },
|
||||
|
||||
/// Struct-level alignment (usually max of all field alignments)
|
||||
struct_alignment: *const fn (fields: []const FieldInfo) -> u8,
|
||||
|
||||
/// Whether tail padding is added to round up to struct alignment
|
||||
tail_padding: bool,
|
||||
pub const Abi = struct {
|
||||
id: []const u8, // "sysv64", what --abi takes
|
||||
label: []const u8, // "64-bit Linux and macOS (x86-64 SysV, AArch64)"
|
||||
shapes: std.EnumArray(Primitive, ?Shape), // size and alignment, null where absent
|
||||
rules: Rules, // .gnu or .microsoft (3.3)
|
||||
};
|
||||
|
||||
// Built-in profiles
|
||||
pub const x86_64_sysv = AbiProfile{ ... }; // default
|
||||
pub const x86_64_win64 = AbiProfile{ ... }; // future
|
||||
pub const packed = AbiProfile{ ... }; // no padding
|
||||
pub const Primitive = enum {
|
||||
char, bool, short, int, long, long_long, int128,
|
||||
float, double, pointer, wchar, enumeration,
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 Layout Computation Algorithm
|
||||
`Primitive` lists C's kinds rather than its spellings: `uint32_t`, `u32` and `unsigned int`
|
||||
all arrive as `.int`, and `size_t` and every pointer as `.pointer`, because on every
|
||||
profile here `size_t` is pointer-sized. A profile is a value in a table, not an
|
||||
implementation of an interface: `EnumArray.init` requires a shape for every kind, so a
|
||||
profile cannot forget one, and the proposal's function pointers would have bought
|
||||
nothing a table cannot say.
|
||||
|
||||
| Profile | `long` | pointer | 8-byte types align to | `wchar_t` | `__int128` | Rules |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `sysv64` (default) | 8 | 8 | 8 | 4 | 16, aligned 16 | GNU |
|
||||
| `win64` | 4 | 8 | 8 | 2 | 16, aligned 16 | Microsoft |
|
||||
| `i386` | 4 | 4 | 4 | 4 | absent | GNU |
|
||||
| `arm32` | 4 | 4 | 8 | 4 | absent | GNU |
|
||||
|
||||
AArch64 is not its own profile: for every type here it matches x86-64 SysV, on Linux and
|
||||
macOS alike (they differ in `long double`, which is not modelled). The clang check
|
||||
(3.5) holds `sysv64` to all three targets, which is the evidence for folding them.
|
||||
|
||||
### 3.3 The algorithm
|
||||
|
||||
```
|
||||
function compute_layout(fields, abi):
|
||||
offset = 0
|
||||
struct_align = 1
|
||||
result_fields = []
|
||||
|
||||
for each field in fields:
|
||||
(size, align) = abi.field_layout(field.type)
|
||||
struct_align = max(struct_align, align)
|
||||
|
||||
# Insert padding for alignment
|
||||
padding = (align - (offset % align)) % align
|
||||
if padding > 0:
|
||||
emit_padding(offset, padding)
|
||||
offset += padding
|
||||
|
||||
# Place field
|
||||
emit_field(field.name, field.type, offset, size)
|
||||
offset += size
|
||||
|
||||
# Tail padding
|
||||
if abi.tail_padding:
|
||||
tail = (struct_align - (offset % struct_align)) % struct_align
|
||||
if tail > 0:
|
||||
emit_padding(offset, tail)
|
||||
offset += tail
|
||||
|
||||
return StructLayout {
|
||||
total_size: offset,
|
||||
alignment: struct_align,
|
||||
fields: result_fields,
|
||||
}
|
||||
for each member:
|
||||
alignment = scalar alignment
|
||||
if packed (member or struct): alignment = 1
|
||||
gnu: alignment = max(alignment, aligned); alignment = min(alignment, pack)
|
||||
microsoft: alignment = min(alignment, pack); alignment = max(alignment, aligned)
|
||||
offset = round_up(offset, alignment) # padding_before = the difference
|
||||
place the member; offset += element size * product(dims) # 0 for a flexible array
|
||||
struct_alignment = max(struct_alignment, alignment)
|
||||
struct_alignment = max(struct_alignment, aligned on the struct)
|
||||
size = round_up(offset, struct_alignment) # tail padding = the difference
|
||||
```
|
||||
|
||||
### 3.4 Layout Result Structure
|
||||
**The order of `aligned` and `pack` is the one rule that differs by compiler, and the
|
||||
clang check found it.** The first version used the GNU order everywhere, and the
|
||||
`windows-msvc` targets refused two of the test structs: under `#pragma pack(2)`,
|
||||
`char c; alignas(8) int i;` puts `i` at 2 under GCC's rules and at 8 under MSVC's, where
|
||||
an explicit alignment is applied after the cap and wins. That is now `Abi.rules`, a
|
||||
property of the profile, since clang chooses it by target too. Bitfields are where the
|
||||
two families differ most, which is one reason they are out of scope (FR-3.12).
|
||||
|
||||
Every size is checked arithmetic, and a struct larger than the ABI can address
|
||||
(`PTRDIFF_MAX`: 2^31 - 1 on the 32-bit profiles) is `StructTooLarge`, because a C
|
||||
compiler refuses it and a layout that no program could have is not an answer.
|
||||
|
||||
### 3.4 The result
|
||||
|
||||
```zig
|
||||
pub const StructLayoutResult = struct {
|
||||
total_size: usize,
|
||||
alignment: u8,
|
||||
fields: []const LayoutField,
|
||||
bytes: []const ByteInfo, // byte-by-byte map for visualization
|
||||
abi_name: []const u8,
|
||||
};
|
||||
|
||||
pub const LayoutField = struct {
|
||||
name: []const u8,
|
||||
type_name: []const u8,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
alignment: u8,
|
||||
padding_before: usize,
|
||||
};
|
||||
|
||||
pub const ByteInfo = union(enum) {
|
||||
field: struct { field_index: usize, byte_within_field: usize },
|
||||
padding: struct { before_field_index: usize },
|
||||
tail_padding: void,
|
||||
pub const Layout = struct {
|
||||
abi: *const Abi,
|
||||
members: []const Member, // borrowed from the definition
|
||||
placements: []Placement, // owned: offset, size, alignment, padding_before
|
||||
size: u64,
|
||||
alignment: u32,
|
||||
tail_padding: u64,
|
||||
packed_size: u64, // sum of member sizes: FR-3.10's comparison
|
||||
pub fn padding(self) u64;
|
||||
pub fn segments(self) SegmentIterator; // member / padding / tail runs, in order
|
||||
};
|
||||
```
|
||||
|
||||
The proposal's `bytes: []const ByteInfo` is `segments()` instead: the same information,
|
||||
derived on demand. A struct holding `char buf[1 << 20]` is three segments, not a million
|
||||
entries, and the CLI's memory map draws a 2 GiB struct as quickly as a small one because
|
||||
it finds its rows from the segments.
|
||||
|
||||
### 3.5 Checked against clang
|
||||
|
||||
NFR-3 says a layout must match what a C compiler produces, and this is how that is held
|
||||
rather than hoped for. `engine/test/struct_oracle.zig` runs on the build machine and
|
||||
writes one C file per profile:
|
||||
|
||||
- a probe per scalar kind: `sizeof`, and the offset of the scalar after a `char` (its
|
||||
in-struct alignment, which on i386 is not `_Alignof`'s answer for `double`);
|
||||
- `__int128`'s presence or absence, as an `#error` when the target disagrees;
|
||||
- for each of 29 test structs: `sizeof`, `_Alignof`, and every member's offset and size.
|
||||
|
||||
Each test struct is C read by the engine's own parser, so the compiler and the engine see
|
||||
the same text. `build.zig` compiles each file, freestanding with `-Werror`, for every
|
||||
clang target its profile claims:
|
||||
|
||||
| Profile | Targets |
|
||||
|---|---|
|
||||
| `sysv64` | `x86_64-linux-gnu`, `aarch64-linux-gnu`, `aarch64-macos` |
|
||||
| `win64` | `x86_64-windows-msvc`, `aarch64-windows-msvc` |
|
||||
| `i386` | `x86-linux-gnu` |
|
||||
| `arm32` | `arm-linux-gnueabihf` |
|
||||
|
||||
Compiling is the test, inside `zig build test`, and nothing beyond Zig is needed: the
|
||||
clang is the one Zig ships. A wrong table entry fails with the profile, the struct and the
|
||||
claim in the message; changing i386's `double` to align 8 failed twenty assertions. The
|
||||
generator refuses to run unless every profile in the engine has a target listed, so a
|
||||
profile cannot be added without a compiler to answer to.
|
||||
|
||||
### 3.6 What changed from the proposal, and why
|
||||
|
||||
- **A DSL of our own became C.** The DSL's only user would have been someone retyping a
|
||||
struct they already had in C.
|
||||
- **The function-pointer interface became a table.** Sizes and alignments are data;
|
||||
the one behavioural difference found so far (3.3) is an enum.
|
||||
- **`packed` stopped being a profile.** It is a property of a struct, and applies under
|
||||
any ABI; a "packed profile" could not have said what a pointer's size is.
|
||||
- **The byte map became an iterator** (3.4).
|
||||
- **Endianness was deferred** (FR-3.8): without values it only relabels bytes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Unit Conversion Engine
|
||||
|
|
@ -1657,6 +1721,7 @@ visible in one result, a nonzero `jboolean` as true, refused widths changing not
|
|||
tally [OPTIONS] <expression>
|
||||
tally convert <value> <from-unit> [to] <to-unit>
|
||||
tally amort <principal> <rate-per-period> <periods> [payment] [--monthly] [--summary] [--exact]
|
||||
tally struct <definition | file | -> [--abi <name>] [--color <when>]
|
||||
|
||||
Options:
|
||||
-p, --programmer Programmer mode
|
||||
|
|
@ -1669,10 +1734,11 @@ Options:
|
|||
Each subcommand answers -h with its own usage.
|
||||
```
|
||||
|
||||
The flags this section used to list (`-b`, `--base`, `--json`, `--abi`) were never
|
||||
built. `--bits` is the spelling, there is no way to select a single output base (the
|
||||
The flags this section used to list (`-b`, `--base`, `--json`) were never built.
|
||||
`--bits` is the spelling, there is no way to select a single output base (the
|
||||
multi-base view appears when the expression used a non-decimal literal), and JSON
|
||||
output belongs to the C API rather than the CLI. `tally struct` goes with section 3.
|
||||
output belongs to the C API rather than the CLI. `--abi` exists, but on `tally struct`
|
||||
rather than at the top level, since nothing else has an ABI.
|
||||
|
||||
### CLI Output Examples
|
||||
|
||||
|
|
@ -1694,26 +1760,57 @@ $ tally -p "0xFF & 0x0F"
|
|||
Separators are spaces, not underscores, and the base prefix appears only in the
|
||||
clipboard form. There is no `bits:` line.
|
||||
|
||||
Struct (SKETCH, section 3 is not built):
|
||||
Struct (actual output). Each member's letter in the table is its letter in the map,
|
||||
`.` is padding, and the profile is named because the answer depends on it:
|
||||
```
|
||||
$ tally struct "struct { u32 x; u8 flags; u16 id; u64 ts; }"
|
||||
ABI: x86-64 System V | Endian: little
|
||||
$ tally struct 'struct packet { uint8_t kind; uint32_t len; char name[24]; void *next; }'
|
||||
struct packet
|
||||
ABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64)
|
||||
|
||||
Offset Size Field Type Align Padding
|
||||
────── ──── ────────── ───── ───── ───────
|
||||
0 4 x u32 4
|
||||
4 1 flags u8 1
|
||||
5 1 (padding) 1 byte
|
||||
6 2 id u16 2
|
||||
8 8 ts u64 8
|
||||
Offset Size Align Type Name
|
||||
a 0 1 1 uint8_t kind
|
||||
1 3 (padding)
|
||||
b 4 4 4 uint32_t len
|
||||
c 8 24 1 char[24] name
|
||||
d 32 8 8 void * next
|
||||
|
||||
Total: 16 bytes (alignment: 8, padding: 1 byte, tail: 0)
|
||||
40 bytes, alignment 8, 3 bytes of padding (7.5%); 37 bytes packed
|
||||
|
||||
Memory map:
|
||||
00: [xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx] x
|
||||
04: [ffff ffff] [········] [iiii iiii iiii iiii] flags, pad, id
|
||||
08: [tttt tttt tttt tttt tttt tttt tttt tttt ts
|
||||
0C: tttt tttt tttt tttt tttt tttt tttt tttt]
|
||||
0x00 a . . . b b b b c c c c c c c c
|
||||
0x10 c c c c c c c c c c c c c c c c
|
||||
0x20 d d d d d d d d
|
||||
```
|
||||
The sketch this replaced used box-drawing rules and a middle dot, a `Padding` column, and
|
||||
a map of one bracketed run per member. The output is ASCII, a padding run is a row of its
|
||||
own so the table reads in address order, and the map is one letter per byte: sixteen
|
||||
bytes a row is what a hex dump looks like, so the eye already knows how to read it. Rows
|
||||
that repeat collapse to `*`, as in hexdump, and the map is found from the layout's
|
||||
segments rather than walked byte by byte, so a 2 GiB array costs three lines.
|
||||
|
||||
On a terminal the same output is colored (FR-6.5.1): a member's key and name in the
|
||||
table and its run of bytes in the map share one of the terminal's six hues, and padding
|
||||
is faint, so the map reads as colored blocks with gaps between them. Color is layered on
|
||||
rather than substituted: `format.Paint` is `plain` or `ansi`, the renderer writes the
|
||||
same text either way plus escape codes in the second case, and a test strips the codes
|
||||
and compares the result to the plain output byte for byte. `main` decides which, from
|
||||
`--color`, whether stdout supports escape codes, `NO_COLOR` and `TERM`, through a pure
|
||||
`ColorChoice.resolve` that is tested on its own. Three choices behind it:
|
||||
|
||||
- **The terminal's palette, not fixed RGB.** The TUI's Molokai triples (open item 17)
|
||||
ignore the user's theme; SGR 31-36 are whatever the theme says, and a theme is built
|
||||
so its own colors read on its own background.
|
||||
- **No background, ever.** Forcing black was considered as insurance against an
|
||||
unreadable hue and rejected: it would undo the theme on every light terminal to fix a
|
||||
problem the theme already solves.
|
||||
- **Faint for padding, not bright black.** Several themes (Solarized among them) draw
|
||||
bright black in nearly the background color, which would make padding vanish where it
|
||||
should recede. Every code begins with a reset, because faint persists until cleared.
|
||||
|
||||
An error names its place:
|
||||
```
|
||||
$ tally struct 'struct { int a; uint33_t b; }'
|
||||
error: unknown type (typedefs and macros from elsewhere are not visible)
|
||||
line 1, column 17: uint33_t
|
||||
```
|
||||
|
||||
Unit conversion (actual output):
|
||||
|
|
@ -2013,6 +2110,15 @@ IEEE 754 float interpretation:
|
|||
float format)
|
||||
|
||||
### 8.3 Struct Visualizer (sub-view of Programmer Mode) - DESIGN ONLY, NOT BUILT
|
||||
|
||||
The engine behind this exists (section 3); the view does not, deliberately. It waits
|
||||
until `tally struct` has been used enough to say what an interactive view should do:
|
||||
whether people paste whole definitions or build them a member at a time, and whether
|
||||
reordering to watch the padding move is worth a key. The sketch below predates the CLI,
|
||||
and where they disagree the CLI's table and map (section 7) are the better starting
|
||||
point, not least because the TUI draws one byte per cell and cannot show the box and
|
||||
block glyphs this sketch uses.
|
||||
|
||||
```
|
||||
┌─ Tally ────────────────── [Programmer > Struct Layout] ─────────────┐
|
||||
│ ABI: x86-64 SysV Endian: LE │
|
||||
|
|
@ -2514,7 +2620,8 @@ Rejected or deferred, and why:
|
|||
- **The float view (Ctrl-F), ASCII literals (`'ELF'`) and the system keyboard** are not
|
||||
built: none needs the engine to change, and each is easier to place once the screen
|
||||
has been used.
|
||||
- **Struct layout** (9.4) waits for FR-3.
|
||||
- **Struct layout** (9.4) waits for a C ABI export of the engine's layout (section 3),
|
||||
and for the TUI view to settle what an interactive one should do.
|
||||
|
||||
### 9.4 Struct Layout Screen (Programmer sub-screen)
|
||||
|
||||
|
|
@ -2776,6 +2883,21 @@ pub const Error = error{
|
|||
DomainError, DivisionByZero, OutOfMemory,
|
||||
};
|
||||
|
||||
// struct_layout.zig: what a definition can fail to lay out with
|
||||
pub const Error = error{
|
||||
EmptyStruct, TooManyMembers, DuplicateMember, TypeUnavailable,
|
||||
InvalidArrayLength, MisplacedFlexibleArray, InvalidAlignment, StructTooLarge, OutOfMemory,
|
||||
};
|
||||
|
||||
// CStructParser.zig: what reading C can fail with, sharing the expression parser's
|
||||
// names (and so its wording) for the three they have in common
|
||||
pub const Error = error{
|
||||
UnexpectedToken, UnexpectedEnd, InvalidNumber, UnterminatedComment,
|
||||
UnknownType, UnsupportedType, IncompleteType, UnsupportedAttribute,
|
||||
BitfieldNotSupported, NestedTypeNotSupported, PreprocessorNotSupported,
|
||||
DefinitionTooLong, InvalidArrayLength, InvalidAlignment, TooManyMembers, OutOfMemory,
|
||||
};
|
||||
|
||||
// evaluator.zig: its own, plus every dependency's
|
||||
pub const Error = error{
|
||||
UnknownFunction, WrongArgumentCount, UnknownVariable,
|
||||
|
|
@ -2783,15 +2905,18 @@ pub const Error = error{
|
|||
} || Parser.Error || number.Error || bitwise.Error || financial.Error;
|
||||
|
||||
// engine.zig: what any entry point can return, for frontends to switch over
|
||||
pub const Error = evaluator.Error || programmer.Error || units.Error || financial.Error;
|
||||
pub const Error = evaluator.Error || programmer.Error || units.Error || financial.Error ||
|
||||
struct_layout.Error || CStructParser.Error;
|
||||
```
|
||||
|
||||
This replaced one hand-written `CalcError` with 20 members that every engine function
|
||||
claimed to return. That signature was false in both directions: `parser.parse` said it
|
||||
might return `ConvergenceFailure` and `UnknownUnit`, so no caller could switch on what
|
||||
a parse can actually produce, and four members (`InvalidType`, `InvalidFieldName`,
|
||||
`DuplicateFieldName`, `StructTooLarge`) belonged to a struct layout feature that does
|
||||
not exist, so nothing could return them while `errorPhrase` still gave them wording.
|
||||
`DuplicateFieldName`, `StructTooLarge`) belonged to a struct layout feature that did not
|
||||
exist yet, so nothing could return them while `errorPhrase` still gave them wording.
|
||||
When the feature was built its errors arrived with it, from the module that raises them,
|
||||
and only `StructTooLarge` kept its old name.
|
||||
|
||||
Error members unify by name in Zig, so the per-file sets compose with no
|
||||
coordination: `parser.Error.OutOfMemory` and `units.Error.OutOfMemory` are the same
|
||||
|
|
@ -2829,7 +2954,7 @@ carry its own copy of the whole table, and the TUI's had drifted three errors be
|
|||
| 1 | Pratt parser over recursive descent | Easier to extend precedence levels; cleaner for infix with mixed prefix operators |
|
||||
| 2 | `u128` storage for all programmer integers | One representation for every width; `width` masks and interprets rather than deciding storage, which is what makes the width a non-destructive display lens (8.2.1). This said `u64` until 128-bit support landed |
|
||||
| 3 | JSON over C ABI boundary | Avoids complex struct marshaling; Kotlin/Swift handle JSON natively; small perf cost acceptable for calculator. Still the plan; not built (section 6) |
|
||||
| 4 | Separate parser for struct DSL | Struct grammar is different enough from expressions; keeps both parsers simple. Not built (section 3) |
|
||||
| 4 | A separate parser, for C rather than a DSL | Struct definitions are pasted from C, and the expression tokenizer reads C's numbers wrongly (`010`, `1,000`). Section 3.1 |
|
||||
| 5 | ABI as interface/vtable | Adding Windows x64 or ARM is implementing one struct; no changes to layout algorithm. Not built (section 3) |
|
||||
| 6 | libvaxis for TUI | Most mature Zig TUI; supports Windows/Mac/Linux; active development; used by Ghostty |
|
||||
| 7 | `^` always means power; XOR is the `xor` keyword | Avoids mode-dependent operator overloading; every operator means the same thing in both modes; consistent with existing `rol`/`ror` keyword operators |
|
||||
|
|
|
|||
|
|
@ -46,36 +46,56 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm
|
|||
- Allow entering a decimal float value and seeing the closest IEEE 754 representation
|
||||
- Show when a value is not exactly representable (rounding occurred)
|
||||
|
||||
### FR-3: Struct Layout Visualizer (Programmer Mode Extension) - NOT BUILT, WANTED, SEQUENCED AFTER ANDROID
|
||||
### FR-3: Struct Layout Visualizer (Programmer Mode Extension) - ENGINE AND CLI BUILT, TUI AND ANDROID NOT
|
||||
|
||||
None of FR-3 exists: no DSL parser, no ABI profiles, no layout computation, no CLI
|
||||
subcommand and no TUI sub-view. The four errors it would need were removed from the
|
||||
engine because nothing could produce them, and design.md section 3 (the grammar and
|
||||
algorithm) is marked DESIGN ONLY for the same reason.
|
||||
Built: the engine (`struct_layout.zig` for the model and the layout rules,
|
||||
`CStructParser.zig` for reading C) and the `tally struct` subcommand (FR-6.5), with every
|
||||
layout checked against clang at build time (NFR-3). Not built: the TUI sub-view (FR-7.4)
|
||||
and the Android screen (FR-8.6), which wait until the CLI has been used enough to say what
|
||||
an interactive view should do. The feature is on trial: if it does not earn its keep it
|
||||
comes out whole, which is easier while nothing but the CLI depends on it.
|
||||
|
||||
The feature stays in the specification: it is wanted, and it is the reason programmer
|
||||
mode exists at all for some of its users. It is simply behind the Android app in
|
||||
priority, so expect the DESIGN ONLY markers to stand for a while rather than to be a
|
||||
sign of abandonment.
|
||||
|
||||
- **FR-3.1**: Accept struct definitions in a mini-DSL:
|
||||
- **FR-3.1**: Accept a struct definition written in C, so that a struct can be pasted from
|
||||
real code. The input is a defined subset of C, and anything outside it is an error that
|
||||
names the problem and its line and column, never a guess:
|
||||
```
|
||||
struct {
|
||||
u32 x;
|
||||
u8 flags;
|
||||
u16 id;
|
||||
u64 timestamp;
|
||||
}
|
||||
struct hdr {
|
||||
uint32_t magic; // comments are fine
|
||||
uint8_t version;
|
||||
uint16_t len;
|
||||
uint64_t timestamp;
|
||||
};
|
||||
```
|
||||
- One `struct [tag] { ... }` or `typedef struct [tag] { ... } Name;`, optionally followed
|
||||
by `;`.
|
||||
- Several declarators per declaration (`int a, *b, c[4];`), arrays of any rank, and a
|
||||
flexible array member (`char data[];`) last.
|
||||
- Pointers to anything, including function pointers and pointers to arrays, all laid out
|
||||
as one pointer.
|
||||
- Array lengths are C integer literals, so `010` is eight.
|
||||
- `//` and `/* */` comments; `const`, `volatile` and `restrict` are accepted and ignored.
|
||||
- **FR-3.2**: Compute and display total struct size in bytes.
|
||||
- **FR-3.3**: Display per-field information: offset, size, alignment requirement.
|
||||
- **FR-3.4**: Identify and highlight padding bytes between fields and at the end (tail padding).
|
||||
- **FR-3.5**: Render a byte-level memory map showing which bytes belong to which field and which are padding.
|
||||
- **FR-3.6**: Default to x86-64 System V ABI alignment rules.
|
||||
- **FR-3.7**: Support switching ABI/alignment profiles (extensible design - future: Windows x64, ARM AAPCS, packed).
|
||||
- **FR-3.8**: Support switching endianness display (little-endian default, big-endian available).
|
||||
- **FR-3.9**: Supported field types (initial set): `u8`, `u16`, `u32`, `u64`, `i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `bool`, `ptr` (pointer-sized, arch-dependent).
|
||||
- **FR-3.7**: Support switching ABI profiles. Four exist, each a table entry checked against the clang targets it claims to reproduce:
|
||||
- `sysv64` (default): 64-bit Linux and macOS. x86-64 and AArch64 agree on every type here, so one profile covers both.
|
||||
- `win64`: 64-bit Windows, x64 and ARM64. `long` is 4 bytes and `wchar_t` 2, and explicit alignment beats `#pragma pack` (FR-3.11).
|
||||
- `i386`: 32-bit x86 Linux. 8-byte types align to 4 inside a struct.
|
||||
- `arm32`: 32-bit ARM Linux (AAPCS). 8-byte types align to 8.
|
||||
- "Packed", which this requirement used to list as a profile, is not one: packing is a property of the struct (FR-3.11) and applies under any profile.
|
||||
- **FR-3.8**: DEFERRED. Endianness display. With no field values, byte order only relabels which byte of a member is its most significant, which says nothing a reader does not already know. It returns with decoding values through a struct (bytes typed in, or the programmer register), where byte order changes what is shown.
|
||||
- **FR-3.9**: Supported member types: the scalars in their C spellings (`char`, `short`, `int`, `long`, `long long` with `signed`/`unsigned`, `_Bool`/`bool`, `float`, `double`, the `<stdint.h>` exact-width types, `size_t`, `ssize_t`, `ptrdiff_t`, `intptr_t`, `uintptr_t`, `wchar_t`, `char16_t`, `char32_t`, `__int128` and its spellings, `enum X`), the short names `u8`..`u128`, `i8`..`i128`, `f32`, `f64`, `bool`, `ptr`, `usize`, `isize`, and pointers to any type. `__int128` exists only on the 64-bit profiles; asking for it on a 32-bit one is an error.
|
||||
- **FR-3.10**: Show a "packed" comparison - display what the struct would look like with no padding vs. natural alignment.
|
||||
- **FR-3.11**: Support explicit alignment and packing, as C compilers spell them:
|
||||
- `alignas(N)`, `_Alignas(N)` and `__attribute__((aligned(N)))` on a member raise its alignment, never lower it. `alignas(0)` has no effect, as in C11.
|
||||
- `__attribute__((aligned(N)))` on the struct raises the struct's alignment.
|
||||
- `__attribute__((packed))` on the struct or a member aligns members to 1, except where a member asks for more itself.
|
||||
- `#pragma pack(N)`, `pack(push[, N])`, `pack(pop)` and `pack()`, before the struct, cap member alignment. N is 1, 2, 4, 8 or 16. As in C, a directive is a line of its own.
|
||||
- Where `#pragma pack` meets an explicit member alignment, the profile's compiler family decides: GNU targets cap the explicit alignment too, Microsoft targets let it win. Under `pack(2)`, `char c; alignas(8) int i;` puts `i` at 2 on Linux and at 8 on Windows.
|
||||
- **FR-3.12**: Out of scope, each reported with its own error rather than guessed at: nested structs and unions by value (a pointer to one is fine), unions, bitfields, typedefs and macros defined outside the input, any preprocessor directive but `#pragma pack`, layout-changing attributes (`mode`, `vector_size`, a bare `aligned`), and the types not modelled (`long double`, `_Complex`, `_Atomic`). These are scope decisions rather than a backlog: each would need its own rules and its own clang cases, and a struct full of them is better laid out by the compiler that defines them.
|
||||
- **FR-3.13**: Only C for now. A Rust `#[repr(C)]` or Zig `extern struct` front end would produce the same engine definition, so the layout rules would not change, but the meaning differs enough to be its own piece of work: those languages' default layouts are undefined (the compiler reorders), Zig's `packed struct` is bit-level, and Rust's `u128` alignment changed in 1.77.
|
||||
|
||||
### FR-4: Unit Conversions
|
||||
|
||||
|
|
@ -122,7 +142,8 @@ sign of abandonment.
|
|||
- **FR-6.2**: Flag `--programmer` or `-p` for programmer mode: `tally -p "0xFF & 0x0F"`.
|
||||
- **FR-6.3**: Flag `--bits <8|16|32|64|128>` to set bit width in programmer mode (default: 64). Accepted as `--bits 8` or `--bits=8`. It implies `-p`, because standard mode is fixed at 64-bit signed and the flag would mean nothing there.
|
||||
- **FR-6.4**: REMOVED. Flag `--base <dec|hex|oct|bin>` to control primary output format. Programmer mode prints all four rows and standard mode adds them when the expression used a non-decimal literal (FR-1.9), so there is nothing for a selector to select; "primary" has no meaning in a view that shows every base at once. Anyone wanting one base can pipe through a line filter.
|
||||
- **FR-6.5**: NOT IMPLEMENTED (FR-3 is unbuilt, and sequenced after the Android app). Subcommand `tally struct <definition_or_file>` for struct layout - accepts inline DSL string or path to a file containing the definition.
|
||||
- **FR-6.5**: Subcommand `tally struct <definition | file | -> [--abi <name>]` for struct layout (FR-3). Text containing `{` is the definition itself (several arguments are joined with spaces, as an expression's are, so a definition the shell split still arrives whole); a single argument without one names a file; `-` reads stdin. Input is limited to 64 KiB. The output is a table of members and padding rows (offset, size, alignment, type, name), a summary line with the size, alignment, padding and packed size, and a memory map of sixteen bytes a row with one letter per member and `.` for padding, where a row repeating the one above collapses to `*` as in hexdump. A struct of more than 52 members gets no map, and says so. An error names the problem and, where it is about a place in the input, the line, column and text.
|
||||
- **FR-6.5.1**: Color, as a second channel and never the only one: each member's key letter and name in the table, and its bytes in the map, share a hue, and padding rows and padding bytes are faint. The letters stay, so the output means the same thing in a pipe, a paste, or to someone who cannot tell the hues apart; with the escape codes removed it is byte for byte the plain output. The hues are the terminal's own six (cyan, yellow, magenta, green, blue, red), cycled so neighbouring members differ, so the user's theme decides what they look like; padding is the faint attribute rather than "bright black", which some themes draw in nearly the background color. No background is set. `--color auto` (the default) colors only a terminal, and not when `NO_COLOR` is set to anything non-empty or `TERM` is `dumb`; `--color always` and `--color never` override. This is the first color the CLI has; the other subcommands are unchanged.
|
||||
- **FR-6.6**: REMOVED. Subcommand `tally cagr <start> <end> <periods>`. Superseded by FR-5.7: `tally 'cagr(1000, 2000, 5)'` is the same calculation and composes with arithmetic, which a subcommand cannot. A second spelling would need its own argument checking and its own help, both of which would drift from the function's.
|
||||
- **FR-6.7**: REMOVED. Subcommand `tally tvm` with named flags. Superseded by FR-5.7's `tvm_pmt`, `tvm_fv`, `tvm_pv`, `tvm_n` and `tvm_rate`: naming the variable in the function is clearer than omitting one of five flags, and it needs no rule about which omission means "solve".
|
||||
- **FR-6.8**: Subcommand `tally convert <value> <from_unit> <to_unit>` for unit conversion.
|
||||
|
|
@ -142,7 +163,7 @@ sign of abandonment.
|
|||
- **FR-7.3.1**: When the terminal is too short for the bit grid plus all six base rows, the view says so and how many rows it needs, rather than drawing rows over the separator and the input line.
|
||||
- **FR-7.3.2**: When the terminal is too narrow for the whole binary row, it continues onto further rows rather than being cut off. 64 bits grouped in fours is 79 characters and starts at column 11, so the row needs a 90-column terminal; on the default 80 it was silently clipped, and what went missing was the right-hand end, which is the low bits - the value read as wrong rather than as incomplete. The split halves at a time (64 bits becomes two rows of 32), matching the grouping the bit grid above already uses, and a wider terminal gets the single row back. The label sits on the first row; the rest are the same field, with the same click targets and the same cursor.
|
||||
- **FR-7.3.3**: The IEEE 754 float overlay reports a terminal too short for it, the way FR-7.3.1's integer view does, rather than drawing a panel the furniture then covers. It used to draw its eight rows regardless: the separator, prompt and status were drawn afterwards and overwrote them, except for the two columns the prompt occupies, which left the tail of the "Exp:" row sitting on the input line as `> xp: 0 biased ...` at 100x16. Content that does not fit is absent and accounted for, never relocated.
|
||||
- **FR-7.4**: Programmer mode struct sub-view: field list editor, live-updating memory map visualization.
|
||||
- **FR-7.4**: NOT BUILT. Programmer mode struct sub-view: field list editor, live-updating memory map visualization. Deliberately behind the CLI (FR-6.5): the editing model (paste a definition, add a member, reorder to watch the padding move) is a guess until the CLI has been used for real.
|
||||
- **FR-7.5**: Financial mode: form-style input for parameters, result display with formula breakdown, and a scrollable amortization schedule. Four calculations behind one selector: CAGR, compound interest, TVM, and amortization. TVM and compound interest solve for whichever field is left blank; an unanswerable form reports what it is waiting for rather than doing nothing. Results are live, so financial mode records nothing in history.
|
||||
- **FR-7.5.1**: Form fields group digits as they are typed: entering `200` then `0` displays `2,000`. The stored text stays ungrouped so it still parses, and a typed comma is accepted and discarded rather than corrupting the value.
|
||||
- **FR-7.5.2**: Form fields accept expressions, not only numbers. `12 * 30` in a period count is valid input, shown verbatim while it is an expression and evaluated in place by Enter. A field holding an unevaluated expression is reported as such, so it is never mistaken for a blank field the form is waiting to solve for. A field that does not evaluate keeps its text rather than being silently cleared.
|
||||
|
|
@ -194,7 +215,8 @@ instrumented test are the first things to try.
|
|||
What is built of the requirements below: FR-8.1 (Kotlin and Compose), FR-8.2 (JNI, via a
|
||||
hand-written layer with no NDK - design 6.4), and the standard-mode half of FR-8.4.
|
||||
FR-8.3's navigation, the programmer and financial and convert screens, and everything from
|
||||
FR-8.9 down are not started. FR-8.6 needs FR-3, which is unbuilt.
|
||||
FR-8.9 down are not started. FR-8.6 needs FR-3's engine, which now exists, and a C ABI
|
||||
export for it, which does not.
|
||||
|
||||
- **FR-8.1**: Native Android app using Kotlin and Jetpack Compose.
|
||||
- **FR-8.2**: Call into the Zig engine via JNI (C ABI shared library).
|
||||
|
|
@ -217,7 +239,7 @@ FR-8.9 down are not started. FR-8.6 needs FR-3, which is unbuilt.
|
|||
|
||||
- Expression parsing and evaluation must complete in < 1ms for typical expressions.
|
||||
- TUI must render at >= 30fps during interaction (bit toggling, navigation).
|
||||
- Struct layout computation must handle structs with up to 256 fields without perceptible delay.
|
||||
- Struct layout computation must handle structs with up to 256 fields without perceptible delay. (It takes up to 1024, `struct_layout.max_members`, and the memory map's cost follows the number of members rather than the size: a 2 GiB array renders as fast as a byte.)
|
||||
|
||||
### NFR-2: Portability
|
||||
|
||||
|
|
@ -229,7 +251,7 @@ FR-8.9 down are not started. FR-8.6 needs FR-3, which is unbuilt.
|
|||
|
||||
- Floating-point arithmetic uses IEEE 754 double precision (64-bit).
|
||||
- Programmer mode integer operations must be exact (no floating-point).
|
||||
- Struct layout computation must match the actual layout produced by a C compiler under the specified ABI.
|
||||
- Struct layout computation must match the actual layout produced by a C compiler under the specified ABI. Enforced, not aspired to: `zig build test` compiles the engine's claims about every profile as `_Static_assert`s for every clang target the profile names (design 3.5), so a disagreement fails the build.
|
||||
|
||||
### NFR-4: Extensibility
|
||||
|
||||
|
|
@ -241,7 +263,7 @@ FR-8.9 down are not started. FR-8.6 needs FR-3, which is unbuilt.
|
|||
### NFR-5: Testability
|
||||
|
||||
- Engine must be testable in isolation (no I/O dependencies).
|
||||
- Struct layout results must be verifiable against `pahole` or compiler output for reference structs.
|
||||
- Struct layout results must be verifiable against compiler output for reference structs. The compiler is clang, the one Zig ships, rather than `pahole` as first written: `pahole` reads a binary's debug information, so it needs a compiler and a binary per target first, and installing it is a system dependency NFR-6 rules out.
|
||||
- Engine test coverage target: >= 80% line coverage. The engine is pure computation with no I/O, so this is achievable and expected.
|
||||
- Frontend coverage is measured, not waived. The CLI and TUI each get their own instrumented coverage report alongside the engine's, with include paths kept disjoint so no file is accounted for twice. The TUI is testable further than "smoke tests" suggests: views are drawn into a surface and the cells read back, and key and mouse handling goes through the real handlers, so layout and binding regressions are caught rather than eyeballed. Target is the same >= 80% as the engine.
|
||||
- What genuinely cannot be covered is the terminal itself: the event loop, `vaxis` setup, and the process entry point. Those are the only frontend paths exempt.
|
||||
|
|
@ -277,7 +299,7 @@ FR-8.9 down are not started. FR-8.6 needs FR-3, which is unbuilt.
|
|||
|
||||
- C API must not use null-terminated strings. All string inputs take `(pointer, length)` pairs to prevent buffer overread.
|
||||
- Engine must validate all input lengths before processing.
|
||||
- No unbounded allocations from untrusted input (enforce maximum expression length, maximum struct field count).
|
||||
- No unbounded allocations from untrusted input (enforce maximum expression length, maximum struct field count). A struct definition is limited to 64 KiB of text and 1024 members.
|
||||
|
||||
### NFR-9: Numeric Correctness (Exact Arithmetic)
|
||||
|
||||
|
|
|
|||
|
|
@ -288,39 +288,64 @@ binary approximation before `convertUnits` did its own multiply and divide.
|
|||
glued value+unit.
|
||||
- 596 tests pass (was 564). Coverage 99.40%.
|
||||
|
||||
### Task 2.1: Implement struct DSL tokenizer and parser
|
||||
- Create `engine/src/struct_layout.zig`
|
||||
- Tokenize: `struct`, `{`, `}`, `;`, type names, identifiers, `[`, `]`, numbers (for arrays)
|
||||
- Parse into a list of `FieldDef` structs: `{ name, type, array_count? }`
|
||||
- Support types: u8, u16, u32, u64, i8, i16, i32, i64, f32, f64, bool, ptr
|
||||
- Verify: unit tests parsing valid structs, error messages for malformed input
|
||||
### Task 2.1: Struct layout engine, C parser, clang check, CLI [DONE, on trial]
|
||||
|
||||
### Task 2.2: Implement ABI profile interface and x86-64 System V profile
|
||||
- Define `AbiProfile` struct with function pointers/vtable (field_layout, struct_alignment, tail_padding, pointer_size)
|
||||
- Implement `x86_64_sysv` profile:
|
||||
- u8/i8/bool -> size 1, align 1
|
||||
- u16/i16 -> size 2, align 2
|
||||
- u32/i32/f32 -> size 4, align 4
|
||||
- u64/i64/f64/ptr -> size 8, align 8
|
||||
- Struct alignment = max of field alignments
|
||||
- Tail padding to struct alignment boundary
|
||||
- Implement `packed` profile (all alignment = 1, no padding)
|
||||
- Verify: unit tests matching `pahole` output for known structs
|
||||
Tasks 2.1 to 2.4 as planned were a DSL tokenizer and parser, an `AbiProfile` of function
|
||||
pointers with a packed profile, `compute_layout` with a stored byte map, and an
|
||||
endianness display. What was built is one task, and differs from that plan in four ways
|
||||
design 3.6 records: the input is C, a profile is a table, packing is a property of the
|
||||
struct, and endianness is deferred (FR-3.8). Stage 1 of two: the TUI view (Task 5.5)
|
||||
waits until the CLI has been used. The feature is on trial and may come out whole.
|
||||
|
||||
### Task 2.3: Implement layout computation
|
||||
- Implement `compute_layout(fields, abi) -> StructLayoutResult`
|
||||
- Generate per-field: offset, size, alignment, padding_before
|
||||
- Generate byte-level map (`[]ByteInfo`) for visualization
|
||||
- Compute totals: size, alignment, total padding bytes, padding percentage
|
||||
- Compute "packed comparison" size
|
||||
- Verify: unit tests with structs of varying complexity, compare against known C compiler layouts
|
||||
- `engine/src/struct_layout.zig`, the model. Four profiles (`sysv64` default, `win64`,
|
||||
`i386`, `arm32`) as `EnumArray(Primitive, ?Shape)` tables plus `Rules`, a language-free
|
||||
`Definition`, and `compute` returning a `Layout` with per-member placements, size,
|
||||
alignment, tail padding, the packed size (FR-3.10), and `segments()` for drawing.
|
||||
Checked arithmetic, `PTRDIFF_MAX` as the ceiling, 1024 members at most, and a
|
||||
`Diagnostic` naming the member an error is about. 17 tests, including an
|
||||
allocation-failure sweep. 100% line coverage.
|
||||
- `engine/src/CStructParser.zig`, reading C (FR-3.1, FR-3.9, FR-3.11, FR-3.12). Its own
|
||||
lexer, because the expression tokenizer reads `010` as ten and `1,000` as one number.
|
||||
Type words counted and resolved C's way; declarator lists, pointers, function
|
||||
pointers, arrays of any rank, a flexible last member; `alignas`, `aligned`, `packed`
|
||||
and `#pragma pack` with push and pop; comments. Everything outside the subset is a
|
||||
specific error with a span, and `locate` gives its line and column. 17 tests, the
|
||||
error test asserting the exact text each error points at. 99.2%, the rest being
|
||||
failure reports inside tests.
|
||||
- `engine/test/struct_oracle.zig` and a block in `build.zig`: the clang check (NFR-3,
|
||||
design 3.5). Per profile, a C file of `_Static_assert`s on every scalar and 29 test
|
||||
structs, compiled for each of eight clang targets inside `zig build test`.
|
||||
- `tally struct <definition | file | -> [--abi <name>]` (FR-6.5): `parseStructArgs` in
|
||||
`main.zig`, `readStructSource` for the file and stdin reads, and `format.structLayout`
|
||||
for the table, summary and memory map. 13 tests across the two files.
|
||||
- Color (FR-6.5.1), added after the first look at the output, as a second channel:
|
||||
each member's key, name and bytes share one of the terminal's six hues, padding is
|
||||
faint, and `--color auto|always|never` with `NO_COLOR` and `TERM=dumb` honoured.
|
||||
`format.Paint` carries the decision and `main.ColorChoice.resolve` makes it, as a pure
|
||||
function. The test that matters strips the escapes from colored output and requires
|
||||
the plain output byte for byte, for six structs including a collapsed map and one
|
||||
with no map. 5 more tests. The CLI's first color.
|
||||
- 22 errors reach `engine.Error`: 8 from layout, 11 from parsing, and 3 shared with the
|
||||
expression parser. The test asserting the old struct-layout errors were gone is
|
||||
deleted, and `UnexpectedEnd` is now "unexpected end of input", since it is no longer
|
||||
only about expressions.
|
||||
|
||||
### Task 2.4: Implement endianness display
|
||||
- Given a `StructLayoutResult` and a set of field values (optional), show byte ordering
|
||||
- Little-endian (default): LSB at lowest address
|
||||
- Big-endian: MSB at lowest address
|
||||
- This affects the byte-map visualization labels, not the layout computation itself
|
||||
- Verify: unit test showing a u32 value's bytes in both endiannesses
|
||||
**The clang check paid for itself on its first run.** The layout rules were written
|
||||
from memory as GCC's, and both `windows-msvc` targets refused two test structs: under
|
||||
`#pragma pack(2)`, `char c; alignas(8) int i;` puts `i` at 2 under GCC's rules and at 8
|
||||
under MSVC's, where an explicit alignment is applied after the pack cap. That became
|
||||
`Abi.Rules`, with a unit test of its own, and the requirement (FR-3.11) was written from
|
||||
what the compiler said rather than from memory. To prove the check can fail for a table
|
||||
error too, i386's `double` was set to align 8: twenty assertions failed, each naming the
|
||||
profile, the struct and the claim. Removing a profile's targets from `build.zig` fails
|
||||
the generator with "profile 'arm32' has no compiler target".
|
||||
|
||||
- Verify: 1046 tests pass in Debug, ReleaseSafe, ReleaseFast and ReleaseSmall, plus the
|
||||
eight clang compiles; fmt and zlint clean on the whole tree; ASCII clean. The Android
|
||||
libraries still audit clean (no undefined symbols, TLS or `DT_NEEDED`) with the new
|
||||
modules compiled in. Checked by hand: each profile on `char c; long l; double d;
|
||||
wchar_t w; void *p;`, a 4 KiB array collapsing in the map, a 2 GiB one rendering in
|
||||
5 ms, pack under GNU and Microsoft rules, a file, stdin, and every error shape.
|
||||
|
||||
### Task 2.5: Implement financial module [DONE]
|
||||
- Created `engine/src/financial.zig`
|
||||
|
|
@ -432,7 +457,8 @@ binary approximation before `convertUnits` did its own multiply and divide.
|
|||
including the glued form `tally convert 100km to mi` (numeric prefix split from
|
||||
the unit suffix, with `e`/`E` only treated as an exponent when followed by a
|
||||
digit or sign so `5eV` parses as 5 eV)
|
||||
- Subcommands (`struct`, `cagr`, `tvm`) NOT YET IMPLEMENTED
|
||||
- `struct` subcommand: Task 2.1. `cagr` and `tvm`: superseded by expression functions,
|
||||
Task 4.5
|
||||
- Verify: unit tests for arg parsing; `--help` prints usage
|
||||
|
||||
NOTE: Multi-arg joining implemented (`tally 2 + 2` works same as `tally "2+2"`).
|
||||
|
|
@ -443,7 +469,7 @@ Remaining subcommands deferred until their engine modules exist.
|
|||
- Programmer mode: print multi-base table (dec signed, dec unsigned, hex, bin)
|
||||
- Convert: `formatConversion` prints `100 km = 62.13711922373339 mi`, resolving
|
||||
aliases to canonical names
|
||||
- Struct mode: NOT YET IMPLEMENTED
|
||||
- Struct layout: `format.structLayout`, Task 2.1
|
||||
- Financial mode: NOT YET IMPLEMENTED
|
||||
- `--json` flag: NOT YET IMPLEMENTED
|
||||
- Verify: unit tests for evaluate and formatConversion output
|
||||
|
|
@ -600,6 +626,14 @@ Remaining subcommands deferred until their engine modules exist.
|
|||
- Verify: can navigate grid, toggle bits, see values change in real-time
|
||||
|
||||
### Task 5.5: Implement struct visualizer TUI view
|
||||
|
||||
Stage 2 of FR-3, held until `tally struct` has been used (FR-7.4). The engine it needs
|
||||
exists (Task 2.1): `CStructParser.parse` and `struct_layout.compute` are everything the
|
||||
view computes with. Decided while planning it, before it was deferred: a programmer
|
||||
sub-view on Ctrl-S rather than a fifth mode, with `float_view_active` becoming a
|
||||
three-way `prog_view` enum so the two sub-views cannot both be on; and handling
|
||||
bracketed paste first, since a pasted multi-line struct would otherwise submit a line at
|
||||
a time (open item 20). The plan below is the original one and predates both.
|
||||
- Create `tui/src/views/struct_viz.zig`
|
||||
- Text input for struct definition (multi-line mini editor or single-line with parsing)
|
||||
- Live-updating field table (offset, size, align, padding)
|
||||
|
|
@ -607,7 +641,8 @@ Remaining subcommands deferred until their engine modules exist.
|
|||
- ABI toggle (`a` key), endian toggle (`e` key)
|
||||
- "Packed comparison" shown at bottom
|
||||
- Transition: from programmer mode, press a key (e.g., `S`) to enter struct sub-view, Esc to return
|
||||
- Verify: can enter struct definition, see layout update, toggle ABI/endian
|
||||
(plain letters are hex digits in the value zone, so this is Ctrl-S)
|
||||
- Verify: can enter struct definition, see layout update, toggle ABI
|
||||
|
||||
### Task 5.6: Implement financial mode TUI view [DONE]
|
||||
- Created `src/tui/financial.zig`; financial is now a top-level mode and Tab
|
||||
|
|
@ -1578,7 +1613,8 @@ signedness and byte order the TUI exposes were unreachable from the command line
|
|||
`--unsigned` gives 127, `--endian little '0xDEAD'` reverses the hex row.
|
||||
|
||||
NOT DONE, still open from FR-6: `--base` (FR-6.4), `--json` (FR-6.9), and the
|
||||
`struct`, `cagr` and `tvm` subcommands (FR-6.5 to FR-6.7).
|
||||
`struct`, `cagr` and `tvm` subcommands (FR-6.5 to FR-6.7). (`struct` landed in Task 2.1;
|
||||
`cagr` and `tvm` were superseded by expression functions.)
|
||||
|
||||
### Task 5.10: Act on the full-codebase review [PARTIAL]
|
||||
|
||||
|
|
@ -1896,6 +1932,35 @@ review reached that file.
|
|||
than a `readelf` invocation in a shell. That step is also the natural home for the
|
||||
16 KiB `LOAD` alignment check, which is currently only a Play submission rule nobody
|
||||
verifies locally.
|
||||
20. **Bracketed paste submits a line at a time.** vxfw turns bracketed paste on, and the
|
||||
TUI drops `paste_start` and `paste_end` (`typeErasedEventHandler`'s `else`), so a
|
||||
pasted newline arrives as Enter. Pasting a two-line expression in standard mode
|
||||
evaluates the first line and leaves the second in the prompt. Small and
|
||||
mode-independent: while a paste is open, Enter inserts a space. The struct view
|
||||
(Task 5.5) needs it first, since a pasted struct is several lines.
|
||||
21. **Struct layout, after stage 1** (Task 2.1). In rough order of value:
|
||||
|
||||
- **Use it, then decide.** The feature is on trial; if `tally struct` is not
|
||||
reached for, it comes out whole (two engine files, the oracle, a block in
|
||||
`build.zig`, the subcommand, and FR-3), which is easier before anything else
|
||||
depends on it.
|
||||
- The TUI view (Task 5.5, FR-7.4), then a C ABI export (`tally_struct_layout`, JSON
|
||||
shaped like the CLI's table) so Android can have one (Task 6.5).
|
||||
- Decoding bytes through a struct, which is where endianness (FR-3.8) earns its
|
||||
place: a hex string, or the programmer register, read as each member's value.
|
||||
- Rust `#[repr(C)]` and Zig `extern struct` front ends (FR-3.13). Zig's own
|
||||
`@offsetOf` would be the oracle for the second.
|
||||
- A "reorder by alignment" line: the size the same members would have sorted by
|
||||
descending alignment, which is the question padding usually prompts.
|
||||
- Types left out by decision rather than difficulty, each needing clang cases of its
|
||||
own: `long double` (which would split `sysv64` into x86-64 and AArch64 macOS, where
|
||||
it is 16 and 8 bytes), `_Atomic` (which raises alignment on i386), and a
|
||||
bare `aligned` (the target's largest alignment, which no profile records yet).
|
||||
- An i386-Windows profile: MSVC aligns 8-byte types to 8 there, unlike i386 Linux,
|
||||
and it would exercise the Microsoft rules on a 32-bit target.
|
||||
- Out of scope unless a real need appears (FR-3.12): nested structs and unions,
|
||||
bitfields, typedefs and macros from elsewhere. These are where a C front end turns
|
||||
into a C compiler, and `zig cc` is already one.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -2157,7 +2222,11 @@ Superseded plan:
|
|||
- FAB/toolbar button navigating to struct layout sub-screen
|
||||
- Verify: bit toggling works, base displays update, expressions evaluate
|
||||
|
||||
### Task 6.5: Implement struct visualizer screen (BLOCKED: FR-3 is unbuilt)
|
||||
### Task 6.5: Implement struct visualizer screen (BLOCKED: no C ABI export yet)
|
||||
|
||||
The engine exists (Task 2.1). What blocks this is a `tally_struct_layout` export with a
|
||||
JSON shape, and the TUI view settling what an interactive version should do.
|
||||
|
||||
- Navigation: accessed from programmer mode via toolbar button
|
||||
- Form-based struct builder: type dropdown + name field per row, add/remove/reorder fields
|
||||
- Toggle for raw DSL text input (for power users / copy-paste from code)
|
||||
|
|
@ -2329,7 +2398,8 @@ Original plan:
|
|||
and the one-time signing setup.
|
||||
- `CONTRIBUTING.md`: dev setup, architecture overview, how to add an ABI profile
|
||||
- CLI `--help` text finalized
|
||||
- Struct DSL syntax reference (in README or separate doc)
|
||||
- Struct definition syntax reference: `tally struct --help` and FR-3.1/FR-3.12 cover it;
|
||||
a README section if the feature stays
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
27
README.md
27
README.md
|
|
@ -37,15 +37,23 @@ $ tally -p '0xDEADBEEF and 0xFFFF'
|
|||
- **Financial functions.** CAGR, compound interest, time value of money and amortization,
|
||||
as ordinary functions: `cagr(10000, 25000, 5) * 100`. `tally --help` lists them all.
|
||||
- **Variables and `Ans`.** `x = 21`, then `x * 2`, then `Ans + 1`.
|
||||
- **Struct layout.** Paste a C struct and see where every member lands, the padding
|
||||
between them and a map of the bytes, under 64-bit Linux and macOS, 64-bit Windows,
|
||||
32-bit x86 or 32-bit ARM rules, `alignas`, `packed` and `#pragma pack` included. On a
|
||||
terminal each member's row and bytes share a color. Every profile is checked against
|
||||
clang at build time. Command line only, for now.
|
||||
|
||||
The requirements and design, including what is planned and not built (a struct layout
|
||||
visualizer, a financial screen on Android), are in [.kiro/specs/calculator](.kiro/specs/calculator).
|
||||
view in the TUI and on Android, a financial screen on Android), are in
|
||||
[.kiro/specs/calculator](.kiro/specs/calculator).
|
||||
|
||||
## The three frontends
|
||||
|
||||
**Command line.** `tally EXPRESSION` evaluates and prints. `-p` is programmer mode;
|
||||
`--bits N`, `--unsigned` and `--endian little` set it up and imply `-p`. Subcommands:
|
||||
`tally convert 100 km to mi` and `tally amort 200000 6 360 --monthly` (a schedule). Run
|
||||
`tally convert 100 km to mi`, `tally amort 200000 6 360 --monthly` (a schedule), and
|
||||
`tally struct 'struct hdr { uint32_t magic; uint8_t ver; uint64_t ts; }'` (a layout; a
|
||||
file name or `-` for stdin works too, and `--abi win64` picks the rules). Run
|
||||
`tally --help`, or `tally SUBCOMMAND --help`.
|
||||
|
||||
**Terminal UI.** `tally` with no arguments. Standard, Programmer, Financial and Convert
|
||||
|
|
@ -220,11 +228,14 @@ mise run android-install # all of the above, then install a
|
|||
|
||||
```
|
||||
engine/src/ the engine: tokenizer, Parser, evaluator, Rational, Integer, units,
|
||||
financial, programmer, bitwise, float_interp
|
||||
financial, programmer, bitwise, float_interp, struct_layout and the
|
||||
CStructParser that reads C into it
|
||||
engine/src/c_api.zig the C ABI every non-Zig frontend uses: sessions, JSON results
|
||||
engine/src/jni.zig JNI entry points for Android, written against the JNI function
|
||||
table by index - no NDK, no jni.h
|
||||
include/tally.h the C header, checked against the library by engine/test/c_abi_test.c
|
||||
engine/test/struct_oracle.zig writes the engine's struct layouts as C static asserts,
|
||||
which zig build test compiles for eight clang targets
|
||||
src/main.zig, src/cli/ the command line
|
||||
src/tui.zig, src/tui/ the terminal UI (libvaxis)
|
||||
android/ the Compose app; Kotlin parses no expressions and formats no numbers
|
||||
|
|
@ -250,10 +261,12 @@ A few decisions shape everything else, and the design document explains each:
|
|||
- `zig build test`: about a thousand Zig tests across the engine, CLI, TUI (rendering
|
||||
real frames and reading the cells back), the C ABI, and the JNI layer against a
|
||||
synthetic function table; plus a C program that links the shared library through
|
||||
`tally.h`, which catches a header that disagrees with the library. CI also runs it with
|
||||
`-Doptimize=ReleaseSafe`, `ReleaseSmall` and `ReleaseFast`: what ships is not Debug,
|
||||
and Debug's 0xaa fill of undefined memory can hide a read of something never written
|
||||
(it hid one in the JNI tests).
|
||||
`tally.h`, which catches a header that disagrees with the library; plus the engine's
|
||||
struct layouts compiled as `_Static_assert`s for every clang target each ABI profile
|
||||
claims, which is how a layout is known to match a compiler rather than believed to.
|
||||
CI also runs it with `-Doptimize=ReleaseSafe`, `ReleaseSmall` and `ReleaseFast`: what
|
||||
ships is not Debug, and Debug's 0xaa fill of undefined memory can hide a read of
|
||||
something never written (it hid one in the JNI tests).
|
||||
- `zig build jvm-test`: the JNI entry points from Java, which is the only way to check
|
||||
the function-table indices without a device. CI runs it in Debug and in ReleaseSmall,
|
||||
the optimization the Android library ships with.
|
||||
|
|
|
|||
51
build.zig
51
build.zig
|
|
@ -147,6 +147,57 @@ pub fn build(b: *std.Build) void {
|
|||
test_step.dependOn(&run_jni_tests.step);
|
||||
test_step.dependOn(&run_c_abi_test.step);
|
||||
|
||||
// -- Struct layouts against a C compiler (NFR-3) --
|
||||
//
|
||||
// `struct_oracle.zig` writes, per profile, a C file of `_Static_assert`s on what
|
||||
// the engine computed, and each file is compiled for every clang target that
|
||||
// profile claims to reproduce. Compiling is the test: a layout the engine gets
|
||||
// wrong is a compile error naming the struct and the claim. The generator refuses
|
||||
// to run unless every profile in the engine appears below, so a profile cannot be
|
||||
// added without a compiler to answer to.
|
||||
{
|
||||
const oracle_targets = [_]struct { abi: []const u8, triples: []const []const u8 }{
|
||||
.{ .abi = "sysv64", .triples = &.{ "x86_64-linux-gnu", "aarch64-linux-gnu", "aarch64-macos" } },
|
||||
.{ .abi = "win64", .triples = &.{ "x86_64-windows-msvc", "aarch64-windows-msvc" } },
|
||||
.{ .abi = "i386", .triples = &.{"x86-linux-gnu"} },
|
||||
.{ .abi = "arm32", .triples = &.{"arm-linux-gnueabihf"} },
|
||||
};
|
||||
// The generator runs on the build machine whatever -Dtarget says.
|
||||
const host_engine = b.createModule(.{
|
||||
.root_source_file = b.path("engine/src/engine.zig"),
|
||||
.target = b.graph.host,
|
||||
});
|
||||
const generator = b.addExecutable(.{
|
||||
.name = "struct-oracle",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("engine/test/struct_oracle.zig"),
|
||||
.target = b.graph.host,
|
||||
.imports = &.{.{ .name = "engine", .module = host_engine }},
|
||||
}),
|
||||
});
|
||||
const generate = b.addRunArtifact(generator);
|
||||
for (oracle_targets) |entry| {
|
||||
generate.addArg(entry.abi);
|
||||
const c_file = generate.addOutputFileArg(b.fmt("{s}.c", .{entry.abi}));
|
||||
for (entry.triples) |triple| {
|
||||
const query = std.Target.Query.parse(.{ .arch_os_abi = triple }) catch
|
||||
@panic("bad struct oracle target triple");
|
||||
const object = b.addObject(.{
|
||||
.name = b.fmt("struct-oracle-{s}", .{triple}),
|
||||
.root_module = b.createModule(.{
|
||||
.target = b.resolveTargetQuery(query),
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
object.root_module.addCSourceFile(.{
|
||||
.file = c_file,
|
||||
.flags = &.{ "-std=c11", "-ffreestanding", "-Wall", "-Wextra", "-Werror" },
|
||||
});
|
||||
test_step.dependOn(&object.step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Coverage step (uses kcov, Linux x86_64/aarch64 only) --
|
||||
//
|
||||
// One report per source tree, with disjoint include patterns so no file is
|
||||
|
|
|
|||
1241
engine/src/CStructParser.zig
Normal file
1241
engine/src/CStructParser.zig
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,8 @@ pub const programmer = @import("programmer.zig");
|
|||
pub const float_interp = @import("float_interp.zig");
|
||||
pub const units = @import("units.zig");
|
||||
pub const financial = @import("financial.zig");
|
||||
pub const struct_layout = @import("struct_layout.zig");
|
||||
pub const CStructParser = @import("CStructParser.zig");
|
||||
|
||||
// Reached only from inside this file. `Rational`, `tokenizer`, `ast`, `bitwise` and
|
||||
// `Parser` were public too and no frontend imported any of them: a caller evaluates
|
||||
|
|
@ -52,7 +54,9 @@ pub const Number = number.Number;
|
|||
pub const Error = evaluator.Error ||
|
||||
programmer.Error ||
|
||||
units.Error ||
|
||||
financial.Error;
|
||||
financial.Error ||
|
||||
struct_layout.Error ||
|
||||
CStructParser.Error;
|
||||
|
||||
/// The human-readable phrase for an error, with no prefix and no newline.
|
||||
///
|
||||
|
|
@ -66,15 +70,15 @@ pub const Error = evaluator.Error ||
|
|||
/// here rather than falling back to something vague. It also cannot word an error the
|
||||
/// engine is incapable of returning: a prong naming one is a type error, since the
|
||||
/// switch is over `Error`. That second property is what the hand-written `CalcError`
|
||||
/// got wrong, and it needs no assertion of its own to hold. It carried `InvalidType`,
|
||||
/// `InvalidFieldName`, `DuplicateFieldName` and `StructTooLarge` for a struct layout
|
||||
/// module that does not exist yet, and gave all four a phrase.
|
||||
/// got wrong, and it needs no assertion of its own to hold. It carried four struct
|
||||
/// layout errors, with phrases, years before there was a struct layout module that
|
||||
/// could return them.
|
||||
pub fn phrase(err: Error) []const u8 {
|
||||
return switch (err) {
|
||||
// Parsing
|
||||
error.UnexpectedToken => "unexpected token",
|
||||
error.UnmatchedParen => "unmatched parenthesis",
|
||||
error.UnexpectedEnd => "unexpected end of expression",
|
||||
error.UnexpectedEnd => "unexpected end of input",
|
||||
error.ExpressionTooLarge => "the expression has too many terms",
|
||||
error.NestingTooDeep => "the expression is nested too deeply",
|
||||
error.UnterminatedString => "unterminated string literal",
|
||||
|
|
@ -109,6 +113,27 @@ pub fn phrase(err: Error) []const u8 {
|
|||
error.InsufficientParameters => "these values do not determine an answer",
|
||||
error.ConvergenceFailure => "no solution found",
|
||||
|
||||
// Struct layout
|
||||
error.EmptyStruct => "the struct has no members",
|
||||
error.TooManyMembers => "the struct has too many members",
|
||||
error.DuplicateMember => "two members have the same name",
|
||||
error.TypeUnavailable => "this type does not exist on the selected ABI",
|
||||
error.InvalidArrayLength => "an array length must be at least 1",
|
||||
error.MisplacedFlexibleArray => "only the last member can be a flexible array",
|
||||
error.InvalidAlignment => "an alignment must be a power of two (and a pack 1 to 16)",
|
||||
error.StructTooLarge => "the struct is too large for the selected ABI",
|
||||
|
||||
// Reading a C struct
|
||||
error.UnterminatedComment => "unterminated comment",
|
||||
error.UnknownType => "unknown type (typedefs and macros from elsewhere are not visible)",
|
||||
error.UnsupportedType => "this type is not supported yet",
|
||||
error.IncompleteType => "a member cannot be void",
|
||||
error.UnsupportedAttribute => "this attribute is not supported",
|
||||
error.BitfieldNotSupported => "bitfields are not supported yet",
|
||||
error.NestedTypeNotSupported => "nested structs, unions and enums are not supported yet",
|
||||
error.PreprocessorNotSupported => "only #pragma pack is understood, not other directives",
|
||||
error.DefinitionTooLong => "the definition is too long",
|
||||
|
||||
// System
|
||||
error.OutOfMemory => "out of memory",
|
||||
};
|
||||
|
|
@ -161,16 +186,13 @@ test "the error set is derived from the modules, not enumerated here" {
|
|||
const promoted: Error = @field(Error, field.name);
|
||||
try testing.expect(phrase(promoted).len > 0);
|
||||
}
|
||||
}
|
||||
|
||||
test "the struct-layout errors are gone, not merely unused" {
|
||||
// They were members of the old set with phrases nothing could produce. Absence
|
||||
// is the assertion: naming one in `phrase` would not compile.
|
||||
inline for (@typeInfo(Error).error_set.?) |field| {
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "StructTooLarge"));
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "InvalidFieldName"));
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "DuplicateFieldName"));
|
||||
try testing.expect(!std.mem.eql(u8, field.name, "InvalidType"));
|
||||
inline for (@typeInfo(struct_layout.Error).error_set.?) |field| {
|
||||
const promoted: Error = @field(Error, field.name);
|
||||
try testing.expect(phrase(promoted).len > 0);
|
||||
}
|
||||
inline for (@typeInfo(CStructParser.Error).error_set.?) |field| {
|
||||
const promoted: Error = @field(Error, field.name);
|
||||
try testing.expect(phrase(promoted).len > 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
842
engine/src/struct_layout.zig
Normal file
842
engine/src/struct_layout.zig
Normal file
|
|
@ -0,0 +1,842 @@
|
|||
//! Struct layout: where each member of a C-compatible struct lands in memory under a
|
||||
//! given ABI, and how much of the struct is padding (FR-3).
|
||||
//!
|
||||
//! Nothing here knows any language's syntax. A front end (today only
|
||||
//! `CStructParser.zig`) turns text into a `Definition`, and `compute` lays that out
|
||||
//! under an `Abi`. A Rust `#[repr(C)]` or a Zig `extern struct` front end would
|
||||
//! produce the same `Definition` and need nothing below to change.
|
||||
//!
|
||||
//! An ABI is data, not code: a table giving each scalar kind its size and alignment,
|
||||
//! or none where the type does not exist. Adding a profile is adding a table entry
|
||||
//! (NFR-4), and every profile names the C compiler targets it claims to reproduce,
|
||||
//! which is what the clang check in `engine/test/struct_oracle.zig` holds it to.
|
||||
//!
|
||||
//! No I/O. `compute` allocates the per-member results and nothing else.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// What laying out a definition can fail with.
|
||||
pub const Error = error{
|
||||
/// The definition has no members. C forbids an empty struct, and GNU C's
|
||||
/// zero-size answer is a different language's.
|
||||
EmptyStruct,
|
||||
/// More members than `max_members`.
|
||||
TooManyMembers,
|
||||
/// Two members share a name.
|
||||
DuplicateMember,
|
||||
/// A scalar the ABI does not have: `__int128` on a 32-bit target.
|
||||
TypeUnavailable,
|
||||
/// An array dimension of zero. A flexible array member is spelled `[]`.
|
||||
InvalidArrayLength,
|
||||
/// A flexible array member anywhere but last.
|
||||
MisplacedFlexibleArray,
|
||||
/// An alignment or pack value that is not a power of two.
|
||||
InvalidAlignment,
|
||||
/// Larger than the ABI can address, or than the arithmetic can hold.
|
||||
StructTooLarge,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
/// Ceiling on members, so untrusted input cannot ask for unbounded work (NFR-8).
|
||||
/// NFR-1 asks that 256 be handled without perceptible delay; this leaves room above
|
||||
/// that for generated code without making the duplicate check expensive.
|
||||
pub const max_members: usize = 1024;
|
||||
|
||||
/// The scalar kinds whose size or alignment a profile has to state.
|
||||
///
|
||||
/// These are C's kinds rather than shapes: `bool` and `char` are both one byte on
|
||||
/// every profile here, but they are different questions to ask of an ABI, and a
|
||||
/// profile where they differ would need no new member. Spellings are the front end's
|
||||
/// business; `uint32_t`, `u32` and `unsigned int` all arrive as `.int`.
|
||||
pub const Primitive = enum {
|
||||
/// One byte: `char`, `int8_t`, `u8`.
|
||||
char,
|
||||
/// `_Bool`.
|
||||
bool,
|
||||
/// Two bytes: `short`, `int16_t`, `char16_t`.
|
||||
short,
|
||||
/// Four bytes: `int`, `int32_t`, `char32_t`.
|
||||
int,
|
||||
/// `long`: eight bytes on LP64, four on LLP64 and ILP32. The reason a 64-bit
|
||||
/// Windows profile exists at all.
|
||||
long,
|
||||
/// Eight-byte integers: `long long`, `int64_t`. Aligned to four on i386.
|
||||
long_long,
|
||||
/// `__int128`. Absent on the 32-bit profiles.
|
||||
int128,
|
||||
float,
|
||||
/// Aligned to four on i386.
|
||||
double,
|
||||
/// Any data or function pointer, and the types defined to be pointer-sized here
|
||||
/// (`size_t`, `ptrdiff_t`, `intptr_t`). Every profile in the table has
|
||||
/// `size_t` the width of a pointer, so they share a row.
|
||||
pointer,
|
||||
/// `wchar_t`: two bytes on Windows, four elsewhere.
|
||||
wchar,
|
||||
/// An `enum`. Int-sized on every profile here; a bare-metal ARM profile with
|
||||
/// short enums is the case that would make this row differ.
|
||||
enumeration,
|
||||
};
|
||||
|
||||
/// Size and alignment of one scalar, in bytes.
|
||||
pub const Shape = struct {
|
||||
size: u8,
|
||||
alignment: u8,
|
||||
};
|
||||
|
||||
/// A target's layout rules.
|
||||
pub const Abi = struct {
|
||||
/// Stable and short: what `--abi` takes and what the clang check names.
|
||||
id: []const u8,
|
||||
/// What a person reads.
|
||||
label: []const u8,
|
||||
/// Every scalar kind, with null where the type does not exist. `EnumArray.init`
|
||||
/// takes a struct with a field per member, so a profile cannot forget one.
|
||||
shapes: std.EnumArray(Primitive, ?Shape),
|
||||
/// Which compiler family's rules the attributes follow. No default: a profile
|
||||
/// has to say.
|
||||
rules: Rules,
|
||||
|
||||
/// How explicit alignment and packing combine, which is where compilers differ.
|
||||
/// Clang reproduces both, choosing by target, and the clang check is what
|
||||
/// established the one difference that matters so far.
|
||||
pub const Rules = enum {
|
||||
/// GCC's, and clang's everywhere but Windows: `#pragma pack` caps every
|
||||
/// member's alignment, including one raised by `aligned` or `alignas`.
|
||||
gnu,
|
||||
/// MSVC's, and clang's on `*-windows-msvc`: `#pragma pack` caps a member's
|
||||
/// natural alignment, and an explicit `aligned` or `alignas` is applied after
|
||||
/// it and wins. Under `pack(2)`, `char c; alignas(8) int i;` puts `i` at 8,
|
||||
/// where the GNU rules put it at 2.
|
||||
microsoft,
|
||||
};
|
||||
|
||||
/// The largest object the ABI can describe: `PTRDIFF_MAX`, half the address
|
||||
/// space. A C compiler refuses anything bigger, so a layout claiming otherwise
|
||||
/// would be one no program could have.
|
||||
fn maxObjectSize(self: *const Abi) u64 {
|
||||
const pointer_bits: u6 = @intCast(@as(u16, self.shapes.get(.pointer).?.size) * 8 - 1);
|
||||
return (@as(u64, 1) << pointer_bits) - 1;
|
||||
}
|
||||
};
|
||||
|
||||
fn s(size: u8, alignment: u8) ?Shape {
|
||||
return .{ .size = size, .alignment = alignment };
|
||||
}
|
||||
|
||||
/// The profiles, default first. Each is checked against every clang target listed
|
||||
/// for it in `build.zig`.
|
||||
pub const abis = [_]Abi{
|
||||
.{
|
||||
.id = "sysv64",
|
||||
.label = "64-bit Linux and macOS (x86-64 SysV, AArch64)",
|
||||
.rules = .gnu,
|
||||
.shapes = .init(.{
|
||||
.char = s(1, 1),
|
||||
.bool = s(1, 1),
|
||||
.short = s(2, 2),
|
||||
.int = s(4, 4),
|
||||
.long = s(8, 8),
|
||||
.long_long = s(8, 8),
|
||||
.int128 = s(16, 16),
|
||||
.float = s(4, 4),
|
||||
.double = s(8, 8),
|
||||
.pointer = s(8, 8),
|
||||
.wchar = s(4, 4),
|
||||
.enumeration = s(4, 4),
|
||||
}),
|
||||
},
|
||||
.{
|
||||
.id = "win64",
|
||||
.label = "64-bit Windows (x64, ARM64)",
|
||||
.rules = .microsoft,
|
||||
.shapes = .init(.{
|
||||
.char = s(1, 1),
|
||||
.bool = s(1, 1),
|
||||
.short = s(2, 2),
|
||||
.int = s(4, 4),
|
||||
.long = s(4, 4),
|
||||
.long_long = s(8, 8),
|
||||
.int128 = s(16, 16),
|
||||
.float = s(4, 4),
|
||||
.double = s(8, 8),
|
||||
.pointer = s(8, 8),
|
||||
.wchar = s(2, 2),
|
||||
.enumeration = s(4, 4),
|
||||
}),
|
||||
},
|
||||
.{
|
||||
.id = "i386",
|
||||
.label = "32-bit x86 Linux (i386 SysV)",
|
||||
.rules = .gnu,
|
||||
.shapes = .init(.{
|
||||
.char = s(1, 1),
|
||||
.bool = s(1, 1),
|
||||
.short = s(2, 2),
|
||||
.int = s(4, 4),
|
||||
.long = s(4, 4),
|
||||
.long_long = s(8, 4),
|
||||
.int128 = null,
|
||||
.float = s(4, 4),
|
||||
.double = s(8, 4),
|
||||
.pointer = s(4, 4),
|
||||
.wchar = s(4, 4),
|
||||
.enumeration = s(4, 4),
|
||||
}),
|
||||
},
|
||||
.{
|
||||
.id = "arm32",
|
||||
.label = "32-bit ARM Linux (AAPCS)",
|
||||
.rules = .gnu,
|
||||
.shapes = .init(.{
|
||||
.char = s(1, 1),
|
||||
.bool = s(1, 1),
|
||||
.short = s(2, 2),
|
||||
.int = s(4, 4),
|
||||
.long = s(4, 4),
|
||||
.long_long = s(8, 8),
|
||||
.int128 = null,
|
||||
.float = s(4, 4),
|
||||
.double = s(8, 8),
|
||||
.pointer = s(4, 4),
|
||||
.wchar = s(4, 4),
|
||||
.enumeration = s(4, 4),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
/// The profile used when none is named (FR-3.6).
|
||||
pub const default_abi: *const Abi = &abis[0];
|
||||
|
||||
/// The profile with this id, or null.
|
||||
pub fn findAbi(id: []const u8) ?*const Abi {
|
||||
for (&abis) |*abi| {
|
||||
if (std.mem.eql(u8, abi.id, id)) return abi;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Where a member was written, as a byte range in the front end's source. Lets a
|
||||
/// layout error point at the member it is about.
|
||||
pub const Span = struct {
|
||||
start: usize,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
/// One member, as the front end understood it.
|
||||
pub const Member = struct {
|
||||
name: []const u8,
|
||||
/// The type as the user wrote it, for display only: `uint32_t`, `struct node *`.
|
||||
/// Array dimensions are not part of it; they are `dims`.
|
||||
type_name: []const u8,
|
||||
primitive: Primitive,
|
||||
/// Array dimensions, outermost first. Empty for a scalar.
|
||||
dims: []const u64 = &.{},
|
||||
/// `T name[]` as the last member: no size of its own, but it still aligns.
|
||||
flexible: bool = false,
|
||||
/// `alignas(N)` or `aligned(N)`: raises the member's alignment, never lowers it.
|
||||
aligned: ?u32 = null,
|
||||
/// `__attribute__((packed))` on the member: alignment 1.
|
||||
@"packed": bool = false,
|
||||
span: ?Span = null,
|
||||
};
|
||||
|
||||
/// A struct to lay out. Borrows everything: a front end that allocates owns the
|
||||
/// memory and says how it is released.
|
||||
pub const Definition = struct {
|
||||
/// The struct's tag or typedef name, if it had one.
|
||||
name: ?[]const u8 = null,
|
||||
/// Whether `name` is a tag (`struct hdr`) or a typedef (`hdr`), which decides how
|
||||
/// the struct is referred to.
|
||||
name_kind: NameKind = .tag,
|
||||
members: []const Member,
|
||||
/// `__attribute__((packed))` on the struct: every member aligned to 1, unless the
|
||||
/// member asks for more itself.
|
||||
@"packed": bool = false,
|
||||
/// `#pragma pack(N)` in effect: no member aligned to more than N.
|
||||
pack: ?u32 = null,
|
||||
/// `aligned(N)` on the struct: the struct's alignment is at least N.
|
||||
aligned: ?u32 = null,
|
||||
|
||||
pub const NameKind = enum { tag, typedef };
|
||||
};
|
||||
|
||||
/// Where one member landed. Parallel to `Definition.members`.
|
||||
pub const Placement = struct {
|
||||
offset: u64,
|
||||
size: u64,
|
||||
/// The alignment the member was placed at, after every attribute and pack.
|
||||
alignment: u32,
|
||||
/// Bytes inserted between the previous member's end and this one's start.
|
||||
padding_before: u64,
|
||||
};
|
||||
|
||||
/// Which member a layout error is about, when it is about one.
|
||||
pub const Diagnostic = struct {
|
||||
member: ?usize = null,
|
||||
};
|
||||
|
||||
/// A laid-out struct.
|
||||
pub const Layout = struct {
|
||||
abi: *const Abi,
|
||||
/// Borrowed from the definition this came from.
|
||||
members: []const Member,
|
||||
/// Owned, one per member.
|
||||
placements: []Placement,
|
||||
size: u64,
|
||||
alignment: u32,
|
||||
/// Padding after the last member, rounding the size up to the alignment.
|
||||
tail_padding: u64,
|
||||
/// The sum of the members' own sizes: the struct with no padding at all
|
||||
/// (FR-3.10).
|
||||
packed_size: u64,
|
||||
|
||||
pub fn deinit(self: *Layout, allocator: Allocator) void {
|
||||
allocator.free(self.placements);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Every padding byte, between members and at the tail.
|
||||
pub fn padding(self: Layout) u64 {
|
||||
return self.size - self.packed_size;
|
||||
}
|
||||
|
||||
/// The struct as consecutive byte ranges, in address order: every member, and
|
||||
/// every run of padding between and after them. One list for both a table and a
|
||||
/// memory map, so they cannot disagree about what is where. Derived from
|
||||
/// `placements` on demand rather than stored as a byte array, because the answer
|
||||
/// for a struct holding `char buf[1 << 20]` is three segments, not a million
|
||||
/// entries.
|
||||
pub fn segments(self: *const Layout) SegmentIterator {
|
||||
return .{ .layout = self };
|
||||
}
|
||||
};
|
||||
|
||||
/// A run of bytes in a layout.
|
||||
pub const Segment = struct {
|
||||
offset: u64,
|
||||
len: u64,
|
||||
kind: Kind,
|
||||
|
||||
pub const Kind = union(enum) {
|
||||
/// Bytes of the member at this index.
|
||||
member: usize,
|
||||
/// Padding between members, before the member at this index.
|
||||
padding: usize,
|
||||
/// Padding after the last member.
|
||||
tail,
|
||||
};
|
||||
};
|
||||
|
||||
/// Yields the segments of a layout. Every member is yielded, a flexible array member
|
||||
/// included with a length of zero, because a table lists it; padding is yielded only
|
||||
/// where there is some.
|
||||
pub const SegmentIterator = struct {
|
||||
layout: *const Layout,
|
||||
index: usize = 0,
|
||||
/// Whether the padding before `placements[index]` has been yielded.
|
||||
padding_done: bool = false,
|
||||
tail_done: bool = false,
|
||||
|
||||
pub fn next(self: *SegmentIterator) ?Segment {
|
||||
const placements = self.layout.placements;
|
||||
while (self.index < placements.len) {
|
||||
const placement = placements[self.index];
|
||||
if (!self.padding_done) {
|
||||
self.padding_done = true;
|
||||
if (placement.padding_before > 0) return .{
|
||||
.offset = placement.offset - placement.padding_before,
|
||||
.len = placement.padding_before,
|
||||
.kind = .{ .padding = self.index },
|
||||
};
|
||||
}
|
||||
const index = self.index;
|
||||
self.index += 1;
|
||||
self.padding_done = false;
|
||||
return .{
|
||||
.offset = placement.offset,
|
||||
.len = placement.size,
|
||||
.kind = .{ .member = index },
|
||||
};
|
||||
}
|
||||
if (!self.tail_done) {
|
||||
self.tail_done = true;
|
||||
const tail = self.layout.tail_padding;
|
||||
if (tail > 0) return .{ .offset = self.layout.size - tail, .len = tail, .kind = .tail };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Lay out `definition` under `abi`.
|
||||
///
|
||||
/// Members are placed in order, each at the next offset that is a multiple of its
|
||||
/// alignment, and the struct's size is rounded up to its own alignment, which is the
|
||||
/// largest of its members'. A member's alignment starts at its scalar's and is
|
||||
/// lowered to 1 by `packed` (on the member or the struct). Then `aligned` raises it
|
||||
/// and `#pragma pack` caps it, in the order `abi.rules` says: under the GNU rules
|
||||
/// the cap comes last, so it caps an explicit alignment too; under the Microsoft
|
||||
/// rules the explicit alignment comes last and wins.
|
||||
///
|
||||
/// On failure, `diag.member` names the member the error is about, if any.
|
||||
pub fn compute(
|
||||
allocator: Allocator,
|
||||
definition: Definition,
|
||||
abi: *const Abi,
|
||||
diag: ?*Diagnostic,
|
||||
) Error!Layout {
|
||||
const members = definition.members;
|
||||
if (members.len == 0) return error.EmptyStruct;
|
||||
if (members.len > max_members) return error.TooManyMembers;
|
||||
|
||||
if (definition.pack) |n| try checkAlignment(n);
|
||||
if (definition.aligned) |n| try checkAlignment(n);
|
||||
|
||||
for (members, 0..) |member, i| {
|
||||
errdefer if (diag) |d| {
|
||||
d.member = i;
|
||||
};
|
||||
if (member.aligned) |n| try checkAlignment(n);
|
||||
for (member.dims) |dim| {
|
||||
if (dim == 0) return error.InvalidArrayLength;
|
||||
}
|
||||
if (member.flexible and i != members.len - 1) return error.MisplacedFlexibleArray;
|
||||
for (members[0..i]) |earlier| {
|
||||
if (std.mem.eql(u8, earlier.name, member.name)) return error.DuplicateMember;
|
||||
}
|
||||
}
|
||||
|
||||
const placements = try allocator.alloc(Placement, members.len);
|
||||
errdefer allocator.free(placements);
|
||||
|
||||
const limit = abi.maxObjectSize();
|
||||
var offset: u64 = 0;
|
||||
var struct_alignment: u32 = 1;
|
||||
var packed_size: u64 = 0;
|
||||
for (members, placements, 0..) |member, *placement, i| {
|
||||
errdefer if (diag) |d| {
|
||||
d.member = i;
|
||||
};
|
||||
const scalar = abi.shapes.get(member.primitive) orelse return error.TypeUnavailable;
|
||||
|
||||
var count: u64 = 1;
|
||||
for (member.dims) |dim| count = try mul(count, dim);
|
||||
const size: u64 = if (member.flexible) 0 else try mul(scalar.size, count);
|
||||
|
||||
var alignment: u32 = scalar.alignment;
|
||||
if (member.@"packed" or definition.@"packed") alignment = 1;
|
||||
switch (abi.rules) {
|
||||
.gnu => {
|
||||
if (member.aligned) |n| alignment = @max(alignment, n);
|
||||
if (definition.pack) |n| alignment = @min(alignment, n);
|
||||
},
|
||||
.microsoft => {
|
||||
if (definition.pack) |n| alignment = @min(alignment, n);
|
||||
if (member.aligned) |n| alignment = @max(alignment, n);
|
||||
},
|
||||
}
|
||||
|
||||
const start = try alignUp(offset, alignment);
|
||||
placement.* = .{
|
||||
.offset = start,
|
||||
.size = size,
|
||||
.alignment = alignment,
|
||||
.padding_before = start - offset,
|
||||
};
|
||||
offset = try add(start, size);
|
||||
if (offset > limit) return error.StructTooLarge;
|
||||
packed_size = try add(packed_size, size);
|
||||
struct_alignment = @max(struct_alignment, alignment);
|
||||
}
|
||||
|
||||
if (definition.aligned) |n| struct_alignment = @max(struct_alignment, n);
|
||||
const size = try alignUp(offset, struct_alignment);
|
||||
if (size > limit) return error.StructTooLarge;
|
||||
|
||||
return .{
|
||||
.abi = abi,
|
||||
.members = members,
|
||||
.placements = placements,
|
||||
.size = size,
|
||||
.alignment = struct_alignment,
|
||||
.tail_padding = size - offset,
|
||||
.packed_size = packed_size,
|
||||
};
|
||||
}
|
||||
|
||||
fn checkAlignment(n: u32) Error!void {
|
||||
// `isPowerOfTwo` asserts a nonzero argument, and zero is a value a definition
|
||||
// can carry.
|
||||
if (n == 0 or !std.math.isPowerOfTwo(n)) return error.InvalidAlignment;
|
||||
}
|
||||
|
||||
fn mul(a: u64, b: u64) Error!u64 {
|
||||
return std.math.mul(u64, a, b) catch error.StructTooLarge;
|
||||
}
|
||||
|
||||
fn add(a: u64, b: u64) Error!u64 {
|
||||
return std.math.add(u64, a, b) catch error.StructTooLarge;
|
||||
}
|
||||
|
||||
/// `offset` rounded up to a multiple of `alignment`, which is a power of two.
|
||||
fn alignUp(offset: u64, alignment: u32) Error!u64 {
|
||||
const rem = offset % alignment;
|
||||
if (rem == 0) return offset;
|
||||
return add(offset, alignment - rem);
|
||||
}
|
||||
|
||||
// -- Tests --
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
fn m(name: []const u8, primitive: Primitive) Member {
|
||||
return .{ .name = name, .type_name = @tagName(primitive), .primitive = primitive };
|
||||
}
|
||||
|
||||
fn expectPlacement(layout: Layout, index: usize, offset: u64, size: u64, alignment: u32, padding_before: u64) !void {
|
||||
const placement = layout.placements[index];
|
||||
try testing.expectEqual(offset, placement.offset);
|
||||
try testing.expectEqual(size, placement.size);
|
||||
try testing.expectEqual(alignment, placement.alignment);
|
||||
try testing.expectEqual(padding_before, placement.padding_before);
|
||||
}
|
||||
|
||||
test "the design document's example: one byte of padding in sixteen" {
|
||||
// struct { u32 x; u8 flags; u16 id; u64 ts; }
|
||||
const members = [_]Member{ m("x", .int), m("flags", .char), m("id", .short), m("ts", .long_long) };
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
|
||||
try expectPlacement(layout, 0, 0, 4, 4, 0);
|
||||
try expectPlacement(layout, 1, 4, 1, 1, 0);
|
||||
try expectPlacement(layout, 2, 6, 2, 2, 1);
|
||||
try expectPlacement(layout, 3, 8, 8, 8, 0);
|
||||
try testing.expectEqual(@as(u64, 16), layout.size);
|
||||
try testing.expectEqual(@as(u32, 8), layout.alignment);
|
||||
try testing.expectEqual(@as(u64, 0), layout.tail_padding);
|
||||
try testing.expectEqual(@as(u64, 15), layout.packed_size);
|
||||
try testing.expectEqual(@as(u64, 1), layout.padding());
|
||||
}
|
||||
|
||||
test "tail padding rounds the size up to the struct's alignment" {
|
||||
// struct { u64 a; u8 b; } is 16 bytes, seven of them after b.
|
||||
const members = [_]Member{ m("a", .long_long), m("b", .char) };
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u64, 16), layout.size);
|
||||
try testing.expectEqual(@as(u64, 7), layout.tail_padding);
|
||||
try testing.expectEqual(@as(u64, 7), layout.padding());
|
||||
}
|
||||
|
||||
test "what makes each profile its own" {
|
||||
const Case = struct { abi: []const u8, size: u64, alignment: u32 };
|
||||
// struct { char c; double d; } shows where 8-byte types align.
|
||||
const doubles = [_]Member{ m("c", .char), m("d", .double) };
|
||||
for ([_]Case{
|
||||
.{ .abi = "sysv64", .size = 16, .alignment = 8 },
|
||||
.{ .abi = "win64", .size = 16, .alignment = 8 },
|
||||
.{ .abi = "i386", .size = 12, .alignment = 4 },
|
||||
.{ .abi = "arm32", .size = 16, .alignment = 8 },
|
||||
}) |case| {
|
||||
var layout = try compute(testing.allocator, .{ .members = &doubles }, findAbi(case.abi).?, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(case.size, layout.size);
|
||||
try testing.expectEqual(case.alignment, layout.alignment);
|
||||
}
|
||||
|
||||
// struct { long l; void *p; wchar_t w; } shows the data model.
|
||||
const model = [_]Member{ m("l", .long), m("p", .pointer), m("w", .wchar) };
|
||||
for ([_]Case{
|
||||
.{ .abi = "sysv64", .size = 24, .alignment = 8 },
|
||||
.{ .abi = "win64", .size = 24, .alignment = 8 },
|
||||
.{ .abi = "i386", .size = 12, .alignment = 4 },
|
||||
.{ .abi = "arm32", .size = 12, .alignment = 4 },
|
||||
}) |case| {
|
||||
var layout = try compute(testing.allocator, .{ .members = &model }, findAbi(case.abi).?, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(case.size, layout.size);
|
||||
try testing.expectEqual(case.alignment, layout.alignment);
|
||||
}
|
||||
|
||||
// On Windows `long` is four bytes, so the pointer after it is padded.
|
||||
var win = try compute(testing.allocator, .{ .members = &model }, findAbi("win64").?, null);
|
||||
defer win.deinit(testing.allocator);
|
||||
try expectPlacement(win, 1, 8, 8, 8, 4);
|
||||
try expectPlacement(win, 2, 16, 2, 2, 0);
|
||||
}
|
||||
|
||||
test "__int128 is sixteen-aligned on 64-bit and absent on 32-bit" {
|
||||
const members = [_]Member{ m("c", .char), m("big", .int128) };
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try expectPlacement(layout, 1, 16, 16, 16, 15);
|
||||
try testing.expectEqual(@as(u64, 32), layout.size);
|
||||
|
||||
var diag: Diagnostic = .{};
|
||||
try testing.expectError(error.TypeUnavailable, compute(testing.allocator, .{ .members = &members }, findAbi("i386").?, &diag));
|
||||
try testing.expectEqual(@as(?usize, 1), diag.member);
|
||||
}
|
||||
|
||||
test "arrays multiply the element size and keep the element alignment" {
|
||||
// struct { char tag[3]; int grid[2][3]; } : the grid starts at 4 and is 24 bytes.
|
||||
const members = [_]Member{
|
||||
.{ .name = "tag", .type_name = "char", .primitive = .char, .dims = &.{3} },
|
||||
.{ .name = "grid", .type_name = "int", .primitive = .int, .dims = &.{ 2, 3 } },
|
||||
};
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try expectPlacement(layout, 0, 0, 3, 1, 0);
|
||||
try expectPlacement(layout, 1, 4, 24, 4, 1);
|
||||
try testing.expectEqual(@as(u64, 28), layout.size);
|
||||
}
|
||||
|
||||
test "a flexible array member has no size but still aligns" {
|
||||
// struct { short len; double data[]; } : data starts at 8, sizeof is 8.
|
||||
const members = [_]Member{
|
||||
m("len", .short),
|
||||
.{ .name = "data", .type_name = "double", .primitive = .double, .flexible = true },
|
||||
};
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try expectPlacement(layout, 1, 8, 0, 8, 6);
|
||||
try testing.expectEqual(@as(u64, 8), layout.size);
|
||||
try testing.expectEqual(@as(u64, 2), layout.packed_size);
|
||||
}
|
||||
|
||||
test "packed removes padding, aligned adds it back where asked" {
|
||||
const members = [_]Member{ m("c", .char), m("i", .int), m("d", .double) };
|
||||
var packed_layout = try compute(testing.allocator, .{ .members = &members, .@"packed" = true }, default_abi, null);
|
||||
defer packed_layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u64, 13), packed_layout.size);
|
||||
try testing.expectEqual(@as(u32, 1), packed_layout.alignment);
|
||||
try testing.expectEqual(@as(u64, 0), packed_layout.padding());
|
||||
|
||||
// A member's own `aligned` survives the struct being packed.
|
||||
const mixed = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .aligned = 4 } };
|
||||
var aligned_layout = try compute(testing.allocator, .{ .members = &mixed, .@"packed" = true }, default_abi, null);
|
||||
defer aligned_layout.deinit(testing.allocator);
|
||||
try expectPlacement(aligned_layout, 1, 4, 4, 4, 3);
|
||||
try testing.expectEqual(@as(u32, 4), aligned_layout.alignment);
|
||||
|
||||
// Packing one member leaves the rest natural.
|
||||
const one = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .@"packed" = true }, m("s", .short) };
|
||||
var one_layout = try compute(testing.allocator, .{ .members = &one }, default_abi, null);
|
||||
defer one_layout.deinit(testing.allocator);
|
||||
try expectPlacement(one_layout, 1, 1, 4, 1, 0);
|
||||
try expectPlacement(one_layout, 2, 6, 2, 2, 1);
|
||||
try testing.expectEqual(@as(u64, 8), one_layout.size);
|
||||
}
|
||||
|
||||
test "aligned on a member raises it; on the struct it raises the struct" {
|
||||
const members = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .aligned = 16 } };
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try expectPlacement(layout, 1, 16, 4, 16, 15);
|
||||
try testing.expectEqual(@as(u64, 32), layout.size);
|
||||
|
||||
// An `aligned` smaller than the natural alignment does nothing.
|
||||
const lower = [_]Member{ m("c", .char), .{ .name = "d", .type_name = "double", .primitive = .double, .aligned = 2 } };
|
||||
var lower_layout = try compute(testing.allocator, .{ .members = &lower }, default_abi, null);
|
||||
defer lower_layout.deinit(testing.allocator);
|
||||
try expectPlacement(lower_layout, 1, 8, 8, 8, 7);
|
||||
|
||||
const plain = [_]Member{ m("a", .int), m("b", .char) };
|
||||
var struct_layout = try compute(testing.allocator, .{ .members = &plain, .aligned = 32 }, default_abi, null);
|
||||
defer struct_layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u32, 32), struct_layout.alignment);
|
||||
try testing.expectEqual(@as(u64, 32), struct_layout.size);
|
||||
try testing.expectEqual(@as(u64, 27), struct_layout.tail_padding);
|
||||
}
|
||||
|
||||
test "pragma pack caps every member's alignment" {
|
||||
const members = [_]Member{ m("c", .char), m("d", .double), m("s", .short) };
|
||||
var layout = try compute(testing.allocator, .{ .members = &members, .pack = 2 }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try expectPlacement(layout, 1, 2, 8, 2, 1);
|
||||
try expectPlacement(layout, 2, 10, 2, 2, 0);
|
||||
try testing.expectEqual(@as(u64, 12), layout.size);
|
||||
try testing.expectEqual(@as(u32, 2), layout.alignment);
|
||||
|
||||
// A pack larger than anything in the struct changes nothing.
|
||||
var wide = try compute(testing.allocator, .{ .members = &members, .pack = 16 }, default_abi, null);
|
||||
defer wide.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u64, 24), wide.size);
|
||||
}
|
||||
|
||||
test "the rules decide whether pragma pack caps an explicit alignment" {
|
||||
// struct { char c; alignas(8) int i; } under pack(2). Found by the clang check:
|
||||
// the GNU targets put `i` at 2, the windows-msvc ones at 8.
|
||||
const members = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .aligned = 8 } };
|
||||
const def: Definition = .{ .members = &members, .pack = 2 };
|
||||
|
||||
var gnu = try compute(testing.allocator, def, findAbi("sysv64").?, null);
|
||||
defer gnu.deinit(testing.allocator);
|
||||
try testing.expectEqual(Abi.Rules.gnu, gnu.abi.rules);
|
||||
try expectPlacement(gnu, 1, 2, 4, 2, 1);
|
||||
try testing.expectEqual(@as(u64, 6), gnu.size);
|
||||
|
||||
var microsoft = try compute(testing.allocator, def, findAbi("win64").?, null);
|
||||
defer microsoft.deinit(testing.allocator);
|
||||
try testing.expectEqual(Abi.Rules.microsoft, microsoft.abi.rules);
|
||||
try expectPlacement(microsoft, 1, 8, 4, 8, 7);
|
||||
try testing.expectEqual(@as(u64, 16), microsoft.size);
|
||||
|
||||
// Without an explicit alignment the two agree: pack caps the natural one.
|
||||
const plain = [_]Member{ m("c", .char), m("d", .double) };
|
||||
for ([_][]const u8{ "sysv64", "win64" }) |id| {
|
||||
var layout = try compute(testing.allocator, .{ .members = &plain, .pack = 2 }, findAbi(id).?, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try expectPlacement(layout, 1, 2, 8, 2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
test "segments walk the struct in address order, every member included" {
|
||||
// struct { char c; int i; short s; char flex[]; } : c, pad, i, s, tail.
|
||||
const members = [_]Member{
|
||||
m("c", .char),
|
||||
m("i", .int),
|
||||
m("s", .short),
|
||||
.{ .name = "flex", .type_name = "char", .primitive = .char, .flexible = true },
|
||||
};
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u64, 12), layout.size);
|
||||
|
||||
const expected = [_]Segment{
|
||||
.{ .offset = 0, .len = 1, .kind = .{ .member = 0 } },
|
||||
.{ .offset = 1, .len = 3, .kind = .{ .padding = 1 } },
|
||||
.{ .offset = 4, .len = 4, .kind = .{ .member = 1 } },
|
||||
.{ .offset = 8, .len = 2, .kind = .{ .member = 2 } },
|
||||
// The flexible member has no bytes and is still there to be listed.
|
||||
.{ .offset = 10, .len = 0, .kind = .{ .member = 3 } },
|
||||
.{ .offset = 10, .len = 2, .kind = .tail },
|
||||
};
|
||||
var it = layout.segments();
|
||||
for (expected) |want| {
|
||||
const got = it.next() orelse return error.TestUnexpectedResult;
|
||||
try testing.expectEqualDeep(want, got);
|
||||
}
|
||||
try testing.expect(it.next() == null);
|
||||
|
||||
// The segments tile the struct exactly.
|
||||
var covered: u64 = 0;
|
||||
var again = layout.segments();
|
||||
while (again.next()) |seg| {
|
||||
try testing.expectEqual(covered, seg.offset);
|
||||
covered += seg.len;
|
||||
}
|
||||
try testing.expectEqual(layout.size, covered);
|
||||
}
|
||||
|
||||
test "a struct with no padding has one segment per member" {
|
||||
const members = [_]Member{ m("a", .int), m("b", .int) };
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
var it = layout.segments();
|
||||
var count: usize = 0;
|
||||
while (it.next()) |seg| : (count += 1) {
|
||||
try testing.expect(seg.kind == .member);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
|
||||
test "definitions that cannot be laid out say why and where" {
|
||||
var diag: Diagnostic = .{};
|
||||
|
||||
try testing.expectError(error.EmptyStruct, compute(testing.allocator, .{ .members = &.{} }, default_abi, &diag));
|
||||
|
||||
const dup = [_]Member{ m("a", .int), m("b", .int), m("a", .char) };
|
||||
try testing.expectError(error.DuplicateMember, compute(testing.allocator, .{ .members = &dup }, default_abi, &diag));
|
||||
try testing.expectEqual(@as(?usize, 2), diag.member);
|
||||
|
||||
const zero = [_]Member{ m("a", .int), .{ .name = "z", .type_name = "char", .primitive = .char, .dims = &.{ 2, 0 } } };
|
||||
diag = .{};
|
||||
try testing.expectError(error.InvalidArrayLength, compute(testing.allocator, .{ .members = &zero }, default_abi, &diag));
|
||||
try testing.expectEqual(@as(?usize, 1), diag.member);
|
||||
|
||||
const flex_first = [_]Member{ .{ .name = "f", .type_name = "char", .primitive = .char, .flexible = true }, m("a", .int) };
|
||||
diag = .{};
|
||||
try testing.expectError(error.MisplacedFlexibleArray, compute(testing.allocator, .{ .members = &flex_first }, default_abi, &diag));
|
||||
try testing.expectEqual(@as(?usize, 0), diag.member);
|
||||
|
||||
const odd = [_]Member{.{ .name = "a", .type_name = "int", .primitive = .int, .aligned = 3 }};
|
||||
diag = .{};
|
||||
try testing.expectError(error.InvalidAlignment, compute(testing.allocator, .{ .members = &odd }, default_abi, &diag));
|
||||
try testing.expectEqual(@as(?usize, 0), diag.member);
|
||||
|
||||
const plain = [_]Member{m("a", .int)};
|
||||
diag = .{};
|
||||
try testing.expectError(error.InvalidAlignment, compute(testing.allocator, .{ .members = &plain, .pack = 6 }, default_abi, &diag));
|
||||
try testing.expectEqual(@as(?usize, null), diag.member);
|
||||
try testing.expectError(error.InvalidAlignment, compute(testing.allocator, .{ .members = &plain, .aligned = 0 }, default_abi, null));
|
||||
}
|
||||
|
||||
test "too many members is refused before any work is done" {
|
||||
const many = try testing.allocator.alloc(Member, max_members + 1);
|
||||
defer testing.allocator.free(many);
|
||||
for (many) |*member| member.* = m("same", .char);
|
||||
// Every name is the same, so reaching the duplicate check would be a different
|
||||
// error: the count is checked first.
|
||||
try testing.expectError(error.TooManyMembers, compute(testing.allocator, .{ .members = many }, default_abi, null));
|
||||
|
||||
const names = try testing.allocator.alloc([8]u8, max_members);
|
||||
defer testing.allocator.free(names);
|
||||
for (many[0..max_members], names, 0..) |*member, *name, i| {
|
||||
member.* = m(std.fmt.bufPrint(name, "f{d}", .{i}) catch unreachable, .char);
|
||||
}
|
||||
var layout = try compute(testing.allocator, .{ .members = many[0..max_members] }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u64, max_members), layout.size);
|
||||
}
|
||||
|
||||
test "a struct larger than the ABI can address is refused" {
|
||||
// 2^31 bytes is one past PTRDIFF_MAX on a 32-bit target and fine on a 64-bit one.
|
||||
const members = [_]Member{.{ .name = "buf", .type_name = "char", .primitive = .char, .dims = &.{1 << 31} }};
|
||||
var diag: Diagnostic = .{};
|
||||
try testing.expectError(error.StructTooLarge, compute(testing.allocator, .{ .members = &members }, findAbi("i386").?, &diag));
|
||||
try testing.expectEqual(@as(?usize, 0), diag.member);
|
||||
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
||||
defer layout.deinit(testing.allocator);
|
||||
try testing.expectEqual(@as(u64, 1 << 31), layout.size);
|
||||
|
||||
// Dimensions whose product overflows u64 are the same error, not a wrap.
|
||||
const huge = [_]Member{.{ .name = "h", .type_name = "long long", .primitive = .long_long, .dims = &.{ 1 << 40, 1 << 40 } }};
|
||||
try testing.expectError(error.StructTooLarge, compute(testing.allocator, .{ .members = &huge }, default_abi, null));
|
||||
|
||||
// Rounding up to the struct's alignment can push it over too: the members fit
|
||||
// in PTRDIFF_MAX, the padded struct does not.
|
||||
const edge = [_]Member{
|
||||
m("i", .int),
|
||||
.{ .name = "buf", .type_name = "char", .primitive = .char, .dims = &.{(1 << 31) - 5} },
|
||||
};
|
||||
try testing.expectError(error.StructTooLarge, compute(testing.allocator, .{ .members = &edge }, findAbi("i386").?, null));
|
||||
}
|
||||
|
||||
test "every profile has the scalars C guarantees, and pointer-sized limits" {
|
||||
for (&abis) |*abi| {
|
||||
for ([_]Primitive{ .char, .bool, .short, .int, .long, .long_long, .float, .double, .pointer, .wchar, .enumeration }) |p| {
|
||||
const shape = abi.shapes.get(p) orelse return error.TestUnexpectedResult;
|
||||
try testing.expect(shape.size > 0);
|
||||
try testing.expect(std.math.isPowerOfTwo(shape.alignment));
|
||||
try testing.expect(shape.alignment <= shape.size);
|
||||
}
|
||||
try testing.expectEqual(@as(u8, 1), abi.shapes.get(.char).?.size);
|
||||
try testing.expect(abi.shapes.get(.int128) == null or abi.shapes.get(.pointer).?.size == 8);
|
||||
try testing.expect(abi.label.len > 0);
|
||||
try testing.expect(findAbi(abi.id) == abi);
|
||||
}
|
||||
try testing.expectEqual(@as(u64, std.math.maxInt(i64)), default_abi.maxObjectSize());
|
||||
try testing.expectEqual(@as(u64, std.math.maxInt(i32)), findAbi("arm32").?.maxObjectSize());
|
||||
try testing.expect(findAbi("vax") == null);
|
||||
}
|
||||
|
||||
test "compute releases everything on any allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, struct {
|
||||
fn run(a: Allocator) !void {
|
||||
const members = [_]Member{ m("x", .int), m("y", .char) };
|
||||
var layout = try compute(a, .{ .members = &members }, default_abi, null);
|
||||
layout.deinit(a);
|
||||
}
|
||||
}.run, .{});
|
||||
}
|
||||
290
engine/test/struct_oracle.zig
Normal file
290
engine/test/struct_oracle.zig
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
//! 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -307,6 +307,381 @@ pub fn amortization(
|
|||
return .{ .output = text, .is_error = false };
|
||||
}
|
||||
|
||||
// -- Struct layout --
|
||||
|
||||
const struct_layout = engine.struct_layout;
|
||||
const CStructParser = engine.CStructParser;
|
||||
|
||||
/// Members a memory map can tell apart, one letter each: a-z, then A-Z.
|
||||
const max_keyed_members = 52;
|
||||
|
||||
/// Whether a struct layout is drawn in color. Deciding (a terminal, `NO_COLOR`,
|
||||
/// `--color`) is `main`'s job; this only carries the answer.
|
||||
///
|
||||
/// Color is a second channel, never the only one: every member keeps its letter, so
|
||||
/// the output means the same thing piped, pasted, or read by someone who cannot tell
|
||||
/// the hues apart. What color adds is that a member's bytes read as one block in the
|
||||
/// map, its row in the table is the same hue, and padding falls back as faint gaps.
|
||||
pub const Paint = enum {
|
||||
plain,
|
||||
ansi,
|
||||
|
||||
/// The terminal's own six hues rather than fixed RGB values, so the user's theme
|
||||
/// decides what they look like and keeps them readable on its own background.
|
||||
/// Cycled by member index, so neighbouring members never share one. Each starts
|
||||
/// with a reset because faint persists until something clears it.
|
||||
const hues = [_][]const u8{
|
||||
"\x1b[0;36m", // cyan
|
||||
"\x1b[0;33m", // yellow
|
||||
"\x1b[0;35m", // magenta
|
||||
"\x1b[0;32m", // green
|
||||
"\x1b[0;34m", // blue
|
||||
"\x1b[0;31m", // red
|
||||
};
|
||||
/// Faint (SGR 2) rather than "bright black", which some themes (Solarized among
|
||||
/// them) draw in nearly the background color.
|
||||
const faint = "\x1b[0;2m";
|
||||
const reset = "\x1b[0m";
|
||||
|
||||
fn member(self: Paint, index: usize) []const u8 {
|
||||
return self.use(hues[index % hues.len]);
|
||||
}
|
||||
|
||||
fn padding(self: Paint) []const u8 {
|
||||
return self.use(faint);
|
||||
}
|
||||
|
||||
fn end(self: Paint) []const u8 {
|
||||
return self.use(reset);
|
||||
}
|
||||
|
||||
/// `code` when painting, nothing when plain.
|
||||
fn use(self: Paint, code: []const u8) []const u8 {
|
||||
return if (self == .ansi) code else "";
|
||||
}
|
||||
};
|
||||
|
||||
/// Lay out a C struct and render it: a table of members and padding, a summary line,
|
||||
/// and a memory map with one letter per byte. Allocated, because a struct's table has
|
||||
/// no useful upper bound.
|
||||
pub fn structLayout(
|
||||
allocator: std.mem.Allocator,
|
||||
source: []const u8,
|
||||
abi: *const struct_layout.Abi,
|
||||
paint: Paint,
|
||||
) Result {
|
||||
var diag: CStructParser.Diagnostic = .{};
|
||||
var parsed = CStructParser.parse(allocator, source, &diag) catch |err| {
|
||||
return located(allocator, err, source, diag.span);
|
||||
};
|
||||
defer parsed.deinit();
|
||||
const definition = parsed.definition;
|
||||
|
||||
var layout_diag: struct_layout.Diagnostic = .{};
|
||||
var layout = struct_layout.compute(allocator, definition, abi, &layout_diag) catch |err| {
|
||||
const span = if (layout_diag.member) |i| definition.members[i].span else null;
|
||||
return located(allocator, err, source, span);
|
||||
};
|
||||
defer layout.deinit(allocator);
|
||||
|
||||
var scratch: std.heap.ArenaAllocator = .init(allocator);
|
||||
defer scratch.deinit();
|
||||
const rows = tableRows(scratch.allocator(), definition, &layout) catch return oom();
|
||||
|
||||
var out: std.Io.Writer.Allocating = .init(allocator);
|
||||
defer out.deinit();
|
||||
writeLayout(&out.writer, rows, definition, &layout, paint) catch return oom();
|
||||
const text = out.toOwnedSlice() catch return oom();
|
||||
return .{ .output = text, .is_error = false };
|
||||
}
|
||||
|
||||
/// An error, and where in the definition it is, when it is anywhere in particular:
|
||||
///
|
||||
/// error: unknown type (typedefs and macros from elsewhere are not visible)
|
||||
/// line 3, column 5: uint33_t
|
||||
fn located(allocator: std.mem.Allocator, err: engine.Error, source: []const u8, span: ?struct_layout.Span) Result {
|
||||
if (err == error.OutOfMemory) return oom();
|
||||
const s = span orelse return .{ .output = decoratedError(err), .is_error = true };
|
||||
const at = CStructParser.locate(source, s.start);
|
||||
const text = source[s.start..][0..s.len];
|
||||
// One line of it, and not much of that: a directive is reported whole.
|
||||
const line_end = std.mem.indexOfScalar(u8, text, '\n') orelse text.len;
|
||||
const shown = text[0..@min(line_end, 40)];
|
||||
const output = std.fmt.allocPrint(allocator, "error: {s}\n line {d}, column {d}{s}{s}\n", .{
|
||||
engine.phrase(err),
|
||||
at.line,
|
||||
at.column,
|
||||
if (shown.len == 0) ", at the end of the input" else ": ",
|
||||
shown,
|
||||
}) catch return oom();
|
||||
return .{ .output = output, .is_error = true };
|
||||
}
|
||||
|
||||
/// The letter a member is drawn with in the map, matching its row in the table.
|
||||
fn memberKey(index: usize) u8 {
|
||||
if (index < 26) return @intCast('a' + index);
|
||||
if (index < max_keyed_members) return @intCast('A' + index - 26);
|
||||
return ' ';
|
||||
}
|
||||
|
||||
/// A count with thousands separators (NFR-7), printed with `{f}`.
|
||||
const Grouped = struct {
|
||||
n: u64,
|
||||
|
||||
pub fn format(self: Grouped, w: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
var digits: [20]u8 = undefined;
|
||||
const len = std.fmt.printInt(&digits, self.n, 10, .lower, .{});
|
||||
try engine.grouping.print(w, digits[0..len]);
|
||||
}
|
||||
};
|
||||
|
||||
/// A member's type as a person writes it, dimensions included: `char[16]`,
|
||||
/// `int[2][3]`, `double[]`. Printed with `{f}`.
|
||||
const TypeName = struct {
|
||||
member: struct_layout.Member,
|
||||
|
||||
pub fn format(self: TypeName, w: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
try w.writeAll(self.member.type_name);
|
||||
if (self.member.flexible) try w.writeAll("[]");
|
||||
for (self.member.dims) |dim| try w.print("[{d}]", .{dim});
|
||||
}
|
||||
};
|
||||
|
||||
/// One line of the table, its cells rendered to text once. A column's width is
|
||||
/// measured from the same text that is printed, so the two cannot disagree.
|
||||
const Row = struct {
|
||||
kind: union(enum) { header, member: usize, padding },
|
||||
offset: []const u8,
|
||||
size: []const u8,
|
||||
alignment: []const u8,
|
||||
type: []const u8,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
/// The table: a header, then one row per segment of the layout, which is the same
|
||||
/// list the memory map draws. Allocated in `arena`, which the caller releases.
|
||||
fn tableRows(arena: std.mem.Allocator, def: struct_layout.Definition, layout: *const struct_layout.Layout) ![]const Row {
|
||||
var rows: std.ArrayList(Row) = .empty;
|
||||
try rows.append(arena, .{
|
||||
.kind = .header,
|
||||
.offset = "Offset",
|
||||
.size = "Size",
|
||||
.alignment = "Align",
|
||||
.type = "Type",
|
||||
.name = "Name",
|
||||
});
|
||||
var it = layout.segments();
|
||||
while (it.next()) |seg| {
|
||||
const offset = try std.fmt.allocPrint(arena, "{f}", .{Grouped{ .n = seg.offset }});
|
||||
const size = try std.fmt.allocPrint(arena, "{f}", .{Grouped{ .n = seg.len }});
|
||||
try rows.append(arena, switch (seg.kind) {
|
||||
.member => |i| .{
|
||||
.kind = .{ .member = i },
|
||||
.offset = offset,
|
||||
.size = size,
|
||||
.alignment = try std.fmt.allocPrint(arena, "{f}", .{Grouped{ .n = layout.placements[i].alignment }}),
|
||||
.type = try std.fmt.allocPrint(arena, "{f}", .{TypeName{ .member = def.members[i] }}),
|
||||
.name = def.members[i].name,
|
||||
},
|
||||
.padding, .tail => .{
|
||||
.kind = .padding,
|
||||
.offset = offset,
|
||||
.size = size,
|
||||
.alignment = "",
|
||||
.type = "",
|
||||
.name = if (seg.kind == .tail) "(tail padding)" else "(padding)",
|
||||
},
|
||||
});
|
||||
}
|
||||
return rows.items;
|
||||
}
|
||||
|
||||
/// The widest cell in each column.
|
||||
const Widths = struct {
|
||||
offset: usize = 0,
|
||||
size: usize = 0,
|
||||
alignment: usize = 0,
|
||||
type: usize = 0,
|
||||
|
||||
fn of(rows: []const Row) Widths {
|
||||
var widths: Widths = .{};
|
||||
for (rows) |row| {
|
||||
widths.offset = @max(widths.offset, row.offset.len);
|
||||
widths.size = @max(widths.size, row.size.len);
|
||||
widths.alignment = @max(widths.alignment, row.alignment.len);
|
||||
widths.type = @max(widths.type, row.type.len);
|
||||
}
|
||||
return widths;
|
||||
}
|
||||
};
|
||||
|
||||
/// A member row colors its key and its name, which is enough to find its bytes in the
|
||||
/// map without turning the table into a wall of color. A padding row is faint from
|
||||
/// end to end, and the header is plain.
|
||||
fn writeRow(w: *std.Io.Writer, row: Row, widths: Widths, paint: Paint) std.Io.Writer.Error!void {
|
||||
const key: u8, const key_color: []const u8, const key_end: []const u8, const name_color: []const u8 =
|
||||
switch (row.kind) {
|
||||
.header => .{ ' ', "", "", "" },
|
||||
.member => |i| .{ memberKey(i), paint.member(i), paint.end(), paint.member(i) },
|
||||
.padding => .{ ' ', paint.padding(), "", "" },
|
||||
};
|
||||
const line_end = if (row.kind == .header) "" else paint.end();
|
||||
try w.print(" {[kc]s}{[key]c}{[ke]s} {[offset]s: >[ow]} {[size]s: >[sw]} {[alignment]s: >[aw]}" ++
|
||||
" {[type]s: <[tw]} {[nc]s}{[name]s}{[end]s}\n", .{
|
||||
.kc = key_color,
|
||||
.key = key,
|
||||
.ke = key_end,
|
||||
.offset = row.offset,
|
||||
.ow = widths.offset,
|
||||
.size = row.size,
|
||||
.sw = widths.size,
|
||||
.alignment = row.alignment,
|
||||
.aw = widths.alignment,
|
||||
.type = row.type,
|
||||
.tw = widths.type,
|
||||
.nc = name_color,
|
||||
.name = row.name,
|
||||
.end = line_end,
|
||||
});
|
||||
}
|
||||
|
||||
fn writeLayout(
|
||||
w: *std.Io.Writer,
|
||||
rows: []const Row,
|
||||
def: struct_layout.Definition,
|
||||
layout: *const struct_layout.Layout,
|
||||
paint: Paint,
|
||||
) std.Io.Writer.Error!void {
|
||||
if (def.name) |name| switch (def.name_kind) {
|
||||
.tag => try w.print("struct {s}\n", .{name}),
|
||||
.typedef => try w.print("{s}\n", .{name}),
|
||||
} else try w.writeAll("struct (anonymous)\n");
|
||||
try w.print("ABI {s}: {s}\n\n", .{ layout.abi.id, layout.abi.label });
|
||||
|
||||
const widths: Widths = .of(rows);
|
||||
for (rows) |row| try writeRow(w, row, widths, paint);
|
||||
|
||||
// The summary: what a reader came for.
|
||||
try w.print("\n {f} {s}, alignment {d}, ", .{ Grouped{ .n = layout.size }, bytes(layout.size), layout.alignment });
|
||||
const padding = layout.padding();
|
||||
if (padding == 0) {
|
||||
try w.writeAll("no padding\n");
|
||||
} else {
|
||||
try w.print("{f} {s} of padding (", .{ Grouped{ .n = padding }, bytes(padding) });
|
||||
try writePercent(w, padding, layout.size);
|
||||
try w.print("); {f} {s} packed\n", .{ Grouped{ .n = layout.packed_size }, bytes(layout.packed_size) });
|
||||
}
|
||||
|
||||
if (layout.size == 0) return;
|
||||
if (def.members.len > max_keyed_members) {
|
||||
try w.print("\n (no memory map: more than {d} members to tell apart)\n", .{max_keyed_members});
|
||||
return;
|
||||
}
|
||||
try w.writeAll("\n");
|
||||
try writeMap(w, layout, paint);
|
||||
}
|
||||
|
||||
fn bytes(n: u64) []const u8 {
|
||||
return if (n == 1) "byte" else "bytes";
|
||||
}
|
||||
|
||||
/// `part` as a percentage of `whole`, rounded half up to two places, with trailing
|
||||
/// zeros dropped: 6.25%, 50%, 33.33%. In integer hundredths, so no float rounding
|
||||
/// decides what is printed.
|
||||
fn writePercent(w: *std.Io.Writer, part: u64, whole: u64) std.Io.Writer.Error!void {
|
||||
const hundredths: u128 = (@as(u128, part) * 20000 + whole) / (@as(u128, whole) * 2);
|
||||
const units = hundredths / 100;
|
||||
const frac = hundredths % 100;
|
||||
if (frac == 0) {
|
||||
try w.print("{d}%", .{units});
|
||||
} else if (frac % 10 == 0) {
|
||||
try w.print("{d}.{d}%", .{ units, frac / 10 });
|
||||
} else {
|
||||
try w.print("{d}.{d:0>2}%", .{ units, frac });
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment holding a byte, for bytes visited in increasing order.
|
||||
const SegmentCursor = struct {
|
||||
it: struct_layout.SegmentIterator,
|
||||
current: struct_layout.Segment,
|
||||
/// How many segments have been passed: which one `current` is.
|
||||
index: usize = 0,
|
||||
|
||||
fn init(layout: *const struct_layout.Layout) SegmentCursor {
|
||||
var it = layout.segments();
|
||||
const first = it.next().?;
|
||||
return .{ .it = it, .current = first };
|
||||
}
|
||||
|
||||
/// Zero-length segments are passed over, since no byte is in them.
|
||||
fn at(self: *SegmentCursor, offset: u64) struct_layout.Segment {
|
||||
while (self.current.offset + self.current.len <= offset) {
|
||||
self.current = self.it.next().?;
|
||||
self.index += 1;
|
||||
}
|
||||
return self.current;
|
||||
}
|
||||
};
|
||||
|
||||
/// Sixteen bytes a row, each drawn as its member's letter or `.` for padding, in
|
||||
/// groups of four. A row identical to the one above it is collapsed to `*`, as
|
||||
/// hexdump does, so `char buf[4096]` costs two lines rather than 256. Rows are found
|
||||
/// from the segments rather than walked byte by byte, so a struct the size of an
|
||||
/// address space costs no more than one with the same number of members.
|
||||
///
|
||||
/// In color, a code is written only where the color changes, so a member's run of
|
||||
/// bytes, gaps included, is one span of one hue.
|
||||
fn writeMap(w: *std.Io.Writer, layout: *const struct_layout.Layout, paint: Paint) std.Io.Writer.Error!void {
|
||||
const per_row = 16;
|
||||
const size = layout.size;
|
||||
const rows = (size + per_row - 1) / per_row;
|
||||
const last_offset = (rows - 1) * per_row;
|
||||
var label_digits: usize = 2;
|
||||
while (label_digits < 16 and last_offset >> @intCast(label_digits * 4) != 0) label_digits += 1;
|
||||
|
||||
var cursor: SegmentCursor = .init(layout);
|
||||
// The segment the last printed row lay wholly inside, if it did.
|
||||
var previous_whole: ?usize = null;
|
||||
var starred = false;
|
||||
|
||||
var row: u64 = 0;
|
||||
while (row < rows) {
|
||||
const start = row * per_row;
|
||||
const end = @min(start + per_row, size);
|
||||
const seg = cursor.at(start);
|
||||
const whole = end - start == per_row and seg.offset + seg.len >= end;
|
||||
if (whole and previous_whole == cursor.index) {
|
||||
if (!starred) try w.writeAll(" *\n");
|
||||
starred = true;
|
||||
// Straight to the first row this segment does not fill.
|
||||
row = (seg.offset + seg.len) / per_row;
|
||||
continue;
|
||||
}
|
||||
starred = false;
|
||||
previous_whole = if (whole) cursor.index else null;
|
||||
|
||||
try w.writeAll(" 0x");
|
||||
try w.printInt(start, 16, .lower, .{ .fill = '0', .width = label_digits });
|
||||
var bytes_cursor = cursor;
|
||||
var current: []const u8 = "";
|
||||
for (start..end) |offset| {
|
||||
const glyph: u8, const color = switch (bytes_cursor.at(offset).kind) {
|
||||
.member => |i| .{ memberKey(i), paint.member(i) },
|
||||
.padding, .tail => .{ '.', paint.padding() },
|
||||
};
|
||||
try w.writeAll(if ((offset - start) % 4 == 0) " " else " ");
|
||||
if (!std.mem.eql(u8, color, current)) {
|
||||
try w.writeAll(color);
|
||||
current = color;
|
||||
}
|
||||
try w.writeByte(glyph);
|
||||
}
|
||||
try w.print("{s}\n", .{paint.end()});
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Errors --
|
||||
|
||||
/// Turn an engine error into a CLI line: "error: <phrase>\n".
|
||||
|
|
@ -731,3 +1106,233 @@ test "amortization: bad terms explain themselves" {
|
|||
try testing.expect(result.is_error);
|
||||
try testing.expect(std.mem.indexOf(u8, result.output, "loan terms") != null);
|
||||
}
|
||||
|
||||
// -- Struct layout --
|
||||
|
||||
/// What `structLayout` prints for `source` under the profile named `abi`, which must
|
||||
/// lay out. Allocated in `arena`.
|
||||
fn rendered(arena: std.mem.Allocator, source: []const u8, abi: []const u8, paint: Paint) ![]const u8 {
|
||||
const result = structLayout(arena, source, struct_layout.findAbi(abi).?, paint);
|
||||
try testing.expect(!result.is_error);
|
||||
return result.output;
|
||||
}
|
||||
|
||||
/// A struct with one more member than the map has letters for.
|
||||
fn manyMembers(arena: std.mem.Allocator) ![]const u8 {
|
||||
var source: std.ArrayList(u8) = .empty;
|
||||
try source.appendSlice(arena, "struct { char ");
|
||||
for (0..max_keyed_members + 1) |i| try source.print(arena, "{s}m{d}", .{ if (i == 0) "" else ", ", i });
|
||||
try source.appendSlice(arena, "; }");
|
||||
return source.items;
|
||||
}
|
||||
|
||||
test "structLayout: the design document's example, in full" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
try testing.expectEqualStrings(
|
||||
\\struct hdr
|
||||
\\ABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64)
|
||||
\\
|
||||
\\ Offset Size Align Type Name
|
||||
\\ a 0 4 4 uint32_t magic
|
||||
\\ b 4 1 1 uint8_t ver
|
||||
\\ 5 1 (padding)
|
||||
\\ c 6 2 2 uint16_t len
|
||||
\\ d 8 8 8 uint64_t ts
|
||||
\\
|
||||
\\ 16 bytes, alignment 8, 1 byte of padding (6.25%); 15 bytes packed
|
||||
\\
|
||||
\\ 0x00 a a a a b . c c d d d d d d d d
|
||||
\\
|
||||
, try rendered(arena.allocator(), "struct hdr { uint32_t magic; uint8_t ver; uint16_t len; uint64_t ts; }", "sysv64", .plain));
|
||||
}
|
||||
|
||||
test "structLayout: arrays, a flexible member, and a map that collapses" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
try testing.expectEqualStrings(
|
||||
\\buf_t
|
||||
\\ABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64)
|
||||
\\
|
||||
\\ Offset Size Align Type Name
|
||||
\\ a 0 4 4 int len
|
||||
\\ b 4 4,096 1 char[4096] data
|
||||
\\ c 4,100 12 2 short[2][3] grid
|
||||
\\ d 4,112 0 8 double[] flex
|
||||
\\
|
||||
\\ 4,112 bytes, alignment 8, no padding
|
||||
\\
|
||||
\\ 0x0000 a a a a b b b b b b b b b b b b
|
||||
\\ 0x0010 b b b b b b b b b b b b b b b b
|
||||
\\ *
|
||||
\\ 0x1000 b b b b c c c c c c c c c c c c
|
||||
\\
|
||||
, try rendered(arena.allocator(), "typedef struct { int len; char data[4096]; short grid[2][3]; double flex[]; } buf_t;", "sysv64", .plain));
|
||||
}
|
||||
|
||||
test "structLayout: a struct under another ABI says so and lays out by its rules" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try rendered(arena.allocator(), "struct { char c; long l; }", "win64", .plain);
|
||||
try testing.expect(std.mem.startsWith(u8, out, "struct (anonymous)\nABI win64: 64-bit Windows (x64, ARM64)\n"));
|
||||
try testing.expect(std.mem.indexOf(u8, out, " b 4 4 4 long l\n") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "8 bytes, alignment 4, 3 bytes of padding (37.5%); 5 bytes packed") != null);
|
||||
}
|
||||
|
||||
test "structLayout: tail padding gets a row and dots of its own" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
try testing.expect(std.mem.endsWith(u8, try rendered(arena.allocator(), "struct tail { uint64_t a; uint8_t b; };", "sysv64", .plain),
|
||||
\\ a 0 8 8 uint64_t a
|
||||
\\ b 8 1 1 uint8_t b
|
||||
\\ 9 7 (tail padding)
|
||||
\\
|
||||
\\ 16 bytes, alignment 8, 7 bytes of padding (43.75%); 9 bytes packed
|
||||
\\
|
||||
\\ 0x00 a a a a a a a a b . . . . . . .
|
||||
\\
|
||||
));
|
||||
}
|
||||
|
||||
test "structLayout: errors name the problem and where it is" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const Case = struct { source: []const u8, abi: []const u8 = "sysv64", output: []const u8 };
|
||||
for ([_]Case{
|
||||
.{
|
||||
.source = "struct {\n int a;\n uint33_t b;\n}",
|
||||
.output = "error: unknown type (typedefs and macros from elsewhere are not visible)\n line 3, column 3: uint33_t\n",
|
||||
},
|
||||
.{
|
||||
// Errors from laying out point at the member they are about.
|
||||
.source = "struct { char c; __int128 big; }",
|
||||
.abi = "arm32",
|
||||
.output = "error: this type does not exist on the selected ABI\n line 1, column 27: big\n",
|
||||
},
|
||||
.{
|
||||
.source = "struct { int a;",
|
||||
.output = "error: unexpected end of input\n line 1, column 16, at the end of the input\n",
|
||||
},
|
||||
.{
|
||||
// A directive is reported by its first line, and not much of that.
|
||||
.source = "#include \"a/very/long/path/to/a/header/that/goes/on.h\"\nstruct { int a; }",
|
||||
.output = "error: only #pragma pack is understood, not other directives\n line 1, column 1: #include \"a/very/long/path/to/a/header/t\n",
|
||||
},
|
||||
.{
|
||||
// An error about the whole struct has nowhere in particular to point.
|
||||
.source = "struct empty { };",
|
||||
.output = "error: the struct has no members\n",
|
||||
},
|
||||
}) |case| {
|
||||
const result = structLayout(arena.allocator(), case.source, struct_layout.findAbi(case.abi).?, .plain);
|
||||
try testing.expect(result.is_error);
|
||||
try testing.expectEqualStrings(case.output, result.output);
|
||||
}
|
||||
}
|
||||
|
||||
test "structLayout: more members than letters gets no map, and says why" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try rendered(arena.allocator(), try manyMembers(arena.allocator()), "sysv64", .plain);
|
||||
try testing.expect(std.mem.indexOf(u8, out, " Z 51 1 1 char m51\n") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, " 52 1 1 char m52\n") != null);
|
||||
try testing.expect(std.mem.endsWith(u8, out, "(no memory map: more than 52 members to tell apart)\n"));
|
||||
}
|
||||
|
||||
test "structLayout: a map of a huge struct is as short as a small one" {
|
||||
// Two gigabytes of array: walking it byte by byte would not finish.
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
try testing.expect(std.mem.endsWith(u8, try rendered(arena.allocator(), "struct { char c; double d[268435456]; }", "sysv64", .plain),
|
||||
\\ 0x00000000 a . . . . . . . b b b b b b b b
|
||||
\\ 0x00000010 b b b b b b b b b b b b b b b b
|
||||
\\ *
|
||||
\\ 0x80000000 b b b b b b b b
|
||||
\\
|
||||
));
|
||||
}
|
||||
|
||||
test "writePercent drops the zeros it does not need" {
|
||||
var buf: [32]u8 = undefined;
|
||||
for ([_]struct { part: u64, whole: u64, text: []const u8 }{
|
||||
.{ .part = 1, .whole = 16, .text = "6.25%" },
|
||||
.{ .part = 1, .whole = 2, .text = "50%" },
|
||||
.{ .part = 1, .whole = 3, .text = "33.33%" },
|
||||
.{ .part = 3, .whole = 8, .text = "37.5%" },
|
||||
}) |case| {
|
||||
var w = std.Io.Writer.fixed(&buf);
|
||||
try writePercent(&w, case.part, case.whole);
|
||||
try testing.expectEqualStrings(case.text, w.buffered());
|
||||
}
|
||||
}
|
||||
|
||||
/// `text` with every ANSI escape sequence (ESC, `[`, parameters, a final letter)
|
||||
/// removed.
|
||||
fn stripAnsi(allocator: std.mem.Allocator, text: []const u8) ![]u8 {
|
||||
var out: std.ArrayList(u8) = .empty;
|
||||
var i: usize = 0;
|
||||
while (i < text.len) {
|
||||
if (text[i] == 0x1b and i + 1 < text.len and text[i + 1] == '[') {
|
||||
i += 2;
|
||||
while (i < text.len and !std.ascii.isAlphabetic(text[i])) i += 1;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
try out.append(allocator, text[i]);
|
||||
i += 1;
|
||||
}
|
||||
return out.items;
|
||||
}
|
||||
|
||||
test "structLayout: color adds escapes and nothing else" {
|
||||
// The letters are the key and color is a second channel, so taking the color
|
||||
// away must leave exactly the plain output: same rows, same columns, same map.
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const a = arena.allocator();
|
||||
for ([_][]const u8{
|
||||
"struct hdr { uint32_t magic; uint8_t ver; uint16_t len; uint64_t ts; }",
|
||||
"struct tail { uint64_t a; uint8_t b; };",
|
||||
"typedef struct { int len; char data[4096]; short grid[2][3]; double flex[]; } buf_t;",
|
||||
"struct { char c; double d[268435456]; }",
|
||||
"struct { char a; char b; char c; char d; char e; char f; char g; char h; }",
|
||||
try manyMembers(a),
|
||||
}) |source| {
|
||||
const plain = try rendered(a, source, "sysv64", .plain);
|
||||
const colored = try rendered(a, source, "sysv64", .ansi);
|
||||
try testing.expect(std.mem.indexOfScalar(u8, plain, 0x1b) == null);
|
||||
try testing.expect(std.mem.indexOfScalar(u8, colored, 0x1b) != null);
|
||||
try testing.expectEqualStrings(plain, try stripAnsi(a, colored));
|
||||
}
|
||||
}
|
||||
|
||||
test "structLayout: a member's row and bytes share its hue, and padding is faint" {
|
||||
var arena: std.heap.ArenaAllocator = .init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const out = try rendered(arena.allocator(), "struct { char c; int i; }", "sysv64", .ansi);
|
||||
const first = Paint.hues[0];
|
||||
const second = Paint.hues[1];
|
||||
const faint = Paint.faint;
|
||||
const reset = Paint.reset;
|
||||
|
||||
// In the table: the key and the name, nothing between them.
|
||||
try testing.expect(std.mem.indexOf(u8, out, " " ++ first ++ "a" ++ reset ++ " 0 1 1 char " ++ first ++ "c" ++ reset ++ "\n") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, " " ++ second ++ "b" ++ reset ++ " 4 4 4 int " ++ second ++ "i" ++ reset ++ "\n") != null);
|
||||
// A padding row is faint from its first column to its end.
|
||||
try testing.expect(std.mem.indexOf(u8, out, " " ++ faint ++ " 1 3 (padding)" ++ reset ++ "\n") != null);
|
||||
// In the map, a code only where the color changes, and a reset at the end of
|
||||
// the row so nothing bleeds into what the terminal prints next.
|
||||
try testing.expect(std.mem.endsWith(u8, out, " 0x00 " ++ first ++ "a " ++ faint ++ ". . . " ++ second ++ "b b b b" ++ reset ++ "\n"));
|
||||
// The header, column titles and summary stay plain.
|
||||
try testing.expect(std.mem.startsWith(u8, out, "struct (anonymous)\nABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64)\n\n Offset"));
|
||||
try testing.expect(std.mem.indexOf(u8, out, "\n 8 bytes, alignment 4, 3 bytes of padding (37.5%); 5 bytes packed\n") != null);
|
||||
}
|
||||
|
||||
test "Paint: neighbouring members never share a hue, and plain is empty" {
|
||||
for (0..max_keyed_members) |i| {
|
||||
try testing.expect(!std.mem.eql(u8, Paint.ansi.member(i), Paint.ansi.member(i + 1)));
|
||||
try testing.expectEqualStrings("", Paint.plain.member(i));
|
||||
}
|
||||
try testing.expectEqualStrings("", Paint.plain.padding());
|
||||
try testing.expectEqualStrings("", Paint.plain.end());
|
||||
}
|
||||
|
|
|
|||
297
src/main.zig
297
src/main.zig
|
|
@ -34,12 +34,56 @@ pub const ParsedArgs = union(enum) {
|
|||
/// Print only the summary block, not the per-period rows.
|
||||
summary_only: bool,
|
||||
},
|
||||
layout: struct {
|
||||
source: StructSource,
|
||||
abi: *const engine.struct_layout.Abi,
|
||||
color: ColorChoice = .auto,
|
||||
},
|
||||
output: struct {
|
||||
text: []const u8,
|
||||
is_error: bool,
|
||||
},
|
||||
};
|
||||
|
||||
/// Where `tally struct` reads its definition from. Reading is `main`'s job, since it
|
||||
/// is the part that touches I/O; parsing only decides which.
|
||||
const StructSource = union(enum) {
|
||||
/// The definition itself, joined from the arguments with the allocator given to
|
||||
/// `parseArgs`.
|
||||
text: []const u8,
|
||||
/// A path to read it from.
|
||||
file: []const u8,
|
||||
stdin,
|
||||
};
|
||||
|
||||
/// `--color`: whether output that has color uses it.
|
||||
const ColorChoice = enum {
|
||||
/// Color when stdout is a terminal that understands it, unless `NO_COLOR` is set
|
||||
/// or `TERM` is `dumb`.
|
||||
auto,
|
||||
always,
|
||||
never,
|
||||
|
||||
fn parse(text: []const u8) ?ColorChoice {
|
||||
return std.meta.stringToEnum(ColorChoice, text);
|
||||
}
|
||||
|
||||
/// The decision, from the facts `main` gathers. `NO_COLOR` counts when it is set
|
||||
/// to anything but the empty string, which is what no-color.org asks of programs.
|
||||
fn resolve(self: ColorChoice, terminal: bool, no_color: ?[]const u8, term: ?[]const u8) format.Paint {
|
||||
return switch (self) {
|
||||
.always => .ansi,
|
||||
.never => .plain,
|
||||
.auto => {
|
||||
if (!terminal) return .plain;
|
||||
if (no_color) |value| if (value.len > 0) return .plain;
|
||||
if (term) |value| if (std.mem.eql(u8, value, "dumb")) return .plain;
|
||||
return .ansi;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedArgs {
|
||||
var mode: format.Mode = .standard;
|
||||
var config: engine.programmer.Config = .{};
|
||||
|
|
@ -58,6 +102,10 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA
|
|||
return parseAmortArgs(args[1..]);
|
||||
}
|
||||
|
||||
if (args.len > 0 and std.mem.eql(u8, args[0], "struct")) {
|
||||
return parseStructArgs(allocator, args[1..]);
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
|
@ -403,6 +451,113 @@ fn parseAmortArgs(args: []const []const u8) ParsedArgs {
|
|||
} };
|
||||
}
|
||||
|
||||
/// The profiles `--abi` takes, one per line, from the engine's table rather than
|
||||
/// written out here.
|
||||
const abi_list = blk: {
|
||||
var text: []const u8 = "";
|
||||
for (engine.struct_layout.abis) |abi| {
|
||||
const default = if (std.mem.eql(u8, abi.id, engine.struct_layout.default_abi.id)) ", the default" else "";
|
||||
text = text ++ std.fmt.comptimePrint(" {s: <8} {s}{s}\n", .{ abi.id, abi.label, default });
|
||||
}
|
||||
break :blk text;
|
||||
};
|
||||
|
||||
const struct_usage =
|
||||
\\usage: tally struct <definition | file | -> [--abi <name>] [--color <when>]
|
||||
\\
|
||||
\\Lays out a C struct: each member's offset, size and alignment, the padding
|
||||
\\between them, and a map of the bytes. Text containing '{' is the definition
|
||||
\\itself; anything else names a file to read it from, and '-' reads stdin.
|
||||
\\
|
||||
\\ABIs:
|
||||
\\
|
||||
++ abi_list ++
|
||||
\\
|
||||
\\--color auto (the default) colors each member's row and bytes when writing to a
|
||||
\\terminal, unless NO_COLOR is set; always and never override that.
|
||||
\\
|
||||
\\Understood: one struct or typedef'd struct of scalars, pointers and arrays, in
|
||||
\\C spellings (uint32_t, unsigned long, size_t, __int128, struct node *) or
|
||||
\\short ones (u32, f64, ptr); alignas, __attribute__((aligned(N), packed)) and
|
||||
\\#pragma pack, which as in C must be on a line of its own. Not understood, and
|
||||
\\reported as such: nested structs and unions, bitfields, typedefs and macros
|
||||
\\from elsewhere, other directives.
|
||||
\\
|
||||
\\examples:
|
||||
\\ tally struct 'struct hdr { uint32_t magic; uint8_t ver; uint16_t len; uint64_t ts; }'
|
||||
\\ tally struct --abi i386 'struct { char c; double d; }'
|
||||
\\ tally struct header.h
|
||||
\\
|
||||
;
|
||||
|
||||
const abi_usage = "error: --abi takes one of\n" ++ abi_list ++ "\n";
|
||||
|
||||
const color_usage =
|
||||
\\error: --color takes auto, always or never
|
||||
\\
|
||||
;
|
||||
|
||||
/// Parse the arguments following the `struct` subcommand.
|
||||
///
|
||||
/// The positional arguments are joined with spaces, as an expression's are, so a
|
||||
/// definition the shell split still arrives whole. Text with a brace in it is a
|
||||
/// definition; a single argument without one is a path, or `-` for stdin.
|
||||
fn parseStructArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedArgs {
|
||||
if (wantsHelp(args)) {
|
||||
return .{ .output = .{ .text = struct_usage, .is_error = false } };
|
||||
}
|
||||
|
||||
var abi = engine.struct_layout.default_abi;
|
||||
var color: ColorChoice = .auto;
|
||||
var parts: std.ArrayList([]const u8) = .empty;
|
||||
defer parts.deinit(allocator);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
switch (flagValue(arg, "--abi", args, &i)) {
|
||||
.absent => {},
|
||||
.missing => return .{ .output = .{ .text = abi_usage, .is_error = true } },
|
||||
.value => |id| {
|
||||
abi = engine.struct_layout.findAbi(id) orelse
|
||||
return .{ .output = .{ .text = abi_usage, .is_error = true } };
|
||||
continue;
|
||||
},
|
||||
}
|
||||
switch (flagValue(arg, "--color", args, &i)) {
|
||||
.absent => {},
|
||||
.missing => return .{ .output = .{ .text = color_usage, .is_error = true } },
|
||||
.value => |when| {
|
||||
color = ColorChoice.parse(when) orelse
|
||||
return .{ .output = .{ .text = color_usage, .is_error = true } };
|
||||
continue;
|
||||
},
|
||||
}
|
||||
if (std.mem.startsWith(u8, arg, "--")) {
|
||||
return .{ .output = .{ .text = struct_usage, .is_error = true } };
|
||||
}
|
||||
parts.append(allocator, arg) catch return oomArgs();
|
||||
}
|
||||
|
||||
const found = structSource(allocator, parts.items) catch return oomArgs();
|
||||
const source = found orelse return .{ .output = .{ .text = struct_usage, .is_error = true } };
|
||||
return .{ .layout = .{ .source = source, .abi = abi, .color = color } };
|
||||
}
|
||||
|
||||
/// What the positional arguments of `tally struct` name, or null for nothing usable:
|
||||
/// none at all, or several words without a brace, which are neither a definition nor
|
||||
/// a path.
|
||||
fn structSource(allocator: std.mem.Allocator, parts: []const []const u8) error{OutOfMemory}!?StructSource {
|
||||
for (parts) |part| {
|
||||
if (std.mem.indexOfScalar(u8, part, '{') != null) {
|
||||
return .{ .text = try std.mem.join(allocator, " ", parts) };
|
||||
}
|
||||
}
|
||||
if (parts.len != 1) return null;
|
||||
if (std.mem.eql(u8, parts[0], "-")) return .stdin;
|
||||
return .{ .file = parts[0] };
|
||||
}
|
||||
|
||||
const help_text = help_head ++ financial_function_help ++ help_tail;
|
||||
|
||||
const help_head =
|
||||
|
|
@ -451,6 +606,8 @@ const help_tail =
|
|||
\\Subcommands:
|
||||
\\ tally convert 100 km to mi Explicit unit conversion
|
||||
\\ tally amort 200000 6 360 --monthly Amortization schedule
|
||||
\\ tally struct 'struct { char c; double d; }'
|
||||
\\ Struct layout: offsets and padding
|
||||
\\
|
||||
\\Each subcommand takes -h for its own usage: tally amort --help
|
||||
\\
|
||||
|
|
@ -510,9 +667,56 @@ pub fn main(init: std.process.Init) u8 {
|
|||
if (!result.is_error) write(io, std.Io.File.stdout(), "\n");
|
||||
return if (result.is_error) @as(u8, 1) else 0;
|
||||
},
|
||||
.layout => |request| {
|
||||
// The reader has already said what went wrong.
|
||||
const source = readStructSource(allocator, io, request.source) catch return 1;
|
||||
// A terminal check that is cancelled answers no, which errs toward plain.
|
||||
const terminal = std.Io.File.stdout().supportsAnsiEscapeCodes(io) catch false;
|
||||
const paint = request.color.resolve(
|
||||
terminal,
|
||||
init.environ_map.get("NO_COLOR"),
|
||||
init.environ_map.get("TERM"),
|
||||
);
|
||||
const result = format.structLayout(allocator, source, request.abi, paint);
|
||||
const file = if (result.is_error) std.Io.File.stderr() else std.Io.File.stdout();
|
||||
write(io, file, result.output);
|
||||
return if (result.is_error) @as(u8, 1) else 0;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of a struct definition, from wherever the arguments said, or the message
|
||||
/// to print instead. Reading is bounded by the parser's own limit, plus one byte so
|
||||
/// that a source over it is reported as too long rather than silently cut short.
|
||||
fn readStructSource(allocator: std.mem.Allocator, io: std.Io, source: StructSource) error{Failed}![]const u8 {
|
||||
const limit: std.Io.Limit = .limited(engine.CStructParser.max_source_bytes + 1);
|
||||
return switch (source) {
|
||||
.text => |text| text,
|
||||
.file => |path| std.Io.Dir.cwd().readFileAlloc(io, path, allocator, limit) catch |err| {
|
||||
return readFailure(allocator, io, path, err == error.StreamTooLong, @errorName(err));
|
||||
},
|
||||
.stdin => blk: {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var reader = std.Io.File.stdin().reader(io, &buf);
|
||||
break :blk reader.interface.allocRemaining(allocator, limit) catch |err| {
|
||||
return readFailure(allocator, io, "stdin", err == error.StreamTooLong, @errorName(err));
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Report a definition that could not be read, naming the source and the reason.
|
||||
fn readFailure(allocator: std.mem.Allocator, io: std.Io, what: []const u8, too_long: bool, reason: []const u8) error{Failed} {
|
||||
const message = if (too_long)
|
||||
std.fmt.allocPrint(allocator, "error: {s}: {s} (the limit is {d} bytes)\n", .{
|
||||
what, engine.phrase(error.DefinitionTooLong), engine.CStructParser.max_source_bytes,
|
||||
})
|
||||
else
|
||||
std.fmt.allocPrint(allocator, "error: cannot read {s}: {s}\n", .{ what, reason });
|
||||
write(io, std.Io.File.stderr(), message catch "error: out of memory\n");
|
||||
return error.Failed;
|
||||
}
|
||||
|
||||
fn write(io: std.Io, file: std.Io.File, msg: []const u8) void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var writer = file.writer(io, &buf);
|
||||
|
|
@ -667,6 +871,8 @@ test "parseArgs: subcommands answer --help on stdout" {
|
|||
&.{ "amort", "--help" },
|
||||
&.{ "amort", "-h" },
|
||||
&.{ "amortize", "-h" },
|
||||
&.{ "struct", "--help" },
|
||||
&.{ "struct", "-h" },
|
||||
}) |args| {
|
||||
const out = parseArgs(testing.allocator, args).output;
|
||||
try testing.expect(!out.is_error);
|
||||
|
|
@ -910,3 +1116,94 @@ test "parseArgs: amort rejects incomplete or malformed terms" {
|
|||
const zero = parseArgs(testing.allocator, &.{ "amort", "200000", "0.5", "0" });
|
||||
try testing.expect(zero == .output and zero.output.is_error);
|
||||
}
|
||||
|
||||
// -- Struct layout CLI --
|
||||
|
||||
test "parseArgs: struct takes a definition, joined if the shell split it" {
|
||||
const whole = parseArgs(testing.allocator, &.{ "struct", "struct { int a; }" }).layout;
|
||||
defer testing.allocator.free(whole.source.text);
|
||||
try testing.expectEqualStrings("struct { int a; }", whole.source.text);
|
||||
try testing.expectEqual(engine.struct_layout.default_abi, whole.abi);
|
||||
|
||||
const split = parseArgs(testing.allocator, &.{ "struct", "struct", "{", "int", "a;", "}" }).layout;
|
||||
defer testing.allocator.free(split.source.text);
|
||||
try testing.expectEqualStrings("struct { int a; }", split.source.text);
|
||||
}
|
||||
|
||||
test "parseArgs: struct without a brace is a file, and - is stdin" {
|
||||
const file = parseArgs(testing.allocator, &.{ "struct", "header.h" }).layout;
|
||||
try testing.expectEqualStrings("header.h", file.source.file);
|
||||
|
||||
const stdin = parseArgs(testing.allocator, &.{ "struct", "-" }).layout;
|
||||
try testing.expect(stdin.source == .stdin);
|
||||
|
||||
// Several words and no brace is neither.
|
||||
const neither = parseArgs(testing.allocator, &.{ "struct", "int", "a;" });
|
||||
try testing.expect(neither.output.is_error);
|
||||
try testing.expectEqualStrings(struct_usage, neither.output.text);
|
||||
}
|
||||
|
||||
test "parseArgs: struct --abi picks a profile, in either spelling, anywhere" {
|
||||
for ([_][]const []const u8{
|
||||
&.{ "struct", "--abi", "i386", "struct { int a; }" },
|
||||
&.{ "struct", "struct { int a; }", "--abi=i386" },
|
||||
}) |args| {
|
||||
const parsed = parseArgs(testing.allocator, args).layout;
|
||||
defer testing.allocator.free(parsed.source.text);
|
||||
try testing.expectEqualStrings("i386", parsed.abi.id);
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: struct reports bad arguments with the usage" {
|
||||
try testing.expectEqualStrings(abi_usage, parseArgs(testing.allocator, &.{ "struct", "--abi", "vax", "x.h" }).output.text);
|
||||
try testing.expectEqualStrings(abi_usage, parseArgs(testing.allocator, &.{ "struct", "x.h", "--abi" }).output.text);
|
||||
for ([_][]const []const u8{
|
||||
&.{"struct"},
|
||||
&.{ "struct", "--packed", "x.h" },
|
||||
}) |args| {
|
||||
const out = parseArgs(testing.allocator, args).output;
|
||||
try testing.expect(out.is_error);
|
||||
try testing.expectEqualStrings(struct_usage, out.text);
|
||||
}
|
||||
}
|
||||
|
||||
test "the struct usage lists every profile the engine has" {
|
||||
for (engine.struct_layout.abis) |abi| {
|
||||
try testing.expect(std.mem.indexOf(u8, struct_usage, abi.label) != null);
|
||||
try testing.expect(std.mem.indexOf(u8, abi_usage, abi.id) != null);
|
||||
}
|
||||
try testing.expect(std.mem.indexOf(u8, help_text, "tally struct") != null);
|
||||
}
|
||||
|
||||
test "parseArgs: struct --color takes auto, always or never" {
|
||||
const default = parseArgs(testing.allocator, &.{ "struct", "x.h" }).layout;
|
||||
try testing.expectEqual(ColorChoice.auto, default.color);
|
||||
for ([_]struct { args: []const []const u8, color: ColorChoice }{
|
||||
.{ .args = &.{ "struct", "--color", "never", "x.h" }, .color = .never },
|
||||
.{ .args = &.{ "struct", "x.h", "--color=always" }, .color = .always },
|
||||
.{ .args = &.{ "struct", "--color=auto", "-" }, .color = .auto },
|
||||
}) |case| {
|
||||
try testing.expectEqual(case.color, parseArgs(testing.allocator, case.args).layout.color);
|
||||
}
|
||||
for ([_][]const []const u8{
|
||||
&.{ "struct", "--color", "sometimes", "x.h" },
|
||||
&.{ "struct", "x.h", "--color" },
|
||||
&.{ "struct", "--color=", "x.h" },
|
||||
}) |args| {
|
||||
try testing.expectEqualStrings(color_usage, parseArgs(testing.allocator, args).output.text);
|
||||
}
|
||||
}
|
||||
|
||||
test "ColorChoice: auto colors a terminal unless asked not to" {
|
||||
try testing.expectEqual(format.Paint.ansi, ColorChoice.auto.resolve(true, null, "xterm-256color"));
|
||||
try testing.expectEqual(format.Paint.ansi, ColorChoice.auto.resolve(true, null, null));
|
||||
// Not a terminal: a pipe or a file gets what it got before color existed.
|
||||
try testing.expectEqual(format.Paint.plain, ColorChoice.auto.resolve(false, null, "xterm"));
|
||||
// NO_COLOR set to anything counts; set to nothing does not (no-color.org).
|
||||
try testing.expectEqual(format.Paint.plain, ColorChoice.auto.resolve(true, "1", "xterm"));
|
||||
try testing.expectEqual(format.Paint.ansi, ColorChoice.auto.resolve(true, "", "xterm"));
|
||||
try testing.expectEqual(format.Paint.plain, ColorChoice.auto.resolve(true, null, "dumb"));
|
||||
// The flag overrides all of it, both ways.
|
||||
try testing.expectEqual(format.Paint.ansi, ColorChoice.always.resolve(false, "1", "dumb"));
|
||||
try testing.expectEqual(format.Paint.plain, ColorChoice.never.resolve(true, null, "xterm"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue