hoozuki 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Hoozuki::VM::Compiler do
4
+ describe '#compile' do
5
+ let(:compiler) { described_class.new }
6
+
7
+ context 'with Literal node' do
8
+ it 'compiles to Char and Match instructions' do
9
+ node = Hoozuki::Node::Literal.new('a')
10
+ compiler.compile(node)
11
+
12
+ expect(compiler.instructions.size).to eq(2)
13
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Char)
14
+ expect(compiler.instructions[0].char).to eq('a')
15
+ expect(compiler.instructions[1]).to be_a(Hoozuki::Instruction::Match)
16
+ end
17
+
18
+ it 'handles multibyte characters' do
19
+ node = Hoozuki::Node::Literal.new('あ')
20
+ compiler.compile(node)
21
+
22
+ expect(compiler.instructions[0].char).to eq('あ')
23
+ end
24
+ end
25
+
26
+ context 'with Epsilon node' do
27
+ it 'compiles to only Match instruction' do
28
+ node = Hoozuki::Node::Epsilon.new
29
+ compiler.compile(node)
30
+
31
+ expect(compiler.instructions.size).to eq(1)
32
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Match)
33
+ end
34
+ end
35
+
36
+ context 'with Concatenation node' do
37
+ it 'compiles to sequential Char instructions' do
38
+ node = Hoozuki::Node::Concatenation.new([
39
+ Hoozuki::Node::Literal.new('a'),
40
+ Hoozuki::Node::Literal.new('b')
41
+ ])
42
+ compiler.compile(node)
43
+
44
+ expect(compiler.instructions.size).to eq(3)
45
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Char)
46
+ expect(compiler.instructions[0].char).to eq('a')
47
+ expect(compiler.instructions[1]).to be_a(Hoozuki::Instruction::Char)
48
+ expect(compiler.instructions[1].char).to eq('b')
49
+ expect(compiler.instructions[2]).to be_a(Hoozuki::Instruction::Match)
50
+ end
51
+
52
+ it 'handles longer concatenations' do
53
+ node = Hoozuki::Node::Concatenation.new([
54
+ Hoozuki::Node::Literal.new('a'),
55
+ Hoozuki::Node::Literal.new('b'),
56
+ Hoozuki::Node::Literal.new('c')
57
+ ])
58
+ compiler.compile(node)
59
+
60
+ expect(compiler.instructions.size).to eq(4)
61
+ expect(compiler.instructions.map { |i| i.is_a?(Hoozuki::Instruction::Char) ? i.char : nil }.compact).to eq(['a', 'b', 'c'])
62
+ end
63
+ end
64
+
65
+ context 'with Choice node' do
66
+ it 'compiles with Split and Jmp instructions' do
67
+ node = Hoozuki::Node::Choice.new([
68
+ Hoozuki::Node::Literal.new('a'),
69
+ Hoozuki::Node::Literal.new('b')
70
+ ])
71
+ compiler.compile(node)
72
+
73
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Split)
74
+ expect(compiler.instructions).to include(an_instance_of(Hoozuki::Instruction::Jmp))
75
+ expect(compiler.instructions.last).to be_a(Hoozuki::Instruction::Match)
76
+ end
77
+
78
+ it 'sets correct Split targets' do
79
+ node = Hoozuki::Node::Choice.new([
80
+ Hoozuki::Node::Literal.new('a'),
81
+ Hoozuki::Node::Literal.new('b')
82
+ ])
83
+ compiler.compile(node)
84
+
85
+ split = compiler.instructions[0]
86
+ expect(split.left).to eq(1)
87
+ expect(split.right).to be > split.left
88
+ end
89
+ end
90
+
91
+ context 'with Repetition node' do
92
+ context 'with zero_or_more' do
93
+ it 'compiles with Split and Jmp for looping' do
94
+ node = Hoozuki::Node::Repetition.new(
95
+ Hoozuki::Node::Literal.new('a'),
96
+ :zero_or_more
97
+ )
98
+ compiler.compile(node)
99
+
100
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Split)
101
+ expect(compiler.instructions).to include(an_instance_of(Hoozuki::Instruction::Jmp))
102
+ expect(compiler.instructions.last).to be_a(Hoozuki::Instruction::Match)
103
+ end
104
+
105
+ it 'creates correct loop structure' do
106
+ node = Hoozuki::Node::Repetition.new(
107
+ Hoozuki::Node::Literal.new('a'),
108
+ :zero_or_more
109
+ )
110
+ compiler.compile(node)
111
+
112
+ split = compiler.instructions[0]
113
+ expect(split).to be_a(Hoozuki::Instruction::Split)
114
+ jmp = compiler.instructions.find { |i| i.is_a?(Hoozuki::Instruction::Jmp) }
115
+ expect(jmp.target).to eq(0)
116
+ end
117
+ end
118
+
119
+ context 'with one_or_more' do
120
+ it 'compiles with Char followed by Split' do
121
+ node = Hoozuki::Node::Repetition.new(
122
+ Hoozuki::Node::Literal.new('a'),
123
+ :one_or_more
124
+ )
125
+ compiler.compile(node)
126
+
127
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Char)
128
+ expect(compiler.instructions[1]).to be_a(Hoozuki::Instruction::Split)
129
+ expect(compiler.instructions.last).to be_a(Hoozuki::Instruction::Match)
130
+ end
131
+
132
+ it 'creates correct loop structure' do
133
+ node = Hoozuki::Node::Repetition.new(
134
+ Hoozuki::Node::Literal.new('a'),
135
+ :one_or_more
136
+ )
137
+ compiler.compile(node)
138
+
139
+ split = compiler.instructions[1]
140
+ expect(split.left).to eq(0)
141
+ expect(split.right).to eq(2)
142
+ end
143
+ end
144
+
145
+ context 'with optional' do
146
+ it 'compiles with Split around the node' do
147
+ node = Hoozuki::Node::Repetition.new(
148
+ Hoozuki::Node::Literal.new('a'),
149
+ :optional
150
+ )
151
+ compiler.compile(node)
152
+
153
+ expect(compiler.instructions[0]).to be_a(Hoozuki::Instruction::Split)
154
+ expect(compiler.instructions[1]).to be_a(Hoozuki::Instruction::Char)
155
+ expect(compiler.instructions.last).to be_a(Hoozuki::Instruction::Match)
156
+ end
157
+
158
+ it 'sets Split to allow skipping' do
159
+ node = Hoozuki::Node::Repetition.new(
160
+ Hoozuki::Node::Literal.new('a'),
161
+ :optional
162
+ )
163
+ compiler.compile(node)
164
+
165
+ split = compiler.instructions[0]
166
+ expect(split.left).to eq(1)
167
+ expect(split.right).to eq(2)
168
+ end
169
+ end
170
+ end
171
+
172
+ context 'with complex nested structures' do
173
+ it 'compiles choice within concatenation' do
174
+ node = Hoozuki::Node::Concatenation.new([
175
+ Hoozuki::Node::Literal.new('x'),
176
+ Hoozuki::Node::Choice.new([
177
+ Hoozuki::Node::Literal.new('a'),
178
+ Hoozuki::Node::Literal.new('b')
179
+ ]),
180
+ Hoozuki::Node::Literal.new('y')
181
+ ])
182
+ compiler.compile(node)
183
+
184
+ expect(compiler.instructions).to include(an_instance_of(Hoozuki::Instruction::Split))
185
+ expect(compiler.instructions.last).to be_a(Hoozuki::Instruction::Match)
186
+ end
187
+
188
+ it 'compiles repetition within choice' do
189
+ node = Hoozuki::Node::Choice.new([
190
+ Hoozuki::Node::Repetition.new(
191
+ Hoozuki::Node::Literal.new('a'),
192
+ :zero_or_more
193
+ ),
194
+ Hoozuki::Node::Literal.new('b')
195
+ ])
196
+ compiler.compile(node)
197
+
198
+ expect(compiler.instructions).to include(an_instance_of(Hoozuki::Instruction::Split))
199
+ expect(compiler.instructions).to include(an_instance_of(Hoozuki::Instruction::Jmp))
200
+ expect(compiler.instructions.last).to be_a(Hoozuki::Instruction::Match)
201
+ end
202
+ end
203
+ end
204
+
205
+ describe '#instructions' do
206
+ it 'returns empty array initially' do
207
+ compiler = described_class.new
208
+ expect(compiler.instructions).to eq([])
209
+ end
210
+
211
+ it 'accumulates instructions after compile' do
212
+ compiler = described_class.new
213
+ node = Hoozuki::Node::Literal.new('a')
214
+ compiler.compile(node)
215
+
216
+ expect(compiler.instructions).not_to be_empty
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Hoozuki::VM::Evaluator do
4
+ describe '.evaluate' do
5
+ context 'with single Char instruction' do
6
+ it 'matches exact character' do
7
+ instructions = [
8
+ Hoozuki::Instruction::Char.new('a'),
9
+ Hoozuki::Instruction::Match.new
10
+ ]
11
+
12
+ expect(described_class.evaluate(instructions, 'a')).to be true
13
+ end
14
+
15
+ it 'does not match different character' do
16
+ instructions = [
17
+ Hoozuki::Instruction::Char.new('a'),
18
+ Hoozuki::Instruction::Match.new
19
+ ]
20
+
21
+ expect(described_class.evaluate(instructions, 'b')).to be false
22
+ end
23
+
24
+ it 'does not match empty string' do
25
+ instructions = [
26
+ Hoozuki::Instruction::Char.new('a'),
27
+ Hoozuki::Instruction::Match.new
28
+ ]
29
+
30
+ expect(described_class.evaluate(instructions, '')).to be false
31
+ end
32
+
33
+ it 'handles multibyte characters' do
34
+ instructions = [
35
+ Hoozuki::Instruction::Char.new('あ'),
36
+ Hoozuki::Instruction::Match.new
37
+ ]
38
+
39
+ expect(described_class.evaluate(instructions, 'あ')).to be true
40
+ expect(described_class.evaluate(instructions, 'い')).to be false
41
+ end
42
+ end
43
+
44
+ context 'with multiple Char instructions' do
45
+ it 'matches sequential characters' do
46
+ instructions = [
47
+ Hoozuki::Instruction::Char.new('a'),
48
+ Hoozuki::Instruction::Char.new('b'),
49
+ Hoozuki::Instruction::Char.new('c'),
50
+ Hoozuki::Instruction::Match.new
51
+ ]
52
+
53
+ expect(described_class.evaluate(instructions, 'abc')).to be true
54
+ end
55
+
56
+ it 'does not match partial sequence' do
57
+ instructions = [
58
+ Hoozuki::Instruction::Char.new('a'),
59
+ Hoozuki::Instruction::Char.new('b'),
60
+ Hoozuki::Instruction::Match.new
61
+ ]
62
+
63
+ expect(described_class.evaluate(instructions, 'a')).to be false
64
+ expect(described_class.evaluate(instructions, 'abc')).to be false
65
+ end
66
+ end
67
+
68
+ context 'with Jmp instruction' do
69
+ it 'jumps to target address' do
70
+ instructions = [
71
+ Hoozuki::Instruction::Jmp.new(2),
72
+ Hoozuki::Instruction::Char.new('x'),
73
+ Hoozuki::Instruction::Char.new('a'),
74
+ Hoozuki::Instruction::Match.new
75
+ ]
76
+
77
+ expect(described_class.evaluate(instructions, 'a')).to be true
78
+ expect(described_class.evaluate(instructions, 'x')).to be false
79
+ end
80
+
81
+ it 'handles backward jumps for loops' do
82
+ instructions = [
83
+ Hoozuki::Instruction::Split.new(1, 3),
84
+ Hoozuki::Instruction::Char.new('a'),
85
+ Hoozuki::Instruction::Jmp.new(0),
86
+ Hoozuki::Instruction::Match.new
87
+ ]
88
+
89
+ expect(described_class.evaluate(instructions, '')).to be true
90
+ expect(described_class.evaluate(instructions, 'a')).to be true
91
+ expect(described_class.evaluate(instructions, 'aaa')).to be true
92
+ end
93
+ end
94
+
95
+ context 'with Split instruction' do
96
+ it 'tries left branch first' do
97
+ instructions = [
98
+ Hoozuki::Instruction::Split.new(1, 3),
99
+ Hoozuki::Instruction::Char.new('a'),
100
+ Hoozuki::Instruction::Match.new,
101
+ Hoozuki::Instruction::Char.new('b'),
102
+ Hoozuki::Instruction::Match.new
103
+ ]
104
+
105
+ expect(described_class.evaluate(instructions, 'a')).to be true
106
+ end
107
+
108
+ it 'tries right branch if left fails' do
109
+ instructions = [
110
+ Hoozuki::Instruction::Split.new(1, 3),
111
+ Hoozuki::Instruction::Char.new('a'),
112
+ Hoozuki::Instruction::Match.new,
113
+ Hoozuki::Instruction::Char.new('b'),
114
+ Hoozuki::Instruction::Match.new
115
+ ]
116
+
117
+ expect(described_class.evaluate(instructions, 'b')).to be true
118
+ end
119
+
120
+ it 'returns false if both branches fail' do
121
+ instructions = [
122
+ Hoozuki::Instruction::Split.new(1, 3),
123
+ Hoozuki::Instruction::Char.new('a'),
124
+ Hoozuki::Instruction::Match.new,
125
+ Hoozuki::Instruction::Char.new('b'),
126
+ Hoozuki::Instruction::Match.new
127
+ ]
128
+
129
+ expect(described_class.evaluate(instructions, 'c')).to be false
130
+ end
131
+ end
132
+
133
+ context 'with Match instruction' do
134
+ it 'returns true only if entire input is consumed' do
135
+ instructions = [
136
+ Hoozuki::Instruction::Match.new
137
+ ]
138
+
139
+ expect(described_class.evaluate(instructions, '')).to be true
140
+ expect(described_class.evaluate(instructions, 'a')).to be false
141
+ end
142
+
143
+ it 'returns false if input remains unconsumed' do
144
+ instructions = [
145
+ Hoozuki::Instruction::Char.new('a'),
146
+ Hoozuki::Instruction::Match.new
147
+ ]
148
+
149
+ expect(described_class.evaluate(instructions, 'ab')).to be false
150
+ end
151
+ end
152
+
153
+ context 'with complex patterns' do
154
+ it 'handles choice pattern (a|b)' do
155
+ instructions = [
156
+ Hoozuki::Instruction::Split.new(1, 3),
157
+ Hoozuki::Instruction::Char.new('a'),
158
+ Hoozuki::Instruction::Jmp.new(4),
159
+ Hoozuki::Instruction::Char.new('b'),
160
+ Hoozuki::Instruction::Match.new
161
+ ]
162
+
163
+ expect(described_class.evaluate(instructions, 'a')).to be true
164
+ expect(described_class.evaluate(instructions, 'b')).to be true
165
+ expect(described_class.evaluate(instructions, 'c')).to be false
166
+ end
167
+
168
+ it 'handles zero-or-more pattern (a*)' do
169
+ instructions = [
170
+ Hoozuki::Instruction::Split.new(1, 3),
171
+ Hoozuki::Instruction::Char.new('a'),
172
+ Hoozuki::Instruction::Jmp.new(0),
173
+ Hoozuki::Instruction::Match.new
174
+ ]
175
+
176
+ expect(described_class.evaluate(instructions, '')).to be true
177
+ expect(described_class.evaluate(instructions, 'a')).to be true
178
+ expect(described_class.evaluate(instructions, 'aa')).to be true
179
+ expect(described_class.evaluate(instructions, 'aaa')).to be true
180
+ expect(described_class.evaluate(instructions, 'b')).to be false
181
+ end
182
+
183
+ it 'handles one-or-more pattern (a+)' do
184
+ instructions = [
185
+ Hoozuki::Instruction::Char.new('a'),
186
+ Hoozuki::Instruction::Split.new(0, 2),
187
+ Hoozuki::Instruction::Match.new
188
+ ]
189
+
190
+ expect(described_class.evaluate(instructions, 'a')).to be true
191
+ expect(described_class.evaluate(instructions, 'aa')).to be true
192
+ expect(described_class.evaluate(instructions, 'aaa')).to be true
193
+ expect(described_class.evaluate(instructions, '')).to be false
194
+ end
195
+
196
+ it 'handles optional pattern (a?)' do
197
+ instructions = [
198
+ Hoozuki::Instruction::Split.new(1, 2),
199
+ Hoozuki::Instruction::Char.new('a'),
200
+ Hoozuki::Instruction::Match.new
201
+ ]
202
+
203
+ expect(described_class.evaluate(instructions, '')).to be true
204
+ expect(described_class.evaluate(instructions, 'a')).to be true
205
+ expect(described_class.evaluate(instructions, 'aa')).to be false
206
+ end
207
+ end
208
+
209
+ context 'with edge cases' do
210
+ it 'handles empty instruction list' do
211
+ instructions = []
212
+
213
+ expect(described_class.evaluate(instructions, '')).to be false
214
+ expect(described_class.evaluate(instructions, 'a')).to be false
215
+ end
216
+
217
+ it 'handles invalid pc (out of bounds)' do
218
+ instructions = [
219
+ Hoozuki::Instruction::Jmp.new(10),
220
+ Hoozuki::Instruction::Match.new
221
+ ]
222
+
223
+ expect(described_class.evaluate(instructions, '')).to be false
224
+ end
225
+
226
+ it 'handles long input strings' do
227
+ instructions = [
228
+ Hoozuki::Instruction::Split.new(1, 3),
229
+ Hoozuki::Instruction::Char.new('a'),
230
+ Hoozuki::Instruction::Jmp.new(0),
231
+ Hoozuki::Instruction::Match.new
232
+ ]
233
+
234
+ long_input = 'a' * 1000
235
+ expect(described_class.evaluate(instructions, long_input)).to be true
236
+ end
237
+ end
238
+
239
+ context 'with initial positions' do
240
+ it 'can start from non-zero input position' do
241
+ instructions = [
242
+ Hoozuki::Instruction::Char.new('b'),
243
+ Hoozuki::Instruction::Match.new
244
+ ]
245
+
246
+ expect(described_class.evaluate(instructions, 'ab', 1, 0)).to be true
247
+ end
248
+
249
+ it 'can start from non-zero pc' do
250
+ instructions = [
251
+ Hoozuki::Instruction::Char.new('x'),
252
+ Hoozuki::Instruction::Char.new('a'),
253
+ Hoozuki::Instruction::Match.new
254
+ ]
255
+
256
+ expect(described_class.evaluate(instructions, 'a', 0, 1)).to be true
257
+ end
258
+ end
259
+ end
260
+ end