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,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../decorators/word'
|
|
4
|
+
require_relative '../../literals'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'date'
|
|
7
|
+
require 'tzinfo'
|
|
8
|
+
|
|
9
|
+
module Forthic
|
|
10
|
+
module Modules
|
|
11
|
+
# DateTimeModule - Date and time operations
|
|
12
|
+
#
|
|
13
|
+
# Provides timezone-aware datetime manipulation using Ruby's Time and Date classes.
|
|
14
|
+
class DateTimeModule < Decorators::DecoratedModule
|
|
15
|
+
# Register module documentation
|
|
16
|
+
module_doc <<~DOC
|
|
17
|
+
Date and time operations using Ruby Time and Date classes for timezone-aware datetime manipulation.
|
|
18
|
+
|
|
19
|
+
## Categories
|
|
20
|
+
- Current: TODAY, NOW
|
|
21
|
+
- Time adjustment: AM, PM
|
|
22
|
+
- Conversion to: >TIME, >DATE, >DATETIME, AT
|
|
23
|
+
- Conversion from: TIME>STR, DATE>STR, DATE>INT
|
|
24
|
+
- Timestamps: >TIMESTAMP, TIMESTAMP>DATETIME
|
|
25
|
+
- Date math: ADD-DAYS, SUBTRACT-DATES
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
TODAY
|
|
29
|
+
NOW
|
|
30
|
+
"14:30" >TIME
|
|
31
|
+
"2024-01-15" >DATE
|
|
32
|
+
TODAY "14:30" >TIME AT
|
|
33
|
+
TODAY 7 ADD-DAYS
|
|
34
|
+
DOC
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
super("datetime")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Helper method to get Time in specific timezone
|
|
41
|
+
def get_time_in_timezone(timezone_name)
|
|
42
|
+
tz = TZInfo::Timezone.get(timezone_name)
|
|
43
|
+
tz.now
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Helper method to convert Unix timestamp to Time in specific timezone
|
|
47
|
+
def timestamp_to_time(timestamp, timezone_name)
|
|
48
|
+
tz = TZInfo::Timezone.get(timezone_name)
|
|
49
|
+
tz.to_local(Time.at(timestamp).utc)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Helper method to parse time string in specific timezone
|
|
53
|
+
def parse_time_in_timezone(str, timezone_name)
|
|
54
|
+
tz = TZInfo::Timezone.get(timezone_name)
|
|
55
|
+
utc_time = Time.parse(str).utc
|
|
56
|
+
tz.to_local(utc_time)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ========================================
|
|
60
|
+
# Current Time/Date
|
|
61
|
+
# ========================================
|
|
62
|
+
|
|
63
|
+
forthic_direct_word :TODAY, "( -- date:Date )", "Get current date"
|
|
64
|
+
def TODAY(interp)
|
|
65
|
+
timezone = interp.get_timezone
|
|
66
|
+
# Get current time in timezone, then convert to Date
|
|
67
|
+
today = get_time_in_timezone(timezone)
|
|
68
|
+
date = Date.new(today.year, today.month, today.day)
|
|
69
|
+
interp.stack_push(date)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
forthic_direct_word :NOW, "( -- datetime:Time )", "Get current datetime"
|
|
73
|
+
def NOW(interp)
|
|
74
|
+
timezone = interp.get_timezone
|
|
75
|
+
now = get_time_in_timezone(timezone)
|
|
76
|
+
interp.stack_push(now)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ========================================
|
|
80
|
+
# Time Adjustment
|
|
81
|
+
# ========================================
|
|
82
|
+
|
|
83
|
+
forthic_direct_word :AM, "( time:SimpleTime -- time:SimpleTime )", "Convert time to AM (subtract 12 from hour if >= 12)"
|
|
84
|
+
def AM(interp)
|
|
85
|
+
time = interp.stack_pop
|
|
86
|
+
result = if time && time.respond_to?(:hour)
|
|
87
|
+
if time.hour >= 12
|
|
88
|
+
Forthic::SimpleTime.new(hour: time.hour - 12, minute: time.minute)
|
|
89
|
+
else
|
|
90
|
+
time
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
time
|
|
94
|
+
end
|
|
95
|
+
interp.stack_push(result)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
forthic_direct_word :PM, "( time:SimpleTime -- time:SimpleTime )", "Convert time to PM (add 12 to hour if < 12)"
|
|
99
|
+
def PM(interp)
|
|
100
|
+
time = interp.stack_pop
|
|
101
|
+
result = if time && time.respond_to?(:hour)
|
|
102
|
+
if time.hour < 12
|
|
103
|
+
Forthic::SimpleTime.new(hour: time.hour + 12, minute: time.minute)
|
|
104
|
+
else
|
|
105
|
+
time
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
time
|
|
109
|
+
end
|
|
110
|
+
interp.stack_push(result)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ========================================
|
|
114
|
+
# Conversion To Date/Time
|
|
115
|
+
# ========================================
|
|
116
|
+
|
|
117
|
+
forthic_direct_word :to_TIME, "( item:any -- time:SimpleTime )", "Convert string or datetime to SimpleTime", ">TIME"
|
|
118
|
+
def to_TIME(interp)
|
|
119
|
+
item = interp.stack_pop
|
|
120
|
+
result = nil
|
|
121
|
+
|
|
122
|
+
if item
|
|
123
|
+
# If already a SimpleTime, return it
|
|
124
|
+
if item.is_a?(Forthic::SimpleTime)
|
|
125
|
+
result = item
|
|
126
|
+
# If it's a Time, extract the hour and minute
|
|
127
|
+
elsif item.is_a?(Time)
|
|
128
|
+
result = Forthic::SimpleTime.new(hour: item.hour, minute: item.min)
|
|
129
|
+
else
|
|
130
|
+
# Otherwise, parse as string
|
|
131
|
+
str = item.to_s.strip
|
|
132
|
+
|
|
133
|
+
# Handle "HH:MM AM/PM" format
|
|
134
|
+
ampm_match = str.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i)
|
|
135
|
+
if ampm_match
|
|
136
|
+
hour = ampm_match[1].to_i
|
|
137
|
+
minute = ampm_match[2].to_i
|
|
138
|
+
meridiem = ampm_match[3].upcase
|
|
139
|
+
|
|
140
|
+
if meridiem == "PM" && hour < 12
|
|
141
|
+
hour += 12
|
|
142
|
+
elsif meridiem == "AM" && hour == 12
|
|
143
|
+
hour = 0
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
result = Forthic::SimpleTime.new(hour: hour, minute: minute)
|
|
147
|
+
else
|
|
148
|
+
# Try standard parsing (HH:MM or HH:MM:SS)
|
|
149
|
+
begin
|
|
150
|
+
match = str.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
|
151
|
+
if match
|
|
152
|
+
hour = match[1].to_i
|
|
153
|
+
minute = match[2].to_i
|
|
154
|
+
result = Forthic::SimpleTime.new(hour: hour, minute: minute)
|
|
155
|
+
end
|
|
156
|
+
rescue StandardError
|
|
157
|
+
# Fall through
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
interp.stack_push(result)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
forthic_direct_word :to_DATE, "( item:any -- date:Date )", "Convert string or datetime to Date", ">DATE"
|
|
167
|
+
def to_DATE(interp)
|
|
168
|
+
item = interp.stack_pop
|
|
169
|
+
result = nil
|
|
170
|
+
|
|
171
|
+
if item
|
|
172
|
+
# If already a Date, return it
|
|
173
|
+
if item.is_a?(Date)
|
|
174
|
+
result = item
|
|
175
|
+
# If it's a Time, extract the date
|
|
176
|
+
elsif item.is_a?(Time)
|
|
177
|
+
result = Date.new(item.year, item.month, item.day)
|
|
178
|
+
else
|
|
179
|
+
# Otherwise, parse as string
|
|
180
|
+
str = item.to_s.strip
|
|
181
|
+
|
|
182
|
+
# Try standard ISO format (YYYY-MM-DD)
|
|
183
|
+
begin
|
|
184
|
+
result = Date.parse(str)
|
|
185
|
+
rescue StandardError
|
|
186
|
+
# Fall through
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
interp.stack_push(result)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
forthic_direct_word :to_DATETIME, "( str_or_timestamp:any -- datetime:Time )", "Convert string or timestamp to Time", ">DATETIME"
|
|
195
|
+
def to_DATETIME(interp)
|
|
196
|
+
item = interp.stack_pop
|
|
197
|
+
|
|
198
|
+
unless item
|
|
199
|
+
interp.stack_push(nil)
|
|
200
|
+
return
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# If already a Time, return it
|
|
204
|
+
if item.is_a?(Time)
|
|
205
|
+
interp.stack_push(item)
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# If it's a number, treat as Unix timestamp (seconds)
|
|
210
|
+
if item.is_a?(Numeric)
|
|
211
|
+
timezone = interp.get_timezone
|
|
212
|
+
time = timestamp_to_time(item, timezone)
|
|
213
|
+
interp.stack_push(time)
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Otherwise, parse as string
|
|
218
|
+
str = item.to_s.strip
|
|
219
|
+
|
|
220
|
+
begin
|
|
221
|
+
# Try parsing as ISO datetime string
|
|
222
|
+
timezone = interp.get_timezone
|
|
223
|
+
time = parse_time_in_timezone(str, timezone)
|
|
224
|
+
interp.stack_push(time)
|
|
225
|
+
rescue StandardError
|
|
226
|
+
interp.stack_push(nil)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
forthic_direct_word :AT, "( date:Date time:SimpleTime -- datetime:Time )", "Combine date and time into datetime"
|
|
231
|
+
def AT(interp)
|
|
232
|
+
time = interp.stack_pop
|
|
233
|
+
date = interp.stack_pop
|
|
234
|
+
|
|
235
|
+
unless date && time
|
|
236
|
+
interp.stack_push(nil)
|
|
237
|
+
return
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Create Time from date and time components in the specified timezone
|
|
241
|
+
timezone_name = interp.get_timezone
|
|
242
|
+
hour = time.respond_to?(:hour) ? (time.hour || 0) : 0
|
|
243
|
+
minute = time.respond_to?(:minute) ? (time.minute || 0) : 0
|
|
244
|
+
|
|
245
|
+
# Create local time in the specified timezone
|
|
246
|
+
tz = TZInfo::Timezone.get(timezone_name)
|
|
247
|
+
local_time = Time.new(date.year, date.month, date.day, hour, minute, 0)
|
|
248
|
+
datetime = tz.to_local(tz.local_to_utc(local_time))
|
|
249
|
+
|
|
250
|
+
interp.stack_push(datetime)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# ========================================
|
|
254
|
+
# Conversion From Date/Time
|
|
255
|
+
# ========================================
|
|
256
|
+
|
|
257
|
+
forthic_word :TIME_to_STR, "( time:SimpleTime -- str:string )", "Convert time to HH:MM string", "TIME>STR"
|
|
258
|
+
def TIME_to_STR(time)
|
|
259
|
+
return "" unless time && time.respond_to?(:hour)
|
|
260
|
+
|
|
261
|
+
hour = time.hour.to_s.rjust(2, '0')
|
|
262
|
+
minute = time.minute.to_s.rjust(2, '0')
|
|
263
|
+
"#{hour}:#{minute}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
forthic_word :DATE_to_STR, "( date:Date -- str:string )", "Convert date to YYYY-MM-DD string", "DATE>STR"
|
|
267
|
+
def DATE_to_STR(date)
|
|
268
|
+
return "" unless date && date.respond_to?(:year)
|
|
269
|
+
|
|
270
|
+
date.to_s
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
forthic_word :DATE_to_INT, "( date:Date -- int:number )", "Convert date to integer (YYYYMMDD)", "DATE>INT"
|
|
274
|
+
def DATE_to_INT(date)
|
|
275
|
+
return nil unless date && date.respond_to?(:year)
|
|
276
|
+
|
|
277
|
+
year = date.year
|
|
278
|
+
month = date.month.to_s.rjust(2, '0')
|
|
279
|
+
day = date.day.to_s.rjust(2, '0')
|
|
280
|
+
"#{year}#{month}#{day}".to_i
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ========================================
|
|
284
|
+
# Timestamps
|
|
285
|
+
# ========================================
|
|
286
|
+
|
|
287
|
+
forthic_direct_word :to_TIMESTAMP, "( datetime:Time -- timestamp:number )", "Convert datetime to Unix timestamp (seconds)", ">TIMESTAMP"
|
|
288
|
+
def to_TIMESTAMP(interp)
|
|
289
|
+
datetime = interp.stack_pop
|
|
290
|
+
|
|
291
|
+
unless datetime
|
|
292
|
+
interp.stack_push(nil)
|
|
293
|
+
return
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Convert to Unix timestamp (seconds)
|
|
297
|
+
if datetime.is_a?(Time)
|
|
298
|
+
interp.stack_push(datetime.to_i)
|
|
299
|
+
else
|
|
300
|
+
interp.stack_push(nil)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
forthic_direct_word :TIMESTAMP_to_DATETIME, "( timestamp:number -- datetime:Time )", "Convert Unix timestamp (seconds) to datetime", "TIMESTAMP>DATETIME"
|
|
305
|
+
def TIMESTAMP_to_DATETIME(interp)
|
|
306
|
+
timestamp = interp.stack_pop
|
|
307
|
+
|
|
308
|
+
unless timestamp.is_a?(Numeric)
|
|
309
|
+
interp.stack_push(nil)
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Convert timestamp (seconds) to Time
|
|
314
|
+
timezone = interp.get_timezone
|
|
315
|
+
datetime = timestamp_to_time(timestamp, timezone)
|
|
316
|
+
interp.stack_push(datetime)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# ========================================
|
|
320
|
+
# Date Math
|
|
321
|
+
# ========================================
|
|
322
|
+
|
|
323
|
+
forthic_direct_word :ADD_DAYS, "( date:Date num_days:number -- date:Date )", "Add days to a date", "ADD-DAYS"
|
|
324
|
+
def ADD_DAYS(interp)
|
|
325
|
+
num_days = interp.stack_pop
|
|
326
|
+
date = interp.stack_pop
|
|
327
|
+
result = if date && date.respond_to?(:year) && !num_days.nil?
|
|
328
|
+
date + num_days
|
|
329
|
+
else
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
332
|
+
interp.stack_push(result)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
forthic_direct_word :SUBTRACT_DATES, "( date1:Date date2:Date -- num_days:number )", "Get difference in days between dates (date1 - date2)", "SUBTRACT-DATES"
|
|
336
|
+
def SUBTRACT_DATES(interp)
|
|
337
|
+
date2 = interp.stack_pop
|
|
338
|
+
date1 = interp.stack_pop
|
|
339
|
+
result = if date1 && date2 && date1.respond_to?(:year) && date2.respond_to?(:year)
|
|
340
|
+
# Get the difference in days: date1 - date2
|
|
341
|
+
(date1 - date2).to_i
|
|
342
|
+
else
|
|
343
|
+
nil
|
|
344
|
+
end
|
|
345
|
+
interp.stack_push(result)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../decorators/word'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Forthic
|
|
7
|
+
module Modules
|
|
8
|
+
# JsonModule - JSON serialization, parsing, and formatting operations
|
|
9
|
+
#
|
|
10
|
+
# Provides operations for converting between Ruby objects and JSON strings.
|
|
11
|
+
class JsonModule < Decorators::DecoratedModule
|
|
12
|
+
# Register module documentation
|
|
13
|
+
module_doc <<~DOC
|
|
14
|
+
JSON serialization, parsing, and formatting operations.
|
|
15
|
+
|
|
16
|
+
## Categories
|
|
17
|
+
- Conversion: >JSON, JSON>
|
|
18
|
+
- Formatting: JSON-PRETTIFY
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
{name: "Alice", age: 30} >JSON
|
|
22
|
+
'{"name":"Alice"}' JSON>
|
|
23
|
+
'{"a":1}' JSON-PRETTIFY
|
|
24
|
+
DOC
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
super("json")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
forthic_word :to_JSON, "( object:any -- json:string )", "Convert object to JSON string", ">JSON"
|
|
31
|
+
def to_JSON(object)
|
|
32
|
+
return "null" if object.nil?
|
|
33
|
+
JSON.generate(object)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
forthic_direct_word :from_JSON, "( json:string -- object:any )", "Parse JSON string to object", "JSON>"
|
|
37
|
+
def from_JSON(interp)
|
|
38
|
+
json = interp.stack_pop
|
|
39
|
+
result = if json.nil? || json.strip.empty?
|
|
40
|
+
nil
|
|
41
|
+
else
|
|
42
|
+
JSON.parse(json)
|
|
43
|
+
end
|
|
44
|
+
interp.stack_push(result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
forthic_word :JSON_PRETTIFY, "( json:string -- pretty:string )", "Format JSON with 2-space indentation", "JSON-PRETTIFY"
|
|
48
|
+
def JSON_PRETTIFY(json)
|
|
49
|
+
return "" if json.nil? || json.strip.empty?
|
|
50
|
+
obj = JSON.parse(json)
|
|
51
|
+
JSON.pretty_generate(obj)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|