diff --git a/.ruby-version b/.ruby-version index ec1cf33..24ba9a3 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.3 +2.7.0 diff --git a/Gemfile.lock b/Gemfile.lock index b8f83d3..62a25be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,37 +1,41 @@ PATH remote: . specs: - feeble (0.1.0) + feeble (0.2.0) + immutable-ruby zeitwerk GEM remote: https://rubygems.org/ specs: - byebug (11.0.1) + byebug (11.1.1) coderay (1.1.2) + concurrent-ruby (1.0.5) diff-lcs (1.3) + immutable-ruby (0.0.4) + concurrent-ruby (~> 1.0.0) method_source (0.9.2) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - pry-byebug (3.7.0) + pry-byebug (3.8.0) byebug (~> 11.0) pry (~> 0.10) - rake (10.5.0) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.3) + rake (13.0.1) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.1) + rspec-support (~> 3.9.1) + rspec-expectations (3.9.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - zeitwerk (2.1.6) + rspec-support (~> 3.9.0) + rspec-support (3.9.2) + zeitwerk (2.2.2) PLATFORMS ruby @@ -39,10 +43,9 @@ PLATFORMS DEPENDENCIES bundler (~> 2.0) feeble! - pry - pry-byebug - rake (~> 10.0) - rspec (~> 3.0) + pry-byebug (~> 3) + rake (~> 13.0) + rspec BUNDLED WITH - 2.0.1 + 2.1.2 diff --git a/README.md b/README.md index 09aff9d..f00abca 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Feeble -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/feeble`. To experiment with that code, run `bin/console` for an interactive prompt. - -TODO: Delete this and the text above, and describe your gem +A small programming language for education purposes. +It is functional, favoring immuatbility and supports TCO. +Meant to be hosted, uses Ruby as a "runtime". ## Installation diff --git a/bin/console b/bin/console index 3e7ae31..10c9409 100755 --- a/bin/console +++ b/bin/console @@ -10,8 +10,5 @@ require "feeble" require "pry" include Feeble -include Feeble::Reader -include Feeble::Evaler -include Feeble::Runtime Pry.start diff --git a/feeble.gemspec b/feeble.gemspec index e0814f9..048ce29 100644 --- a/feeble.gemspec +++ b/feeble.gemspec @@ -27,10 +27,10 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_development_dependency "bundler", "~> 2.0" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.0" - spec.add_development_dependency "pry" - spec.add_development_dependency "pry-byebug" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rspec" + spec.add_development_dependency "pry-byebug", "~>3" spec.add_dependency "zeitwerk" + spec.add_dependency "immutable-ruby" end diff --git a/lib/feeble/evaler/lispey.rb b/lib/feeble/evaler/lispey.rb deleted file mode 100644 index 697a605..0000000 --- a/lib/feeble/evaler/lispey.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Feeble::Evaler - class Lispey - def initialize - @verify = Feeble::Runtime::Verifier.new - end - - def eval(form, env: Feeble::Language::Ruby::Fbl.new) - if @verify.list? form - eval_list form, env - else - eval_expression form, env - end - end - - private - - def eval_list(list, env) - first_form = fn = env.lookup(list.first) - # return eval_list(first_form, env) if @verify.list? first_form - - if @verify.fn? fn - if fn.prop?(:special) - env.register Feeble::Runtime::Symbol.new("%xenv"), env - return fn.invoke(*Array(list.rest), scope: env) - else - evaled = Array(list.rest).map { |expression| - eval_expression(expression, env) - } - return fn.invoke(*evaled, scope: env) - end - end - - raise "Can't invoke <#{list.first}>, not a function" - end - - def eval_expression(expression, env) - if @verify.symbol? expression - # TODO: maybe raise if not registered (instead of returning undefined) - # "unable to resolve" is actually pretty sweet - env.lookup expression - else - expression - end - end - end -end diff --git a/lib/feeble/language/ruby/define.rb b/lib/feeble/language/ruby/define.rb deleted file mode 100644 index 33b7a6c..0000000 --- a/lib/feeble/language/ruby/define.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Feeble::Language::Ruby - class Define - include Feeble::Runtime - include Invokable - - def initialize - # A special invokable: - # - will have all arguments quoted (during read phase) - # - have a %xenv reference to the external environment - - @evaler = Feeble::Evaler::Lispey.new - - prop :special - - arity(Symbol.new(:symbol), Symbol.new(:expression)) { |env| - name = env.lookup Symbol.new(:symbol) - expression = env.lookup Symbol.new(:expression) - value = @evaler.eval(expression, env: env) - - xenv = env.lookup Symbol.new(:"%xenv") - xenv.register name, value - } - end - end -end diff --git a/lib/feeble/language/ruby/fbl.rb b/lib/feeble/language/ruby/fbl.rb deleted file mode 100644 index 0fd5da6..0000000 --- a/lib/feeble/language/ruby/fbl.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Feeble::Language::Ruby - class Fbl < Feeble::Runtime::Env - include Feeble::Runtime - - def initialize - super - - register Symbol.new(:define), Define.new - register Symbol.new(:quote), Quote.new - register Symbol.new(:lambda), Feeble::Language::Ruby::Lambda.new - register Symbol.new(:print), Feeble::Language::Ruby::Print.new - register Symbol.new(:println), Feeble::Language::Ruby::Println.new - end - end -end diff --git a/lib/feeble/language/ruby/lambda.rb b/lib/feeble/language/ruby/lambda.rb deleted file mode 100644 index 8db6a6d..0000000 --- a/lib/feeble/language/ruby/lambda.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Feeble::Language::Ruby - class Lambda - include Feeble::Runtime - include Invokable - - def initialize - arity Symbol.new(:body) do |env| - create_lambda_with [], env.lookup(Symbol.new(:body)) - end - - arity Symbol.new(:params), Symbol.new(:body) do |env| - params = env.lookup(Symbol.new(:params)) - body = env.lookup(Symbol.new(:body)) - - create_lambda_with params, body - end - end - - private - - def create_lambda_with(params, body) - Feeble::Runtime::Lambda.new(params, body) - end - end -end diff --git a/lib/feeble/language/ruby/print.rb b/lib/feeble/language/ruby/print.rb deleted file mode 100644 index ac3c5b6..0000000 --- a/lib/feeble/language/ruby/print.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Feeble::Language::Ruby - class Print - include Feeble::Runtime - include Invokable - - def initialize - arity(Symbol.new(:expression)) do |env| - print env.lookup(Symbol.new(:expression)) - end - end - - private - - def print(expression) - $stdout.print expression - end - end -end diff --git a/lib/feeble/language/ruby/println.rb b/lib/feeble/language/ruby/println.rb deleted file mode 100644 index 633fb96..0000000 --- a/lib/feeble/language/ruby/println.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Feeble::Language::Ruby - class Println - include Feeble::Runtime - include Invokable - - def initialize - arity(Symbol.new(:expression)) do |env| - print env.lookup(Symbol.new(:expression)) - end - end - - private - - def print(expression) - $stdout.print expression + "\n" - end - end -end diff --git a/lib/feeble/language/ruby/quote.rb b/lib/feeble/language/ruby/quote.rb deleted file mode 100644 index 24f4d03..0000000 --- a/lib/feeble/language/ruby/quote.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Feeble::Language::Ruby - class Quote - include Feeble::Runtime - include Invokable - - def initialize - prop :special - - arity(Symbol.new("expression")) { |env| - env.lookup(Symbol.new("expression")) - } - end - end -end diff --git a/lib/feeble/printer/expression.rb b/lib/feeble/printer/expression.rb deleted file mode 100644 index 9908bc7..0000000 --- a/lib/feeble/printer/expression.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Feeble::Printer - class Expression - include Feeble::Runtime - - def initialize - @verify = Verifier.new - end - - def print(expression, to: $stdout) - to.print " > #{printable_for(expression)}" - end - - private - - def printable_for(expression) - if expression.is_a?(Printable) - expression.to_print { |el| printable_for(el) } - else - if expression.nil? - "NIL" - else - "#{expression.inspect} (#{expression.class})" - end - end - end - end -end diff --git a/lib/feeble/printer/printable.rb b/lib/feeble/printer/printable.rb deleted file mode 100644 index 8b15195..0000000 --- a/lib/feeble/printer/printable.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Feeble::Printer - module Printable - def to_print - to_s - end - end -end diff --git a/lib/feeble/pushback_reader.rb b/lib/feeble/pushback_reader.rb new file mode 100644 index 0000000..3576ebb --- /dev/null +++ b/lib/feeble/pushback_reader.rb @@ -0,0 +1,48 @@ +class Feeble::PushbackReader + def self.with(io, chunk_size: 16384) + raise "Pass the block that will receive the pushback reader" if !block_given? + + reader = new(io, chunk_size: chunk_size) + yield(reader) + reader.close + end + + def initialize(io, chunk_size: 16384) + @io = io.is_a?(String) ? StringIO.new(io) : io + @chunk_size = chunk_size + @started = nil + @buffer = [] + end + + def next + return nil if eof? + + @started ||= true + bufferize if @buffer.length == 0 + @buffer.shift + end + + def peek(length = 1) + bufferize if !@started + str = @buffer[0...length] + str.length > 0 ? str.join : nil + end + + def push(string) + @buffer = string.to_s.chars + @buffer + end + + def eof? + @buffer.length == 0 && @io.eof? + end + + def close + @io.close + end + + private + + def bufferize + @buffer = @io.read(@chunk_size).chars + end +end diff --git a/lib/feeble/reader.rb b/lib/feeble/reader.rb new file mode 100644 index 0000000..b21a8e0 --- /dev/null +++ b/lib/feeble/reader.rb @@ -0,0 +1,145 @@ +require "immutable/vector" +require "immutable/list" + +class Feeble::Reader + def next(source, col = 1) + reader = reader_with source + token_content = "" + + while(char = reader.next) do + token_content << char + if token = tokenize(token_content, reader, col) + return token + end + end + + raise "unrecognizable token #{token_content}" + end + + def call(source) + reader = Feeble::PushbackReader.new(source) + col = 1 + + tokens = Immutable::Vector.new + while(!reader.eof?) + token = self.next(reader, col) + tokens = tokens.add token + _, meta = token + col = meta[:end] + 1 + end + + tokens + end + + private + + DIGIT = /\A[\d-]/ + NUMERIC = /\A[\d_]/ + SEPARATOR = /\A[,\s]/ + + def reader_with(source) + return source if source.is_a? Feeble::PushbackReader + + Feeble::PushbackReader.new io_with(source) + end + + def io_with(source) + return source if source.is_a? StringIO + + # TODO: if source is a valid path, read file + StringIO.new source + end + + def tokenize(token_content, reader, col) + return token(token_content, {type: :separator, start: col, end: col}) if separator?(token_content) + return token(token_content, {type: :new_line, start: col, end: col}) if new_line?(token_content) + return read_number(token_content, reader, col) if number?(token_content, reader) + + delimiter = delimiters.find { |d| d.first == token_content } + return read_delimited_token(delimiter, reader, col) if delimiter + end + + def separator?(token_content) + token_content == "\s" || token_content == "," + end + + def new_line?(token_content) + token_content == "\n" + end + + def number?(token_content, reader) + DIGIT.match?(token_content) || ending?(reader.peek) + end + + def ending?(char) + SEPARATOR.match?(char) || char == nil + end + + def read_number(start, reader, col) + number = read_digits start, reader + value, meta = + if reader.peek == "." + number = read_digits number + reader.next, reader + [Float(number), {type: :float}] + else + [Integer(number), {type: :int}] + end + + if !ending?(reader.peek) + raise Feeble::Syntax::NumberFormatError.new number + reader.peek + end + + token value, meta.merge(start: col, end: col + number.length) + end + + def read_digits(start, reader) + number = start + + while NUMERIC.match?(char = reader.next) + number << char + end + + reader.push char + number + end + + def delimiters + @delimiters ||= [ + ["\"", "\"", :string], + [";", "\n", :comment], + ["[", "]", :vector, ->(content) { call(content) }], + ["{", "}", :block, ->(content) { call(content) }], + ["(", ")", :list, ->(content) { Immutable::List[*call(content)] }] + ] + end + + def read_delimited_token(delimiter, reader, col) + content, meta = read_until delimiter, reader, col + token_value = transformer_from(delimiter).call(content) + token token_value, meta + end + + def read_until(delimiter, reader, col) + open, close, type, _ = delimiter + + content = "" + # TODO: this won't work for delimiters bigger than one char + while((char = reader.next) && char != close) + content << char + end + + end_at = (open.length + content.length + close.length) + meta = (char == close || close == "\n") ? {} : {open: true} + [content, meta.merge(type: type, start: col, end: end_at)] + end + + def transformer_from(delimiter) + _, _, _, transformer = delimiter + @_default_transformer ||= ->(content) { content } + transformer || @_default_transformer + end + + def token(token_value, meta) + Immutable::Vector[token_value, meta] + end +end diff --git a/lib/feeble/reader/char.rb b/lib/feeble/reader/char.rb deleted file mode 100644 index 4304b84..0000000 --- a/lib/feeble/reader/char.rb +++ /dev/null @@ -1,122 +0,0 @@ -require "logger" - -module Feeble::Reader - class Char - attr_reader :prev - # TODO: consider enumerable... - - def initialize(string, debug: false) - # TODO: if string is a IO stream already, do not convert it. - @io = StringIO.new(string) - @started = false - @prev = nil - - @logger = Logger.new(STDOUT) - @logger.level = debug ? Logger::DEBUG : Logger::INFO - end - - def start - if @started - self.current - else - self.next - end - end - - def next - @prev = @current - - if @peek - @current = @peek - @peek = nil - return @current - end - - @started = true unless @started - @current = @io.getc - end - - def current - @current - end - - def peek - return @peek if @peek - - current = @current - @peek = self.next - @current = current - @peek - end - - def eof? - @peek == nil && @io.eof? - end - - def until_next(pattern, or_eof: false, &condition) - condition ||= - if pattern.is_a?(Regexp) - ->(current_char, _) { pattern.match? current_char } - else - ->(current_char, _) { pattern == current_char } - end - - read_until("Expected #{pattern} but nothing was found", or_eof, &condition) - end - - def read_until(raise_not_found = nil, or_eof = false, &condition) - condition ||= ->(_, _) { false } - - if condition.call(self.current, nil) - @logger.debug "condition was immediately verified" - @logger.debug " looking to #{self.current}" - @logger.debug " raising < #{raise_not_found} >" - @logger.debug "returning empty handed." - - return "" - end - - string = self.current || "" - found = false - - @logger.debug "starting #read_until" - @logger.debug " looking to #{self.current}" - @logger.debug " raising < #{raise_not_found} >" - @logger.debug " accumulated started with: #{string}" - - until(eof?) - self.next - - @logger.debug " now looking into: #{self.current}" - - if condition.call(self.current, string) - - @logger.debug "condition verified" - @logger.debug " accumulator is: #{string}" - - found = true - break - end - - string << current - - @logger.debug "iterating, accumulator is: #{string}" - end - - @logger.debug "Out of loop" - @logger.debug " looking to #{self.current}" - @logger.debug " accumulator is: #{string}" - @logger.debug " found? < #{found} > AND raise: < #{raise_not_found} >" - - if !or_eof && !found && !raise_not_found.nil? - raise raise_not_found - else - string - end - end - - def logger_level(level) - @logger.level = level - end - end -end diff --git a/lib/feeble/reader/code.rb b/lib/feeble/reader/code.rb deleted file mode 100644 index f019f7c..0000000 --- a/lib/feeble/reader/code.rb +++ /dev/null @@ -1,192 +0,0 @@ -module Feeble::Reader - class Code - include Feeble::Runtime - - def initialize - @syntax = Tree.new - - @syntax.add LAMBDA, :read_lambda - end - - def read(code, env: nil) - reader = code.is_a?(Char) ? code : Char.new(code.strip) - component = "" - values = [] - - while reader.next - if SEPARATOR.match?(reader.current) - ignore_separators(reader) - - if component.length > 0 - if known_syntax = @syntax.search(component) - values << send(known_syntax, reader, env) - else - values << make_expression(component) - end - - ignore_separators(reader) - component = "" - end - end - - if reader.current == ")" - values << make_expression(component) if component.length > 0 - - component = "" - next - end - - if reader.current == "}" - values << make_expression(component) if component.length > 0 - - component = "" - # TODO: Why are we breaking here? XD - break - end - - if reader.current == "'" - values << read_quote(reader, env) - - component = "" - next - end - - if reader.current == "\"" - values << read_string(reader, env) - - component = "" - next - end - - if reader.current == "(" - if component.length > 0 - values << read_invokation(component, reader, env) - else - values << read_list(reader, env) - end - - component = "" - next - end - - if reader.current == "{" - values << read_map(reader, env) - - component = "" - next - end - - component << reader.current if reader.current - end - - values << make_expression(component) if component.length > 0 - values - end - - private - - LAMBDA = "->" - SEPARATOR = /\A[\s,]/ - NUMBER = /\A[\d-]([\d\_\.]*\z)/ - - def make_expression(component) - return Keyword.new(component) if component.end_with? ":" - - value = value_of(component) - if value.nil? - Symbol.new(component) - else - value - end - end - - def value_of(component) - case - when component == "true" || component == "false" - component == "true" - when NUMBER.match?(component) - /\./.match?(component) ? Float(component) : Integer(component) - else - nil - end - end - - def read_quote(reader, env) - reader.next - - args = - if reader.current == "(" - List.create(read_list(reader, env)) - else - expression_content = reader.until_next(SEPARATOR, or_eof: true) - read(expression_content, env: env) - end - - List.create(Symbol.new("quote"), *args) - end - - def read_string(reader, env) - reader.next - - reader.until_next("\"") - end - - def read_lambda(reader, env) - ignore_separators reader - params = nil - - map_or_body = - if reader.current == "{" # map or body - read_list(reader, env).to_a - else - params_content = reader.until_next "{" - params = read(params_content, env: env) - read_list(reader, env).to_a - end - reader.next # consume } - ignore_separators reader - - map, body = - if reader.current == "{" - ignore_separators(reader) - body = read_list(reader, env).to_a - ignore_separators(reader) - reader.next - - [Hash[*map_or_body], body] - else - [nil, map_or_body] - end - - lambda_declaration = [] - lambda_declaration << params if !params.nil? - lambda_declaration << body - lambda_declaration << map if !map.nil? - - List.create(*lambda_declaration).cons Symbol.new("lambda") - end - - def read_map(reader, env) - Hash[*read(reader, env: env)].tap { reader.next } - end - - def ignore_separators(reader) - while SEPARATOR.match?(reader.current) && !reader.eof? - reader.next - end - end - - def read_list(reader, env) - list_content = read(reader, env: env) { ignore_separators(reader) } - reader.next - - List.create(*list_content) - end - - def read_invokation(fn_name, reader, env) - params = read_list(reader, env) - - List.create(Symbol.new(fn_name), *params) - end - end -end diff --git a/lib/feeble/reader/number.rb b/lib/feeble/reader/number.rb deleted file mode 100644 index c0340dc..0000000 --- a/lib/feeble/reader/number.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Feeble::Reader - class Number - START = /\A[\d\-_]{1}/ - BODY = /\A[\d\-_\.]{1}/ - - include Feeble::Runtime - include Invokable - - def initialize - prop :reader - - arity(Feeble::Runtime::Symbol.new(:code)) { |env| - source_code = env.lookup(Feeble::Runtime::Symbol.new(:code)) - number, code = read_number_from source_code - [parse_number(number), code] - } - end - - private - - FLOAT = /\A[\-\d_]+\.[\d_]+\z/ - - def read_number_from(source_code, current_number = Str.create("")) - finished_or_not_number = (source_code.empty? || - (current_number.empty? && !START.match?(source_code.first))) - return current_number, source_code if finished_or_not_number - return current_number, source_code if !BODY.match?(source_code.first) - - read_number_from( - source_code.rest, current_number.apnd(source_code.first)) - end - - def parse_number(number) - number = String(number) - return nil if number.length == 0 - - FLOAT.match?(number) ? Float(number) : Integer(number) - end - end -end diff --git a/lib/feeble/reader/read.rb b/lib/feeble/reader/read.rb deleted file mode 100644 index ae34ceb..0000000 --- a/lib/feeble/reader/read.rb +++ /dev/null @@ -1,69 +0,0 @@ -module Feeble::Reader - class Read - include Feeble::Runtime - include Invokable - - def initialize - @fn = ListFunctions.new(StrEmpty.instance) - @number = Number.new - - arity(Feeble::Runtime::Symbol.new(:code)) { |env| - read Str.create(env.lookup(Feeble::Runtime::Symbol.new(:code))), env - } - end - - private - - SEPARATOR = /\A[\s,]/ - - attr_reader :fn - - Sym = Feeble::Runtime::Symbol - - def read(code, env, expressions: []) - return expressions if fn.empty?(code) - - token, remaining_code = *next_token(code, env) - # the whole read two token at once can be done here... - expressions << token - - read(remaining_code, env, expressions: expressions) - end - - def ignore_separators(string) - return StrEmpty.instance if fn.empty? string - return string if !SEPARATOR.match? String(string.first) - - ignore_separators(string.rest) - end - - def next_token(code, env, looking_at = StrEmpty.instance) - if fn.empty? code - return [handle_token(looking_at, env), StrEmpty.instance] - end - - case String(code.first) - when SEPARATOR - [handle_token(looking_at, env), ignore_separators(code.rest)] - when Number::START - number, remaining_code = *@number.invoke(code) - [number, ignore_separators(remaining_code)] - else - next_token(code.rest, env, looking_at.apnd(code.first)) - end - end - - def handle_token(token, env) - # TODO: can be: - # - an invokation ( some() ) - # - an invokation ( some(more 1 stuff) ) - # - an invokation ( some({ more: stuff }) ) - case - when token.first == ":" || token.to_s[-1] == ":" - Keyword.new(token) - else - Sym.new(token) - end - end - end -end diff --git a/lib/feeble/repl.rb b/lib/feeble/repl.rb deleted file mode 100644 index 49e2967..0000000 --- a/lib/feeble/repl.rb +++ /dev/null @@ -1,57 +0,0 @@ -require "pry-byebug" - -module Feeble - class Repl - COMMAND = /\A\// - COMMANDS = { - exit: /\Aexit$/i - } - - def run - puts "Feeble (REPL) #{Feeble::VERSION}" - puts " /[COMMAND] (/exit to getoutahier!)\n\n\n" - - reader = Feeble::Reader::Code.new - evaler = Feeble::Evaler::Lispey.new - printer = Feeble::Printer::Expression.new - env = Feeble::Language::Ruby::Fbl.new - - while true - begin - print "feeble > " - - input = gets.chomp - - break if execute(input) if COMMAND.match? input - - expressions = reader.read(input, env: env) - result = expressions.reduce(nil) { |res, expression| - res = evaler.eval expression, env: env - } - - printer.print result - $stdout.print "\n" - rescue StandardError => e - puts " !error > #{e.message}" - puts " !error > inspect? [Y/n]" - res = gets.chomp.downcase - if res == "y" || res == "yes" || res == "" - pp e.backtrace - end - end - end - end - - private - - def execute(cmd) - command = cmd.chomp[1..] - - case command - when COMMANDS[:exit] - $stdout.puts "Bye!" - true - end - end - end -end diff --git a/lib/feeble/runtime/env.rb b/lib/feeble/runtime/env.rb deleted file mode 100644 index d9b5655..0000000 --- a/lib/feeble/runtime/env.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Feeble::Runtime - class Env - def initialize(fallback = EnvNull.instance) - @registry = {} - @verify = Verifier.new - @fallback = fallback - @funcs = Tree.new - end - - def lookup(id) - @registry.fetch(id) { fallback.lookup id } - end - - def register(name, value = nil) - check_name_type name - if @verify.fn? value - @funcs.add name.to_s, value - end - @registry[name] = value - end - - def fn(name) - @funcs.search name.to_s - end - - protected - - attr_accessor :fallback - - private - - def check_name_type(name) - return if @verify.symbol?(name) - - raise "Only Symbols can be associated with values, not <#{name.inspect}>" - end - end -end diff --git a/lib/feeble/runtime/env_null.rb b/lib/feeble/runtime/env_null.rb deleted file mode 100644 index 1f30066..0000000 --- a/lib/feeble/runtime/env_null.rb +++ /dev/null @@ -1,12 +0,0 @@ -require "singleton" - -module Feeble::Runtime - class EnvNull - include Singleton - - def lookup(_) - # TODO: raise symbol not registered? - nil - end - end -end diff --git a/lib/feeble/runtime/invokable.rb b/lib/feeble/runtime/invokable.rb deleted file mode 100644 index 50f6eea..0000000 --- a/lib/feeble/runtime/invokable.rb +++ /dev/null @@ -1,88 +0,0 @@ -module Feeble::Runtime - module Invokable - include Feeble::Printer::Printable - - def arity(*names, &callable) - if names.find { |name| !_verify.symbol? name } - raise ArgumentTypeMismatch.new(nil, Symbol) - end - - arities_identifier = - # TODO: generalize star so it can appear in any position - if names.length == 1 && String(names.first).start_with?("*") - "*" - else - names.count - end - - _arities[arities_identifier] = { - callable: callable, - symbols: names, - } - end - - def invoke(*params, scope: EnvNull.instance) - env = Env.new(scope) - function = _arity_for(params) - - function[:symbols].each_with_index do |symbol, index| - name, value = - if String(symbol).start_with?("*") - [Symbol.new(String(symbol)[1..]), params] - else - [symbol, params[index]] - end - - env.register name, value - end - - function[:callable].call(env) - end - - def prop(key, value = true) - _props[key] = value - end - - def prop?(key) - _props[key] == true - end - - def to_print - _arities.keys.map { |arity| - names = _arities[arity][:symbols] - "lambda(#{names.map(&:id).join(", ")})" - }.join("\n") - end - - private - - def _verify - @_verify ||= Verifier.new - end - - def _arities - @_arities ||= {} - end - - def _arity_for(params) - _arities.fetch(params.count) { _arities["*"] } || - raise(ArityMismatch.new(params.count, _arities)) - end - - def _props - @_props ||= {} - end - end - - class ArityMismatch < StandardError - def initialize(given, arities) - super with_message(given, arities) - end - - private - - def with_message(given, arities) - "No arity < #{given} > for this function. Existent: #{arities.keys.join(", ")}" - end - end -end diff --git a/lib/feeble/runtime/keyword.rb b/lib/feeble/runtime/keyword.rb deleted file mode 100644 index 74a288d..0000000 --- a/lib/feeble/runtime/keyword.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Feeble::Runtime - class Keyword - include Feeble::Printer::Printable - - def initialize(id) - @symbol = Symbol.new(id) - end - - def value - @symbol - end - - def ==(other) - return false if self.class != other.class - value == other.value - end - - def eql?(other) - self == other - end - - def hash - value.hash + :keyword.hash - end - - def to_s - @symbol.id - end - end -end diff --git a/lib/feeble/runtime/lambda.rb b/lib/feeble/runtime/lambda.rb deleted file mode 100644 index 541ad1a..0000000 --- a/lib/feeble/runtime/lambda.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Feeble::Runtime - class Lambda - include Invokable - - def initialize(params = [], body = [nil], evaler: Feeble::Evaler::Lispey.new) - @evaler = evaler - # create a arity for the params "arity(*params) {} - # for the block: create an env for the function, - # wrap it around the caller env - # evaluate the body in the context of this new env - arity(*params) do |env| - fn_env = Env.new(env) - body.reduce(nil) { |result, form| - result = @evaler.eval(form, env: fn_env) - } - end - end - end -end diff --git a/lib/feeble/runtime/list.rb b/lib/feeble/runtime/list.rb deleted file mode 100644 index c420b26..0000000 --- a/lib/feeble/runtime/list.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Feeble::Runtime - class List - include Feeble::Printer::Printable - include ListProperties - - def self.create(*args) - return ListEmpty.instance if args.length == 0 - - new args[0], create(*args[1..args.length]), count: args.length - end - - def self.fn - @fn ||= ListFunctions.new - end - - def initialize(obj, rest = ListEmpty.instance, count: 1) - @count = count - @first = obj - @rest = rest - @fn = self.class.fn - end - - def nill - ListEmpty.instance - end - - def cons(obj) - self.class.new obj, self, count: count + 1 - end - - def to_a - [first] + rest.to_a - end - - def to_print(&printable_for) - and_more = count > 5 ? " ..." : "" - elements = Array(@fn.take(5, self)) - "(#{elements.map{ |el| printable_for.call(el) }.join(" ")}#{and_more})" - end - - private - - def same?(list, other) - return true if list == ListEmpty.instance && other == ListEmpty.instance - list.first == other.first && same?(list.rest, other.rest) - end - end -end diff --git a/lib/feeble/runtime/list_empty.rb b/lib/feeble/runtime/list_empty.rb deleted file mode 100644 index 1be02cb..0000000 --- a/lib/feeble/runtime/list_empty.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "singleton" - -module Feeble::Runtime - class ListEmpty - include Singleton - include ListProperties - - def cons(obj) - List.create obj - end - - def conj(obj) - cons obj - end - - def apnd(obj) - cons obj - end - - def ==(other) - other.is_a? self.class - end - - def to_a - [] - end - - def printable - "()" - end - end -end diff --git a/lib/feeble/runtime/list_functions.rb b/lib/feeble/runtime/list_functions.rb deleted file mode 100644 index ee15536..0000000 --- a/lib/feeble/runtime/list_functions.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Feeble::Runtime - class ListFunctions - attr_reader :nill - - def initialize(nill = ListEmpty.instance) - @nill = nill - end - - def cons(a, list) - list.cons(a) - end - - def car(list) - list.first - end - - def cdr(list) - list.rest - end - - def empty?(list) - list == nill - end - - def take(n, list) - return nill if empty?(list) || n == 0 - - cons( - car(list), - take(n - 1, cdr(list))) - end - end -end diff --git a/lib/feeble/runtime/list_properties.rb b/lib/feeble/runtime/list_properties.rb deleted file mode 100644 index c2c92f9..0000000 --- a/lib/feeble/runtime/list_properties.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Feeble::Runtime - module ListProperties - def first - @first - end - - def rest - return nill if @rest.nil? - @rest - end - - def count - return 0 if @count.nil? - @count - end - - def head - first - end - - def car - first - end - - def tail - rest - end - - def cdr - rest - end - - def apnd(*args) - args.reduce(self) { |list, obj| - self.class.new list.first, list.rest.apnd(obj), count: list.count + 1 - } - end - - def conj(*args) - args.reduce(self) { |list, obj| - self.class.new obj, list, count: list.count + 1 - } - end - - def empty? - nill == self - end - - def ==(other) - return false if self.class != other.class - same? self, other - end - - def eql?(other) - self == other - end - - def hash - @first.hash + @second.hash + :fbl_list.hash - end - - def nill - @nill || ListEmpty.instance - end - end -end diff --git a/lib/feeble/runtime/str.rb b/lib/feeble/runtime/str.rb deleted file mode 100644 index 5beeb01..0000000 --- a/lib/feeble/runtime/str.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Feeble::Runtime - class Str - include ListProperties - - def self.create(body) - body = String(body) - return StrEmpty.instance if body.length == 0 - - new body[0], create(body[1..-1]), count: body.length, body: body - end - - def initialize(char, rest = StrEmpty.instance, count: 1, body: nil) - @nill = StrEmpty.instance - @count = count - @first = String(char) - @rest = rest - @body = String(body || @first + String(rest)) - @fn = ListFunctions.new(nill) - end - - def cons(char) - if String(char).length > 1 - self.class.create String(char) + self.to_s - else - self.class.new char, self, count: count + 1 - end - end - - def to_s - @body.to_s - end - - def to_a - @body.split "" - end - - def to_print - and_more = count > 5 ? " ..." : "" - elements = fn.take(5, self) - "\"#{elements}#{and_more}\"" - end - - def inspect - "#{@body[0..20]} ...(str [#{@body.length} chars])" - end - - private - - attr_reader :fn - - def same?(list, other) - return true if list == StrEmpty.instance && other == StrEmpty.instance - list.first == other.first && same?(list.rest, other.rest) - end - end -end diff --git a/lib/feeble/runtime/str_empty.rb b/lib/feeble/runtime/str_empty.rb deleted file mode 100644 index 5c1a064..0000000 --- a/lib/feeble/runtime/str_empty.rb +++ /dev/null @@ -1,36 +0,0 @@ -require "singleton" - -module Feeble::Runtime - class StrEmpty - include Singleton - include ListProperties - - def cons(char) - Str.create char - end - - def conj(char) - cons char - end - - def apnd(char) - cons char - end - - def ==(other) - other.is_a? self.class - end - - def to_a - [] - end - - def to_s - "" - end - - def nill - StrEmpty.instance - end - end -end diff --git a/lib/feeble/runtime/symbol.rb b/lib/feeble/runtime/symbol.rb deleted file mode 100644 index 829ee19..0000000 --- a/lib/feeble/runtime/symbol.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Feeble::Runtime - class Symbol - include Feeble::Printer::Printable - - attr_reader :id - - def initialize(id) - @id = String(id).to_sym - end - - def ==(other) - return false if self.class != other.class - id == other.id - end - - def eql?(other) - self == other - end - - def hash - id.hash + :symbol.hash - end - - def to_s - id.to_s - end - end -end diff --git a/lib/feeble/runtime/tree.rb b/lib/feeble/runtime/tree.rb deleted file mode 100644 index 87eba58..0000000 --- a/lib/feeble/runtime/tree.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Feeble::Runtime - # TODO: Reimplement it in terms of a bitwise trie if the time comes. =) - class Tree - attr_accessor :value - attr_reader :children - - def initialize - @value = nil - @children = {} - end - - def search(pattern) - node = self - - pattern.each_char do |char| - if node.children.key? char - node = node.children[char] - else - return nil - end - end - - node.value - end - - def add(pattern, value) - stopped_at = nil - node = self - - pattern.each_char.with_index do |char, idx| - if node.children.key? char - node = node.children[char] - else - break stopped_at = idx - end - end - - # means the word is not here yet - if stopped_at - pattern[stopped_at..].each_char do |char| - node = node.children[char] = self.class.new - end - end - - node.value = value - end - end -end diff --git a/lib/feeble/runtime/verifier.rb b/lib/feeble/runtime/verifier.rb deleted file mode 100644 index ba7d697..0000000 --- a/lib/feeble/runtime/verifier.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Feeble::Runtime - class Verifier - def list?(obj) - obj.is_a?(List) || obj.is_a?(ListEmpty) - end - - def symbol?(obj) - obj.is_a? Feeble::Runtime::Symbol - end - - def keyword?(obj) - obj.is_a? Keyword - end - - def fn?(obj) - obj.is_a? Invokable - end - end -end diff --git a/lib/feeble/syntax/number_format_error.rb b/lib/feeble/syntax/number_format_error.rb new file mode 100644 index 0000000..89832d7 --- /dev/null +++ b/lib/feeble/syntax/number_format_error.rb @@ -0,0 +1,6 @@ +class Feeble::Syntax::NumberFormatError < StandardError + def initialize(invalid_number) + msg = "Numbers should end in a separator (like ' ' or ',').\n" + super msg + "Invalid number: #{invalid_number}" + end +end diff --git a/lib/feeble/version.rb b/lib/feeble/version.rb index cf245c0..0805f90 100644 --- a/lib/feeble/version.rb +++ b/lib/feeble/version.rb @@ -1,3 +1,3 @@ module Feeble - VERSION = "0.1.0" + VERSION = "0.2.0" end diff --git a/spec/example.fbl b/spec/example.fbl index 397ddb7..079eb20 100644 --- a/spec/example.fbl +++ b/spec/example.fbl @@ -1,6 +1,7 @@ ;; This is a comment. ;; +;; ----- ;; Host (Ruby) Interop: ::puts(1) @@ -21,12 +22,16 @@ ;; => [] ::Array -;; => %host.Array +;; => ::Array ;; ----- - ;; Functions: +sum = -> augend, addend { + {special: false} ;; map with meta data about the function + augend + addend ;; evaluation of symbols invoke of + with two params +} + ;; binop means it can be invoked as "lhs fn rhs" form + = -> augend, addend { binop: true } { % augend + addend ;; delegates to the host: @@ -35,14 +40,9 @@ ;; the rest is all parameters } -sum = -> augend, addend { - {special: false} ;; map with meta data about the function - augend + addend ;; evaluation of symbols invoke of + with two params -} - ;; recursive function rember = -> atom list { ;; commas or spaces are "the same" - return '() { if null?(list) } - return rest(list) { if eq?(list(first), atom) } - cons(list(first), rember(atom, rest(list)) + return '() if: null?(list) + return rest(list) if: list(first) == atom + cons(first, rember(atom, rest(list)) } diff --git a/spec/feeble/evaler/lispey_spec.rb b/spec/feeble/evaler/lispey_spec.rb deleted file mode 100644 index 860ec2c..0000000 --- a/spec/feeble/evaler/lispey_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Feeble::Evaler - include ::Feeble::Runtime - - RSpec.describe Lispey do - subject(:evaler) { described_class.new } - - context "quoting" do - it "knows how to apply the 'quote' function" do - expression = List.create(Symbol.new("quote"), Symbol.new("a")) - - expect(evaler.eval(expression)).to eq Symbol.new("a") - end - - it "knows how to apply the 'quote' function to a List" do - expression = List.create(Symbol.new("quote"), List.create(Symbol.new("a"))) - - expect(evaler.eval(expression)).to eq List.create(Symbol.new("a")) - end - end - - context "resolving symbols" do - it "evaluates a Symbol to it's (env) underlying value" do - env = Env.new - env.register Symbol.new("lol"), true - - expect(evaler.eval(Symbol.new("lol"), env: env)).to eq true - end - end - - context "defining values" do - it "associates a value with a symbol/name" do - expression = List.create( - Symbol.new("define"), Symbol.new("lol"), "bbq") - env = Env.new(Feeble::Language::Ruby::Fbl.new) - evaler.eval(expression, env: env) - - expect(env.lookup(Symbol.new("lol"))).to eq "bbq" - end - end - end -end diff --git a/spec/feeble/language/ruby/define_spec.rb b/spec/feeble/language/ruby/define_spec.rb deleted file mode 100644 index 5a896b8..0000000 --- a/spec/feeble/language/ruby/define_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Feeble::Language::Ruby - RSpec.describe Define do - - subject(:definer) { described_class.new } - - describe "#invoke" do - context "when receiving and env and an array with two elements" do - it "treats the 2nd and 3rd params as symbol and value and register it on env" do - external_env = Feeble::Runtime::Env.new - fn_env = Feeble::Runtime::Env.new - fn_env.register Symbol.new("%xenv"), external_env - definer.invoke(Feeble::Runtime::Symbol.new(:lol), 123, scope: fn_env) - - expect(external_env.lookup(Feeble::Runtime::Symbol.new(:lol))).to eq 123 - end - end - end - end -end diff --git a/spec/feeble/language/ruby/lambda_spec.rb b/spec/feeble/language/ruby/lambda_spec.rb deleted file mode 100644 index 5d0144e..0000000 --- a/spec/feeble/language/ruby/lambda_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Feeble::Language::Ruby - include Feeble::Runtime - - RSpec.describe Lambda do - subject(:creator) { described_class.new } - - it "returns an invokable" do - new_lambda = creator.invoke([], []) - expect(new_lambda.is_a?(Invokable)).to eq(true) - end - - it "returns an invokable with arity related to the passed params" do - zero_arity = creator.invoke([1]) - expect(zero_arity.invoke).to eq 1 - - one_arity = creator.invoke([Symbol.new("a")], [2]) - expect(one_arity.invoke(1)).to eq 2 - - two_arity = creator.invoke([ - Symbol.new("a"), Symbol.new("b")], [3]) - expect(two_arity.invoke(1, 2)).to eq 3 - - var_args = creator.invoke([ - Symbol.new("*all")], [4]) - expect(var_args.invoke(1, 2, 3)).to eq 4 - expect(var_args.invoke(1, 2, 3, 4)).to eq 4 - end - - it "creates a lambda with properties" - end -end diff --git a/spec/feeble/language/ruby/print_spec.rb b/spec/feeble/language/ruby/print_spec.rb deleted file mode 100644 index 26922e3..0000000 --- a/spec/feeble/language/ruby/print_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Feeble::Language::Ruby - RSpec.describe Print do - subject(:printer) { described_class.new } - - describe "one argument" do - it "send the argument as it is to stdout" do - expect { - printer.invoke "omg" - }.to output("omg").to_stdout - end - end - end -end diff --git a/spec/feeble/language/ruby/println_spec.rb b/spec/feeble/language/ruby/println_spec.rb deleted file mode 100644 index 41f3a66..0000000 --- a/spec/feeble/language/ruby/println_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Feeble::Language::Ruby - RSpec.describe Println do - subject(:printer) { described_class.new } - - describe "one argument" do - it "send the argument as it is to stdout" do - expect { - printer.invoke "omg" - }.to output("omg\n").to_stdout - end - end - end -end diff --git a/spec/feeble/language/ruby/quote_spec.rb b/spec/feeble/language/ruby/quote_spec.rb deleted file mode 100644 index 898827f..0000000 --- a/spec/feeble/language/ruby/quote_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Feeble::Language::Ruby - include Feeble::Runtime - RSpec.describe Quote do - subject(:quoter) { described_class.new } - - it "when receives a list, return it as it is" do - list = List.create(Symbol.new("a"), Symbol.new("b")) - result = quoter.invoke list - - expect(result).to eq List.create(Symbol.new("a"), Symbol.new("b")) - end - end -end diff --git a/spec/feeble/printer/expression_spec.rb b/spec/feeble/printer/expression_spec.rb deleted file mode 100644 index a1c749d..0000000 --- a/spec/feeble/printer/expression_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "stringio" - -module Feeble::Printer - include Feeble::Runtime - - RSpec.describe Expression do - subject(:printer) { described_class.new } - - describe "#print" do - it "prints to a specific output stream" do - output = StringIO.new - printer.print(Keyword.new("a:"), to: output) - - expect(output.string).to eq " > a:" - end - - it "uses stdout by default" do - expect { - printer.print(Keyword.new("a:")) - }.to output(" > a:").to_stdout - end - end - end -end diff --git a/spec/feeble/pushback_reader_spec.rb b/spec/feeble/pushback_reader_spec.rb new file mode 100644 index 0000000..2b26d5d --- /dev/null +++ b/spec/feeble/pushback_reader_spec.rb @@ -0,0 +1,55 @@ +RSpec.describe Feeble::PushbackReader do + let(:content) { StringIO.new("OMG lol BBQ, so much chars!\nSome more chars.") } + subject(:reader) { described_class.new content } + + describe "#next" do + it "reads the next char from io" do + expect(reader.next).to eq "O" + expect(reader.next).to eq "M" + expect(reader.next).to eq "G" + end + end + + describe "#push" do + it "pushes a string 'back' to the reader" do + reader.next + reader.next + reader.next + + reader.push "Zing!" + + expect(reader.next).to eq "Z" + expect(reader.next).to eq "i" + expect(reader.next).to eq "n" + expect(reader.next).to eq "g" + expect(reader.next).to eq "!" + + expect(reader.next).to eq " " + expect(reader.next).to eq "l" + expect(reader.next).to eq "o" + expect(reader.next).to eq "l" + end + end + + describe "#peek" do + let(:content) { StringIO.new("OMG") } + + it "shows the next char without poping it from the buffer" do + expect(reader.peek).to eq "O" + expect(reader.next).to eq "O" + end + + it "reveals N chars, given by param, without popping them from buffer" do + expect(reader.peek(3)).to eq "OMG" + expect(reader.next).to eq "O" + end + + it "returns nil if no more chars available" do + reader.next + reader.next + reader.next + + expect(reader.peek).to eq nil + end + end +end diff --git a/spec/feeble/reader/char_spec.rb b/spec/feeble/reader/char_spec.rb deleted file mode 100644 index c309113..0000000 --- a/spec/feeble/reader/char_spec.rb +++ /dev/null @@ -1,152 +0,0 @@ -module Feeble::Reader - RSpec.describe Char do - let(:string_to_read) { "omg lol bbq" } - subject(:reader) { described_class.new string_to_read } - - describe "#next" do - it "'consumes' a character" do - expect(reader.next).to eq "o" - expect(reader.next).to eq "m" - end - - it "returns nil when there is no more characters left" do - string_to_read.split("").each { |_| reader.next } - - expect(reader.next).to eq nil - end - end - - describe "#eof?" do - it "returns false if there are still chars to consume" do - reader.next - - expect(reader.eof?).to eq false - end - - it "returns true if everything was consumed" do - string_to_read.split("").each { |_| reader.next } - - expect(reader.eof?).to eq true - end - - it "doesn't each anything while checking" do - reader.next # start consuming, first is "o" - reader.eof? - - expect(reader.current).to eq "o" - - reader.eof? - - expect(reader.current).to eq "o" - end - end - - describe "#current" do - it "knows the current char" do - reader.next # "o" - reader.next # "m" - - expect(reader.current).to eq "m" - end - - it "returns nil if not started" do - expect(reader.current).to eq nil - - reader.next - - expect(reader.current).to eq "o" - end - - it "returns nil if eof" do - string_to_read.split("").each { |_| reader.next } - reader.next - - expect(reader.current).to eq nil - end - end - - describe "#peek" do - it "allows look the next char without eating it" do - expect(reader.peek).to eq "o" - - reader.next - - expect(reader.peek).to eq "m" - end - end - - describe "#start" do - it "moves cursor to the first char" do - expect(reader.current).to eq nil - - expect(reader.start).to eq "o" - expect(reader.current).to eq "o" - end - - it "doesn't move the cursor if reading is already started" do - expect(reader.start).to eq "o" - expect(reader.start).to eq "o" - expect(reader.current).to eq "o" - end - end - - describe "#until_next" do - subject(:reader) { described_class.new('"omg lol bbq"nice') } - - before { reader.start } - - it "consumes the IO until find the parameter char" do - reader.next - - expect(reader.until_next('"')).to eq "omg lol bbq" - expect(reader.current).to eq '"' - end - - it "raises if char is not found" do - expect { - reader.until_next ")" - }.to raise_error "Expected ) but nothing was found" - end - - it "accepts a condition to 'stop'" do - reader = described_class.new("omg lol:bbq") - string = reader.until_next("omg lol:bbq") { |char| char == ":" } - - expect(string).to eq "omg lol" - end - - it "accepts a flag for 'or pattern or eof?'" do - reader = described_class.new("omg lol") - string = reader.until_next("|", or_eof: true) - - expect(string).to eq "omg lol" - end - end - - describe "#prev" do - it "returns the previous char before the current one" do - reader.next - reader.next - - expect(reader.prev).to eq "o" - end - - it "returns nil if first char" do - reader.next - - expect(reader.prev).to eq nil - end - - it "returns nil if not started" do - expect(reader.prev).to eq nil - end - - it "returns last char if eof?" do - reader.until_next("q") - reader.next - - expect(reader.prev).to eq "q" - end - end - end -end diff --git a/spec/feeble/reader/code_spec.rb b/spec/feeble/reader/code_spec.rb deleted file mode 100644 index d68dd47..0000000 --- a/spec/feeble/reader/code_spec.rb +++ /dev/null @@ -1,157 +0,0 @@ -module Feeble::Reader - include ::Feeble::Runtime - - RSpec.describe Code do - subject(:reader) { described_class.new } - - context "Quoting" do - it "reads ' syntax into a (quote ...) invokation" do - expect(reader.read "'(a)").to eq [ - List.create( - Symbol.new("quote"), - List.new(Symbol.new("a"))) - ] - end - end - - context "Keywords" do - it "recognizes an atom in the wild" do - expect(reader.read "a:").to eq [ - Keyword.new("a:") - ] - end - end - - context "Booleans" do - it "recognizes boolean values (as 'host' Boolean)" do - expect(reader.read(" true ")).to eq [true] - expect(reader.read(" false ")).to eq [false] - end - end - - context "Maps" do - it "recognizes maps (as 'host' Hash)" do - expect(reader.read(" {a: true b: false} ")).to eq [ - { - Keyword.new("a:") => true, - Keyword.new("b:") => false - } - ] - - expect(reader.read(" {a: true, b: false} ")).to eq [ - { - Keyword.new("a:") => true, - Keyword.new("b:") => false - } - ] - end - - it "recognizes nested maps" do - expect(reader.read("{a: true b: {c: false}}")).to eq [ - { - Keyword.new("a:") => true, - Keyword.new("b:") => { - Keyword.new("c:") => false - } - } - ] - end - end - - context "Function invokation" do - it "recognizes the list invokation pattern" do - code = "(quote (a:))" - - expect(reader.read(code)).to eq [ - List.create( - Symbol.new("quote"), - List.create(Keyword.new("a:"))) - ] - end - - it "recognizes nested lists" do - code = "(quote (a: (b: (c:))))" - - expect(reader.read(code)).to eq [ - List.create( - Symbol.new("quote"), - List.create( - Keyword.new("a:"), - List.create( - Keyword.new("b:"), - List.create(Keyword.new("c:"))))) - ] - end - - it "recognizes the 'c-like' invokation pattern" do - code = "define(omg, true)" - - expect(reader.read(code)).to eq [ - List.create( - Symbol.new("define"), - Symbol.new("omg"), - true - ) - ] - end - end - - context "Function declaration" do - it "recognizes lambda without params" do - expect(reader.read "-> {a:}").to eq [ - List.create( - Symbol.new("lambda"), [Keyword.new("a:")]) - ] - end - - it "recognizes lambda with some body" do - expect(reader.read "-> { yes: }").to eq [ - List.create( - Symbol.new("lambda"), [Keyword.new("yes:")]) - ] - end - - it "recognizes lambda with params" do - expect(reader.read "-> a, b { true }").to eq [ - List.create( - Symbol.new("lambda"), - [Symbol.new("a"), Symbol.new("b")], - [true] - ) - ] - end - - it "recognizes lambda with params and meta-data" do - expect(reader.read "-> a, b {special: false} { true }").to eq [ - List.create( - Symbol.new("lambda"), - [Symbol.new("a"), Symbol.new("b")], - [true], - {Keyword.new("special:") => false}) - ] - end - end - - context "Numbers" do - it "recognizes integers" do - expect(reader.read "1").to eq [1] - expect(reader.read "1_001").to eq [1001] - expect(reader.read "-1").to eq [-1] - expect(reader.read "-100_1").to eq [-1001] - end - - it "recognizes floats" do - expect(reader.read "4.2").to eq [4.2] - expect(reader.read "4_200.1").to eq [4200.1] - expect(reader.read "-4.2").to eq [-4.2] - expect(reader.read "-4_200.1").to eq [-4200.1] - end - end - - context "String" do - it "regonizes the \" string delimiter" do - expect(reader.read "\"lol\"").to eq ["lol"] - end - end - end -end diff --git a/spec/feeble/reader/delimited_literals_spec.rb b/spec/feeble/reader/delimited_literals_spec.rb new file mode 100644 index 0000000..ed50236 --- /dev/null +++ b/spec/feeble/reader/delimited_literals_spec.rb @@ -0,0 +1,96 @@ +RSpec.describe Feeble::Reader do + subject(:reader) { described_class.new } + + describe "#next" do + context "delimited literals" do + context "strings" do + it "recognizes strings" do + token = reader.next "\"omg, lol! a string.\"" + + expect(token).to be_a_token(:string, "omg, lol! a string.") + expect(token[1].keys).to_not include :open + end + + it "returns 'open' string if no closing delimiter is found" do + token = reader.next "\"omg, lol! a string." + + expect(token).to be_a_token( + :string, + "omg, lol! a string.", + {open: true} + ) + end + end + + context "comments" do + it "recognizes comments" do + token = reader.next ";\"a comment.\"" + + expect(token).to be_a_token :comment, "\"a comment.\"" + end + + it "recognizes comments ended by new line" do + token = reader.next ";some other comment\n" + + expect(token).to be_a_token :comment, "some other comment" + end + end + + context "vectors" do + it "recongizes vectors" do + token = reader.next "[\"str 1\"]" + + expect(token).to be_a_token :vector + expect(token[0]).to include a_token(:string, "str 1") + end + + it "recongizes vectors ignoring separators" do + token = reader.next "[\"str 1\" \"str 2\" \"str 3\"\n\"str 4\"]" + + expect(token).to be_a_token :vector + expect(token[0]).to include( + a_token(:string, "str 1"), + a_token(:separator, " "), + a_token(:string, "str 2"), + a_token(:separator, " "), + a_token(:separator, " "), + a_token(:string, "str 3"), + a_token(:new_line, "\n"), + a_token(:string, "str 4"), + ) + end + end + + context "blocks" do + it "recognizes blocks" do + token = reader.next "{\"one\",\"block\"\n\"three strings\"}" + + expect(token).to be_a_token :block + expect(token[0]).to include( + a_token(:string, "one"), + a_token(:separator, ","), + a_token(:string, "block"), + a_token(:new_line, "\n"), + a_token(:string, "three strings"), + ) + end + end + + context "lists" do + it "recongnizes lists" do + token = reader.next "(\"and\" \"now\" \"a list\"\n)" + + expect(token).to be_a_token :list + expect(token[0].head).to be_a_token(:string, "and") + expect(token[0].tail.to_a).to include( + a_token(:separator, " "), + a_token(:string, "now"), + a_token(:separator, " "), + a_token(:string, "a list"), + a_token(:new_line, "\n"), + ) + end + end + end + end +end diff --git a/spec/feeble/reader/number_spec.rb b/spec/feeble/reader/number_spec.rb deleted file mode 100644 index b9f328f..0000000 --- a/spec/feeble/reader/number_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Feeble::Reader - RSpec.describe Number do - let(:str) { Feeble::Runtime::Str } - let(:empty) { Feeble::Runtime::StrEmpty.instance } - subject(:reader) { described_class.new } - - context "recognizing numbers" do - it "recognizes integers" do - expect(reader.invoke str.create("1")).to eq [1, empty] - expect(reader.invoke str.create("1_001")).to eq [1001, empty] - expect(reader.invoke str.create("-1")).to eq [-1, empty] - expect(reader.invoke str.create("-100_1")).to eq [-1001, empty] - end - - it "recognizes floats" do - expect(reader.invoke str.create("4.2")).to eq [4.2, empty] - expect(reader.invoke str.create("4_200.1")).to eq [4200.1, empty] - expect(reader.invoke str.create("-4.2")).to eq [-4.2, empty] - expect(reader.invoke str.create("-4_200.1")).to eq [-4200.1, empty] - end - end - - context "when there are more things in the source code" do - it "returns the number found and 'stops'" do - code = str.create("1_2.1 something else") - rest = str.create(" something else") - - expect(reader.invoke(code)).to eq [12.1, rest] - end - - it "returns nil if first thing isn't a number and 'stops'" do - code = str.create("not a number") - - expect(reader.invoke(code)).to eq [nil, code] - end - end - end -end diff --git a/spec/feeble/reader/numbers_spec.rb b/spec/feeble/reader/numbers_spec.rb new file mode 100644 index 0000000..b3dd827 --- /dev/null +++ b/spec/feeble/reader/numbers_spec.rb @@ -0,0 +1,38 @@ +RSpec.describe Feeble::Reader do + subject(:reader) { described_class.new } + + describe "#next" do + it "recognizes integers" do + expect(reader.next "1").to be_a_token :int, 1 + expect(reader.next "2").to be_a_token :int, 2 + end + + it "recognizes integers ignoring _ in the middle" do + expect(reader.next "1_2").to be_a_token :int, 12 + expect(reader.next "0_2").to be_a_token :int, 2 + end + + it "recognizes negative integers" do + expect(reader.next "-1").to be_a_token :int, -1 + expect(reader.next "-12").to be_a_token :int, -12 + expect(reader.next "-4_2").to be_a_token :int, -42 + end + + it "recognizes float numbers" do + expect(reader.next "1.2").to be_a_token :float, 1.2 + expect(reader.next "0.2").to be_a_token :float, 0.2 + expect(reader.next "0.4_2").to be_a_token :float, 0.42 + expect(reader.next "-4.2").to be_a_token :float, -4.2 + expect(reader.next "-1.6_9").to be_a_token :float, -1.69 + end + + it "raises if the number is not delimited by a separator" do + msg = "Numbers should end in a separator (like ' ' or ',').\n" + + expect { reader.next "1a" }.to raise_error msg + "Invalid number: 1a" + expect { reader.next "4.2a" }.to raise_error msg + "Invalid number: 4.2a" + expect { reader.next "-1a" }.to raise_error msg + "Invalid number: -1a" + expect { reader.next "-4.2a" }.to raise_error msg + "Invalid number: -4.2a" + end + end +end diff --git a/spec/feeble/reader/read_spec.rb b/spec/feeble/reader/read_spec.rb deleted file mode 100644 index 70d0913..0000000 --- a/spec/feeble/reader/read_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Feeble::Reader - RSpec.describe Read do - subject(:reader) { described_class.new } - - it "reads symbols into a list of forms" do - expect(reader.invoke("omg lol bbq")).to eq [ - Feeble::Runtime::Symbol.new("omg"), - Feeble::Runtime::Symbol.new("lol"), - Feeble::Runtime::Symbol.new("bbq"), - ] - end - - it "reads numbers into... numbers" do - expect(reader.invoke("omg 1_2.1 -4 420 lol")).to eq [ - Feeble::Runtime::Symbol.new("omg"), - 12.1, - -4, - 420, - Feeble::Runtime::Symbol.new("lol"), - ] - end - - it "reads keywords into... keywords" do - expect(reader.invoke("omg :lol bbq:")).to eq [ - Feeble::Runtime::Symbol.new("omg"), - Feeble::Runtime::Keyword.new(":lol"), - Feeble::Runtime::Keyword.new("bbq:"), - ] - end - end -end diff --git a/spec/feeble/reader/seaparators_spec.rb b/spec/feeble/reader/seaparators_spec.rb new file mode 100644 index 0000000..7030ba9 --- /dev/null +++ b/spec/feeble/reader/seaparators_spec.rb @@ -0,0 +1,31 @@ +RSpec.describe Feeble::Reader do + subject(:reader) { described_class.new } + + describe "#next" do + context "separators" do + context "new lines" do + it "regonizes new lines" do + token = reader.next "\n" + + expect(token).to be_a_token :new_line, "\n" + end + end + + context "space" do + it "recognizes spaces as separators" do + token = reader.next " " + + expect(token).to be_a_token :separator, " " + end + end + + context "comma" do + it "recognizes commas as separators" do + token = reader.next "," + + expect(token).to be_a_token :separator, "," + end + end + end + end +end diff --git a/spec/feeble/reader_spec.rb b/spec/feeble/reader_spec.rb new file mode 100644 index 0000000..f28496d --- /dev/null +++ b/spec/feeble/reader_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe Feeble::Reader do + subject(:reader) { described_class.new } + + describe "#call" do + it "reads all tokens in a given string" do + code = "\"this code\" \"has two strings\"\n[\"and\" \"a\" \"vector\"]" + tokens = reader.call code + vector_tokens = tokens[4][0] + + expect(tokens).to include( + a_token(:string, "this code"), + a_token(:separator, " "), + a_token(:string, "has two strings"), + a_token(:new_line, "\n"), + a_token(:vector), + ) + + expect(vector_tokens).to include( + a_token(:string, "and"), + a_token(:separator, " "), + a_token(:string, "a"), + a_token(:separator, " "), + a_token(:string, "vector"), + ) + end + end +end diff --git a/spec/feeble/runtime/argument_type_mismatch.rb b/spec/feeble/runtime/argument_type_mismatch.rb deleted file mode 100644 index 7e199b1..0000000 --- a/spec/feeble/runtime/argument_type_mismatch.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Feeble::Runtime - class ArgumentTypeMismatch < StandardError - def initialize(given_type, expected_type) - super "< #{expected_type.class} > received while expecting < #{expected_type} >" - end - end -end diff --git a/spec/feeble/runtime/atom_spec.rb b/spec/feeble/runtime/atom_spec.rb deleted file mode 100644 index 04abd0b..0000000 --- a/spec/feeble/runtime/atom_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Feeble::Runtime - RSpec.describe Keyword do - it "recognizes equality based on value/id" do - this = Keyword.new("lol") - that = Keyword.new("lol") - - expect(this).to eq that - end - - it "can be used as hash key" do - hash = { - Keyword.new("lol") => 1, - Keyword.new("bbq") => 2 - } - - expect(hash[Keyword.new("lol")]).to eq 1 - expect(hash[Keyword.new("bbq")]).to eq 2 - end - end -end diff --git a/spec/feeble/runtime/env_spec.rb b/spec/feeble/runtime/env_spec.rb deleted file mode 100644 index 216e1aa..0000000 --- a/spec/feeble/runtime/env_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -module Feeble::Runtime - RSpec.describe Env do - subject(:env) { described_class.new } - - context "#register and #lookup" do - it "associates a key with an object" do - env.register Symbol.new(:omg), "lol" - - expect(env.lookup(Symbol.new(:omg))).to eq "lol" - end - - it "registers functions on a specific 'area'" do - fn = Feeble::Language::Ruby::Print.new - env.register Symbol.new(:fn), fn - - expect(env.lookup(Symbol.new(:fn))).to eq fn - expect(env.fn(:fn)).to eq fn - expect(env.fn("fn")).to eq fn - expect(env.fn(Symbol.new(:fn))).to eq fn - end - end - - describe "#lookup" do - it "fetches the value associated with a symbol" do - env.register Symbol.new(1), "omg" - - expect(env.lookup(Symbol.new(1))).to eq "omg" - end - - it "fallbacks to the 'around' environment if symbol not found" do - env.register Symbol.new("lol"), "bbq" - inner = Env.new env - - expect(inner.lookup(Symbol.new("lol"))).to eq "bbq" - end - end - - describe "#fn" do - it "returns nil if no function with the given name is registered" do - expect(env.fn(:nope)).to eq nil - end - end - end -end diff --git a/spec/feeble/runtime/invokable_spec.rb b/spec/feeble/runtime/invokable_spec.rb deleted file mode 100644 index 7cc8346..0000000 --- a/spec/feeble/runtime/invokable_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -require "feeble/runtime/argument_type_mismatch" - -module Feeble::Runtime - class SomeFunction - include Invokable - end - - RSpec.describe SomeFunction do - subject(:fn) { described_class.new } - - describe "#arity" do - it "only accepts symbols" do - expect { - fn.arity(:omg) - }.to raise_error ArgumentTypeMismatch - # TODO: why Zeitwerk doesn't find this one without the require? - # might be a bug... - end - end - - describe "#invoke" do - it "returns the value returned by the block" do - fn.arity(Symbol.new("a")) { 1 } - - expect(fn.invoke(nil)).to eq 1 - end - - it "raises if the function doesn't have the given arity" do - fn.arity(Symbol.new("only_one")) { 1 } - - expect { fn.invoke }.to raise_error ArityMismatch - end - end - - context "Invoke routing via arity" do - it "routes the invokation to the correct arity" do - arity_one = false - fn.arity(Symbol.new("a")) { |env| - arity_one = true - } - - bigger_arity = false - fn.arity(Symbol.new("a"), Symbol.new("b")) { |env| - bigger_arity = true - } - - fn.invoke 1 - expect(arity_one).to eq true - expect(bigger_arity).to eq false - - arity_one = false - fn.invoke 1, 2 - expect(arity_one).to eq false - expect(bigger_arity).to eq true - end - - it "creates variables named after the arguments declared" do - value = nil - fn.arity(Symbol.new("a")) { |env| - value = env.lookup(Symbol.new("a")) - } - - fn.invoke 1 - expect(value).to eq 1 - end - - context "varargs" do - it "allow create a varargs function using the splat syntax" do - values = nil - fn.arity(Symbol.new("*stuff")) { |env| - values = env.lookup(Symbol.new("stuff")) - } - - fn.invoke 1, 2, 3, 4 - expect(values).to eq [1, 2, 3, 4] - - fn.invoke - expect(values).to eq [] - - fn.invoke "1", "2" - expect(values).to eq ["1", "2"] - end - end - - context "scope" do - it "creates an environment for the invokation" do - value = nil - outter_scope = Env.new - outter_scope.register Symbol.new("lol"), true - fn.arity(Symbol.new("lol")) { |env| - value = env.lookup Symbol.new("lol") - } - - fn.invoke(false, scope: outter_scope) - expect(value).to eq false - end - - it "fallback to the external scope" do - value = nil - outter_scope = Env.new - outter_scope.register Symbol.new("lol"), true - fn.arity(Symbol.new("dont_override")) { |env| - value = env.lookup Symbol.new("lol") - } - - fn.invoke(false, scope: outter_scope) - expect(value).to eq true - end - end - end - - context "Properties and execution strategy" do - it "allows to mark an invokable as special" do - fn.prop(:special) - - expect(fn.prop?(:special)).to eq true - end - end - end -end diff --git a/spec/feeble/runtime/list_functions_spec.rb b/spec/feeble/runtime/list_functions_spec.rb deleted file mode 100644 index 1a1d069..0000000 --- a/spec/feeble/runtime/list_functions_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Feeble::Runtime - RSpec.describe ListFunctions do - subject(:fn) { described_class.new } - - context "The building blocks" do - let(:list) { List.create 1, 2, 3, 4, 5 } - - describe "#cons" do - it "consing a element to a list" do - expect(fn.cons(0, list)).to eq List.create(0, 1, 2, 3, 4, 5) - end - end - - describe "#car" do - it "returns the head of the list" do - expect(fn.car(list)).to eq 1 - end - end - - describe "#cdr" do - it "returns the tail of the list" do - expect(fn.cdr(list)).to eq List.create(2, 3, 4, 5) - end - end - - describe "#nill" do - it "returns a nil(l)ist" do - expect(fn.nill).to eq ListEmpty.instance - end - end - end - - describe "#empty?" do - it "returns true for an empty list" do - expect(fn.empty?(List.new(1).rest)).to eq true - expect(fn.empty?(ListEmpty.instance)).to eq true - end - - it "returns false for a list with one or more elements" do - expect(fn.empty?(List.new(1))).to eq false - end - end - - describe "#take" do - it "returns a new listw with the first N elements of a list" do - list = List.create 1, 2, 3, 4, 5 - expect(fn.take(3, list)).to eq List.create(1, 2, 3) - end - - it "returns the whole list if N > list.count" do - list = List.create 1, 2, 3, 4, 5 - expect(fn.take(30, list)).to eq List.create(1, 2, 3, 4, 5) - end - end - end -end diff --git a/spec/feeble/runtime/list_spec.rb b/spec/feeble/runtime/list_spec.rb deleted file mode 100644 index 40a11ac..0000000 --- a/spec/feeble/runtime/list_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -module Feeble::Runtime - RSpec.describe List do - subject(:list) { described_class } - - context "constructor function" do - it "creates a list with one element" do - new_list = list.create 1 - - expect(new_list).to eq list.new 1 - expect(new_list.count).to eq 1 - end - - it "creates a list with two elements" do - new_list = list.create 1, 2 - - expect(new_list).to eq list.new 1, list.new(2) - expect(new_list.count).to eq 2 - end - - it "creates a list with three elements" do - new_list = list.create 1, 2, 3 - - expect(new_list).to eq list.new 1, list.new(2, list.new(3)) - expect(new_list.count).to eq 3 - end - end - - describe "#first" do - it "returns the first item on the list" do - new_list = list.create(1, 2) - - expect(new_list.first).to eq 1 - end - - it "returns the first even when it is false XD" do - new_list = list.create(false, true) - - expect(new_list.first).to eq false - end - end - - describe "#rest" do - it "returns a list with all but the first element" do - new_list = list.create 1, 2, 3, 4 - - expect(new_list.rest).to eq list.create 2, 3, 4 - end - - it "returns an empty list if just one element exists" do - new_list = list.create 1 - - expect(new_list.rest).to eq ListEmpty.instance - end - - it "if rest is one element, returns a list with one element" do - new_list = list.create 1, 2 - - expect(new_list.rest).to eq list.create 2 - end - end - - describe "#cons" do - it "prepends an object" do - new_list = list.create(2, 3).cons 1 - - expect(new_list).to eq list.create 1, 2, 3 - expect(new_list.count).to eq 3 - end - - it "returns a new_list with just one element when cons(ing) to Empty List" do - new_list = ListEmpty.instance.cons 1 - - expect(new_list).to eq list.new 1 - expect(new_list.count).to eq 1 - end - - it "knows how to cons a list with a list" do - new_list = list.create(3, 4).cons list.create(1, 2) - - expect(new_list).to eq list.create(list.create(1, 2), 3, 4) - end - end - - describe "#conj" do - it "prepends arguments to the new_list" do - new_list = list.create(1, 2, 3, 4).conj 5, 6, 7 - - expect(new_list).to eq list.create 7, 6, 5, 1, 2, 3, 4 - expect(new_list.count).to eq 7 - end - - it "returns a new_list of one when conj(ing) empty new_list" do - new_list = ListEmpty.instance.conj 1 - - expect(new_list).to eq list.new 1 - expect(new_list.count).to eq 1 - end - end - - describe "#apnd" do - it "append arguments to the new_list" do - new_list = list.create(1, 2).apnd 3, 4, 5 - - expect(new_list).to eq list.create 1, 2, 3, 4, 5 - expect(new_list.count).to eq 5 - end - end - - describe "#to_a" do - it "transforms a list into a (ruby) array" do - expect(list.create(1, 2, 3, 4).to_a).to eq [1, 2, 3, 4] - end - end - end -end diff --git a/spec/feeble/runtime/str_spec.rb b/spec/feeble/runtime/str_spec.rb deleted file mode 100644 index 0943503..0000000 --- a/spec/feeble/runtime/str_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -module Feeble::Runtime - RSpec.describe Str do - let(:s) { ListFunctions.new } - let(:body) { "this is going to be... feeble" } - let(:str) { described_class } - subject(:string) { str.create body } - - context "behaves like a list" do - describe "#car" do - it "returns the first char" do - expect(string.car).to eq "t" - expect(string.first).to eq "t" - end - end - - describe "#rest" do - it "returns the remaining of the string" do - expect(string.cdr).to eq described_class.create(body[1..-1]) - expect(string.rest).to eq described_class.create(body[1..-1]) - end - - it "returns StrEmpty if there is no more chars" do - expect(described_class.create("").rest).to eq StrEmpty.instance - end - end - end - - describe "#cons" do - it "prepends a char" do - new_str = str.create("ol").cons "l" - - expect(new_str).to eq str.create "lol" - expect(new_str.count).to eq 3 - end - - it "returns a new str with just one element when cons(ing) to Empty String" do - new_str = StrEmpty.instance.cons "M" - - expect(new_str).to eq str.create "M" - expect(new_str.count).to eq 1 - end - - it "knows how to cons a str with a str" do - # This behavior is "as expected", but it differs from a list consing... - new_str = str.create("wzers").cons str.create("wo") - - expect(new_str).to eq str.create(str.create("wowzers")) - end - end - - describe "#take" do - it "returns a printable version of the string" do - expect(string.to_print).to eq "\"this ...\"" - end - end - end -end diff --git a/spec/feeble/runtime/symbol_spec.rb b/spec/feeble/runtime/symbol_spec.rb deleted file mode 100644 index 73cb909..0000000 --- a/spec/feeble/runtime/symbol_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -RSpec.describe Feeble::Runtime::Symbol do - describe "#id" do - it "symbolizes the id passed as a parameter" do - expect(described_class.new("omg").id).to eq :omg - expect(described_class.new(:lol).id).to eq :lol - expect(described_class.new(1).id).to eq :"1" - end - end - - describe "#==" do - it "consider symbols with the same id equal" do - expect(described_class.new("omg")).to eq described_class.new "omg" - expect(described_class.new(:lol)).to eq described_class.new :lol - end - end -end diff --git a/spec/feeble/runtime/tree_spec.rb b/spec/feeble/runtime/tree_spec.rb deleted file mode 100644 index 9177959..0000000 --- a/spec/feeble/runtime/tree_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Feeble::Runtime - RSpec.describe Tree do - - subject(:tree) { described_class.new } - - let(:eql) { Object.new } - let(:eqeq) { Object.new } - let(:eqeqeq) { Object.new } - let(:eqbang) { Object.new } - let(:eqtilde) { Object.new } - let(:stareq) { Object.new } - - before do - tree.add "=", eql - tree.add "==", eqeq - tree.add "===", eqeqeq - tree.add "=!", eqbang - tree.add "=~", eqtilde - tree.add "*=", stareq - end - - describe "#search" do - it "returns the object associated with a given pattern" do - expect(tree.search("=")).to be eql - expect(tree.search("==")).to be eqeq - expect(tree.search("===")).to be eqeqeq - expect(tree.search("=!")).to be eqbang - expect(tree.search("=~")).to be eqtilde - expect(tree.search("*=")).to be stareq - - expect(tree.search("*")).to be nil - end - end - end -end diff --git a/spec/feeble/runtime/verifier_spec.rb b/spec/feeble/runtime/verifier_spec.rb deleted file mode 100644 index 3f783bf..0000000 --- a/spec/feeble/runtime/verifier_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Feeble::Runtime - RSpec.describe Verifier do - subject(:verify) { described_class.new } - - describe "#list?" do - it "returns true only for Lists" do - expect(verify.list?(ListEmpty.instance)).to eq true - expect(verify.list?(List.new(1))).to eq true - expect(verify.list?(:omg)).to eq false - expect(verify.list?(nil)).to eq false - end - end - - describe "#symbol?" do - it "returns true only for Symbols" do - expect(verify.symbol?(Symbol.new(:omg))).to eq true - expect(verify.symbol?(:omg)).to eq false - expect(verify.symbol?(nil)).to eq false - end - end - - describe "#keyword?" do - it "returns true only for Keywords" do - expect(verify.keyword?(Keyword.new("omg:"))).to eq true - expect(verify.keyword?("omg:")).to eq false - expect(verify.keyword?(:"omg:")).to eq false - expect(verify.keyword?(:omg)).to eq false - expect(verify.keyword?(nil)).to eq false - end - end - - describe "#fn?" do - class SomeFN - include Feeble::Runtime::Invokable - end - - it "returns true only for Functions" do - expect(verify.fn?(SomeFN.new)).to eq true - expect(verify.fn?(:omg)).to eq false - expect(verify.fn?(nil)).to eq false - expect(verify.fn?(Proc.new {})).to eq false - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 596a9d8..861f680 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,3 +13,19 @@ c.syntax = :expect end end + +RSpec::Matchers.define :be_a_token do |type, value = nil, meta = nil| + match do |actual| + actual_value, actual_meta = actual + + return false if actual_meta[:type] != type + return true if ! value + + valid = actual_value == value + return valid if ! meta + + valid && expect(actual_meta).to(include(meta)) + end +end + +RSpec::Matchers.alias_matcher :a_token, :be_a_token