//! AML (ACPI Machine Language) — the bytecode in the DSDT and SSDTs that describes //! the parts of the machine the static tables don't. //! //! This module has two stages. `parser.zig` walks the entire byte stream and //! records every named object into a namespace tree (`namespace.zig`), capturing //! method bodies and field/region layout. `interp.zig` then *evaluates* control //! methods on demand — running operators, control flow, and OperationRegion field //! access — so callers can resolve device status (`_STA`), current resource //! settings (`_CRS`), sleep states (`_Sx`), and the like against the live namespace. const std = @import("std"); const op = @import("opcodes.zig"); const parser = @import("parser.zig"); const namespace = @import("namespace.zig"); const interp = @import("interp.zig"); pub const Namespace = namespace.Namespace; pub const Node = namespace.Node; pub const NodeKind = namespace.NodeKind; /// The AML evaluator: interprets control methods (and reads Names/Fields) far /// enough for device discovery. See `interp.zig`. pub const Interp = interp.Interp; pub const Object = interp.Object; pub const EvalHal = interp.Hal; /// The SLP_TYP values written to PM1a/PM1b control to enter a sleep state. pub const SleepType = struct { slp_typ_a: u8, slp_typ_b: u8, }; pub const ParseResult = struct { namespace: Namespace, /// Bytes the parser consumed across all blocks... consumed: usize, /// ...out of this many. A clean full traversal has `consumed == total`. total: usize, }; /// Parse the given AML blocks (DSDT first, then SSDTs) into one namespace. Later /// blocks extend the namespace built by earlier ones, exactly as ACPI intends. pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseResult { var ns = try Namespace.init(allocator); var consumed: usize = 0; var total: usize = 0; for (blocks) |block| { var p = parser.Parser.init(block, &ns); consumed += p.parseAll(); total += block.len; } return .{ .namespace = ns, .consumed = consumed, .total = total }; } /// Look up the `\_S{state}` sleep package in a parsed namespace and return its /// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent. pub fn sleepState(ns: *Namespace, state: u8) ?SleepType { const seg = [4]u8{ '_', 'S', '0' + state, '_' }; const node = ns.resolve(ns.root, false, 0, &.{seg}) orelse return null; if (node.kind != .name) return null; return parseSleepPackage(node.value); } /// Decode a `Package(){ SLP_TYPa, SLP_TYPb, ... }` from the raw AML of a Name's /// value. Returns the first two elements as bytes (missing elements default to 0). fn parseSleepPackage(value: []const u8) ?SleepType { if (value.len == 0 or value[0] != op.package_op) return null; var p: usize = 1; p += pkgLengthSize(value, p) orelse return null; if (p >= value.len) return null; const num_elements = value[p]; p += 1; const a: u8 = if (num_elements >= 1) @truncate(readInteger(value, &p) orelse 0) else 0; const b: u8 = if (num_elements >= 2) @truncate(readInteger(value, &p) orelse 0) else 0; return .{ .slp_typ_a = a, .slp_typ_b = b }; } /// Bytes a PkgLength field occupies at `p` (we only need to step over it here). fn pkgLengthSize(bytes: []const u8, p: usize) ?usize { if (p >= bytes.len) return null; const follow: usize = bytes[p] >> 6; if (p + 1 + follow > bytes.len) return null; return 1 + follow; } /// Read one AML integer data object at `p`, advancing `p`. fn readInteger(bytes: []const u8, p: *usize) ?u64 { if (p.* >= bytes.len) return null; const opcode = bytes[p.*]; p.* += 1; return switch (opcode) { op.zero_op => 0, op.one_op => 1, op.ones_op => 0xFF, op.byte_prefix => readLittle(bytes, p, 1), op.word_prefix => readLittle(bytes, p, 2), op.dword_prefix => readLittle(bytes, p, 4), op.qword_prefix => readLittle(bytes, p, 8), else => null, }; } fn readLittle(bytes: []const u8, p: *usize, n: usize) ?u64 { if (p.* + n > bytes.len) return null; var v: u64 = 0; var k: usize = 0; while (k < n) : (k += 1) v |= @as(u64, bytes[p.* + k]) << @intCast(k * 8); p.* += n; return v; } // --- tests ------------------------------------------------------------------ test "parses a nested namespace and finds the sleep package" { // A hand-assembled AML blob (all PkgLengths computed to be single-byte): // Name(_S5, Package(2){0x05, 0x00}) // Scope(\_SB) { Device(PCI0) { // Name(_HID, 0x11) // Method(MTHD, 1) {} // Method(CALL, 0) { MTHD(Zero) } // invocation of a 1-arg method // } } // OperationRegion(DBG0, SystemIO, 0x0402, 1) // Field(DBG0, ...) { DBGB, 8 } const blob = [_]u8{ // Name(_S5, Package(2){Byte 0x05, Byte 0x00}) 0x08, 0x5F, 0x53, 0x35, 0x5F, 0x12, 0x06, 0x02, 0x0A, 0x05, 0x0A, 0x00, // Scope(\_SB) pkglen=0x27 0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F, // Device(PCI0) pkglen=0x1F 0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30, // Name(_HID, 0x11) 0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11, // Method(MTHD, flags=1) empty, pkglen=0x06 0x14, 0x06, 0x4D, 0x54, 0x48, 0x44, 0x01, // Method(CALL, flags=0) { MTHD(Zero) }, pkglen=0x0B 0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D, 0x54, 0x48, 0x44, 0x00, // OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1) 0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B, 0x02, 0x04, 0x0A, 0x01, // Field(DBG0, flags=1) { DBGB, 8 }, pkglen=0x0B 0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01, 0x44, 0x42, 0x47, 0x42, 0x08, }; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var result = try parse(arena.allocator(), &.{&blob}); // Integrity: the parser consumed exactly the whole blob (no desync). try std.testing.expectEqual(blob.len, result.consumed); try std.testing.expectEqual(blob.len, result.total); const ns = &result.namespace; // Expected top-level nodes. const sb = ns.resolve(ns.root, false, 0, &.{.{ '_', 'S', 'B', '_' }}) orelse return error.NoSB; try std.testing.expectEqual(NodeKind.scope, sb.kind); const pci0 = ns.resolve(sb, false, 0, &.{.{ 'P', 'C', 'I', '0' }}) orelse return error.NoPCI0; try std.testing.expectEqual(NodeKind.device, pci0.kind); _ = ns.resolve(pci0, false, 0, &.{.{ '_', 'H', 'I', 'D' }}) orelse return error.NoHID; // The 1-arg method's arg count was parsed from its flags byte. const mthd = ns.resolve(pci0, false, 0, &.{.{ 'M', 'T', 'H', 'D' }}) orelse return error.NoMTHD; try std.testing.expectEqual(NodeKind.method, mthd.kind); try std.testing.expectEqual(@as(u8, 1), mthd.arg_count); // OperationRegion and the Field unit made it into the namespace. _ = ns.resolve(ns.root, false, 0, &.{.{ 'D', 'B', 'G', '0' }}) orelse return error.NoRegion; _ = ns.resolve(ns.root, false, 0, &.{.{ 'D', 'B', 'G', 'B' }}) orelse return error.NoField; // The sleep package decoded. const s5 = sleepState(ns, 5) orelse return error.NoS5; try std.testing.expectEqual(@as(u8, 5), s5.slp_typ_a); try std.testing.expectEqual(@as(u8, 0), s5.slp_typ_b); } fn noMap(_: u64, _: u64, _: bool) void {} fn noRead(_: u8, _: u16) u32 { return 0; } fn noWrite(_: u8, _: u16, _: u32) void {} test "interpreter runs a method with args, arithmetic, and control flow" { // Method(TST_, 1) { // Store(Arg0, Local0); Add(Local0, 5, Local0) // If (LGreater(Local0, 10)) { Return(One) } // Return(Zero) // } const blob = [_]u8{ 0x14, 0x18, 0x54, 0x53, 0x54, 0x5F, 0x01, // Method TST_, 1 arg 0x70, 0x68, 0x60, // Store(Arg0, Local0) 0x72, 0x60, 0x0A, 0x05, 0x60, // Add(Local0, 5, Local0) 0xA0, 0x07, 0x94, 0x60, 0x0A, 0x0A, 0xA4, 0x01, // If(LGreater(Local0,10)) { Return(One) } 0xA4, 0x00, // Return(Zero) }; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var result = try parse(arena.allocator(), &.{&blob}); const ns = &result.namespace; const tst = ns.resolve(ns.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod; var ev = Interp.init(ns, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator()); const hi = try ev.evaluate(tst, &.{.{ .integer = 7 }}); // 7+5=12 > 10 -> 1 try std.testing.expectEqual(@as(u64, 1), try hi.asInt()); const lo = try ev.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0 try std.testing.expectEqual(@as(u64, 0), try lo.asInt()); }