campa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +37 -0
  5. data/.travis.yml +8 -0
  6. data/CHANGELOG.md +5 -0
  7. data/Gemfile +15 -0
  8. data/Gemfile.lock +82 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +39 -0
  11. data/Rakefile +23 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/campa.gemspec +34 -0
  15. data/campa/core.cmp +59 -0
  16. data/campa/example.cmp +2 -0
  17. data/campa/test.cmp +2 -0
  18. data/exe/campa +7 -0
  19. data/lib/campa.rb +18 -0
  20. data/lib/campa/cli.rb +66 -0
  21. data/lib/campa/context.rb +42 -0
  22. data/lib/campa/core/load.rb +25 -0
  23. data/lib/campa/core/print.rb +20 -0
  24. data/lib/campa/core/print_ln.rb +17 -0
  25. data/lib/campa/core/test.rb +52 -0
  26. data/lib/campa/core/test_report.rb +59 -0
  27. data/lib/campa/error/arity.rb +11 -0
  28. data/lib/campa/error/illegal_argument.rb +9 -0
  29. data/lib/campa/error/invalid_number.rb +9 -0
  30. data/lib/campa/error/missing_delimiter.rb +9 -0
  31. data/lib/campa/error/not_a_function.rb +9 -0
  32. data/lib/campa/error/not_found.rb +9 -0
  33. data/lib/campa/error/parameters.rb +11 -0
  34. data/lib/campa/error/reserved.rb +11 -0
  35. data/lib/campa/error/resolution.rb +9 -0
  36. data/lib/campa/evaler.rb +106 -0
  37. data/lib/campa/execution_error.rb +3 -0
  38. data/lib/campa/lambda.rb +45 -0
  39. data/lib/campa/language.rb +33 -0
  40. data/lib/campa/lisp/atom.rb +14 -0
  41. data/lib/campa/lisp/cadr.rb +41 -0
  42. data/lib/campa/lisp/car.rb +22 -0
  43. data/lib/campa/lisp/cdr.rb +22 -0
  44. data/lib/campa/lisp/cond.rb +50 -0
  45. data/lib/campa/lisp/cons.rb +23 -0
  46. data/lib/campa/lisp/core.rb +35 -0
  47. data/lib/campa/lisp/defun.rb +36 -0
  48. data/lib/campa/lisp/eq.rb +9 -0
  49. data/lib/campa/lisp/label.rb +29 -0
  50. data/lib/campa/lisp/lambda_fn.rb +33 -0
  51. data/lib/campa/lisp/list_fn.rb +9 -0
  52. data/lib/campa/lisp/quote.rb +13 -0
  53. data/lib/campa/list.rb +83 -0
  54. data/lib/campa/node.rb +17 -0
  55. data/lib/campa/printer.rb +70 -0
  56. data/lib/campa/reader.rb +198 -0
  57. data/lib/campa/repl.rb +75 -0
  58. data/lib/campa/symbol.rb +23 -0
  59. data/lib/campa/version.rb +5 -0
  60. 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
@@ -0,0 +1,11 @@
1
+ module Campa
2
+ module Error
3
+ class Arity < ExecutionError
4
+ def initialize(fun, expected, given)
5
+ msg = "Arity error when invoking #{fun}: "
6
+ msg += "expected #{expected} arg(s) but #{given} given"
7
+ super msg
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Campa
2
+ module Error
3
+ class IllegalArgument < ExecutionError
4
+ def initialize(given, expected)
5
+ super "Illegal argument: #{given} was expected to be a #{expected}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Campa
2
+ module Error
3
+ class InvalidNumber < ExecutionError
4
+ def initialize(fake_number)
5
+ super "Invalid number: #{fake_number}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Campa
2
+ module Error
3
+ class MissingDelimiter < ExecutionError
4
+ def initialize(delimiter)
5
+ super "#{delimiter} was expected but none was found"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Campa
2
+ module Error
3
+ class NotAFunction < ExecutionError
4
+ def initialize(label)
5
+ super "The symbol: #{label} does not resolve to a function"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Campa
2
+ module Error
3
+ class NotFound < ExecutionError
4
+ def initialize(path)
5
+ super "Can't find a file at: #{path}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Campa
2
+ module Error
3
+ class Parameters < ExecutionError
4
+ def initialize(given, expected_type)
5
+ msg = "Parameter list may only contain #{expected_type}: "
6
+ msg += "#{given} is not a #{expected_type}"
7
+ super msg
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Campa
2
+ module Error
3
+ class Reserved < ExecutionError
4
+ def initialize(label)
5
+ msg = "Reserved function name: #{label} "
6
+ msg += "is already taken, sorry about that"
7
+ super msg
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Campa
2
+ module Error
3
+ class Resolution < ExecutionError
4
+ def initialize(label)
5
+ super "Unable to resolve symbol: #{label} in this context"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Campa
2
+ class ExecutionError < StandardError; end
3
+ end
@@ -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