ld-patch 0.1.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.
@@ -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