Class LogoParser
In: LogoParser.rb
Parent: Object

The LogoParser class provides a code parser for an associated Turtle instance. The LogoParser#parse method translates a given string into equivalent method calls for the LogoTurtle. It resolves code structures and stores the variables and function definitions it encounters for future use. Variables and functions can also be set when initializing a LogoParser instance. This allows the LogoParser to use predefined function libraries and to resolve loops in scripts recursively.

Methods

merge   new   parse   parse_active   parse_passive   resolve   to_a  

Constants

RMATH = /cos|exp|log|log10|sin|sqrt|tan/i   Regular expression which matches the method names from Ruby’s Math module
ATT = /((\s*([-+]?\d+\.?\d*|:\w+|[-+*\/%]|#{RMATH}|[()]))*)/i   Regular expression which matches attributes for logo commands. attributes may be:
  • a float or fixnum with optional sign
  • a variable (even if not initialized)
  • a mathematical expression containing numbers, variables, parentheses, and mathematical operators in any (possibly invalid) combination

Attributes

f_vars  [R]  Variables expected by a known function {function => [":var", …]} [Hash]
functions  [R]  Functions known to the LogoParser instance {function => code block} [Hash]
target  [RW]  Currently assigned recipient of translated commands [LogoTurtle]
variables  [R]  Initialized variables and their current value {:var => value} [Hash]

Public Class methods

Return an initialized LogoParser instance, assigned to a LogoTurtle instance which will receive the translated method calls and error messages.

Parameters:

turtle: The Turtle object which will receive the translated commands [Turtle]

functions: The functions which should be already known to the LogoParser object {function name [String] => code block [String]} [Hash]

f_vars: The variables associated to the known functions {function name [String] => variable names [Array]} [Hash]

variables: The variables which are already initialized {variable name [String] => value [Float]} [Hash]

Returns:

The initialized LogoParser object [LogoParser]

[Source]

# File LogoParser.rb, line 60
  def initialize(turtle, functions=Hash.new, f_vars=Hash.new, variables=Hash.new)
    @target = turtle
    @functions = functions
    @f_vars = f_vars
    @variables = variables
    @count, @f_name, @condition, @collect, @depth = nil, nil, nil, nil, 0
  end

Public Instance methods

Evaluate the given string line by line. Different parsing methods are called depending on the current state of the Parser. Commands inside a block are not immediately executed but stored for later evaluation.

Parameters:

script: The Logo program to be evaluated [String]

Returns:

The LogoParser object used to parse the program [LogoParser]

[Source]

# File LogoParser.rb, line 90
  def parse(script)
    begin
      script.each_line do |line|
        while line    # loop until line is completely parsed
          if @collect  # currently inside loop or function, store commands
            line = parse_passive(line)
          else         # immediately execute code
            line = parse_active(line)
          end
        end
      end
      self
    rescue SystemStackError
      @target.log "ERROR: STACK LEVEL TOO DEEP!"
      self
    end
  end

returns an array representation of the LogoParser instance

Returns:

An array containing all attributes of the LogoParser’s current state [Array]

[Source]

# File LogoParser.rb, line 74
  def to_a
    return [@target.to_a, @functions, @f_vars, @variables]
  end

Private Instance methods

merges all functions and variables from the given parser instance and the calling instance

Parameters:

parser: The LogoParser object that will be merged with the calling object [LogoParser]

Returns:

A LogoParser containing all functions and variables from both objects [LogoParser]

[Source]

# File LogoParser.rb, line 379
  def merge(parser)
    parser.functions.each_pair { |key, value| @functions[key] = value }
    parser.f_vars.each_pair { |key, value| @f_vars[key] = value }
    parser.variables.each_pair { |key, value| @variables[key] = value }
    self
  end

Parses commands which are executed immediately. Only the first command in the given string is evaluated. The rest of the string is the method’s return value.

Parameters:

line: The current program line to be evaluated [String]

Returns:

The remaining part of the line which has not been parsed [String] or nil if nothing remains unparsed

[Source]

# File LogoParser.rb, line 121
  def parse_active(line)
    case line
    when /^\s*([#;].*)?$/i                                # empty line or comment
      return nil
  # turtle actions
    when /^\s*forward#{ATT}\s*(.*)?$/i                    # forward x
      @target.forward(resolve($1))
      return $4
    when /^\s*fd#{ATT}\s*(.*)?$/i                         # fd x
      @target.forward(resolve($1))
      return $4
    when /^\s*backward#{ATT}\s*(.*)?$/i                   # backward x
      @target.backward(resolve($1))
      return $4
    when /^\s*bk#{ATT}\s*(.*)?$/i                         # bk x
      @target.backward(resolve($1))
      return $4
    when /^\s*right#{ATT}\s*(.*)?$/i                      # right x
      @target.right(resolve($1))
      return $4
    when /^\s*rt#{ATT}\s*(.*)?$/i                         # rt x
      @target.right(resolve($1))
      return $4
    when /^\s*left#{ATT}\s*(.*)?$/i                       # left x
      @target.left(resolve($1))
      return $4
    when /^\s*lt#{ATT}\s*(.*)?$/i                         # lt x
      @target.left(resolve($1))
      return $4
    when /^\s*move\s*([-+:\w.]+)\s*([-+:\w.]+)\s*(.*)?$/i # move x y
      @target.move(resolve($1), resolve($2))
      return $3
    when /^\s*home\s*(.*)?$/i                             # home
      @target.home
      return $1
    when /^\s*turn#{ATT}\s*(.*)?$/i                       # turn x
      @target.turn(resolve($1))
      return $4
    when /^\s*reset\s*(.*)?$/i                            # reset
      @target.reset
      return $1
    when /^\s*penup\s*(.*)?$/i                            # penup
      @target.penup
      return $1
    when /^\s*pu\s*(.*)?$/i                               # pu
      @target.penup
      return $1
    when /^\s*pendown\s*(.*)?$/i                          # pendown
      @target.pendown
      return $1
    when /^\s*pd\s*(.*)?$/i                               # pd
      @target.pendown
      return $1
    when /^\s*hide\s*(.*)?$/i                             # hide
      @target.hide
      return $1
    when /^\s*show\s*(.*)?$/i                             # show
      @target.show
      return $1
    when /^\s*color\s*(\w+)\s*(.*)?$/i                    # color x
      @target.setColor($1)
      return $2
    when /^\s*fg\s*(\w+)\s*(.*)?$/i                       # fg x
      @target.setColor($1)
      return $2
    when /^\s*background\s*(\w+)\s*(.*)?$/i               # background x
      @target.setBackground($1)
      return $2
    when /^\s*bg\s*(\w+)\s*(.*)?$/i                       # bg x
      @target.setBackground($1)
      return $2
  # variables
    when /^\s*(:\w+)\s*=#{ATT}\s*(.*)?$/i
      v_name, v_value = $1, resolve($2.to_s)
      @variables[v_name] = v_value
      return $5
  # code structures
    when /^\s*repeat#{ATT}\s*(.*)?$/i                                # loop 'repeat x'
      @count, @collect, @depth = resolve($1).round, "", @depth+1
      return $4
    when /^\s*if#{ATT}\s*([<>!=]+)#{ATT}\s*then\s*(.*)?$/i           # if ... then
      @collect, @depth = "", @depth+1
      begin
        @condition = eval("#{resolve($1)} #{$4} #{resolve($5)}")
      rescue SyntaxError
        # condition can't be evaluated. Return default value and log error
        @target.log "ERROR: INVALID COMPARISON \'#{$4}\'"
        @condition = false
      end
      return $8
    when /^\s*while#{ATT}\s*([<>!=]+)#{ATT}\s*do\s*(.*)?$/i          # while ... do
      @collect, @depth = "", @depth+1
      @condition = ["#{$1}", "#{$4}", "#{$5}"]
      return $8
    when /^\s*(def|to)\s*([a-zA-Z]\w*)((\s*(:\w+))*)\s*(.*)?$/i      # define function
      @f_name, @collect, @depth = $2, "", @depth+1
      $3.split(" ")
      @f_vars[@f_name] = $3.split(" ")
      return $6
    when /^\s*([a-zA-Z]\w*)((\s*([-+]?\d+\.?\d*|:\w+))*)\s*(.*)?$/i  # execute function
      if @functions.key?($1)
        parameters = $2.split(" ")
        if @f_vars[$1].length == parameters.length
          tempvars = @variables.dup                        # include all known variables
          i = 0
          @f_vars[$1].each do |name|
            tempvars[name] = resolve(parameters[i])        # add function parameters
            i += 1
          end
          merge(LogoParser.new(@target, @functions, @f_vars, tempvars).parse(@functions[$1]))
        else
          @target.log "ERROR: WRONG NUMBER OF ARGUMENTS FOR FUNCTION \'#{$1}\'"
        end
      else
        @target.log "ERROR: UNKNOWN FUNCTION \'#{$1}\'"
      end
      return $5
    else # syntax error. drop first element and reparse line
      line =~ /^\s*([^\s]+)(\s*)(.*)?$/
      @target.log "SYNTAX ERROR: #{$1}"
      return $3
    end
  end

Parses commands inside a loop or function which are not immediately executed. Only the first command in the given string is evaluated. The rest of the string is the method’s return value.

Parameters:

line: The current program line to be evaluated [String]

Returns:

The remaining part of the line which has not been parsed [String] or nil if nothing remains unparsed

[Source]

# File LogoParser.rb, line 258
  def parse_passive(line)
    case line
    when /^\s*(repeat#{ATT})\s*(.*)?$/i                       # start of inner 'repeat x' loop
      @depth += 1
      @collect << $1+" "
      return $5
    when /^\s*((def|to)\s*([a-z]+)((\s+:\w+)*))\s*(.*)?$/i    # start of inner function
      @depth += 1
      @collect << $1+" "
      return $6
    when /^\s*(if#{ATT}\s*([<>!=]+)#{ATT}\s*then)\s*(.*)?$/i  # start of inner 'if' conditional
      @depth += 1
      @collect << $1+" "
      return $9
    when /^\s*(while#{ATT}\s*([<>!=]+)#{ATT}\s*do)\s*(.*)?$/i # start of inner 'while' conditional
      @depth += 1
      @collect << $1+" "
      return $9
    when /^\s*(end)(\s*)(.*)?$/i                              # end of a block
      @depth -= 1
      if @depth == 0                                           # outermost block, stop collecting
        if @count                                               # block was loop, execute it now
          @count.times do
            merge(LogoParser.new(@target, @functions, @f_vars, @variables).parse(@collect))
          end
        elsif @f_name                                           # block was function, store it
          @functions[@f_name] = @collect
        else                                                    # block was condition
          if @condition.instance_of?(TrueClass)                  # if
            merge(LogoParser.new(@target, @functions, @f_vars, @variables).parse(@collect))
          elsif @condition.instance_of?(Array)                   # while
            loop_while = true
            while loop_while
              begin
                loop_while = eval("#{resolve(@condition[0])} #{@condition[1]} #{resolve(@condition[2])}")
              rescue SyntaxError
                # condition can't be evaluated. Return default value and log error
                @target.log "ERROR: INVALID COMPARISON \'#{@condition[1]}\'"
                loop_while = false
              end
              if loop_while
                merge(LogoParser.new(@target, @functions, @f_vars, @variables).parse(@collect))
              end
            end
          end
        end
        @count, @f_name, @condition, @collect = nil, nil, nil, nil
      else                                                     # inner block, continue collecting
        @collect << $1+$2
        @collect << "\n" if !$2
      end
      return $3
    else                                                      # normal command
      line =~ /^\s*([^\s]+)(\s*)(.*)?$/i                       # collect first element, reparse rest
      @collect << $1+$2 if $1
      @collect << "\n" if !$2
      return $3
    end
  end

Resolve a given expression containing any combination of numbers, variables, parentheses and basic mathematical operators and return the result. All variables are replaced with their assigned values. Uninitialized variables are deleted from the expression. Invalid expressions return default value 0. Errors are sent to the LogoTurtle’s message log.

Parameters:

expression: The mathematical expression to be evaluated [String]

Returns:

The result of the evaluation [Float]

[Source]

# File LogoParser.rb, line 332
  def resolve(expression)
    list = expression.dup
     while list =~ /^(.*?)(:\w+)(.*)$/i # resolve all variables
      value = @variables[$2]
      @target.log "ERROR: UNINITIALIZED VARIABLE \'#{$2}\'" if !value
      list = $1.to_s + value.to_s + $3.to_s
    end
     while list =~ /^(.*?)[^(Math\.)](#{RMATH})(\s*\()(.*)$/i # translate method calls
      list = $1.to_s + "Math." + $2.downcase + "(" + $4.to_s
    end
    begin
      result = eval(list).to_f
      # result can get out of range without raising an exception (-+Inf)
      raise RangeError if !result.finite?
    rescue SyntaxError
      # expression can't be evaluated. Return default value and log error
      @target.log "ERROR: BAD EXPRESSION \'#{expression}\'"
      result = 0.0
    rescue ZeroDivisionError
      # divide by zero. Return default value and log error
      @target.log "ERROR: DIVIDE BY ZERO IN \'#{expression}\'"
      result = 0.0
    rescue RangeError
      # result is our of range. Return Default value and log error
      @target.log "ERROR: RESULT OUT OF RANGE \'#{result}\'"
      result = 0.0
    rescue StandardError
      # syntax error in method. Return default value and log error
      @target.log "ERROR: BAD EXPRESSION \'#{expression}\'"
      result = 0.0
    end
    result
  end

[Validate]