class Keisan::AST::LineBuilder

Public Class Methods

new(components) click to toggle source

Build from parser

# File lib/keisan/ast/line_builder.rb, line 5
def initialize(components)
  @components = components
  @nodes = components_to_basic_nodes(@components)

  # Negative means not an operator
  @priorities = @nodes.map {|node| node.is_a?(Keisan::Parsing::Operator) ? node.priority : -1}

  consume_operators!

  case @nodes.count
  when 0
    # Empty string, set to just Null
    @nodes = [Keisan::AST::Null.new]
  when 1
    # Good
  else
    raise Keisan::Exceptions::ASTError.new("Should end up with a single node")
  end
end

Public Instance Methods

ast() click to toggle source
# File lib/keisan/ast/line_builder.rb, line 29
def ast
  node
end
node() click to toggle source
# File lib/keisan/ast/line_builder.rb, line 25
def node
  @nodes.first
end

Private Instance Methods

apply_postfix_component_to_node(postfix_component, node) click to toggle source
# File lib/keisan/ast/line_builder.rb, line 78
def apply_postfix_component_to_node(postfix_component, node)
  case postfix_component
  when Keisan::Parsing::Indexing
    postfix_component.node_class.new(
      node,
      postfix_component.arguments.map {|parsing_argument|
        Builder.new(components: parsing_argument.components).node
      }
    )
  when Keisan::Parsing::DotWord
    Keisan::AST::Function.new(
      [node],
      postfix_component.name
    )
  when Keisan::Parsing::DotOperator
    Keisan::AST::Function.new(
      [node] + postfix_component.arguments.map {|parsing_argument|
        Builder.new(components: parsing_argument.components).node
      },
      postfix_component.name
    )
  else
    raise Keisan::Exceptions::ASTError.new("Invalid postfix component #{postfix_component}")
  end
end
components_to_basic_nodes(components) click to toggle source

Array of AST elements, and Parsing operators

# File lib/keisan/ast/line_builder.rb, line 36
def components_to_basic_nodes(components)
  nodes_components = []

  components.each do |component|
    if component.is_a?(Keisan::Parsing::LineSeparator)
      nodes_components << [component]
    elsif nodes_components.empty? || nodes_components.last.last.is_a?(Keisan::Parsing::LineSeparator)
      nodes_components << [component]
    else
      is_operator = [nodes_components.last.last.is_a?(Keisan::Parsing::Operator), component.is_a?(Keisan::Parsing::Operator)]

      if is_operator.first == is_operator.last
        nodes_components.last << component
      else
        nodes_components << [component]
      end
    end
  end

  nodes_components.inject([]) do |nodes, node_or_component_group|
    if node_or_component_group.first.is_a?(Keisan::Parsing::Operator)
      node_or_component_group.each do |component|
        nodes << component
      end
    else
      nodes << node_from_components(node_or_component_group)
    end

    nodes
  end
end
consume_operators!() click to toggle source
# File lib/keisan/ast/line_builder.rb, line 183
def consume_operators!
  loop do
    break if @priorities.empty?
    max_priority = @priorities.max
    break if max_priority < 0

    consume_operators_with_priority!(max_priority)
  end
end
consume_operators_with_priority!(priority) click to toggle source
# File lib/keisan/ast/line_builder.rb, line 193
def consume_operators_with_priority!(priority)
  p_indexes = @priorities.map.with_index.select {|p,index| p == priority}
  # :left, :right, or :none
  associativity = AST::Operator.associativity_of_priority(priority)

  if associativity == :right
    index = p_indexes[-1][1]
  else
    index = p_indexes[0][1]
  end

  operator = @nodes[index]

  # If has unary operators after, must process those first
  if @nodes[index+1].is_a?(Keisan::Parsing::UnaryOperator)
    loop do
      break if !@nodes[index+1].is_a?(Keisan::Parsing::UnaryOperator)
      index += 1
    end
    operator = @nodes[index]
  end

  # operator is the current operator to process, and index is its index
  if operator.is_a?(Keisan::Parsing::UnaryOperator)
    replacement_node = operator.node_class.new(
      children = [@nodes[index+1]]
    )
    @nodes.delete_if.with_index {|node, i| i >= index && i <= index+1}
    @priorities.delete_if.with_index {|node, i| i >= index && i <= index+1}
    @nodes.insert(index, replacement_node)
    @priorities.insert(index, -1)
  elsif operator.is_a?(Keisan::Parsing::Operator)
    if operator.is_a?(Keisan::Parsing::CompoundAssignment)
      replacement_node = operator.node_class.new(
        children = [@nodes[index-1],@nodes[index+1]],
        parsing_operators = [operator],
        compound_operator: operator.compound_operator
      )
    else
      replacement_node = operator.node_class.new(
        children = [@nodes[index-1],@nodes[index+1]],
        parsing_operators = [operator]
      )
    end

    @nodes.delete_if.with_index {|node, i| i >= index-1 && i <= index+1}
    @priorities.delete_if.with_index {|node, i| i >= index-1 && i <= index+1}
    @nodes.insert(index-1, replacement_node)
    @priorities.insert(index-1, -1)
  else
    raise Keisan::Exceptions::ASTError.new("Can only consume operators")
  end
end
node_from_components(components) click to toggle source
# File lib/keisan/ast/line_builder.rb, line 68
def node_from_components(components)
  node, postfix_components = *node_postfixes(components)
  # Apply postfix operators
  postfix_components.each do |postfix_component|
    node = apply_postfix_component_to_node(postfix_component, node)
  end

  node
end
node_of_component(component) click to toggle source
# File lib/keisan/ast/line_builder.rb, line 128
def node_of_component(component)
  case component
  when Parsing::Number
    AST::Number.new(component.value)
  when Parsing::String
    AST::String.new(component.value)
  when Parsing::Null
    AST::Null.new
  when Parsing::Variable
    AST::Variable.new(component.name)
  when Parsing::Boolean
    AST::Boolean.new(component.value)
  when Parsing::List
    AST::List.new(
      component.arguments.map {|parsing_argument|
        Builder.new(components: parsing_argument.components).node
      }
    )
  when Parsing::Hash
    AST::Hash.new(
      component.key_value_pairs.map {|key_value_pair|
        [
          Builder.new(components: [key_value_pair[0]]).node,
          Builder.new(components: key_value_pair[1].components).node
        ]
      }
    )
  when Parsing::RoundGroup
    Builder.new(components: component.components).node
  when Parsing::CurlyGroup
    Block.new(Builder.new(components: component.components).node)
  when Parsing::Function
    AST::Function.new(
      component.arguments.map {|parsing_argument|
        Builder.new(components: parsing_argument.components).node
      },
      component.name
    )
  when Parsing::DotWord
    AST::Function.new(
      [node_of_component(component.target)],
      component.name
    )
  when Parsing::DotOperator
    AST::Function.new(
      [node_of_component(component.target)] + component.arguments.map {|parsing_argument|
        Builder.new(components: parsing_argument.components).node
      },
      component.name
    )
  else
    raise Exceptions::ASTError.new("Unhandled component, #{component}")
  end
end
node_postfixes(components) click to toggle source

Returns an array of the form

node, postfix_operators

middle_node is the main node which will be modified by prefix and postfix operators postfix_operators is an array of Keisan::Parsing::Indexing, DotWord, and DotOperator objects

# File lib/keisan/ast/line_builder.rb, line 108
def node_postfixes(components)
  index_of_postfix_components = components.map.with_index {|c,i| [c,i]}.select {|c,i|
    c.is_a?(Keisan::Parsing::Indexing) || c.is_a?(Keisan::Parsing::DotWord) || c.is_a?(Keisan::Parsing::DotOperator)
  }.map(&:last)
  unless index_of_postfix_components.reverse.map.with_index.all? {|i,j| i + j == components.size - 1 }
    raise Keisan::Exceptions::ASTError.new("postfix components must be in back")
  end

  num_postfix = index_of_postfix_components.size

  unless num_postfix + 1 == components.size
    raise Keisan::Exceptions::ASTError.new("have too many components")
  end

  [
    node_of_component(components[0]),
    index_of_postfix_components.map {|i| components[i]}
  ]
end