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.
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