campa 0.1.0
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/.rubocop.yml +37 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +82 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +23 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/campa.gemspec +34 -0
- data/campa/core.cmp +59 -0
- data/campa/example.cmp +2 -0
- data/campa/test.cmp +2 -0
- data/exe/campa +7 -0
- data/lib/campa.rb +18 -0
- data/lib/campa/cli.rb +66 -0
- data/lib/campa/context.rb +42 -0
- data/lib/campa/core/load.rb +25 -0
- data/lib/campa/core/print.rb +20 -0
- data/lib/campa/core/print_ln.rb +17 -0
- data/lib/campa/core/test.rb +52 -0
- data/lib/campa/core/test_report.rb +59 -0
- data/lib/campa/error/arity.rb +11 -0
- data/lib/campa/error/illegal_argument.rb +9 -0
- data/lib/campa/error/invalid_number.rb +9 -0
- data/lib/campa/error/missing_delimiter.rb +9 -0
- data/lib/campa/error/not_a_function.rb +9 -0
- data/lib/campa/error/not_found.rb +9 -0
- data/lib/campa/error/parameters.rb +11 -0
- data/lib/campa/error/reserved.rb +11 -0
- data/lib/campa/error/resolution.rb +9 -0
- data/lib/campa/evaler.rb +106 -0
- data/lib/campa/execution_error.rb +3 -0
- data/lib/campa/lambda.rb +45 -0
- data/lib/campa/language.rb +33 -0
- data/lib/campa/lisp/atom.rb +14 -0
- data/lib/campa/lisp/cadr.rb +41 -0
- data/lib/campa/lisp/car.rb +22 -0
- data/lib/campa/lisp/cdr.rb +22 -0
- data/lib/campa/lisp/cond.rb +50 -0
- data/lib/campa/lisp/cons.rb +23 -0
- data/lib/campa/lisp/core.rb +35 -0
- data/lib/campa/lisp/defun.rb +36 -0
- data/lib/campa/lisp/eq.rb +9 -0
- data/lib/campa/lisp/label.rb +29 -0
- data/lib/campa/lisp/lambda_fn.rb +33 -0
- data/lib/campa/lisp/list_fn.rb +9 -0
- data/lib/campa/lisp/quote.rb +13 -0
- data/lib/campa/list.rb +83 -0
- data/lib/campa/node.rb +17 -0
- data/lib/campa/printer.rb +70 -0
- data/lib/campa/reader.rb +198 -0
- data/lib/campa/repl.rb +75 -0
- data/lib/campa/symbol.rb +23 -0
- data/lib/campa/version.rb +5 -0
- metadata +119 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Campa
|
2
|
+
class Context
|
3
|
+
attr_accessor :fallback
|
4
|
+
|
5
|
+
def initialize(env = {})
|
6
|
+
@env = env
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(symbol, value)
|
10
|
+
env[symbol] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](symbol)
|
14
|
+
return env[symbol] if env.include?(symbol)
|
15
|
+
|
16
|
+
fallback[symbol] if !fallback.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def include?(symbol)
|
20
|
+
env.include?(symbol) ||
|
21
|
+
(!fallback.nil? && fallback.include?(symbol))
|
22
|
+
end
|
23
|
+
|
24
|
+
def push(new_env = {})
|
25
|
+
# Context is explicit here
|
26
|
+
# (instead of self.class.new)
|
27
|
+
# because we can inherit a context,
|
28
|
+
# like the Lisp::Core does
|
29
|
+
# and then we want a normal context when pushing to it
|
30
|
+
# (and not a Lisp::Core).
|
31
|
+
Context.new(new_env).tap { |c| c.fallback = self }
|
32
|
+
end
|
33
|
+
|
34
|
+
def bindings
|
35
|
+
@bindings ||= env.is_a?(Context) ? env.bindings : env.to_a
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :env
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Campa
|
2
|
+
module Core
|
3
|
+
class Load
|
4
|
+
def initialize
|
5
|
+
@evaler = Evaler.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(*paths, env:)
|
9
|
+
verify_presence(paths)
|
10
|
+
paths.reduce(nil) do |_, file|
|
11
|
+
evaler.eval(Reader.new(file), env)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :evaler
|
18
|
+
|
19
|
+
def verify_presence(paths)
|
20
|
+
not_here = paths.find { |f| !File.exist?(f) }
|
21
|
+
raise Error::NotFound, not_here if !not_here.nil?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Campa
|
2
|
+
module Core
|
3
|
+
class Print
|
4
|
+
def call(*stuff, env:)
|
5
|
+
string =
|
6
|
+
stuff
|
7
|
+
.map { |s| printer.call(s) }
|
8
|
+
.join(" ")
|
9
|
+
(env[SYMBOL_OUT] || $stdout).print(string)
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def printer
|
16
|
+
@printer ||= Printer.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Campa
|
2
|
+
module Core
|
3
|
+
class PrintLn
|
4
|
+
def call(*stuff, env:)
|
5
|
+
out = env[SYMBOL_OUT] || $stdout
|
6
|
+
stuff.each { |s| out.puts printer.call(s) }
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def printer
|
13
|
+
@printer ||= Printer.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Campa
|
2
|
+
module Core
|
3
|
+
class Test
|
4
|
+
TEST_REGEXP = /\Atest(_|-)(.+)$/i
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@evaler = Campa::Evaler.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(*tests, env:)
|
11
|
+
summary = execute_all(tests, env)
|
12
|
+
List.new(
|
13
|
+
List.new(Symbol.new("success"), List.new(*summary[:success])),
|
14
|
+
List.new(Symbol.new("failures"), List.new(*summary[:failures]))
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :evaler
|
21
|
+
|
22
|
+
def execute_all(tests, env)
|
23
|
+
{ success: [], failures: [] }.tap do |summary|
|
24
|
+
env
|
25
|
+
.bindings
|
26
|
+
.select { |(sym, object)| test_func?(sym, object) }
|
27
|
+
.select { |(sym, _)| included?(tests, sym) }
|
28
|
+
.each { |(sym, fn)| add_to_summary(summary, sym, fn, env) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_func?(sym, object)
|
33
|
+
TEST_REGEXP.match?(sym.label) && object.respond_to?(:call)
|
34
|
+
end
|
35
|
+
|
36
|
+
def included?(tests, sym)
|
37
|
+
tests.empty? || tests.include?(TEST_REGEXP.match(sym.label)[2])
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_to_summary(summary, sym, func, env)
|
41
|
+
type = safely_execute(func, env) ? :success : :failures
|
42
|
+
summary[type] << sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def safely_execute(func, env)
|
46
|
+
func.call(env: env)
|
47
|
+
rescue StandardError
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Campa
|
2
|
+
module Core
|
3
|
+
class TestReport
|
4
|
+
def call(result, env:)
|
5
|
+
success, failures = %i[success failures].map { |t| filter(t, result) }
|
6
|
+
out = env[SYMBOL_OUT] || $stdout
|
7
|
+
show_summary(success, failures, out)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def show_summary(success, failures, out)
|
13
|
+
out.puts "\n\n#{success.length + failures.length} tests ran"
|
14
|
+
|
15
|
+
if failures.empty?
|
16
|
+
success_summary(success, out)
|
17
|
+
else
|
18
|
+
failure_summary(failures, out)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def filter(type, result)
|
23
|
+
filtered =
|
24
|
+
result
|
25
|
+
.to_a
|
26
|
+
.find { |l| l.head == Symbol.new(type.to_s) }
|
27
|
+
return [] if filtered.nil?
|
28
|
+
|
29
|
+
label_only(filtered)
|
30
|
+
end
|
31
|
+
|
32
|
+
def label_only(filtered)
|
33
|
+
filtered
|
34
|
+
.tail
|
35
|
+
.head
|
36
|
+
.to_a
|
37
|
+
.map(&:label)
|
38
|
+
end
|
39
|
+
|
40
|
+
def success_summary(_success, out)
|
41
|
+
out.puts "Success: none of those returned false"
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def failure_summary(failures, out)
|
47
|
+
[
|
48
|
+
"FAIL!",
|
49
|
+
" #{failures.length} tests failed",
|
50
|
+
" they are:",
|
51
|
+
].each { |str| out.puts str }
|
52
|
+
|
53
|
+
failures.each { |t| out.puts " - #{t}" }
|
54
|
+
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/campa/evaler.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Campa
|
2
|
+
class Evaler
|
3
|
+
def initialize
|
4
|
+
@printer = Printer.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(expression, env = {})
|
8
|
+
context = self.context(env)
|
9
|
+
|
10
|
+
case expression
|
11
|
+
when Numeric, TrueClass, FalseClass, NilClass, String, ::Symbol, List::EMPTY
|
12
|
+
expression
|
13
|
+
when Symbol
|
14
|
+
resolve(expression, context)
|
15
|
+
when List
|
16
|
+
invoke(expression, context)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def eval(reader, env = {})
|
21
|
+
context = self.context(env)
|
22
|
+
|
23
|
+
result = nil
|
24
|
+
while (token = reader.next)
|
25
|
+
result = call(token, context)
|
26
|
+
end
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :printer
|
33
|
+
|
34
|
+
def context(env)
|
35
|
+
return env if env.is_a?(Context)
|
36
|
+
|
37
|
+
Context.new(env)
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolve(symbol, context)
|
41
|
+
raise Error::Resolution, printer.call(symbol) if !context.include?(symbol)
|
42
|
+
|
43
|
+
context[symbol]
|
44
|
+
end
|
45
|
+
|
46
|
+
def invoke(invocation, context)
|
47
|
+
return invoke_cadr(invocation, context) if cr?(invocation)
|
48
|
+
|
49
|
+
fn = extract_fun(invocation, context)
|
50
|
+
args = args_for_fun(fn, invocation.tail.to_a, context)
|
51
|
+
if with_env?(fn)
|
52
|
+
fn.call(*args, env: context)
|
53
|
+
else
|
54
|
+
fn.call(*args)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def cr?(invocation)
|
59
|
+
invocation.head.is_a?(Symbol) &&
|
60
|
+
invocation.head.label.match?(CR_REGEX)
|
61
|
+
end
|
62
|
+
|
63
|
+
def invoke_cadr(invocation, context)
|
64
|
+
call(
|
65
|
+
List.new(
|
66
|
+
Symbol.new("_cadr"),
|
67
|
+
invocation.head,
|
68
|
+
call(invocation.tail.head, context)
|
69
|
+
),
|
70
|
+
context
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_fun(invocation, context)
|
75
|
+
# probable lambda invocation
|
76
|
+
return call(invocation.head, context) if invocation.head.is_a?(List)
|
77
|
+
|
78
|
+
resolve(invocation.head, context)
|
79
|
+
.then { |rs| rs.is_a?(List) ? call(rs, context) : rs }
|
80
|
+
.tap { |fn| raise not_a_function(invocation) if !fn.respond_to?(:call) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def not_a_function(invocation)
|
84
|
+
Error::NotAFunction.new printer.call(invocation.head)
|
85
|
+
end
|
86
|
+
|
87
|
+
def args_for_fun(fun, args, context)
|
88
|
+
return args if fun.respond_to?(:macro?) && fun.macro?
|
89
|
+
|
90
|
+
args.map { |exp| call(exp, context) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def with_env?(fun)
|
94
|
+
!params_from_fun(fun)
|
95
|
+
.filter { |param| param[0] == :keyreq }
|
96
|
+
.find { |param| param[1] == :env }
|
97
|
+
.nil?
|
98
|
+
end
|
99
|
+
|
100
|
+
def params_from_fun(fun)
|
101
|
+
return fun.parameters if fun.is_a?(Proc)
|
102
|
+
|
103
|
+
fun.method(:call).parameters
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/campa/lambda.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Campa
|
2
|
+
class Lambda
|
3
|
+
attr_reader :params, :body, :closure
|
4
|
+
|
5
|
+
def initialize(params, body, closure = Context.new)
|
6
|
+
@params = params
|
7
|
+
@body = Array(body)
|
8
|
+
@closure = closure
|
9
|
+
@evaler = Evaler.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(*args, env:)
|
13
|
+
raise arity_error(args) if params.to_a.length != args.length
|
14
|
+
|
15
|
+
@body.reduce(nil) do |_, expression|
|
16
|
+
evaler.call(
|
17
|
+
expression,
|
18
|
+
invocation_env(env, args)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
return false if !other.is_a?(Campa::Lambda)
|
25
|
+
|
26
|
+
params == other.params && body == other.body
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :evaler
|
32
|
+
|
33
|
+
def arity_error(args)
|
34
|
+
Error::Arity.new("lambda", params.to_a.length, args.length)
|
35
|
+
end
|
36
|
+
|
37
|
+
def invocation_env(env, args)
|
38
|
+
closure.push(env.push(Context.new)).tap do |ivk_env|
|
39
|
+
params.each_with_index do |symbol, idx|
|
40
|
+
ivk_env[symbol] = args[idx]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|