From 516e56a6d7d512b82385dbe8106949348d93adeb Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Tue, 30 Nov 2021 20:32:59 -0700 Subject: [PATCH] ruby.2 step 6 (and fixes for comments and fn creation) --- impls/ruby.2/core.rb | 52 +++++++- impls/ruby.2/errors.rb | 3 + impls/ruby.2/printer.rb | 8 +- impls/ruby.2/reader.rb | 8 +- impls/ruby.2/step1_read_print.rb | 2 + impls/ruby.2/step4_if_fn_do.rb | 2 +- impls/ruby.2/step6_file.rb | 206 +++++++++++++++++++++++++++++++ impls/ruby.2/types.rb | 30 +++-- 8 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 impls/ruby.2/step6_file.rb diff --git a/impls/ruby.2/core.rb b/impls/ruby.2/core.rb index e87181d2..761660f3 100644 --- a/impls/ruby.2/core.rb +++ b/impls/ruby.2/core.rb @@ -6,7 +6,10 @@ module Mal def ns { - Types::Symbol.for("+") => Types::Builtin.new { |a, b| a + b }, + Types::Symbol.for("+") => Types::Builtin.new do |a, b| + a + b + end, + 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 }, @@ -175,6 +178,53 @@ module Mal Types::Symbol.for("println") => Types::Builtin.new do |mal| puts mal.map { |m| Mal.pr_str(m, false) }.join(" ") Types::Nil.instance + end, + + Types::Symbol.for("read-string") => Types::Builtin.new do |mal| + if mal.first.is_a?(Types::String) + Mal.read_str(mal.first.value) + else + Types::Nil.instance + end + end, + + Types::Symbol.for("slurp") => Types::Builtin.new do |mal| + if mal.first.is_a?(Types::String) + if File.exist?(mal.first.value) + Types::String.new(File.read(mal.first.value)) + else + raise FileNotFoundError, mal.first.value + end + else + Types::Nil.instance + end + end, + + Types::Symbol.for("atom") => Types::Builtin.new do |mal| + Types::Atom.new(mal.first) + end, + + Types::Symbol.for("atom?") => Types::Builtin.new do |mal| + mal.first.is_a?(Types::Atom) ? Types::True.instance : Types::False.instance + end, + + Types::Symbol.for("deref") => Types::Builtin.new do |mal| + mal.first.is_a?(Types::Atom) ? mal.first.value : Types::Nil.instance + end, + + Types::Symbol.for("reset!") => Types::Builtin.new do |mal| + atom, value = mal + + if value.nil? + value = Types::Nil.instance + end + + atom.value = value + end, + + Types::Symbol.for("swap!") => Types::Builtin.new do |mal| + atom, fn, *args = mal + atom.value = fn.call(Types::List.new([atom.value, *args])) end } end diff --git a/impls/ruby.2/errors.rb b/impls/ruby.2/errors.rb index 12884013..bb78d42b 100644 --- a/impls/ruby.2/errors.rb +++ b/impls/ruby.2/errors.rb @@ -2,6 +2,9 @@ module Mal class Error < ::StandardError; end class TypeError < ::TypeError; end + class SkipCommentError < Error; end + class FileNotFoundError < Error; end + class InvalidHashmapKeyError < TypeError; end class InvalidIfExpressionError < TypeError; end class InvalidLetBindingsError < TypeError; end diff --git a/impls/ruby.2/printer.rb b/impls/ruby.2/printer.rb index 142e197b..1b658829 100644 --- a/impls/ruby.2/printer.rb +++ b/impls/ruby.2/printer.rb @@ -16,17 +16,17 @@ module Mal if print_readably pr_str_keyword(mal) else - mal.inspect + ":#{mal.value}" end when Types::String if print_readably pr_str_string(mal) else - mal.inspect + mal.value end when Types::Atom - mal.inspect - when Types::Callable + "(atom #{pr_str(mal.value, print_readably)})" + when Types::Base, Types::Callable mal.inspect else raise InvalidTypeError diff --git a/impls/ruby.2/reader.rb b/impls/ruby.2/reader.rb index 1b48b7f8..38ba0322 100644 --- a/impls/ruby.2/reader.rb +++ b/impls/ruby.2/reader.rb @@ -22,6 +22,8 @@ module Mal read_false(reader) when /\A-?\d+(\.\d+)?/ read_number(reader) + when /\A;/ + raise SkipCommentError else read_symbol(reader) end @@ -157,7 +159,9 @@ module Mal end def read_str(input) - read_form(Reader.new(tokenize(input))) + tokenized = tokenize(input) + raise SkipCommentError if tokenized.empty? + read_form(Reader.new(tokenized)) end def read_string(reader) @@ -222,7 +226,7 @@ module Mal def tokenize(input) input.scan(TOKEN_REGEX).flatten.each_with_object([]) do |token, tokens| - if token != "" + if token != "" && !token.start_with?(";") tokens << token end end diff --git a/impls/ruby.2/step1_read_print.rb b/impls/ruby.2/step1_read_print.rb index d160d8f5..7cd24c5b 100644 --- a/impls/ruby.2/step1_read_print.rb +++ b/impls/ruby.2/step1_read_print.rb @@ -33,6 +33,8 @@ module Mal "Error! Detected unbalanced string. Check for matching '\"'." rescue UnbalancedVectorError => e "Error! Detected unbalanced list. Check for matching ']'." + rescue SkipCommentError + nil end end diff --git a/impls/ruby.2/step4_if_fn_do.rb b/impls/ruby.2/step4_if_fn_do.rb index b477005d..b833c163 100644 --- a/impls/ruby.2/step4_if_fn_do.rb +++ b/impls/ruby.2/step4_if_fn_do.rb @@ -85,7 +85,7 @@ module Mal when Types::Symbol.for("fn*") _, binds, to_eval = ast - Types::Function.new do |exprs| + Types::Function.new(to_eval, binds, environment) do |exprs| EVAL(to_eval, Env.new(environment, binds, exprs)) end else diff --git a/impls/ruby.2/step6_file.rb b/impls/ruby.2/step6_file.rb new file mode 100644 index 00000000..6f4098d6 --- /dev/null +++ b/impls/ruby.2/step6_file.rb @@ -0,0 +1,206 @@ +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 + + @repl_env.set( + Types::Symbol.for("eval"), + + Types::Builtin.new do |mal| + Mal.EVAL(mal.first, @repl_env) + end + ) + + Mal.rep("(def! not (fn* (a) (if a false true)))") + Mal.rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))") + Mal.rep("(def! *ARGV* (list))") if !run_application? + end + + def run_application? + ARGV.any? + end + + def run! + Mal.rep("(def! *ARGV* (list #{ARGV[1..].map(&:inspect).join(" ")}))") + Mal.rep("(load-file #{ARGV.first.inspect})") + 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| + exprs = + if exprs.is_a?(Types::List) + exprs + else + Types::List.new([*exprs]) + end + + 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? + # Continue loop + ast = maybe_callable.ast + environment = Env.new( + maybe_callable.env, + maybe_callable.params, + evaluated[1..], + ) + elsif maybe_callable.respond_to?(:call) && !maybe_callable.is_mal_fn? + return maybe_callable.call(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 ']'." + rescue SkipCommentError + nil + 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! + +if Mal.run_application? + Mal.run! +else + while input = Readline.readline("user> ") + val = Mal.rep(input) + puts val unless val.nil? + end + + puts +end diff --git a/impls/ruby.2/types.rb b/impls/ruby.2/types.rb index 99ce70e4..0ac67974 100644 --- a/impls/ruby.2/types.rb +++ b/impls/ruby.2/types.rb @@ -4,13 +4,21 @@ module Mal class Vector < ::Array; end class Hashmap < ::Hash; end - class Atom < ::Struct.new(:value) + class Base < ::Struct.new(:value) def inspect - value.to_s + value.inspect end end - class Keyword < Atom + class String < Base; end + + class Atom < Base + def inspect + "Atom<#{value.inspect}>" + end + end + + class Keyword < Base def self.for(value) @_keywords ||= {} @@ -22,7 +30,7 @@ module Mal end end - class Number < Atom + class Number < Base def +(other) self.class.new(value + other.value) end @@ -40,9 +48,7 @@ module Mal end end - class String < Atom; end - - class Symbol < Atom + class Symbol < Base def self.for(value) @_symbols ||= {} @@ -52,9 +58,13 @@ module Mal @_symbols[value] = new(value) end end + + def inspect + value + end end - class Nil < Atom + class Nil < Base def self.instance @_instance ||= new(nil) end @@ -64,13 +74,13 @@ module Mal end end - class True < Atom + class True < Base def self.instance @_instance ||= new(true) end end - class False < Atom + class False < Base def self.instance @_instance ||= new(false) end