ruspea_lang 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|