gloo 6.7.1 → 7.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/CLAUDE.md +1 -0
- data/docs/application.md +3 -1
- data/docs/iterators.md +39 -3
- data/docs/language_syntax.md +53 -5
- data/docs/objects.md +58 -1
- data/docs/operators.md +16 -0
- data/docs/plugins.md +9 -1
- data/docs/verbs.md +4 -2
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +24 -0
- data/lib/gloo/app/engine.rb +65 -9
- data/lib/gloo/app/log.rb +47 -6
- data/lib/gloo/convert/converter.rb +63 -2
- data/lib/gloo/convert/string_to_date.rb +14 -0
- data/lib/gloo/convert/string_to_datetime.rb +16 -0
- data/lib/gloo/convert/string_to_decimal.rb +13 -0
- data/lib/gloo/convert/string_to_integer.rb +9 -2
- data/lib/gloo/convert/string_to_time.rb +28 -0
- data/lib/gloo/core/dictionary.rb +30 -21
- data/lib/gloo/core/error.rb +11 -2
- data/lib/gloo/core/event_manager.rb +7 -4
- data/lib/gloo/core/factory.rb +16 -11
- data/lib/gloo/core/here.rb +6 -1
- data/lib/gloo/core/invoker.rb +2 -3
- data/lib/gloo/core/not_found.rb +42 -0
- data/lib/gloo/core/obj.rb +28 -2
- data/lib/gloo/core/op.rb +2 -0
- data/lib/gloo/core/parser.rb +1 -1
- data/lib/gloo/core/pn.rb +33 -12
- data/lib/gloo/core/tokens.rb +38 -0
- data/lib/gloo/core/verb.rb +54 -0
- data/lib/gloo/exec/dispatch.rb +5 -6
- data/lib/gloo/exec/exec_env.rb +13 -0
- data/lib/gloo/exec/runner.rb +10 -2
- data/lib/gloo/exec/script.rb +13 -1
- data/lib/gloo/expr/expression.rb +39 -1
- data/lib/gloo/expr/op_plus.rb +3 -0
- data/lib/gloo/objs/basic/boolean.rb +16 -0
- data/lib/gloo/objs/basic/container.rb +154 -6
- data/lib/gloo/objs/basic/decimal.rb +1 -0
- data/lib/gloo/objs/basic/integer.rb +1 -0
- data/lib/gloo/objs/basic/string.rb +0 -1
- data/lib/gloo/objs/basic/string_msgs.rb +68 -32
- data/lib/gloo/objs/ctrl/each.rb +22 -3
- data/lib/gloo/objs/ctrl/each_dir.rb +20 -7
- data/lib/gloo/objs/ctrl/each_file.rb +78 -9
- data/lib/gloo/objs/ctrl/function.rb +6 -5
- data/lib/gloo/objs/dt/date.rb +28 -11
- data/lib/gloo/objs/dt/datetime.rb +72 -21
- data/lib/gloo/objs/dt/dt_tools.rb +46 -2
- data/lib/gloo/objs/dt/time.rb +28 -11
- data/lib/gloo/objs/str_utils/cipher.rb +17 -4
- data/lib/gloo/objs/str_utils/password.rb +6 -2
- data/lib/gloo/objs/system/erb.rb +9 -3
- data/lib/gloo/objs/system/file_handle.rb +57 -21
- data/lib/gloo/objs/system/system.rb +6 -2
- data/lib/gloo/objs/web/http_get.rb +12 -2
- data/lib/gloo/objs/web/http_post.rb +6 -2
- data/lib/gloo/objs/web/json.rb +38 -18
- data/lib/gloo/objs/web/uri.rb +36 -16
- data/lib/gloo/persist/file_loader.rb +72 -11
- data/lib/gloo/persist/file_saver.rb +2 -1
- data/lib/gloo/persist/indent_stack.rb +49 -33
- data/lib/gloo/persist/line_splitter.rb +40 -3
- data/lib/gloo/persist/persist_man.rb +2 -4
- data/lib/gloo/plugin/callback.rb +2 -2
- data/lib/gloo/plugin/ext_manager.rb +6 -9
- data/lib/gloo/plugin/lib_manager.rb +2 -2
- data/lib/gloo/verbs/break.rb +1 -0
- data/lib/gloo/verbs/check.rb +1 -1
- data/lib/gloo/verbs/cls.rb +1 -0
- data/lib/gloo/verbs/create.rb +32 -4
- data/lib/gloo/verbs/execute.rb +1 -1
- data/lib/gloo/verbs/exists.rb +27 -15
- data/lib/gloo/verbs/files.rb +2 -1
- data/lib/gloo/verbs/help.rb +1 -0
- data/lib/gloo/verbs/if.rb +1 -1
- data/lib/gloo/verbs/invoke.rb +1 -1
- data/lib/gloo/verbs/list.rb +4 -3
- data/lib/gloo/verbs/load.rb +3 -3
- data/lib/gloo/verbs/move.rb +10 -8
- data/lib/gloo/verbs/put.rb +20 -8
- data/lib/gloo/verbs/quit.rb +1 -0
- data/lib/gloo/verbs/redirect.rb +8 -2
- data/lib/gloo/verbs/reload.rb +9 -1
- data/lib/gloo/verbs/run.rb +2 -2
- data/lib/gloo/verbs/save.rb +4 -2
- data/lib/gloo/verbs/tell.rb +1 -1
- data/lib/gloo/verbs/unless.rb +1 -1
- data/lib/gloo/verbs/unload.rb +8 -0
- data/test.gloo/ctrl/each.test.gloo +88 -0
- data/test.gloo/lang/declaration.test.gloo +27 -0
- data/test.gloo/lang/exceptions.test.gloo +60 -0
- data/test.gloo/lang/failures.test.gloo +139 -0
- data/test.gloo/lang/it.test.gloo +133 -0
- data/test.gloo/lang/naming.test.gloo +11 -0
- data/test.gloo/lang/ops.test.gloo +30 -0
- data/test.gloo/objs/can.test.gloo +101 -0
- data/test.gloo/objs/string.test.gloo +24 -0
- data/test.gloo/string/str.test.gloo +28 -0
- data/test.gloo/verbs/create.test.gloo +36 -0
- data/test.gloo/verbs/exists.test.gloo +33 -1
- data/test.gloo/verbs/unload.test.gloo +34 -0
- metadata +6 -6
data/lib/gloo/core/pn.rb
CHANGED
|
@@ -14,6 +14,8 @@ module Gloo
|
|
|
14
14
|
ERROR = 'error'.freeze
|
|
15
15
|
CONTEXT = '@'.freeze
|
|
16
16
|
NAMED_COLORS = %w[red blue green white black yellow].freeze
|
|
17
|
+
IT_NOT_AN_OBJECT = "it isn't an object and can't be %s; %s.".freeze
|
|
18
|
+
IT_HINT = 'put it into an object first (put it into x)'.freeze
|
|
17
19
|
|
|
18
20
|
attr_reader :src, :elements
|
|
19
21
|
|
|
@@ -61,6 +63,19 @@ module Gloo
|
|
|
61
63
|
return @src.downcase == IT
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
#
|
|
67
|
+
# it is a read-only result value, not an object, so it can't
|
|
68
|
+
# be the target of a verb or message. If the path refers to it,
|
|
69
|
+
# report that (what it can't be: 'sent messages', 'run', ...,
|
|
70
|
+
# and what to do instead) and return true.
|
|
71
|
+
#
|
|
72
|
+
def it_target_err?( what, hint = IT_HINT )
|
|
73
|
+
return false unless self.it?
|
|
74
|
+
|
|
75
|
+
@engine.err format( IT_NOT_AN_OBJECT, what, hint )
|
|
76
|
+
return true
|
|
77
|
+
end
|
|
78
|
+
|
|
64
79
|
#
|
|
65
80
|
# Does the pathname reference refer to error?
|
|
66
81
|
#
|
|
@@ -141,19 +156,29 @@ module Gloo
|
|
|
141
156
|
self.set_to( "#{@engine.heap.context}#{@src[1..-1]}" )
|
|
142
157
|
end
|
|
143
158
|
|
|
159
|
+
#
|
|
160
|
+
# Expand any here (^) or context (@) reference into the
|
|
161
|
+
# full path, so the lookup below starts from the heap root.
|
|
162
|
+
#
|
|
163
|
+
def expand_refs
|
|
164
|
+
Here.expand_here( @engine, self ) if Here.includes_here_ref?( @elements )
|
|
165
|
+
expand_context if self.includes_context?
|
|
166
|
+
end
|
|
167
|
+
|
|
144
168
|
#
|
|
145
169
|
# Get the parent that contains the object referenced.
|
|
146
170
|
#
|
|
171
|
+
# This is a lookup, not a use of the object: a missing element
|
|
172
|
+
# returns nil without reporting an error. A caller that needs the
|
|
173
|
+
# object reports its own error.
|
|
174
|
+
#
|
|
147
175
|
def get_parent
|
|
148
176
|
o = @engine.heap.root
|
|
149
177
|
|
|
150
178
|
if self.includes_path?
|
|
151
179
|
@elements[ 0..-2 ].each do |e|
|
|
152
180
|
o = o.find_child( e )
|
|
153
|
-
if o.nil?
|
|
154
|
-
@engine.err "Object '#{e}' was not found."
|
|
155
|
-
return nil
|
|
156
|
-
end
|
|
181
|
+
return nil if o.nil?
|
|
157
182
|
end
|
|
158
183
|
end
|
|
159
184
|
|
|
@@ -162,12 +187,14 @@ module Gloo
|
|
|
162
187
|
|
|
163
188
|
#
|
|
164
189
|
# Does the object at the path exist?
|
|
190
|
+
# A question, so a missing object is an answer, not an error.
|
|
165
191
|
#
|
|
166
192
|
def exists?
|
|
167
193
|
return true if self.root?
|
|
168
194
|
return true if self.it?
|
|
169
195
|
return true if self.error?
|
|
170
196
|
|
|
197
|
+
expand_refs
|
|
171
198
|
parent = self.get_parent
|
|
172
199
|
return false unless parent
|
|
173
200
|
|
|
@@ -186,6 +213,7 @@ module Gloo
|
|
|
186
213
|
#
|
|
187
214
|
# Resolve the pathname reference.
|
|
188
215
|
# Find the object referenced or return nil if it is not found.
|
|
216
|
+
# Like get_parent, a missing object is not reported here.
|
|
189
217
|
#
|
|
190
218
|
def resolve
|
|
191
219
|
return @engine.heap.root if self.root?
|
|
@@ -194,14 +222,7 @@ module Gloo
|
|
|
194
222
|
return Gloo::Core::GlooSystem.new(
|
|
195
223
|
@engine, self ) if self.gloo_sys?
|
|
196
224
|
|
|
197
|
-
|
|
198
|
-
Here.expand_here( @engine, self )
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
if self.includes_context?
|
|
202
|
-
expand_context
|
|
203
|
-
end
|
|
204
|
-
|
|
225
|
+
expand_refs
|
|
205
226
|
parent = self.get_parent
|
|
206
227
|
return nil unless parent
|
|
207
228
|
|
data/lib/gloo/core/tokens.rb
CHANGED
|
@@ -18,6 +18,10 @@ module Gloo
|
|
|
18
18
|
|
|
19
19
|
QUOTE_CHARS = [ '"', "'" ].freeze
|
|
20
20
|
|
|
21
|
+
UNCLOSED_QUOTE_ERR = 'Unclosed quote.'.freeze
|
|
22
|
+
UNCLOSED_PAREN_ERR = 'Unclosed parenthesis.'.freeze
|
|
23
|
+
UNMATCHED_PAREN_ERR = "Unmatched ')'.".freeze
|
|
24
|
+
|
|
21
25
|
attr_reader :cmd, :tokens
|
|
22
26
|
|
|
23
27
|
# ---------------------------------------------------------------------
|
|
@@ -38,6 +42,40 @@ module Gloo
|
|
|
38
42
|
# Public functions
|
|
39
43
|
# ---------------------------------------------------------------------
|
|
40
44
|
|
|
45
|
+
#
|
|
46
|
+
# A syntax problem in the command (an unclosed quote, or
|
|
47
|
+
# parentheses that don't match), or nil. The tokens are still
|
|
48
|
+
# the best guess: an unclosed quote runs to the end of the line.
|
|
49
|
+
# Quotes are read the same way tokenize reads them.
|
|
50
|
+
#
|
|
51
|
+
def syntax_problem
|
|
52
|
+
quote = nil
|
|
53
|
+
depth = 0
|
|
54
|
+
escaped = false
|
|
55
|
+
@cmd.each_char do |ch|
|
|
56
|
+
if quote
|
|
57
|
+
if escaped
|
|
58
|
+
escaped = false
|
|
59
|
+
elsif ch == '\\'
|
|
60
|
+
escaped = true
|
|
61
|
+
elsif ch == quote
|
|
62
|
+
quote = nil
|
|
63
|
+
end
|
|
64
|
+
elsif QUOTE_CHARS.include?( ch )
|
|
65
|
+
quote = ch
|
|
66
|
+
elsif ch == '('
|
|
67
|
+
depth += 1
|
|
68
|
+
elsif ch == ')'
|
|
69
|
+
depth -= 1
|
|
70
|
+
return UNMATCHED_PAREN_ERR if depth.negative?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
return UNCLOSED_QUOTE_ERR if quote
|
|
74
|
+
return UNCLOSED_PAREN_ERR if depth.positive?
|
|
75
|
+
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
|
|
41
79
|
#
|
|
42
80
|
# Get the number of tokens
|
|
43
81
|
#
|
data/lib/gloo/core/verb.rb
CHANGED
|
@@ -29,6 +29,60 @@ module Gloo
|
|
|
29
29
|
Dictionary.instance.register_verb( subclass )
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
#
|
|
33
|
+
# Report any syntax problem in the command's tokens or params
|
|
34
|
+
# (see Tokens#syntax_problem) as a syntax error. The verb still
|
|
35
|
+
# runs afterward, with the tokens' best guess.
|
|
36
|
+
#
|
|
37
|
+
def check_syntax
|
|
38
|
+
[ @tokens, @params ].each do |t|
|
|
39
|
+
problem = t.syntax_problem if t.respond_to?( :syntax_problem )
|
|
40
|
+
@engine.syntax_err problem if problem
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# For a verb that takes no object: whatever was written after it,
|
|
46
|
+
# as one string (empty if nothing was).
|
|
47
|
+
#
|
|
48
|
+
def extra_words
|
|
49
|
+
words = @tokens.respond_to?( :params ) ? @tokens.params.to_a : []
|
|
50
|
+
words += @params.tokens.to_a if @params.respond_to?( :tokens )
|
|
51
|
+
return words.join( ' ' )
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# The verb as written in the command (keyword or shortcut).
|
|
56
|
+
#
|
|
57
|
+
def written_verb
|
|
58
|
+
written = @tokens.verb if @tokens.respond_to?( :verb )
|
|
59
|
+
return written || self.class.keyword
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# For a harmless verb that takes no object: warn if anything was
|
|
64
|
+
# written after it. The verb still runs.
|
|
65
|
+
#
|
|
66
|
+
def warn_extra_words
|
|
67
|
+
extra = extra_words
|
|
68
|
+
return if extra.empty?
|
|
69
|
+
|
|
70
|
+
@engine.warn "#{written_verb} takes no object; ignoring '#{extra}'."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# For a destructive verb that takes no object: if anything was
|
|
75
|
+
# written after it, report an error and return true; the verb
|
|
76
|
+
# should not run. The detail says what didn't happen and what to
|
|
77
|
+
# do instead.
|
|
78
|
+
#
|
|
79
|
+
def extra_words_err?( detail )
|
|
80
|
+
return false if extra_words.empty?
|
|
81
|
+
|
|
82
|
+
@engine.err "#{written_verb} takes no object, so #{detail}."
|
|
83
|
+
return true
|
|
84
|
+
end
|
|
85
|
+
|
|
32
86
|
#
|
|
33
87
|
# Run the verb.
|
|
34
88
|
#
|
data/lib/gloo/exec/dispatch.rb
CHANGED
|
@@ -12,7 +12,6 @@ module Gloo
|
|
|
12
12
|
module Exec
|
|
13
13
|
class Dispatch
|
|
14
14
|
|
|
15
|
-
OBJ_NOT_FOUND_ERR = 'Object was not found: '.freeze
|
|
16
15
|
|
|
17
16
|
#
|
|
18
17
|
# Send a message to an object of a given name (and path).
|
|
@@ -20,10 +19,14 @@ module Gloo
|
|
|
20
19
|
#
|
|
21
20
|
def self.send_message( engine, msg, to_obj_pn, params = nil )
|
|
22
21
|
pn = Gloo::Core::Pn.new( engine, to_obj_pn )
|
|
22
|
+
return if pn.it_target_err?( 'sent messages' )
|
|
23
|
+
|
|
23
24
|
target_obj = pn.resolve
|
|
24
25
|
|
|
25
26
|
unless target_obj
|
|
26
|
-
|
|
27
|
+
# The message never ran, so there is no result.
|
|
28
|
+
engine.err Gloo::Core::NotFound.object( to_obj_pn )
|
|
29
|
+
engine.heap.it.set_to false
|
|
27
30
|
return
|
|
28
31
|
end
|
|
29
32
|
Gloo::Exec::Dispatch.message( engine, msg, target_obj, params )
|
|
@@ -44,10 +47,6 @@ module Gloo
|
|
|
44
47
|
# Dispatch an action.
|
|
45
48
|
#
|
|
46
49
|
def self.action( engine, action )
|
|
47
|
-
unless action.valid?
|
|
48
|
-
engine.log.warn "Object #{action.to.name} does not respond to #{action.msg}"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
50
|
engine.exec_env.push_action action
|
|
52
51
|
engine.log.debug "Sending message #{action.msg} to #{action.to.name}"
|
|
53
52
|
action.dispatch
|
data/lib/gloo/exec/exec_env.rb
CHANGED
|
@@ -12,6 +12,9 @@ module Gloo
|
|
|
12
12
|
attr_accessor :verbs, :actions, :scripts, :here
|
|
13
13
|
attr_reader :running_script
|
|
14
14
|
|
|
15
|
+
# The file and line being loaded, if a file is being loaded.
|
|
16
|
+
attr_accessor :load_location
|
|
17
|
+
|
|
15
18
|
VERB_STACK = 'verbs'.freeze
|
|
16
19
|
ACTION_STACK = 'actions'.freeze
|
|
17
20
|
SCRIPT_STACK = 'scripts'.freeze
|
|
@@ -31,6 +34,16 @@ module Gloo
|
|
|
31
34
|
@here = Gloo::Exec::Stack.new( @engine, HERE_STACK )
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
#
|
|
38
|
+
# Where the line now running is, for error messages: the running
|
|
39
|
+
# script and its line, or the file and line being loaded.
|
|
40
|
+
#
|
|
41
|
+
def current_location
|
|
42
|
+
return @running_script.location if @running_script
|
|
43
|
+
|
|
44
|
+
return @load_location
|
|
45
|
+
end
|
|
46
|
+
|
|
34
47
|
#
|
|
35
48
|
# Get the here object.
|
|
36
49
|
#
|
data/lib/gloo/exec/runner.rb
CHANGED
|
@@ -20,8 +20,12 @@ module Gloo
|
|
|
20
20
|
engine.heap.error.start_tracking
|
|
21
21
|
engine.exec_env.verbs.push verb
|
|
22
22
|
begin
|
|
23
|
+
verb&.check_syntax
|
|
23
24
|
verb&.run
|
|
24
|
-
rescue => ex
|
|
25
|
+
rescue StandardError, ScriptError => ex
|
|
26
|
+
# ScriptError (a SyntaxError from eval'd code, a LoadError
|
|
27
|
+
# from a missing gem) isn't a StandardError, and would
|
|
28
|
+
# otherwise stop gloo altogether.
|
|
25
29
|
engine.handle_exception( ex )
|
|
26
30
|
ensure
|
|
27
31
|
engine.exec_env.verbs.pop
|
|
@@ -36,12 +40,16 @@ module Gloo
|
|
|
36
40
|
def self.run( engine, path_name )
|
|
37
41
|
engine.log.debug "running script at #{path_name}"
|
|
38
42
|
pn = Gloo::Core::Pn.new( engine, path_name )
|
|
43
|
+
return if pn.it_target_err?( 'run' )
|
|
44
|
+
|
|
39
45
|
o = pn.resolve
|
|
40
46
|
|
|
41
47
|
if o
|
|
42
48
|
o.send_message 'run'
|
|
43
49
|
else
|
|
44
|
-
|
|
50
|
+
# The message never ran, so there is no result.
|
|
51
|
+
engine.err Gloo::Core::NotFound.object( path_name )
|
|
52
|
+
engine.heap.it.set_to false
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
|
data/lib/gloo/exec/script.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Gloo
|
|
|
16
16
|
@engine = engine
|
|
17
17
|
@obj = obj
|
|
18
18
|
@break_out = false
|
|
19
|
+
@line = 0
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
#
|
|
@@ -27,10 +28,13 @@ module Gloo
|
|
|
27
28
|
@engine.exec_env.push_script self
|
|
28
29
|
|
|
29
30
|
if @obj.value.is_a? String
|
|
31
|
+
@line = 1
|
|
30
32
|
@engine.parser.run @obj.value
|
|
31
33
|
elsif @obj.value.is_a? Array
|
|
32
|
-
@obj.value.
|
|
34
|
+
@obj.value.each_with_index do |line, i|
|
|
33
35
|
break if @break_out
|
|
36
|
+
|
|
37
|
+
@line = i + 1
|
|
34
38
|
@engine.parser.run line
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -38,6 +42,14 @@ module Gloo
|
|
|
38
42
|
@engine.exec_env.pop_script
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
#
|
|
46
|
+
# Where this script is in its run: its path and the line now
|
|
47
|
+
# running (counting from 1), for error messages.
|
|
48
|
+
#
|
|
49
|
+
def location
|
|
50
|
+
return "#{@obj.pn}, line #{@line}"
|
|
51
|
+
end
|
|
52
|
+
|
|
41
53
|
#
|
|
42
54
|
# Generic function to get display value.
|
|
43
55
|
# Can be used for debugging, etc.
|
data/lib/gloo/expr/expression.rb
CHANGED
|
@@ -8,6 +8,8 @@ module Gloo
|
|
|
8
8
|
module Expr
|
|
9
9
|
class Expression
|
|
10
10
|
|
|
11
|
+
DIVIDE_BY_ZERO_ERR = 'Division by zero.'.freeze
|
|
12
|
+
|
|
11
13
|
# ---------------------------------------------------------------------
|
|
12
14
|
# Constructor
|
|
13
15
|
# ---------------------------------------------------------------------
|
|
@@ -36,6 +38,7 @@ module Gloo
|
|
|
36
38
|
|
|
37
39
|
@symbols.each do |sym|
|
|
38
40
|
if sym.is_a? Gloo::Core::Op
|
|
41
|
+
missing_value( @op ) if @op
|
|
39
42
|
@op = sym
|
|
40
43
|
elsif @left.nil?
|
|
41
44
|
@left = sym
|
|
@@ -45,6 +48,7 @@ module Gloo
|
|
|
45
48
|
|
|
46
49
|
perform_op if @left && @right
|
|
47
50
|
end
|
|
51
|
+
missing_value( @op ) if @op
|
|
48
52
|
|
|
49
53
|
return @left.value if @left.is_a? Gloo::Core::Literal
|
|
50
54
|
return @left.value if @left.is_a? Gloo::Expr::Call
|
|
@@ -66,11 +70,39 @@ module Gloo
|
|
|
66
70
|
@op ||= Gloo::Core::Op.default_op
|
|
67
71
|
l = evaluate_sym @left
|
|
68
72
|
r = evaluate_sym @right
|
|
69
|
-
@left = @op
|
|
73
|
+
@left = apply_op( @op, l, r )
|
|
70
74
|
@right = nil
|
|
71
75
|
@op = nil
|
|
72
76
|
end
|
|
73
77
|
|
|
78
|
+
#
|
|
79
|
+
# Apply the operator. An operator that can't be used with the value
|
|
80
|
+
# (eg. 'a' / 2), or a division by zero, is a runtime error, and the
|
|
81
|
+
# result is no value. A missing value (nil) was already reported.
|
|
82
|
+
#
|
|
83
|
+
def apply_op( op, left, right )
|
|
84
|
+
result = op.perform( left, right )
|
|
85
|
+
if op.is_a?( Gloo::Expr::OpDiv ) && result.is_a?( Float ) &&
|
|
86
|
+
( result.nan? || result.infinite? )
|
|
87
|
+
raise ZeroDivisionError
|
|
88
|
+
end
|
|
89
|
+
return result unless result.nil? && !left.nil?
|
|
90
|
+
|
|
91
|
+
@engine.err "Operator '#{op.class::SYMBOL}' can't be used with '#{left}'."
|
|
92
|
+
return nil
|
|
93
|
+
rescue ZeroDivisionError
|
|
94
|
+
@engine.err DIVIDE_BY_ZERO_ERR
|
|
95
|
+
return nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# An operator without a value on one side (eg. 'show 1 +') is a
|
|
100
|
+
# syntax error; the best guess is to leave the operator out.
|
|
101
|
+
#
|
|
102
|
+
def missing_value( op )
|
|
103
|
+
@engine.syntax_err "Operator '#{op.class::SYMBOL}' is missing a value."
|
|
104
|
+
end
|
|
105
|
+
|
|
74
106
|
#
|
|
75
107
|
# Evaluate the symbol and get a simple value.
|
|
76
108
|
#
|
|
@@ -88,8 +120,14 @@ module Gloo
|
|
|
88
120
|
def resolve_ref( ref )
|
|
89
121
|
return ref.src if ref.named_color?
|
|
90
122
|
|
|
123
|
+
name = ref.src
|
|
91
124
|
ob = ref.resolve
|
|
92
125
|
return ob.value if ob
|
|
126
|
+
|
|
127
|
+
# The object is being used as a value, so a missing object is
|
|
128
|
+
# a runtime error (the lookup itself doesn't report it).
|
|
129
|
+
@engine.err Gloo::Core::NotFound.object( name )
|
|
130
|
+
return nil
|
|
93
131
|
end
|
|
94
132
|
|
|
95
133
|
#
|
data/lib/gloo/expr/op_plus.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Gloo
|
|
|
9
9
|
class OpPlus < Gloo::Core::Op
|
|
10
10
|
|
|
11
11
|
SYMBOL = '+'.freeze
|
|
12
|
+
# An alternate spelling that reads better when joining strings:
|
|
13
|
+
# put first and ' ' and last into full_name
|
|
14
|
+
ALT_SYMBOL = 'and'.freeze
|
|
12
15
|
|
|
13
16
|
#
|
|
14
17
|
# Perform the operation and return the result.
|
|
@@ -12,6 +12,7 @@ module Gloo
|
|
|
12
12
|
KEYWORD_SHORT = 'bool'.freeze
|
|
13
13
|
TRUE = 'true'.freeze
|
|
14
14
|
FALSE = 'false'.freeze
|
|
15
|
+
BOOLEAN_STRINGS = [ TRUE, FALSE, 't', 'f' ].freeze
|
|
15
16
|
|
|
16
17
|
#
|
|
17
18
|
# The name of the object type.
|
|
@@ -32,6 +33,20 @@ module Gloo
|
|
|
32
33
|
#
|
|
33
34
|
def set_value( new_value )
|
|
34
35
|
self.value = Gloo::Objs::Boolean.coerse_to_bool( new_value )
|
|
36
|
+
return unless Gloo::Objs::Boolean.bad_value?( new_value )
|
|
37
|
+
|
|
38
|
+
@engine.warn "'#{new_value}' is not true or false; using #{self.value}."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Is the value a string that isn't a boolean? A blank value is
|
|
43
|
+
# no value (false).
|
|
44
|
+
#
|
|
45
|
+
def self.bad_value?( new_value )
|
|
46
|
+
return false unless new_value.is_a?( ::String )
|
|
47
|
+
return false if new_value.strip.empty?
|
|
48
|
+
|
|
49
|
+
return !BOOLEAN_STRINGS.include?( new_value.strip.downcase )
|
|
35
50
|
end
|
|
36
51
|
|
|
37
52
|
#
|
|
@@ -132,6 +147,7 @@ module Gloo
|
|
|
132
147
|
'true — Set the boolean to true.',
|
|
133
148
|
'false — Set the boolean to false.'
|
|
134
149
|
],
|
|
150
|
+
:notes => 'Recognized values are true, false, t and f (in any case), and integers (0 is false). Any other string is false, with a warning: "\'maybe\' is not true or false; using false." A blank value is false.',
|
|
135
151
|
:examples => <<~EXAMPLES.strip
|
|
136
152
|
b [can] :
|
|
137
153
|
flag [boolean] : true
|
|
@@ -10,6 +10,8 @@ module Gloo
|
|
|
10
10
|
|
|
11
11
|
KEYWORD = 'container'.freeze
|
|
12
12
|
KEYWORD_SHORT = 'can'.freeze
|
|
13
|
+
MISSING_INDEX_MSG = 'Missing index!'.freeze
|
|
14
|
+
MISSING_NAME_MSG = 'Missing child name!'.freeze
|
|
13
15
|
|
|
14
16
|
#
|
|
15
17
|
# The name of the object type.
|
|
@@ -33,7 +35,8 @@ module Gloo
|
|
|
33
35
|
# Get a list of message names that this object receives.
|
|
34
36
|
#
|
|
35
37
|
def self.messages
|
|
36
|
-
return super + %w[count delete_children child_exists show_key_value_table
|
|
38
|
+
return super + %w[count delete_children child_exists show_key_value_table
|
|
39
|
+
child_value_at child_path_at random_child_value random_child_path]
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
#
|
|
@@ -65,17 +68,138 @@ module Gloo
|
|
|
65
68
|
# Check to see if there is a child with the given name.
|
|
66
69
|
#
|
|
67
70
|
def msg_child_exists
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
unless @params&.token_count&.positive?
|
|
72
|
+
@engine.syntax_err MISSING_NAME_MSG
|
|
73
|
+
return put_it( false )
|
|
71
74
|
end
|
|
72
|
-
|
|
75
|
+
|
|
76
|
+
expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
|
|
77
|
+
data = expr.evaluate
|
|
78
|
+
return put_it( false ) if data.nil?
|
|
73
79
|
|
|
74
80
|
val = self.contains_child?( data )
|
|
75
81
|
@engine.heap.it.set_to val
|
|
76
82
|
return val
|
|
77
83
|
end
|
|
78
84
|
|
|
85
|
+
#
|
|
86
|
+
# Get the value of the child at the given 0-based index.
|
|
87
|
+
#
|
|
88
|
+
def msg_child_value_at
|
|
89
|
+
child = child_at_param
|
|
90
|
+
return child if child == false
|
|
91
|
+
|
|
92
|
+
return put_child_value( child )
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# Get the path of the child at the given 0-based index.
|
|
97
|
+
#
|
|
98
|
+
def msg_child_path_at
|
|
99
|
+
child = child_at_param
|
|
100
|
+
return child if child == false
|
|
101
|
+
|
|
102
|
+
return put_it( child.pn )
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#
|
|
106
|
+
# Get the value of a random child.
|
|
107
|
+
#
|
|
108
|
+
def msg_random_child_value
|
|
109
|
+
child = random_child
|
|
110
|
+
return child if child == false
|
|
111
|
+
|
|
112
|
+
return put_child_value( child )
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
#
|
|
116
|
+
# Get the path of a random child.
|
|
117
|
+
#
|
|
118
|
+
def msg_random_child_path
|
|
119
|
+
child = random_child
|
|
120
|
+
return child if child == false
|
|
121
|
+
|
|
122
|
+
return put_it( child.pn )
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------
|
|
126
|
+
# Child Access Helpers
|
|
127
|
+
# ---------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
#
|
|
130
|
+
# Find the child at the index given as the message parameter.
|
|
131
|
+
# Reports the problem, puts false into it, and returns false
|
|
132
|
+
# if there is no index or no child at that index.
|
|
133
|
+
#
|
|
134
|
+
def child_at_param
|
|
135
|
+
unless @params&.token_count&.positive?
|
|
136
|
+
@engine.syntax_err MISSING_INDEX_MSG
|
|
137
|
+
return put_it( false )
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
|
|
141
|
+
data = expr.evaluate
|
|
142
|
+
index = Container.to_index( data )
|
|
143
|
+
if index.nil?
|
|
144
|
+
@engine.err "Index '#{data}' is not a number!"
|
|
145
|
+
return put_it( false )
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if index.negative? || index >= child_count
|
|
149
|
+
@engine.err "Index #{index} is out of range (0 to #{child_count - 1})!"
|
|
150
|
+
return put_it( false )
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return children[ index ]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
#
|
|
157
|
+
# Get a whole number index from an evaluated parameter.
|
|
158
|
+
# Returns nil if it isn't one.
|
|
159
|
+
#
|
|
160
|
+
def self.to_index( data )
|
|
161
|
+
return data if data.is_a?( ::Integer )
|
|
162
|
+
return data.strip.to_i if data.is_a?( ::String ) && data.strip.match?( /\A-?\d+\z/ )
|
|
163
|
+
|
|
164
|
+
return nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
#
|
|
168
|
+
# Pick a random child.
|
|
169
|
+
# Reports the problem, puts false into it, and returns false
|
|
170
|
+
# if the container is empty.
|
|
171
|
+
#
|
|
172
|
+
def random_child
|
|
173
|
+
if child_count.zero?
|
|
174
|
+
@engine.err "Container '#{pn}' is empty!"
|
|
175
|
+
return put_it( false )
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return children.sample
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#
|
|
182
|
+
# Put the value of the child into it.
|
|
183
|
+
# A container child has no value of its own; use its path instead.
|
|
184
|
+
#
|
|
185
|
+
def put_child_value( child )
|
|
186
|
+
child = Gloo::Objs::Alias.resolve_alias( @engine, child )
|
|
187
|
+
if child.is_container?
|
|
188
|
+
@engine.err "Child '#{child.pn}' is a container, not a value! Use its path instead."
|
|
189
|
+
return put_it( false )
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
return put_it( child.value )
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
#
|
|
196
|
+
# Put the value into it and return it.
|
|
197
|
+
#
|
|
198
|
+
def put_it( val )
|
|
199
|
+
@engine.heap.it.set_to val
|
|
200
|
+
return val
|
|
201
|
+
end
|
|
202
|
+
|
|
79
203
|
# ---------------------------------------------------------------------
|
|
80
204
|
# Object Documentation
|
|
81
205
|
# ---------------------------------------------------------------------
|
|
@@ -99,8 +223,17 @@ module Gloo
|
|
|
99
223
|
'count — Count the number of children objects in the container. The result is put in it.',
|
|
100
224
|
'delete_children — Delete all children objects from the container.',
|
|
101
225
|
'show_key_value_table — Show a table with key (name) and values for all children in the container.',
|
|
102
|
-
'child_exists ({name}) — Check to see if there is a child with the given name. A parameter is required. It will have a boolean.'
|
|
226
|
+
'child_exists ({name}) — Check to see if there is a child with the given name. A parameter is required. It will have a boolean.',
|
|
227
|
+
'child_value_at ({index}) — Get the value of the child at position {index}. Positions are 0-based and in the order the children were added, so after split_list, index 0 is the child named 1. A parameter is required. It will have the value; an out-of-range or non-numeric index, or a child that is a container, is an error, and it will have false.',
|
|
228
|
+
'child_path_at ({index}) — Get the path (from root) of the child at position {index}, 0-based. Works for any child, including a container; put it into an alias (put it into ptr*) to reach the child and its fields. A parameter is required. It will have the path; an out-of-range or non-numeric index is an error, and it will have false.',
|
|
229
|
+
'random_child_value — Get the value of a randomly chosen child. It will have the value; an empty container, or picking a child that is a container, is an error, and it will have false.',
|
|
230
|
+
'random_child_path — Get the path (from root) of a randomly chosen child. It will have the path; an empty container is an error, and it will have false.'
|
|
103
231
|
],
|
|
232
|
+
:notes => 'The random_child messages pick with replacement: two ' \
|
|
233
|
+
'calls can pick the same child. For distinct picks, pick a ' \
|
|
234
|
+
'random index with an integer\'s randomize message, keep the ' \
|
|
235
|
+
'indexes already used as children of another container, and ' \
|
|
236
|
+
'check it with child_exists before using child_path_at.',
|
|
104
237
|
:examples => <<~EXAMPLES.strip
|
|
105
238
|
can [can] :
|
|
106
239
|
data [can] :
|
|
@@ -109,6 +242,21 @@ module Gloo
|
|
|
109
242
|
3 : three
|
|
110
243
|
on_load [script] :
|
|
111
244
|
tell can.data to show_key_value_table
|
|
245
|
+
|
|
246
|
+
#
|
|
247
|
+
# Reach a container child's fields through an alias.
|
|
248
|
+
#
|
|
249
|
+
books [can] :
|
|
250
|
+
list [can] :
|
|
251
|
+
a [can] :
|
|
252
|
+
title [string] : Walden
|
|
253
|
+
b [can] :
|
|
254
|
+
title [string] : Emma
|
|
255
|
+
ptr [alias] :
|
|
256
|
+
on_load [script] :
|
|
257
|
+
tell books.list to child_path_at (1)
|
|
258
|
+
put it into books.ptr*
|
|
259
|
+
show books.ptr.title
|
|
112
260
|
EXAMPLES
|
|
113
261
|
}
|
|
114
262
|
end
|
|
@@ -109,6 +109,7 @@ module Gloo
|
|
|
109
109
|
'an optional parameter is included, round to the precision specified.',
|
|
110
110
|
'format ({fmt}) — With no parameter, adds comma separators to the whole part (e.g. 1000.5 -> 1,000.5). With a parameter, uses it as a sprintf-style format string (e.g. \'%.2f\'). It will have the formatted string.'
|
|
111
111
|
],
|
|
112
|
+
:notes => 'A value that isn\'t a number (eg. \'x\' or \'1,5\') is used as a best guess, with a warning: "\'1,5\' is not a decimal number; using 1.0." A blank value is 0.0.',
|
|
112
113
|
:examples => <<~EXAMPLES.strip
|
|
113
114
|
d [can] :
|
|
114
115
|
x [decimal] : 100
|
|
@@ -140,6 +140,7 @@ module Gloo
|
|
|
140
140
|
'range to 6 and add 1 to the result.',
|
|
141
141
|
'format ({fmt}) — With no parameter, adds comma separators (e.g. 1000 -> 1,000). With a parameter, uses it as a sprintf-style format string (e.g. \'%05d\'). It will have the formatted string.'
|
|
142
142
|
],
|
|
143
|
+
:notes => 'A value that isn\'t a whole number (eg. \'x\', \'12abc\' or \'3.7\') is used as a best guess, with a warning: "\'x\' is not an integer; using 0." A blank value is 0.',
|
|
143
144
|
:examples => <<~EXAMPLES.strip
|
|
144
145
|
#
|
|
145
146
|
# Integer object.
|