ruspea_lang 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/ruspea +8 -0
- data/lib/example.rb +10 -0
- data/lib/example.rsp +23 -0
- data/lib/language/standard.rsp +77 -0
- data/lib/ruspea.rb +9 -0
- data/lib/ruspea/code.rb +19 -0
- data/lib/ruspea/error/arity.rb +7 -0
- data/lib/ruspea/error/resolution.rb +7 -0
- data/lib/ruspea/error/standard.rb +3 -0
- data/lib/ruspea/error/syntax.rb +4 -0
- data/lib/ruspea/interpreter/evaler.rb +67 -0
- data/lib/ruspea/interpreter/form.rb +27 -0
- data/lib/ruspea/interpreter/reader.rb +127 -0
- data/lib/ruspea/language/core.rb +185 -0
- data/lib/ruspea/language/ruby.rb +36 -0
- data/lib/ruspea/printer.rb +54 -0
- data/lib/ruspea/repl/loop.rb +85 -0
- data/lib/ruspea/repl/repl.rsp +2 -0
- data/lib/ruspea/runtime/env.rb +119 -0
- data/lib/ruspea/runtime/fn.rb +23 -0
- data/lib/ruspea/runtime/list.rb +67 -0
- data/lib/ruspea/runtime/lm.rb +74 -0
- data/lib/ruspea/runtime/nill.rb +54 -0
- data/lib/ruspea/runtime/sym.rb +28 -0
- data/lib/ruspea/version.rb +3 -0
- data/lib/ruspea_lang.rb +4 -0
- data/ruspea.gemspec +31 -0
- metadata +137 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module Ruspea::Language
|
2
|
+
class Ruby < Ruspea::Runtime::Env
|
3
|
+
# This method was copied as is from:
|
4
|
+
# https://github.com/rails/rails/blob/66cabeda2c46c582d19738e1318be8d59584cc5b/activesupport/lib/active_support/inflector/methods.rb#L271
|
5
|
+
def constantize(camel_cased_word)
|
6
|
+
names = camel_cased_word.split("::")
|
7
|
+
|
8
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
9
|
+
Object.const_get(camel_cased_word) if names.empty?
|
10
|
+
|
11
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
12
|
+
names.shift if names.size > 1 && names.first.empty?
|
13
|
+
|
14
|
+
names.inject(Object) do |constant, name|
|
15
|
+
if constant == Object
|
16
|
+
constant.const_get(name)
|
17
|
+
else
|
18
|
+
candidate = constant.const_get(name)
|
19
|
+
next candidate if constant.const_defined?(name, false)
|
20
|
+
next candidate unless Object.const_defined?(name)
|
21
|
+
|
22
|
+
# Go down the ancestors to check if it is owned directly. The check
|
23
|
+
# stops when we reach Object or the end of ancestors tree.
|
24
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
25
|
+
break const if ancestor == Object
|
26
|
+
break ancestor if ancestor.const_defined?(name, false)
|
27
|
+
const
|
28
|
+
end
|
29
|
+
|
30
|
+
# owner is in Object, so raise
|
31
|
+
constant.const_get(name, false)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Ruspea
|
2
|
+
class Printer
|
3
|
+
include Ruspea::Runtime
|
4
|
+
|
5
|
+
def call(value)
|
6
|
+
case value
|
7
|
+
when Ruspea::Interpreter::Form
|
8
|
+
call(value.value)
|
9
|
+
when String
|
10
|
+
value.inspect
|
11
|
+
when Numeric
|
12
|
+
value.inspect
|
13
|
+
when Sym
|
14
|
+
value.to_s
|
15
|
+
when List
|
16
|
+
print_list(value)
|
17
|
+
when Lm
|
18
|
+
print_fn(value)
|
19
|
+
else
|
20
|
+
value.inspect
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def print_list(list)
|
27
|
+
count = list.count
|
28
|
+
|
29
|
+
printed = list
|
30
|
+
.take(10)
|
31
|
+
.to_a
|
32
|
+
.map { |intern| call(intern) }
|
33
|
+
.join(" ")
|
34
|
+
|
35
|
+
if count > 10
|
36
|
+
"(#{printed} ...) # count: #{count}"
|
37
|
+
else
|
38
|
+
"(#{printed})"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def print_fn(fn)
|
43
|
+
params = fn.params.map { |param| call(param) }.join(" ")
|
44
|
+
body =
|
45
|
+
if fn.body.respond_to?(:call)
|
46
|
+
"#- internal -#\n #{fn.body.inspect}"
|
47
|
+
else
|
48
|
+
body = fn.body.to_a.map { |exp| call(exp) }.join("\n ")
|
49
|
+
end
|
50
|
+
|
51
|
+
"(fn [#{params}]\n #{body})"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Ruspea::Repl
|
2
|
+
class Loop
|
3
|
+
include Ruspea::Interpreter
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@reader = Reader.new
|
7
|
+
@evaler = Evaler.new
|
8
|
+
@printer = Ruspea::Printer.new
|
9
|
+
@env = Ruspea::Runtime::Env.new(Ruspea::Language::Core.new)
|
10
|
+
|
11
|
+
load_repl(@env)
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(input: STDIN, env: nil)
|
15
|
+
trap "SIGINT" do
|
16
|
+
puts "See you soon."
|
17
|
+
exit(130)
|
18
|
+
end
|
19
|
+
|
20
|
+
env ||= @env
|
21
|
+
prompt_back
|
22
|
+
|
23
|
+
while(line = input.gets)
|
24
|
+
begin
|
25
|
+
interprete(line.chomp, env)
|
26
|
+
prompt_back
|
27
|
+
rescue Ruspea::Error::Standard => e
|
28
|
+
print_error e
|
29
|
+
rescue StandardError => e
|
30
|
+
puts "A Ruby exception was raised. Inspect it? Y/n"
|
31
|
+
yes = !["n", "no"].include?(input.gets.chomp.downcase)
|
32
|
+
if yes
|
33
|
+
puts "Error: #{e.message}\nBacktrace:\n"
|
34
|
+
puts "\s\s#{e.backtrace.join("\n\s\s")}"
|
35
|
+
end
|
36
|
+
prompt_back
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "See you soon."
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def load_repl(env)
|
46
|
+
Dir.glob(
|
47
|
+
File.expand_path("../*.rsp", __FILE__)).each do |file|
|
48
|
+
puts "loading repl: #{Pathname.new(file).basename}"
|
49
|
+
_, forms = @reader.call(File.read(file))
|
50
|
+
@evaler.call(forms, context: env)
|
51
|
+
end
|
52
|
+
puts "repl loaded."
|
53
|
+
end
|
54
|
+
|
55
|
+
def prompt_back(ns: "#user")
|
56
|
+
puts ""
|
57
|
+
print "#{ns}=> "
|
58
|
+
end
|
59
|
+
|
60
|
+
def print_error(e)
|
61
|
+
puts e.class
|
62
|
+
puts e.message
|
63
|
+
prompt_back
|
64
|
+
end
|
65
|
+
|
66
|
+
def should_exit?(env)
|
67
|
+
env.call Ruspea::Runtime::Sym.new("%repl_should_exit?")
|
68
|
+
rescue Ruspea::Error::Resolution
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def interprete(line, env)
|
73
|
+
_, forms = @reader.call(line)
|
74
|
+
forms.each do |expression|
|
75
|
+
# puts "read: "
|
76
|
+
# pp expression
|
77
|
+
result = @evaler.call(expression, context: env)
|
78
|
+
# puts "evaled: "
|
79
|
+
# pp result
|
80
|
+
Process.kill("SIGINT", Process.pid) if should_exit?(env)
|
81
|
+
print @printer.call(result)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
module Ruspea::Runtime
|
4
|
+
class Env
|
5
|
+
class Empty
|
6
|
+
include Ruspea::Error
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize(*_)
|
10
|
+
end
|
11
|
+
|
12
|
+
def define(*_)
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def lookup(sym)
|
17
|
+
raise(Resolution.new(sym))
|
18
|
+
end
|
19
|
+
|
20
|
+
def eql?(other)
|
21
|
+
self == other
|
22
|
+
end
|
23
|
+
|
24
|
+
def around(env)
|
25
|
+
env
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
return true if other.is_a? Empty
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
include Ruspea::Error
|
35
|
+
|
36
|
+
def initialize(context = nil, table = nil)
|
37
|
+
@table =
|
38
|
+
if table.nil?
|
39
|
+
{}
|
40
|
+
else
|
41
|
+
table.dup
|
42
|
+
end
|
43
|
+
@context = context || Empty.instance
|
44
|
+
|
45
|
+
@fn = Fn.new(fn_define, fn_fetch)
|
46
|
+
end
|
47
|
+
|
48
|
+
def define(sym, value)
|
49
|
+
@table[sym] = value
|
50
|
+
end
|
51
|
+
|
52
|
+
def lookup(sym)
|
53
|
+
@table.fetch(sym) { @context.lookup(sym) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(*args, context: nil, evaler: nil)
|
57
|
+
# evaler will always send an array
|
58
|
+
# to a #call that is not a #arity
|
59
|
+
if args.is_a?(Array) && args.length == 1
|
60
|
+
args = args.first
|
61
|
+
end
|
62
|
+
@fn.call(*args, context: context, evaler: evaler)
|
63
|
+
end
|
64
|
+
|
65
|
+
def around(env)
|
66
|
+
new_context = env
|
67
|
+
.context
|
68
|
+
.around(self)
|
69
|
+
|
70
|
+
Env.new(new_context, env.table)
|
71
|
+
end
|
72
|
+
|
73
|
+
def eql?(other)
|
74
|
+
self == other
|
75
|
+
end
|
76
|
+
|
77
|
+
def ==(other)
|
78
|
+
return false if self.class != other.class
|
79
|
+
@table == other.table && @context == other.context
|
80
|
+
end
|
81
|
+
|
82
|
+
def hash
|
83
|
+
@table.hash + @context.hash + :rsp_env.hash
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
@table.map { |k, v|
|
88
|
+
"#{k} => #{v}"
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
attr_accessor :table, :context
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def fn_define
|
99
|
+
@fn_define ||= Lm.new(
|
100
|
+
params: [Sym.new("sym"), Sym.new("val")],
|
101
|
+
body: ->(env, _) {
|
102
|
+
define(
|
103
|
+
env.lookup(Sym.new("sym")),
|
104
|
+
env.lookup(Sym.new("val"))
|
105
|
+
)
|
106
|
+
}
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def fn_fetch
|
111
|
+
@fn_fetch ||= Lm.new(
|
112
|
+
params: [Sym.new("sym")],
|
113
|
+
body: ->(env, _) {
|
114
|
+
lookup env.lookup(Sym.new("sym"))
|
115
|
+
}
|
116
|
+
)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ruspea::Runtime
|
2
|
+
class Fn
|
3
|
+
def initialize(*lambdas)
|
4
|
+
@arities = {}
|
5
|
+
|
6
|
+
lambdas.each { |lm| add lm }
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(lm)
|
10
|
+
arities[lm.arity] = lm
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(*args, context: nil, evaler: nil)
|
14
|
+
arities
|
15
|
+
.fetch(args.length)
|
16
|
+
.call(*args, context: context, evaler: evaler)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :arities
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Ruspea::Runtime
|
2
|
+
class List
|
3
|
+
attr_reader :head, :tail, :count
|
4
|
+
|
5
|
+
def self.create(*items)
|
6
|
+
return Nill.instance if items.length == 0
|
7
|
+
|
8
|
+
new items[0], create(*items[1..items.length]), count: items.length
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(head, tail = Nill.instance, count: 0)
|
12
|
+
@head = head
|
13
|
+
@tail = tail
|
14
|
+
@count = count
|
15
|
+
end
|
16
|
+
|
17
|
+
def cons(el)
|
18
|
+
self.class.new el, self, count: @count + 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def car
|
22
|
+
head
|
23
|
+
end
|
24
|
+
|
25
|
+
def cdr
|
26
|
+
tail
|
27
|
+
end
|
28
|
+
|
29
|
+
def take(amount, from = self)
|
30
|
+
return Nill.instance if amount == 0 || from.empty?
|
31
|
+
|
32
|
+
from
|
33
|
+
.take(amount - 1, from.tail)
|
34
|
+
.cons(from.head)
|
35
|
+
end
|
36
|
+
|
37
|
+
def empty?
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_a(list = self, array = [])
|
42
|
+
return array if list.empty?
|
43
|
+
|
44
|
+
to_a(
|
45
|
+
list.tail,
|
46
|
+
array + [list.head])
|
47
|
+
end
|
48
|
+
|
49
|
+
def eq?(other)
|
50
|
+
self == other
|
51
|
+
end
|
52
|
+
|
53
|
+
def eql?(other)
|
54
|
+
self == other
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
return false if self.class != other.class
|
59
|
+
head == other.head && tail == other.tail
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
@printer ||= Ruspea::Printer.new
|
64
|
+
@printer.call self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Ruspea::Runtime
|
2
|
+
class Lm
|
3
|
+
attr_reader :arity, :params, :body
|
4
|
+
|
5
|
+
def initialize(params: [], body: Nill.instance, closure: Env::Empty.instance)
|
6
|
+
@params = params
|
7
|
+
@arity = params.length
|
8
|
+
@body = body
|
9
|
+
@closure = closure
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(*args, context: nil, evaler: nil)
|
13
|
+
context ||= Env::Empty.instance
|
14
|
+
evaler ||= Ruspea::Interpreter::Evaler.new
|
15
|
+
|
16
|
+
env, callable = env_and_callable_body(args, context, evaler)
|
17
|
+
env.define Sym.new("%ctx"), context
|
18
|
+
|
19
|
+
callable.call closure.around(env), evaler, args
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :closure
|
25
|
+
|
26
|
+
def env_and_callable_body(args, context, evaler)
|
27
|
+
if body.respond_to? :call
|
28
|
+
[
|
29
|
+
environment_with(args, context, evaler: ->(arg, _) { arg }),
|
30
|
+
->(environment, evaler, _) { body.call environment, evaler }
|
31
|
+
]
|
32
|
+
else
|
33
|
+
if args.length != arity
|
34
|
+
raise Ruspea::Error::Arity.new(arity, args.length)
|
35
|
+
end
|
36
|
+
|
37
|
+
[
|
38
|
+
environment = environment_with(args, context, evaler: evaler),
|
39
|
+
->(environment, evaler, args) {
|
40
|
+
evaluate(body, context: environment, evaler: evaler)
|
41
|
+
}
|
42
|
+
]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def environment_with(args, context, evaler:)
|
47
|
+
env = Env.new(context)
|
48
|
+
args.each_with_index.reduce(env) { |env, tuple|
|
49
|
+
arg, idx = tuple
|
50
|
+
sym =
|
51
|
+
if arg.is_a?(Ruspea::Interpreter::Form)
|
52
|
+
arg.value
|
53
|
+
else
|
54
|
+
arg
|
55
|
+
end
|
56
|
+
|
57
|
+
env.tap { |e|
|
58
|
+
e.define(
|
59
|
+
params[idx], evaler.call(sym, context: context))
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def evaluate(forms, result = nil, evaler:, context:)
|
65
|
+
return result if forms.empty?
|
66
|
+
|
67
|
+
evaluate(
|
68
|
+
forms.tail,
|
69
|
+
evaler.call(forms.head, context: context),
|
70
|
+
evaler: evaler,
|
71
|
+
context: context)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|