forthic 0.1.0 → 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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +314 -14
  3. data/Rakefile +37 -8
  4. data/lib/forthic/decorators/docs.rb +69 -0
  5. data/lib/forthic/decorators/word.rb +331 -0
  6. data/lib/forthic/errors.rb +270 -0
  7. data/lib/forthic/grpc/client.rb +223 -0
  8. data/lib/forthic/grpc/errors.rb +149 -0
  9. data/lib/forthic/grpc/forthic_runtime_pb.rb +32 -0
  10. data/lib/forthic/grpc/forthic_runtime_services_pb.rb +31 -0
  11. data/lib/forthic/grpc/remote_module.rb +120 -0
  12. data/lib/forthic/grpc/remote_runtime_module.rb +148 -0
  13. data/lib/forthic/grpc/remote_word.rb +91 -0
  14. data/lib/forthic/grpc/runtime_manager.rb +60 -0
  15. data/lib/forthic/grpc/serializer.rb +184 -0
  16. data/lib/forthic/grpc/server.rb +361 -0
  17. data/lib/forthic/interpreter.rb +682 -133
  18. data/lib/forthic/literals.rb +170 -0
  19. data/lib/forthic/module.rb +383 -0
  20. data/lib/forthic/modules/standard/array_module.rb +940 -0
  21. data/lib/forthic/modules/standard/boolean_module.rb +176 -0
  22. data/lib/forthic/modules/standard/core_module.rb +362 -0
  23. data/lib/forthic/modules/standard/datetime_module.rb +349 -0
  24. data/lib/forthic/modules/standard/json_module.rb +55 -0
  25. data/lib/forthic/modules/standard/math_module.rb +365 -0
  26. data/lib/forthic/modules/standard/record_module.rb +203 -0
  27. data/lib/forthic/modules/standard/string_module.rb +170 -0
  28. data/lib/forthic/tokenizer.rb +225 -78
  29. data/lib/forthic/utils.rb +35 -0
  30. data/lib/forthic/websocket/handler.rb +548 -0
  31. data/lib/forthic/websocket/serializer.rb +160 -0
  32. data/lib/forthic/word_options.rb +141 -0
  33. data/lib/forthic.rb +30 -20
  34. data/protos/README.md +43 -0
  35. data/protos/v1/forthic_runtime.proto +200 -0
  36. metadata +76 -39
  37. data/.standard.yml +0 -3
  38. data/CHANGELOG.md +0 -5
  39. data/Guardfile +0 -42
  40. data/lib/forthic/code_location.rb +0 -20
  41. data/lib/forthic/forthic_error.rb +0 -51
  42. data/lib/forthic/forthic_module.rb +0 -145
  43. data/lib/forthic/global_module.rb +0 -2341
  44. data/lib/forthic/positioned_string.rb +0 -19
  45. data/lib/forthic/token.rb +0 -38
  46. data/lib/forthic/variable.rb +0 -34
  47. data/lib/forthic/version.rb +0 -5
  48. data/lib/forthic/words/definition_word.rb +0 -40
  49. data/lib/forthic/words/end_array_word.rb +0 -28
  50. data/lib/forthic/words/end_module_word.rb +0 -16
  51. data/lib/forthic/words/imported_word.rb +0 -27
  52. data/lib/forthic/words/map_word.rb +0 -169
  53. data/lib/forthic/words/module_memo_bang_at_word.rb +0 -22
  54. data/lib/forthic/words/module_memo_bang_word.rb +0 -21
  55. data/lib/forthic/words/module_memo_word.rb +0 -35
  56. data/lib/forthic/words/module_word.rb +0 -21
  57. data/lib/forthic/words/push_value_word.rb +0 -21
  58. data/lib/forthic/words/start_module_word.rb +0 -31
  59. data/lib/forthic/words/word.rb +0 -30
  60. data/sig/forthic.rbs +0 -4
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'date'
5
+
6
+ module Forthic
7
+ # Literal Handlers for Forthic Interpreters
8
+ #
9
+ # This module provides literal parsing functions that convert string tokens into typed values.
10
+ # These handlers are used by the Forthic interpreter to recognize and parse different literal types.
11
+ #
12
+ # You can use these built-in handlers or create custom literal handlers for your own Forthic
13
+ # interpreters. Each handler should be a Proc/lambda that takes a string and returns the parsed
14
+ # value or nil if the string doesn't match the expected format.
15
+ #
16
+ # Built-in literal types:
17
+ # - Boolean: TRUE, FALSE
18
+ # - Integer: 42, -10, 0
19
+ # - Float: 3.14, -2.5, 0.0
20
+ # - Time: 9:00, 11:30 PM, 22:15
21
+ # - Date: 2020-06-05, YYYY-MM-DD (with wildcards)
22
+ # - ZonedDateTime: ISO 8601 timestamps with timezone support
23
+
24
+ # LiteralHandler type: Proc that takes string, returns parsed value or nil
25
+ # @example
26
+ # handler = ->(str) { str == "TRUE" ? true : nil }
27
+
28
+ # Parse boolean literals: TRUE, FALSE
29
+ #
30
+ # @param str [String] The string to parse
31
+ # @return [Boolean, nil] true, false, or nil if not a boolean
32
+ def self.to_bool(str)
33
+ return true if str == "TRUE"
34
+ return false if str == "FALSE"
35
+ nil
36
+ end
37
+
38
+ # Parse float literals: 3.14, -2.5, 0.0
39
+ # Must contain a decimal point
40
+ #
41
+ # @param str [String] The string to parse
42
+ # @return [Float, nil] The parsed float or nil if invalid
43
+ def self.to_float(str)
44
+ return nil unless str.include?(".")
45
+ result = Float(str)
46
+ result
47
+ rescue ArgumentError
48
+ nil
49
+ end
50
+
51
+ # Parse integer literals: 42, -10, 0
52
+ # Must not contain a decimal point
53
+ #
54
+ # @param str [String] The string to parse
55
+ # @return [Integer, nil] The parsed integer or nil if invalid
56
+ def self.to_int(str)
57
+ return nil if str.include?(".")
58
+ result = Integer(str, 10)
59
+ # Verify it's actually an integer string (not "42abc")
60
+ return nil unless result.to_s == str
61
+ result
62
+ rescue ArgumentError
63
+ nil
64
+ end
65
+
66
+ # SimpleTime - Represents a time of day (hour and minute)
67
+ #
68
+ # This is a simplified replacement for Temporal.PlainTime
69
+ # @!attribute hour
70
+ # @return [Integer] Hour (0-23)
71
+ # @!attribute minute
72
+ # @return [Integer] Minute (0-59)
73
+ SimpleTime = Struct.new(:hour, :minute, keyword_init: true) do
74
+ def to_s
75
+ format("%02d:%02d", hour, minute)
76
+ end
77
+ end
78
+
79
+ # Parse time literals: 9:00, 11:30 PM, 22:15 AM
80
+ #
81
+ # @param str [String] The string to parse
82
+ # @return [SimpleTime, nil] The parsed time or nil if invalid
83
+ def self.to_time(str)
84
+ match = str.match(/^(\d{1,2}):(\d{2})(?:\s*(AM|PM))?$/)
85
+ return nil unless match
86
+
87
+ hours = match[1].to_i
88
+ minutes = match[2].to_i
89
+ meridiem = match[3]
90
+
91
+ # Adjust for AM/PM
92
+ if meridiem == "PM" && hours < 12
93
+ hours += 12
94
+ elsif meridiem == "AM" && hours == 12
95
+ hours = 0
96
+ elsif meridiem == "AM" && hours > 12
97
+ # Handle invalid cases like "22:15 AM"
98
+ hours -= 12
99
+ end
100
+
101
+ return nil if hours > 23 || minutes >= 60
102
+
103
+ SimpleTime.new(hour: hours, minute: minutes)
104
+ end
105
+
106
+ # Create a date literal handler with timezone support
107
+ # Parses: 2020-06-05, YYYY-MM-DD (with wildcards)
108
+ #
109
+ # @param timezone [String] The timezone to use for wildcard resolution
110
+ # @return [Proc] A literal handler proc
111
+ def self.to_literal_date(timezone)
112
+ lambda do |str|
113
+ match = str.match(/^(\d{4}|YYYY)-(\d{2}|MM)-(\d{2}|DD)$/)
114
+ return nil unless match
115
+
116
+ # Get current date in the specified timezone
117
+ # Use ENV['TZ'] to temporarily set timezone for Time.now
118
+ old_tz = ENV['TZ']
119
+ begin
120
+ ENV['TZ'] = timezone
121
+ now = Time.now
122
+ year = match[1] == "YYYY" ? now.year : match[1].to_i
123
+ month = match[2] == "MM" ? now.month : match[2].to_i
124
+ day = match[3] == "DD" ? now.day : match[3].to_i
125
+
126
+ Date.new(year, month, day)
127
+ rescue ArgumentError
128
+ nil
129
+ ensure
130
+ ENV['TZ'] = old_tz
131
+ end
132
+ end
133
+ end
134
+
135
+ # Create a zoned datetime literal handler with timezone support
136
+ # Parses: 2025-05-24T10:15:00Z, 2025-05-24T10:15:00-05:00, 2025-05-24T10:15:00
137
+ #
138
+ # @param timezone [String] The default timezone to use
139
+ # @return [Proc] A literal handler proc
140
+ def self.to_zoned_datetime(timezone)
141
+ lambda do |str|
142
+ return nil unless str.include?("T")
143
+
144
+ begin
145
+ # Handle explicit UTC (Z suffix)
146
+ if str.end_with?("Z")
147
+ return Time.parse(str).utc
148
+ end
149
+
150
+ # Handle explicit timezone offset (+05:00, -05:00)
151
+ if str.match?(/[+-]\d{2}:\d{2}$/)
152
+ return Time.parse(str)
153
+ end
154
+
155
+ # No timezone specified, use interpreter's timezone
156
+ # Parse as UTC first, then convert to specified timezone using ENV['TZ']
157
+ old_tz = ENV['TZ']
158
+ begin
159
+ ENV['TZ'] = timezone
160
+ time = Time.parse(str)
161
+ time
162
+ ensure
163
+ ENV['TZ'] = old_tz
164
+ end
165
+ rescue ArgumentError
166
+ nil
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tokenizer'
4
+ require_relative 'errors'
5
+ require 'set'
6
+
7
+ module Forthic
8
+ # WordHandler type: Proc that takes an interpreter
9
+ # @example
10
+ # handler = ->(interp) { interp.stack_push(42) }
11
+
12
+ # -------------------------------------
13
+ # Variable
14
+ # Variable - Named mutable value container
15
+ #
16
+ # Represents a variable that can store and retrieve values within a module scope.
17
+ # Variables are accessed by name and can be set to any value type.
18
+ class Variable
19
+ attr_reader :name
20
+ attr_accessor :value
21
+
22
+ def initialize(name, value = nil)
23
+ @name = name
24
+ @value = value
25
+ end
26
+
27
+ def get_name
28
+ @name
29
+ end
30
+
31
+ def set_value(val)
32
+ @value = val
33
+ end
34
+
35
+ def get_value
36
+ @value
37
+ end
38
+
39
+ def dup
40
+ Variable.new(@name, @value)
41
+ end
42
+ end
43
+
44
+ # -------------------------------------
45
+ # Words
46
+
47
+ # Word - Base class for all executable words in Forthic
48
+ #
49
+ # A word is the fundamental unit of execution in Forthic. When interpreted,
50
+ # it performs an action (typically manipulating the stack or control flow).
51
+ # All concrete word types must override the execute method.
52
+ class Word
53
+ attr_reader :name, :string
54
+ attr_accessor :location
55
+
56
+ def initialize(name)
57
+ @name = name
58
+ @string = name
59
+ @location = nil
60
+ end
61
+
62
+ def set_location(location)
63
+ @location = location
64
+ end
65
+
66
+ def get_location
67
+ @location
68
+ end
69
+
70
+ def execute(_interp)
71
+ raise NotImplementedError, "Must override Word#execute"
72
+ end
73
+ end
74
+
75
+ # PushValueWord - Word that pushes a value onto the stack
76
+ #
77
+ # Executes by pushing its stored value onto the interpreter's stack.
78
+ # Used for literals, variables, and constants.
79
+ class PushValueWord < Word
80
+ attr_reader :value
81
+
82
+ def initialize(name, value)
83
+ super(name)
84
+ @value = value
85
+ end
86
+
87
+ def execute(interp)
88
+ interp.stack_push(@value)
89
+ end
90
+ end
91
+
92
+ # DefinitionWord - User-defined word composed of other words
93
+ #
94
+ # Represents a word defined in Forthic code using `:`
95
+ # Contains a sequence of words that are executed in order.
96
+ # Provides error context by tracking both call site and definition location.
97
+ class DefinitionWord < Word
98
+ attr_reader :words
99
+
100
+ def initialize(name)
101
+ super(name)
102
+ @words = []
103
+ end
104
+
105
+ def add_word(word)
106
+ @words << word
107
+ end
108
+
109
+ def execute(interp)
110
+ @words.each do |word|
111
+ begin
112
+ word.execute(interp)
113
+ rescue => e
114
+ tokenizer = interp.get_tokenizer
115
+ raise WordExecutionError.new(
116
+ "Error executing #{@name}",
117
+ e,
118
+ call_location: tokenizer.get_token_location, # Where the word was called
119
+ definition_location: word.get_location # Where the word was defined
120
+ )
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ # ModuleMemoWord - Memoized word that caches its result
127
+ #
128
+ # Executes the wrapped word once and caches the result on the stack.
129
+ # Subsequent calls return the cached value without re-executing.
130
+ # Defined in Forthic using `@:`. Can be refreshed using the `!` and `!@` variants.
131
+ class ModuleMemoWord < Word
132
+ attr_reader :word, :value
133
+ attr_accessor :has_value
134
+
135
+ def initialize(word)
136
+ super(word.name)
137
+ @word = word
138
+ @has_value = false
139
+ @value = nil
140
+ end
141
+
142
+ def refresh(interp)
143
+ @word.execute(interp)
144
+ @value = interp.stack_pop
145
+ @has_value = true
146
+ end
147
+
148
+ def execute(interp)
149
+ refresh(interp) unless @has_value
150
+ interp.stack_push(@value)
151
+ end
152
+ end
153
+
154
+ # ModuleMemoBangWord - Forces refresh of a memoized word
155
+ #
156
+ # Re-executes the memoized word and updates its cached value.
157
+ # Named with a `!` suffix (e.g., `WORD!` for a memo word named `WORD`).
158
+ # Does not push the new value onto the stack.
159
+ class ModuleMemoBangWord < Word
160
+ attr_reader :memo_word
161
+
162
+ def initialize(memo_word)
163
+ super("#{memo_word.name}!")
164
+ @memo_word = memo_word
165
+ end
166
+
167
+ def execute(interp)
168
+ @memo_word.refresh(interp)
169
+ end
170
+ end
171
+
172
+ # ModuleMemoBangAtWord - Refreshes a memoized word and returns its value
173
+ #
174
+ # Re-executes the memoized word, updates its cached value, and pushes the new value onto the stack.
175
+ # Named with a `!@` suffix (e.g., `WORD!@` for a memo word named `WORD`).
176
+ # Combines the refresh and retrieval operations.
177
+ class ModuleMemoBangAtWord < Word
178
+ attr_reader :memo_word
179
+
180
+ def initialize(memo_word)
181
+ super("#{memo_word.name}!@")
182
+ @memo_word = memo_word
183
+ end
184
+
185
+ def execute(interp)
186
+ @memo_word.refresh(interp)
187
+ interp.stack_push(@memo_word.value)
188
+ end
189
+ end
190
+
191
+ # ExecuteWord - Wrapper word that executes another word
192
+ #
193
+ # Delegates execution to a target word. Used for prefixed module imports
194
+ # to create words like `prefix.word` that execute the original word from the imported module.
195
+ class ExecuteWord < Word
196
+ attr_reader :target_word
197
+
198
+ def initialize(name, target_word)
199
+ super(name)
200
+ @target_word = target_word
201
+ end
202
+
203
+ def execute(interp)
204
+ @target_word.execute(interp)
205
+ end
206
+ end
207
+
208
+ # -------------------------------------
209
+ # Module
210
+
211
+ # Module - Container for words, variables, and imported modules
212
+ #
213
+ # Modules provide namespacing and code organization in Forthic.
214
+ # Each module maintains its own dictionary of words, variables, and imported modules.
215
+ #
216
+ # Features:
217
+ # - Word and variable management
218
+ # - Module importing with optional prefixes
219
+ # - Exportable word lists for controlled visibility
220
+ # - Module duplication and copying for isolated execution contexts
221
+ #
222
+ # Modules can be defined inline with `{module_name ... }` syntax or
223
+ # loaded from external sources.
224
+ class Module
225
+ attr_accessor :words, :exportable, :variables, :modules, :module_prefixes
226
+ attr_accessor :name, :forthic_code, :interp
227
+
228
+ def initialize(name, forthic_code = "")
229
+ @words = []
230
+ @exportable = []
231
+ @variables = {}
232
+ @modules = {}
233
+ @module_prefixes = {}
234
+ @name = name
235
+ @forthic_code = forthic_code
236
+ @interp = nil
237
+ end
238
+
239
+ def get_name
240
+ @name
241
+ end
242
+
243
+ def set_interp(interp)
244
+ @interp = interp
245
+ end
246
+
247
+ def get_interp
248
+ raise "Module #{@name} has no interpreter" unless @interp
249
+ @interp
250
+ end
251
+
252
+ # Duplication methods
253
+ def dup
254
+ result = Module.new(@name)
255
+ result.words = @words.dup
256
+ result.exportable = @exportable.dup
257
+ @variables.each do |key, var|
258
+ result.variables[key] = var.dup
259
+ end
260
+ @modules.each do |key, mod|
261
+ result.modules[key] = mod
262
+ end
263
+ result.forthic_code = @forthic_code
264
+ result
265
+ end
266
+
267
+ def copy(interp)
268
+ result = Module.new(@name)
269
+ result.words = @words.dup
270
+ result.exportable = @exportable.dup
271
+ @variables.each do |key, var|
272
+ result.variables[key] = var.dup
273
+ end
274
+ @modules.each do |key, mod|
275
+ result.modules[key] = mod
276
+ end
277
+
278
+ # Restore module_prefixes
279
+ @module_prefixes.each do |module_name, prefixes|
280
+ prefixes.each do |prefix|
281
+ result.import_module(prefix, @modules[module_name], interp)
282
+ end
283
+ end
284
+
285
+ result.forthic_code = @forthic_code
286
+ result
287
+ end
288
+
289
+ # Module management
290
+ def find_module(name)
291
+ @modules[name]
292
+ end
293
+
294
+ def register_module(module_name, prefix, mod)
295
+ @modules[module_name] = mod
296
+
297
+ @module_prefixes[module_name] ||= Set.new
298
+ @module_prefixes[module_name].add(prefix)
299
+ end
300
+
301
+ def import_module(prefix, mod, _interp)
302
+ new_module = mod.dup
303
+
304
+ words = new_module.exportable_words
305
+ words.each do |word|
306
+ # For unprefixed imports, add word directly
307
+ if prefix == ""
308
+ add_word(word)
309
+ else
310
+ # For prefixed imports, create word that executes the target word
311
+ prefixed_word = ExecuteWord.new("#{prefix}.#{word.name}", word)
312
+ add_word(prefixed_word)
313
+ end
314
+ end
315
+ register_module(mod.name, prefix, new_module)
316
+ end
317
+
318
+ # Word management
319
+ def add_word(word)
320
+ @words << word
321
+ end
322
+
323
+ def add_memo_words(word)
324
+ memo_word = ModuleMemoWord.new(word)
325
+ @words << memo_word
326
+ @words << ModuleMemoBangWord.new(memo_word)
327
+ @words << ModuleMemoBangAtWord.new(memo_word)
328
+ memo_word
329
+ end
330
+
331
+ def add_exportable(names)
332
+ @exportable.concat(names)
333
+ end
334
+
335
+ def add_exportable_word(word)
336
+ @words << word
337
+ @exportable << word.name
338
+ end
339
+
340
+ def add_module_word(word_name, word_func)
341
+ word = Word.new(word_name)
342
+ # Define the execute method for this specific instance
343
+ word.define_singleton_method(:execute) do |interp|
344
+ word_func.call(interp)
345
+ end
346
+ add_exportable_word(word)
347
+ end
348
+
349
+ def exportable_words
350
+ result = []
351
+ @words.each do |word|
352
+ result << word if @exportable.include?(word.name)
353
+ end
354
+ result
355
+ end
356
+
357
+ def find_word(name)
358
+ result = find_dictionary_word(name)
359
+ result = find_variable(name) unless result
360
+ result
361
+ end
362
+
363
+ def find_dictionary_word(word_name)
364
+ # Search backwards (most recent first)
365
+ (@words.length - 1).downto(0) do |i|
366
+ w = @words[i]
367
+ return w if w.name == word_name
368
+ end
369
+ nil
370
+ end
371
+
372
+ def find_variable(varname)
373
+ var_result = @variables[varname]
374
+ return PushValueWord.new(varname, var_result) if var_result
375
+ nil
376
+ end
377
+
378
+ # Variable management
379
+ def add_variable(name, value = nil)
380
+ @variables[name] ||= Variable.new(name, value)
381
+ end
382
+ end
383
+ end