gloo 7.1.2 → 7.2

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +23 -0
  3. data/docs/application.md +47 -2
  4. data/docs/development.md +10 -0
  5. data/docs/iterators.md +22 -5
  6. data/docs/language_objects.md +20 -15
  7. data/docs/language_scripting.md +6 -2
  8. data/docs/plugins.md +1 -1
  9. data/docs/testing.md +4 -7
  10. data/docs/verbs.md +23 -3
  11. data/lib/VERSION +1 -1
  12. data/lib/VERSION_NOTES +13 -0
  13. data/lib/dependencies.rb +4 -1
  14. data/lib/gloo/app/engine.rb +18 -6
  15. data/lib/gloo/core/obj.rb +0 -23
  16. data/lib/gloo/exec/script.rb +12 -2
  17. data/lib/gloo/objs/basic/script.rb +5 -28
  18. data/lib/gloo/objs/ctrl/each.rb +4 -3
  19. data/lib/gloo/objs/ctrl/repeat.rb +4 -3
  20. data/lib/gloo/persist/declaration_ledger.rb +9 -9
  21. data/lib/gloo/persist/declaration_writer.rb +208 -0
  22. data/lib/gloo/persist/disc_mech.rb +70 -39
  23. data/lib/gloo/persist/file_loader.rb +125 -66
  24. data/lib/gloo/persist/file_saver.rb +130 -198
  25. data/lib/gloo/persist/file_storage.rb +15 -4
  26. data/lib/gloo/persist/file_style.rb +89 -0
  27. data/lib/gloo/persist/line_splitter.rb +16 -1
  28. data/lib/gloo/persist/persist_man.rb +112 -30
  29. data/lib/gloo/persist/save_index.rb +211 -0
  30. data/lib/gloo/persist/script_body_collector.rb +16 -21
  31. data/lib/gloo/persist/source/blank_node.rb +10 -3
  32. data/lib/gloo/persist/source/comment_node.rb +10 -2
  33. data/lib/gloo/persist/source/directive_node.rb +10 -2
  34. data/lib/gloo/persist/source/obj_node.rb +174 -9
  35. data/lib/gloo/persist/source/source_doc.rb +54 -0
  36. data/lib/gloo/persist/subtree_extractor.rb +9 -37
  37. data/lib/gloo/persist/trivia_buffer.rb +7 -7
  38. data/lib/gloo/verbs/break.rb +99 -10
  39. data/lib/gloo/verbs/load.rb +27 -21
  40. data/lib/gloo/verbs/move.rb +42 -3
  41. data/lib/gloo/verbs/save.rb +26 -4
  42. data/test.gloo/app/exit_status.test.gloo +34 -0
  43. data/test.gloo/app/fixtures/exit_after_error.gloo +9 -0
  44. data/test.gloo/lang/declaration.test.gloo +36 -0
  45. data/test.gloo/lang/fixtures/mixed_indent.gloo +5 -0
  46. data/test.gloo/lang/naming.test.gloo +44 -6
  47. data/test.gloo/verbs/break.test.gloo +104 -11
  48. data/test.gloo/verbs/fixtures/load_beside.gloo +6 -0
  49. data/test.gloo/verbs/fixtures/lossless_text.gloo +7 -0
  50. data/test.gloo/verbs/fixtures/lossless_text_saved.gloo +9 -0
  51. data/test.gloo/verbs/load.test.gloo +18 -0
  52. data/test.gloo/verbs/move.test.gloo +39 -0
  53. data/test.gloo/verbs/save.test.gloo +239 -0
  54. metadata +10 -1
@@ -7,6 +7,11 @@
7
7
  # raw formatting) -- so a later save can rewrite the file instead of
8
8
  # regenerating it from scratch.
9
9
  #
10
+ # Every byte of the file goes into exactly one source node, in file
11
+ # order, so the document's text is the file. When the load finishes,
12
+ # each declaration remembers its object's place and value, so a save
13
+ # can tell what changed.
14
+ #
10
15
  # Work is delegated to TriviaBuffer (comment/blank-line buffering),
11
16
  # ScriptBodyCollector (script bodies), IndentStack (nesting shared by
12
17
  # the heap and source trees), ShorthandExpander (nested-container
@@ -29,6 +34,10 @@ module Gloo
29
34
  'the line above; nesting it under that line.'.freeze
30
35
  MISALIGNED_WARNING = "Indentation doesn't line up with any line " \
31
36
  'above; nesting it under the closest one.'.freeze
37
+ INVALID_UTF8_WARNING = 'Line is not valid UTF-8; using U+FFFD for ' \
38
+ 'the bytes that are not.'.freeze
39
+ NO_END_WARNING = "BEGIN block has no END; it runs to the end of the file.".freeze
40
+ CONTINUATION = "\\\n".freeze
32
41
 
33
42
  # A 'load lib {name}' (or 'load ext {name}') statement at the top
34
43
  # of a file, before the first object declaration. It makes a core
@@ -55,9 +64,9 @@ module Gloo
55
64
  @shorthand = Gloo::Persist::ShorthandExpander.new( engine )
56
65
  @ledger = Gloo::Persist::DeclarationLedger.new( engine )
57
66
  @in_block = false
58
- @block_value = ''
67
+ @block_value = +''
68
+ @save_raw = nil
59
69
  @body_started = false
60
- @debug = false
61
70
  end
62
71
 
63
72
  #
@@ -87,14 +96,14 @@ module Gloo
87
96
 
88
97
  @engine.log.debug "Loading file '#{@pn}'"
89
98
  @indent_stack = IndentStack.new( @engine.heap.root, @source_doc )
90
- f = @mech.read( @pn )
99
+ f = @data = @mech.read( @pn )
91
100
 
92
101
  exec_env = @engine.exec_env
93
102
  outer_location = exec_env.load_location
94
103
  begin
95
- each_joined_line( f ) do |line, number|
104
+ each_joined_line( f ) do |line, raw, number|
96
105
  exec_env.load_location = "#{@pn}:#{number}"
97
- dispatch_line( line )
106
+ dispatch_line( valid_utf8( line ), raw )
98
107
  end
99
108
  finish
100
109
  ensure
@@ -114,27 +123,44 @@ module Gloo
114
123
  end
115
124
 
116
125
  #
117
- # Yield each line of the file with its line number (counting from
118
- # 1). A line ending in a backslash continues on the next line:
119
- # the two are joined, dropping the backslash and the line break,
120
- # and the joined line has the number of the line it starts on.
126
+ # Yield each line of the file, its exact source text, and its line
127
+ # number (counting from 1). A line ending in a backslash continues
128
+ # on the next line: the two are joined, dropping the backslash and
129
+ # the line break, and the joined line has the number of the line
130
+ # it starts on. Not inside a BEGIN/END block, whose lines are the
131
+ # value, exactly as written.
121
132
  #
122
133
  def each_joined_line( data )
123
134
  pending = nil
135
+ raw = nil
124
136
  start = 0
125
137
  data.each_line.with_index( 1 ) do |line, number|
126
138
  if pending
127
139
  pending << line
140
+ raw << line
128
141
  else
129
142
  pending = line.dup
143
+ raw = line.dup
130
144
  start = number
131
145
  end
132
- next if pending.delete_suffix!( "\\\n" )
146
+ next if !@in_block && pending.delete_suffix!( CONTINUATION )
133
147
 
134
- yield pending, start
148
+ yield pending, raw, start
135
149
  pending = nil
136
150
  end
137
- yield pending, start if pending
151
+ yield pending, raw, start if pending
152
+ end
153
+
154
+ #
155
+ # A line to work with: as read if it's valid UTF-8, otherwise with
156
+ # U+FFFD in place of the bytes that aren't, and a warning. The
157
+ # source text keeps the original bytes.
158
+ #
159
+ def valid_utf8( line )
160
+ return line if line.valid_encoding?
161
+
162
+ @engine.warn INVALID_UTF8_WARNING
163
+ return line.scrub( "\uFFFD" )
138
164
  end
139
165
 
140
166
  # ---------------------------------------------------------------------
@@ -145,30 +171,49 @@ module Gloo
145
171
  # Route one line of the file to whichever mode we're currently in:
146
172
  # collecting a script body, collecting a BEGIN/END block, trivia
147
173
  # (comment/blank), a top-of-file directive, or a normal
148
- # declaration.
174
+ # declaration. line is the line as read (continuations joined),
175
+ # raw its exact source text.
149
176
  #
150
- def dispatch_line( line )
151
- return handle_body_line( line ) if @body.active
152
- return handle_block_line( line ) if @in_block
153
- return handle_trivia_line( line ) if skip_line?( line )
177
+ def dispatch_line( line, raw )
178
+ return handle_body_line( line, raw ) if @body.active
179
+ return handle_block_line( line, raw ) if @in_block
180
+ return handle_trivia_line( line, raw ) if skip_line?( line )
154
181
 
155
182
  if !@body_started && line =~ LIB_DIRECTIVE
156
- @indent_stack.node.children << Source::DirectiveNode.new( line.strip )
183
+ # Comments before the directive stay before it.
184
+ @trivia.flush_into( @indent_stack.node.children )
185
+ @indent_stack.node.children << Source::DirectiveNode.new( raw )
157
186
  run_lib_directive line
158
187
  return
159
188
  end
160
189
  @body_started = true
161
190
 
162
- handle_declaration_line( line )
191
+ handle_declaration_line( line, raw )
163
192
  end
164
193
 
165
194
  #
166
195
  # End of file: close out anything still open so its content
167
- # isn't silently dropped.
196
+ # isn't silently dropped, then have every declaration remember
197
+ # its object's place and value as loaded.
168
198
  #
169
199
  def finish
200
+ finish_open_block if @in_block
170
201
  @body.finish
171
- @trivia.flush_into( @indent_stack.node.children )
202
+ # Comments and blank lines at the end of the file belong to the
203
+ # file, not to whatever declaration happened to come last.
204
+ @trivia.flush_into( @source_doc.children )
205
+ @source_doc.missing_final_newline! unless @data.empty? || @data.end_with?( "\n" )
206
+ @source_doc.obj_nodes.each( &:remember )
207
+ end
208
+
209
+ #
210
+ # A BEGIN block with no END: the best guess is that it runs to the
211
+ # end of the file, so the object still loads with that value.
212
+ #
213
+ def finish_open_block
214
+ @engine.warn NO_END_WARNING
215
+ @in_block = false
216
+ finalize_declaration( @save_line, @save_raw + @block_raw )
172
217
  end
173
218
 
174
219
  # ---------------------------------------------------------------------
@@ -187,11 +232,11 @@ module Gloo
187
232
  # resolving once the right moment comes (the next declaration, or
188
233
  # end of file).
189
234
  #
190
- def handle_trivia_line( line )
235
+ def handle_trivia_line( line, raw )
191
236
  if line.strip.empty?
192
- @trivia.push_blank( chomped( line ) )
237
+ @trivia.push_blank( raw )
193
238
  else
194
- @trivia.push_comment( chomped( line ), tab_count( line ) )
239
+ @trivia.push_comment( raw, tab_count( line ) )
195
240
  end
196
241
  end
197
242
 
@@ -203,11 +248,11 @@ module Gloo
203
248
  # Inside a BEGIN/END block every line is literal content -- kept
204
249
  # exactly as-is, comments and blank lines included.
205
250
  #
206
- def handle_block_line( line )
251
+ def handle_block_line( line, raw )
252
+ @block_raw << raw
207
253
  if line.strip == END_BLOCK
208
254
  @in_block = false
209
- @block_end_indent = leading_ws( line )
210
- finalize_declaration( @save_line )
255
+ finalize_declaration( @save_line, @save_raw + @block_raw )
211
256
  else
212
257
  @block_value << line
213
258
  end
@@ -221,9 +266,9 @@ module Gloo
221
266
  # One line while a script body might be open. Re-dispatches the
222
267
  # line normally if it turns out to have closed the body.
223
268
  #
224
- def handle_body_line( line )
225
- consumed = @body.handle_line( line, tab_count( line ) )
226
- dispatch_line( line ) unless consumed
269
+ def handle_body_line( line, raw )
270
+ consumed = @body.handle_line( line, raw, tab_count( line ) )
271
+ dispatch_line( line, raw ) unless consumed
227
272
  end
228
273
 
229
274
  # ---------------------------------------------------------------------
@@ -233,25 +278,28 @@ module Gloo
233
278
  #
234
279
  # A line that starts (or is) an object declaration.
235
280
  #
236
- def handle_declaration_line( line )
281
+ def handle_declaration_line( line, raw )
237
282
  if line.strip.end_with?( BEGIN_BLOCK )
238
283
  @in_block = true
239
284
  @save_line = line
285
+ @save_raw = raw
286
+ @block_raw = +''
240
287
  return
241
288
  end
242
289
 
243
- finalize_declaration( line )
290
+ finalize_declaration( line, raw )
244
291
  end
245
292
 
246
293
  #
247
294
  # Place the declaration at its indentation level and create the
248
- # object (and its Source::ObjNode).
295
+ # object (and its Source::ObjNode). raw is the declaration's exact
296
+ # text (with its BEGIN/END block, if it has one).
249
297
  #
250
- def finalize_declaration( line )
298
+ def finalize_declaration( line, raw )
251
299
  line_tabs = tab_count( line )
252
300
  placed = @indent_stack.place( line_tabs, @last, @last_node )
253
301
  warn_indent( placed ) if placed
254
- create_declared_obj( line, line_tabs )
302
+ create_declared_obj( line, raw, line_tabs )
255
303
  end
256
304
 
257
305
  #
@@ -284,8 +332,9 @@ module Gloo
284
332
  # Create the heap object and its source node for one declaration
285
333
  # line, and start body collection if its type calls for one.
286
334
  #
287
- def create_declared_obj( line, line_tabs )
335
+ def create_declared_obj( line, raw, line_tabs )
288
336
  name, type, value, style = split_declaration( line )
337
+ style[ :repair ] ||= name.nil?
289
338
  name ||= placeholder_name( @indent_stack.parent )
290
339
  leaf, parent, roots, created = @shorthand.expand( name, @indent_stack.parent )
291
340
  roots.each { |r| @ledger.root( r ) }
@@ -298,7 +347,9 @@ module Gloo
298
347
  @ledger.created( @last )
299
348
  @ledger.root( @last ) if parent == @engine.heap.root
300
349
 
301
- node = build_obj_node( leading_ws( line ), name, type, value, style )
350
+ node = build_obj_node( line, raw, name, type, value, style )
351
+ # A declaration gloo had to guess at is written as understood.
352
+ node.repaired_head = "#{node.raw_indent}#{name} [#{type}]" if style[ :repair ]
302
353
  node.leading_doc = @trivia.take_leading_doc( line_tabs, @indent_stack.node.children )
303
354
  # First non-empty doc wins, same as "first value wins" for a
304
355
  # name re-declared across files -- @last is the same object
@@ -329,29 +380,43 @@ module Gloo
329
380
  splitter = LineSplitter.new( line, @indent_stack&.tabs || 0 )
330
381
  name, type, value = splitter.split
331
382
  @engine.syntax_err "#{MISSING_BRACKET_ERR}'#{type}'." if splitter.missing_bracket
332
- style = { :raw_tail => splitter.raw_tail, :block_style => :inline }
333
- return name, type, value, style if @block_value == ''
383
+ style = { :raw_tail => splitter.raw_tail, :block_style => :inline,
384
+ :repair => splitter.missing_bracket && closable?( splitter.raw_tail ) }
385
+ return name, type, value, style unless @save_raw
334
386
 
335
387
  value = @block_value
336
- @block_value = ''
388
+ @block_value = +''
389
+ @save_raw = nil
337
390
  style[ :block_style ] = :begin_end
338
- style[ :raw_end_indent ] = @block_end_indent
339
391
  return name, type, value, style
340
392
  end
341
393
 
342
394
  #
343
- # Build the source node for one object declaration. raw_value is
344
- # kept byte-exact with whatever was handed to the heap object's
345
- # set_value (trailing newline included, for a BEGIN/END block) so
346
- # a save can compare the two directly.
395
+ # Can a missing ']' be closed on save? Only when the value comes
396
+ # next ('b [int : 3'), not when a ']' does ('a [string ] :', where
397
+ # closing it would add a second).
347
398
  #
348
- def build_obj_node( raw_indent, name, type, value, style )
399
+ def closable?( tail )
400
+ rest = tail.lstrip
401
+ return rest.empty? || rest.start_with?( ':' )
402
+ end
403
+
404
+ #
405
+ # Build the source node for one object declaration: its exact text
406
+ # (raw), and its head -- the declaration line up to its value
407
+ # (indent, name and [type], as written) -- for a save that only
408
+ # changes the value. raw_value is what was handed to the heap
409
+ # object's set_value.
410
+ #
411
+ def build_obj_node( line, raw, name, type, value, style )
349
412
  node = Source::ObjNode.new( :name => name, :raw_type => type )
350
- node.raw_indent = raw_indent
413
+ first = line.lines.first.to_s.chomp
414
+ node.raw_indent = leading_ws( first )
351
415
  node.raw_tail = style[ :raw_tail ]
416
+ node.head = first[ 0, first.length - node.raw_tail.length ]
352
417
  node.block_style = style[ :block_style ]
353
418
  node.raw_value = value
354
- node.raw_end_indent = style[ :raw_end_indent ] || ''
419
+ node.raw = raw.dup
355
420
  node.obj = @last
356
421
  return node
357
422
  end
@@ -369,20 +434,21 @@ module Gloo
369
434
  end
370
435
 
371
436
  #
372
- # Get the number of leading tabs.
437
+ # Get the indentation level of a line: a tab or SPACE_CNT spaces is
438
+ # one level, and they can be mixed, on one line or across lines.
373
439
  #
374
440
  def tab_count( line )
375
- i = 0
376
-
377
- if line[ i ] == ' '
378
- i += 1 while line[ i ] == ' '
379
- tab_equiv = ( i / SPACE_CNT ).to_i
380
- puts "Found #{i} spaces => #{tab_equiv}" if @debug
381
- return tab_equiv
441
+ width = 0
442
+ line.each_char do |c|
443
+ if c == ' '
444
+ width += 1
445
+ elsif c == "\t"
446
+ width += SPACE_CNT
447
+ else
448
+ break
449
+ end
382
450
  end
383
-
384
- i += 1 while line[ i ] == "\t"
385
- return i
451
+ return width / SPACE_CNT
386
452
  end
387
453
 
388
454
  #
@@ -400,13 +466,6 @@ module Gloo
400
466
  return line[ /\A[ \t]*/ ]
401
467
  end
402
468
 
403
- #
404
- # A line's raw text with its trailing newline removed.
405
- #
406
- def chomped( line )
407
- return line.chomp( "\n" )
408
- end
409
-
410
469
  end
411
470
  end
412
471
  end