From b76c04aacd0c0ae25186331210ab2396566d3d01 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Fri, 26 Nov 2021 17:00:10 -0700 Subject: [PATCH] ruby.2 step 4 & 5 --- impls/ruby.2/core.rb | 182 +++++++++++++++++++++++++++++++++ impls/ruby.2/env.rb | 16 ++- impls/ruby.2/errors.rb | 1 + impls/ruby.2/printer.rb | 14 +-- impls/ruby.2/reader.rb | 19 ++-- impls/ruby.2/step4_if_fn_do.rb | 162 +++++++++++++++++++++++++++++ impls/ruby.2/step5_tco.rb | 175 +++++++++++++++++++++++++++++++ impls/ruby.2/types.rb | 63 +++++++++--- 8 files changed, 598 insertions(+), 34 deletions(-) create mode 100644 impls/ruby.2/core.rb create mode 100644 impls/ruby.2/step4_if_fn_do.rb create mode 100644 impls/ruby.2/step5_tco.rb diff --git a/impls/ruby.2/core.rb b/impls/ruby.2/core.rb new file mode 100644 index 00000000..e87181d2 --- /dev/null +++ b/impls/ruby.2/core.rb @@ -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 diff --git a/impls/ruby.2/env.rb b/impls/ruby.2/env.rb index f59e0b27..57ee82ee 100644 --- a/impls/ruby.2/env.rb +++ b/impls/ruby.2/env.rb @@ -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) diff --git a/impls/ruby.2/errors.rb b/impls/ruby.2/errors.rb index 69eeebed..12884013 100644 --- a/impls/ruby.2/errors.rb +++ b/impls/ruby.2/errors.rb @@ -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 diff --git a/impls/ruby.2/printer.rb b/impls/ruby.2/printer.rb index c8b43b00..142e197b 100644 --- a/impls/ruby.2/printer.rb +++ b/impls/ruby.2/printer.rb @@ -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 diff --git a/impls/ruby.2/reader.rb b/impls/ruby.2/reader.rb index e4bbb715..1b48b7f8 100644 --- a/impls/ruby.2/reader.rb +++ b/impls/ruby.2/reader.rb @@ -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 diff --git a/impls/ruby.2/step4_if_fn_do.rb b/impls/ruby.2/step4_if_fn_do.rb new file mode 100644 index 00000000..b477005d --- /dev/null +++ b/impls/ruby.2/step4_if_fn_do.rb @@ -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 + + diff --git a/impls/ruby.2/step5_tco.rb b/impls/ruby.2/step5_tco.rb new file mode 100644 index 00000000..21cfee17 --- /dev/null +++ b/impls/ruby.2/step5_tco.rb @@ -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 + + diff --git a/impls/ruby.2/types.rb b/impls/ruby.2/types.rb index 6b2a3513..99ce70e4 100644 --- a/impls/ruby.2/types.rb +++ b/impls/ruby.2/types.rb @@ -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 + "#" + 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 + "#" + end + + def is_mal_fn? + true end end end