Files
RubyAlgebra/lib/ruby_algebra/parser.rb
2026-04-13 15:05:52 +03:00

413 lines
12 KiB
Ruby

# frozen_string_literal: true
module RubyAlgebra
module Parser
# Токен выражения или команды
class Token
attr_reader :type
def initialize; end
end
# Знак сложения
class PlusToken < Token
def initialize
super()
@type = :plus
end
end
# Знак вычитания
class MinusToken < Token
def initialize
super()
@type = :minus
end
end
# Знак умножения
class MultiplyToken < Token
def initialize
super()
@type = :mult
end
end
# Знак деления
class DivideToken < Token
def initialize
super()
@type = :div
end
end
# Знак возведения в степень
class PowerToken < Token
def initialize
super()
@type = :pow
end
end
# Скобка
class ParenthesisToken < Token
attr_reader :closing
def initialize(closing)
super()
@type = :paren
@closing = closing
end
end
# Знак присвоения :=
class AssignmentToken < Token
def initialize
super()
@type = :assign
end
end
# Знак равенства =
class EqualsToken < Token
def initialize
super()
@type = :equals
end
end
# Запятая
class CommaToken < Token
def initialize
super()
@type = :comma
end
end
# Имя переменной или функции
class IdentifierToken < Token
attr_reader :symbol
def initialize(symbol)
super()
@type = :id
@symbol = symbol
end
def variable?
@symbol.match?(/^\p{Ll}/)
end
end
# Целое или дробное число
class NumberToken < Token
attr_reader :value
def initialize(value)
super()
@type = :num
@value = value
end
end
# Специальный токен, означающий конец ввода
class EndToken < Token
def initialize
super()
@type = :end
end
end
# Класс для разбора выражений на токены
class Tokenizer
attr_reader :lookahead
def initialize(expr)
@expr = expr
@i = 0
@lookahead = _next_token
end
def next_token
token = @lookahead
@lookahead = _next_token unless token.type == :end
token
end
def _next_token
puts '_next_token'
loop do
return EndToken.new unless @i < @expr.length
puts "i = #{@i}"
c = @expr[@i]
puts "c = #{c}"
if c.match?(/\p{Alpha}/)
puts 'letter'
j = @i + 1
j += 1 while j < @expr.length && @expr[j].match?(/\p{Alnum}/)
token = IdentifierToken.new(@expr[@i...j])
@i = j
return token
elsif c.match(%r{[+\-*/^():,=]})
puts 'operator'
token = nil
case c
when '+'
token = PlusToken.new
when '-'
token = MinusToken.new
when '*'
token = MultiplyToken.new
when '/'
token = DivideToken.new
when '^'
token = PowerToken.new
when '('
token = ParenthesisToken.new(false)
when ')'
token = ParenthesisToken.new(true)
when ','
token = CommaToken.new
when '='
token = EqualsToken.new
end
@i += 1
if c == ':' && @expr[@i] == '='
token = AssignmentToken.new
@i += 1
end
return token
elsif c.match?(/\p{Digit}|\./)
puts 'digit'
j = @i + 1
j += 1 while j < @expr.length && @expr[j].match?(/\p{Digit}|\./)
s = @expr[@i...j]
@i = j
puts "ssss: #{s}"
fl = s.count '.'
raise StandardError, 'malformed number' if fl > 1
return fl.zero? ? NumberToken.new(s.to_i) : NumberToken.new(s.to_f)
elsif c == ' '
puts 'whitespace'
@i += 1
else
puts 'unknown'
@i += 1
end
end
end
end
def self.parse_command(expr)
tokenizer = Tokenizer.new(expr)
n = tokenizer.lookahead
if n.type == :id && !n.variable?
left_hand_side = tokenizer.next_token.symbol
else
return DisplayCommand.new(parse_polynomial(tokenizer))
end
if tokenizer.lookahead.type == :assign
n = tokenizer.next_token
operand1 = tokenizer.lookahead
if operand1.type == :id && !operand1.variable?
if operand1.symbol == 'Diff'
tokenizer.next_token
n = tokenizer.next_token
unless n.type == :paren && n.closing == false
raise StandardError, "unexpected token #{n.type}, expected ("
end
n = tokenizer.lookahead
if n.type == :id && !n.variable?
tokenizer.next_token
target = n.symbol
else
target = parse_polynomial(tokenizer)
end
diff_variables = []
while tokenizer.lookahead.type == :comma
tokenizer.next_token
n = tokenizer.next_token
unless n.type == :id && n.variable?
raise StandardError, "unexpected token #{n.type}, expected variable"
end
diff_variables << n.symbol
end
n = tokenizer.next_token
unless n.type == :paren && n.closing == true
raise StandardError, "unexpected token #{n.type}, expected )"
end
unless tokenizer.next_token.type == :end
raise StandardError, "unexpected token at the end"
end
return AssignmentCommand.new(left_hand_side, :diff, target, diff_variables)
elsif operand1.symbol == 'Subs'
tokenizer.next_token
n = tokenizer.next_token
unless n.type == :paren && n.closing == false
raise StandardError, "unexpected token #{n.type}, expected ("
end
n = tokenizer.lookahead
if n.type == :id && !n.variable?
tokenizer.next_token
target = n.symbol
else
target = parse_polynomial(tokenizer)
end
substitutions = {}
while tokenizer.lookahead.type == :comma
tokenizer.next_token
n = tokenizer.next_token
unless n.type == :id && n.variable?
raise StandardError, "unexpected token #{n.type}, expected variable"
end
variable = n.symbol
unless tokenizer.next_token.type == :equals
raise StandardError, "unexpected token #{n.type}, expected ="
end
n = tokenizer.next_token
unless n.type == :num
raise StandardError, "unexpected token #{n.type}, expected number"
end
substitutions[variable] = n.value
end
n = tokenizer.next_token
unless n.type == :paren && n.closing == true
raise StandardError, "unexpected token #{n.type}, expected )"
end
unless tokenizer.next_token.type == :end
raise StandardError, "unexpected token at the end"
end
return AssignmentCommand.new(left_hand_side, :subs, target, substitutions)
else
tokenizer.next_token
operator = tokenizer.next_token
raise StandardError, "unexpected token #{operator.type}, expected +, -, * or /" unless [:plus, :minus, :mult, :div].include?(operator.type)
operand2 = tokenizer.next_token
unless ([:plus, :minus, :mult].include?(operator.type) && operand2.type == :id && !operand2.variable?) ||
([:mult, :div].include?(operator.type) && operand2.type == :num)
raise StandardError, 'unsupported operation or invalid syntax'
end
if operand2.type == :num
if operator.type == :mult
return AssignmentCommand.new(left_hand_side, :scale, operand1.symbol, operand2.value)
else
return AssignmentCommand.new(left_hand_side, :scale, operand1.symbol, 1.0 / operand2.value)
end
elsif operand2.type == :id
case operator.type
when :plus
return AssignmentCommand.new(left_hand_side, :add, operand1.symbol, operand2.symbol)
when :minus
return AssignmentCommand.new(left_hand_side, :sub, operand1.symbol, operand2.symbol)
when :mult
return AssignmentCommand.new(left_hand_side, :mult, operand1.symbol, operand2.symbol)
end
end
end
else
return AssignmentCommand.new(left_hand_side, :assign, parse_polynomial(tokenizer), nil)
end
elsif tokenizer.lookahead.type == :comma
n = tokenizer.next_token
second_lhs = tokenizer.next_token
unless second_lhs.type == :id && !second_lhs.variable?
raise StandardError, "unexpected token #{second_lhs.type}, expected polynomial variable"
end
unless tokenizer.next_token.type == :assign
raise StandardError, "unexpected token, expected :="
end
operand1 = tokenizer.next_token
unless operand1.type == :id && !operand1.variable?
raise StandardError, "unexpected token #{operand1.type}, expected polynomial variable"
end
unless tokenizer.next_token.type == :div
raise StandardError, "unexpected token, expected /"
end
operand2 = tokenizer.next_token
unless operand2.type == :id && !operand2.variable?
raise StandardError, "unexpected token #{operand2.type}, expected polynomial variable"
end
return AssignmentCommand.new([left_hand_side, second_lhs.symbol], :div, operand1.symbol)
elsif tokenizer.lookahead.type == :end
return DisplayCommand.new(left_hand_side)
else
raise StandardError, "unexpected token #{n.type}, expected ':=' or ','"
end
end
def self.parse_polynomial(tokenizer)
puts "parse_polynomial <lookahead #{tokenizer.lookahead.type}>"
terms = [parse_term(tokenizer)]
while [:plus, :minus].include?(tokenizer.lookahead.type)
sign = tokenizer.next_token.type == :plus ? +1 : -1
terms << parse_term(tokenizer) * sign
end
puts 'parse_polynomial end'
Polynomial.new(terms)
end
def self.parse_term(tokenizer)
puts "parse_term <lookahead #{tokenizer.lookahead.type}>"
n = tokenizer.lookahead
unless n.type == :id && n.variable? || n.type == :num
raise StandardError, "unexpected token #{n.type}, expected number or variable"
end
if n.type == :num
puts "eat coeff"
tokenizer.next_token
coeff = n.value
variables = {}
else
coeff = 1
v = parse_variable_factor(tokenizer)
variables = {v[0] => v[1]}
end
loop do
n = tokenizer.lookahead
puts "<lookahead #{n.type}>"
unless n.type == :mult || n.type == :id && n.variable?
break
end
v = parse_variable_factor(tokenizer)
variables[v[0]] = v[1]
end
puts 'parse_term end'
pp [coeff, variables]
Term.new(coeff, variables)
end
def self.parse_variable_factor(tokenizer)
puts "parse_term <lookahead #{tokenizer.lookahead.type}>"
n = tokenizer.lookahead
tokenizer.next_token
puts 'eat variable'
if n.type == :mult
n = tokenizer.next_token
puts 'eat *'
end
unless n.type == :id && n.variable?
raise StandardError, "unexpected token #{n.type}, expected variable"
end
variable = n.symbol
if tokenizer.lookahead.type == :pow
tokenizer.next_token
puts 'eat ^'
unless tokenizer.lookahead.type == :num
raise StandardError, "unexpected token #{n.type}, expected number"
end
power = tokenizer.next_token.value
puts 'eat exponent'
else
power = 1
end
puts 'parse_variable_factor end'
pp [variable, power]
[variable, power]
end
end
end