├── .ruby-version ├── exe └── ruspea ├── .rspec ├── lib ├── ruspea │ ├── version.rb │ ├── error │ │ ├── syntax.rb │ │ └── execution.rb │ ├── lisp │ │ ├── quote.rb │ │ ├── car.rb │ │ ├── cdr.rb │ │ ├── cons.rb │ │ ├── atom.rb │ │ ├── errors.rb │ │ ├── eq.rb │ │ ├── cond.rb │ │ └── lambda.rb │ ├── core │ │ ├── symbol.rb │ │ ├── predicates.rb │ │ ├── casting.rb │ │ ├── function.rb │ │ └── environment.rb │ └── evaluator.rb ├── ruspea_lang.rb ├── example.rb ├── ruspea.rb ├── example.rsp └── language │ └── standard.rsp ├── spec ├── ruspea_spec.rb ├── spec_helper.rb └── ruspea │ ├── core │ ├── symbol_spec.rb │ ├── function_spec.rb │ └── environment_spec.rb │ ├── lisp │ ├── car_spec.rb │ ├── cdr_spec.rb │ ├── quote_spec.rb │ ├── cons_spec.rb │ ├── cond_spec.rb │ ├── lambda_spec.rb │ ├── atom_spec.rb │ └── eq_spec.rb │ └── evaluator_spec.rb ├── Rakefile ├── bin ├── setup └── console ├── Gemfile ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── ruspea.gemspec ├── Gemfile.lock └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /exe/ruspea: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "ruspea" 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/ruspea/version.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/ruspea/error/syntax.rb: -------------------------------------------------------------------------------- 1 | module Ruspea::Error 2 | class Syntax < StandardError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/ruspea/error/execution.rb: -------------------------------------------------------------------------------- 1 | module Ruspea::Error 2 | class Execution < StandardError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/ruspea_lang.rb: -------------------------------------------------------------------------------- 1 | require_relative "./ruspea" 2 | module RuspeaLang 3 | # Nothing to do here. 4 | end 5 | -------------------------------------------------------------------------------- /spec/ruspea_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Ruspea do 2 | it "has a version number" do 3 | expect(Ruspea::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in ruspea.gemspec 4 | gemspec 5 | 6 | gem "pry-byebug" 7 | gem "simplecov", require: false 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /lib/example.rb: -------------------------------------------------------------------------------- 1 | require "ruspea" 2 | 3 | code = <<~c 4 | (def plus1 5 | (fn [num] (+ num 1))) 6 | (plus1 10) 7 | c 8 | 9 | eleven = Ruspea::Code.new.run(code).last 10 | puts eleven 11 | -------------------------------------------------------------------------------- /lib/ruspea.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "zeitwerk" 3 | 4 | module Ruspea 5 | def self.root 6 | Pathname.new __dir__ 7 | end 8 | end 9 | 10 | loader = Zeitwerk::Loader.for_gem 11 | loader.ignore("#{__dir__}/example.rb") 12 | loader.ignore("#{__dir__}/ruspea_lang.rb") 13 | loader.setup 14 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/quote.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Quote 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, _) 7 | raise arg_type_error(args) if !list?(args) 8 | check_args(args, 1) 9 | args.head 10 | end 11 | 12 | def eval_args? 13 | false 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/car.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Car 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, _) 7 | raise arg_type_error(args) if !list?(args) 8 | raise arg_type_error(args.head) if !list?(args.head) 9 | check_args(args, 1) 10 | args.head.head 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/cdr.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Cdr 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, _) 7 | raise arg_type_error(args) if !list?(args) 8 | raise arg_type_error(args.head) if !list?(args.head) 9 | check_args(args, 1) 10 | args.head.tail 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "ruspea" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 3.0 6 | before_install: gem install bundler 7 | before_script: 8 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 9 | - chmod +x ./cc-test-reporter 10 | - ./cc-test-reporter before-build 11 | after_script: 12 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 13 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/cons.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Cons 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, _) 7 | raise arg_type_error(args) if !list?(args) 8 | check_args(args, 2) 9 | raise arg_type_error(args.tail.head) if !list?(args.tail.head) 10 | expr, list = args.head, args.tail.head 11 | list.cons(expr) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/example.rsp: -------------------------------------------------------------------------------- 1 | (def zerify 2 | (fn [n] 3 | (cond 4 | ((= n 0) 5 | (puts "-- end --")) 6 | (true 7 | (puts n) 8 | (puts "--") 9 | (cond 10 | ((> n 10) (puts "not yet...")) 11 | (true (puts "almost there")) 12 | ) 13 | (zerify (- n 1)))))) 14 | 15 | (def fib 16 | (fn [n] 17 | (cond 18 | ((= n 0) n) 19 | ((= n 1) n) 20 | (true 21 | (+ 22 | (fib (- n 1)) 23 | (fib (- n 2))))))) 24 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/atom.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Atom 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, _) 7 | raise arg_type_error(args) if !list?(args) 8 | check_args(args, 1) 9 | atom?(args.head) 10 | end 11 | 12 | private 13 | 14 | def atom?(expr) 15 | return sym?(expr) || 16 | list?(expr) && expr.empty? || 17 | bool?(expr) || 18 | numeric?(expr) || 19 | expr.is_a?(String) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/errors.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | module Lisp::Errors 3 | def arg_type_error(given) 4 | given ||= "'()" 5 | Error::Execution.new <<~ERR 6 | Argument should be a list, received #{given} instead 7 | ERR 8 | end 9 | 10 | def check_args(args, expected) 11 | if args.empty? || args.size != expected 12 | raise Error::Execution.new <<~ERR 13 | Wrong number of arguments: given #{args.size}, expected #{expected} 14 | ERR 15 | end 16 | end 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/ruspea/core/symbol.rb: -------------------------------------------------------------------------------- 1 | module Ruspea::Core 2 | class Symbol 3 | attr_reader :label 4 | 5 | def initialize(label) 6 | @label = String(label) 7 | end 8 | 9 | def ==(other) 10 | return false if self.class != other.class 11 | label == other.label 12 | end 13 | 14 | alias eql? == 15 | 16 | # Todo: use MurMurHash here? 17 | def hash 18 | label.hash 19 | end 20 | 21 | def to_s 22 | label 23 | end 24 | 25 | def inspect 26 | "'#{label}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require :default 3 | require "ruspea" 4 | 5 | begin 6 | require "simplecov" 7 | SimpleCov.start do 8 | add_filter /spec/ 9 | end 10 | rescue LoadError 11 | end 12 | 13 | RSpec.configure do |config| 14 | # Enable flags like --only-failures and --next-failure 15 | config.example_status_persistence_file_path = ".rspec_status" 16 | 17 | # Disable RSpec exposing methods globally on `Module` and `main` 18 | config.disable_monkey_patching! 19 | 20 | config.expect_with :rspec do |c| 21 | c.syntax = :expect 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/ruspea/core/predicates.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | module Core::Predicates 3 | def list?(*exprs) 4 | exprs.all? { |expr| expr.respond_to?(:head) && expr.respond_to?(:tail) } 5 | end 6 | 7 | def sym?(*exprs) 8 | exprs.all? { |expr| expr.is_a?(Core::Symbol) } 9 | end 10 | 11 | def numeric?(*exprs) 12 | exprs.all? { |expr| expr.is_a?(Numeric) } 13 | end 14 | 15 | def bool?(*exprs) 16 | exprs.all? { |expr| expr.is_a?(TrueClass) || expr.is_a?(FalseClass) } 17 | end 18 | 19 | def string?(*exprs) 20 | exprs.all? { |expr| expr.is_a?(String) } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/ruspea/core/casting.rb: -------------------------------------------------------------------------------- 1 | require "immutable/list" 2 | require "immutable/hash" 3 | 4 | module Ruspea 5 | module Core::Casting 6 | include Core::Predicates 7 | 8 | def Symbol(label) 9 | Core::Symbol.new(label.to_s) 10 | end 11 | 12 | def List(*elements) 13 | Immutable::List[*elements] 14 | end 15 | 16 | def Scope(thing) 17 | return thing if thing.is_a?(Immutable::Hash) 18 | 19 | thing = Hash[thing] if !thing.is_a?(Hash) 20 | if thing.keys.any? { |k| !sym?(k) } 21 | thing = Hash[thing.map { |k, v| [Symbol(k), v] }] 22 | end 23 | Immutable::Hash.new(Hash[thing]) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/eq.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Eq 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, _) 7 | raise arg_type_error(args) if !list?(args) 8 | check_args(args, 2) 9 | lhs, rhs = args.head, args.tail.head 10 | return lhs == rhs if comparable?(lhs, rhs) 11 | return true if empty_list?(lhs, rhs) 12 | false 13 | end 14 | 15 | private 16 | 17 | def comparable?(lhs, rhs) 18 | sym?(lhs, rhs) || numeric?(lhs, rhs) || bool?(lhs, rhs) 19 | end 20 | 21 | def empty_list?(lhs, rhs) 22 | list?(lhs, rhs) && lhs.empty? && rhs.empty? 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/ruspea/core/symbol_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Core::Symbol do 3 | subject(:symbol) { described_class.new "lol" } 4 | 5 | context "equality" do 6 | it "knows when two symbols are equal" do 7 | expect(symbol == described_class.new("lol")).to eq true 8 | expect(symbol.eql? described_class.new("lol")).to eq true 9 | end 10 | end 11 | 12 | describe "#hash" do 13 | it "relies on label for hash-keying" do 14 | expect(symbol.hash).to eq "lol".hash 15 | end 16 | end 17 | 18 | describe "#inspect" do 19 | it "returns the quote representation for the symbol" do 20 | expect(symbol.inspect).to eq "'lol" 21 | end 22 | end 23 | 24 | describe "#to_s" do 25 | it "returns the label for the symbol" do 26 | expect(symbol.to_s).to eq "lol" 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/cond.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Cond 3 | include Core::Predicates 4 | include Lisp::Errors 5 | EVALUATOR = Evaluator.new 6 | 7 | def call(args, env, evaluator: EVALUATOR) 8 | raise arg_type_error(args) if !list?(args) 9 | return nil if ! clause = truthy_clause(args, env, evaluator) 10 | clause.tail.reduce(nil) { |_, expr| evaluator.eval(expr, env) } 11 | end 12 | 13 | private 14 | 15 | def non_list_clause_error(position) 16 | Error::Execution.new <<~ERR 17 | A non-list clause was found in position: #{position} 18 | ERR 19 | end 20 | 21 | def find(list, counter = 0, &blk) 22 | return nil if list.empty? 23 | return list.head if yield(list.head, counter) 24 | find(list.tail, counter + 1, &blk) 25 | end 26 | 27 | def truthy_clause(clauses, env, evaluator) 28 | find(clauses) { |clause, idx| 29 | raise non_list_clause_error(idx + 1) if !list?(clause) 30 | first_expr = evaluator.eval(clause.head, env) 31 | first_expr != false && first_expr != nil 32 | } 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/car_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Car do 3 | include Core::Casting 4 | 5 | let(:ctx) { {} } 6 | subject(:car) { described_class.new } 7 | 8 | describe "#call" do 9 | it "raises if parameter is not a list" do 10 | expect { car.call(1, ctx) }.to raise_error( 11 | Error::Execution, 12 | /Argument should be a list, received 1 instead/ 13 | ) 14 | end 15 | 16 | it "raises if wrong arity is used" do 17 | expect { car.call(List(), ctx) }.to raise_error( 18 | Error::Execution, 19 | /Argument should be a list/ 20 | ) 21 | 22 | expect { car.call(List(List(1), 1), ctx) }.to raise_error( 23 | Error::Execution, 24 | /Wrong number of arguments: given 2, expected 1/ 25 | ) 26 | end 27 | 28 | it "returns the first element on a list" do 29 | expect(car.call(List(List(1, 2)), ctx)).to eq 1 30 | end 31 | 32 | it "returns nil if the list empty" do 33 | expect(car.call(List(List()), ctx)).to eq nil 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/cdr_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Cdr do 3 | include Core::Casting 4 | 5 | let(:ctx) { {} } 6 | subject(:cdr) { described_class.new } 7 | 8 | describe "#call" do 9 | it "raises if parameter is not a list" do 10 | expect { cdr.call(1, ctx) }.to raise_error( 11 | Error::Execution, 12 | /Argument should be a list, received 1 instead/ 13 | ) 14 | end 15 | 16 | it "raises if wrong arity is used" do 17 | expect { cdr.call(List(), ctx) }.to raise_error( 18 | Error::Execution, 19 | /Argument should be a list/ 20 | ) 21 | 22 | expect { cdr.call(List(List(1), 1), ctx) }.to raise_error( 23 | Error::Execution, 24 | /Wrong number of arguments: given 2, expected 1/ 25 | ) 26 | end 27 | 28 | it "returns the rest of a list" do 29 | expect(cdr.call(List(List(1, 2)), ctx)).to eq List(2) 30 | end 31 | 32 | it "returns nil if the list empty" do 33 | expect(cdr.call(List(List()), ctx)).to eq List() 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/ruspea/core/function.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Core::Function 3 | EVALUATOR = Evaluator.new 4 | 5 | attr_reader :arity 6 | 7 | def initialize(parameters, body, closure) 8 | @parameters = parameters 9 | @arity = parameters.size 10 | @body = body 11 | @closure = closure 12 | end 13 | 14 | def call(args, invocation_env, evaluator: EVALUATOR) 15 | raise arguments_error(args.size) if args.size != arity 16 | 17 | execution_env = closure 18 | .push(invocation_env) 19 | .push(bind(args)) 20 | 21 | body.reduce(nil) { |_, expr| 22 | evaluator.eval(expr, execution_env) 23 | } 24 | end 25 | 26 | private 27 | 28 | attr_reader :closure, :body, :parameters 29 | 30 | def arguments_error(passed) 31 | Error::Execution.new <<~ERR 32 | Wrong number of args: #{passed} passed, #{arity} expected. 33 | ERR 34 | end 35 | 36 | def bind(args) 37 | parameters.each_with_index.reduce(Core::Environment.new) { |env, (param, idx)| 38 | env.tap { |e| e[param] = args.at(idx) } 39 | } 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ricardo Valeriano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/quote_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Quote do 3 | include Core::Casting 4 | 5 | let(:ctx) { {} } 6 | subject(:quote) { described_class.new } 7 | 8 | describe "#call" do 9 | it "returns the expr without evaluating it" do 10 | expect(quote.call(List(List(1, 2, 3)), ctx)).to eq List(1, 2, 3) 11 | expect(quote.call(List(Symbol("a")), ctx)).to eq Symbol("a") 12 | expect(quote.call(List("lol"), ctx)).to eq "lol" 13 | end 14 | 15 | it "raises if parameter is not a list" do 16 | expect { quote.call(1, ctx) }.to raise_error( 17 | Error::Execution, 18 | /Argument should be a list, received 1 instead/ 19 | ) 20 | end 21 | 22 | it "raises if wrong arity is used" do 23 | expect { quote.call(List(), ctx) }.to raise_error( 24 | Error::Execution, 25 | /Wrong number of arguments: given 0, expected 1/ 26 | ) 27 | 28 | expect { quote.call(List(1, 2), ctx) }.to raise_error( 29 | Error::Execution, 30 | /Wrong number of arguments: given 2, expected 1/ 31 | ) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/ruspea/lisp/lambda.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Lisp::Lambda 3 | include Core::Predicates 4 | include Lisp::Errors 5 | 6 | def call(args, ctx) 7 | raise arg_type_error(args) if !list?(args) 8 | raise params_required_error if args.empty? 9 | raise params_required_error if !list?(args.head) 10 | if non_symbol = find(args.head) { |arg| !sym?(arg) } 11 | raise non_symbol_param_error(non_symbol) 12 | end 13 | 14 | Core::Function.new(args.head, args.tail.head, ctx) 15 | end 16 | 17 | def eval_args? 18 | false 19 | end 20 | 21 | private 22 | 23 | def find(list, &blk) 24 | return list.head if yield(list.head) 25 | return nil if list.empty? 26 | find(list.tail, &blk) 27 | end 28 | 29 | def params_required_error 30 | Error::Execution.new <<~ERR 31 | A lambda needs the parameters list. 32 | It might be empty, you can try: (lambda ()) 33 | ERR 34 | end 35 | 36 | def non_symbol_param_error(non_symbol) 37 | Error::Execution.new <<~ERR 38 | Invalid parameter identifier. 39 | The element #{non_symbol} should be a Symbol 40 | ERR 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/ruspea/evaluator.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | class Evaluator 3 | include Core::Predicates 4 | 5 | def eval(expr, env = Core::Environment.new) 6 | return expr if prim?(expr) 7 | return sym(expr, env) if sym?(expr) 8 | return list(expr, env) if list?(expr) 9 | end 10 | 11 | private 12 | 13 | def sym(expr, env) 14 | env[expr] 15 | end 16 | 17 | def list(expr, env) 18 | fun = 19 | if list?(expr.head) # allow for inline invocation 20 | list(expr.head, env) 21 | else 22 | as_callable(expr.head, env) 23 | end 24 | 25 | eval_args = fun.respond_to?(:eval_args?) ? fun.eval_args? : true 26 | args = 27 | if eval_args 28 | expr.tail.map { |e| self.eval(e, env) } 29 | else 30 | expr.tail 31 | end 32 | fun.call(args, env) 33 | end 34 | 35 | def as_callable(to_call, env) 36 | env[to_call].tap { |fun| 37 | raise not_callable_error(fun) if !fun.respond_to?(:call) 38 | } 39 | end 40 | 41 | def not_callable_error(label) 42 | Error::Execution.new <<~ERR 43 | Unable to treat #{label} as a callable thing 44 | ERR 45 | end 46 | 47 | def prim?(expr) 48 | string?(expr) || 49 | numeric?(expr) || 50 | bool?(expr) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/cons_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Cons do 3 | include Core::Casting 4 | 5 | let(:ctx) { {} } 6 | subject(:cons) { described_class.new } 7 | 8 | describe "#call" do 9 | it "raises if parameter is not a list" do 10 | expect { cons.call(1, ctx) }.to raise_error( 11 | Error::Execution, 12 | /Argument should be a list, received 1 instead/ 13 | ) 14 | end 15 | 16 | it "raises if second parameter is not a list" do 17 | expect { cons.call(List(1, 2), ctx) }.to raise_error( 18 | Error::Execution, 19 | /Argument should be a list, received 2 instead/ 20 | ) 21 | end 22 | 23 | it "raises if wrong arity is used" do 24 | expect { cons.call(List(1), ctx) }.to raise_error( 25 | Error::Execution, 26 | /Wrong number of arguments: given 1, expected 2/ 27 | ) 28 | 29 | expect { cons.call(List(1, List(1), 1), ctx) }.to raise_error( 30 | Error::Execution, 31 | /Wrong number of arguments: given 3, expected 2/ 32 | ) 33 | end 34 | 35 | it "prepends an element to a list" do 36 | expect(cons.call(List(1, List(2, 3)), ctx)).to eq List(1, 2, 3) 37 | expect(cons.call(List(List(1, 2), List(3, 4)), ctx)).to eq List(List(1, 2), 3, 4) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/cond_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Cond do 3 | include Core::Casting 4 | 5 | let(:env) { Core::Environment.new } 6 | subject(:cond) { described_class.new } 7 | 8 | describe "#call" do 9 | it "raises if parameter is not a list" do 10 | expect { cond.call(1, env) }.to raise_error( 11 | Error::Execution, 12 | /Argument should be a list, received 1 instead/ 13 | ) 14 | end 15 | 16 | it "raises if a clause list is empty" do 17 | expect { cond.call(List(List(false, 1), 1), env) }.to raise_error( 18 | Error::Execution, 19 | /A non-list clause was found in position: 2/ 20 | ) 21 | end 22 | 23 | it "evaluates all expressions on a list if first element is truthy" do 24 | env["eq"] = Lisp::Eq.new 25 | 26 | expect(cond.call(List(List(true, 1, 2), List(true, 2)), env)).to eq 2 27 | expect(cond.call(List(List(false, 1), List(1, 420)), env)).to eq 420 28 | expect( 29 | cond.call( 30 | List( 31 | List(false, 1), 32 | List(Symbol("eq"), 1, 1, 420), 33 | ), env 34 | ) 35 | ).to eq 420 36 | end 37 | 38 | it "returns nil if there is no list with truthy first element" do 39 | expect(cond.call(List(List(false), List(false)), env)).to eq nil 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/language/standard.rsp: -------------------------------------------------------------------------------- 1 | (def puts 2 | (fn [str] 3 | (. 4 | (:: Kernel) puts str))) 5 | 6 | (def cons 7 | (fn [el list] 8 | (. list cons el))) 9 | 10 | (def head 11 | (fn [list] 12 | (. list head))) 13 | 14 | (def tail 15 | (fn [list] 16 | (. list tail))) 17 | 18 | (def empty? 19 | (fn [list] 20 | (. list empty?))) 21 | 22 | (def take 23 | (fn [num list] 24 | (. list take num))) 25 | 26 | (def = 27 | (fn [lhs rhs] 28 | (cond 29 | ((. lhs == rhs) true) 30 | (true false)))) 31 | 32 | (def != 33 | (fn [lhs rhs] 34 | (cond 35 | ((. lhs != rhs) true) 36 | (true false)))) 37 | 38 | (def > 39 | (fn [lhs rhs] 40 | (cond 41 | ((. lhs > rhs) true) 42 | (true false)))) 43 | 44 | (def < 45 | (fn [lhs rhs] 46 | (cond 47 | ((. lhs > rhs) true) 48 | (true false)))) 49 | 50 | (def + 51 | (fn [lhs rhs] 52 | (. lhs + rhs))) 53 | 54 | (def - 55 | (fn [lhs rhs] 56 | (. lhs - rhs))) 57 | 58 | (def / 59 | (fn [lhs rhs] 60 | (. lhs / rhs))) 61 | 62 | (def * 63 | (fn [lhs rhs] 64 | (. lhs * rhs))) 65 | 66 | (def read 67 | (fn [code] 68 | (def reader (. (:: Ruspea::Interpreter::Reader) new)) 69 | (def code_and_forms (. reader call code)) 70 | (. code_and_forms "[]" 1))) 71 | 72 | (def load 73 | (fn [path] 74 | (puts (. "reading from " << path)) 75 | (eval 76 | (read (. (:: File) read path)) 77 | (%ctx %ctx)))) 78 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/lambda_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Lambda do 3 | include Core::Casting 4 | 5 | let(:env) { Core::Environment.new } 6 | subject(:lbd) { described_class.new } 7 | 8 | describe ".call" do 9 | it "raises if parameter is not a list" do 10 | expect { lbd.call(1, env) }.to raise_error( 11 | Error::Execution, 12 | /Argument should be a list, received 1 instead/ 13 | ) 14 | end 15 | 16 | it "raises if no arguments list is passed" do 17 | expect { lbd.call(List(), env) }.to raise_error( 18 | Error::Execution, 19 | /A lambda needs the parameters list/ 20 | ) 21 | 22 | expect { lbd.call(List(1), env) }.to raise_error( 23 | Error::Execution, 24 | /A lambda needs the parameters list/ 25 | ) 26 | end 27 | 28 | it "raises if non symbols are passed in the args list" do 29 | expect { lbd.call(List(List(Symbol("a"), 1)), env) }.to raise_error( 30 | Error::Execution, 31 | /The element 1 should be a Symbol/ 32 | ) 33 | end 34 | 35 | it "returns a callable" do 36 | expect(lbd.call(List(List()), env).respond_to?(:call)).to eq true 37 | end 38 | 39 | it "the callable built has the right arity" do 40 | expect(lbd.call(List(List()), env).arity).to eq 0 41 | expect(lbd.call(List(List(Symbol("a"))), env).arity).to eq 1 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ruspea.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/ruspea/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "ruspea_lang" 5 | spec.version = Ruspea::VERSION 6 | spec.authors = ["Ricardo Valeriano"] 7 | spec.email = ["mister.sourcerer@gmail.com"] 8 | 9 | spec.summary = %q{A full featured lisp to be used as a Ruby Library (written in Ruby)} 10 | spec.description = %q{A full featured lisp to be used as a Ruby Library (written in Ruby)} 11 | spec.homepage = "https://github.com/mistersourcerer/ruspea" 12 | spec.license = "MIT" 13 | spec.required_ruby_version = Gem::Requirement.new(">= 3.0.1") 14 | 15 | # Specify which files should be added to the gem when it is released. 16 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 17 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 18 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "bundler", "~> 2.2" 25 | spec.add_development_dependency "rake", "~> 13.0" 26 | spec.add_development_dependency "rspec", "~> 3.10" 27 | 28 | spec.add_dependency "fiddle" 29 | spec.add_dependency "immutable-ruby" 30 | spec.add_dependency "logger" 31 | spec.add_dependency "ostruct" 32 | spec.add_dependency "zeitwerk" 33 | end 34 | -------------------------------------------------------------------------------- /spec/ruspea/core/function_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Core::Function do 3 | include Core::Casting 4 | 5 | let(:params) { List("a", "b") } 6 | let(:body) { List(1) } 7 | let(:env) { Core::Environment.new } 8 | 9 | subject(:fun) { described_class.new(params, body, env) } 10 | 11 | describe "#arity" do 12 | it "knows it's arity given the params list" do 13 | expect(fun.arity).to eq 2 14 | end 15 | end 16 | 17 | describe "#call" do 18 | it "raises if wrong number of args is passed" do 19 | expect { fun.call(List(), env) }.to raise_error( 20 | Error::Execution, 21 | /Wrong number of args: 0 passed, 2 expected/ 22 | ) 23 | 24 | expect { fun.call(List(1), env) }.to raise_error( 25 | Error::Execution, 26 | /Wrong number of args: 1 passed, 2 expected/ 27 | ) 28 | 29 | expect { fun.call(List(1, 2, 3), env) }.to raise_error( 30 | Error::Execution, 31 | /Wrong number of args: 3 passed, 2 expected/ 32 | ) 33 | end 34 | 35 | it "uses context to evaluate the body" do 36 | expect(fun.call(List(4, 20), env)).to eq 1 37 | end 38 | 39 | it "binds the arguments for passed to the function" do 40 | body = List(Symbol("a")) 41 | f = described_class.new(params, body, env) 42 | expect(f.call(List(4, 20), env)).to eq 4 43 | 44 | body = List(Symbol("b")) 45 | f = described_class.new(params, body, env) 46 | expect(f.call(List(4, 20), env)).to eq 20 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/atom_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Atom do 3 | include Core::Casting 4 | 5 | let(:ctx) { {} } 6 | subject(:atom) { described_class.new } 7 | 8 | describe "#call" do 9 | it "raises if parameter is not a list" do 10 | expect { atom.call(1, ctx) }.to raise_error( 11 | Error::Execution, 12 | /Argument should be a list, received 1 instead/ 13 | ) 14 | end 15 | 16 | it "raises if wrong arity is used" do 17 | expect { atom.call(List(), ctx) }.to raise_error( 18 | Error::Execution, 19 | /Wrong number of arguments: given 0, expected 1/ 20 | ) 21 | 22 | expect { atom.call(List(1, 2), ctx) }.to raise_error( 23 | Error::Execution, 24 | /Wrong number of arguments: given 2, expected 1/ 25 | ) 26 | end 27 | 28 | it "returns true for symbols" do 29 | expect(atom.call(List(Symbol("a")), ctx)).to eq true 30 | end 31 | 32 | it "returns true for empty lists" do 33 | expect(atom.call(List(List()), ctx)).to eq true 34 | end 35 | 36 | it "returns true for strings" do 37 | expect(atom.call(List("no dice"), ctx)).to eq true 38 | end 39 | 40 | it "returns true for numbers" do 41 | expect(atom.call(List(1), ctx)).to eq true 42 | end 43 | 44 | it "returns true for booleans" do 45 | expect(atom.call(List(true), ctx)).to eq true 46 | expect(atom.call(List(false), ctx)).to eq true 47 | end 48 | 49 | it "returns false for lists with elements" do 50 | expect(atom.call(List(List(1, 2)), ctx)).to eq false 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ruspea_lang (0.2.0) 5 | fiddle 6 | immutable-ruby 7 | logger 8 | ostruct 9 | zeitwerk 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | byebug (11.1.3) 15 | coderay (1.1.3) 16 | concurrent-ruby (1.3.4) 17 | diff-lcs (1.5.1) 18 | docile (1.4.1) 19 | fiddle (1.1.2) 20 | immutable-ruby (0.2.0) 21 | concurrent-ruby (~> 1.1) 22 | sorted_set (~> 1.0) 23 | logger (1.6.1) 24 | method_source (1.1.0) 25 | ostruct (0.6.0) 26 | pry (0.14.2) 27 | coderay (~> 1.1) 28 | method_source (~> 1.0) 29 | pry-byebug (3.10.1) 30 | byebug (~> 11.0) 31 | pry (>= 0.13, < 0.15) 32 | rake (13.2.1) 33 | rbtree (0.4.6) 34 | rspec (3.13.0) 35 | rspec-core (~> 3.13.0) 36 | rspec-expectations (~> 3.13.0) 37 | rspec-mocks (~> 3.13.0) 38 | rspec-core (3.13.2) 39 | rspec-support (~> 3.13.0) 40 | rspec-expectations (3.13.3) 41 | diff-lcs (>= 1.2.0, < 2.0) 42 | rspec-support (~> 3.13.0) 43 | rspec-mocks (3.13.2) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.13.0) 46 | rspec-support (3.13.1) 47 | set (1.1.0) 48 | simplecov (0.22.0) 49 | docile (~> 1.1) 50 | simplecov-html (~> 0.11) 51 | simplecov_json_formatter (~> 0.1) 52 | simplecov-html (0.13.1) 53 | simplecov_json_formatter (0.1.4) 54 | sorted_set (1.0.3) 55 | rbtree 56 | set (~> 1.0) 57 | zeitwerk (2.7.1) 58 | 59 | PLATFORMS 60 | ruby 61 | 62 | DEPENDENCIES 63 | bundler (~> 2.2) 64 | pry-byebug 65 | rake (~> 13.0) 66 | rspec (~> 3.10) 67 | ruspea_lang! 68 | simplecov 69 | 70 | BUNDLED WITH 71 | 2.5.23 72 | -------------------------------------------------------------------------------- /lib/ruspea/core/environment.rb: -------------------------------------------------------------------------------- 1 | require "immutable/hash" 2 | require "immutable/deque" 3 | 4 | module Ruspea 5 | class Core::Environment 6 | include Core::Casting 7 | 8 | STACK_ENGINE = Immutable::Deque.new 9 | SCOPE_ENGINE = Immutable::Hash.new 10 | EMPTY_SCOPE = Immutable::Hash.new 11 | 12 | def initialize(lookup: STACK_ENGINE, new_scope: SCOPE_ENGINE) 13 | @lookup = lookup.push(Scope(new_scope)) 14 | end 15 | NIL_ENV = new 16 | 17 | def push(scope = nil) 18 | if scope.is_a?(self.class) 19 | return self.class.new(lookup: @lookup, new_scope: scope.lookup.last) 20 | end 21 | 22 | scope = Scope(scope) if !scope.nil? 23 | scope ||= EMPTY_SCOPE 24 | self.class.new(lookup: @lookup, new_scope: scope) 25 | end 26 | 27 | def pop 28 | new_lookup = @lookup.pop 29 | if new_lookup.empty? 30 | NIL_ENV 31 | else 32 | self.class.new(lookup: new_lookup) 33 | end 34 | end 35 | 36 | def []=(label, value) 37 | scope = @lookup.last 38 | @lookup = @lookup.pop.push(scope.put(Symbol(label), value)) 39 | self 40 | end 41 | 42 | def [](label) 43 | raise unbound_error(label) if !bound?(label) 44 | find(Symbol(label), @lookup) 45 | end 46 | 47 | def empty?(to_lookup = nil) 48 | to_lookup ||= @lookup 49 | return true if to_lookup.size == 0 50 | return false if !to_lookup.last.empty? 51 | empty?(to_lookup.pop) 52 | end 53 | 54 | def bound?(label, to_lookup = nil) 55 | to_lookup ||= @lookup 56 | return false if to_lookup.empty? 57 | return true if to_lookup.last.key?(Symbol(label)) 58 | bound?(label, to_lookup.pop) 59 | end 60 | 61 | protected 62 | 63 | attr_reader :lookup 64 | 65 | private 66 | 67 | def find(label, to_lookup) 68 | return EMPTY_SCOPE[label] if to_lookup.empty? 69 | return to_lookup.last.get(Symbol(label)) if to_lookup.last.key?(Symbol(label)) 70 | find(label, to_lookup.pop) 71 | end 72 | 73 | def unbound_error(label) 74 | Error::Execution.new <<~ERR 75 | Unable to resolve the symbol #{label} in this context 76 | ERR 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/ruspea/lisp/eq_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Lisp::Eq do 3 | include Core::Casting 4 | let(:ctx) { {} } 5 | subject(:eq_fun) { described_class.new } # to not override RSpec#eq 6 | 7 | describe "#call" do 8 | it "raises if parameter is not a list" do 9 | expect { eq_fun.call(1, ctx) }.to raise_error( 10 | Error::Execution, 11 | /Argument should be a list, received 1 instead/ 12 | ) 13 | end 14 | 15 | it "raises if wrong arity is used" do 16 | expect { eq_fun.call(List(), ctx) }.to raise_error( 17 | Error::Execution, 18 | /Wrong number of arguments: given 0, expected 2/ 19 | ) 20 | 21 | expect { eq_fun.call(List(1), ctx) }.to raise_error( 22 | Error::Execution, 23 | /Wrong number of arguments: given 1, expected 2/ 24 | ) 25 | 26 | expect { eq_fun.call(List(1, 1, 1), ctx) }.to raise_error( 27 | Error::Execution, 28 | /Wrong number of arguments: given 3, expected 2/ 29 | ) 30 | end 31 | 32 | it "compares numbers" do 33 | expect(eq_fun.call(List(1, 1), ctx)).to eq true 34 | expect(eq_fun.call(List(4.20, 4.20), ctx)).to eq true 35 | expect(eq_fun.call(List(1, 1.0), ctx)).to eq true 36 | expect(eq_fun.call(List(-1, -1.0), ctx)).to eq true 37 | expect(eq_fun.call(List(1, 2), ctx)).to eq false 38 | end 39 | 40 | it "compares strings" do 41 | # as per clisp, they are different 42 | expect(eq_fun.call(List("lol", "lol"), ctx)).to eq false 43 | expect(eq_fun.call(List("lol", "lola"), ctx)).to eq false 44 | end 45 | 46 | it "compares lists" do 47 | # as per clisp, they are different 48 | expect(eq_fun.call(List(List(1), List(1)), ctx)).to eq false 49 | expect(eq_fun.call(List(List(), List()), ctx)).to eq true 50 | end 51 | 52 | it "compares symbols" do 53 | expect(eq_fun.call(List(Symbol("lol"), Symbol("lol")), ctx)).to eq true 54 | expect(eq_fun.call(List(Symbol("lol"), Symbol("lola")), ctx)).to eq false 55 | end 56 | 57 | it "compares booleans" do 58 | expect(eq_fun.call(List(true, true), ctx)).to eq true 59 | expect(eq_fun.call(List(false, false), ctx)).to eq true 60 | expect(eq_fun.call(List(true, false), ctx)).to eq false 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/ruspea/core/environment_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Core::Environment do 3 | include Core::Casting 4 | 5 | subject(:env) { described_class.new } 6 | 7 | before do 8 | env["fourtwenty"] = true 9 | end 10 | 11 | describe "#empty?" do 12 | it "returns false if there is any bounded values on the scope" do 13 | expect(env.empty?).to eq false 14 | end 15 | 16 | it "returns true if there is no bindings" do 17 | new_env = described_class.new 18 | 19 | expect(new_env.empty?).to eq true 20 | end 21 | 22 | it "returns true if there are many 'empty scopes' stacked" do 23 | stacked_env = described_class.new.push.push 24 | 25 | expect(stacked_env.empty?).to eq true 26 | end 27 | end 28 | 29 | describe "#push" do 30 | it "stacks the new bindings on top of the environment" do 31 | new_env = env.push(schemer: "Friedman") 32 | 33 | expect(new_env["schemer"]).to eq "Friedman" 34 | end 35 | 36 | it "ensures last added scope override declarations on previous ones" do 37 | new_env = env.push(fourtwenty: "yup") 38 | 39 | expect(new_env["fourtwenty"]).to eq "yup" 40 | expect(env["fourtwenty"]).to eq true 41 | end 42 | 43 | it "works without a parameter" do 44 | new_env = env.push 45 | new_env["Yasiin"] = "Bey" 46 | 47 | expect(new_env["Yasiin"]).to eq "Bey" 48 | expect(new_env["fourtwenty"]).to eq true 49 | end 50 | 51 | it "works with an actual environment" do 52 | new_env = env.push(described_class.new(new_scope: {mos: "def"})) 53 | 54 | expect(new_env["mos"]).to eq "def" 55 | end 56 | end 57 | 58 | describe "#pop" do 59 | it "returns a new scope with the last added one removed from the top" do 60 | new_env = env.push(fourtwenty: "yup") 61 | 62 | expect(new_env.pop["fourtwenty"]).to eq true 63 | end 64 | 65 | it "returns 'empty' environment when popping last scope" do 66 | empty = env.pop 67 | 68 | expect(empty.empty?).to eq true 69 | end 70 | end 71 | 72 | describe "#[]=" do 73 | it "creates a new binding in the current env" do 74 | env["lol"] = "bbq" 75 | 76 | expect(env["lol"]).to eq "bbq" 77 | end 78 | 79 | it "doesn't mutate 'upper' environments" do 80 | new_env = env.push(fourtwenty: "yup") 81 | 82 | expect(new_env["fourtwenty"]).to eq "yup" 83 | expect(env["fourtwenty"]).to eq true 84 | end 85 | end 86 | 87 | describe "#[]" do 88 | it "finds bindings in the current scope" do 89 | new_env = env.push 90 | new_env["answer"] = 42 91 | 92 | expect(new_env["answer"]).to eq 42 93 | end 94 | 95 | it "searches for bindings in the lookup chain" do 96 | new_env = env 97 | .push(second_scope: "here") 98 | .push(third_scope: "also here") 99 | .push 100 | 101 | expect(new_env["third_scope"]).to eq "also here" 102 | expect(new_env["second_scope"]).to eq "here" 103 | expect(new_env["fourtwenty"]).to eq true 104 | end 105 | 106 | describe "#bound?" do 107 | it "returns true for bounded and false for unbounded symbols" do 108 | env[Symbol("lol")] = 420 109 | 110 | expect(env.bound? Symbol("nada")).to eq false 111 | expect(env.bound? Symbol("lol")).to eq true 112 | end 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/ruspea/evaluator_spec.rb: -------------------------------------------------------------------------------- 1 | module Ruspea 2 | RSpec.describe Evaluator do 3 | subject(:evaluator) { described_class.new } 4 | 5 | describe "#eval" do 6 | include Core::Casting 7 | 8 | let(:env) { Core::Environment.new } 9 | 10 | context "Primitives" do 11 | it "evaluates a number to itself" do 12 | expect(evaluator.eval(1)).to eq 1 13 | expect(evaluator.eval(-1)).to eq -1 14 | expect(evaluator.eval(4.20)).to eq 4.20 15 | end 16 | 17 | it "evaluates a string to itself" do 18 | expect(evaluator.eval("lol")).to eq "lol" 19 | end 20 | 21 | it "evaluates bools" do 22 | expect(evaluator.eval(true)).to eq true 23 | expect(evaluator.eval(false)).to eq false 24 | end 25 | end 26 | 27 | context "Symbol" do 28 | it "lookup the symbol in the environment" do 29 | env["lol"] = 420 30 | 31 | expect(evaluator.eval(Symbol("lol"), env)).to eq 420 32 | end 33 | 34 | it "raises if symbol cannot be found in the environment" do 35 | expect { evaluator.eval(Symbol("bbq"), env) }.to raise_error( 36 | Error::Execution, 37 | /Unable to resolve/ 38 | ) 39 | end 40 | end 41 | 42 | context "Lists" do 43 | it "handles lists as function invocations" do 44 | env["going_down"] = ->(_, _) { 42 } 45 | expr = List(Symbol("going_down")) 46 | 47 | expect(evaluator.eval(expr, env)).to eq 42 48 | end 49 | 50 | it "passes args and environment to the callable" do 51 | passed_args = nil 52 | passed_env = nil 53 | env["going_down"] = ->(args, _env) { 54 | passed_args = args 55 | passed_env = _env 56 | } 57 | expr = List(Symbol("going_down"), 1, 2, 3) 58 | evaluator.eval(expr, env) 59 | 60 | expect(passed_args).to eq List(1, 2, 3) 61 | expect(passed_env).to eq env 62 | end 63 | 64 | it "evaluates args before invocate the function" do 65 | passed_args = nil 66 | env["answer"] = 42 67 | env["going_down"] = ->(args, _) { 68 | passed_args = args 69 | } 70 | expr = List(Symbol("going_down"), Symbol("answer")) 71 | evaluator.eval(expr, env) 72 | 73 | expect(passed_args.head).to eq 42 74 | end 75 | 76 | it "raise if first element is not bound" do 77 | expect { evaluator.eval(Symbol("bbq"), env) }.to raise_error( 78 | Error::Execution, 79 | /Unable to resolve/ 80 | ) 81 | end 82 | 83 | it "raises if first element doesn't evaluates to a callable" do 84 | env["not_going"] = 420 85 | expr = List(Symbol("not_going")) 86 | 87 | expect { evaluator.eval(expr, env) }.to raise_error( 88 | Error::Execution, 89 | /Unable to treat 420 as a callable thing/ 90 | ) 91 | end 92 | 93 | context "inline invocation" do 94 | before do 95 | env["lambda"] = Lisp::Lambda.new 96 | env["quote"] = Lisp::Quote.new 97 | env["cons"] = Lisp::Cons.new 98 | env["cdr"] = Lisp::Cdr.new 99 | end 100 | 101 | it "allows 'inline' invokation for the lambda" do 102 | # ((lambda (x y) (cons x (cdr y))) 'z '(a b c)) 103 | expr = 104 | List(# outer list/invokation 105 | List( 106 | Symbol("lambda"), List(Symbol("x"), Symbol("y")), #declaration 107 | List( 108 | List(Symbol("cons"), Symbol("x"), List("cdr", Symbol("y"))) 109 | ) # body 110 | ),# lambda declaration 111 | List("quote", Symbol("z")), 112 | List("quote", List(Symbol("a"), Symbol("b"), Symbol("c"))) 113 | ) 114 | 115 | expect(evaluator.eval(expr, env)).to eq List(Symbol("z"), Symbol("b"), Symbol("c")) 116 | end 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruspea 2 | 3 | A Lisp dialect written in Ruby (and on itself) 4 | meant to be used as a standard Ruby gem. 5 | 6 | So if anyone asks: 7 | 8 | > What is this Ruspea thing on the Gemfile? 9 | 10 | You can go: 11 | 12 | > Nah. Just a gem. 13 | 14 | Then you can sneak in code like this in your project 15 | 16 | ```lisp 17 | (def %fib 18 | (fn [n] 19 | (cond 20 | ((= n 0) n) 21 | ((= n 1) n) 22 | (true 23 | (+ 24 | (%fib (- n 1)) 25 | (%fib (- n 2))))))) 26 | ``` 27 | 28 | And execute it from Ruby like: 29 | 30 | ```ruby 31 | Ruspea::Code.new.load("my/awesome/ruspea/script.rsp") 32 | ``` 33 | 34 | You can also bypass the file loading: 35 | 36 | ```ruby 37 | require "ruspea-lang" 38 | 39 | code = <<~c 40 | (def plus1 41 | (fn [num] (+ num 1))) 42 | (plus1 10) 43 | c 44 | 45 | eleven = Ruspea::Code.new.run(code).last 46 | puts eleven # => 11 47 | ``` 48 | 49 | ## Is this a functional language? Is it interpreted? 50 | 51 | Yes. 52 | 53 | ## Does it have a REPL? 54 | 55 | YES! After install it as a gem (`gem install ruspea_lang`), 56 | just go ahead and: 57 | 58 | ```bash 59 | ➜ ruspea 60 | loading repl: repl.rsp 61 | repl loaded. 62 | 63 | #user=> (def lol 1) 64 | 1 65 | #user=> lol 66 | 1 67 | #user=> (puts "hello world") 68 | hello world 69 | nil 70 | #user=> ^CSee you soon. 71 | ``` 72 | 73 | ## Humm... but what about the Ruby stuff? And the gems?! HUH!? 74 | 75 | Ruspea has Ruby interoperabillity built-in 76 | so you can "just use" Ruby: 77 | 78 | ```lisp 79 | (def puts 80 | (fn [str] 81 | (. 82 | (:: Kernel) puts str))) 83 | ``` 84 | 85 | In fact, the very minimal standard library is 86 | built on top of this interop capability. 87 | It is also the best source to look for usage/syntax/etc right now: 88 | [`lib/language/standard.rsp`](https://github.com/mistersourcerer/ruspea/blob/master/lib/language/standard.rsp) 89 | 90 | ### OH! I see! Was this inspired by Clojure? 91 | 92 | 100%! 93 | 94 | ### OH! I see! Is this as good as Clojure!? 95 | 96 | ![LOL](https://i.imgflip.com/1joc8h.jpg) 97 | 98 | ## Why would I use that, though? All those parenthesis... 99 | 100 | This is the actual question, isn't it? 101 | Sadly this is way out of the scope of this README. 102 | 103 | You will need to convince yourself here. 😬 104 | 105 | ## Is this ready for production usage? 106 | 107 | Nope. 108 | And it will probably never be. 109 | 110 | This was just an exercise: 111 | 112 | - Ruby is really fun. 113 | - Lisp too. 114 | - I am really (I mean REALLY) enjoying Clojure. 115 | - And I really wanted to learn how to implemente a programming language. 116 | - Lists are a great way to organize thoughts. 117 | 118 | If you put all this together you have this project. 119 | 120 | ## Shortcomings 121 | 122 | Actually, I would prefer to avoid the term ~~shortcomings~~. 123 | 124 | The current social norm forces impossible standards 125 | on everyone and everything! 126 | 127 | I want to list the things I know are not perfect 128 | about this pretty little thing called Ruspea 129 | and I don't want to hurt it's feelings. 130 | 131 | Let's call those rough edges... *features*. 132 | Those are enhancements that are not here... just yet 😬. 133 | 134 | ![Ironic](https://media.giphy.com/media/9MJ6xrgVR9aEwF8zCJ/source.gif) 135 | 136 | ### Features 137 | 138 | - No performance! Seriously. There is no optmization whatsoever to see here. 139 | - No TCO. Goes hand to hand with the previous one. 140 | - No standard library. 141 | - No multi-arity functions. 142 | They are in the Runtime though. Just too lazy to build the syntax reader for it right now. 143 | - No examples. Besides the "standard library" ones ([`lib/language`](https://github.com/mistersourcerer/ruspea/blob/master/lib/language/standard.rsp)) 144 | 145 | ## Installation 146 | 147 | Add this line to your application's Gemfile: 148 | 149 | ```ruby 150 | gem "ruspea_lang" 151 | ``` 152 | 153 | And then execute: 154 | 155 | $ bundle 156 | 157 | Or install it yourself: 158 | 159 | $ gem install ruspea_lang 160 | 161 | 162 | ### Why not just ruspea? Do we really need a `_lang` suffix?! 163 | 164 | Yes, we do: 165 | 166 | ``` 167 | Pushing gem to https://rubygems.org... 168 | There was a problem saving your gem: Name 'ruspea' is too close to typo-protected gem: rspec 169 | ``` 170 | 171 | ## Development 172 | 173 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 174 | 175 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 176 | 177 | ## Contributing 178 | 179 | Bug reports and pull requests are welcome on GitHub at https://github.com/mistersourcerer/ruspea. 180 | 181 | ## License 182 | 183 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 184 | --------------------------------------------------------------------------------