<commit_before># Hold a variable name (not a reference or value)
class VariableName < AbstractToken
  def self.init?(text)
    /\A[A-Z]\d?\z/.match(text)
  end

  def initialize(text)
    super()
    fail(BASICException, "'#{text}' is not a variable name") unless
      VariableName.init?(text)
    @var_name = text
    @variable = true
    @operand = true
    @precedence = 6
  end

  def eql?(other)
    @var_name == other.to_s
  end

  def ==(other)
    @var_name == other.to_s
  end

  def hash
    @var_name.hash
  end

  def to_s
    @var_name
  end
end

# Hold a variable (name with possible subscripts and value)
class Variable < AbstractToken
  attr_reader :subscripts

  def initialize(variable_name)
    super()
    fail(BASICException, "'#{variable_name}' is not a variable name") if
      variable_name.class.to_s != 'VariableName'
    @variable_name = variable_name
    @subscripts = []
    @variable = true
    @operand = true
    @precedence = 6
  end

  def name
    @variable_name
  end

  def to_s
    if subscripts.length > 0
      @variable_name.to_s + '(' + @subscripts.join(',') + ')'
    else
      @variable_name.to_s
    end
  end
end

# Scalar value (not a matrix)
class ScalarValue < Variable
  def initialize(variable_name)
    super
  end

  private

  def previous_is_array(stack)
    stack.size > 0 && stack[-1].class.to_s == 'Array'
  end

  def get_subscripts(stack)
    subscripts = stack.pop
    num_args = subscripts.length
    fail(Exception,
         'Variable expects subscripts, found empty parentheses') if
      num_args == 0
    subscripts
  end

  def eval_var_name(name, subscripts)
    name.to_s + '(' + subscripts.join(',') + ')'
  end

  public

  # return a single value
  def evaluate(interpreter, stack)
    if previous_is_array(stack)
      @subscripts = get_subscripts(stack)
      interpreter.check_subscripts(@variable_name, @subscripts)
      evaled_var_name = eval_var_name(@variable_name, @subscripts)
      interpreter.get_value(evaled_var_name)
    else
      interpreter.get_value(@variable_name.to_s)
    end
  end
end

# Scalar reference (not a matrix)
class ScalarReference < Variable
  def initialize(variable_value)
    super(variable_value.name)
  end

  # return a single value, a reference to this object
  def evaluate(interpreter, stack)
    if stack.size > 0 && stack[-1].class.to_s == 'Array'
      @subscripts = stack.pop
      num_args = @subscripts.length
      fail(BASICException,
           'Variable expects subscripts, found empty parentheses') if
        num_args == 0
      interpreter.check_subscripts(@variable_name, @subscripts)
    end
    self
  end
end

# Matrix with values
class Matrix
  attr_reader :dimensions

  def initialize(dimensions, values)
    @dimensions = dimensions
    @values = values
  end

  def clone
    Matrix.new(@dimensions.clone, @values.clone)
  end

  def matrix?
    true
  end

  def get_value_1(col)
    coords = make_coord(col)
    return @values[coords] if @values.key?(coords)
    0
  end

  def get_value_2(row, col)
    coords = make_coords(row, col)
    return @values[coords] if @values.key?(coords)
    0
  end

  def to_s
    'MATRIX: ' + @values.to_s
  end

  def print(printer, interpreter, carriage)
    case @dimensions.size
    when 0
      fail BASICException, 'Need dimensions in matrix'
    when 1
      print_1(printer, interpreter, carriage)
    when 2
      print_2(printer, interpreter, carriage)
    else
      fail BASICException, 'Too many dimensions in matrix'
    end
  end

  private

  def make_coord(c)
    '(' + c.to_s + ')'
  end

  def make_coords(r, c)
    '(' + r.to_s + ',' + c.to_s + ')'
  end

  def print_1(printer, interpreter, carriage)
    upper = @dimensions[0].to_i

    (1..upper).each do |index|
      value = get_value_1(index)
      value.print(printer)
      carriage.print(printer, interpreter)
    end
    printer.newline
    printer.newline
  end

  def print_2(printer, interpreter, carriage)
    upper_i = @dimensions[0].to_i
    upper_j = @dimensions[1].to_i

    (1..upper_i).each do |i|
      (1..upper_j).each do |j|
        value = get_value_2(i, j)
        value.print(printer)
        carriage.print(printer, interpreter)
      end
      printer.newline
    end
    printer.newline
  end
end

# Matrix value
class MatrixValue < Variable
  def initialize(variable_name)
    super
  end

  def evaluate(interpreter, _)
    dims = interpreter.get_dimensions(@variable_name)
    fail(BASICException, 'Variable has no dimensions') if dims.nil?
    values = {}
    if dims.size == 1
      n_cols = dims[0].to_i
      (1..n_cols).each do |col|
        coords = make_coord(col)
        var_name = @variable_name.to_s + coords
        values[coords] = interpreter.get_value(var_name)
      end
    end
    if dims.size == 2
      n_rows = dims[0].to_i
      n_cols = dims[1].to_i
      (1..n_rows).each do |row|
        (1..n_cols).each do |col|
          coords = make_coords(row, col)
          var_name = @variable_name.to_s + coords
          values[coords] = interpreter.get_value(var_name)
        end
      end
    end
    Matrix.new(dims, values)
  end
end

# Matrix reference
class MatrixReference < Variable
  def initialize(variable_value)
    super(variable_value.name)
  end

  def dimensions?
    @subscripts.size > 0
  end

  def dimensions
    @subscripts
  end

  # return a single value, a reference to this object
  def evaluate(interpreter, stack)
    if stack.size > 0 && stack[-1].class.to_s == 'Array'
      @subscripts = stack.pop
      num_args = @subscripts.length
      fail(BASICException,
           'Variable expects subscripts, found empty parentheses') if
        num_args == 0
      interpreter.check_subscripts(@variable_name, @subscripts)
    end
    self
  end
end

# Dimensions to a variable
class VariableDimension < Variable
  def initialize(variable_value)
    super(variable_value.name)
  end

  # return a single value, a reference to this object
  def evaluate(_, stack)
    if stack.size > 0 && stack[-1].class.to_s == 'Array'
      @subscripts = stack.pop
      num_args = @subscripts.length
      fail(BASICException,
           'Variable expects subscripts, found empty parentheses') if
        num_args == 0
    end
    self
  end
end

# A list (needed because it has precedence value)
class List < AbstractToken
  def initialize(parsed_expressions)
    super()
    @parsed_expressions = parsed_expressions
    @variable = true
    @precedence = 6
  end

  def list
    @parsed_expressions
  end

  def evaluate(interpreter, _)
    eval_scalar(interpreter, @parsed_expressions)
  end

  def to_s
    "[#{@parsed_expressions.join('] [')}]"
  end
end

# Scalar function (provides a scalar)
class Function < AbstractToken
  def initialize(text)
    super()
    @name = text
    @function = true
    @operand = true
    @precedence = 6
  end

  private

  def check_args(args)
    fail(BASICException, 'No arguments for function') if
      args.class.to_s != 'Array'
  end

  def check_value(value, type)
    fail(BASICException,
         "Argument #{x} #{x.class} not of type #{type.class}") if
      value.class.to_s != type
  end

  def check_arg_types(args, types)
    check_args(args)
    n_types = types.size
    n_args = args.size
    fail(BASICException,
         "Function #{@name} expects #{n_types} argument, found #{n_args}") if
      n_args != n_types
    (0..types.size - 1).each do |i|
      check_value(args[i], types[i])
    end
  end
end

# Function that expects scalar parameters
class AbstractScalarFunction < Function
  def initialize(text)
    super
  end

  def default_type
    ScalarValue
  end
end

# Function that expects matrix parameters
class AbstractMatrixFunction < Function
  def initialize(text)
    super
  end

  def default_type
    MatrixValue
  end
end

# function INT
class FunctionInt < AbstractScalarFunction
  def initialize(text)
    super
  end

  # return a single value
  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'INT requires single value') if args.size != 1
    check_arg_types(args, ['NumericConstant'])
    x = args[0]
    x.truncate
  end
end

# function RND
class FunctionRnd < AbstractScalarFunction
  def initialize(text)
    super
  end

  # return a single value
  def evaluate(interpreter, stack)
    args = stack.pop
    fail(BASICException, 'Zero or one argument required for RND()') unless
      args.size == 1 || args.size == 2
    x = NumericConstant.new(100) if args.size == 0
    if args.size == 1
      check_arg_types(args, ['NumericConstant'])
      x = args[0]
    end
    interpreter.rand(x)
  end
end

# function EXP
class FunctionExp < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for EXP()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].exp
  end
end

# function LOG
class FunctionLog < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for LOG()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].log
  end
end

# function ABS
class FunctionAbs < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for ABS()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].abs
  end
end

# function SQR
class FunctionSqr < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for SQR()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].sqrt
  end
end

# function SIN
class FunctionSin < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for SIN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].sin
  end
end

# function COS
class FunctionCos < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for COS()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].cos
  end
end

# function TAN
class FunctionTan < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for TAN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].tan
  end
end

# function ATN
class FunctionAtn < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for ATN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].atn
  end
end

# function SGN
class FunctionSgn < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for SGN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].sign
  end
end

# function TRN
class FunctionTrn < AbstractMatrixFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for TRN()') unless
      args.size == 1
    check_arg_types(args, ['Matrix'])
    matrix = args[0]
    dims = matrix.dimensions
    fail(BASICException, 'TRN requires matrix') unless dims.size == 2
    n_rows = dims[0].to_i
    n_cols = dims[1].to_i
    new_dims = [dims[1], dims[0]]
    new_values = {}
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        value = matrix.get_value_2(row, col)
        coords = make_coords(col, row)
        new_values[coords] = value
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function ZER
class FunctionZer < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One or two arguments required for ZER()') unless
      args.size == 1 || args.size == 2
    if args.size == 1
      check_arg_types(args, ['NumericConstant'])
      n_cols = args[0].to_i
      new_dims = [args[0]]
      new_values = {}
      value = NumericConstant.new(0)
      (1..n_cols).each do |col|
        coords = make_coord(col)
        new_values[coords] = value
      end
    end
    if args.size == 2
      check_arg_types(args, %w(NumericConstant NumericConstant))
      n_rows = args[0].to_i
      n_cols = args[1].to_i
      new_dims = [args[0], args[1]]
      new_values = {}
      value = NumericConstant.new(0)
      (1..n_rows).each do |row|
        (1..n_cols).each do |col|
          coords = make_coords(row, col)
          new_values[coords] = value
        end
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function CON
class FunctionCon < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One or two arguments required for CON()') unless
      args.size == 1 || args.size == 2
    if args.size == 1
      check_arg_types(args, ['NumericConstant'])
      n_cols = args[0].to_i
      new_dims = [args[0]]
      new_values = {}
      value = NumericConstant.new(1)
      (1..n_cols).each do |col|
        coords = make_coord(col)
        new_values[coords] = value
      end
    end
    if args.size == 2
      check_arg_types(args, %w(NumericConstant NumericConstant))
      n_rows = args[0].to_i
      n_cols = args[1].to_i
      new_dims = [args[0], args[1]]
      new_values = {}
      value = NumericConstant.new(1)
      (1..n_rows).each do |row|
        (1..n_cols).each do |col|
          coords = make_coords(row, col)
          new_values[coords] = value
        end
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function IDN
class FunctionIdn < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    check_arg_types(args, ['NumericConstant']) if args.size == 1
    check_arg_types(args, %w(NumericConstant NumericConstant)) if
      args.size == 2
    fail(BASICException, 'IDN requires square matrix') if
      args.size == 2 && args[1] != args[0]
    n_rows = args[0].to_i
    n_cols = args[0].to_i
    new_dims = [args[0], args[0]]
    new_values = {}
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        if col == row
          value = NumericConstant.new(1)
        else
          value = NumericConstant.new(0)
        end
        coords = make_coords(row, col)
        new_values[coords] = value
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function DET
class FunctionDet < AbstractMatrixFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for DET()') unless
      args.size == 1
    check_arg_types(args, ['Matrix'])
    determinant(args[0])
  end

  private

  def determinant(matrix)
    dims = matrix.dimensions
    fail(BASICException, 'DET requires matrix') unless dims.size == 2
    fail(BASICException, 'DET requires square matrix') if dims[1] != dims[0]
    if dims[0].to_i == 1
      det = matrix.get_value_2(1, 1)
    elsif dims[0].to_i == 2
      a = matrix.get_value_2(1, 1)
      b = matrix.get_value_2(1, 2)
      c = matrix.get_value_2(2, 1)
      d = matrix.get_value_2(2, 2)
      det = (a * d) - (b * c)
    else
      minus_one = NumericConstant.new(-1)
      sign = NumericConstant.new(1)
      det = NumericConstant.new(0)
      n_cols = dims[1].to_i
      # for each element in first row
      (1..n_cols).each do |col|
        v = matrix.get_value_2(1, col)
        # create submatrix
        subm = submatrix(matrix, 1, col)
        d = v * determinant(subm) * sign
        det += d
        sign *= minus_one
      end
    end
    det
  end

  def submatrix(matrix, exclude_row, exclude_col)
    dims = matrix.dimensions
    one = NumericConstant.new(1)
    new_dims = [dims[0] - one, dims[1] - one]
    n_rows = dims[0].to_i
    n_cols = dims[1].to_i
    new_values = {}
    new_row = 1
    (1..n_rows).each do |row|
      new_col = 1
      next if row == exclude_row
      (1..n_cols).each do |col|
        next if col == exclude_col
        value = matrix.get_value_2(row, col)
        coords = make_coords(new_row, new_col)
        new_values[coords] = value
        new_col += 1
      end
      new_row += 1
    end
    Matrix.new(new_dims, new_values)
  end
end

# function INV
class FunctionInv < AbstractMatrixFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for DET()') unless
      args.size == 1
    check_arg_types(args, ['Matrix'])
    dims = args[0].dimensions
    fail(BASICException, 'INV requires matrix') unless dims.size == 2
    fail(BASICException, 'INV requires square matrix') if dims[1] != dims[0]
    inverse(args[0])
  end

  private

  def inverse(matrix)
    dims = matrix.dimensions
    values = {}
    n_rows = dims[0].to_i
    n_cols = dims[1].to_i
    # set all values
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        value = matrix.get_value_2(row, col)
        coords = make_coords(row, col)
        values[coords] = value
      end
    end
    # create identity matrix
    inv_values = {}
    # set all values
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        coords = make_coords(row, col)
        inv_values[coords] = NumericConstant.new(0)
        inv_values[coords] = NumericConstant.new(1) if col == row
      end
    end
    # convert to upper triangular form
    (1..n_cols - 1).each do |col|
      (col + 1..n_rows).each do |row|
        denom_coords = make_coords(col, col)
        denominator = values[denom_coords]
        numer_coords = make_coords(row, col)
        numerator = values[numer_coords]
        factor = numerator / denominator
        # adjust values for this row
        (1..n_cols).each do |wcol|
          value_coords = make_coords(row, wcol)
          subtrahend = values[value_coords]
          minuend_coords = make_coords(col, wcol)
          minuend = values[minuend_coords]
          new_value = subtrahend - minuend * factor
          values[value_coords] = new_value
          # update inverse matrix
          subtrahend = inv_values[value_coords]
          minuend = inv_values[minuend_coords]
          new_value = subtrahend - minuend * factor<commit_msg>Simplify operations for matrix variables.<commit_after># Hold a variable name (not a reference or value)
class VariableName < AbstractToken
  def self.init?(text)
    /\A[A-Z]\d?\z/.match(text)
  end

  def initialize(text)
    super()
    fail(BASICException, "'#{text}' is not a variable name") unless
      VariableName.init?(text)
    @var_name = text
    @variable = true
    @operand = true
    @precedence = 6
  end

  def eql?(other)
    @var_name == other.to_s
  end

  def ==(other)
    @var_name == other.to_s
  end

  def hash
    @var_name.hash
  end

  def to_s
    @var_name
  end
end

# Hold a variable (name with possible subscripts and value)
class Variable < AbstractToken
  attr_reader :subscripts

  def initialize(variable_name)
    super()
    fail(BASICException, "'#{variable_name}' is not a variable name") if
      variable_name.class.to_s != 'VariableName'
    @variable_name = variable_name
    @subscripts = []
    @variable = true
    @operand = true
    @precedence = 6
  end

  def name
    @variable_name
  end

  def to_s
    if subscripts.length > 0
      @variable_name.to_s + '(' + @subscripts.join(',') + ')'
    else
      @variable_name.to_s
    end
  end
end

# Scalar value (not a matrix)
class ScalarValue < Variable
  def initialize(variable_name)
    super
  end

  private

  def previous_is_array(stack)
    stack.size > 0 && stack[-1].class.to_s == 'Array'
  end

  def get_subscripts(stack)
    subscripts = stack.pop
    num_args = subscripts.length
    fail(Exception,
         'Variable expects subscripts, found empty parentheses') if
      num_args == 0
    subscripts
  end

  def eval_var_name(name, subscripts)
    name.to_s + '(' + subscripts.join(',') + ')'
  end

  public

  # return a single value
  def evaluate(interpreter, stack)
    if previous_is_array(stack)
      @subscripts = get_subscripts(stack)
      interpreter.check_subscripts(@variable_name, @subscripts)
      evaled_var_name = eval_var_name(@variable_name, @subscripts)
      interpreter.get_value(evaled_var_name)
    else
      interpreter.get_value(@variable_name.to_s)
    end
  end
end

# Scalar reference (not a matrix)
class ScalarReference < Variable
  def initialize(variable_value)
    super(variable_value.name)
  end

  # return a single value, a reference to this object
  def evaluate(interpreter, stack)
    if stack.size > 0 && stack[-1].class.to_s == 'Array'
      @subscripts = stack.pop
      num_args = @subscripts.length
      fail(BASICException,
           'Variable expects subscripts, found empty parentheses') if
        num_args == 0
      interpreter.check_subscripts(@variable_name, @subscripts)
    end
    self
  end
end

# Matrix with values
class Matrix
  attr_reader :dimensions

  def initialize(dimensions, values)
    @dimensions = dimensions
    @values = values
  end

  def clone
    Matrix.new(@dimensions.clone, @values.clone)
  end

  def matrix?
    true
  end

  def values_2
    n_rows = @dimensions[0].to_i
    n_cols = @dimensions[1].to_i
    values = {}
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        value = get_value_2(row, col)
        coords = make_coords(row, col)
        values[coords] = value
      end
    end
    values
  end

  def get_value_1(col)
    coords = make_coord(col)
    return @values[coords] if @values.key?(coords)
    0
  end

  def get_value_2(row, col)
    coords = make_coords(row, col)
    return @values[coords] if @values.key?(coords)
    0
  end

  def to_s
    'MATRIX: ' + @values.to_s
  end

  def print(printer, interpreter, carriage)
    case @dimensions.size
    when 0
      fail BASICException, 'Need dimensions in matrix'
    when 1
      print_1(printer, interpreter, carriage)
    when 2
      print_2(printer, interpreter, carriage)
    else
      fail BASICException, 'Too many dimensions in matrix'
    end
  end

  private

  def make_coord(c)
    '(' + c.to_s + ')'
  end

  def make_coords(r, c)
    '(' + r.to_s + ',' + c.to_s + ')'
  end

  def print_1(printer, interpreter, carriage)
    n_cols = @dimensions[0].to_i

    (1..n_cols).each do |col|
      value = get_value_1(col)
      value.print(printer)
      carriage.print(printer, interpreter)
    end
    printer.newline
    printer.newline
  end

  def print_2(printer, interpreter, carriage)
    n_rows = @dimensions[0].to_i
    n_cols = @dimensions[1].to_i

    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        value = get_value_2(row, col)
        value.print(printer)
        carriage.print(printer, interpreter)
      end
      printer.newline
    end
    printer.newline
  end
end

# Matrix value
class MatrixValue < Variable
  def initialize(variable_name)
    super
  end

  def evaluate(interpreter, _)
    dims = interpreter.get_dimensions(@variable_name)
    fail(BASICException, 'Variable has no dimensions') if dims.nil?
    values = {}
    if dims.size == 1
      n_cols = dims[0].to_i
      values = evaluate_1(interpreter, n_cols)
    end
    if dims.size == 2
      n_rows = dims[0].to_i
      n_cols = dims[1].to_i
      values = evaluate_2(interpreter, n_rows, n_cols)
    end
    Matrix.new(dims, values)
  end

  private

  def evaluate_1(interpreter, n_cols)
    values = {}
    (1..n_cols).each do |col|
      coords = make_coord(col)
      var_name = @variable_name.to_s + coords
      values[coords] = interpreter.get_value(var_name)
    end
    values
  end

  def evaluate_2(interpreter, n_rows, n_cols)
    values = {}
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        coords = make_coords(row, col)
        var_name = @variable_name.to_s + coords
        values[coords] = interpreter.get_value(var_name)
      end
    end
    values
  end
end

# Matrix reference
class MatrixReference < Variable
  def initialize(variable_value)
    super(variable_value.name)
  end

  def dimensions?
    @subscripts.size > 0
  end

  def dimensions
    @subscripts
  end

  # return a single value, a reference to this object
  def evaluate(interpreter, stack)
    if stack.size > 0 && stack[-1].class.to_s == 'Array'
      @subscripts = stack.pop
      num_args = @subscripts.length
      fail(BASICException,
           'Variable expects subscripts, found empty parentheses') if
        num_args == 0
      interpreter.check_subscripts(@variable_name, @subscripts)
    end
    self
  end
end

# Dimensions to a variable
class VariableDimension < Variable
  def initialize(variable_value)
    super(variable_value.name)
  end

  # return a single value, a reference to this object
  def evaluate(_, stack)
    if stack.size > 0 && stack[-1].class.to_s == 'Array'
      @subscripts = stack.pop
      num_args = @subscripts.length
      fail(BASICException,
           'Variable expects subscripts, found empty parentheses') if
        num_args == 0
    end
    self
  end
end

# A list (needed because it has precedence value)
class List < AbstractToken
  def initialize(parsed_expressions)
    super()
    @parsed_expressions = parsed_expressions
    @variable = true
    @precedence = 6
  end

  def list
    @parsed_expressions
  end

  def evaluate(interpreter, _)
    eval_scalar(interpreter, @parsed_expressions)
  end

  def to_s
    "[#{@parsed_expressions.join('] [')}]"
  end
end

# Scalar function (provides a scalar)
class Function < AbstractToken
  def initialize(text)
    super()
    @name = text
    @function = true
    @operand = true
    @precedence = 6
  end

  private

  def check_args(args)
    fail(BASICException, 'No arguments for function') if
      args.class.to_s != 'Array'
  end

  def check_value(value, type)
    fail(BASICException,
         "Argument #{x} #{x.class} not of type #{type.class}") if
      value.class.to_s != type
  end

  def check_arg_types(args, types)
    check_args(args)
    n_types = types.size
    n_args = args.size
    fail(BASICException,
         "Function #{@name} expects #{n_types} argument, found #{n_args}") if
      n_args != n_types
    (0..types.size - 1).each do |i|
      check_value(args[i], types[i])
    end
  end
end

# Function that expects scalar parameters
class AbstractScalarFunction < Function
  def initialize(text)
    super
  end

  def default_type
    ScalarValue
  end
end

# Function that expects matrix parameters
class AbstractMatrixFunction < Function
  def initialize(text)
    super
  end

  def default_type
    MatrixValue
  end
end

# function INT
class FunctionInt < AbstractScalarFunction
  def initialize(text)
    super
  end

  # return a single value
  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'INT requires single value') if args.size != 1
    check_arg_types(args, ['NumericConstant'])
    x = args[0]
    x.truncate
  end
end

# function RND
class FunctionRnd < AbstractScalarFunction
  def initialize(text)
    super
  end

  # return a single value
  def evaluate(interpreter, stack)
    args = stack.pop
    fail(BASICException, 'Zero or one argument required for RND()') unless
      args.size == 1 || args.size == 2
    x = NumericConstant.new(100) if args.size == 0
    if args.size == 1
      check_arg_types(args, ['NumericConstant'])
      x = args[0]
    end
    interpreter.rand(x)
  end
end

# function EXP
class FunctionExp < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for EXP()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].exp
  end
end

# function LOG
class FunctionLog < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for LOG()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].log
  end
end

# function ABS
class FunctionAbs < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for ABS()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].abs
  end
end

# function SQR
class FunctionSqr < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for SQR()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].sqrt
  end
end

# function SIN
class FunctionSin < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for SIN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].sin
  end
end

# function COS
class FunctionCos < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for COS()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].cos
  end
end

# function TAN
class FunctionTan < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for TAN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].tan
  end
end

# function ATN
class FunctionAtn < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for ATN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].atn
  end
end

# function SGN
class FunctionSgn < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for SGN()') unless
      args.size == 1
    check_arg_types(args, ['NumericConstant'])
    args[0].sign
  end
end

# function TRN
class FunctionTrn < AbstractMatrixFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for TRN()') unless
      args.size == 1
    check_arg_types(args, ['Matrix'])
    matrix = args[0]
    dims = matrix.dimensions
    fail(BASICException, 'TRN requires matrix') unless dims.size == 2
    n_rows = dims[0].to_i
    n_cols = dims[1].to_i
    new_dims = [dims[1], dims[0]]
    new_values = {}
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        value = matrix.get_value_2(row, col)
        coords = make_coords(col, row)
        new_values[coords] = value
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function ZER
class FunctionZer < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One or two arguments required for ZER()') unless
      args.size == 1 || args.size == 2
    if args.size == 1
      check_arg_types(args, ['NumericConstant'])
      n_cols = args[0].to_i
      new_dims = [args[0]]
      new_values = {}
      value = NumericConstant.new(0)
      (1..n_cols).each do |col|
        coords = make_coord(col)
        new_values[coords] = value
      end
    end
    if args.size == 2
      check_arg_types(args, %w(NumericConstant NumericConstant))
      n_rows = args[0].to_i
      n_cols = args[1].to_i
      new_dims = [args[0], args[1]]
      new_values = {}
      value = NumericConstant.new(0)
      (1..n_rows).each do |row|
        (1..n_cols).each do |col|
          coords = make_coords(row, col)
          new_values[coords] = value
        end
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function CON
class FunctionCon < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One or two arguments required for CON()') unless
      args.size == 1 || args.size == 2
    if args.size == 1
      check_arg_types(args, ['NumericConstant'])
      n_cols = args[0].to_i
      new_dims = [args[0]]
      new_values = {}
      value = NumericConstant.new(1)
      (1..n_cols).each do |col|
        coords = make_coord(col)
        new_values[coords] = value
      end
    end
    if args.size == 2
      check_arg_types(args, %w(NumericConstant NumericConstant))
      n_rows = args[0].to_i
      n_cols = args[1].to_i
      new_dims = [args[0], args[1]]
      new_values = {}
      value = NumericConstant.new(1)
      (1..n_rows).each do |row|
        (1..n_cols).each do |col|
          coords = make_coords(row, col)
          new_values[coords] = value
        end
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function IDN
class FunctionIdn < AbstractScalarFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    check_arg_types(args, ['NumericConstant']) if args.size == 1
    check_arg_types(args, %w(NumericConstant NumericConstant)) if
      args.size == 2
    fail(BASICException, 'IDN requires square matrix') if
      args.size == 2 && args[1] != args[0]
    n_rows = args[0].to_i
    n_cols = args[0].to_i
    new_dims = [args[0], args[0]]
    new_values = {}
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        if col == row
          value = NumericConstant.new(1)
        else
          value = NumericConstant.new(0)
        end
        coords = make_coords(row, col)
        new_values[coords] = value
      end
    end
    Matrix.new(new_dims, new_values)
  end
end

# function DET
class FunctionDet < AbstractMatrixFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for DET()') unless
      args.size == 1
    check_arg_types(args, ['Matrix'])
    determinant(args[0])
  end

  private

  def determinant(matrix)
    dims = matrix.dimensions
    fail(BASICException, 'DET requires matrix') unless dims.size == 2
    fail(BASICException, 'DET requires square matrix') if dims[1] != dims[0]
    if dims[0].to_i == 1
      det = matrix.get_value_2(1, 1)
    elsif dims[0].to_i == 2
      a = matrix.get_value_2(1, 1)
      b = matrix.get_value_2(1, 2)
      c = matrix.get_value_2(2, 1)
      d = matrix.get_value_2(2, 2)
      det = (a * d) - (b * c)
    else
      minus_one = NumericConstant.new(-1)
      sign = NumericConstant.new(1)
      det = NumericConstant.new(0)
      n_cols = dims[1].to_i
      # for each element in first row
      (1..n_cols).each do |col|
        v = matrix.get_value_2(1, col)
        # create submatrix
        subm = submatrix(matrix, 1, col)
        d = v * determinant(subm) * sign
        det += d
        sign *= minus_one
      end
    end
    det
  end

  def submatrix(matrix, exclude_row, exclude_col)
    dims = matrix.dimensions
    one = NumericConstant.new(1)
    new_dims = [dims[0] - one, dims[1] - one]
    n_rows = dims[0].to_i
    n_cols = dims[1].to_i
    new_values = {}
    new_row = 1
    (1..n_rows).each do |row|
      new_col = 1
      next if row == exclude_row
      (1..n_cols).each do |col|
        next if col == exclude_col
        value = matrix.get_value_2(row, col)
        coords = make_coords(new_row, new_col)
        new_values[coords] = value
        new_col += 1
      end
      new_row += 1
    end
    Matrix.new(new_dims, new_values)
  end
end

# function INV
class FunctionInv < AbstractMatrixFunction
  def initialize(text)
    super
  end

  def evaluate(_, stack)
    args = stack.pop
    fail(BASICException, 'One argument required for DET()') unless
      args.size == 1
    check_arg_types(args, ['Matrix'])
    dims = args[0].dimensions
    fail(BASICException, 'INV requires matrix') unless dims.size == 2
    fail(BASICException, 'INV requires square matrix') if dims[1] != dims[0]
    inverse(args[0])
  end

  private

  def inverse(matrix)
    # set all values
    values = matrix.values_2

    dims = matrix.dimensions
    n_rows = dims[0].to_i
    n_cols = dims[1].to_i

    # create identity matrix
    inv_values = {}
    # set all values
    (1..n_rows).each do |row|
      (1..n_cols).each do |col|
        coords = make_coords(row, col)
        inv_values[coords] = NumericConstant.new(0)
        inv_values[coords] = NumericConstant.new(1) if col == row
      end
    end

    # convert to upper triangular form
    (1..n_cols - 1).each do |col|
      (col + 1..n_rows).each do |row|
        denom_coords = make_coords(col, col)
        denominator = values[denom_coords]
        numer_coords = make_coords(row, col)
        numerator = values[numer_coords]
        factor = numerator / denominator
        # adjust values for this row
        (1..n_cols).each do |wcol|
          value_coords = make_coords(row, wcol)
          subtrahend = values[value_coords]
          minuend_coords = make_coords(col, wcol)
          minuend = values[minuend_coords]
          new_value = subtrahend - minuend * factor
          values[value_coords] = new_value
          # update inverse matrix
          subtrahend = inv_values[value_coords]
          minuend = inv_values[minuend_coords]
          new_value = subtrahend - minuend * factor
{'fraction_non_alphanumeric': 0.10219361618284513, 'fraction_numerical': 0.010245632470773676, 'mean_word_length': 2.686779661016949, 'pattern_counts': {'":': 0, '<': 59, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '464013', 'n_tokens_mistral': 13046, 'n_tokens_neox': 12706, 'n_words': 3296}