forthic 0.2.4 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +311 -21
- data/Rakefile +36 -7
- data/lib/forthic/decorators/docs.rb +69 -0
- data/lib/forthic/decorators/word.rb +331 -0
- data/lib/forthic/errors.rb +270 -0
- data/lib/forthic/grpc/client.rb +223 -0
- data/lib/forthic/grpc/errors.rb +149 -0
- data/lib/forthic/grpc/forthic_runtime_pb.rb +32 -0
- data/lib/forthic/grpc/forthic_runtime_services_pb.rb +31 -0
- data/lib/forthic/grpc/remote_module.rb +120 -0
- data/lib/forthic/grpc/remote_runtime_module.rb +148 -0
- data/lib/forthic/grpc/remote_word.rb +91 -0
- data/lib/forthic/grpc/runtime_manager.rb +60 -0
- data/lib/forthic/grpc/serializer.rb +184 -0
- data/lib/forthic/grpc/server.rb +361 -0
- data/lib/forthic/interpreter.rb +677 -318
- data/lib/forthic/literals.rb +170 -0
- data/lib/forthic/module.rb +383 -0
- data/lib/forthic/modules/standard/array_module.rb +940 -0
- data/lib/forthic/modules/standard/boolean_module.rb +176 -0
- data/lib/forthic/modules/standard/core_module.rb +362 -0
- data/lib/forthic/modules/standard/datetime_module.rb +349 -0
- data/lib/forthic/modules/standard/json_module.rb +55 -0
- data/lib/forthic/modules/standard/math_module.rb +365 -0
- data/lib/forthic/modules/standard/record_module.rb +203 -0
- data/lib/forthic/modules/standard/string_module.rb +170 -0
- data/lib/forthic/tokenizer.rb +224 -77
- data/lib/forthic/utils.rb +35 -0
- data/lib/forthic/websocket/handler.rb +548 -0
- data/lib/forthic/websocket/serializer.rb +160 -0
- data/lib/forthic/word_options.rb +141 -0
- data/lib/forthic.rb +30 -24
- data/protos/README.md +43 -0
- data/protos/v1/forthic_runtime.proto +200 -0
- metadata +73 -46
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -11
- data/CLAUDE.md +0 -74
- data/Guardfile +0 -42
- data/lib/forthic/code_location.rb +0 -35
- data/lib/forthic/errors/unknown_word_error.rb +0 -39
- data/lib/forthic/forthic_error.rb +0 -65
- data/lib/forthic/forthic_module.rb +0 -146
- data/lib/forthic/global_module.rb +0 -2328
- data/lib/forthic/positioned_string.rb +0 -19
- data/lib/forthic/token.rb +0 -37
- data/lib/forthic/variable.rb +0 -34
- data/lib/forthic/version.rb +0 -5
- data/lib/forthic/words/definition_word.rb +0 -38
- data/lib/forthic/words/end_array_word.rb +0 -28
- data/lib/forthic/words/end_module_word.rb +0 -16
- data/lib/forthic/words/imported_word.rb +0 -27
- data/lib/forthic/words/map_word.rb +0 -169
- data/lib/forthic/words/module_memo_bang_at_word.rb +0 -22
- data/lib/forthic/words/module_memo_bang_word.rb +0 -21
- data/lib/forthic/words/module_memo_word.rb +0 -35
- data/lib/forthic/words/module_word.rb +0 -21
- data/lib/forthic/words/push_value_word.rb +0 -21
- data/lib/forthic/words/start_module_word.rb +0 -31
- data/lib/forthic/words/word.rb +0 -30
- data/sig/forthic.rbs +0 -4
data/lib/forthic/interpreter.rb
CHANGED
|
@@ -1,518 +1,877 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
|
|
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"
|
|
3
|
+
require_relative 'tokenizer'
|
|
4
|
+
require_relative 'module'
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'literals'
|
|
7
|
+
require 'time'
|
|
16
8
|
|
|
17
9
|
module Forthic
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
10
|
+
# Timestamp - Struct for profiling timestamps
|
|
11
|
+
Timestamp = Struct.new(:label, :time_ms, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# StartModuleWord - Handles module creation and switching
|
|
14
|
+
#
|
|
15
|
+
# Pushes a module onto the module stack, creating it if necessary.
|
|
16
|
+
# An empty name refers to the app module.
|
|
17
|
+
class StartModuleWord < Word
|
|
18
|
+
def execute(interp)
|
|
19
|
+
# The app module is the only module with a blank name
|
|
20
|
+
if @name == ""
|
|
21
|
+
interp.module_stack_push(interp.get_app_module)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# If the module is used by the current module, push it onto the stack, otherwise
|
|
26
|
+
# create a new module.
|
|
27
|
+
mod = interp.cur_module.find_module(@name)
|
|
28
|
+
unless mod
|
|
29
|
+
mod = Module.new(@name)
|
|
30
|
+
interp.cur_module.register_module(mod.name, mod.name, mod)
|
|
31
|
+
|
|
32
|
+
# If we're at the app module, also register with interpreter
|
|
33
|
+
if interp.cur_module.name == ""
|
|
34
|
+
interp.register_module(mod)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
interp.module_stack_push(mod)
|
|
38
|
+
end
|
|
31
39
|
end
|
|
32
40
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
# EndModuleWord - Pops the current module from the module stack
|
|
42
|
+
#
|
|
43
|
+
# Completes module context and returns to the previous module.
|
|
44
|
+
class EndModuleWord < Word
|
|
45
|
+
def initialize
|
|
46
|
+
super("}")
|
|
47
|
+
end
|
|
37
48
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
@module_stack = [app_module]
|
|
41
|
-
@is_compiling = false
|
|
42
|
-
@should_stop = false
|
|
43
|
-
@is_memo_definition = false
|
|
44
|
-
@cur_definition = nil
|
|
45
|
-
@string_location = nil
|
|
49
|
+
def execute(interp)
|
|
50
|
+
interp.module_stack_pop
|
|
46
51
|
end
|
|
52
|
+
end
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
# EndArrayWord - Collects items from stack into an array
|
|
55
|
+
#
|
|
56
|
+
# Pops items from the stack until a START_ARRAY token is found,
|
|
57
|
+
# then pushes them as a single array in the correct order.
|
|
58
|
+
class EndArrayWord < Word
|
|
59
|
+
def initialize
|
|
60
|
+
super("]")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def execute(interp)
|
|
64
|
+
items = []
|
|
65
|
+
item = interp.stack_pop
|
|
66
|
+
# NOTE: This won't infinite loop because interp.stack_pop will eventually fail
|
|
67
|
+
loop do
|
|
68
|
+
break if item.is_a?(Token) && item.type == TokenType::START_ARRAY
|
|
69
|
+
items << item
|
|
70
|
+
item = interp.stack_pop
|
|
71
|
+
end
|
|
72
|
+
items.reverse!
|
|
73
|
+
interp.stack_push(items)
|
|
55
74
|
end
|
|
56
75
|
end
|
|
57
76
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
# Stack - Wrapper for the interpreter's data stack
|
|
78
|
+
#
|
|
79
|
+
# Provides stack operations with support for array indexing.
|
|
80
|
+
# Handles PositionedString unwrapping and provides JSON serialization.
|
|
81
|
+
# Items can be accessed with bracket notation (e.g., stack[0]).
|
|
82
|
+
class Stack
|
|
83
|
+
def initialize(items = [])
|
|
84
|
+
@items = items
|
|
85
|
+
end
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@
|
|
65
|
-
@start_profile_time = nil
|
|
66
|
-
@timestamps = []
|
|
87
|
+
# Array-like access methods
|
|
88
|
+
def [](index)
|
|
89
|
+
@items[index]
|
|
67
90
|
end
|
|
68
91
|
|
|
69
|
-
def
|
|
70
|
-
@
|
|
71
|
-
@timestamps = []
|
|
72
|
-
@start_profile_time = Time.now
|
|
73
|
-
add_timestamp("START")
|
|
74
|
-
@word_counts = {}
|
|
92
|
+
def []=(index, value)
|
|
93
|
+
@items[index] = value
|
|
75
94
|
end
|
|
76
95
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
@is_profiling = false
|
|
96
|
+
def length
|
|
97
|
+
@items.length
|
|
80
98
|
end
|
|
81
99
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@
|
|
100
|
+
alias size length
|
|
101
|
+
|
|
102
|
+
def get_items
|
|
103
|
+
@items.map do |item|
|
|
104
|
+
if item.is_a?(PositionedString)
|
|
105
|
+
item.value
|
|
106
|
+
else
|
|
107
|
+
item
|
|
108
|
+
end
|
|
109
|
+
end
|
|
86
110
|
end
|
|
87
111
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
timestamp = {label: label, time_ms: (Time.now - @start_profile_time) * 1000}
|
|
91
|
-
@timestamps.push(timestamp)
|
|
112
|
+
def get_raw_items
|
|
113
|
+
@items
|
|
92
114
|
end
|
|
93
115
|
|
|
94
|
-
def
|
|
95
|
-
@
|
|
116
|
+
def set_raw_items(items)
|
|
117
|
+
@items = items
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_json(*args)
|
|
121
|
+
@items.to_json(*args)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pop
|
|
125
|
+
@items.pop
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def push(item)
|
|
129
|
+
@items.push(item)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Duplicate stack with a shallow copy of items
|
|
133
|
+
def dup
|
|
134
|
+
Stack.new(@items.dup)
|
|
96
135
|
end
|
|
97
136
|
end
|
|
98
137
|
|
|
138
|
+
# Interpreter - Base Forthic interpreter
|
|
139
|
+
#
|
|
140
|
+
# Core interpreter that tokenizes and executes Forthic code.
|
|
141
|
+
# Manages the data stack, module stack, and execution context.
|
|
142
|
+
#
|
|
143
|
+
# Features:
|
|
144
|
+
# - Stack-based execution model
|
|
145
|
+
# - Module system with imports and namespacing
|
|
146
|
+
# - Literal handlers for parsing values (numbers, dates, booleans, etc.)
|
|
147
|
+
# - Error handling with recovery attempts
|
|
148
|
+
# - Profiling and performance tracking
|
|
149
|
+
# - Streaming execution support
|
|
150
|
+
#
|
|
151
|
+
# Note: This is the base interpreter without standard library modules.
|
|
152
|
+
# Use StandardInterpreter for a full-featured interpreter with stdlib.
|
|
99
153
|
class Interpreter
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
154
|
+
attr_reader :timezone, :stack
|
|
155
|
+
attr_accessor :handle_error, :max_attempts, :on_word_execute
|
|
120
156
|
|
|
121
|
-
def initialize
|
|
157
|
+
def initialize(modules = [], timezone = "UTC")
|
|
158
|
+
@timezone = timezone
|
|
159
|
+
@stack = Stack.new
|
|
160
|
+
|
|
161
|
+
@tokenizer_stack = []
|
|
162
|
+
@max_attempts = 3
|
|
163
|
+
@handle_error = nil
|
|
164
|
+
|
|
165
|
+
@app_module = Module.new("")
|
|
166
|
+
@app_module.set_interp(self)
|
|
167
|
+
@module_stack = [@app_module]
|
|
122
168
|
@registered_modules = {}
|
|
123
|
-
@
|
|
124
|
-
@
|
|
125
|
-
@
|
|
169
|
+
@is_compiling = false
|
|
170
|
+
@is_memo_definition = false
|
|
171
|
+
@cur_definition = nil
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
@
|
|
173
|
+
# Debug support
|
|
174
|
+
@string_location = nil
|
|
175
|
+
@previous_token = nil
|
|
129
176
|
|
|
130
|
-
|
|
131
|
-
@
|
|
132
|
-
|
|
177
|
+
# Profiling support
|
|
178
|
+
@word_counts = {}
|
|
179
|
+
@is_profiling = false
|
|
180
|
+
@start_profile_time = nil
|
|
181
|
+
@timestamps = []
|
|
182
|
+
|
|
183
|
+
# Streaming support
|
|
184
|
+
@streaming_token_index = 0
|
|
185
|
+
@stream = false
|
|
186
|
+
@previous_delta_length = 0
|
|
187
|
+
|
|
188
|
+
# Progress callback for streaming execution
|
|
189
|
+
@on_word_execute = nil
|
|
190
|
+
@word_execution_count = 0
|
|
191
|
+
@total_words_estimate = 0
|
|
133
192
|
|
|
134
|
-
|
|
135
|
-
@
|
|
193
|
+
# Literal handlers
|
|
194
|
+
@literal_handlers = []
|
|
195
|
+
register_standard_literals
|
|
196
|
+
|
|
197
|
+
# If modules are provided, import them unprefixed as a convenience
|
|
198
|
+
import_modules(modules)
|
|
136
199
|
end
|
|
137
200
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@app_module
|
|
201
|
+
def get_timezone
|
|
202
|
+
@timezone
|
|
141
203
|
end
|
|
142
204
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
@execution_state.string_location
|
|
205
|
+
def set_timezone(timezone)
|
|
206
|
+
@timezone = timezone
|
|
146
207
|
end
|
|
147
208
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@execution_state.stack
|
|
209
|
+
def get_app_module
|
|
210
|
+
@app_module
|
|
151
211
|
end
|
|
152
212
|
|
|
153
|
-
def
|
|
154
|
-
@
|
|
213
|
+
def get_top_input_string
|
|
214
|
+
return "" if @tokenizer_stack.empty?
|
|
215
|
+
@tokenizer_stack[0].get_input_string
|
|
155
216
|
end
|
|
156
217
|
|
|
157
|
-
def
|
|
158
|
-
@
|
|
218
|
+
def get_tokenizer
|
|
219
|
+
@tokenizer_stack.last
|
|
159
220
|
end
|
|
160
221
|
|
|
161
|
-
def
|
|
162
|
-
@
|
|
222
|
+
def get_string_location
|
|
223
|
+
@string_location
|
|
163
224
|
end
|
|
164
225
|
|
|
165
|
-
def
|
|
166
|
-
@
|
|
226
|
+
def set_max_attempts(max_attempts)
|
|
227
|
+
@max_attempts = max_attempts
|
|
167
228
|
end
|
|
168
229
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def set_flags(module_id, flags)
|
|
172
|
-
@default_module_flags[module_id] = flags
|
|
173
|
-
@module_flags[module_id] = flags
|
|
230
|
+
def set_error_handler(handle_error)
|
|
231
|
+
@handle_error = handle_error
|
|
174
232
|
end
|
|
175
233
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def get_flags(module_id)
|
|
179
|
-
module_flags = @module_flags[module_id] || {}
|
|
180
|
-
result = module_flags.dup
|
|
181
|
-
@module_flags[module_id] = @default_module_flags[module_id].dup
|
|
182
|
-
result
|
|
234
|
+
def get_max_attempts
|
|
235
|
+
@max_attempts
|
|
183
236
|
end
|
|
184
237
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def modify_flags(module_id, flags)
|
|
188
|
-
module_flags = @module_flags[module_id] || {}
|
|
189
|
-
@module_flags[module_id] = module_flags.merge(flags)
|
|
238
|
+
def get_error_handler
|
|
239
|
+
@handle_error
|
|
190
240
|
end
|
|
191
241
|
|
|
192
242
|
def reset
|
|
243
|
+
@stack = Stack.new
|
|
193
244
|
@app_module.variables = {}
|
|
194
|
-
@execution_state.reset(@app_module)
|
|
195
|
-
end
|
|
196
245
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
246
|
+
@module_stack = [@app_module]
|
|
247
|
+
@is_compiling = false
|
|
248
|
+
@is_memo_definition = false
|
|
249
|
+
@cur_definition = nil
|
|
250
|
+
|
|
251
|
+
# Debug support
|
|
252
|
+
@string_location = nil
|
|
203
253
|
end
|
|
204
254
|
|
|
205
|
-
# @param [String] string
|
|
206
|
-
# @param [CodeLocation, nil] reference_location
|
|
207
|
-
# @return [Boolean]
|
|
208
255
|
def run(string, reference_location = nil)
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
@tokenizer_stack.push(Tokenizer.new(string, reference_location))
|
|
257
|
+
|
|
258
|
+
if @handle_error
|
|
259
|
+
execute_with_recovery
|
|
260
|
+
else
|
|
261
|
+
run_with_tokenizer(@tokenizer_stack.last)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
@tokenizer_stack.pop
|
|
265
|
+
true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def execute_with_recovery(num_attempts = 0)
|
|
269
|
+
num_attempts += 1
|
|
270
|
+
if num_attempts > @max_attempts
|
|
271
|
+
raise TooManyAttemptsError.new(
|
|
272
|
+
get_top_input_string,
|
|
273
|
+
num_attempts,
|
|
274
|
+
@max_attempts
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
continue_execution
|
|
278
|
+
num_attempts
|
|
279
|
+
rescue => e
|
|
280
|
+
raise unless @handle_error
|
|
281
|
+
@handle_error.call(e, self)
|
|
282
|
+
execute_with_recovery(num_attempts)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def continue_execution
|
|
286
|
+
run_with_tokenizer(@tokenizer_stack.last)
|
|
211
287
|
end
|
|
212
288
|
|
|
213
|
-
# @param [Tokenizer] tokenizer
|
|
214
|
-
# @return [Boolean]
|
|
215
289
|
def run_with_tokenizer(tokenizer)
|
|
216
290
|
token = nil
|
|
217
291
|
loop do
|
|
292
|
+
@previous_token = token
|
|
218
293
|
token = tokenizer.next_token
|
|
219
294
|
handle_token(token)
|
|
220
|
-
break if token.type == TokenType::EOS
|
|
221
|
-
next if [TokenType::START_DEF, TokenType::END_DEF, TokenType::COMMENT].include?(token.type) || @execution_state.is_compiling
|
|
295
|
+
break if token.type == TokenType::EOS
|
|
222
296
|
end
|
|
223
|
-
true
|
|
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
|
|
297
|
+
true # Done executing
|
|
232
298
|
end
|
|
233
299
|
|
|
234
|
-
# @return [ForthicModule]
|
|
235
300
|
def cur_module
|
|
236
|
-
@
|
|
301
|
+
@module_stack.last
|
|
237
302
|
end
|
|
238
303
|
|
|
239
|
-
# @param [String] name
|
|
240
|
-
# @return [ForthicModule]
|
|
241
304
|
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
|
-
|
|
245
305
|
result = @registered_modules[name]
|
|
246
|
-
|
|
306
|
+
unless result
|
|
307
|
+
raise UnknownModuleError.new(
|
|
308
|
+
get_top_input_string,
|
|
309
|
+
name,
|
|
310
|
+
location: @string_location
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
result
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def stack_peek
|
|
317
|
+
top = @stack[@stack.length - 1]
|
|
318
|
+
result = top
|
|
319
|
+
result = top.value if top.is_a?(PositionedString)
|
|
247
320
|
result
|
|
248
321
|
end
|
|
249
322
|
|
|
250
|
-
# @param [Object] val
|
|
251
323
|
def stack_push(val)
|
|
252
|
-
@
|
|
324
|
+
@stack.push(val)
|
|
253
325
|
end
|
|
254
326
|
|
|
255
|
-
# @return [Object]
|
|
256
327
|
def stack_pop
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
328
|
+
if @stack.length.zero?
|
|
329
|
+
tokenizer = @tokenizer_stack.length > 0 ? get_tokenizer : nil
|
|
330
|
+
raise StackUnderflowError.new(
|
|
331
|
+
get_top_input_string,
|
|
332
|
+
location: tokenizer&.get_token_location
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
result = @stack.pop
|
|
336
|
+
|
|
337
|
+
# If we have a PositionedString, we need to record the location
|
|
338
|
+
@string_location = nil
|
|
339
|
+
if result.is_a?(PositionedString)
|
|
340
|
+
positioned_string = result
|
|
341
|
+
result = positioned_string.value
|
|
342
|
+
@string_location = positioned_string.location
|
|
343
|
+
end
|
|
344
|
+
result
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def get_stack
|
|
348
|
+
@stack
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def set_stack(stack)
|
|
352
|
+
@stack = stack
|
|
261
353
|
end
|
|
262
354
|
|
|
263
|
-
# @param [ForthicModule] mod
|
|
264
355
|
def module_stack_push(mod)
|
|
265
|
-
|
|
266
|
-
@execution_state.module_stack.push(mod)
|
|
356
|
+
@module_stack.push(mod)
|
|
267
357
|
end
|
|
268
358
|
|
|
269
359
|
def module_stack_pop
|
|
270
|
-
@
|
|
360
|
+
@module_stack.pop
|
|
271
361
|
end
|
|
272
362
|
|
|
273
|
-
# @param [ForthicModule] mod
|
|
274
363
|
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)
|
|
277
364
|
@registered_modules[mod.name] = mod
|
|
365
|
+
mod.set_interp(self)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# If names is an array of strings, import each module without a prefix (empty string)
|
|
369
|
+
# If names is an array of arrays, import each module using the first element as the
|
|
370
|
+
# module name and the second element as the prefix
|
|
371
|
+
def use_modules(names)
|
|
372
|
+
names.each do |name|
|
|
373
|
+
module_name = name
|
|
374
|
+
prefix = "" # Default to empty prefix (no prefix)
|
|
375
|
+
if name.is_a?(Array)
|
|
376
|
+
module_name = name[0]
|
|
377
|
+
prefix = name[1] # Allow explicit prefix specification
|
|
378
|
+
end
|
|
379
|
+
mod = find_module(module_name)
|
|
380
|
+
get_app_module.import_module(prefix, mod, self)
|
|
381
|
+
end
|
|
278
382
|
end
|
|
279
383
|
|
|
280
|
-
#
|
|
281
|
-
# @param [String] prefix
|
|
384
|
+
# A convenience method to register and use a module
|
|
282
385
|
def import_module(mod, prefix = "")
|
|
283
|
-
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
284
386
|
register_module(mod)
|
|
285
|
-
|
|
387
|
+
use_modules([[mod.name, prefix]])
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def import_modules(modules)
|
|
391
|
+
modules.each do |mod|
|
|
392
|
+
import_module(mod)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Transforms simple module names to unprefixed imports: "math" -> ["math", ""]
|
|
397
|
+
# Preserves explicit prefix specifications: ["math", "m"] -> ["math", "m"]
|
|
398
|
+
def use_modules_unprefixed(names)
|
|
399
|
+
unprefixed = names.map do |name|
|
|
400
|
+
name.is_a?(Array) ? name : [name, ""]
|
|
401
|
+
end
|
|
402
|
+
use_modules(unprefixed)
|
|
286
403
|
end
|
|
287
404
|
|
|
288
|
-
# @param [ForthicModule] mod
|
|
289
405
|
def run_module_code(mod)
|
|
290
|
-
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
291
406
|
module_stack_push(mod)
|
|
292
|
-
|
|
407
|
+
begin
|
|
408
|
+
# Set source to module name when running module code
|
|
409
|
+
module_location = CodeLocation.new(source: mod.name)
|
|
410
|
+
run(mod.forthic_code, module_location)
|
|
411
|
+
rescue => e
|
|
412
|
+
raise ModuleError.new(
|
|
413
|
+
get_top_input_string,
|
|
414
|
+
mod.name,
|
|
415
|
+
e,
|
|
416
|
+
location: @string_location,
|
|
417
|
+
cause: e
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
|
|
293
421
|
module_stack_pop
|
|
294
|
-
rescue => e
|
|
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.")
|
|
296
|
-
error.set_caught_error(e)
|
|
297
|
-
raise error
|
|
298
422
|
end
|
|
299
423
|
|
|
300
|
-
#
|
|
301
|
-
#
|
|
424
|
+
# ======================
|
|
425
|
+
# Literal Handlers
|
|
426
|
+
|
|
427
|
+
# Register standard literal handlers
|
|
428
|
+
# Order matters: more specific handlers first
|
|
429
|
+
def register_standard_literals
|
|
430
|
+
@literal_handlers = [
|
|
431
|
+
method(:to_bool).to_proc, # TRUE, FALSE
|
|
432
|
+
method(:to_float).to_proc, # 3.14
|
|
433
|
+
Forthic.to_zoned_datetime(@timezone), # 2020-06-05T10:15:00Z
|
|
434
|
+
Forthic.to_literal_date(@timezone), # 2020-06-05, YYYY-MM-DD
|
|
435
|
+
method(:to_time).to_proc, # 9:00, 11:30 PM
|
|
436
|
+
method(:to_int).to_proc, # 42
|
|
437
|
+
]
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Register a custom literal handler
|
|
441
|
+
# Handlers are checked in registration order
|
|
442
|
+
def register_literal_handler(handler)
|
|
443
|
+
@literal_handlers << handler
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Unregister a literal handler
|
|
447
|
+
def unregister_literal_handler(handler)
|
|
448
|
+
@literal_handlers.delete(handler)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Try to parse string as a literal value
|
|
452
|
+
# Returns PushValueWord if successful, nil otherwise
|
|
453
|
+
def find_literal_word(name)
|
|
454
|
+
@literal_handlers.each do |handler|
|
|
455
|
+
value = handler.call(name)
|
|
456
|
+
return PushValueWord.new(name, value) unless value.nil?
|
|
457
|
+
end
|
|
458
|
+
nil
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# ======================
|
|
462
|
+
# Find Word
|
|
463
|
+
|
|
302
464
|
def find_word(name)
|
|
465
|
+
# 1. Check module stack (dictionary words + variables)
|
|
303
466
|
result = nil
|
|
304
|
-
@
|
|
467
|
+
(@module_stack.length - 1).downto(0) do |i|
|
|
468
|
+
m = @module_stack[i]
|
|
305
469
|
result = m.find_word(name)
|
|
306
470
|
break if result
|
|
307
471
|
end
|
|
308
|
-
|
|
472
|
+
|
|
473
|
+
# 2. Check literal handlers as fallback
|
|
474
|
+
result = find_literal_word(name) unless result
|
|
475
|
+
|
|
476
|
+
# 3. Throw error if still not found
|
|
477
|
+
unless result
|
|
478
|
+
raise UnknownWordError.new(
|
|
479
|
+
get_top_input_string,
|
|
480
|
+
name,
|
|
481
|
+
location: get_string_location
|
|
482
|
+
)
|
|
483
|
+
end
|
|
484
|
+
|
|
309
485
|
result
|
|
310
486
|
end
|
|
311
487
|
|
|
312
|
-
#
|
|
488
|
+
# ======================
|
|
489
|
+
# Profiling
|
|
490
|
+
|
|
313
491
|
def start_profiling
|
|
314
|
-
@
|
|
492
|
+
@is_profiling = true
|
|
493
|
+
@timestamps = []
|
|
494
|
+
@start_profile_time = Time.now.to_f * 1000 # milliseconds
|
|
495
|
+
add_timestamp("START")
|
|
496
|
+
@word_counts = {}
|
|
315
497
|
end
|
|
316
498
|
|
|
317
|
-
def
|
|
318
|
-
@
|
|
499
|
+
def count_word(word)
|
|
500
|
+
return unless @is_profiling
|
|
501
|
+
name = word.name
|
|
502
|
+
@word_counts[name] ||= 0
|
|
503
|
+
@word_counts[name] += 1
|
|
319
504
|
end
|
|
320
505
|
|
|
321
|
-
def
|
|
322
|
-
|
|
506
|
+
def stop_profiling
|
|
507
|
+
add_timestamp("END")
|
|
508
|
+
@is_profiling = false
|
|
323
509
|
end
|
|
324
510
|
|
|
325
511
|
def add_timestamp(label)
|
|
326
|
-
@
|
|
512
|
+
return unless @is_profiling
|
|
513
|
+
timestamp = Timestamp.new(
|
|
514
|
+
label: label,
|
|
515
|
+
time_ms: (Time.now.to_f * 1000) - @start_profile_time
|
|
516
|
+
)
|
|
517
|
+
@timestamps << timestamp
|
|
327
518
|
end
|
|
328
519
|
|
|
329
520
|
def word_histogram
|
|
330
|
-
|
|
521
|
+
items = []
|
|
522
|
+
@word_counts.each do |name, count|
|
|
523
|
+
items << { word: name, count: count }
|
|
524
|
+
end
|
|
525
|
+
items.sort_by { |item| -item[:count] }
|
|
331
526
|
end
|
|
332
527
|
|
|
333
528
|
def profile_timestamps
|
|
334
|
-
@
|
|
529
|
+
@timestamps
|
|
335
530
|
end
|
|
336
531
|
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
return if token.type == TokenType::EOS
|
|
532
|
+
# ======================
|
|
533
|
+
# Handle tokens
|
|
340
534
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
535
|
+
def handle_token(token)
|
|
536
|
+
case token.type
|
|
537
|
+
when TokenType::STRING
|
|
538
|
+
handle_string_token(token)
|
|
539
|
+
when TokenType::COMMENT
|
|
540
|
+
handle_comment_token(token)
|
|
541
|
+
when TokenType::START_ARRAY
|
|
542
|
+
handle_start_array_token(token)
|
|
543
|
+
when TokenType::END_ARRAY
|
|
544
|
+
handle_end_array_token(token)
|
|
545
|
+
when TokenType::START_MODULE
|
|
546
|
+
handle_start_module_token(token)
|
|
547
|
+
when TokenType::END_MODULE
|
|
548
|
+
handle_end_module_token(token)
|
|
549
|
+
when TokenType::START_DEF
|
|
550
|
+
handle_start_definition_token(token)
|
|
551
|
+
when TokenType::START_MEMO
|
|
552
|
+
handle_start_memo_token(token)
|
|
553
|
+
when TokenType::END_DEF
|
|
554
|
+
handle_end_definition_token(token)
|
|
555
|
+
when TokenType::DOT_SYMBOL
|
|
556
|
+
handle_dot_symbol_token(token)
|
|
557
|
+
when TokenType::WORD
|
|
558
|
+
handle_word_token(token)
|
|
559
|
+
when TokenType::EOS
|
|
560
|
+
if @is_compiling
|
|
561
|
+
raise MissingSemicolonError.new(
|
|
562
|
+
get_top_input_string,
|
|
563
|
+
location: @previous_token&.location
|
|
564
|
+
)
|
|
565
|
+
end
|
|
344
566
|
else
|
|
345
|
-
raise
|
|
567
|
+
raise UnknownTokenError.new(
|
|
568
|
+
get_top_input_string,
|
|
569
|
+
token.string,
|
|
570
|
+
location: @string_location
|
|
571
|
+
)
|
|
346
572
|
end
|
|
347
573
|
end
|
|
348
574
|
|
|
349
|
-
# @param [Token] token
|
|
350
575
|
def handle_string_token(token)
|
|
351
576
|
value = PositionedString.new(token.string, token.location)
|
|
352
577
|
handle_word(PushValueWord.new("<string>", value))
|
|
353
578
|
end
|
|
354
579
|
|
|
355
|
-
|
|
580
|
+
def handle_dot_symbol_token(token)
|
|
581
|
+
value = PositionedString.new(token.string, token.location)
|
|
582
|
+
handle_word(PushValueWord.new("<dot-symbol>", value))
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Start/end module tokens are treated as IMMEDIATE words *and* are also compiled
|
|
356
586
|
def handle_start_module_token(token)
|
|
357
587
|
word = StartModuleWord.new(token.string)
|
|
358
|
-
|
|
359
|
-
|
|
588
|
+
|
|
589
|
+
@cur_definition.add_word(word) if @is_compiling
|
|
590
|
+
count_word(word) # For profiling
|
|
360
591
|
word.execute(self)
|
|
361
592
|
end
|
|
362
593
|
|
|
363
|
-
# @param [Token] _token
|
|
364
594
|
def handle_end_module_token(_token)
|
|
365
595
|
word = EndModuleWord.new
|
|
366
|
-
|
|
596
|
+
|
|
597
|
+
@cur_definition.add_word(word) if @is_compiling
|
|
367
598
|
count_word(word)
|
|
368
599
|
word.execute(self)
|
|
369
600
|
end
|
|
370
601
|
|
|
371
|
-
# @param [Token] token
|
|
372
602
|
def handle_start_array_token(token)
|
|
373
603
|
handle_word(PushValueWord.new("<start_array_token>", token))
|
|
374
604
|
end
|
|
375
605
|
|
|
376
|
-
# @param [Token] _token
|
|
377
606
|
def handle_end_array_token(_token)
|
|
378
607
|
handle_word(EndArrayWord.new)
|
|
379
608
|
end
|
|
380
609
|
|
|
381
|
-
# @param [Token] _token
|
|
382
610
|
def handle_comment_token(_token)
|
|
383
|
-
#
|
|
611
|
+
# Comment handling (currently no-op)
|
|
384
612
|
end
|
|
385
613
|
|
|
386
|
-
# @param [Token] token
|
|
387
614
|
def handle_start_definition_token(token)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
615
|
+
if @is_compiling
|
|
616
|
+
raise MissingSemicolonError.new(
|
|
617
|
+
get_top_input_string,
|
|
618
|
+
location: @previous_token&.location
|
|
619
|
+
)
|
|
620
|
+
end
|
|
621
|
+
@cur_definition = DefinitionWord.new(token.string)
|
|
622
|
+
@is_compiling = true
|
|
623
|
+
@is_memo_definition = false
|
|
392
624
|
end
|
|
393
625
|
|
|
394
|
-
# @param [Token] token
|
|
395
626
|
def handle_start_memo_token(token)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
627
|
+
if @is_compiling
|
|
628
|
+
raise MissingSemicolonError.new(
|
|
629
|
+
get_top_input_string,
|
|
630
|
+
location: @previous_token&.location
|
|
631
|
+
)
|
|
632
|
+
end
|
|
633
|
+
@cur_definition = DefinitionWord.new(token.string)
|
|
634
|
+
@is_compiling = true
|
|
635
|
+
@is_memo_definition = true
|
|
400
636
|
end
|
|
401
637
|
|
|
402
|
-
# @param [Token] token
|
|
403
638
|
def handle_end_definition_token(token)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
639
|
+
unless @is_compiling && @cur_definition
|
|
640
|
+
raise ExtraSemicolonError.new(
|
|
641
|
+
get_top_input_string,
|
|
642
|
+
location: token.location
|
|
643
|
+
)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
if @is_memo_definition
|
|
647
|
+
cur_module.add_memo_words(@cur_definition)
|
|
408
648
|
else
|
|
409
|
-
cur_module.add_word(@
|
|
649
|
+
cur_module.add_word(@cur_definition)
|
|
410
650
|
end
|
|
411
|
-
@
|
|
651
|
+
@is_compiling = false
|
|
412
652
|
end
|
|
413
653
|
|
|
414
|
-
# @param [Token] token
|
|
415
654
|
def handle_word_token(token)
|
|
416
|
-
word = find_word(token.string)
|
|
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
|
|
655
|
+
word = find_word(token.string) # Throws UnknownWordError if not found
|
|
422
656
|
handle_word(word, token.location)
|
|
423
657
|
end
|
|
424
658
|
|
|
425
|
-
# @param [Word] word
|
|
426
|
-
# @param [CodeLocation, nil] location
|
|
427
659
|
def handle_word(word, location = nil)
|
|
428
|
-
if @
|
|
660
|
+
if @is_compiling
|
|
429
661
|
word.set_location(location)
|
|
430
|
-
@
|
|
662
|
+
@cur_definition.add_word(word)
|
|
431
663
|
else
|
|
432
664
|
count_word(word)
|
|
665
|
+
|
|
666
|
+
# Notify progress callback if set (for streaming execution)
|
|
667
|
+
if @on_word_execute && word.is_a?(DefinitionWord)
|
|
668
|
+
@word_execution_count += 1
|
|
669
|
+
@on_word_execute.call(word.name, @word_execution_count, @total_words_estimate)
|
|
670
|
+
end
|
|
671
|
+
|
|
433
672
|
word.execute(self)
|
|
434
673
|
end
|
|
435
674
|
end
|
|
436
675
|
|
|
437
|
-
|
|
676
|
+
# Estimate the number of words in code (for progress tracking)
|
|
677
|
+
def estimate_word_count(code)
|
|
678
|
+
# Simple heuristic: count uppercase words (likely Forthic words)
|
|
679
|
+
code.scan(/[A-Z][A-Z0-9_-]*/).length
|
|
680
|
+
end
|
|
438
681
|
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
682
|
+
# Set up progress tracking for streaming execution
|
|
683
|
+
def setup_progress_tracking(code)
|
|
684
|
+
@word_execution_count = 0
|
|
685
|
+
@total_words_estimate = estimate_word_count(code)
|
|
686
|
+
end
|
|
444
687
|
|
|
445
|
-
|
|
688
|
+
# Reset progress tracking
|
|
689
|
+
def reset_progress_tracking
|
|
690
|
+
@word_execution_count = 0
|
|
691
|
+
@total_words_estimate = 0
|
|
692
|
+
@on_word_execute = nil
|
|
693
|
+
end
|
|
446
694
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
695
|
+
# Streaming execution support
|
|
696
|
+
def streaming_run(code_stream, done, reference_location = nil)
|
|
697
|
+
# Create a new Tokenizer for the full string
|
|
698
|
+
tokenizer = Tokenizer.new(code_stream, reference_location, done ? false : true)
|
|
699
|
+
tokens = []
|
|
700
|
+
eos_found = false
|
|
451
701
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
702
|
+
@tokenizer_stack.push(tokenizer)
|
|
703
|
+
|
|
704
|
+
# Gather tokens from the beginning
|
|
705
|
+
loop do
|
|
706
|
+
token = tokenizer.next_token
|
|
707
|
+
break unless token
|
|
708
|
+
|
|
709
|
+
# If we hit an EOS token then push it and break
|
|
710
|
+
if token.type == TokenType::EOS
|
|
711
|
+
tokens << token
|
|
712
|
+
eos_found = true
|
|
713
|
+
break
|
|
457
714
|
end
|
|
715
|
+
|
|
716
|
+
tokens << token
|
|
458
717
|
end
|
|
459
718
|
|
|
460
|
-
|
|
461
|
-
end
|
|
719
|
+
delta = eos_found ? nil : tokenizer.get_string_delta
|
|
462
720
|
|
|
463
|
-
|
|
464
|
-
# @return [Array<String>] Array of all available word names
|
|
465
|
-
def collect_available_words
|
|
466
|
-
words = []
|
|
721
|
+
new_stop = find_last_word_or_eos(tokens)
|
|
467
722
|
|
|
468
|
-
|
|
469
|
-
|
|
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)
|
|
723
|
+
new_stop -= 1 if eos_found && !done
|
|
724
|
+
new_stop += 1 if !eos_found && !done
|
|
477
725
|
|
|
478
|
-
#
|
|
479
|
-
@
|
|
480
|
-
|
|
481
|
-
|
|
726
|
+
# Execute only tokens we have not executed previously
|
|
727
|
+
(@streaming_token_index...new_stop).each do |i|
|
|
728
|
+
token = tokens[i]
|
|
729
|
+
next unless token
|
|
730
|
+
|
|
731
|
+
handle_token(token)
|
|
732
|
+
|
|
733
|
+
if @stream && (token.type != TokenType::WORD || token.string != "START-LOG")
|
|
734
|
+
yield token.string
|
|
482
735
|
end
|
|
736
|
+
@previous_token = token
|
|
483
737
|
end
|
|
484
738
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
739
|
+
# Done with this tokenizer
|
|
740
|
+
@tokenizer_stack.pop
|
|
741
|
+
|
|
742
|
+
if @stream && !eos_found && delta
|
|
743
|
+
# Yield string delta if we're streaming and tokenizer has a delta
|
|
744
|
+
new_portion = delta[@previous_delta_length..-1]
|
|
745
|
+
|
|
746
|
+
yield({ string_delta: new_portion }) if new_portion && !new_portion.empty?
|
|
747
|
+
@previous_delta_length = delta.length
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
if done
|
|
751
|
+
end_stream
|
|
752
|
+
return
|
|
513
753
|
end
|
|
514
754
|
|
|
515
|
-
|
|
755
|
+
# Update our pointer and reset if done
|
|
756
|
+
@streaming_token_index = new_stop
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def start_stream
|
|
760
|
+
@stream = true
|
|
761
|
+
@previous_delta_length = 0
|
|
762
|
+
@streaming_token_index = 0
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def end_stream
|
|
766
|
+
@stream = false
|
|
767
|
+
@previous_delta_length = 0
|
|
768
|
+
@streaming_token_index = 0
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
private
|
|
772
|
+
|
|
773
|
+
def find_last_word_or_eos(tokens)
|
|
774
|
+
tokens.rindex { |token| token.type == TokenType::WORD || token.type == TokenType::EOS } || -1
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Wrapper methods for literal handlers to match expected interface
|
|
778
|
+
def to_bool(str)
|
|
779
|
+
Forthic.to_bool(str)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def to_float(str)
|
|
783
|
+
Forthic.to_float(str)
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def to_int(str)
|
|
787
|
+
Forthic.to_int(str)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def to_time(str)
|
|
791
|
+
Forthic.to_time(str)
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Duplicate an interpreter
|
|
796
|
+
#
|
|
797
|
+
# @param interp [Interpreter] The interpreter to duplicate
|
|
798
|
+
# @return [Interpreter] A new interpreter with copied state
|
|
799
|
+
def self.dup_interpreter(interp)
|
|
800
|
+
# Create new interpreter of the same type as the source
|
|
801
|
+
result_interp = interp.class.new([], interp.get_timezone)
|
|
802
|
+
|
|
803
|
+
# Use copy() instead of dup() to preserve module_prefixes
|
|
804
|
+
result_interp.instance_variable_set(:@app_module, interp.instance_variable_get(:@app_module).copy(result_interp))
|
|
805
|
+
result_interp.instance_variable_set(:@module_stack, [result_interp.instance_variable_get(:@app_module)])
|
|
806
|
+
|
|
807
|
+
# Use Stack.dup() method
|
|
808
|
+
result_interp.instance_variable_set(:@stack, interp.instance_variable_get(:@stack).dup)
|
|
809
|
+
|
|
810
|
+
# Share registered modules reference (modules are shared, not copied)
|
|
811
|
+
result_interp.instance_variable_set(:@registered_modules, interp.instance_variable_get(:@registered_modules))
|
|
812
|
+
|
|
813
|
+
# Copy error handler if present
|
|
814
|
+
handle_error = interp.instance_variable_get(:@handle_error)
|
|
815
|
+
result_interp.instance_variable_set(:@handle_error, handle_error) if handle_error
|
|
816
|
+
|
|
817
|
+
result_interp
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# StandardInterpreter - Full-featured interpreter with standard library
|
|
821
|
+
#
|
|
822
|
+
# Extends Interpreter and automatically imports standard modules:
|
|
823
|
+
# - CoreModule: Stack operations, variables, module system, control flow
|
|
824
|
+
# - ArrayModule: Array/collection operations
|
|
825
|
+
# - RecordModule: Record/hash operations
|
|
826
|
+
# - StringModule: String operations
|
|
827
|
+
# - MathModule: Mathematical operations
|
|
828
|
+
# - BooleanModule: Boolean logic
|
|
829
|
+
# - JsonModule: JSON operations
|
|
830
|
+
# - DateTimeModule: DateTime operations
|
|
831
|
+
#
|
|
832
|
+
# For most use cases, use this class. Use Interpreter if you need
|
|
833
|
+
# full control over which modules are loaded.
|
|
834
|
+
class StandardInterpreter < Interpreter
|
|
835
|
+
def initialize(modules = [], timezone = "UTC")
|
|
836
|
+
# Don't pass modules to super - we'll import them after stdlib
|
|
837
|
+
super([], timezone)
|
|
838
|
+
|
|
839
|
+
# Import standard library modules FIRST (checked last during lookup)
|
|
840
|
+
# This allows user modules to shadow stdlib words
|
|
841
|
+
import_standard_library
|
|
842
|
+
|
|
843
|
+
# Import user modules AFTER stdlib (checked first during lookup)
|
|
844
|
+
import_modules(modules)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
private
|
|
848
|
+
|
|
849
|
+
def import_standard_library
|
|
850
|
+
require_relative 'modules/standard/core_module'
|
|
851
|
+
require_relative 'modules/standard/array_module'
|
|
852
|
+
require_relative 'modules/standard/record_module'
|
|
853
|
+
require_relative 'modules/standard/string_module'
|
|
854
|
+
require_relative 'modules/standard/math_module'
|
|
855
|
+
require_relative 'modules/standard/boolean_module'
|
|
856
|
+
require_relative 'modules/standard/json_module'
|
|
857
|
+
require_relative 'modules/standard/datetime_module'
|
|
858
|
+
|
|
859
|
+
stdlib = [
|
|
860
|
+
Modules::CoreModule.new,
|
|
861
|
+
Modules::ArrayModule.new,
|
|
862
|
+
Modules::RecordModule.new,
|
|
863
|
+
Modules::StringModule.new,
|
|
864
|
+
Modules::MathModule.new,
|
|
865
|
+
Modules::BooleanModule.new,
|
|
866
|
+
Modules::JsonModule.new,
|
|
867
|
+
Modules::DateTimeModule.new,
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
# Import unprefixed at the BOTTOM of module stack
|
|
871
|
+
# This ensures they're checked LAST during find_word()
|
|
872
|
+
stdlib.each do |mod|
|
|
873
|
+
import_module(mod, "")
|
|
874
|
+
end
|
|
516
875
|
end
|
|
517
876
|
end
|
|
518
877
|
end
|