.syntax program

outarg = '$' <'@out.print @match # Print last matched token on output stream'>

| .string      <'@out.print ' $ '    # Print literal string on output stream'>;

out = '<' *outarg '>' <'@out.print ā€œnā€ # Print newline on output stream'>;

exp3 = .id <'compile_' $ ' # Call the method for a rule'>

| .string        <'scan ' $>
| '.id'          <'scan /[A-Za-z]+[A-Za-z0-9_]+/'>
| '.string'      <'scan /\047[^\047]*\047/'>
| '(' exp1 ')'
| '.e'           <'@match = true        # .e means empty so always matches => set flag.'>
| '*'  <'begin'>
         exp3 
       <'end while @match              # Loop while there is a match'> 
       <'@match = true                 # Since also zero matches is ok set flag here.'>;

exp2 = ( exp3 <'if @match'> | out <'if true'> )

*(   exp3      <'report_error() unless @match'>
   | out )
 <'end'>;

exp1 = <'begin'> exp2

*( '|' <'break if @match'> exp2 )
<'end while false  # This "while false" is needed since the break is not parsed correctly by Ruby otherwise'>;

rule = .id <'def compile_' $>

'=' exp1 ';' <'end'>;

program = '.syntax' .id

<'#!/usr/bin/env ruby'>
<'require "strscan"'>
<'class StringScanner'>
<'  # Add scan for string since base StringScanner lack that...'>
<'  def scan_str(string)'>
<'    return nil unless self.peek(string.length) == string'>
<'    self.pos += string.length           # Advance the position in the input string'>
<'    return string'>
<'  end'>
<'  alias :old_scan :scan'>
<'  def scan(strOrRegexp)'>
<'    String === strOrRegexp ? scan_str(strOrRegexp) : old_scan(strOrRegexp)'>
<'  end'>
<'end'>
<'class MetaCompiler_' $> 
<'  $compiler_class = self # Save class in global var for later reference below'>
<'  def compile_string(string, outFile)'>
<'    @in, @out = StringScanner.new(string), outFile'>
<'    compile_' $ ' # call the main compile method to start compiling'> 
<'  end'>
<'  # Scan for a string or regexp and update state based on match. Skips leading whitespace.'>
<'  def scan strOrRegexp'>
<'    @in.scan /\s*/   # Skip whitespace'>
<'    @match = @in.scan strOrRegexp'>
<'    # Since nil is same as false in Ruby we can set the flag to the matched token'>
<'    @last_matched_token = @match if @match # Update last matched only if a token was matched'>
<'  end'>
<'  def report_error'>
<'    pre_lines = @in.string[0, @in.pos].split("\n") # lines of input up to current position'>
<'    post_lines = @in.rest.split("\n")              # lines of input after current position'>
<'    message = "PARSE ERROR at line #{pre_lines.length}:\n  " + pre_lines.last.inspect + " @ "'>
<'    message += post_lines.first.inspect'>
<'    message += "\n  Last matched token: #{@last_matched_token}"'>
<'    raise message'>
<'  end'>
<'  def self.compile_file(inFile, out = nil)'>
<'    outfh = (out == nil ? STDOUT : File.open(out, "w"))'>
<'    self.new.compile_string(File.read(inFile), outfh)'>
<'    outfh.close if out == nil'>
<'  end'>
*rule '.end'
<'end'>
<'if ARGV.length < 1 || ARGV.length > 2'>
<'  puts "ERROR: wrong number of parameters\n\n"'>
<'  puts "Usage: #{File.basename($0)} <input_file> [output_file]"'>
<'  exit(-1)'>
<'else'>
<'  $compiler_class.compile_file(ARGV[0], ARGV[1])'>
<'end'>;

.end