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,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