ld-patch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,621 @@
1
+ require 'ebnf'
2
+ require 'ebnf/ll1/parser'
3
+ require 'ld/patch/meta'
4
+
5
+ module LD::Patch
6
+ ##
7
+ # A parser for the LD Patch grammar.
8
+ #
9
+ # @see http://www.w3.org/TR/ldpatch/#concrete-syntax
10
+ # @see http://en.wikipedia.org/wiki/LR_parser
11
+ class Parser
12
+ include LD::Patch::Meta
13
+ include LD::Patch::Terminals
14
+ include EBNF::LL1::Parser
15
+
16
+ ##
17
+ # Any additional options for the parser.
18
+ #
19
+ # @return [Hash]
20
+ attr_reader :options
21
+
22
+ ##
23
+ # The current input string being processed.
24
+ #
25
+ # @return [String]
26
+ attr_accessor :input
27
+
28
+ ##
29
+ # The current input tokens being processed.
30
+ #
31
+ # @return [Array<Token>]
32
+ attr_reader :tokens
33
+
34
+ ##
35
+ # The internal representation of the result
36
+ # @return [Array]
37
+ attr_accessor :result
38
+
39
+ # Terminals passed to lexer. Order matters!
40
+ terminal(:ANON, ANON) do |prod, token, input|
41
+ input[:resource] = bnode
42
+ end
43
+ terminal(:BLANK_NODE_LABEL, BLANK_NODE_LABEL) do |prod, token, input|
44
+ input[:resource] = bnode(token.value[2..-1])
45
+ end
46
+ terminal(:IRIREF, IRIREF, unescape: true) do |prod, token, input|
47
+ begin
48
+ input[:iri] = iri(token.value[1..-2])
49
+ rescue ArgumentError => e
50
+ raise ParseError, e.message
51
+ end
52
+ end
53
+ terminal(:DOUBLE, DOUBLE) do |prod, token, input|
54
+ # Note that a Turtle Double may begin with a '.[eE]', so tack on a leading
55
+ # zero if necessary
56
+ value = token.value.sub(/\.([eE])/, '.0\1')
57
+ input[:literal] = literal(value, datatype: RDF::XSD.double)
58
+ end
59
+ terminal(:DECIMAL, DECIMAL) do |prod, token, input|
60
+ # Note that a Turtle Decimal may begin with a '.', so tack on a leading
61
+ # zero if necessary
62
+ value = token.value
63
+ value = "0#{token.value}" if token.value[0,1] == "."
64
+ input[:literal] = literal(value, datatype: RDF::XSD.decimal)
65
+ end
66
+ terminal(:INTEGER, INTEGER) do |prod, token, input|
67
+ input[:literal] = literal(token.value, datatype: RDF::XSD.integer)
68
+ end
69
+ terminal(:PNAME_LN, PNAME_LN, unescape: true) do |prod, token, input|
70
+ prefix, suffix = token.value.split(":", 2)
71
+ input[:iri] = ns(prefix, suffix)
72
+ end
73
+ terminal(:PNAME_NS, PNAME_NS) do |prod, token, input|
74
+ prefix = token.value[0..-2]
75
+
76
+ # Two contexts, one when prefix is being defined, the other when being used
77
+ case prod
78
+ when :prefixID
79
+ input[:prefix] = prefix
80
+ else
81
+ input[:iri] = ns(prefix, nil)
82
+ end
83
+ end
84
+ terminal(:STRING_LITERAL_LONG_SINGLE_QUOTE, STRING_LITERAL_LONG_SINGLE_QUOTE, unescape: true) do |prod, token, input|
85
+ input[:string] = token.value[3..-4]
86
+ end
87
+ terminal(:STRING_LITERAL_LONG_QUOTE, STRING_LITERAL_LONG_QUOTE, unescape: true) do |prod, token, input|
88
+ input[:string] = token.value[3..-4]
89
+ end
90
+ terminal(:STRING_LITERAL_QUOTE, STRING_LITERAL_QUOTE, unescape: true) do |prod, token, input|
91
+ input[:string] = token.value[1..-2]
92
+ end
93
+ terminal(:STRING_LITERAL_SINGLE_QUOTE, STRING_LITERAL_SINGLE_QUOTE, unescape: true) do |prod, token, input|
94
+ input[:string] = token.value[1..-2]
95
+ end
96
+ terminal(:VAR1, VAR1) do |prod, token, input|
97
+ input[:resource] = variable(token.value[1..-1])
98
+ end
99
+
100
+ # Keyword terminals
101
+ terminal(nil, STR_EXPR) do |prod, token, input|
102
+ case token.value
103
+ when '^' then input[:reverse] = token.value
104
+ when '/' then input[:slash] = token.value
105
+ when '!' then input[:not] = token.value
106
+ when 'a' then input[:predicate] = (a = RDF.type.dup; a.lexical = 'a'; a)
107
+ when /true|false/ then input[:literal] = RDF::Literal::Boolean.new(token.value)
108
+ when '@prefix' then input[:prefix] = token.value
109
+ when %r{
110
+ AddNew|Add|A|
111
+ Bind|B|
112
+ Cut|C|
113
+ DeleteExisting|Delete|DE|D|
114
+ UpdateList|UL|
115
+ @prefix
116
+ }x
117
+ input[token.value.to_sym] = token.value
118
+ else
119
+ #add_prod_datum(:string, token.value)
120
+ end
121
+ end
122
+
123
+ terminal(:LANGTAG, LANGTAG) do |prod, token, input|
124
+ add_prod_datum(:language, token.value[1..-1])
125
+ end
126
+
127
+ # [1] ldpatch ::= prologue statement*
128
+ production(:ldpatch) do |input, current, callback|
129
+ patch = Algebra::Patch.new(*current[:statements])
130
+ input[:ldpatch] = if prefixes.empty?
131
+ patch
132
+ else
133
+ Algebra::Prefix.new(prefixes.to_a, patch)
134
+ end
135
+ end
136
+
137
+ # [4] bind ::= ("Bind" | "B") VAR1 value path? "."
138
+ production(:bind) do |input, current, callback|
139
+ path = Algebra::Path.new(*Array(current[:path]))
140
+ (input[:statements] ||= []) << Algebra::Bind.new(current[:resource], current[:value], path)
141
+ end
142
+
143
+ # [5] add ::= ("Add" | "A") "{" graph "}" "."
144
+ production(:add) do |input, current, callback|
145
+ (input[:statements] ||= []) << Algebra::Add.new(current[:graph], new: false)
146
+ end
147
+
148
+ # [6] addNew ::= ("AddNew" | "AN") "{" graph "}" "."
149
+ production(:addNew) do |input, current, callback|
150
+ (input[:statements] ||= []) << Algebra::Add.new(current[:graph], new: true)
151
+ end
152
+
153
+ # [7] delete ::= ("Delete" | "D") "{" graph "}" "."
154
+ production(:delete) do |input, current, callback|
155
+ (input[:statements] ||= []) << Algebra::Delete.new(current[:graph], existing: false)
156
+ end
157
+
158
+ # [8] deleteExisting ::= ("DeleteExisting" | "DE") "{" graph "}" "."
159
+ production(:deleteExisting) do |input, current, callback|
160
+ (input[:statements] ||= []) << Algebra::Delete.new(current[:graph], existing: true)
161
+ end
162
+
163
+ # [9] cut ::= ("Cut" | "C") VAR1 "."
164
+ production(:cut) do |input, current, callback|
165
+ (input[:statements] ||= []) << Algebra::Cut.new(current[:resource])
166
+ end
167
+
168
+ # [10] updateList ::= ("UpdateList" | "UL") varOrIRI predicate slice collection "."
169
+ production(:updateList) do |input, current, callback|
170
+ var_or_iri = current[:resource] || current[:iri]
171
+ (input[:statements] ||= []) << Algebra::UpdateList.new(var_or_iri, current[:predicate], current[:slice1], current[:slice2], current[:collection].to_a)
172
+ end
173
+
174
+ # [12] value ::= iri | literal | VAR1
175
+ production(:value) do |input, current, callback|
176
+ input[:value] = current[:iri] || current[:literal] || current[:resource]
177
+ end
178
+
179
+ # [13] path ::= ( '/' step | constraint )*
180
+ # ( '/' step | constraint )
181
+ production(:_path_1) do |input, current, callback|
182
+ step = case
183
+ when current[:literal] then Algebra::Index.new(current[:literal])
184
+ when current[:constraint] then current[:constraint]
185
+ when current[:reverse] then Algebra::Reverse.new(current[:iri])
186
+ else current[:iri]
187
+ end
188
+ (input[:path] ||= []) << step
189
+ end
190
+
191
+ # [15] constraint ::= '[' path ( '=' value )? ']' | '!'
192
+ production(:constraint) do |input, current, callback|
193
+ path = Algebra::Path.new(*Array(current[:path]))
194
+ input[:constraint] = if current[:value]
195
+ Algebra::Constraint.new(path, current[:value])
196
+ elsif current[:path]
197
+ Algebra::Constraint.new(path)
198
+ else
199
+ Algebra::Constraint.new(:unique)
200
+ end
201
+ end
202
+
203
+ # [16] slice ::= INDEX? '..' INDEX?
204
+ production(:_slice_1) do |input, current, callback|
205
+ input[:slice1] = current[:literal]
206
+ end
207
+ production(:_slice_2) do |input, current, callback|
208
+ input[:slice2] = current[:literal]
209
+ end
210
+
211
+ # [4t] prefixID defines a prefix mapping
212
+ production(:prefixID) do |input, current, callback|
213
+ prefix = current[:prefix]
214
+ iri = current[:iri]
215
+ debug("prefixID") {"Defined prefix #{prefix.inspect} mapping to #{iri.inspect}"}
216
+ prefix(prefix, iri)
217
+ end
218
+
219
+ # [18] graph ::= triples ( '.' triples )* '.'?
220
+ production(:graph) do |input, current, callback|
221
+ input[:graph] = current[:triples]
222
+ end
223
+
224
+ # [10t*] subject ::= iri | BlankNode | collection | VAR1
225
+ production(:subject) do |input, current, callback|
226
+ if list = current[:collection]
227
+ # Add collection patterns
228
+ list.each_statement do |statement|
229
+ (input[:triples] ||= []) << RDF::Query::Pattern.from(statement)
230
+ end
231
+
232
+ current[:resource] = current[:collection].subject
233
+ end
234
+
235
+ (input[:triples] ||= []).concat(current[:triples]) if current[:triples]
236
+ input[:subject] = current[:resource] || current[:iri]
237
+ end
238
+
239
+ # [11t] predicate ::= iri
240
+ production(:predicate) do |input, current, callback|
241
+ input[:predicate] = current[:iri]
242
+ end
243
+
244
+ # [12t*] object ::= iri | BlankNode | collection | blankNodePropertyList | literal | VAR1
245
+ production(:object) do |input, current, callback|
246
+ if list = current[:collection]
247
+ # Add collection patterns
248
+ list.each_statement do |statement|
249
+ (input[:triples] ||= []) << RDF::Query::Pattern.from(statement)
250
+ end
251
+
252
+ current[:resource] = current[:collection].subject
253
+ end
254
+
255
+ # Add triples from blankNodePropertyList
256
+ (input[:triples] ||= []).concat(current[:triples]) if current[:triples]
257
+
258
+ if input[:object_list]
259
+ # Part of an rdf:List collection
260
+ input[:object_list] << (current[:resource] || current[:iri] || current[:literal])
261
+ else
262
+ debug("object") {"current: #{current.inspect}"}
263
+ object = current[:resource] || current[:literal] || current[:iri]
264
+ (input[:triples] ||= []) << RDF::Query::Pattern.new(input[:subject], input[:predicate], object)
265
+ end
266
+ end
267
+
268
+ # [14t] blankNodePropertyList ::= "[" predicateObjectList "]"
269
+ start_production(:blankNodePropertyList) do |input, current, callback|
270
+ current[:subject] = self.bnode
271
+ end
272
+
273
+ production(:blankNodePropertyList) do |input, current, callback|
274
+ input[:subject] = input[:resource] = current[:subject]
275
+ (input[:triples] ||= []).concat(current[:triples]) if current[:triples]
276
+ end
277
+
278
+ # [15t] collection ::= "(" object* ")"
279
+ start_production(:collection) do |input, current, callback|
280
+ # Tells the object production to collect and not generate statements
281
+ current[:object_list] = []
282
+ end
283
+
284
+ production(:collection) do |input, current, callback|
285
+ # Create an RDF list
286
+ objects = current[:object_list]
287
+ (input[:triples] ||= []).concat(current[:triples]) if current[:triples]
288
+ input[:collection] = RDF::List[*objects]
289
+ end
290
+
291
+ # [129s] RDFLiteral ::= String ( LANGTAG | ( '^^' iri ) )?
292
+ production(:RDFLiteral) do |input, current, callback|
293
+ if current[:string]
294
+ lit = current.dup
295
+ str = lit.delete(:string)
296
+ lit[:datatype] = lit.delete(:iri) if lit[:iri]
297
+ lit[:language] = lit.delete(:language).last.downcase if lit[:language]
298
+ input[:literal] = RDF::Literal.new(str, lit) if str
299
+ end
300
+ end
301
+
302
+ ##
303
+ # Initializes a new parser instance.
304
+ #
305
+ # @param [String, IO, StringIO, #to_s] input
306
+ # @param [Hash{Symbol => Object}] options
307
+ # @option options [#to_s] :base_uri (nil)
308
+ # the base URI to use when resolving relative URIs
309
+ # @option options [#to_s] :anon_base ("b0")
310
+ # Basis for generating anonymous Nodes
311
+ # @option options [Boolean] :resolve_iris (false)
312
+ # Resolve prefix and relative IRIs, otherwise, when serializing the parsed SSE as S-Expressions, use the original prefixed and relative URIs along with `base` and `prefix` definitions.
313
+ # @option options [Boolean] :validate (false)
314
+ # whether to validate the parsed statements and values
315
+ # @option options [Array] :errors
316
+ # array for placing errors found when parsing
317
+ # @option options [Array] :warnings
318
+ # array for placing warnings found when parsing
319
+ # @option options [Boolean] :progress
320
+ # Show progress of parser productions
321
+ # @option options [Boolean] :debug
322
+ # Detailed debug output
323
+ # @yield [parser] `self`
324
+ # @yieldparam [LD::Patch::Parser] parser
325
+ # @return [LD::Patch::Parser] The parser instance, or result returned from block
326
+ def initialize(input = nil, options = {}, &block)
327
+ @input = case input
328
+ when IO, StringIO then input.read
329
+ else input.to_s.dup
330
+ end
331
+ @input.encode!(Encoding::UTF_8) if @input.respond_to?(:encode!)
332
+ @options = {anon_base: "b0", validate: false}.merge(options)
333
+ @errors = @options[:errors]
334
+ @options[:debug] ||= case
335
+ when options[:progress] then 2
336
+ when options[:validate] then (@errors ? nil : 1)
337
+ end
338
+
339
+ debug("base IRI") {base_uri.inspect}
340
+ debug("validate") {validate?.inspect}
341
+
342
+ @vars = {}
343
+
344
+ if block_given?
345
+ case block.arity
346
+ when 0 then instance_eval(&block)
347
+ else block.call(self)
348
+ end
349
+ end
350
+ end
351
+
352
+ ##
353
+ # Returns `true` if the input string is syntactically valid.
354
+ #
355
+ # @return [Boolean]
356
+ def valid?
357
+ parse
358
+ true
359
+ rescue ParseError
360
+ false
361
+ end
362
+
363
+ # @return [String]
364
+ def to_sxp_bin
365
+ @result
366
+ end
367
+
368
+ def to_s
369
+ @result.to_sxp
370
+ end
371
+
372
+ ##
373
+ # Accumulated errors found during processing
374
+ # @return [Array<String>]
375
+ attr_reader :errors
376
+
377
+ alias_method :ll1_parse, :parse
378
+ # Parse patch
379
+ #
380
+ # The result is an S-List. Productions return an array such as the following:
381
+ #
382
+ # (prefix ((: <http://example/>))
383
+ #
384
+ # @param [Symbol, #to_s] prod The starting production for the parser.
385
+ # It may be a URI from the grammar, or a symbol representing the local_name portion of the grammar URI.
386
+ # @return [SPARQL::Algebra::Operator, Array]
387
+ # @raise [ParseError] when illegal grammar detected.
388
+ def parse(prod = START)
389
+ ll1_parse(@input, prod.to_sym, @options.merge(branch: BRANCH,
390
+ first: FIRST,
391
+ follow: FOLLOW,
392
+ whitespace: WS)
393
+ ) do |context, *data|
394
+ case context
395
+ when :trace
396
+ level, lineno, depth, *args = data
397
+ message = args.to_sse
398
+ d_str = depth > 100 ? ' ' * 100 + '+' : ' ' * depth
399
+ str = "[#{lineno}](#{level})#{d_str}#{message}".chop
400
+ if @errors && level == 0
401
+ @errors << str
402
+ else
403
+ case @options[:debug]
404
+ when Array
405
+ @options[:debug] << str
406
+ when TrueClass
407
+ $stderr.puts str
408
+ when Integer
409
+ $stderr.puts(str) if level <= @options[:debug]
410
+ end
411
+ end
412
+ end
413
+ end
414
+
415
+ # The last thing on the @prod_data stack is the result
416
+ @result = case
417
+ when !prod_data.is_a?(Hash)
418
+ prod_data
419
+ when prod_data.empty?
420
+ nil
421
+ when prod_data[:ldpatch]
422
+ prod_data[:ldpatch]
423
+ else
424
+ key = prod_data.keys.first
425
+ [key] + Array(prod_data[key]) # Creates [:key, [:triple], ...]
426
+ end
427
+
428
+ # Validate resulting expression
429
+ @result.validate! if @result && validate?
430
+ @result
431
+ rescue EBNF::LL1::Parser::Error, EBNF::LL1::Lexer::Error => e
432
+ raise LD::Patch::ParseError.new(e.message, lineno: e.lineno, token: e.token)
433
+ end
434
+
435
+ ##
436
+ # Returns the Base URI defined for the parser,
437
+ # as specified or when parsing a BASE prologue element.
438
+ #
439
+ # @example
440
+ # base #=> RDF::URI('http://example.com/')
441
+ #
442
+ # @return [HRDF::URI]
443
+ def base_uri
444
+ RDF::URI(@options[:base_uri])
445
+ end
446
+
447
+ ##
448
+ # Set the Base URI to use for this parser.
449
+ #
450
+ # @param [RDF::URI, #to_s] iri
451
+ #
452
+ # @example
453
+ # base_uri = RDF::URI('http://purl.org/dc/terms/')
454
+ #
455
+ # @return [RDF::URI]
456
+ def base_uri=(iri)
457
+ @options[:base_uri] = RDF::URI(iri)
458
+ end
459
+
460
+ ##
461
+ # Returns the URI prefixes currently defined for this parser.
462
+ #
463
+ # @example
464
+ # prefixes[:dc] #=> RDF::URI('http://purl.org/dc/terms/')
465
+ #
466
+ # @return [Hash{Symbol => RDF::URI}]
467
+ # @since 0.3.0
468
+ def prefixes
469
+ @options[:prefixes] ||= {}
470
+ end
471
+
472
+ ##
473
+ # Defines the given named URI prefix for this parser.
474
+ #
475
+ # @example Defining a URI prefix
476
+ # prefix :dc, RDF::URI('http://purl.org/dc/terms/')
477
+ #
478
+ # @example Returning a URI prefix
479
+ # prefix(:dc) #=> RDF::URI('http://purl.org/dc/terms/')
480
+ #
481
+ # @overload prefix(name, uri)
482
+ # @param [Symbol, #to_s] name
483
+ # @param [RDF::URI, #to_s] uri
484
+ #
485
+ # @overload prefix(name)
486
+ # @param [Symbol, #to_s] name
487
+ #
488
+ # @return [RDF::URI]
489
+ def prefix(name = nil, iri = nil)
490
+ name = name.to_s.empty? ? nil : (name.respond_to?(:to_sym) ? name.to_sym : name.to_s.to_sym)
491
+ iri.nil? ? prefixes[name] : prefixes[name] = iri
492
+ end
493
+
494
+ private
495
+ ##
496
+ # Returns `true` if parsed statements and values should be validated.
497
+ #
498
+ # @return [Boolean] `true` or `false`
499
+ # @since 0.3.0
500
+ def resolve_iris?
501
+ @options[:resolve_iris]
502
+ end
503
+
504
+ ##
505
+ # Returns `true` when resolving IRIs, otherwise BASE and PREFIX are retained in the output algebra.
506
+ #
507
+ # @return [Boolean] `true` or `false`
508
+ # @since 1.0.3
509
+ def validate?
510
+ @options[:validate]
511
+ end
512
+
513
+ ##
514
+ # Return variable allocated to an ID.
515
+ # If no ID is provided, a new variable
516
+ # is allocated. Otherwise, any previous assignment will be used.
517
+ #
518
+ # The variable has a #distinguished? method applied depending on if this
519
+ # is a disinguished or non-distinguished variable. Non-distinguished
520
+ # variables are effectively the same as BNodes.
521
+ # @return [RDF::Query::Variable]
522
+ def variable(id, distinguished = true)
523
+ id = nil if id.to_s.empty?
524
+
525
+ if id
526
+ @vars[id] ||= begin
527
+ v = RDF::Query::Variable.new(id)
528
+ v.distinguished = distinguished
529
+ v
530
+ end
531
+ else
532
+ unless distinguished
533
+ # Allocate a non-distinguished variable identifier
534
+ id = @nd_var_gen
535
+ @nd_var_gen = id.succ
536
+ end
537
+ v = RDF::Query::Variable.new(id)
538
+ v.distinguished = distinguished
539
+ v
540
+ end
541
+ end
542
+
543
+ # Used for generating BNode labels
544
+ attr_accessor :nd_var_gen
545
+
546
+ # Reset the bnode cache, always generating new nodes, and start generating BNodes instead of non-distinguished variables
547
+ def clear_bnode_cache
548
+ @nd_var_gen = false
549
+ @bnode_cache = {}
550
+ end
551
+
552
+ # Generate a BNode identifier
553
+ def bnode(id = nil)
554
+ if @nd_var_gen
555
+ # Use non-distinguished variables within patterns
556
+ variable(id, false)
557
+ else
558
+ unless id
559
+ id = @options[:anon_base]
560
+ @options[:anon_base] = @options[:anon_base].succ
561
+ end
562
+ # Don't use provided ID to avoid aliasing issues when re-serializing the graph, when the bnode identifiers are re-used
563
+ (@bnode_cache ||= {})[id.to_s] ||= begin
564
+ new_bnode = RDF::Node.new
565
+ new_bnode.lexical = "_:#{id}"
566
+ new_bnode
567
+ end
568
+ end
569
+ end
570
+
571
+ # Create URIs
572
+ def iri(value)
573
+ # If we have a base URI, use that when constructing a new URI
574
+ iri = if base_uri
575
+ u = base_uri.join(value.to_s)
576
+ u.lexical = "<#{value}>" unless u.to_s == value.to_s || resolve_iris?
577
+ u
578
+ else
579
+ RDF::URI(value)
580
+ end
581
+
582
+ iri.validate! if validate? && iri.respond_to?(:validate)
583
+ #iri = RDF::URI.intern(iri) if intern?
584
+ iri
585
+ end
586
+
587
+ def ns(prefix, suffix)
588
+ error("pname", "undefined prefix #{prefix.inspect}") unless prefix(prefix)
589
+ base = prefix(prefix).to_s
590
+ suffix = suffix.to_s.sub(/^\#/, "") if base.index("#")
591
+ debug {"ns(#{prefix.inspect}): base: '#{base}', suffix: '#{suffix}'"}
592
+ iri = iri(base + suffix.to_s)
593
+ # Cause URI to be serialized as a lexical
594
+ iri.lexical = "#{prefix}:#{suffix}" unless resolve_iris?
595
+ iri
596
+ end
597
+
598
+ # Create a literal
599
+ def literal(value, options = {})
600
+ options = options.dup
601
+ # Internal representation is to not use xsd:string, although it could arguably go the other way.
602
+ options.delete(:datatype) if options[:datatype] == RDF::XSD.string
603
+ debug("literal") do
604
+ "value: #{value.inspect}, " +
605
+ "options: #{options.inspect}, " +
606
+ "validate: #{validate?.inspect}, "
607
+ end
608
+ RDF::Literal.new(value, options.merge(validate: validate?))
609
+ end
610
+ end
611
+ end
612
+
613
+
614
+ # Update RDF::Node to set lexical representation of BNode
615
+ ##
616
+ # Extensions for RDF::URI
617
+ class RDF::Node
618
+ # Original lexical value of this URI to allow for round-trip serialization.
619
+ def lexical=(value); @lexical = value; end
620
+ def lexical; @lexical; end
621
+ end