gloo 7.1.3 → 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.
- checksums.yaml +4 -4
- data/Rakefile +23 -0
- data/docs/application.md +47 -2
- data/docs/development.md +10 -0
- data/docs/language_objects.md +20 -15
- data/docs/language_scripting.md +6 -2
- data/docs/testing.md +4 -7
- data/docs/verbs.md +23 -3
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +8 -0
- data/lib/dependencies.rb +4 -1
- data/lib/gloo/app/engine.rb +18 -6
- data/lib/gloo/core/obj.rb +0 -23
- data/lib/gloo/objs/basic/script.rb +0 -23
- data/lib/gloo/persist/declaration_ledger.rb +9 -9
- data/lib/gloo/persist/declaration_writer.rb +208 -0
- data/lib/gloo/persist/disc_mech.rb +70 -39
- data/lib/gloo/persist/file_loader.rb +125 -66
- data/lib/gloo/persist/file_saver.rb +130 -198
- data/lib/gloo/persist/file_storage.rb +15 -4
- data/lib/gloo/persist/file_style.rb +89 -0
- data/lib/gloo/persist/line_splitter.rb +16 -1
- data/lib/gloo/persist/persist_man.rb +112 -30
- data/lib/gloo/persist/save_index.rb +211 -0
- data/lib/gloo/persist/script_body_collector.rb +16 -21
- data/lib/gloo/persist/source/blank_node.rb +10 -3
- data/lib/gloo/persist/source/comment_node.rb +10 -2
- data/lib/gloo/persist/source/directive_node.rb +10 -2
- data/lib/gloo/persist/source/obj_node.rb +174 -9
- data/lib/gloo/persist/source/source_doc.rb +54 -0
- data/lib/gloo/persist/subtree_extractor.rb +9 -37
- data/lib/gloo/persist/trivia_buffer.rb +7 -7
- data/lib/gloo/verbs/load.rb +27 -21
- data/lib/gloo/verbs/move.rb +42 -3
- data/lib/gloo/verbs/save.rb +26 -4
- data/test.gloo/app/exit_status.test.gloo +34 -0
- data/test.gloo/app/fixtures/exit_after_error.gloo +9 -0
- data/test.gloo/lang/declaration.test.gloo +36 -0
- data/test.gloo/lang/fixtures/mixed_indent.gloo +5 -0
- data/test.gloo/lang/naming.test.gloo +44 -6
- data/test.gloo/verbs/fixtures/load_beside.gloo +6 -0
- data/test.gloo/verbs/fixtures/lossless_text.gloo +7 -0
- data/test.gloo/verbs/fixtures/lossless_text_saved.gloo +9 -0
- data/test.gloo/verbs/load.test.gloo +18 -0
- data/test.gloo/verbs/move.test.gloo +39 -0
- data/test.gloo/verbs/save.test.gloo +239 -0
- 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
|
|
118
|
-
# 1). A line ending in a backslash continues
|
|
119
|
-
# the two are joined, dropping the backslash and
|
|
120
|
-
# and the joined line has the number of the line
|
|
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!(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
237
|
+
@trivia.push_blank( raw )
|
|
193
238
|
else
|
|
194
|
-
@trivia.push_comment(
|
|
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
|
-
@
|
|
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(
|
|
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
|
-
|
|
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
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|