rpl.rb/lib/rpl/interpreter.rb
2022-02-16 16:21:37 +01:00

217 lines
5.3 KiB
Ruby

# frozen_string_literal: true
require 'bigdecimal/math'
require 'rpl/dictionary'
class Interpreter
include BigMath
attr_reader :stack,
:dictionary,
:version
attr_accessor :precision
def initialize( stack = [], dictionary = Rpl::Lang::Dictionary.new )
@version = 0.1
@precision = default_precision
@dictionary = dictionary
@stack = stack
populate_dictionary if @dictionary.words.empty?
end
def default_precision
12
end
def parse( input )
is_numeric = lambda do |elt|
begin
!Float(elt).nil?
rescue ArgumentError
begin
!Integer(elt).nil?
rescue ArgumentError
false
end
end
end
splitted_input = input.split(' ')
# 2-passes:
# 1. regroup strings and programs
opened_programs = 0
closed_programs = 0
string_delimiters = 0
name_delimiters = 0
regrouping = false
regrouped_input = []
splitted_input.each do |elt|
if elt[0] == '«'
opened_programs += 1
elt.gsub!( '«', '« ') if elt.length > 1 && elt[1] != ' '
end
string_delimiters += 1 if elt[0] == '"' && elt.length > 1
name_delimiters += 1 if elt[0] == "'" && elt.length > 1
elt = "#{regrouped_input.pop} #{elt}".strip if regrouping
regrouped_input << elt
if elt[-1] == '»'
closed_programs += 1
elt.gsub!( '»', ' »') if elt.length > 1 && elt[-2] != ' '
end
string_delimiters += 1 if elt[-1] == '"'
name_delimiters += 1 if elt[-1] == "'"
regrouping = string_delimiters.odd? || name_delimiters.odd? || (opened_programs > closed_programs )
end
# 2. parse
# TODO: parse ∞, <NaN> as numerics
parsed_tree = []
regrouped_input.each do |elt|
parsed_entry = { value: elt }
parsed_entry[:type] = case elt[0]
when '«'
:program
when '"'
:string
when "'"
:name # TODO: check for forbidden space
else
if is_numeric.call( elt )
:numeric
else
:word
end
end
if %I[string name].include?( parsed_entry[:type] )
parsed_entry[:value] = parsed_entry[:value][1..-2]
elsif parsed_entry[:type] == :program
parsed_entry[:value] = parsed_entry[:value][2..-3]
elsif parsed_entry[:type] == :numeric
parsed_entry[:base] = 10 # TODO: parse others possible bases 0x...
begin
parsed_entry[:value] = Float( parsed_entry[:value] )
parsed_entry[:value] = parsed_entry[:value].to_i if (parsed_entry[:value] % 1).zero? && elt.index('.').nil?
rescue ArgumentError
parsed_entry[:value] = Integer( parsed_entry[:value] )
end
parsed_entry[:value] = BigDecimal( parsed_entry[:value], @precision )
end
parsed_tree << parsed_entry
end
parsed_tree
end
def run( input )
@dictionary.add_local_vars_layer
parse( input.to_s ).each do |elt|
case elt[:type]
when :word
break if %w[break quit exit].include?( elt[:value] )
command = @dictionary.lookup( elt[:value] )
if command.nil?
# if there isn't a command by that name then it's a name
elt[:type] = :name
@stack << elt
elsif command.is_a?( Proc )
command.call
else
run( command[:value] )
end
else
@stack << elt
end
end
@dictionary.remove_local_vars_layer
# superfluous but feels nice
@stack
end
def stack_extract( needs )
raise ArgumentError, 'Not enough elements' if @stack.size < needs.size
needs.each_with_index do |need, index|
stack_index = (index + 1) * -1
raise ArgumentError, "Type Error, needed #{need} got #{@stack[stack_index]}" unless need == :any || need.include?( @stack[stack_index][:type] )
end
args = []
needs.size.times do
args << @stack.pop
end
args
end
def stringify( elt )
case elt[:type]
when :numeric
prefix = case elt[:base]
when 2
'0b'
when 8
'0o'
when 10
''
when 16
'0x'
else
"0#{elt[:base]}_"
end
if elt[:value].infinite?
suffix = elt[:value].infinite?.positive? ? '∞' : '-∞'
elsif elt[:value].nan?
suffix = '<NaN>'
else
suffix = if elt[:value].to_i == elt[:value]
elt[:value].to_i
else
elt[:value].to_s('F')
end
suffix = elt[:value].to_s( elt[:base] ) unless elt[:base] == 10
end
"#{prefix}#{suffix}"
when :list
"{ #{elt[:value].map { |e| stringify( e ) }.join(' ')} }"
when :program
"« #{elt[:value]} »"
when :string
"\"#{elt[:value]}\""
when :name
"'#{elt[:value]}'"
else
elt[:value]
end
end
def infer_resulting_base( numerics )
10 if numerics.length.zero?
numerics.last[:base]
end
end