forthic 0.1.0 → 0.2.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/CLAUDE.md +74 -0
- data/Guardfile +3 -3
- data/README.md +10 -0
- data/Rakefile +1 -1
- data/lib/forthic/code_location.rb +15 -0
- data/lib/forthic/errors/unknown_word_error.rb +39 -0
- data/lib/forthic/forthic_error.rb +16 -2
- data/lib/forthic/forthic_module.rb +10 -9
- data/lib/forthic/global_module.rb +321 -334
- data/lib/forthic/interpreter.rb +294 -104
- data/lib/forthic/token.rb +1 -2
- data/lib/forthic/tokenizer.rb +3 -3
- data/lib/forthic/variable.rb +1 -1
- data/lib/forthic/version.rb +1 -1
- data/lib/forthic/words/definition_word.rb +15 -17
- data/lib/forthic/words/end_array_word.rb +3 -3
- data/lib/forthic/words/end_module_word.rb +2 -2
- data/lib/forthic/words/imported_word.rb +3 -3
- data/lib/forthic/words/map_word.rb +5 -5
- data/lib/forthic/words/module_memo_bang_at_word.rb +3 -3
- data/lib/forthic/words/module_memo_bang_word.rb +3 -3
- data/lib/forthic/words/module_memo_word.rb +2 -2
- data/lib/forthic/words/module_word.rb +3 -3
- data/lib/forthic/words/push_value_word.rb +3 -3
- data/lib/forthic/words/start_module_word.rb +6 -6
- data/lib/forthic/words/word.rb +1 -1
- data/lib/forthic.rb +23 -19
- metadata +20 -10
data/lib/forthic/interpreter.rb
CHANGED
|
@@ -1,49 +1,138 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
3
|
+
require_relative "forthic_error"
|
|
4
|
+
require_relative "errors/unknown_word_error"
|
|
5
|
+
require_relative "tokenizer"
|
|
6
|
+
require_relative "token"
|
|
7
|
+
require_relative "code_location"
|
|
8
|
+
require_relative "positioned_string"
|
|
9
|
+
require_relative "words/word"
|
|
10
|
+
require_relative "words/push_value_word"
|
|
11
|
+
require_relative "words/start_module_word"
|
|
12
|
+
require_relative "words/end_module_word"
|
|
13
|
+
require_relative "words/end_array_word"
|
|
14
|
+
require_relative "words/definition_word"
|
|
15
|
+
require_relative "global_module"
|
|
15
16
|
|
|
16
17
|
module Forthic
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
# Error codes used throughout the interpreter
|
|
19
|
+
module ErrorCodes
|
|
20
|
+
SCREEN_NOT_FOUND = "screen-not-found"
|
|
21
|
+
EXECUTION_ERROR = "execution-error"
|
|
22
|
+
MODULE_NOT_FOUND = "module-not-found"
|
|
23
|
+
STACK_UNDERFLOW = "stack-underflow"
|
|
24
|
+
MODULE_EXECUTION_ERROR = "module-execution-error"
|
|
25
|
+
UNKNOWN_TOKEN = "unknown-token"
|
|
26
|
+
NESTED_DEFINITION = "nested-definition"
|
|
27
|
+
NESTED_MEMO_DEFINITION = "nested-memo-definition"
|
|
28
|
+
DEFINITION_WITHOUT_START = "definition-without-start"
|
|
29
|
+
MISSING_DEFINITION = "missing-definition"
|
|
30
|
+
WORD_NOT_FOUND = "word-not-found"
|
|
31
|
+
end
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
# Manages execution state for the interpreter
|
|
34
|
+
class ExecutionState
|
|
35
|
+
attr_accessor :stack, :module_stack, :is_compiling, :should_stop,
|
|
36
|
+
:is_memo_definition, :cur_definition, :string_location
|
|
37
|
+
|
|
38
|
+
def initialize(app_module)
|
|
24
39
|
@stack = []
|
|
25
|
-
@
|
|
40
|
+
@module_stack = [app_module]
|
|
26
41
|
@is_compiling = false
|
|
27
42
|
@should_stop = false
|
|
28
43
|
@is_memo_definition = false
|
|
29
44
|
@cur_definition = nil
|
|
30
|
-
@screens = {}
|
|
31
|
-
@default_module_flags = {}
|
|
32
|
-
@module_flags = {}
|
|
33
45
|
@string_location = nil
|
|
46
|
+
end
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
@
|
|
37
|
-
@module_stack = [
|
|
48
|
+
def reset(app_module)
|
|
49
|
+
@stack = []
|
|
50
|
+
@module_stack = [app_module]
|
|
51
|
+
@is_compiling = false
|
|
52
|
+
@is_memo_definition = false
|
|
53
|
+
@cur_definition = nil
|
|
54
|
+
@string_location = nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Manages profiling state and operations
|
|
59
|
+
class ProfilingState
|
|
60
|
+
attr_reader :word_counts, :is_profiling, :start_profile_time, :timestamps
|
|
38
61
|
|
|
62
|
+
def initialize
|
|
39
63
|
@word_counts = {}
|
|
40
64
|
@is_profiling = false
|
|
41
65
|
@start_profile_time = nil
|
|
42
66
|
@timestamps = []
|
|
43
67
|
end
|
|
44
68
|
|
|
69
|
+
def start_profiling
|
|
70
|
+
@is_profiling = true
|
|
71
|
+
@timestamps = []
|
|
72
|
+
@start_profile_time = Time.now
|
|
73
|
+
add_timestamp("START")
|
|
74
|
+
@word_counts = {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def stop_profiling
|
|
78
|
+
add_timestamp("END")
|
|
79
|
+
@is_profiling = false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def count_word(word)
|
|
83
|
+
return unless @is_profiling
|
|
84
|
+
@word_counts[word.name] ||= 0
|
|
85
|
+
@word_counts[word.name] += 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_timestamp(label)
|
|
89
|
+
return unless @is_profiling
|
|
90
|
+
timestamp = {label: label, time_ms: (Time.now - @start_profile_time) * 1000}
|
|
91
|
+
@timestamps.push(timestamp)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def word_histogram
|
|
95
|
+
@word_counts.map { |name, count| {word: name, count: count} }.sort_by { |item| -item[:count] }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Interpreter
|
|
100
|
+
# Core interpreter components
|
|
101
|
+
attr_reader :global_module, :app_module, :registered_modules
|
|
102
|
+
# Screen and module management
|
|
103
|
+
attr_accessor :screens, :default_module_flags, :module_flags
|
|
104
|
+
# State objects
|
|
105
|
+
attr_reader :execution_state, :profiling_state
|
|
106
|
+
|
|
107
|
+
# Token handler lookup table
|
|
108
|
+
TOKEN_HANDLERS = {
|
|
109
|
+
TokenType::STRING => :handle_string_token,
|
|
110
|
+
TokenType::COMMENT => :handle_comment_token,
|
|
111
|
+
TokenType::START_ARRAY => :handle_start_array_token,
|
|
112
|
+
TokenType::END_ARRAY => :handle_end_array_token,
|
|
113
|
+
TokenType::START_MODULE => :handle_start_module_token,
|
|
114
|
+
TokenType::END_MODULE => :handle_end_module_token,
|
|
115
|
+
TokenType::START_DEF => :handle_start_definition_token,
|
|
116
|
+
TokenType::START_MEMO => :handle_start_memo_token,
|
|
117
|
+
TokenType::END_DEF => :handle_end_definition_token,
|
|
118
|
+
TokenType::WORD => :handle_word_token
|
|
119
|
+
}.freeze
|
|
120
|
+
|
|
121
|
+
def initialize
|
|
122
|
+
@registered_modules = {}
|
|
123
|
+
@screens = {}
|
|
124
|
+
@default_module_flags = {}
|
|
125
|
+
@module_flags = {}
|
|
126
|
+
|
|
127
|
+
@global_module = GlobalModule.new(self)
|
|
128
|
+
@app_module = ForthicModule.new("", self)
|
|
129
|
+
|
|
130
|
+
@execution_state = ExecutionState.new(@app_module)
|
|
131
|
+
@profiling_state = ProfilingState.new
|
|
132
|
+
end
|
|
133
|
+
|
|
45
134
|
def halt
|
|
46
|
-
@should_stop = true
|
|
135
|
+
@execution_state.should_stop = true
|
|
47
136
|
end
|
|
48
137
|
|
|
49
138
|
# @return [ForthicModule]
|
|
@@ -53,7 +142,28 @@ module Forthic
|
|
|
53
142
|
|
|
54
143
|
# @return [CodeLocation, nil]
|
|
55
144
|
def get_string_location
|
|
56
|
-
@string_location
|
|
145
|
+
@execution_state.string_location
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Delegation methods for execution state
|
|
149
|
+
def stack
|
|
150
|
+
@execution_state.stack
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def stack=(new_stack)
|
|
154
|
+
@execution_state.stack = new_stack
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def module_stack
|
|
158
|
+
@execution_state.module_stack
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def is_compiling
|
|
162
|
+
@execution_state.is_compiling
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def cur_definition
|
|
166
|
+
@execution_state.cur_definition
|
|
57
167
|
end
|
|
58
168
|
|
|
59
169
|
# @param [String] module_id
|
|
@@ -80,20 +190,15 @@ module Forthic
|
|
|
80
190
|
end
|
|
81
191
|
|
|
82
192
|
def reset
|
|
83
|
-
@stack = []
|
|
84
193
|
@app_module.variables = {}
|
|
85
|
-
@
|
|
86
|
-
@is_compiling = false
|
|
87
|
-
@is_memo_definition = false
|
|
88
|
-
@cur_definition = nil
|
|
89
|
-
@string_location = nil
|
|
194
|
+
@execution_state.reset(@app_module)
|
|
90
195
|
end
|
|
91
196
|
|
|
92
197
|
# @param [String] screen_name
|
|
93
198
|
# @return [String]
|
|
94
199
|
def get_screen_forthic(screen_name)
|
|
95
200
|
screen = @screens[screen_name]
|
|
96
|
-
raise ForthicError.new(
|
|
201
|
+
raise ForthicError.new(ErrorCodes::SCREEN_NOT_FOUND, "Unable to find screen \"#{screen_name}\"", "Screen not found. Check the screen name for typos or ensure it has been properly registered.") unless screen
|
|
97
202
|
screen
|
|
98
203
|
end
|
|
99
204
|
|
|
@@ -112,63 +217,82 @@ module Forthic
|
|
|
112
217
|
loop do
|
|
113
218
|
token = tokenizer.next_token
|
|
114
219
|
handle_token(token)
|
|
115
|
-
break if token.type == TokenType::EOS || @should_stop
|
|
116
|
-
next if [TokenType::START_DEF, TokenType::END_DEF, TokenType::COMMENT].include?(token.type) || @is_compiling
|
|
220
|
+
break if token.type == TokenType::EOS || @execution_state.should_stop
|
|
221
|
+
next if [TokenType::START_DEF, TokenType::END_DEF, TokenType::COMMENT].include?(token.type) || @execution_state.is_compiling
|
|
117
222
|
end
|
|
118
223
|
true
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
224
|
+
rescue ForthicError => e
|
|
225
|
+
# Re-raise ForthicError and its subclasses without wrapping
|
|
226
|
+
raise e
|
|
227
|
+
rescue => e
|
|
228
|
+
# Wrap non-ForthicError exceptions
|
|
229
|
+
error = ForthicError.new(ErrorCodes::EXECUTION_ERROR, "Error executing token '#{token&.string}'", "An unexpected error occurred during execution. Check the token syntax and try again.", token&.location)
|
|
230
|
+
error.set_caught_error(e)
|
|
231
|
+
raise error
|
|
123
232
|
end
|
|
124
233
|
|
|
125
234
|
# @return [ForthicModule]
|
|
126
235
|
def cur_module
|
|
127
|
-
@module_stack.last
|
|
236
|
+
@execution_state.module_stack.last
|
|
128
237
|
end
|
|
129
238
|
|
|
130
239
|
# @param [String] name
|
|
131
240
|
# @return [ForthicModule]
|
|
132
241
|
def find_module(name)
|
|
242
|
+
raise ArgumentError, "Module name cannot be nil" if name.nil?
|
|
243
|
+
raise ArgumentError, "Module name cannot be empty" if name.empty?
|
|
244
|
+
|
|
133
245
|
result = @registered_modules[name]
|
|
134
|
-
raise ForthicError.new(
|
|
246
|
+
raise ForthicError.new(ErrorCodes::MODULE_NOT_FOUND, "Module '#{name}' not found", "Check the module name for typos and ensure it has been properly registered.") unless result
|
|
135
247
|
result
|
|
136
248
|
end
|
|
137
249
|
|
|
138
250
|
# @param [Object] val
|
|
139
251
|
def stack_push(val)
|
|
140
|
-
@stack.push(val)
|
|
252
|
+
@execution_state.stack.push(val)
|
|
141
253
|
end
|
|
142
254
|
|
|
143
255
|
# @return [Object]
|
|
144
256
|
def stack_pop
|
|
145
|
-
raise ForthicError.new(
|
|
146
|
-
result = @stack.pop
|
|
147
|
-
@string_location = result.is_a?(PositionedString) ? result.location : nil
|
|
257
|
+
raise ForthicError.new(ErrorCodes::STACK_UNDERFLOW, "Stack underflow", "Attempted to pop from an empty stack. This indicates a logical error in the Forthic code.") if @execution_state.stack.empty?
|
|
258
|
+
result = @execution_state.stack.pop
|
|
259
|
+
@execution_state.string_location = result.is_a?(PositionedString) ? result.location : nil
|
|
148
260
|
result.is_a?(PositionedString) ? result.value_of : result
|
|
149
261
|
end
|
|
150
262
|
|
|
151
263
|
# @param [ForthicModule] mod
|
|
152
264
|
def module_stack_push(mod)
|
|
153
|
-
|
|
265
|
+
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
266
|
+
@execution_state.module_stack.push(mod)
|
|
154
267
|
end
|
|
155
268
|
|
|
156
269
|
def module_stack_pop
|
|
157
|
-
@module_stack.pop
|
|
270
|
+
@execution_state.module_stack.pop
|
|
158
271
|
end
|
|
159
272
|
|
|
160
273
|
# @param [ForthicModule] mod
|
|
161
274
|
def register_module(mod)
|
|
275
|
+
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
276
|
+
raise ArgumentError, "Module must respond to :name" unless mod.respond_to?(:name)
|
|
162
277
|
@registered_modules[mod.name] = mod
|
|
163
278
|
end
|
|
164
279
|
|
|
280
|
+
# @param [ForthicModule] mod
|
|
281
|
+
# @param [String] prefix
|
|
282
|
+
def import_module(mod, prefix = "")
|
|
283
|
+
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
284
|
+
register_module(mod)
|
|
285
|
+
@app_module.import_module(prefix, mod, self)
|
|
286
|
+
end
|
|
287
|
+
|
|
165
288
|
# @param [ForthicModule] mod
|
|
166
289
|
def run_module_code(mod)
|
|
290
|
+
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
167
291
|
module_stack_push(mod)
|
|
168
292
|
run(mod.forthic_code)
|
|
169
293
|
module_stack_pop
|
|
170
294
|
rescue => e
|
|
171
|
-
error = ForthicError.new(
|
|
295
|
+
error = ForthicError.new(ErrorCodes::MODULE_EXECUTION_ERROR, "Error executing module '#{mod.name}'", "An error occurred while running the module code. Check the module implementation for syntax errors.")
|
|
172
296
|
error.set_caught_error(e)
|
|
173
297
|
raise error
|
|
174
298
|
end
|
|
@@ -177,7 +301,7 @@ module Forthic
|
|
|
177
301
|
# @return [Word, nil]
|
|
178
302
|
def find_word(name)
|
|
179
303
|
result = nil
|
|
180
|
-
@module_stack.reverse_each do |m|
|
|
304
|
+
@execution_state.module_stack.reverse_each do |m|
|
|
181
305
|
result = m.find_word(name)
|
|
182
306
|
break if result
|
|
183
307
|
end
|
|
@@ -185,59 +309,40 @@ module Forthic
|
|
|
185
309
|
result
|
|
186
310
|
end
|
|
187
311
|
|
|
312
|
+
# Delegation methods for profiling
|
|
188
313
|
def start_profiling
|
|
189
|
-
@
|
|
190
|
-
@timestamps = []
|
|
191
|
-
@start_profile_time = Time.now
|
|
192
|
-
add_timestamp("START")
|
|
193
|
-
@word_counts = {}
|
|
314
|
+
@profiling_state.start_profiling
|
|
194
315
|
end
|
|
195
316
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return unless @is_profiling
|
|
199
|
-
@word_counts[word.name] ||= 0
|
|
200
|
-
@word_counts[word.name] += 1
|
|
317
|
+
def stop_profiling
|
|
318
|
+
@profiling_state.stop_profiling
|
|
201
319
|
end
|
|
202
320
|
|
|
203
|
-
def
|
|
204
|
-
|
|
205
|
-
@is_profiling = false
|
|
321
|
+
def count_word(word)
|
|
322
|
+
@profiling_state.count_word(word)
|
|
206
323
|
end
|
|
207
324
|
|
|
208
|
-
# @param [String] label
|
|
209
325
|
def add_timestamp(label)
|
|
210
|
-
|
|
211
|
-
timestamp = { label: label, time_ms: (Time.now - @start_profile_time) * 1000 }
|
|
212
|
-
@timestamps.push(timestamp)
|
|
326
|
+
@profiling_state.add_timestamp(label)
|
|
213
327
|
end
|
|
214
328
|
|
|
215
|
-
# @return [Array<Hash>]
|
|
216
329
|
def word_histogram
|
|
217
|
-
@
|
|
330
|
+
@profiling_state.word_histogram
|
|
218
331
|
end
|
|
219
332
|
|
|
220
|
-
# @return [Array<Hash>]
|
|
221
333
|
def profile_timestamps
|
|
222
|
-
@timestamps
|
|
334
|
+
@profiling_state.timestamps
|
|
223
335
|
end
|
|
224
336
|
|
|
225
337
|
# @param [Token] token
|
|
226
338
|
def handle_token(token)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
when TokenType::START_MODULE then handle_start_module_token(token)
|
|
233
|
-
when TokenType::END_MODULE then handle_end_module_token(token)
|
|
234
|
-
when TokenType::START_DEF then handle_start_definition_token(token)
|
|
235
|
-
when TokenType::START_MEMO then handle_start_memo_token(token)
|
|
236
|
-
when TokenType::END_DEF then handle_end_definition_token(token)
|
|
237
|
-
when TokenType::WORD then handle_word_token(token)
|
|
238
|
-
when TokenType::EOS then return
|
|
339
|
+
return if token.type == TokenType::EOS
|
|
340
|
+
|
|
341
|
+
handler = TOKEN_HANDLERS[token.type]
|
|
342
|
+
if handler
|
|
343
|
+
send(handler, token)
|
|
239
344
|
else
|
|
240
|
-
raise ForthicError.new(
|
|
345
|
+
raise ForthicError.new(ErrorCodes::UNKNOWN_TOKEN, "Unknown token type '#{token.string}'", "This token type is not recognized. Check for typos or unsupported syntax.", token.location)
|
|
241
346
|
end
|
|
242
347
|
end
|
|
243
348
|
|
|
@@ -250,7 +355,7 @@ module Forthic
|
|
|
250
355
|
# @param [Token] token
|
|
251
356
|
def handle_start_module_token(token)
|
|
252
357
|
word = StartModuleWord.new(token.string)
|
|
253
|
-
@cur_definition.add_word(word) if @is_compiling
|
|
358
|
+
@execution_state.cur_definition.add_word(word) if @execution_state.is_compiling
|
|
254
359
|
count_word(word)
|
|
255
360
|
word.execute(self)
|
|
256
361
|
end
|
|
@@ -258,7 +363,7 @@ module Forthic
|
|
|
258
363
|
# @param [Token] _token
|
|
259
364
|
def handle_end_module_token(_token)
|
|
260
365
|
word = EndModuleWord.new
|
|
261
|
-
@cur_definition.add_word(word) if @is_compiling
|
|
366
|
+
@execution_state.cur_definition.add_word(word) if @execution_state.is_compiling
|
|
262
367
|
count_word(word)
|
|
263
368
|
word.execute(self)
|
|
264
369
|
end
|
|
@@ -280,49 +385,134 @@ module Forthic
|
|
|
280
385
|
|
|
281
386
|
# @param [Token] token
|
|
282
387
|
def handle_start_definition_token(token)
|
|
283
|
-
raise ForthicError.new("
|
|
284
|
-
@cur_definition = DefinitionWord.new(token.string)
|
|
285
|
-
@is_compiling = true
|
|
286
|
-
@is_memo_definition = false
|
|
388
|
+
raise ForthicError.new(ErrorCodes::NESTED_DEFINITION, "Nested definition not allowed", "A definition was started while another definition is active. Ensure all definitions end with semicolons.", token.location) if @execution_state.is_compiling
|
|
389
|
+
@execution_state.cur_definition = DefinitionWord.new(token.string)
|
|
390
|
+
@execution_state.is_compiling = true
|
|
391
|
+
@execution_state.is_memo_definition = false
|
|
287
392
|
end
|
|
288
393
|
|
|
289
394
|
# @param [Token] token
|
|
290
395
|
def handle_start_memo_token(token)
|
|
291
|
-
raise ForthicError.new("
|
|
292
|
-
@cur_definition = DefinitionWord.new(token.string)
|
|
293
|
-
@is_compiling = true
|
|
294
|
-
@is_memo_definition = true
|
|
396
|
+
raise ForthicError.new(ErrorCodes::NESTED_MEMO_DEFINITION, "Nested memo definition not allowed", "A memo definition was started while another definition is active. Ensure all definitions end with semicolons.", token.location) if @execution_state.is_compiling
|
|
397
|
+
@execution_state.cur_definition = DefinitionWord.new(token.string)
|
|
398
|
+
@execution_state.is_compiling = true
|
|
399
|
+
@execution_state.is_memo_definition = true
|
|
295
400
|
end
|
|
296
401
|
|
|
297
402
|
# @param [Token] token
|
|
298
403
|
def handle_end_definition_token(token)
|
|
299
|
-
raise ForthicError.new(
|
|
300
|
-
raise ForthicError.new(
|
|
301
|
-
if @is_memo_definition
|
|
302
|
-
cur_module.add_memo_words(@cur_definition)
|
|
404
|
+
raise ForthicError.new(ErrorCodes::DEFINITION_WITHOUT_START, "Definition ended without start", "A definition was ended when none was active. Check for extra semicolons.", token.location) unless @execution_state.is_compiling
|
|
405
|
+
raise ForthicError.new(ErrorCodes::MISSING_DEFINITION, "No current definition to end", "Internal error: definition state is inconsistent.", token.location) unless @execution_state.cur_definition
|
|
406
|
+
if @execution_state.is_memo_definition
|
|
407
|
+
cur_module.add_memo_words(@execution_state.cur_definition)
|
|
303
408
|
else
|
|
304
|
-
cur_module.add_word(@cur_definition)
|
|
409
|
+
cur_module.add_word(@execution_state.cur_definition)
|
|
305
410
|
end
|
|
306
|
-
@is_compiling = false
|
|
411
|
+
@execution_state.is_compiling = false
|
|
307
412
|
end
|
|
308
413
|
|
|
309
414
|
# @param [Token] token
|
|
310
415
|
def handle_word_token(token)
|
|
311
416
|
word = find_word(token.string)
|
|
312
|
-
|
|
417
|
+
unless word
|
|
418
|
+
# Generate suggested words for better error messages
|
|
419
|
+
suggested_words = find_similar_words(token.string)
|
|
420
|
+
raise Errors::UnknownWordError.new(token.string, token.location, suggested_words)
|
|
421
|
+
end
|
|
313
422
|
handle_word(word, token.location)
|
|
314
423
|
end
|
|
315
424
|
|
|
316
425
|
# @param [Word] word
|
|
317
426
|
# @param [CodeLocation, nil] location
|
|
318
427
|
def handle_word(word, location = nil)
|
|
319
|
-
if @is_compiling
|
|
428
|
+
if @execution_state.is_compiling
|
|
320
429
|
word.set_location(location)
|
|
321
|
-
@cur_definition.add_word(word)
|
|
430
|
+
@execution_state.cur_definition.add_word(word)
|
|
322
431
|
else
|
|
323
432
|
count_word(word)
|
|
324
433
|
word.execute(self)
|
|
325
434
|
end
|
|
326
435
|
end
|
|
436
|
+
|
|
437
|
+
private
|
|
438
|
+
|
|
439
|
+
# Find words similar to the given word name for error suggestions
|
|
440
|
+
# @param [String] word_name The unknown word to find suggestions for
|
|
441
|
+
# @return [Array<String>] Array of similar word names
|
|
442
|
+
def find_similar_words(word_name)
|
|
443
|
+
return [] if word_name.nil? || word_name.empty?
|
|
444
|
+
|
|
445
|
+
all_words = collect_available_words
|
|
446
|
+
|
|
447
|
+
# Find words with similar names using simple string distance
|
|
448
|
+
suggestions = all_words.select do |available_word|
|
|
449
|
+
levenshtein_distance(word_name.downcase, available_word.downcase) <= 2
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# If no close matches, try prefix matching
|
|
453
|
+
if suggestions.empty?
|
|
454
|
+
suggestions = all_words.select do |available_word|
|
|
455
|
+
available_word.downcase.start_with?(word_name[0, 2].downcase) ||
|
|
456
|
+
word_name.downcase.start_with?(available_word[0, 2].downcase)
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
suggestions.take(3)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Collect all available word names from all modules in scope
|
|
464
|
+
# @return [Array<String>] Array of all available word names
|
|
465
|
+
def collect_available_words
|
|
466
|
+
words = []
|
|
467
|
+
|
|
468
|
+
# Collect from global module (hardcoded common words since GlobalModule uses methods)
|
|
469
|
+
global_words = %w[
|
|
470
|
+
POP DUP SWAP >STR CONCAT SPLIT JOIN /N /R /T LOWERCASE UPPERCASE
|
|
471
|
+
APPEND REVERSE UNIQUE MAP FOREACH KEYS VALUES LENGTH RANGE SLICE
|
|
472
|
+
SELECT TAKE DROP NTH LAST FLATTEN REDUCE + - * / MOD
|
|
473
|
+
== != > >= < <= OR AND NOT IN BOOL INT FLOAT
|
|
474
|
+
VARIABLES ! @ !@ INTERPRET EXPORT USE-MODULES REC
|
|
475
|
+
]
|
|
476
|
+
words.concat(global_words)
|
|
477
|
+
|
|
478
|
+
# Collect from module stack
|
|
479
|
+
@execution_state.module_stack.each do |mod|
|
|
480
|
+
if mod.respond_to?(:words) && mod.words
|
|
481
|
+
words.concat(mod.words.map(&:name))
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
words.uniq.compact
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Calculate Levenshtein distance between two strings
|
|
489
|
+
# @param [String] str1
|
|
490
|
+
# @param [String] str2
|
|
491
|
+
# @return [Integer] The edit distance between the strings
|
|
492
|
+
def levenshtein_distance(str1, str2)
|
|
493
|
+
return str2.length if str1.empty?
|
|
494
|
+
return str1.length if str2.empty?
|
|
495
|
+
|
|
496
|
+
# Create matrix
|
|
497
|
+
matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1, 0) }
|
|
498
|
+
|
|
499
|
+
# Initialize first row and column
|
|
500
|
+
(0..str1.length).each { |i| matrix[i][0] = i }
|
|
501
|
+
(0..str2.length).each { |j| matrix[0][j] = j }
|
|
502
|
+
|
|
503
|
+
# Fill matrix
|
|
504
|
+
(1..str1.length).each do |i|
|
|
505
|
+
(1..str2.length).each do |j|
|
|
506
|
+
cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
|
|
507
|
+
matrix[i][j] = [
|
|
508
|
+
matrix[i - 1][j] + 1, # deletion
|
|
509
|
+
matrix[i][j - 1] + 1, # insertion
|
|
510
|
+
matrix[i - 1][j - 1] + cost # substitution
|
|
511
|
+
].min
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
matrix[str1.length][str2.length]
|
|
516
|
+
end
|
|
327
517
|
end
|
|
328
|
-
end
|
|
518
|
+
end
|
data/lib/forthic/token.rb
CHANGED
data/lib/forthic/tokenizer.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Forthic
|
|
4
4
|
class Tokenizer
|
|
5
5
|
attr_accessor :reference_location, :line, :column, :input_string, :input_pos,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
:whitespace, :quote_chars, :token_start_pos, :token_end_pos,
|
|
7
|
+
:token_line, :token_column, :token_string
|
|
8
8
|
|
|
9
9
|
# @param [String] string
|
|
10
10
|
# @param [CodeLocation, nil] reference_location
|
|
@@ -302,4 +302,4 @@ module Forthic
|
|
|
302
302
|
Token.new(TokenType::WORD, @token_string, get_token_location)
|
|
303
303
|
end
|
|
304
304
|
end
|
|
305
|
-
end
|
|
305
|
+
end
|
data/lib/forthic/variable.rb
CHANGED
data/lib/forthic/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative "word"
|
|
4
|
+
require_relative "../forthic_error"
|
|
5
5
|
|
|
6
6
|
module Forthic
|
|
7
7
|
class DefinitionWord < Word
|
|
@@ -9,7 +9,7 @@ module Forthic
|
|
|
9
9
|
|
|
10
10
|
# @param [String] name
|
|
11
11
|
def initialize(name)
|
|
12
|
-
super
|
|
12
|
+
super
|
|
13
13
|
@words = []
|
|
14
14
|
@cur_index = 0
|
|
15
15
|
end
|
|
@@ -22,19 +22,17 @@ module Forthic
|
|
|
22
22
|
# @param [Interpreter] interp
|
|
23
23
|
def execute(interp)
|
|
24
24
|
@words.each do |word|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
raise error
|
|
36
|
-
end
|
|
25
|
+
word.execute(interp)
|
|
26
|
+
rescue => e
|
|
27
|
+
error = ForthicError.new(
|
|
28
|
+
"definition_word-29",
|
|
29
|
+
"Error executing word #{word.name}",
|
|
30
|
+
"Error in #{name} definition",
|
|
31
|
+
interp.get_string_location
|
|
32
|
+
)
|
|
33
|
+
error.set_caught_error(e)
|
|
34
|
+
raise error
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
|
-
end
|
|
38
|
+
end
|