Class: EBNF::Base

Inherits:
Object
  • Object
show all
Includes:
BNF, LL1, Native, PEG
Defined in:
lib/ebnf/base.rb

Instance Attribute Summary collapse

Attributes included from LL1

#branch, #cleanup, #first, #follow, #pass, #start, #terminals

Instance Method Summary collapse

Methods included from PEG

#make_peg, #to_ruby_peg

Methods included from Native

#alt, #diff, #eachRule, #expression, #postfix, #primary, #ruleParts, #seq, #terminal

Methods included from LL1

#build_tables, #first_follow, #outputTable, #to_ruby_ll1

Methods included from BNF

#make_bnf

Constructor Details

#initialize(input, format: :ebnf, **options) ⇒ Base

Parse the string or file input generating an abstract syntax tree in S-Expressions (similar to SPARQL SSE)

Parameters:

  • input (#read, #to_s)
  • format (Symbol) (defaults to: :ebnf)

    (:ebnf) Format of input, one of :abnf, :ebnf, :isoebnf, :isoebnf, :native, or :sxp. Use :native for the native EBNF parser, rather than the PEG parser.

  • options (Hash{Symbol => Object})

Options Hash (**options):

  • :level (Boolean)

    Trace level. 0(debug), 1(info), 2(warn), 3(error).

  • :validate (Boolean, Array)

    Validate resulting grammar.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/ebnf/base.rb', line 113

def initialize(input, format: :ebnf, **options)
  @options = options.dup
  @lineno, @depth, @errors = 1, 0, []
  @ast = []

  input = input.respond_to?(:read) ? input.read : input.to_s

  case format
  when :abnf
    abnf = ABNF.new(input, **options)
    @ast = abnf.ast
  when :ebnf
    ebnf = Parser.new(input, **options)
    @ast = ebnf.ast
  when :isoebnf
    iso = ISOEBNF.new(input, **options)
    @ast = iso.ast
  when :native
    terminals = false
    scanner = StringScanner.new(input)

    eachRule(scanner) do |r|
      debug("rule string") {r.inspect}
      case r
      when /^@terminals/
        # Switch mode to parsing terminals
        terminals = true
        rule = Rule.new(nil, nil, nil, kind: :terminals, ebnf: self)
        @ast << rule
      when /^@pass\s*(.*)$/m
        expr = expression($1).first
        rule = Rule.new(nil, nil, expr, kind: :pass, ebnf: self)
        rule.orig = expr
        @ast << rule
      else
        rule = depth {ruleParts(r)}

        rule.kind = :terminal if terminals # Override after we've parsed @terminals
        rule.orig = r
        @ast << rule
      end
    end
  when :sxp
    @ast = SXP::Reader::Basic.read(input).map {|e| Rule.from_sxp(e)}
  else
    raise "unknown input format #{format.inspect}"
  end

  validate! if @options[:validate]
end

Instance Attribute Details

#astArray<Rule> (readonly)

Abstract syntax tree from parse

Returns:



94
95
96
# File 'lib/ebnf/base.rb', line 94

def ast
  @ast
end

#errorsArray<String>

Grammar errors, or errors found genering parse tables

Returns:

  • (Array<String>)


99
100
101
# File 'lib/ebnf/base.rb', line 99

def errors
  @errors
end

Instance Method Details

#debug(node, message) ⇒ Object #debug(message) ⇒ Object

Progress output when debugging

Overloads:

  • #debug(node, message) ⇒ Object

    Parameters:

    • node (String)

      relative location in input

    • message (String)

      (“”)

  • #debug(message) ⇒ Object

    Parameters:

    • message (String)

      (“”)

Yield Returns:

  • (String)

    added to message



338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/ebnf/base.rb', line 338

def debug(*args, level: Logger::DEBUG, **options)
  return unless @options.key?(:logger)
  depth = options[:depth] || @depth
  args << yield if block_given?
  message = "#{args.join(': ')}"
  str = "[#{@lineno}]#{' ' * depth}#{message}"
  if @options[:logger].respond_to?(:add)
    @options[:logger].add(level, str)
  elsif @options[:logger].respond_to?(:<<)
    @options[:logger] << "[#{lineno}] " + str
  end
end

#depthObject



305
306
307
308
309
310
# File 'lib/ebnf/base.rb', line 305

def depth
  @depth += 1
  ret = yield
  @depth -= 1
  ret
end

#dupObject



291
292
293
294
295
# File 'lib/ebnf/base.rb', line 291

def dup
  new_obj = super
  new_obj.instance_variable_set(:@ast, @ast.dup)
  new_obj
end

#each(kind) {|rule| ... } ⇒ Object

Iterate over each rule or terminal, except empty

Parameters:

  • kind (:termina, :rule)

Yields:

  • rule

Yield Parameters:



198
199
200
# File 'lib/ebnf/base.rb', line 198

def each(kind, &block)
  ast.each {|r| block.call(r) if r.kind == kind && r.sym != :_empty}
end

#error(*args, **options) ⇒ Object

Error output



318
319
320
321
322
323
324
325
# File 'lib/ebnf/base.rb', line 318

def error(*args, **options)
  depth = options[:depth] || @depth
  args << yield if block_given?
  message = "#{args.join(': ')}"
  debug(message, level: Logger::ERROR, **options)
  @errors << message
  $stderr.puts(message)
end

#find_rule(sym) ⇒ Rule

Find a rule given a symbol

Parameters:

  • sym (Symbol)

Returns:



301
302
303
# File 'lib/ebnf/base.rb', line 301

def find_rule(sym)
  (@find ||= {})[sym] ||= ast.detect {|r| r.sym == sym}
end

#progress(*args, **options) ⇒ Object

Progress output, less than debugging



313
314
315
# File 'lib/ebnf/base.rb', line 313

def progress(*args, **options)
  debug(*args, level: Logger::INFO, **options)
end

#renumber!Object

Renumber, rule identifiers



261
262
263
264
265
# File 'lib/ebnf/base.rb', line 261

def renumber!
  ast.each_with_index do |rule, index|
    rule.id = (index + 1).to_s
  end
end

#to_html(format: :ebnf, validate: false) ⇒ String

Output formatted EBNF as HTML

Parameters:

  • format (:abnf, :ebnf, :isoebnf) (defaults to: :ebnf)

    (:ebnf)

  • validate (Boolean) (defaults to: false)

    (false) validate generated HTML.

Returns:

  • (String)


226
227
228
# File 'lib/ebnf/base.rb', line 226

def to_html(format: :ebnf, validate: false)
  Writer.html(*ast, format: format, validate: validate)
end

#to_ruby(output = $stdout, grammarFile: nil, mod_name: 'Meta', **options) ⇒ Object

Output Ruby parser files

Parameters:

  • output (IO, StringIO) (defaults to: $stdout)
  • grammarFile (String) (defaults to: nil)
  • mod_name (String) (defaults to: 'Meta')

    (‘Meta’)



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/ebnf/base.rb', line 236

def to_ruby(output = $stdout, grammarFile: nil, mod_name: 'Meta', **options)
  unless output == $stdout
    output.puts "# This file is automatically generated by ebnf version #{EBNF::VERSION}"
    output.puts "# Derived from #{grammarFile}" if grammarFile
    unless self.errors.empty?
      output.puts "# Note, grammar has errors, may need to be resolved manually:"
      #output.puts "#   #{pp.conflicts.map{|c| c.join("\n#      ")}.join("\n#   ")}"
    end
    output.puts "module #{mod_name}"
    output.puts "  START = #{self.start.inspect}\n" if self.start
  end

  # Either output LL(1) BRANCH tables or rules for PEG parsing
  if ast.first.first
    to_ruby_ll1(output)
  else
    to_ruby_peg(output)
  end
  unless output == $stdout
    output.puts "end"
  end
end

#to_s(format: :ebnf) ⇒ String

Output formatted EBNF

Parameters:

  • format (:abnf, :ebnf, :isoebnf) (defaults to: :ebnf)

    (:ebnf)

Returns:

  • (String)


216
217
218
# File 'lib/ebnf/base.rb', line 216

def to_s(format: :ebnf)
  Writer.string(*ast, format: format)
end

#to_sxp(**options) ⇒ String

Write out parsed syntax string as an S-Expression

Returns:

  • (String)


206
207
208
209
# File 'lib/ebnf/base.rb', line 206

def to_sxp(**options)
  require 'sxp' unless defined?(SXP)
  SXP::Generator.string(ast.map(&:for_sxp))
end

#to_ttl(prefix = nil, ns = "http://example.org/") ⇒ String

Write out syntax tree as Turtle

Parameters:

  • prefix (String) (defaults to: nil)

    for language

  • ns (String) (defaults to: "http://example.org/")

    URI for language

Returns:

  • (String)


272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/ebnf/base.rb', line 272

def to_ttl(prefix = nil, ns = "http://example.org/")
  unless ast.empty?
    [
      "@prefix dc: <http://purl.org/dc/terms/>.",
      "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.",
      "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.",
      ("@prefix #{prefix}: <#{ns}>." if prefix),
      "@prefix : <#{ns}>.",
      "@prefix re: <http://www.w3.org/2000/10/swap/grammar/regex#>.",
      "@prefix g: <http://www.w3.org/2000/10/swap/grammar/ebnf#>.",
      "",
      ":language rdfs:isDefinedBy <>; g:start :#{ast.first.id}.",
      "",
    ].compact
  end.join("\n") +

  ast.map(&:to_ttl).join("\n")
end

#valid?Boolean

Is the grammar valid?

Uses #validate! and catches RangeError

Returns:

  • (Boolean)


187
188
189
190
191
192
# File 'lib/ebnf/base.rb', line 187

def valid?
  validate!
  true
rescue SyntaxError
  false
end

#validate!Object

Validate the grammar.

Makes sure that rules reference either strings or other defined rules.

Raises:

  • (RangeError)


170
171
172
173
174
175
176
177
178
179
# File 'lib/ebnf/base.rb', line 170

def validate!
  ast.each do |rule|
    begin
      rule.validate!(@ast)
    rescue SyntaxError => e
      error("In rule #{rule.sym}: #{e.message}")
    end
  end
  raise SyntaxError, errors.join("\n") unless errors.empty?
end