Skip to content

Commit cc05349

Browse files
authored
Replace the IR tree-walking evaluator with a bytecode VM. (#128)
* rego: Replace the IR tree-walking evaluator with a bytecode VM. Introduce a Bytecode package that compiles IR into a compact instruction stream and executes it in a tight PC loop, replacing the recursive tree-walking IR evaluator. Bytecode is more compact than the IR, more cache-friendly, and eliminates per-evaluation pointer chasing. The tree-walking evaluator copies Statement enum associated values on every dispatch; the VM decodes operands directly from the byte stream instead. The IR-to-bytecode converter runs once at load time. Compact opcode variants pack their operand into the 24-bit header length field, saving a payload word for the most frequent single-operand statements. Validation bounds-checks all string/number/function table references before execution so the VM hot loop can skip those checks entirely. The bytecode VM is up to 25% faster in benchmarks, with the biggest gains on iteration and call-heavy workloads. Signed-off-by: Teemu Koponen <tkoponen@apple.com> * rego: Remove auto-grow from Locals - validate local indices at load time. Signed-off-by: Teemu Koponen <tkoponen@apple.com>
1 parent 4b7ed2d commit cc05349

27 files changed

Lines changed: 5685 additions & 2189 deletions

Package.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,23 @@ let package = Package(
3030
// Targets can depend on other targets in this package and products from dependencies.
3131
.target(
3232
name: "SwiftOPA",
33-
dependencies: ["AST", "IR", "Rego"]
33+
dependencies: ["AST", "IR", "Bytecode", "Rego"]
3434
),
3535
.target(name: "AST"),
3636
.target(
3737
name: "IR",
3838
dependencies: ["AST"]
3939
),
40+
.target(
41+
name: "Bytecode",
42+
dependencies: ["AST", "IR"]
43+
),
4044
.target(
4145
name: "Rego",
4246
dependencies: [
4347
"AST",
4448
"IR",
49+
"Bytecode",
4550
.product(name: "Crypto", package: "swift-crypto", condition: .when(platforms: [.linux])),
4651
]
4752
),
@@ -52,9 +57,13 @@ let package = Package(
5257
),
5358
.testTarget(
5459
name: "IRTests",
55-
dependencies: ["IR"],
60+
dependencies: ["AST", "IR"],
5661
resources: [.copy("Fixtures")]
5762
),
63+
.testTarget(
64+
name: "BytecodeTests",
65+
dependencies: ["AST", "IR", "Bytecode"]
66+
),
5867
.testTarget(
5968
name: "RegoTests",
6069
dependencies: ["Rego"],

Sources/AST/Local.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// Local variable index type used across IR and bytecode evaluation.
2+
public typealias Local = UInt32

Sources/Bytecode/Builder.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import AST
2+
import IR
3+
4+
/// Builds deduplicated string table during bytecode conversion
5+
public struct StringTableBuilder {
6+
private var strings: [String] = []
7+
private var indices: [String: Int] = [:]
8+
9+
public init() {}
10+
11+
/// Intern a string and return its index in the table
12+
public mutating func intern(_ string: String) -> Int {
13+
if let existing = indices[string] {
14+
return existing
15+
}
16+
let index = strings.count
17+
strings.append(string)
18+
indices[string] = index
19+
return index
20+
}
21+
22+
/// Get the final string table
23+
public var table: [String] {
24+
return strings
25+
}
26+
27+
/// Get the number of strings in the table
28+
public var count: Int {
29+
return strings.count
30+
}
31+
}
32+
33+
/// Context for bytecode conversion, tracks state during conversion
34+
public struct ConversionContext {
35+
public var stringTable: StringTableBuilder
36+
public var encoder: Encoder
37+
public var functionIndices: [String: Int]
38+
public var numbers: [RegoNumber?]
39+
40+
public init(functionIndices: [String: Int] = [:], numbers: [RegoNumber?] = []) {
41+
self.stringTable = StringTableBuilder()
42+
self.encoder = Encoder()
43+
self.functionIndices = functionIndices
44+
self.numbers = numbers
45+
}
46+
47+
/// Get current bytecode offset
48+
public var currentOffset: Int {
49+
return encoder.offset
50+
}
51+
52+
/// Convert IR operand to encoded operand, interning strings as needed
53+
public mutating func convertOperand(_ operand: IR.Operand) throws -> EncodedOperand {
54+
switch operand.value {
55+
case .localIndex(let idx):
56+
return .local(UInt32(idx))
57+
case .bool(let value):
58+
return .bool(value)
59+
case .stringIndex(let idx):
60+
return .stringIndex(UInt32(idx))
61+
}
62+
}
63+
64+
/// Convert array of IR operands to encoded operands
65+
public mutating func convertOperands(_ operands: [IR.Operand]) throws -> [EncodedOperand] {
66+
return try operands.map { try convertOperand($0) }
67+
}
68+
}

0 commit comments

Comments
 (0)