campa 0.1.0
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/.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
|