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.
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:
|
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] |
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]
# 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
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]
# 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]
# File LogoParser.rb, line 74 def to_a return [@target.to_a, @functions, @f_vars, @variables] end
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]
# 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
# 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
# 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]
# 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