-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmatcher.go
More file actions
337 lines (279 loc) · 8.77 KB
/
Copy pathmatcher.go
File metadata and controls
337 lines (279 loc) · 8.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
package miniohm
import (
"context"
"encoding/binary"
"fmt"
"io"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
// Constants for memory layout
const (
wasmPageSize = 64 * 1024
InputBufferOffset = wasmPageSize
InputBufferSize = wasmPageSize
MemoTableOffset = InputBufferOffset + InputBufferSize
)
// WasmMatcher is a Go implementation of the JavaScript WasmMatcher class for Ohm
type WasmMatcher struct {
runtime wazero.Runtime
module api.Module
input string
ctx context.Context
ruleIds map[string]int
lastMatchResult bool
}
// GetModule returns the WebAssembly module
func (m *WasmMatcher) GetModule() api.Module {
return m.module
}
func NewWasmMatcher(ctx context.Context) *WasmMatcher {
// Create a new runtime with custom sections enabled
config := wazero.NewRuntimeConfig().WithCustomSections(true)
return &WasmMatcher{
runtime: wazero.NewRuntimeWithConfig(ctx, config),
ctx: ctx,
ruleIds: make(map[string]int),
lastMatchResult: false,
}
}
// parseRuleNames parses the rule names from the custom section data
// The data is formatted as a WebAssembly vector of strings (each string is a length-prefixed UTF-8 bytes)
// with LEB128-encoded lengths
func parseRuleNames(data []byte) ([]string, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty custom section data")
}
// Read the number of names (vec length) as LEB128-encoded uint32
numNamesUint64, bytesRead := binary.Uvarint(data)
if bytesRead <= 0 {
return nil, fmt.Errorf("failed to read number of names: %v", io.ErrUnexpectedEOF)
}
// Ensure the value fits in uint32
if numNamesUint64 > uint64(^uint32(0)) {
return nil, fmt.Errorf("number of names exceeds maximum uint32 value")
}
numNames := uint32(numNamesUint64)
data = data[bytesRead:]
names := make([]string, numNames)
for i := uint32(0); i < numNames; i++ {
// Read the length of the name as LEB128-encoded uint32
nameLenUint64, bytesRead := binary.Uvarint(data)
if bytesRead <= 0 {
return nil, fmt.Errorf("failed to read name length: %v", io.ErrUnexpectedEOF)
}
// Ensure the value fits in uint32
if nameLenUint64 > uint64(^uint32(0)) {
return nil, fmt.Errorf("name length exceeds maximum uint32 value")
}
nameLen := uint32(nameLenUint64)
data = data[bytesRead:]
// Ensure we have enough bytes to read
if uint64(len(data)) < uint64(nameLen) {
return nil, fmt.Errorf("buffer too small to read name bytes")
}
// Read the name bytes
nameBytes := data[:nameLen]
data = data[nameLen:]
// Convert to string
names[i] = string(nameBytes)
}
return names, nil
}
func (m *WasmMatcher) LoadModule(wasmPath string) error {
// Read the WASM file
wasmBytes, err := os.ReadFile(wasmPath)
if err != nil {
return fmt.Errorf("error reading WASM file: %v", err)
}
// Create the env module with required host functions
_, err = m.runtime.NewHostModuleBuilder("env").
NewFunctionBuilder().
WithFunc(func(a, b, c, d int32) {
panic("WebAssembly module aborted execution")
}).
Export("abort").
NewFunctionBuilder().
WithFunc(func(val uint32) {
fmt.Printf("WASM debug: %d\n", val)
}).
Export("printI32").
NewFunctionBuilder().
WithFunc(func(ctx context.Context, mod api.Module, offset, maxLen uint32) uint32 {
return m.fillInputBuffer(ctx, mod, offset, maxLen)
}).
Export("fillInputBuffer").
Instantiate(m.ctx)
if err != nil {
return fmt.Errorf("failed to create host module: %v", err)
}
// First compile the module to access the custom sections
compiledModule, err := m.runtime.CompileModule(m.ctx, wasmBytes)
if err != nil {
return fmt.Errorf("error compiling module: %v", err)
}
// Get all custom sections from the module
customSections := compiledModule.CustomSections()
if customSections == nil {
return fmt.Errorf("no custom sections found in module")
}
var ruleNamesSection api.CustomSection
for _, section := range customSections {
if section.Name() == "ruleNames" {
ruleNamesSection = section
break
}
}
if ruleNamesSection == nil {
return fmt.Errorf("required custom section 'ruleNames' not found")
}
// Parse rule names from the custom section data
ruleNames, err := parseRuleNames(ruleNamesSection.Data())
if err != nil {
return fmt.Errorf("failed to parse rule names from custom section: %v", err)
}
// Now instantiate the module
m.module, err = m.runtime.InstantiateModule(m.ctx, compiledModule, wazero.NewModuleConfig())
if err != nil {
return fmt.Errorf("error instantiating module: %v", err)
}
// Build the ruleIds map (mapping from name to index)
m.ruleIds = make(map[string]int, len(ruleNames))
for i, name := range ruleNames {
m.ruleIds[name] = i
}
return nil
}
func (m *WasmMatcher) SetInput(input string) {
if m.input != input {
m.input = input
}
}
// SetInputFromFile reads input from a file and sets it as the current input
func (m *WasmMatcher) SetInputFromFile(filePath string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("error reading input file: %v", err)
}
m.SetInput(string(data))
return nil
}
func (m *WasmMatcher) GetInput() string {
return m.input
}
func (m *WasmMatcher) Match(ruleName ...string) (bool, error) {
// Get the rule ID
ruleId := uint64(0) // Default to 0 (start rule)
// If a rule name is provided, use it; otherwise use the default start rule
if len(ruleName) > 0 && ruleName[0] != "" {
if id, ok := m.ruleIds[ruleName[0]]; ok {
ruleId = uint64(id)
} else {
return false, fmt.Errorf("rule not found: %s", ruleName[0])
}
}
// Call the match function
matchFunc := m.module.ExportedFunction("match")
if matchFunc == nil {
return false, fmt.Errorf("match function not exported by module")
}
results, err := matchFunc.Call(m.ctx, ruleId)
if err != nil {
return false, fmt.Errorf("error calling match function: %v", err)
}
// Non-zero result means success
result := results[0] != 0
m.lastMatchResult = result
return result, nil
}
// GetCstRoot returns the root node address of the concrete syntax tree
func (m *WasmMatcher) getCstRoot() (uint32, error) {
getCstRootFunc := m.module.ExportedFunction("getCstRoot")
if getCstRootFunc == nil {
return 0, fmt.Errorf("getCstRoot function not exported by module")
}
results, err := getCstRootFunc.Call(m.ctx)
if err != nil {
return 0, fmt.Errorf("error getting CST root: %v", err)
}
return uint32(results[0]), nil
}
// GetCstRoot returns a CstNode object for the current parse tree
func (m *WasmMatcher) GetCstRoot() (*CstNode, error) {
rootAddr, err := m.getCstRoot()
if err != nil {
return nil, fmt.Errorf("failed to get CST root: %v", err)
}
memory := m.module.Memory()
if memory == nil {
return nil, fmt.Errorf("WebAssembly module has no memory")
}
return NewCstNode(m.getRuleNames(), memory, rootAddr), nil
}
// GetRuleNames returns the list of rule names in the grammar
func (m *WasmMatcher) getRuleNames() []string {
// If we have real rule names, use them
if len(m.ruleIds) > 0 {
// Convert the rule IDs map to a slice of rule names
maxID := 0
for _, id := range m.ruleIds {
if id > maxID {
maxID = id
}
}
ruleNames := make([]string, maxID+1)
for name, id := range m.ruleIds {
ruleNames[id] = name
}
return ruleNames
}
// If we don't have rule names yet, create placeholder names for ES5 grammar
// This is just a fallback to make tests work
const expectedRuleCount = 100 // More than enough for most grammars
ruleNames := make([]string, expectedRuleCount)
ruleNames[0] = "Program" // Common name for the start rule
for i := 1; i < expectedRuleCount; i++ {
ruleNames[i] = fmt.Sprintf("rule_%d", i)
}
return ruleNames
}
// fillInputBuffer is called by the WebAssembly module to get more input
func (m *WasmMatcher) fillInputBuffer(_ctx context.Context, mod api.Module, offset, maxLen uint32) uint32 {
bytesToWrite := len(m.input)
if bytesToWrite > int(maxLen) {
bytesToWrite = int(maxLen)
}
// Get module memory
memory := mod.Memory()
if memory == nil {
panic("WebAssembly module has no memory")
}
// Write to WebAssembly memory
if bytesToWrite > 0 {
// Create a buffer with our input data
inputData := []byte(m.input[:bytesToWrite])
// Write directly to WebAssembly memory
memory.Write(InputBufferOffset+offset, inputData)
// Mark end of input with 0xFF (invalid UTF-8 character)
memory.WriteByte(InputBufferOffset+offset+uint32(bytesToWrite), 0xFF)
} else {
// No more input, just write the terminator
memory.WriteByte(InputBufferOffset+offset, 0xFF)
}
return uint32(bytesToWrite)
}
// Close releases all resources
func (m *WasmMatcher) Close() error {
if m.module != nil {
if err := m.module.Close(m.ctx); err != nil {
return err
}
}
if m.runtime != nil {
if err := m.runtime.Close(m.ctx); err != nil {
return err
}
}
return nil
}