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.
- checksums.yaml +4 -4
- data/README.md +314 -14
- data/Rakefile +37 -8
- 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 +682 -133
- 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 +225 -78
- 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 -20
- data/protos/README.md +43 -0
- data/protos/v1/forthic_runtime.proto +200 -0
- metadata +76 -39
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/Guardfile +0 -42
- data/lib/forthic/code_location.rb +0 -20
- data/lib/forthic/forthic_error.rb +0 -51
- data/lib/forthic/forthic_module.rb +0 -145
- data/lib/forthic/global_module.rb +0 -2341
- data/lib/forthic/positioned_string.rb +0 -19
- data/lib/forthic/token.rb +0 -38
- data/lib/forthic/variable.rb +0 -34
- data/lib/forthic/version.rb +0 -5
- data/lib/forthic/words/definition_word.rb +0 -40
- 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
|
@@ -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
|