1
1
mirror of https://github.com/kanaka/mal.git synced 2024-07-14 17:10:30 +03:00

ruby.2 step 4 & 5

This commit is contained in:
Ryan Cook 2021-11-26 17:00:10 -07:00 committed by Joel Martin
parent 229f30fdf1
commit b76c04aacd
8 changed files with 598 additions and 34 deletions

182
impls/ruby.2/core.rb Normal file
View File

@ -0,0 +1,182 @@
require_relative "types"
module Mal
module Core
extend self
def ns
{
Types::Symbol.for("+") => Types::Builtin.new { |a, b| a + b },
Types::Symbol.for("-") => Types::Builtin.new { |a, b| a - b },
Types::Symbol.for("*") => Types::Builtin.new { |a, b| a * b },
Types::Symbol.for("/") => Types::Builtin.new { |a, b| a / b },
Types::Symbol.for("prn") => Types::Builtin.new do |mal|
val =
if mal.any?
mal.first
else
Types::Nil.instance
end
puts Mal.pr_str(val, true)
Types::Nil.instance
end,
Types::Symbol.for("list") => Types::Builtin.new do |mal|
list = Types::List.new
mal.each { |m| list << m }
list
end,
Types::Symbol.for("list?") => Types::Builtin.new do |mal|
is_list =
if mal.any?
Types::List === mal.first
else
false
end
is_list ? Types::True.instance : Types::False.instance
end,
Types::Symbol.for("empty?") => Types::Builtin.new do |mal|
is_empty =
if mal.any?
case mal.first
when Types::List, Types::Vector
mal.first.empty?
else
Types::True.instance
end
else
Types::True.instance
end
is_empty ? Types::True.instance : Types::False.instance
end,
Types::Symbol.for("count") => Types::Builtin.new do |mal|
count =
if mal.any?
case mal.first
when Types::List, Types::Vector
mal.first.size
else
0
end
else
0
end
Types::Number.new(count)
end,
Types::Symbol.for("=") => Types::Builtin.new do |mal|
a, b = mal
if a.nil? || b.nil?
Types::False.instance
else
if a == b
Types::True.instance
else
Types::False.instance
end
end
end,
Types::Symbol.for("<") => Types::Builtin.new do |mal|
a, b = mal
if a.nil? || b.nil?
Types::False.instance
else
if a.is_a?(Types::Number) && b.is_a?(Types::Number)
if a.value < b.value
Types::True.instance
else
Types::False.instance
end
else
Types::False.instance
end
end
end,
Types::Symbol.for("<=") => Types::Builtin.new do |mal|
a, b = mal
if a.nil? || b.nil?
Types::False.instance
else
if a.is_a?(Types::Number) && b.is_a?(Types::Number)
if a.value <= b.value
Types::True.instance
else
Types::False.instance
end
else
Types::False.instance
end
end
end,
Types::Symbol.for(">") => Types::Builtin.new do |mal|
a, b = mal
if a.nil? || b.nil?
Types::False.instance
else
if a.is_a?(Types::Number) && b.is_a?(Types::Number)
if a.value > b.value
Types::True.instance
else
Types::False.instance
end
else
Types::False.instance
end
end
end,
Types::Symbol.for(">=") => Types::Builtin.new do |mal|
a, b = mal
if a.nil? || b.nil?
Types::False.instance
else
if a.is_a?(Types::Number) && b.is_a?(Types::Number)
if a.value >= b.value
Types::True.instance
else
Types::False.instance
end
else
Types::False.instance
end
end
end,
Types::Symbol.for("pr-str") => Types::Builtin.new do |mal|
Types::String.new(mal.map { |m| Mal.pr_str(m, true) }.join(" "))
end,
Types::Symbol.for("str") => Types::Builtin.new do |mal|
Types::String.new(mal.map { |m| Mal.pr_str(m, false) }.join(""))
end,
Types::Symbol.for("prn") => Types::Builtin.new do |mal|
puts mal.map { |m| Mal.pr_str(m, true) }.join(" ")
Types::Nil.instance
end,
Types::Symbol.for("println") => Types::Builtin.new do |mal|
puts mal.map { |m| Mal.pr_str(m, false) }.join(" ")
Types::Nil.instance
end
}
end
end
end

View File

@ -3,9 +3,23 @@ require_relative "types"
module Mal
class Env
def initialize(outer = nil)
def initialize(outer = nil, binds = Types::List.new, exprs = Types::List.new)
@outer = outer
@data = {}
spread_next = false
binds.each_with_index do |b, i|
if b.value == "&"
spread_next = true
else
if spread_next
set(b, exprs[(i - 1)..(exprs.length - 1)] || Types::Nil.instance)
break
else
set(b, exprs[i] || Types::Nil.instance)
end
end
end
end
def set(k, v)

View File

@ -3,6 +3,7 @@ module Mal
class TypeError < ::TypeError; end
class InvalidHashmapKeyError < TypeError; end
class InvalidIfExpressionError < TypeError; end
class InvalidLetBindingsError < TypeError; end
class InvalidReaderPositionError < Error; end
class InvalidTypeError < TypeError; end

View File

@ -12,26 +12,22 @@ module Mal
"[#{mal.map { |m| pr_str(m, print_readably) }.join(" ")}]"
when Types::Hashmap
"{#{mal.map { |k, v| [pr_str(k, print_readably), pr_str(v, print_readably)].join(" ") }.join(" ")}}"
when Types::Nil
"nil"
when Types::True
"true"
when Types::False
"false"
when Types::Keyword
if print_readably
pr_str_keyword(mal)
else
mal.value.to_s
mal.inspect
end
when Types::String
if print_readably
pr_str_string(mal)
else
mal.value.to_s
mal.inspect
end
when Types::Atom
mal.value.to_s
mal.inspect
when Types::Callable
mal.inspect
else
raise InvalidTypeError
end

View File

@ -8,8 +8,10 @@ module Mal
def read_atom(reader)
case reader.peek
when /\A"/
when /\A"(?:\\.|[^\\"])*"\z/
read_string(reader)
when /\A"/
raise UnbalancedStringError
when /\A:/
read_keyword(reader)
when "nil"
@ -161,13 +163,13 @@ module Mal
def read_string(reader)
raw_value = reader.next.dup
value = raw_value[1...-1]
substitute_escaped_chars!(value)
if raw_value.length <= 1 || raw_value[-1] != '"'
raise UnbalancedStringError
end
value = raw_value[1...-1]
substitute_escaped_chars!(value)
Types::String.new(value)
end
@ -257,13 +259,6 @@ module Mal
private
def substitute_escaped_chars!(string_or_keyword)
string_or_keyword.gsub!('\"','"')
string_or_keyword.gsub!('\n',"\n")
if string_or_keyword.count('\\') % 2 != 0
raise UnbalancedEscapingError
end
string_or_keyword.gsub!('\\\\','\\')
string_or_keyword.gsub!(/\\./, {"\\\\" => "\\", "\\n" => "\n", "\\\"" => '"'})
end
end

View File

@ -0,0 +1,162 @@
require "readline"
require_relative "core"
require_relative "env"
require_relative "errors"
require_relative "printer"
require_relative "reader"
module Mal
extend self
def boot_repl!
@repl_env = Env.new
Core.ns.each do |k, v|
@repl_env.set(k, v)
end
Mal.rep("(def! not (fn* (a) (if a false true)))")
end
def READ(input)
read_str(input)
end
def EVAL(ast, environment)
if Types::List === ast && ast.size > 0
case ast.first
when Types::Symbol.for("def!")
_, sym, val = ast
environment.set(sym, EVAL(val, environment))
when Types::Symbol.for("let*")
e = Env.new(environment)
_, bindings, val = ast
unless Types::List === bindings || Types::Vector === bindings
raise InvalidLetBindingsError
end
until bindings.empty?
k, v = bindings.shift(2)
raise InvalidLetBindingsError if k.nil?
v = Types::Nil.instance if v.nil?
e.set(k, EVAL(v, e))
end
if !val.nil?
EVAL(val, e)
else
Types::Nil.instance
end
when Types::Symbol.for("do")
_, *values = ast
if !values.nil?
evaluated = Types::List.new
values.each do |v|
evaluated << EVAL(v, environment)
end
evaluated.last
else
Types::Nil.instance
end
when Types::Symbol.for("if")
_, condition, when_true, when_false = ast
case EVAL(condition, environment)
when Types::False.instance, Types::Nil.instance
if !when_false.nil?
EVAL(when_false, environment)
else
Types::Nil.instance
end
else
if !when_true.nil?
EVAL(when_true, environment)
else
raise InvalidIfExpressionError
end
end
when Types::Symbol.for("fn*")
_, binds, to_eval = ast
Types::Function.new do |exprs|
EVAL(to_eval, Env.new(environment, binds, exprs))
end
else
evaluated = eval_ast(ast, environment)
maybe_callable = evaluated.first
if maybe_callable.respond_to?(:call)
maybe_callable.call(evaluated[1..])
else
raise NotCallableError, "Error! #{PRINT(maybe_callable)} is not callable."
end
end
elsif Types::List === ast && ast.size == 0
ast
else
eval_ast(ast, environment)
end
end
def PRINT(input)
pr_str(input, true)
end
def rep(input)
PRINT(EVAL(READ(input), @repl_env))
rescue InvalidHashmapKeyError => e
"Error! Hashmap keys can only be strings or keywords."
rescue NotCallableError => e
e.message
rescue SymbolNotFoundError => e
e.message
rescue UnbalancedEscapingError => e
"Error! Detected unbalanced escaping. Check for matching '\\'."
rescue UnbalancedHashmapError => e
"Error! Detected unbalanced list. Check for matching '}'."
rescue UnbalancedListError => e
"Error! Detected unbalanced list. Check for matching ')'."
rescue UnbalancedStringError => e
"Error! Detected unbalanced string. Check for matching '\"'."
rescue UnbalancedVectorError => e
"Error! Detected unbalanced list. Check for matching ']'."
end
def eval_ast(mal, environment)
case mal
when Types::Symbol
environment.get(mal)
when Types::List
list = Types::List.new
mal.each { |i| list << EVAL(i, environment) }
list
when Types::Vector
vec = Types::Vector.new
mal.each { |i| vec << EVAL(i, environment) }
vec
when Types::Hashmap
hashmap = Types::Hashmap.new
mal.each { |k, v| hashmap[k] = EVAL(v, environment) }
hashmap
else
mal
end
end
end
Mal.boot_repl!
while input = Readline.readline("user> ")
puts Mal.rep(input)
end
puts

175
impls/ruby.2/step5_tco.rb Normal file
View File

@ -0,0 +1,175 @@
require "readline"
require_relative "core"
require_relative "env"
require_relative "errors"
require_relative "printer"
require_relative "reader"
module Mal
extend self
def boot_repl!
@repl_env = Env.new
Core.ns.each do |k, v|
@repl_env.set(k, v)
end
Mal.rep("(def! not (fn* (a) (if a false true)))")
end
def READ(input)
read_str(input)
end
def EVAL(ast, environment)
loop do
if Types::List === ast && ast.size > 0
case ast.first
when Types::Symbol.for("def!")
_, sym, val = ast
return environment.set(sym, EVAL(val, environment))
when Types::Symbol.for("let*")
e = Env.new(environment)
_, bindings, val = ast
unless Types::List === bindings || Types::Vector === bindings
raise InvalidLetBindingsError
end
until bindings.empty?
k, v = bindings.shift(2)
raise InvalidLetBindingsError if k.nil?
v = Types::Nil.instance if v.nil?
e.set(k, EVAL(v, e))
end
if !val.nil?
# Continue loop
ast = val
environment = e
else
return Types::Nil.instance
end
when Types::Symbol.for("do")
_, *values = ast
if !values.nil? && values.any?
values[0...-1].each do |v|
EVAL(v, environment)
end
# Continue loop
ast = values.last
else
return Types::Nil.instance
end
when Types::Symbol.for("if")
_, condition, when_true, when_false = ast
case EVAL(condition, environment)
when Types::False.instance, Types::Nil.instance
if !when_false.nil?
# Continue loop
ast = when_false
else
return Types::Nil.instance
end
else
if !when_true.nil?
# Continue loop
ast = when_true
else
raise InvalidIfExpressionError
end
end
when Types::Symbol.for("fn*")
_, binds, to_eval = ast
return Types::Function.new(to_eval, binds, environment) do |exprs|
EVAL(to_eval, Env.new(environment, binds, exprs))
end
else
evaluated = eval_ast(ast, environment)
maybe_callable = evaluated.first
if maybe_callable.respond_to?(:call) && !maybe_callable.is_mal_fn?
return maybe_callable.call(evaluated[1..])
elsif maybe_callable.respond_to?(:call) && maybe_callable.is_mal_fn?
# Continue loop
ast = maybe_callable.ast
environment = Env.new(
maybe_callable.env,
maybe_callable.params,
evaluated[1..],
)
else
raise NotCallableError, "Error! #{PRINT(maybe_callable)} is not callable."
end
end
elsif Types::List === ast && ast.size == 0
return ast
else
return eval_ast(ast, environment)
end
end
end
def PRINT(input)
pr_str(input, true)
end
def rep(input)
PRINT(EVAL(READ(input), @repl_env))
rescue InvalidHashmapKeyError => e
"Error! Hashmap keys can only be strings or keywords."
rescue NotCallableError => e
e.message
rescue SymbolNotFoundError => e
e.message
rescue UnbalancedEscapingError => e
"Error! Detected unbalanced escaping. Check for matching '\\'."
rescue UnbalancedHashmapError => e
"Error! Detected unbalanced list. Check for matching '}'."
rescue UnbalancedListError => e
"Error! Detected unbalanced list. Check for matching ')'."
rescue UnbalancedStringError => e
"Error! Detected unbalanced string. Check for matching '\"'."
rescue UnbalancedVectorError => e
"Error! Detected unbalanced list. Check for matching ']'."
end
def eval_ast(mal, environment)
case mal
when Types::Symbol
environment.get(mal)
when Types::List
list = Types::List.new
mal.each { |i| list << EVAL(i, environment) }
list
when Types::Vector
vec = Types::Vector.new
mal.each { |i| vec << EVAL(i, environment) }
vec
when Types::Hashmap
hashmap = Types::Hashmap.new
mal.each { |k, v| hashmap[k] = EVAL(v, environment) }
hashmap
else
mal
end
end
end
Mal.boot_repl!
while input = Readline.readline("user> ")
puts Mal.rep(input)
end
puts

View File

@ -4,7 +4,11 @@ module Mal
class Vector < ::Array; end
class Hashmap < ::Hash; end
class Atom < ::Struct.new(:value); end
class Atom < ::Struct.new(:value)
def inspect
value.to_s
end
end
class Keyword < Atom
def self.for(value)
@ -52,31 +56,66 @@ module Mal
class Nil < Atom
def self.instance
@_instance ||= new
@_instance ||= new(nil)
end
def initialize
@value = nil
def inspect
"nil"
end
end
class True < Atom
def self.instance
@_instance ||= new
end
def initialize
@value = true
@_instance ||= new(true)
end
end
class False < Atom
def self.instance
@_instance ||= new
@_instance ||= new(false)
end
end
class Callable
def initialize(&block)
@fn = block
end
def initialize
@value = false
def call(args)
@fn.call(args)
end
def inspect
raise NotImplementedError
end
end
class Builtin < Callable
def inspect
"#<builtin>"
end
def is_mal_fn?
false
end
end
class Function < Callable
attr_reader :ast, :params, :env
def initialize(ast, params, env, &block)
@ast = ast
@params = params
@env = env
@fn = block
end
def inspect
"#<function>"
end
def is_mal_fn?
true
end
end
end