forthic 0.2.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 +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 +694 -245
- 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 -20
- data/protos/README.md +43 -0
- data/protos/v1/forthic_runtime.proto +200 -0
- metadata +72 -39
- 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 -20
- data/lib/forthic/forthic_error.rb +0 -50
- 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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../module'
|
|
4
|
+
require_relative '../word_options'
|
|
5
|
+
|
|
6
|
+
module Forthic
|
|
7
|
+
module Decorators
|
|
8
|
+
# WordMetadata - Struct for word metadata
|
|
9
|
+
WordMetadata = Struct.new(:stack_effect, :description, :word_name, :method_name, :input_count, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
# DirectWordMetadata - Struct for direct word metadata
|
|
12
|
+
DirectWordMetadata = Struct.new(:stack_effect, :description, :word_name, :method_name, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
# ModuleMetadata - Struct for module-level metadata
|
|
15
|
+
ModuleMetadata = Struct.new(:description, :categories, :options_info, :examples, keyword_init: true) do
|
|
16
|
+
def initialize(description: '', categories: [], options_info: nil, examples: [])
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Category - Struct for category metadata
|
|
22
|
+
Category = Struct.new(:name, :words, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
# Parse markdown-formatted module documentation string
|
|
25
|
+
#
|
|
26
|
+
# Expected format:
|
|
27
|
+
# ```
|
|
28
|
+
# Brief description
|
|
29
|
+
#
|
|
30
|
+
# ## Categories
|
|
31
|
+
# - Category Name: WORD1, WORD2, WORD3
|
|
32
|
+
# - Another Category: WORD4, WORD5
|
|
33
|
+
#
|
|
34
|
+
# ## Options
|
|
35
|
+
# Multi-line text describing the options system
|
|
36
|
+
#
|
|
37
|
+
# ## Examples
|
|
38
|
+
# example code line 1
|
|
39
|
+
# example code line 2
|
|
40
|
+
# ```
|
|
41
|
+
#
|
|
42
|
+
# @param doc_string [String] The documentation string to parse
|
|
43
|
+
# @return [ModuleMetadata] Parsed metadata
|
|
44
|
+
def self.parse_module_doc_string(doc_string)
|
|
45
|
+
lines = doc_string.split("\n").map(&:strip).reject(&:empty?)
|
|
46
|
+
|
|
47
|
+
result = ModuleMetadata.new
|
|
48
|
+
current_section = :description
|
|
49
|
+
options_lines = []
|
|
50
|
+
|
|
51
|
+
lines.each do |line|
|
|
52
|
+
# Check for section headers
|
|
53
|
+
if line.start_with?('## Categories')
|
|
54
|
+
current_section = :categories
|
|
55
|
+
next
|
|
56
|
+
elsif line.start_with?('## Options')
|
|
57
|
+
current_section = :options
|
|
58
|
+
next
|
|
59
|
+
elsif line.start_with?('## Examples')
|
|
60
|
+
current_section = :examples
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Process content based on current section
|
|
65
|
+
case current_section
|
|
66
|
+
when :description
|
|
67
|
+
if result.description.empty?
|
|
68
|
+
result.description = line
|
|
69
|
+
else
|
|
70
|
+
result.description += ' ' + line
|
|
71
|
+
end
|
|
72
|
+
when :categories
|
|
73
|
+
# Parse "- Category Name: WORD1, WORD2, WORD3"
|
|
74
|
+
match = line.match(/^-\s*([^:]+):\s*(.+)$/)
|
|
75
|
+
if match
|
|
76
|
+
result.categories << Category.new(
|
|
77
|
+
name: match[1].strip,
|
|
78
|
+
words: match[2].strip
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
when :options
|
|
82
|
+
options_lines << line
|
|
83
|
+
when :examples
|
|
84
|
+
result.examples << line
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Join options lines into a single string
|
|
89
|
+
result.options_info = options_lines.join("\n") unless options_lines.empty?
|
|
90
|
+
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse Forthic stack notation to extract input count and optional WordOptions
|
|
95
|
+
#
|
|
96
|
+
# Examples:
|
|
97
|
+
# "( a:any b:any -- sum:number )" → { input_count: 2, has_options: false }
|
|
98
|
+
# "( -- value:any )" → { input_count: 0, has_options: false }
|
|
99
|
+
# "( items:any[] -- first:any )" → { input_count: 1, has_options: false }
|
|
100
|
+
# "( array:any[] [options:WordOptions] -- flat:any[] )" → { input_count: 1, has_options: true }
|
|
101
|
+
#
|
|
102
|
+
# @param stack_effect [String] The stack effect notation
|
|
103
|
+
# @return [Hash] Hash with :input_count and :has_options keys
|
|
104
|
+
def self.parse_stack_notation(stack_effect)
|
|
105
|
+
# Remove parentheses and trim
|
|
106
|
+
trimmed = stack_effect.strip
|
|
107
|
+
unless trimmed.start_with?("(") && trimmed.end_with?(")")
|
|
108
|
+
raise ArgumentError, "Stack effect must be wrapped in parentheses: #{stack_effect}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
content = trimmed[1..-2].strip
|
|
112
|
+
parts = content.split("--", 2).map(&:strip) # Use limit of 2 to ensure we get exactly 2 parts
|
|
113
|
+
raise ArgumentError, "Invalid stack notation: #{stack_effect}" unless parts.length == 2
|
|
114
|
+
|
|
115
|
+
input_part = parts[0]
|
|
116
|
+
return { input_count: 0, has_options: false } if input_part.empty?
|
|
117
|
+
|
|
118
|
+
# Check for optional [options:WordOptions] parameter
|
|
119
|
+
has_options = input_part.include?('[options:WordOptions]')
|
|
120
|
+
|
|
121
|
+
# Remove optional parameter from counting
|
|
122
|
+
without_optional = input_part.gsub(/\[options:WordOptions\]/, '').strip
|
|
123
|
+
|
|
124
|
+
# Split by whitespace, count non-empty tokens
|
|
125
|
+
inputs = without_optional.split(/\s+/).reject(&:empty?)
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
input_count: inputs.length,
|
|
129
|
+
has_options: has_options
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# DecoratedModule - Base class for modules using word registration
|
|
134
|
+
#
|
|
135
|
+
# Automatically registers all decorated words when interpreter is set.
|
|
136
|
+
class DecoratedModule < Forthic::Module
|
|
137
|
+
# Class-level metadata storage
|
|
138
|
+
@word_metadata = {}
|
|
139
|
+
@direct_word_metadata = {}
|
|
140
|
+
@module_metadata = {}
|
|
141
|
+
|
|
142
|
+
class << self
|
|
143
|
+
attr_reader :word_metadata, :direct_word_metadata, :module_metadata
|
|
144
|
+
|
|
145
|
+
def inherited(subclass)
|
|
146
|
+
super
|
|
147
|
+
# Each subclass gets its own metadata storage
|
|
148
|
+
subclass.instance_variable_set(:@word_metadata, {})
|
|
149
|
+
subclass.instance_variable_set(:@direct_word_metadata, {})
|
|
150
|
+
subclass.instance_variable_set(:@module_metadata, nil)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Register a word with automatic stack marshalling
|
|
154
|
+
#
|
|
155
|
+
# @param method_name [Symbol] The method name
|
|
156
|
+
# @param stack_effect [String] Stack effect notation
|
|
157
|
+
# @param description [String] Human-readable description
|
|
158
|
+
# @param word_name [String, nil] Custom word name (defaults to method name)
|
|
159
|
+
def register_forthic_word(method_name, stack_effect, description = "", word_name = nil)
|
|
160
|
+
word_name ||= method_name.to_s
|
|
161
|
+
parsed = Decorators.parse_stack_notation(stack_effect)
|
|
162
|
+
|
|
163
|
+
@word_metadata[method_name] = WordMetadata.new(
|
|
164
|
+
stack_effect: stack_effect,
|
|
165
|
+
description: description,
|
|
166
|
+
word_name: word_name,
|
|
167
|
+
method_name: method_name.to_s,
|
|
168
|
+
input_count: parsed[:input_count]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Store parsed options for later use
|
|
172
|
+
@word_metadata[method_name].define_singleton_method(:has_options?) do
|
|
173
|
+
parsed[:has_options]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Register a direct word (no stack marshalling)
|
|
178
|
+
#
|
|
179
|
+
# @param method_name [Symbol] The method name
|
|
180
|
+
# @param stack_effect [String] Stack effect notation
|
|
181
|
+
# @param description [String] Human-readable description
|
|
182
|
+
# @param word_name [String, nil] Custom word name (defaults to method name)
|
|
183
|
+
def register_forthic_direct_word(method_name, stack_effect, description = "", word_name = nil)
|
|
184
|
+
word_name ||= method_name.to_s
|
|
185
|
+
|
|
186
|
+
@direct_word_metadata[method_name] = DirectWordMetadata.new(
|
|
187
|
+
stack_effect: stack_effect,
|
|
188
|
+
description: description,
|
|
189
|
+
word_name: word_name,
|
|
190
|
+
method_name: method_name.to_s
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Register module documentation
|
|
195
|
+
#
|
|
196
|
+
# @param doc_string [String] Markdown-formatted documentation
|
|
197
|
+
def register_module_doc(doc_string)
|
|
198
|
+
@module_metadata = Decorators.parse_module_doc_string(doc_string)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def set_interp(interp)
|
|
203
|
+
super(interp)
|
|
204
|
+
register_decorated_words
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def register_decorated_words
|
|
210
|
+
# Register words with stack marshalling
|
|
211
|
+
self.class.word_metadata.each do |method_name, metadata|
|
|
212
|
+
# Create wrapper that handles stack marshalling
|
|
213
|
+
wrapper = create_word_wrapper(method_name, metadata)
|
|
214
|
+
add_module_word(metadata.word_name, wrapper)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Register direct words (no wrapping)
|
|
218
|
+
self.class.direct_word_metadata.each do |method_name, metadata|
|
|
219
|
+
# Bind method directly
|
|
220
|
+
add_module_word(metadata.word_name, method(method_name).to_proc)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def create_word_wrapper(method_name, metadata)
|
|
225
|
+
original_method = method(method_name)
|
|
226
|
+
input_count = metadata.input_count
|
|
227
|
+
has_options = metadata.respond_to?(:has_options?) && metadata.has_options?
|
|
228
|
+
|
|
229
|
+
lambda do |interp|
|
|
230
|
+
inputs = []
|
|
231
|
+
|
|
232
|
+
# Check for optional WordOptions FIRST (before popping regular args)
|
|
233
|
+
options = nil
|
|
234
|
+
if has_options && interp.get_stack.length > 0
|
|
235
|
+
top = interp.stack_peek
|
|
236
|
+
if top.is_a?(WordOptions)
|
|
237
|
+
opts = interp.stack_pop
|
|
238
|
+
options = opts.to_hash
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Pop required inputs in reverse order (stack is LIFO)
|
|
243
|
+
input_count.times do
|
|
244
|
+
inputs.unshift(interp.stack_pop)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Add options as last parameter if method expects it
|
|
248
|
+
inputs << (options || {}) if has_options
|
|
249
|
+
|
|
250
|
+
# Call original method with popped inputs (+ options if present)
|
|
251
|
+
result = original_method.call(*inputs)
|
|
252
|
+
|
|
253
|
+
# Push result if not nil (Ruby uses nil instead of undefined)
|
|
254
|
+
interp.stack_push(result) unless result.nil?
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
public
|
|
259
|
+
|
|
260
|
+
# Get documentation for all words in this module
|
|
261
|
+
#
|
|
262
|
+
# @return [Array<Hash>] Array of {name, stack_effect, description} hashes
|
|
263
|
+
def get_word_docs
|
|
264
|
+
docs = []
|
|
265
|
+
|
|
266
|
+
# Get words with stack marshalling
|
|
267
|
+
self.class.word_metadata.each_value do |meta|
|
|
268
|
+
docs << {
|
|
269
|
+
name: meta.word_name,
|
|
270
|
+
stack_effect: meta.stack_effect,
|
|
271
|
+
description: meta.description
|
|
272
|
+
}
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Get direct words
|
|
276
|
+
self.class.direct_word_metadata.each_value do |meta|
|
|
277
|
+
docs << {
|
|
278
|
+
name: meta.word_name,
|
|
279
|
+
stack_effect: meta.stack_effect,
|
|
280
|
+
description: meta.description
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
docs
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Get module-level documentation
|
|
288
|
+
#
|
|
289
|
+
# @return [Hash, nil] Module metadata with name, or nil if not set
|
|
290
|
+
def get_module_metadata
|
|
291
|
+
parsed = self.class.module_metadata
|
|
292
|
+
return nil unless parsed
|
|
293
|
+
|
|
294
|
+
# Combine parsed metadata with the module name from the instance
|
|
295
|
+
{
|
|
296
|
+
name: get_name,
|
|
297
|
+
description: parsed.description,
|
|
298
|
+
categories: parsed.categories,
|
|
299
|
+
options_info: parsed.options_info,
|
|
300
|
+
examples: parsed.examples
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Helper method to define a word on a module class
|
|
306
|
+
#
|
|
307
|
+
# Usage:
|
|
308
|
+
# class MyModule < DecoratedModule
|
|
309
|
+
# word :ADD, "( a:number b:number -- sum:number )", "Adds two numbers"
|
|
310
|
+
# def ADD(a, b)
|
|
311
|
+
# a + b
|
|
312
|
+
# end
|
|
313
|
+
# end
|
|
314
|
+
module WordDSL
|
|
315
|
+
def forthic_word(method_name, stack_effect, description = "", word_name = nil)
|
|
316
|
+
register_forthic_word(method_name, stack_effect, description, word_name)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def forthic_direct_word(method_name, stack_effect, description = "", word_name = nil)
|
|
320
|
+
register_forthic_direct_word(method_name, stack_effect, description, word_name)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def module_doc(doc_string)
|
|
324
|
+
register_module_doc(doc_string)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Extend DecoratedModule class to include DSL methods
|
|
329
|
+
DecoratedModule.singleton_class.prepend(WordDSL)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forthic
|
|
4
|
+
# CodeLocationData - Struct for tracking code location information
|
|
5
|
+
#
|
|
6
|
+
# @!attribute source
|
|
7
|
+
# @return [String, nil] Source of the code (e.g., module name, file path)
|
|
8
|
+
# @!attribute line
|
|
9
|
+
# @return [Integer] Line number
|
|
10
|
+
# @!attribute column
|
|
11
|
+
# @return [Integer] Column number
|
|
12
|
+
# @!attribute start_pos
|
|
13
|
+
# @return [Integer] Start position in the source string
|
|
14
|
+
# @!attribute end_pos
|
|
15
|
+
# @return [Integer, nil] End position in the source string
|
|
16
|
+
CodeLocationData = Struct.new(:source, :line, :column, :start_pos, :end_pos, keyword_init: true)
|
|
17
|
+
|
|
18
|
+
# ForthicError - Base error class for Forthic interpreter
|
|
19
|
+
class ForthicError < StandardError
|
|
20
|
+
attr_reader :forthic, :note, :location, :cause
|
|
21
|
+
|
|
22
|
+
def initialize(forthic, note, location: nil, cause: nil)
|
|
23
|
+
super(note)
|
|
24
|
+
@forthic = forthic
|
|
25
|
+
@note = note
|
|
26
|
+
@location = location
|
|
27
|
+
@cause = cause
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_description
|
|
31
|
+
raise NotImplementedError, "#{self.class}#get_description not implemented"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_error
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get_forthic
|
|
39
|
+
@forthic
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_note
|
|
43
|
+
@note
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# UnknownWordError - Error when interpreter encounters an unknown word
|
|
48
|
+
class UnknownWordError < ForthicError
|
|
49
|
+
attr_reader :word
|
|
50
|
+
|
|
51
|
+
def initialize(forthic, word, location: nil, cause: nil)
|
|
52
|
+
note = "Unknown word: #{word}"
|
|
53
|
+
super(forthic, note, location: location, cause: cause)
|
|
54
|
+
@word = word
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def get_word
|
|
58
|
+
@word
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# WordExecutionError - Error during word execution
|
|
63
|
+
class WordExecutionError < ForthicError
|
|
64
|
+
attr_reader :inner_error, :definition_location
|
|
65
|
+
|
|
66
|
+
def initialize(message, error, call_location: nil, definition_location: nil)
|
|
67
|
+
super("", message, location: call_location)
|
|
68
|
+
@inner_error = error
|
|
69
|
+
@definition_location = definition_location
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def get_error
|
|
73
|
+
@inner_error
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def get_definition_location
|
|
77
|
+
@definition_location
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# MissingSemicolonError - Error when semicolon is missing
|
|
82
|
+
class MissingSemicolonError < ForthicError
|
|
83
|
+
def initialize(forthic, location: nil, cause: nil)
|
|
84
|
+
note = "Missing semicolon"
|
|
85
|
+
super(forthic, note, location: location, cause: cause)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ExtraSemicolonError - Error when there's an extra semicolon
|
|
90
|
+
class ExtraSemicolonError < ForthicError
|
|
91
|
+
def initialize(forthic, location: nil, cause: nil)
|
|
92
|
+
note = "Extra semicolon"
|
|
93
|
+
super(forthic, note, location: location, cause: cause)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# StackUnderflowError - Error when stack has too few items
|
|
98
|
+
class StackUnderflowError < ForthicError
|
|
99
|
+
def initialize(forthic, location: nil, cause: nil)
|
|
100
|
+
note = "Stack underflow"
|
|
101
|
+
super(forthic, note, location: location, cause: cause)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# InvalidVariableNameError - Error for invalid variable names
|
|
106
|
+
class InvalidVariableNameError < ForthicError
|
|
107
|
+
attr_reader :varname
|
|
108
|
+
|
|
109
|
+
def initialize(forthic, varname, location: nil, cause: nil)
|
|
110
|
+
note = "Invalid variable name: #{varname}"
|
|
111
|
+
super(forthic, note, location: location, cause: cause)
|
|
112
|
+
@varname = varname
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def get_varname
|
|
116
|
+
@varname
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# UnknownModuleError - Error when module is not found
|
|
121
|
+
class UnknownModuleError < ForthicError
|
|
122
|
+
attr_reader :module_name
|
|
123
|
+
|
|
124
|
+
def initialize(forthic, module_name, location: nil, cause: nil)
|
|
125
|
+
note = "Unknown module: #{module_name}"
|
|
126
|
+
super(forthic, note, location: location, cause: cause)
|
|
127
|
+
@module_name = module_name
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def get_module_name
|
|
131
|
+
@module_name
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# InvalidInputPositionError - Error for invalid input position
|
|
136
|
+
class InvalidInputPositionError < ForthicError
|
|
137
|
+
def initialize(forthic, location: nil, cause: nil)
|
|
138
|
+
note = "Invalid input position"
|
|
139
|
+
super(forthic, note, location: location, cause: cause)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# InvalidWordNameError - Error for invalid word names
|
|
144
|
+
class InvalidWordNameError < ForthicError
|
|
145
|
+
def initialize(forthic, location: nil, note: nil, cause: nil)
|
|
146
|
+
error_note = note || "Invalid word name"
|
|
147
|
+
super(forthic, error_note, location: location, cause: cause)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# UnterminatedStringError - Error for unterminated strings
|
|
152
|
+
class UnterminatedStringError < ForthicError
|
|
153
|
+
def initialize(forthic, location: nil, cause: nil)
|
|
154
|
+
note = "Unterminated string"
|
|
155
|
+
super(forthic, note, location: location, cause: cause)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# UnknownTokenError - Error for unknown token types
|
|
160
|
+
class UnknownTokenError < ForthicError
|
|
161
|
+
attr_reader :token
|
|
162
|
+
|
|
163
|
+
def initialize(forthic, token, location: nil, cause: nil)
|
|
164
|
+
note = "Unknown type of token: #{token}"
|
|
165
|
+
super(forthic, note, location: location, cause: cause)
|
|
166
|
+
@token = token
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def get_token
|
|
170
|
+
@token
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# ModuleError - Error in module execution
|
|
175
|
+
class ModuleError < ForthicError
|
|
176
|
+
attr_reader :module_name, :error
|
|
177
|
+
|
|
178
|
+
def initialize(forthic, module_name, error, location: nil, cause: nil)
|
|
179
|
+
note = "Error in module #{module_name}: #{error.message}"
|
|
180
|
+
super(forthic, note, location: location, cause: cause)
|
|
181
|
+
@module_name = module_name
|
|
182
|
+
@error = error
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def get_module_name
|
|
186
|
+
@module_name
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def get_error
|
|
190
|
+
@error
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# TooManyAttemptsError - Error when recovery attempts exceed maximum
|
|
195
|
+
class TooManyAttemptsError < ForthicError
|
|
196
|
+
attr_reader :num_attempts, :max_attempts
|
|
197
|
+
|
|
198
|
+
def initialize(forthic, num_attempts, max_attempts, location: nil, cause: nil)
|
|
199
|
+
note = "Too many recovery attempts: #{num_attempts} of #{max_attempts}"
|
|
200
|
+
super(forthic, note, location: location, cause: cause)
|
|
201
|
+
@num_attempts = num_attempts
|
|
202
|
+
@max_attempts = max_attempts
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def get_num_attempts
|
|
206
|
+
@num_attempts
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def get_max_attempts
|
|
210
|
+
@max_attempts
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# IntentionalStopError - Error for intentional execution stops (PEEK!, STACK!)
|
|
215
|
+
class IntentionalStopError < StandardError
|
|
216
|
+
def initialize(message)
|
|
217
|
+
super(message)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Get formatted error description with code context
|
|
222
|
+
#
|
|
223
|
+
# @param forthic [String] The Forthic source code
|
|
224
|
+
# @param forthic_error [ForthicError] The error to format
|
|
225
|
+
# @return [String] Formatted error message with code context
|
|
226
|
+
def self.get_error_description(forthic, forthic_error)
|
|
227
|
+
# If don't have any extra info, just return the note
|
|
228
|
+
if forthic.nil? || forthic.empty? || forthic_error.location.nil?
|
|
229
|
+
return forthic_error.get_note
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Otherwise, return the note and indicate where the error occurred
|
|
233
|
+
location = forthic_error.location
|
|
234
|
+
|
|
235
|
+
# For WordExecutionError, show both definition and call locations
|
|
236
|
+
if forthic_error.is_a?(WordExecutionError)
|
|
237
|
+
def_loc = forthic_error.get_definition_location
|
|
238
|
+
if def_loc
|
|
239
|
+
# Show definition location with highlighting
|
|
240
|
+
def_line_num = def_loc.line
|
|
241
|
+
def_lines = forthic.split("\n")[0...def_line_num]
|
|
242
|
+
def_error_line = " " * (def_loc.column - 1) + "^" * ((def_loc.end_pos || def_loc.start_pos + 1) - def_loc.start_pos)
|
|
243
|
+
|
|
244
|
+
def_location_info = "at line #{def_line_num}"
|
|
245
|
+
def_location_info += " in #{def_loc.source}" if def_loc.source
|
|
246
|
+
|
|
247
|
+
# Show call location with highlighting
|
|
248
|
+
call_line_num = location.line
|
|
249
|
+
call_lines = forthic.split("\n")[0...call_line_num]
|
|
250
|
+
call_error_line = " " * (location.column - 1) + "^" * ((location.end_pos || location.start_pos + 1) - location.start_pos)
|
|
251
|
+
|
|
252
|
+
call_location_info = "line #{call_line_num}"
|
|
253
|
+
call_location_info += " in #{location.source}" if location.source
|
|
254
|
+
|
|
255
|
+
return "#{forthic_error.get_note} #{def_location_info}:\n```\n#{def_lines.join("\n")}\n#{def_error_line}\n```\nCalled from #{call_location_info}:\n```\n#{call_lines.join("\n")}\n#{call_error_line}\n```"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Standard error format for other errors
|
|
260
|
+
line_num = location.line
|
|
261
|
+
lines = forthic.split("\n")[0...line_num]
|
|
262
|
+
error_line = " " * (location.column - 1) + "^" * ((location.end_pos || location.start_pos + 1) - location.start_pos)
|
|
263
|
+
|
|
264
|
+
location_info = "at line #{line_num}"
|
|
265
|
+
location_info += " in #{location.source}" if location.source
|
|
266
|
+
|
|
267
|
+
error_message = "#{forthic_error.get_note} #{location_info}:\n```\n#{lines.join("\n")}\n#{error_line}\n```"
|
|
268
|
+
error_message
|
|
269
|
+
end
|
|
270
|
+
end
|