aboutsummaryrefslogtreecommitdiff
path: root/lib/std/debug/Dwarf/expression.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2024-08-01 13:40:47 -0700
committerAndrew Kelley <andrew@ziglang.org>2024-08-01 13:56:12 -0700
commite5b46eab3b16a6bf7924ef837fcd6552f430bd58 (patch)
tree8a90dd4323ba9d89305f33013db9d98e13f3e32d /lib/std/debug/Dwarf/expression.zig
parent377274ee9ab270886cce1ceaf5ffeddaefd9c239 (diff)
downloadzig-e5b46eab3b16a6bf7924ef837fcd6552f430bd58.tar.gz
zig-e5b46eab3b16a6bf7924ef837fcd6552f430bd58.zip
std: dwarf namespace reorg
std.debug.Dwarf is the parsing/decoding logic. std.dwarf remains the unopinionated types and bits alone. If you look at this diff you can see a lot less redundancy in namespaces.
Diffstat (limited to 'lib/std/debug/Dwarf/expression.zig')
-rw-r--r--lib/std/debug/Dwarf/expression.zig1638
1 files changed, 1638 insertions, 0 deletions
diff --git a/lib/std/debug/Dwarf/expression.zig b/lib/std/debug/Dwarf/expression.zig
new file mode 100644
index 0000000000..6243ea9717
--- /dev/null
+++ b/lib/std/debug/Dwarf/expression.zig
@@ -0,0 +1,1638 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const leb = std.leb;
+const OP = std.dwarf.OP;
+const abi = std.debug.Dwarf.abi;
+const mem = std.mem;
+const assert = std.debug.assert;
+const native_endian = builtin.cpu.arch.endian();
+
+/// Expressions can be evaluated in different contexts, each requiring its own set of inputs.
+/// Callers should specify all the fields relevant to their context. If a field is required
+/// by the expression and it isn't in the context, error.IncompleteExpressionContext is returned.
+pub const Context = struct {
+ /// The dwarf format of the section this expression is in
+ format: std.dwarf.Format = .@"32",
+ /// If specified, any addresses will pass through before being accessed
+ memory_accessor: ?*std.debug.StackIterator.MemoryAccessor = null,
+ /// The compilation unit this expression relates to, if any
+ compile_unit: ?*const std.debug.Dwarf.CompileUnit = null,
+ /// When evaluating a user-presented expression, this is the address of the object being evaluated
+ object_address: ?*const anyopaque = null,
+ /// .debug_addr section
+ debug_addr: ?[]const u8 = null,
+ /// Thread context
+ thread_context: ?*std.debug.ThreadContext = null,
+ reg_context: ?abi.RegisterContext = null,
+ /// Call frame address, if in a CFI context
+ cfa: ?usize = null,
+ /// This expression is a sub-expression from an OP.entry_value instruction
+ entry_value_context: bool = false,
+};
+
+pub const Options = struct {
+ /// The address size of the target architecture
+ addr_size: u8 = @sizeOf(usize),
+ /// Endianness of the target architecture
+ endian: std.builtin.Endian = builtin.target.cpu.arch.endian(),
+ /// Restrict the stack machine to a subset of opcodes used in call frame instructions
+ call_frame_context: bool = false,
+};
+
+// Explicitly defined to support executing sub-expressions
+pub const Error = error{
+ UnimplementedExpressionCall,
+ UnimplementedOpcode,
+ UnimplementedUserOpcode,
+ UnimplementedTypedComparison,
+ UnimplementedTypeConversion,
+
+ UnknownExpressionOpcode,
+
+ IncompleteExpressionContext,
+
+ InvalidCFAOpcode,
+ InvalidExpression,
+ InvalidFrameBase,
+ InvalidIntegralTypeSize,
+ InvalidRegister,
+ InvalidSubExpression,
+ InvalidTypeLength,
+
+ TruncatedIntegralType,
+} || abi.AbiError || error{ EndOfStream, Overflow, OutOfMemory, DivisionByZero };
+
+/// A stack machine that can decode and run DWARF expressions.
+/// Expressions can be decoded for non-native address size and endianness,
+/// but can only be executed if the current target matches the configuration.
+pub fn StackMachine(comptime options: Options) type {
+ const addr_type = switch (options.addr_size) {
+ 2 => u16,
+ 4 => u32,
+ 8 => u64,
+ else => @compileError("Unsupported address size of " ++ options.addr_size),
+ };
+
+ const addr_type_signed = switch (options.addr_size) {
+ 2 => i16,
+ 4 => i32,
+ 8 => i64,
+ else => @compileError("Unsupported address size of " ++ options.addr_size),
+ };
+
+ return struct {
+ const Self = @This();
+
+ const Operand = union(enum) {
+ generic: addr_type,
+ register: u8,
+ type_size: u8,
+ branch_offset: i16,
+ base_register: struct {
+ base_register: u8,
+ offset: i64,
+ },
+ composite_location: struct {
+ size: u64,
+ offset: i64,
+ },
+ block: []const u8,
+ register_type: struct {
+ register: u8,
+ type_offset: addr_type,
+ },
+ const_type: struct {
+ type_offset: addr_type,
+ value_bytes: []const u8,
+ },
+ deref_type: struct {
+ size: u8,
+ type_offset: addr_type,
+ },
+ };
+
+ const Value = union(enum) {
+ generic: addr_type,
+
+ // Typed value with a maximum size of a register
+ regval_type: struct {
+ // Offset of DW_TAG_base_type DIE
+ type_offset: addr_type,
+ type_size: u8,
+ value: addr_type,
+ },
+
+ // Typed value specified directly in the instruction stream
+ const_type: struct {
+ // Offset of DW_TAG_base_type DIE
+ type_offset: addr_type,
+ // Backed by the instruction stream
+ value_bytes: []const u8,
+ },
+
+ pub fn asIntegral(self: Value) !addr_type {
+ return switch (self) {
+ .generic => |v| v,
+
+ // TODO: For these two prongs, look up the type and assert it's integral?
+ .regval_type => |regval_type| regval_type.value,
+ .const_type => |const_type| {
+ const value: u64 = switch (const_type.value_bytes.len) {
+ 1 => mem.readInt(u8, const_type.value_bytes[0..1], native_endian),
+ 2 => mem.readInt(u16, const_type.value_bytes[0..2], native_endian),
+ 4 => mem.readInt(u32, const_type.value_bytes[0..4], native_endian),
+ 8 => mem.readInt(u64, const_type.value_bytes[0..8], native_endian),
+ else => return error.InvalidIntegralTypeSize,
+ };
+
+ return std.math.cast(addr_type, value) orelse error.TruncatedIntegralType;
+ },
+ };
+ }
+ };
+
+ stack: std.ArrayListUnmanaged(Value) = .{},
+
+ pub fn reset(self: *Self) void {
+ self.stack.clearRetainingCapacity();
+ }
+
+ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
+ self.stack.deinit(allocator);
+ }
+
+ fn generic(value: anytype) Operand {
+ const int_info = @typeInfo(@TypeOf(value)).Int;
+ if (@sizeOf(@TypeOf(value)) > options.addr_size) {
+ return .{ .generic = switch (int_info.signedness) {
+ .signed => @bitCast(@as(addr_type_signed, @truncate(value))),
+ .unsigned => @truncate(value),
+ } };
+ } else {
+ return .{ .generic = switch (int_info.signedness) {
+ .signed => @bitCast(@as(addr_type_signed, @intCast(value))),
+ .unsigned => @intCast(value),
+ } };
+ }
+ }
+
+ pub fn readOperand(stream: *std.io.FixedBufferStream([]const u8), opcode: u8, context: Context) !?Operand {
+ const reader = stream.reader();
+ return switch (opcode) {
+ OP.addr => generic(try reader.readInt(addr_type, options.endian)),
+ OP.call_ref => switch (context.format) {
+ .@"32" => generic(try reader.readInt(u32, options.endian)),
+ .@"64" => generic(try reader.readInt(u64, options.endian)),
+ },
+ OP.const1u,
+ OP.pick,
+ => generic(try reader.readByte()),
+ OP.deref_size,
+ OP.xderef_size,
+ => .{ .type_size = try reader.readByte() },
+ OP.const1s => generic(try reader.readByteSigned()),
+ OP.const2u,
+ OP.call2,
+ => generic(try reader.readInt(u16, options.endian)),
+ OP.call4 => generic(try reader.readInt(u32, options.endian)),
+ OP.const2s => generic(try reader.readInt(i16, options.endian)),
+ OP.bra,
+ OP.skip,
+ => .{ .branch_offset = try reader.readInt(i16, options.endian) },
+ OP.const4u => generic(try reader.readInt(u32, options.endian)),
+ OP.const4s => generic(try reader.readInt(i32, options.endian)),
+ OP.const8u => generic(try reader.readInt(u64, options.endian)),
+ OP.const8s => generic(try reader.readInt(i64, options.endian)),
+ OP.constu,
+ OP.plus_uconst,
+ OP.addrx,
+ OP.constx,
+ OP.convert,
+ OP.reinterpret,
+ => generic(try leb.readUleb128(u64, reader)),
+ OP.consts,
+ OP.fbreg,
+ => generic(try leb.readIleb128(i64, reader)),
+ OP.lit0...OP.lit31 => |n| generic(n - OP.lit0),
+ OP.reg0...OP.reg31 => |n| .{ .register = n - OP.reg0 },
+ OP.breg0...OP.breg31 => |n| .{ .base_register = .{
+ .base_register = n - OP.breg0,
+ .offset = try leb.readIleb128(i64, reader),
+ } },
+ OP.regx => .{ .register = try leb.readUleb128(u8, reader) },
+ OP.bregx => blk: {
+ const base_register = try leb.readUleb128(u8, reader);
+ const offset = try leb.readIleb128(i64, reader);
+ break :blk .{ .base_register = .{
+ .base_register = base_register,
+ .offset = offset,
+ } };
+ },
+ OP.regval_type => blk: {
+ const register = try leb.readUleb128(u8, reader);
+ const type_offset = try leb.readUleb128(addr_type, reader);
+ break :blk .{ .register_type = .{
+ .register = register,
+ .type_offset = type_offset,
+ } };
+ },
+ OP.piece => .{
+ .composite_location = .{
+ .size = try leb.readUleb128(u8, reader),
+ .offset = 0,
+ },
+ },
+ OP.bit_piece => blk: {
+ const size = try leb.readUleb128(u8, reader);
+ const offset = try leb.readIleb128(i64, reader);
+ break :blk .{ .composite_location = .{
+ .size = size,
+ .offset = offset,
+ } };
+ },
+ OP.implicit_value, OP.entry_value => blk: {
+ const size = try leb.readUleb128(u8, reader);
+ if (stream.pos + size > stream.buffer.len) return error.InvalidExpression;
+ const block = stream.buffer[stream.pos..][0..size];
+ stream.pos += size;
+ break :blk .{
+ .block = block,
+ };
+ },
+ OP.const_type => blk: {
+ const type_offset = try leb.readUleb128(addr_type, reader);
+ const size = try reader.readByte();
+ if (stream.pos + size > stream.buffer.len) return error.InvalidExpression;
+ const value_bytes = stream.buffer[stream.pos..][0..size];
+ stream.pos += size;
+ break :blk .{ .const_type = .{
+ .type_offset = type_offset,
+ .value_bytes = value_bytes,
+ } };
+ },
+ OP.deref_type,
+ OP.xderef_type,
+ => .{
+ .deref_type = .{
+ .size = try reader.readByte(),
+ .type_offset = try leb.readUleb128(addr_type, reader),
+ },
+ },
+ OP.lo_user...OP.hi_user => return error.UnimplementedUserOpcode,
+ else => null,
+ };
+ }
+
+ pub fn run(
+ self: *Self,
+ expression: []const u8,
+ allocator: std.mem.Allocator,
+ context: Context,
+ initial_value: ?usize,
+ ) Error!?Value {
+ if (initial_value) |i| try self.stack.append(allocator, .{ .generic = i });
+ var stream = std.io.fixedBufferStream(expression);
+ while (try self.step(&stream, allocator, context)) {}
+ if (self.stack.items.len == 0) return null;
+ return self.stack.items[self.stack.items.len - 1];
+ }
+
+ /// Reads an opcode and its operands from `stream`, then executes it
+ pub fn step(
+ self: *Self,
+ stream: *std.io.FixedBufferStream([]const u8),
+ allocator: std.mem.Allocator,
+ context: Context,
+ ) Error!bool {
+ if (@sizeOf(usize) != @sizeOf(addr_type) or options.endian != comptime builtin.target.cpu.arch.endian())
+ @compileError("Execution of non-native address sizes / endianness is not supported");
+
+ const opcode = try stream.reader().readByte();
+ if (options.call_frame_context and !isOpcodeValidInCFA(opcode)) return error.InvalidCFAOpcode;
+ const operand = try readOperand(stream, opcode, context);
+ switch (opcode) {
+
+ // 2.5.1.1: Literal Encodings
+ OP.lit0...OP.lit31,
+ OP.addr,
+ OP.const1u,
+ OP.const2u,
+ OP.const4u,
+ OP.const8u,
+ OP.const1s,
+ OP.const2s,
+ OP.const4s,
+ OP.const8s,
+ OP.constu,
+ OP.consts,
+ => try self.stack.append(allocator, .{ .generic = operand.?.generic }),
+
+ OP.const_type => {
+ const const_type = operand.?.const_type;
+ try self.stack.append(allocator, .{ .const_type = .{
+ .type_offset = const_type.type_offset,
+ .value_bytes = const_type.value_bytes,
+ } });
+ },
+
+ OP.addrx,
+ OP.constx,
+ => {
+ if (context.compile_unit == null) return error.IncompleteExpressionContext;
+ if (context.debug_addr == null) return error.IncompleteExpressionContext;
+ const debug_addr_index = operand.?.generic;
+ const offset = context.compile_unit.?.addr_base + debug_addr_index;
+ if (offset >= context.debug_addr.?.len) return error.InvalidExpression;
+ const value = mem.readInt(usize, context.debug_addr.?[offset..][0..@sizeOf(usize)], native_endian);
+ try self.stack.append(allocator, .{ .generic = value });
+ },
+
+ // 2.5.1.2: Register Values
+ OP.fbreg => {
+ if (context.compile_unit == null) return error.IncompleteExpressionContext;
+ if (context.compile_unit.?.frame_base == null) return error.IncompleteExpressionContext;
+
+ const offset: i64 = @intCast(operand.?.generic);
+ _ = offset;
+
+ switch (context.compile_unit.?.frame_base.?.*) {
+ .exprloc => {
+ // TODO: Run this expression in a nested stack machine
+ return error.UnimplementedOpcode;
+ },
+ .loclistx => {
+ // TODO: Read value from .debug_loclists
+ return error.UnimplementedOpcode;
+ },
+ .sec_offset => {
+ // TODO: Read value from .debug_loclists
+ return error.UnimplementedOpcode;
+ },
+ else => return error.InvalidFrameBase,
+ }
+ },
+ OP.breg0...OP.breg31,
+ OP.bregx,
+ => {
+ if (context.thread_context == null) return error.IncompleteExpressionContext;
+
+ const base_register = operand.?.base_register;
+ var value: i64 = @intCast(mem.readInt(usize, (try abi.regBytes(
+ context.thread_context.?,
+ base_register.base_register,
+ context.reg_context,
+ ))[0..@sizeOf(usize)], native_endian));
+ value += base_register.offset;
+ try self.stack.append(allocator, .{ .generic = @intCast(value) });
+ },
+ OP.regval_type => {
+ const register_type = operand.?.register_type;
+ const value = mem.readInt(usize, (try abi.regBytes(
+ context.thread_context.?,
+ register_type.register,
+ context.reg_context,
+ ))[0..@sizeOf(usize)], native_endian);
+ try self.stack.append(allocator, .{
+ .regval_type = .{
+ .type_offset = register_type.type_offset,
+ .type_size = @sizeOf(addr_type),
+ .value = value,
+ },
+ });
+ },
+
+ // 2.5.1.3: Stack Operations
+ OP.dup => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ try self.stack.append(allocator, self.stack.items[self.stack.items.len - 1]);
+ },
+ OP.drop => {
+ _ = self.stack.pop();
+ },
+ OP.pick, OP.over => {
+ const stack_index = if (opcode == OP.over) 1 else operand.?.generic;
+ if (stack_index >= self.stack.items.len) return error.InvalidExpression;
+ try self.stack.append(allocator, self.stack.items[self.stack.items.len - 1 - stack_index]);
+ },
+ OP.swap => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ mem.swap(Value, &self.stack.items[self.stack.items.len - 1], &self.stack.items[self.stack.items.len - 2]);
+ },
+ OP.rot => {
+ if (self.stack.items.len < 3) return error.InvalidExpression;
+ const first = self.stack.items[self.stack.items.len - 1];
+ self.stack.items[self.stack.items.len - 1] = self.stack.items[self.stack.items.len - 2];
+ self.stack.items[self.stack.items.len - 2] = self.stack.items[self.stack.items.len - 3];
+ self.stack.items[self.stack.items.len - 3] = first;
+ },
+ OP.deref,
+ OP.xderef,
+ OP.deref_size,
+ OP.xderef_size,
+ OP.deref_type,
+ OP.xderef_type,
+ => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ const addr = try self.stack.items[self.stack.items.len - 1].asIntegral();
+ const addr_space_identifier: ?usize = switch (opcode) {
+ OP.xderef,
+ OP.xderef_size,
+ OP.xderef_type,
+ => blk: {
+ _ = self.stack.pop();
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ break :blk try self.stack.items[self.stack.items.len - 1].asIntegral();
+ },
+ else => null,
+ };
+
+ // Usage of addr_space_identifier in the address calculation is implementation defined.
+ // This code will need to be updated to handle any architectures that utilize this.
+ _ = addr_space_identifier;
+
+ const size = switch (opcode) {
+ OP.deref,
+ OP.xderef,
+ => @sizeOf(addr_type),
+ OP.deref_size,
+ OP.xderef_size,
+ => operand.?.type_size,
+ OP.deref_type,
+ OP.xderef_type,
+ => operand.?.deref_type.size,
+ else => unreachable,
+ };
+
+ if (context.memory_accessor) |memory_accessor| {
+ if (!switch (size) {
+ 1 => memory_accessor.load(u8, addr) != null,
+ 2 => memory_accessor.load(u16, addr) != null,
+ 4 => memory_accessor.load(u32, addr) != null,
+ 8 => memory_accessor.load(u64, addr) != null,
+ else => return error.InvalidExpression,
+ }) return error.InvalidExpression;
+ }
+
+ const value: addr_type = std.math.cast(addr_type, @as(u64, switch (size) {
+ 1 => @as(*const u8, @ptrFromInt(addr)).*,
+ 2 => @as(*const u16, @ptrFromInt(addr)).*,
+ 4 => @as(*const u32, @ptrFromInt(addr)).*,
+ 8 => @as(*const u64, @ptrFromInt(addr)).*,
+ else => return error.InvalidExpression,
+ })) orelse return error.InvalidExpression;
+
+ switch (opcode) {
+ OP.deref_type,
+ OP.xderef_type,
+ => {
+ self.stack.items[self.stack.items.len - 1] = .{
+ .regval_type = .{
+ .type_offset = operand.?.deref_type.type_offset,
+ .type_size = operand.?.deref_type.size,
+ .value = value,
+ },
+ };
+ },
+ else => {
+ self.stack.items[self.stack.items.len - 1] = .{ .generic = value };
+ },
+ }
+ },
+ OP.push_object_address => {
+ // In sub-expressions, `push_object_address` is not meaningful (as per the
+ // spec), so treat it like a nop
+ if (!context.entry_value_context) {
+ if (context.object_address == null) return error.IncompleteExpressionContext;
+ try self.stack.append(allocator, .{ .generic = @intFromPtr(context.object_address.?) });
+ }
+ },
+ OP.form_tls_address => {
+ return error.UnimplementedOpcode;
+ },
+ OP.call_frame_cfa => {
+ if (context.cfa) |cfa| {
+ try self.stack.append(allocator, .{ .generic = cfa });
+ } else return error.IncompleteExpressionContext;
+ },
+
+ // 2.5.1.4: Arithmetic and Logical Operations
+ OP.abs => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ const value: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral());
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = @abs(value),
+ };
+ },
+ OP.@"and" => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = try self.stack.pop().asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = a & try self.stack.items[self.stack.items.len - 1].asIntegral(),
+ };
+ },
+ OP.div => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a: isize = @bitCast(try self.stack.pop().asIntegral());
+ const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral());
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = @bitCast(try std.math.divTrunc(isize, b, a)),
+ };
+ },
+ OP.minus => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const b = try self.stack.pop().asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = try std.math.sub(addr_type, try self.stack.items[self.stack.items.len - 1].asIntegral(), b),
+ };
+ },
+ OP.mod => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a: isize = @bitCast(try self.stack.pop().asIntegral());
+ const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral());
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = @bitCast(@mod(b, a)),
+ };
+ },
+ OP.mul => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a: isize = @bitCast(try self.stack.pop().asIntegral());
+ const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral());
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = @bitCast(@mulWithOverflow(a, b)[0]),
+ };
+ },
+ OP.neg => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = @bitCast(
+ try std.math.negate(
+ @as(isize, @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral())),
+ ),
+ ),
+ };
+ },
+ OP.not => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = ~try self.stack.items[self.stack.items.len - 1].asIntegral(),
+ };
+ },
+ OP.@"or" => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = try self.stack.pop().asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = a | try self.stack.items[self.stack.items.len - 1].asIntegral(),
+ };
+ },
+ OP.plus => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const b = try self.stack.pop().asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = try std.math.add(addr_type, try self.stack.items[self.stack.items.len - 1].asIntegral(), b),
+ };
+ },
+ OP.plus_uconst => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ const constant = operand.?.generic;
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = try std.math.add(addr_type, try self.stack.items[self.stack.items.len - 1].asIntegral(), constant),
+ };
+ },
+ OP.shl => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = try self.stack.pop().asIntegral();
+ const b = try self.stack.items[self.stack.items.len - 1].asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = std.math.shl(usize, b, a),
+ };
+ },
+ OP.shr => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = try self.stack.pop().asIntegral();
+ const b = try self.stack.items[self.stack.items.len - 1].asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = std.math.shr(usize, b, a),
+ };
+ },
+ OP.shra => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = try self.stack.pop().asIntegral();
+ const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral());
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = @bitCast(std.math.shr(isize, b, a)),
+ };
+ },
+ OP.xor => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = try self.stack.pop().asIntegral();
+ self.stack.items[self.stack.items.len - 1] = .{
+ .generic = a ^ try self.stack.items[self.stack.items.len - 1].asIntegral(),
+ };
+ },
+
+ // 2.5.1.5: Control Flow Operations
+ OP.le,
+ OP.ge,
+ OP.eq,
+ OP.lt,
+ OP.gt,
+ OP.ne,
+ => {
+ if (self.stack.items.len < 2) return error.InvalidExpression;
+ const a = self.stack.pop();
+ const b = self.stack.items[self.stack.items.len - 1];
+
+ if (a == .generic and b == .generic) {
+ const a_int: isize = @bitCast(a.asIntegral() catch unreachable);
+ const b_int: isize = @bitCast(b.asIntegral() catch unreachable);
+ const result = @intFromBool(switch (opcode) {
+ OP.le => b_int <= a_int,
+ OP.ge => b_int >= a_int,
+ OP.eq => b_int == a_int,
+ OP.lt => b_int < a_int,
+ OP.gt => b_int > a_int,
+ OP.ne => b_int != a_int,
+ else => unreachable,
+ });
+
+ self.stack.items[self.stack.items.len - 1] = .{ .generic = result };
+ } else {
+ // TODO: Load the types referenced by these values, find their comparison operator, and run it
+ return error.UnimplementedTypedComparison;
+ }
+ },
+ OP.skip, OP.bra => {
+ const branch_offset = operand.?.branch_offset;
+ const condition = if (opcode == OP.bra) blk: {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ break :blk try self.stack.pop().asIntegral() != 0;
+ } else true;
+
+ if (condition) {
+ const new_pos = std.math.cast(
+ usize,
+ try std.math.add(isize, @as(isize, @intCast(stream.pos)), branch_offset),
+ ) orelse return error.InvalidExpression;
+
+ if (new_pos < 0 or new_pos > stream.buffer.len) return error.InvalidExpression;
+ stream.pos = new_pos;
+ }
+ },
+ OP.call2,
+ OP.call4,
+ OP.call_ref,
+ => {
+ const debug_info_offset = operand.?.generic;
+ _ = debug_info_offset;
+
+ // TODO: Load a DIE entry at debug_info_offset in a .debug_info section (the spec says that it
+ // can be in a separate exe / shared object from the one containing this expression).
+ // Transfer control to the DW_AT_location attribute, with the current stack as input.
+
+ return error.UnimplementedExpressionCall;
+ },
+
+ // 2.5.1.6: Type Conversions
+ OP.convert => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ const type_offset = operand.?.generic;
+
+ // TODO: Load the DW_TAG_base_type entries in context.compile_unit and verify both types are the same size
+ const value = self.stack.items[self.stack.items.len - 1];
+ if (type_offset == 0) {
+ self.stack.items[self.stack.items.len - 1] = .{ .generic = try value.asIntegral() };
+ } else {
+ // TODO: Load the DW_TAG_base_type entry in context.compile_unit, find a conversion operator
+ // from the old type to the new type, run it.
+ return error.UnimplementedTypeConversion;
+ }
+ },
+ OP.reinterpret => {
+ if (self.stack.items.len == 0) return error.InvalidExpression;
+ const type_offset = operand.?.generic;
+
+ // TODO: Load the DW_TAG_base_type entries in context.compile_unit and verify both types are the same size
+ const value = self.stack.items[self.stack.items.len - 1];
+ if (type_offset == 0) {
+ self.stack.items[self.stack.items.len - 1] = .{ .generic = try value.asIntegral() };
+ } else {
+ self.stack.items[self.stack.items.len - 1] = switch (value) {
+ .generic => |v| .{
+ .regval_type = .{
+ .type_offset = type_offset,
+ .type_size = @sizeOf(addr_type),
+ .value = v,
+ },
+ },
+ .regval_type => |r| .{
+ .regval_type = .{
+ .type_offset = type_offset,
+ .type_size = r.type_size,
+ .value = r.value,
+ },
+ },
+ .const_type => |c| .{
+ .const_type = .{
+ .type_offset = type_offset,
+ .value_bytes = c.value_bytes,
+ },
+ },
+ };
+ }
+ },
+
+ // 2.5.1.7: Special Operations
+ OP.nop => {},
+ OP.entry_value => {
+ const block = operand.?.block;
+ if (block.len == 0) return error.InvalidSubExpression;
+
+ // TODO: The spec states that this sub-expression needs to observe the state (ie. registers)
+ // as it was upon entering the current subprogram. If this isn't being called at the
+ // end of a frame unwind operation, an additional ThreadContext with this state will be needed.
+
+ if (isOpcodeRegisterLocation(block[0])) {
+ if (context.thread_context == null) return error.IncompleteExpressionContext;
+
+ var block_stream = std.io.fixedBufferStream(block);
+ const register = (try readOperand(&block_stream, block[0], context)).?.register;
+ const value = mem.readInt(usize, (try abi.regBytes(context.thread_context.?, register, context.reg_context))[0..@sizeOf(usize)], native_endian);
+ try self.stack.append(allocator, .{ .generic = value });
+ } else {
+ var stack_machine: Self = .{};
+ defer stack_machine.deinit(allocator);
+
+ var sub_context = context;
+ sub_context.entry_value_context = true;
+ const result = try stack_machine.run(block, allocator, sub_context, null);
+ try self.stack.append(allocator, result orelse return error.InvalidSubExpression);
+ }
+ },
+
+ // These have already been handled by readOperand
+ OP.lo_user...OP.hi_user => unreachable,
+ else => {
+ //std.debug.print("Unknown DWARF expression opcode: {x}\n", .{opcode});
+ return error.UnknownExpressionOpcode;
+ },
+ }
+
+ return stream.pos < stream.buffer.len;
+ }
+ };
+}
+
+pub fn Builder(comptime options: Options) type {
+ const addr_type = switch (options.addr_size) {
+ 2 => u16,
+ 4 => u32,
+ 8 => u64,
+ else => @compileError("Unsupported address size of " ++ options.addr_size),
+ };
+
+ return struct {
+ /// Zero-operand instructions
+ pub fn writeOpcode(writer: anytype, comptime opcode: u8) !void {
+ if (options.call_frame_context and !comptime isOpcodeValidInCFA(opcode)) return error.InvalidCFAOpcode;
+ switch (opcode) {
+ OP.dup,
+ OP.drop,
+ OP.over,
+ OP.swap,
+ OP.rot,
+ OP.deref,
+ OP.xderef,
+ OP.push_object_address,
+ OP.form_tls_address,
+ OP.call_frame_cfa,
+ OP.abs,
+ OP.@"and",
+ OP.div,
+ OP.minus,
+ OP.mod,
+ OP.mul,
+ OP.neg,
+ OP.not,
+ OP.@"or",
+ OP.plus,
+ OP.shl,
+ OP.shr,
+ OP.shra,
+ OP.xor,
+ OP.le,
+ OP.ge,
+ OP.eq,
+ OP.lt,
+ OP.gt,
+ OP.ne,
+ OP.nop,
+ OP.stack_value,
+ => try writer.writeByte(opcode),
+ else => @compileError("This opcode requires operands, use `write<Opcode>()` instead"),
+ }
+ }
+
+ // 2.5.1.1: Literal Encodings
+ pub fn writeLiteral(writer: anytype, literal: u8) !void {
+ switch (literal) {
+ 0...31 => |n| try writer.writeByte(n + OP.lit0),
+ else => return error.InvalidLiteral,
+ }
+ }
+
+ pub fn writeConst(writer: anytype, comptime T: type, value: T) !void {
+ if (@typeInfo(T) != .Int) @compileError("Constants must be integers");
+
+ switch (T) {
+ u8, i8, u16, i16, u32, i32, u64, i64 => {
+ try writer.writeByte(switch (T) {
+ u8 => OP.const1u,
+ i8 => OP.const1s,
+ u16 => OP.const2u,
+ i16 => OP.const2s,
+ u32 => OP.const4u,
+ i32 => OP.const4s,
+ u64 => OP.const8u,
+ i64 => OP.const8s,
+ else => unreachable,
+ });
+
+ try writer.writeInt(T, value, options.endian);
+ },
+ else => switch (@typeInfo(T).Int.signedness) {
+ .unsigned => {
+ try writer.writeByte(OP.constu);
+ try leb.writeUleb128(writer, value);
+ },
+ .signed => {
+ try writer.writeByte(OP.consts);
+ try leb.writeIleb128(writer, value);
+ },
+ },
+ }
+ }
+
+ pub fn writeConstx(writer: anytype, debug_addr_offset: anytype) !void {
+ try writer.writeByte(OP.constx);
+ try leb.writeUleb128(writer, debug_addr_offset);
+ }
+
+ pub fn writeConstType(writer: anytype, die_offset: anytype, value_bytes: []const u8) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ if (value_bytes.len > 0xff) return error.InvalidTypeLength;
+ try writer.writeByte(OP.const_type);
+ try leb.writeUleb128(writer, die_offset);
+ try writer.writeByte(@intCast(value_bytes.len));
+ try writer.writeAll(value_bytes);
+ }
+
+ pub fn writeAddr(writer: anytype, value: addr_type) !void {
+ try writer.writeByte(OP.addr);
+ try writer.writeInt(addr_type, value, options.endian);
+ }
+
+ pub fn writeAddrx(writer: anytype, debug_addr_offset: anytype) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ try writer.writeByte(OP.addrx);
+ try leb.writeUleb128(writer, debug_addr_offset);
+ }
+
+ // 2.5.1.2: Register Values
+ pub fn writeFbreg(writer: anytype, offset: anytype) !void {
+ try writer.writeByte(OP.fbreg);
+ try leb.writeIleb128(writer, offset);
+ }
+
+ pub fn writeBreg(writer: anytype, register: u8, offset: anytype) !void {
+ if (register > 31) return error.InvalidRegister;
+ try writer.writeByte(OP.breg0 + register);
+ try leb.writeIleb128(writer, offset);
+ }
+
+ pub fn writeBregx(writer: anytype, register: anytype, offset: anytype) !void {
+ try writer.writeByte(OP.bregx);
+ try leb.writeUleb128(writer, register);
+ try leb.writeIleb128(writer, offset);
+ }
+
+ pub fn writeRegvalType(writer: anytype, register: anytype, offset: anytype) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ try writer.writeByte(OP.regval_type);
+ try leb.writeUleb128(writer, register);
+ try leb.writeUleb128(writer, offset);
+ }
+
+ // 2.5.1.3: Stack Operations
+ pub fn writePick(writer: anytype, index: u8) !void {
+ try writer.writeByte(OP.pick);
+ try writer.writeByte(index);
+ }
+
+ pub fn writeDerefSize(writer: anytype, size: u8) !void {
+ try writer.writeByte(OP.deref_size);
+ try writer.writeByte(size);
+ }
+
+ pub fn writeXDerefSize(writer: anytype, size: u8) !void {
+ try writer.writeByte(OP.xderef_size);
+ try writer.writeByte(size);
+ }
+
+ pub fn writeDerefType(writer: anytype, size: u8, die_offset: anytype) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ try writer.writeByte(OP.deref_type);
+ try writer.writeByte(size);
+ try leb.writeUleb128(writer, die_offset);
+ }
+
+ pub fn writeXDerefType(writer: anytype, size: u8, die_offset: anytype) !void {
+ try writer.writeByte(OP.xderef_type);
+ try writer.writeByte(size);
+ try leb.writeUleb128(writer, die_offset);
+ }
+
+ // 2.5.1.4: Arithmetic and Logical Operations
+
+ pub fn writePlusUconst(writer: anytype, uint_value: anytype) !void {
+ try writer.writeByte(OP.plus_uconst);
+ try leb.writeUleb128(writer, uint_value);
+ }
+
+ // 2.5.1.5: Control Flow Operations
+
+ pub fn writeSkip(writer: anytype, offset: i16) !void {
+ try writer.writeByte(OP.skip);
+ try writer.writeInt(i16, offset, options.endian);
+ }
+
+ pub fn writeBra(writer: anytype, offset: i16) !void {
+ try writer.writeByte(OP.bra);
+ try writer.writeInt(i16, offset, options.endian);
+ }
+
+ pub fn writeCall(writer: anytype, comptime T: type, offset: T) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ switch (T) {
+ u16 => try writer.writeByte(OP.call2),
+ u32 => try writer.writeByte(OP.call4),
+ else => @compileError("Call operand must be a 2 or 4 byte offset"),
+ }
+
+ try writer.writeInt(T, offset, options.endian);
+ }
+
+ pub fn writeCallRef(writer: anytype, comptime is_64: bool, value: if (is_64) u64 else u32) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ try writer.writeByte(OP.call_ref);
+ try writer.writeInt(if (is_64) u64 else u32, value, options.endian);
+ }
+
+ pub fn writeConvert(writer: anytype, die_offset: anytype) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ try writer.writeByte(OP.convert);
+ try leb.writeUleb128(writer, die_offset);
+ }
+
+ pub fn writeReinterpret(writer: anytype, die_offset: anytype) !void {
+ if (options.call_frame_context) return error.InvalidCFAOpcode;
+ try writer.writeByte(OP.reinterpret);
+ try leb.writeUleb128(writer, die_offset);
+ }
+
+ // 2.5.1.7: Special Operations
+
+ pub fn writeEntryValue(writer: anytype, expression: []const u8) !void {
+ try writer.writeByte(OP.entry_value);
+ try leb.writeUleb128(writer, expression.len);
+ try writer.writeAll(expression);
+ }
+
+ // 2.6: Location Descriptions
+ pub fn writeReg(writer: anytype, register: u8) !void {
+ try writer.writeByte(OP.reg0 + register);
+ }
+
+ pub fn writeRegx(writer: anytype, register: anytype) !void {
+ try writer.writeByte(OP.regx);
+ try leb.writeUleb128(writer, register);
+ }
+
+ pub fn writeImplicitValue(writer: anytype, value_bytes: []const u8) !void {
+ try writer.writeByte(OP.implicit_value);
+ try leb.writeUleb128(writer, value_bytes.len);
+ try writer.writeAll(value_bytes);
+ }
+ };
+}
+
+// Certain opcodes are not allowed in a CFA context, see 6.4.2
+fn isOpcodeValidInCFA(opcode: u8) bool {
+ return switch (opcode) {
+ OP.addrx,
+ OP.call2,
+ OP.call4,
+ OP.call_ref,
+ OP.const_type,
+ OP.constx,
+ OP.convert,
+ OP.deref_type,
+ OP.regval_type,
+ OP.reinterpret,
+ OP.push_object_address,
+ OP.call_frame_cfa,
+ => false,
+ else => true,
+ };
+}
+
+fn isOpcodeRegisterLocation(opcode: u8) bool {
+ return switch (opcode) {
+ OP.reg0...OP.reg31, OP.regx => true,
+ else => false,
+ };
+}
+
+const testing = std.testing;
+test "DWARF expressions" {
+ const allocator = std.testing.allocator;
+
+ const options = Options{};
+ var stack_machine = StackMachine(options){};
+ defer stack_machine.deinit(allocator);
+
+ const b = Builder(options);
+
+ var program = std.ArrayList(u8).init(allocator);
+ defer program.deinit();
+
+ const writer = program.writer();
+
+ // Literals
+ {
+ const context = Context{};
+ for (0..32) |i| {
+ try b.writeLiteral(writer, @intCast(i));
+ }
+
+ _ = try stack_machine.run(program.items, allocator, context, 0);
+
+ for (0..32) |i| {
+ const expected = 31 - i;
+ try testing.expectEqual(expected, stack_machine.stack.popOrNull().?.generic);
+ }
+ }
+
+ // Constants
+ {
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+
+ const input = [_]comptime_int{
+ 1,
+ -1,
+ @as(usize, @truncate(0x0fff)),
+ @as(isize, @truncate(-0x0fff)),
+ @as(usize, @truncate(0x0fffffff)),
+ @as(isize, @truncate(-0x0fffffff)),
+ @as(usize, @truncate(0x0fffffffffffffff)),
+ @as(isize, @truncate(-0x0fffffffffffffff)),
+ @as(usize, @truncate(0x8000000)),
+ @as(isize, @truncate(-0x8000000)),
+ @as(usize, @truncate(0x12345678_12345678)),
+ @as(usize, @truncate(0xffffffff_ffffffff)),
+ @as(usize, @truncate(0xeeeeeeee_eeeeeeee)),
+ };
+
+ try b.writeConst(writer, u8, input[0]);
+ try b.writeConst(writer, i8, input[1]);
+ try b.writeConst(writer, u16, input[2]);
+ try b.writeConst(writer, i16, input[3]);
+ try b.writeConst(writer, u32, input[4]);
+ try b.writeConst(writer, i32, input[5]);
+ try b.writeConst(writer, u64, input[6]);
+ try b.writeConst(writer, i64, input[7]);
+ try b.writeConst(writer, u28, input[8]);
+ try b.writeConst(writer, i28, input[9]);
+ try b.writeAddr(writer, input[10]);
+
+ var mock_compile_unit: std.debug.Dwarf.CompileUnit = undefined;
+ mock_compile_unit.addr_base = 1;
+
+ var mock_debug_addr = std.ArrayList(u8).init(allocator);
+ defer mock_debug_addr.deinit();
+
+ try mock_debug_addr.writer().writeInt(u16, 0, native_endian);
+ try mock_debug_addr.writer().writeInt(usize, input[11], native_endian);
+ try mock_debug_addr.writer().writeInt(usize, input[12], native_endian);
+
+ const context = Context{
+ .compile_unit = &mock_compile_unit,
+ .debug_addr = mock_debug_addr.items,
+ };
+
+ try b.writeConstx(writer, @as(usize, 1));
+ try b.writeAddrx(writer, @as(usize, 1 + @sizeOf(usize)));
+
+ const die_offset: usize = @truncate(0xaabbccdd);
+ const type_bytes: []const u8 = &.{ 1, 2, 3, 4 };
+ try b.writeConstType(writer, die_offset, type_bytes);
+
+ _ = try stack_machine.run(program.items, allocator, context, 0);
+
+ const const_type = stack_machine.stack.popOrNull().?.const_type;
+ try testing.expectEqual(die_offset, const_type.type_offset);
+ try testing.expectEqualSlices(u8, type_bytes, const_type.value_bytes);
+
+ const expected = .{
+ .{ usize, input[12], usize },
+ .{ usize, input[11], usize },
+ .{ usize, input[10], usize },
+ .{ isize, input[9], isize },
+ .{ usize, input[8], usize },
+ .{ isize, input[7], isize },
+ .{ usize, input[6], usize },
+ .{ isize, input[5], isize },
+ .{ usize, input[4], usize },
+ .{ isize, input[3], isize },
+ .{ usize, input[2], usize },
+ .{ isize, input[1], isize },
+ .{ usize, input[0], usize },
+ };
+
+ inline for (expected) |e| {
+ try testing.expectEqual(@as(e[0], e[1]), @as(e[2], @bitCast(stack_machine.stack.popOrNull().?.generic)));
+ }
+ }
+
+ // Register values
+ if (@sizeOf(std.debug.ThreadContext) != 0) {
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+
+ const reg_context = abi.RegisterContext{
+ .eh_frame = true,
+ .is_macho = builtin.os.tag == .macos,
+ };
+ var thread_context: std.debug.ThreadContext = undefined;
+ std.debug.relocateContext(&thread_context);
+ const context = Context{
+ .thread_context = &thread_context,
+ .reg_context = reg_context,
+ };
+
+ // Only test register operations on arch / os that have them implemented
+ if (abi.regBytes(&thread_context, 0, reg_context)) |reg_bytes| {
+
+ // TODO: Test fbreg (once implemented): mock a DIE and point compile_unit.frame_base at it
+
+ mem.writeInt(usize, reg_bytes[0..@sizeOf(usize)], 0xee, native_endian);
+ (try abi.regValueNative(usize, &thread_context, abi.fpRegNum(reg_context), reg_context)).* = 1;
+ (try abi.regValueNative(usize, &thread_context, abi.spRegNum(reg_context), reg_context)).* = 2;
+ (try abi.regValueNative(usize, &thread_context, abi.ipRegNum(), reg_context)).* = 3;
+
+ try b.writeBreg(writer, abi.fpRegNum(reg_context), @as(usize, 100));
+ try b.writeBreg(writer, abi.spRegNum(reg_context), @as(usize, 200));
+ try b.writeBregx(writer, abi.ipRegNum(), @as(usize, 300));
+ try b.writeRegvalType(writer, @as(u8, 0), @as(usize, 400));
+
+ _ = try stack_machine.run(program.items, allocator, context, 0);
+
+ const regval_type = stack_machine.stack.popOrNull().?.regval_type;
+ try testing.expectEqual(@as(usize, 400), regval_type.type_offset);
+ try testing.expectEqual(@as(u8, @sizeOf(usize)), regval_type.type_size);
+ try testing.expectEqual(@as(usize, 0xee), regval_type.value);
+
+ try testing.expectEqual(@as(usize, 303), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 202), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 101), stack_machine.stack.popOrNull().?.generic);
+ } else |err| {
+ switch (err) {
+ error.UnimplementedArch,
+ error.UnimplementedOs,
+ error.ThreadContextNotSupported,
+ => {},
+ else => return err,
+ }
+ }
+ }
+
+ // Stack operations
+ {
+ var context = Context{};
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u8, 1);
+ try b.writeOpcode(writer, OP.dup);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 1), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 1), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u8, 1);
+ try b.writeOpcode(writer, OP.drop);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expect(stack_machine.stack.popOrNull() == null);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u8, 4);
+ try b.writeConst(writer, u8, 5);
+ try b.writeConst(writer, u8, 6);
+ try b.writePick(writer, 2);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 4), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u8, 4);
+ try b.writeConst(writer, u8, 5);
+ try b.writeConst(writer, u8, 6);
+ try b.writeOpcode(writer, OP.over);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u8, 5);
+ try b.writeConst(writer, u8, 6);
+ try b.writeOpcode(writer, OP.swap);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 6), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u8, 4);
+ try b.writeConst(writer, u8, 5);
+ try b.writeConst(writer, u8, 6);
+ try b.writeOpcode(writer, OP.rot);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 4), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 6), stack_machine.stack.popOrNull().?.generic);
+
+ const deref_target: usize = @truncate(0xffeeffee_ffeeffee);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeAddr(writer, @intFromPtr(&deref_target));
+ try b.writeOpcode(writer, OP.deref);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(deref_target, stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeLiteral(writer, 0);
+ try b.writeAddr(writer, @intFromPtr(&deref_target));
+ try b.writeOpcode(writer, OP.xderef);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(deref_target, stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeAddr(writer, @intFromPtr(&deref_target));
+ try b.writeDerefSize(writer, 1);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeLiteral(writer, 0);
+ try b.writeAddr(writer, @intFromPtr(&deref_target));
+ try b.writeXDerefSize(writer, 1);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), stack_machine.stack.popOrNull().?.generic);
+
+ const type_offset: usize = @truncate(0xaabbaabb_aabbaabb);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeAddr(writer, @intFromPtr(&deref_target));
+ try b.writeDerefType(writer, 1, type_offset);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ const deref_type = stack_machine.stack.popOrNull().?.regval_type;
+ try testing.expectEqual(type_offset, deref_type.type_offset);
+ try testing.expectEqual(@as(u8, 1), deref_type.type_size);
+ try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), deref_type.value);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeLiteral(writer, 0);
+ try b.writeAddr(writer, @intFromPtr(&deref_target));
+ try b.writeXDerefType(writer, 1, type_offset);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ const xderef_type = stack_machine.stack.popOrNull().?.regval_type;
+ try testing.expectEqual(type_offset, xderef_type.type_offset);
+ try testing.expectEqual(@as(u8, 1), xderef_type.type_size);
+ try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), xderef_type.value);
+
+ context.object_address = &deref_target;
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeOpcode(writer, OP.push_object_address);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, @intFromPtr(context.object_address.?)), stack_machine.stack.popOrNull().?.generic);
+
+ // TODO: Test OP.form_tls_address
+
+ context.cfa = @truncate(0xccddccdd_ccddccdd);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeOpcode(writer, OP.call_frame_cfa);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(context.cfa.?, stack_machine.stack.popOrNull().?.generic);
+ }
+
+ // Arithmetic and Logical Operations
+ {
+ const context = Context{};
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, i16, -4096);
+ try b.writeOpcode(writer, OP.abs);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 4096), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xff0f);
+ try b.writeConst(writer, u16, 0xf0ff);
+ try b.writeOpcode(writer, OP.@"and");
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0xf00f), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, i16, -404);
+ try b.writeConst(writer, i16, 100);
+ try b.writeOpcode(writer, OP.div);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(isize, -404 / 100), @as(isize, @bitCast(stack_machine.stack.popOrNull().?.generic)));
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 200);
+ try b.writeConst(writer, u16, 50);
+ try b.writeOpcode(writer, OP.minus);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 150), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 123);
+ try b.writeConst(writer, u16, 100);
+ try b.writeOpcode(writer, OP.mod);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 23), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xff);
+ try b.writeConst(writer, u16, 0xee);
+ try b.writeOpcode(writer, OP.mul);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0xed12), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 5);
+ try b.writeOpcode(writer, OP.neg);
+ try b.writeConst(writer, i16, -6);
+ try b.writeOpcode(writer, OP.neg);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 6), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(isize, -5), @as(isize, @bitCast(stack_machine.stack.popOrNull().?.generic)));
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xff0f);
+ try b.writeOpcode(writer, OP.not);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(~@as(usize, 0xff0f), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xff0f);
+ try b.writeConst(writer, u16, 0xf0ff);
+ try b.writeOpcode(writer, OP.@"or");
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0xffff), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, i16, 402);
+ try b.writeConst(writer, i16, 100);
+ try b.writeOpcode(writer, OP.plus);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 502), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 4096);
+ try b.writePlusUconst(writer, @as(usize, 8192));
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 4096 + 8192), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xfff);
+ try b.writeConst(writer, u16, 1);
+ try b.writeOpcode(writer, OP.shl);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0xfff << 1), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xfff);
+ try b.writeConst(writer, u16, 1);
+ try b.writeOpcode(writer, OP.shr);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0xfff >> 1), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xfff);
+ try b.writeConst(writer, u16, 1);
+ try b.writeOpcode(writer, OP.shr);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, @bitCast(@as(isize, 0xfff) >> 1)), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConst(writer, u16, 0xf0ff);
+ try b.writeConst(writer, u16, 0xff0f);
+ try b.writeOpcode(writer, OP.xor);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0x0ff0), stack_machine.stack.popOrNull().?.generic);
+ }
+
+ // Control Flow Operations
+ {
+ const context = Context{};
+ const expected = .{
+ .{ OP.le, 1, 1, 0 },
+ .{ OP.ge, 1, 0, 1 },
+ .{ OP.eq, 1, 0, 0 },
+ .{ OP.lt, 0, 1, 0 },
+ .{ OP.gt, 0, 0, 1 },
+ .{ OP.ne, 0, 1, 1 },
+ };
+
+ inline for (expected) |e| {
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+
+ try b.writeConst(writer, u16, 0);
+ try b.writeConst(writer, u16, 0);
+ try b.writeOpcode(writer, e[0]);
+ try b.writeConst(writer, u16, 0);
+ try b.writeConst(writer, u16, 1);
+ try b.writeOpcode(writer, e[0]);
+ try b.writeConst(writer, u16, 1);
+ try b.writeConst(writer, u16, 0);
+ try b.writeOpcode(writer, e[0]);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, e[3]), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, e[2]), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, e[1]), stack_machine.stack.popOrNull().?.generic);
+ }
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeLiteral(writer, 2);
+ try b.writeSkip(writer, 1);
+ try b.writeLiteral(writer, 3);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 2), stack_machine.stack.popOrNull().?.generic);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeLiteral(writer, 2);
+ try b.writeBra(writer, 1);
+ try b.writeLiteral(writer, 3);
+ try b.writeLiteral(writer, 0);
+ try b.writeBra(writer, 1);
+ try b.writeLiteral(writer, 4);
+ try b.writeLiteral(writer, 5);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic);
+ try testing.expectEqual(@as(usize, 4), stack_machine.stack.popOrNull().?.generic);
+ try testing.expect(stack_machine.stack.popOrNull() == null);
+
+ // TODO: Test call2, call4, call_ref once implemented
+
+ }
+
+ // Type conversions
+ {
+ const context = Context{};
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+
+ // TODO: Test typed OP.convert once implemented
+
+ const value: usize = @truncate(0xffeeffee_ffeeffee);
+ var value_bytes: [options.addr_size]u8 = undefined;
+ mem.writeInt(usize, &value_bytes, value, native_endian);
+
+ // Convert to generic type
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConstType(writer, @as(usize, 0), &value_bytes);
+ try b.writeConvert(writer, @as(usize, 0));
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(value, stack_machine.stack.popOrNull().?.generic);
+
+ // Reinterpret to generic type
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConstType(writer, @as(usize, 0), &value_bytes);
+ try b.writeReinterpret(writer, @as(usize, 0));
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(value, stack_machine.stack.popOrNull().?.generic);
+
+ // Reinterpret to new type
+ const die_offset: usize = 0xffee;
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeConstType(writer, @as(usize, 0), &value_bytes);
+ try b.writeReinterpret(writer, die_offset);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ const const_type = stack_machine.stack.popOrNull().?.const_type;
+ try testing.expectEqual(die_offset, const_type.type_offset);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeLiteral(writer, 0);
+ try b.writeReinterpret(writer, die_offset);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ const regval_type = stack_machine.stack.popOrNull().?.regval_type;
+ try testing.expectEqual(die_offset, regval_type.type_offset);
+ }
+
+ // Special operations
+ {
+ var context = Context{};
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeOpcode(writer, OP.nop);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expect(stack_machine.stack.popOrNull() == null);
+
+ // Sub-expression
+ {
+ var sub_program = std.ArrayList(u8).init(allocator);
+ defer sub_program.deinit();
+ const sub_writer = sub_program.writer();
+ try b.writeLiteral(sub_writer, 3);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeEntryValue(writer, sub_program.items);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 3), stack_machine.stack.popOrNull().?.generic);
+ }
+
+ // Register location description
+ const reg_context = abi.RegisterContext{
+ .eh_frame = true,
+ .is_macho = builtin.os.tag == .macos,
+ };
+ var thread_context: std.debug.ThreadContext = undefined;
+ std.debug.relocateContext(&thread_context);
+ context = Context{
+ .thread_context = &thread_context,
+ .reg_context = reg_context,
+ };
+
+ if (abi.regBytes(&thread_context, 0, reg_context)) |reg_bytes| {
+ mem.writeInt(usize, reg_bytes[0..@sizeOf(usize)], 0xee, native_endian);
+
+ var sub_program = std.ArrayList(u8).init(allocator);
+ defer sub_program.deinit();
+ const sub_writer = sub_program.writer();
+ try b.writeReg(sub_writer, 0);
+
+ stack_machine.reset();
+ program.clearRetainingCapacity();
+ try b.writeEntryValue(writer, sub_program.items);
+ _ = try stack_machine.run(program.items, allocator, context, null);
+ try testing.expectEqual(@as(usize, 0xee), stack_machine.stack.popOrNull().?.generic);
+ } else |err| {
+ switch (err) {
+ error.UnimplementedArch,
+ error.UnimplementedOs,
+ error.ThreadContextNotSupported,
+ => {},
+ else => return err,
+ }
+ }
+ }
+}