gloo 6.3.0 → 6.4.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +13 -0
  3. data/docs/language_objects.md +38 -0
  4. data/docs/language_scripting.md +41 -0
  5. data/docs/verbs.md +27 -2
  6. data/lib/VERSION +1 -1
  7. data/lib/VERSION_NOTES +10 -0
  8. data/lib/gloo/core/obj.rb +40 -1
  9. data/lib/gloo/core/parser.rb +30 -1
  10. data/lib/gloo/core/tokens.rb +44 -18
  11. data/lib/gloo/expr/l_string.rb +8 -4
  12. data/lib/gloo/objs/basic/script.rb +28 -2
  13. data/lib/gloo/objs/web/json.rb +4 -1
  14. data/lib/gloo/persist/comment_buffer.rb +58 -0
  15. data/lib/gloo/persist/declaration_ledger.rb +61 -0
  16. data/lib/gloo/persist/disc_mech.rb +15 -2
  17. data/lib/gloo/persist/file_loader.rb +201 -100
  18. data/lib/gloo/persist/file_saver.rb +251 -6
  19. data/lib/gloo/persist/file_storage.rb +10 -4
  20. data/lib/gloo/persist/indent_stack.rb +90 -0
  21. data/lib/gloo/persist/line_splitter.rb +18 -3
  22. data/lib/gloo/persist/persist_man.rb +159 -24
  23. data/lib/gloo/persist/script_body_collector.rb +102 -0
  24. data/lib/gloo/persist/shorthand_expander.rb +51 -0
  25. data/lib/gloo/persist/source/blank_node.rb +27 -0
  26. data/lib/gloo/persist/source/comment_node.rb +27 -0
  27. data/lib/gloo/persist/source/directive_node.rb +26 -0
  28. data/lib/gloo/persist/source/obj_node.rb +50 -0
  29. data/lib/gloo/persist/source/source_doc.rb +41 -0
  30. data/lib/gloo/verbs/load.rb +5 -0
  31. data/lib/gloo/verbs/reload.rb +3 -0
  32. data/lib/gloo/verbs/save.rb +57 -10
  33. data/test.gloo/lang/shorthand.test.gloo +29 -0
  34. data/test.gloo/objs/json.test.gloo +9 -0
  35. data/test.gloo/objs/script.test.gloo +1 -1
  36. data/test.gloo/objs/text.test.gloo +22 -1
  37. data/test.gloo/string/str.test.gloo +15 -0
  38. data/test.gloo/verbs/reload.test.gloo +57 -0
  39. data/test.gloo/verbs/save.test.gloo +115 -0
  40. metadata +14 -1
@@ -8,7 +8,7 @@ module Gloo
8
8
  module Persist
9
9
  class FileStorage
10
10
 
11
- attr_reader :obj, :pn
11
+ attr_reader :obj, :pn, :roots, :source_doc
12
12
 
13
13
  #
14
14
  # Set up a file storage for an object.
@@ -17,13 +17,17 @@ module Gloo
17
17
  @engine = engine
18
18
  @obj = obj
19
19
  @pn = pn
20
+ @roots = obj ? [ obj ] : []
21
+ @source_doc = nil
20
22
  end
21
23
 
22
24
  #
23
- # Save the object to the file.
25
+ # Save the object to the file. batch, when given, lets a
26
+ # multi-file namespace round-trip -- see PersistMan#save_batch and
27
+ # FileSaver.
24
28
  #
25
- def save
26
- fs = FileSaver.new( @engine, @pn, @obj )
29
+ def save( batch = nil )
30
+ fs = FileSaver.new( @engine, @pn, @obj, @source_doc, batch )
27
31
  fs.save
28
32
  end
29
33
 
@@ -34,6 +38,8 @@ module Gloo
34
38
  fl = FileLoader.new( @engine, @pn )
35
39
  fl.load
36
40
  @obj = fl.obj
41
+ @roots = fl.roots
42
+ @source_doc = fl.source_doc
37
43
  if @obj
38
44
  @engine.log.debug "Loaded object: #{@obj.name}"
39
45
  else
@@ -0,0 +1,90 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Tracks the current nesting level shared by the heap tree and the
5
+ # source tree as declaration lines are processed, pushing and popping
6
+ # both stacks in lockstep so their nesting always matches -- an object
7
+ # only becomes a parent once a deeper line actually follows it.
8
+ #
9
+
10
+ module Gloo
11
+ module Persist
12
+ class IndentStack
13
+
14
+ attr_reader :tabs
15
+
16
+ #
17
+ # Set up a stack rooted at the given heap object and source node.
18
+ #
19
+ def initialize( root_obj, root_node )
20
+ @tabs = 0
21
+ @parent_stack = [ root_obj ]
22
+ @node_stack = [ root_node ]
23
+ end
24
+
25
+ #
26
+ # The heap object new children should be created under.
27
+ #
28
+ def parent
29
+ return @parent_stack.last
30
+ end
31
+
32
+ #
33
+ # The source node new declarations/trivia should be appended to.
34
+ #
35
+ def node
36
+ return @node_stack.last
37
+ end
38
+
39
+ #
40
+ # Move the stacks to the right depth for a line at the given
41
+ # indentation, then -- if it turned out to be deeper than the
42
+ # previous line -- push the previous line's object/node as the
43
+ # new parent. last_obj/last_node are whatever was created for the
44
+ # previous declaration line.
45
+ #
46
+ def place( line_tabs, last_obj, last_node )
47
+ indent = indent_delta( line_tabs )
48
+ if indent.positive?
49
+ # last_obj is nil when the previous line failed to make an
50
+ # object (eg. an unknown type in a malformed file) -- keep
51
+ # the current parent rather than pushing nil.
52
+ @parent_stack.push( last_obj || @parent_stack.last )
53
+ @node_stack.push( last_node || @node_stack.last )
54
+ elsif indent.negative?
55
+ indent.abs.times do
56
+ # never pop the root -- a file with erratic (mixed
57
+ # tab/space) indentation can outdent further than it ever
58
+ # indented; clamp instead of emptying the stack.
59
+ break if @parent_stack.length <= 1
60
+
61
+ @parent_stack.pop
62
+ @node_stack.pop
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ #
70
+ # Update @tabs for the new line and return the signed change in
71
+ # nesting depth: positive if it's more indented than the last
72
+ # line placed, negative if less, zero if the same.
73
+ #
74
+ def indent_delta( line_tabs )
75
+ if line_tabs > @tabs
76
+ # TODO: What if indent is more than one more level?
77
+ @tabs = line_tabs
78
+ return 1
79
+ elsif line_tabs < @tabs
80
+ diff = @tabs - line_tabs
81
+ @tabs -= diff
82
+ return -diff
83
+ end
84
+
85
+ return 0
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -12,7 +12,7 @@ module Gloo
12
12
  BEGIN_BLOCK = 'BEGIN'.freeze
13
13
  END_BLOCK = 'END'.freeze
14
14
 
15
- attr_reader :obj
15
+ attr_reader :obj, :raw_tail
16
16
 
17
17
  #
18
18
  # Set up a line splitter
@@ -20,6 +20,7 @@ module Gloo
20
20
  def initialize( line, tabs )
21
21
  @line = line
22
22
  @tabs = tabs
23
+ @raw_tail = ''
23
24
  end
24
25
 
25
26
  #
@@ -36,15 +37,26 @@ module Gloo
36
37
  #
37
38
  # Detect the object name.
38
39
  #
40
+ # Only leading whitespace (the indentation, captured separately by
41
+ # the loader) and the trailing newline are removed here --
42
+ # trailing spaces/tabs are left on the line so they survive into
43
+ # a string value, which can matter when the value is used to
44
+ # build HTML.
45
+ #
39
46
  def detect_name
40
- @line = @line.strip
47
+ @line = @line.lstrip.chomp
41
48
  @idx = @line.index( ' ' )
42
49
  @idx = 0 unless @idx
43
50
  @name = @line[ 0..@idx - 1 ]
44
51
  end
45
52
 
46
53
  #
47
- # Detect the object type.
54
+ # Detect the object type, and capture the exact source text that
55
+ # follows it -- everything from the closing ']' (or, for an
56
+ # untyped declaration with no brackets at all, from right after
57
+ # the name) to the end of the line. Kept byte-exact so a save can
58
+ # reproduce a declaration's original spacing when its value
59
+ # hasn't changed.
48
60
  #
49
61
  def detect_type
50
62
  @line = @line[ @idx + 1..-1 ]
@@ -52,12 +64,15 @@ module Gloo
52
64
 
53
65
  if @line[ 0 ] == ':'
54
66
  @type = 'untyped'
67
+ @raw_tail = @line
55
68
  return
56
69
  end
57
70
 
58
71
  @type = @line[ 0..( @idx ? @idx - 1 : -1 ) ]
59
72
  @type = @type[ 1..-1 ] if @type[ 0 ] == '['
60
73
  @type = @type[ 0..-2 ] if @type[ -1 ] == ']'
74
+ close = @line.index( ']' )
75
+ @raw_tail = close ? @line[ close + 1..-1 ] : ''
61
76
  end
62
77
 
63
78
  #
@@ -12,6 +12,10 @@ module Gloo
12
12
 
13
13
  attr_reader :maps, :mech
14
14
 
15
+ OBJ_NOT_FOUND_ERR = 'Could not resolve object to save: '.freeze
16
+ PATH_EXISTS_ERR = 'Will not overwrite a file not already saved there: '.freeze
17
+ RELOAD_DIRTY_WARNING = 'Reloading will discard unsaved changes in: '.freeze
18
+
15
19
  #
16
20
  # Constructor for the persistence manager.
17
21
  #
@@ -22,31 +26,52 @@ module Gloo
22
26
  end
23
27
 
24
28
  #
25
- # Save one object to the file.
29
+ # Save one object to the file, or every open file if no name is
30
+ # given.
26
31
  #
27
32
  def save( name = '' )
28
33
  name.blank? ? save_all : save_one( name )
29
34
  end
30
35
 
31
36
  #
32
- # Save one object to the file.
37
+ # Save every open file.
33
38
  #
34
39
  def save_all
35
- @maps.each( &:save )
40
+ save_batch( @maps )
36
41
  end
37
42
 
38
43
  #
39
- # Save one object to the file.
44
+ # Save the given object: every file that owns a declaration in
45
+ # its root's tree (usually one -- more when the root is a
46
+ # multi-file namespace). If it isn't mapped to any file yet, it's
47
+ # saved fresh to a default path derived from its root's name.
40
48
  #
41
49
  def save_one( name )
42
- ref = Gloo::Core::Pn.new( @engine, name )
43
- obj = ref.resolve
50
+ obj = resolve_for_save( name )
51
+ return unless obj
52
+
53
+ fs_list = find_file_storages( obj )
54
+ return save_new( obj, @mech.resolve_save_path( root_of( obj ).name ) ) if fs_list.empty?
44
55
 
45
- # Send an on_save event
46
56
  @engine.event_manager.on_save obj
57
+ save_batch( fs_list )
58
+ end
47
59
 
48
- fs = find_file_storage( obj )
49
- fs.save
60
+ #
61
+ # Save the given object to an explicit path. Registers the
62
+ # mapping so a future bare save includes it. Refuses to overwrite
63
+ # a file that exists but isn't already mapped to this object.
64
+ #
65
+ def save_to( name, path )
66
+ obj = resolve_for_save( name )
67
+ return unless obj
68
+
69
+ pn = @mech.resolve_save_path( path )
70
+ mapped = @maps.find { |fs| fs.pn == pn }
71
+ return save_mapped( obj, mapped, pn ) if mapped
72
+ return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) if @mech.exist?( pn )
73
+
74
+ save_new( obj, pn )
50
75
  end
51
76
 
52
77
  #
@@ -61,6 +86,8 @@ module Gloo
61
86
  begin
62
87
  fs = Gloo::Persist::FileStorage.new( @engine, pn )
63
88
  fs.load
89
+ next unless fs.obj # a failed load -- don't map it
90
+
64
91
  @maps << fs
65
92
  @engine.event_manager.on_load fs.obj
66
93
  rescue => ex
@@ -74,8 +101,8 @@ module Gloo
74
101
  # The engine is reset to a clean state.
75
102
  #
76
103
  def unload_all
77
- objs = self.maps.map { |fs| fs.obj }
78
- objs.each { |o| o.msg_unload }
104
+ objs = self.maps.map( &:obj ).compact
105
+ objs.each( &:msg_unload )
79
106
  @engine.reset_state
80
107
  end
81
108
 
@@ -88,22 +115,21 @@ module Gloo
88
115
  def unload( obj )
89
116
  @engine.event_manager.on_unload obj
90
117
  @engine.heap.unload obj
91
- @maps.each_with_index do |o, i|
92
- if o.obj.pn === obj.pn
93
- @maps.delete_at( i )
94
- return
95
- end
96
- end
118
+ # Drop every mapping for this object -- more than one when the
119
+ # root came from several files (the namespace pattern) -- and
120
+ # sweep out any mapping whose object is already gone.
121
+ @maps.reject! { |o| o.obj.nil? || ( o.obj.pn === obj.pn ) }
97
122
  end
98
123
 
99
- #
124
+ #
100
125
  # Reload all objects.
101
126
  # First send a message to each object to let it know it is being reloaded.
102
127
  # Then let the engine restart to reload the files.
103
- #
128
+ #
104
129
  def reload_all
105
130
  return unless @maps
106
-
131
+
132
+ @maps.each { |fs| warn_if_dirty( fs ) }
107
133
  @maps.each do |fs|
108
134
  @engine.event_manager.on_reload fs.obj
109
135
  end
@@ -112,23 +138,24 @@ module Gloo
112
138
  @engine.restart
113
139
  end
114
140
 
115
- #
141
+ #
116
142
  # Re-load the given object from file.
117
143
  # This is used to reload a single object and comes from a
118
144
  # message sent to the object.
119
- #
145
+ #
120
146
  def reload( obj )
121
147
  fs = find_file_storage( obj )
122
148
  return unless fs
123
149
 
150
+ warn_if_dirty( fs )
124
151
  @engine.event_manager.on_reload obj
125
152
  @engine.heap.unload obj
126
153
  fs.load
127
154
  end
128
155
 
129
- #
156
+ #
130
157
  # Find the objects FileStorage in the list.
131
- #
158
+ #
132
159
  def find_file_storage( obj )
133
160
  @maps.each do |o|
134
161
  return o if ( o.obj.pn === obj.pn )
@@ -138,6 +165,16 @@ module Gloo
138
165
  return nil
139
166
  end
140
167
 
168
+ #
169
+ # Find every FileStorage that owns a declaration in the given
170
+ # object's root -- usually one, more when the root is shared
171
+ # across files (the multi-file namespace pattern).
172
+ #
173
+ def find_file_storages( obj )
174
+ root = root_of( obj )
175
+ return @maps.select { |fs| fs.roots.include?( root ) }
176
+ end
177
+
141
178
 
142
179
  #
143
180
  # Get the full path and name of the file.
@@ -176,6 +213,104 @@ module Gloo
176
213
  end
177
214
  end
178
215
 
216
+ private
217
+
218
+ #
219
+ # Resolve a name to an object for saving, reporting an error and
220
+ # returning nil if it can't be found.
221
+ #
222
+ def resolve_for_save( name )
223
+ obj = Gloo::Core::Pn.new( @engine, name ).resolve
224
+ @engine.err( "#{OBJ_NOT_FOUND_ERR}#{name}" ) unless obj
225
+ return obj
226
+ end
227
+
228
+ #
229
+ # Walk up to the top-level declared object that contains obj --
230
+ # its parent is the heap root, not another declared object.
231
+ #
232
+ def root_of( obj )
233
+ o = obj
234
+ o = o.parent until o.parent.nil? || o.parent.root?
235
+ return o
236
+ end
237
+
238
+ #
239
+ # Save a set of files as one batch so a multi-file namespace
240
+ # round-trips: each file only rewrites the declarations its own
241
+ # SourceDoc holds and leaves other files' declarations alone; a
242
+ # brand-new object no file owns is written once, by the first file
243
+ # in the batch to reach it.
244
+ #
245
+ def save_batch( files )
246
+ claimed = {}.compare_by_identity
247
+ files.each do |fs|
248
+ fs.save( :others => owned_elsewhere( fs ), :claimed => claimed )
249
+ end
250
+ end
251
+
252
+ #
253
+ # Every heap object declared by some loaded file's SourceDoc
254
+ # other than fs.
255
+ #
256
+ def owned_elsewhere( fs )
257
+ owned = {}.compare_by_identity
258
+ @maps.each do |other|
259
+ next if other.equal?( fs )
260
+
261
+ collect_owned( other.source_doc&.children, owned )
262
+ end
263
+ return owned
264
+ end
265
+
266
+ #
267
+ # Recursively collect the objects declared by the given source
268
+ # nodes into owned.
269
+ #
270
+ def collect_owned( nodes, owned )
271
+ return unless nodes
272
+
273
+ nodes.each do |node|
274
+ next unless node.is_a?( Gloo::Persist::Source::ObjNode )
275
+
276
+ owned[ node.obj ] = true if node.obj
277
+ collect_owned( node.children, owned )
278
+ end
279
+ end
280
+
281
+ #
282
+ # Save to a path that's already mapped to some file. If that
283
+ # mapping is for a different root, it's a real collision -- the
284
+ # path belongs to something else.
285
+ #
286
+ def save_mapped( obj, mapped, pn )
287
+ return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) unless mapped.roots.include?( root_of( obj ) )
288
+
289
+ @engine.event_manager.on_save obj
290
+ save_batch( [ mapped ] )
291
+ end
292
+
293
+ #
294
+ # Save an object's root fresh to a new path, and register the
295
+ # resulting mapping.
296
+ #
297
+ def save_new( obj, pn )
298
+ @engine.event_manager.on_save obj
299
+ fs = Gloo::Persist::FileStorage.new( @engine, pn, root_of( obj ) )
300
+ fs.save
301
+ @maps << fs
302
+ end
303
+
304
+ #
305
+ # Warn (without blocking) if reloading fs would discard changes
306
+ # that haven't been saved -- any object whose value no longer
307
+ # round-trips to what's on disk.
308
+ #
309
+ def warn_if_dirty( fs )
310
+ saver = Gloo::Persist::FileSaver.new( @engine, fs.pn, fs.obj, fs.source_doc )
311
+ @engine.log.warn "#{RELOAD_DIRTY_WARNING}#{fs.pn}" if saver.dirty?
312
+ end
313
+
179
314
  end
180
315
  end
181
316
  end
@@ -0,0 +1,102 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Collects an indented, unparsed script body (no BEGIN/END) for the
5
+ # object that was just declared. We don't yet know whether a body
6
+ # actually follows -- that's decided by the next line's indentation,
7
+ # so this may close immediately with nothing collected (an empty
8
+ # script).
9
+ #
10
+
11
+ module Gloo
12
+ module Persist
13
+ class ScriptBodyCollector
14
+
15
+ attr_reader :active
16
+
17
+ #
18
+ # Set up an inactive collector.
19
+ #
20
+ def initialize
21
+ @active = false
22
+ end
23
+
24
+ #
25
+ # Start tentatively collecting a body for the object just
26
+ # declared. node is its Source::ObjNode, heap_obj the object
27
+ # itself, base_tabs the indentation of the declaration line
28
+ # (body lines must be indented deeper than this).
29
+ #
30
+ def start( node, heap_obj, base_tabs )
31
+ @active = true
32
+ @node = node
33
+ @obj = heap_obj
34
+ @base_tabs = base_tabs
35
+ @lines = []
36
+ end
37
+
38
+ #
39
+ # One line while a body might be open. line_tabs is its
40
+ # indentation (ignored for a blank line, which is always kept).
41
+ # Returns true if the line was consumed as body content, or
42
+ # false if it closed the body and still needs to be dispatched
43
+ # normally by the caller.
44
+ #
45
+ def handle_line( line, line_tabs )
46
+ if line.strip.empty?
47
+ @lines << chomped( line )
48
+ return true
49
+ end
50
+
51
+ if line_tabs <= @base_tabs
52
+ close
53
+ return false
54
+ end
55
+
56
+ add_line( line )
57
+ return true
58
+ end
59
+
60
+ #
61
+ # End of file with a body still open (eg. the last object in the
62
+ # file is an empty or in-progress script) -- close it so its
63
+ # content isn't silently dropped.
64
+ #
65
+ def finish
66
+ close if @active
67
+ end
68
+
69
+ private
70
+
71
+ #
72
+ # Record one real body line: kept verbatim for the source model;
73
+ # added as a runnable command only if it isn't a comment (a
74
+ # comment inside a script body is documentation, not something
75
+ # to execute).
76
+ #
77
+ def add_line( line )
78
+ @lines << chomped( line )
79
+ @obj.add_line( line ) unless line.strip.start_with?( '#' )
80
+ end
81
+
82
+ #
83
+ # Close out the body, recording its raw content on the node.
84
+ #
85
+ def close
86
+ @active = false
87
+ @node.raw_value = @lines.join( "\n" )
88
+ @node.block_style = :body unless @lines.empty?
89
+ @node = nil
90
+ @obj = nil
91
+ end
92
+
93
+ #
94
+ # A line's raw text with its trailing newline removed.
95
+ #
96
+ def chomped( line )
97
+ return line.chomp( "\n" )
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,51 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Nested container shorthand: a dotted name in a declaration
5
+ # (page.core.users.list [type] :) means "find or create a container
6
+ # for each prefix segment, then declare the real object under the
7
+ # last one". A segment that already names some other kind of object
8
+ # is used as-is, not forced to a container.
9
+ #
10
+
11
+ module Gloo
12
+ module Persist
13
+ class ShorthandExpander
14
+
15
+ #
16
+ # Set up an expander for the given engine.
17
+ #
18
+ def initialize( engine )
19
+ @engine = engine
20
+ end
21
+
22
+ #
23
+ # Split a possibly-dotted name against the given parent. Returns
24
+ # [leaf_name, parent_for_the_leaf, roots_touched, created]:
25
+ # - roots_touched: containers created OR reused directly under
26
+ # the heap root, for the loader to register as this file's roots
27
+ # - created: containers this call actually created (any level),
28
+ # for the loader to remember as its own
29
+ #
30
+ def expand( name, parent )
31
+ return [ name, parent, [], [] ] unless name.include?( '.' )
32
+
33
+ segments = name.split( '.' )
34
+ leaf = segments.pop
35
+ roots = []
36
+ created = []
37
+ segments.each do |seg|
38
+ child = parent.find_child( seg )
39
+ unless child
40
+ child = @engine.factory.create_can( seg, parent )
41
+ created << child
42
+ end
43
+ roots << child if parent == @engine.heap.root
44
+ parent = child
45
+ end
46
+ return [ leaf, parent, roots, created ]
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,27 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # A blank line between declarations. Kept (not discarded) so a save
5
+ # can reproduce the file's original spacing.
6
+ #
7
+
8
+ module Gloo
9
+ module Persist
10
+ module Source
11
+ class BlankNode
12
+
13
+ attr_reader :raw
14
+
15
+ #
16
+ # Set up a blank line node. raw is the full source line, minus
17
+ # its trailing newline (may be non-empty if the blank line had
18
+ # trailing whitespace -- preserved rather than normalized).
19
+ #
20
+ def initialize( raw = '' )
21
+ @raw = raw
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # A whole-line comment that is not attached to any object declaration
5
+ # as its leading_doc -- either because a blank line separated it from
6
+ # the declaration that follows, or nothing follows it at all.
7
+ #
8
+
9
+ module Gloo
10
+ module Persist
11
+ module Source
12
+ class CommentNode
13
+
14
+ attr_reader :raw
15
+
16
+ #
17
+ # Set up a comment node. raw is the full source line, minus
18
+ # its trailing newline.
19
+ #
20
+ def initialize( raw )
21
+ @raw = raw
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # A top-of-file 'load lib {name}' (or 'load ext {name}') directive,
5
+ # run by the loader itself before the declarations that follow it.
6
+ #
7
+
8
+ module Gloo
9
+ module Persist
10
+ module Source
11
+ class DirectiveNode
12
+
13
+ attr_reader :raw
14
+
15
+ #
16
+ # Set up a directive node. raw is the full source line, minus
17
+ # its trailing newline.
18
+ #
19
+ def initialize( raw )
20
+ @raw = raw
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end