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
data/lib/campa/node.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Campa
2
+ class Node
3
+ attr_accessor :next_node
4
+ attr_reader :value
5
+
6
+ def initialize(value:, next_node: nil)
7
+ @value = value
8
+ @next_node = next_node
9
+ end
10
+
11
+ def ==(other)
12
+ return false if !other.is_a?(Node)
13
+
14
+ value == other.value
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,70 @@
1
+ module Campa
2
+ class Printer
3
+ FORMATS = {
4
+ String => :string,
5
+ Symbol => :symbol,
6
+ List => :list,
7
+ TrueClass => :boolean,
8
+ FalseClass => :boolean,
9
+ Lambda => :lambda,
10
+ Context => :context,
11
+ NilClass => :null,
12
+ }.freeze
13
+
14
+ def call(expr)
15
+ format = FORMATS.fetch(expr.class) do
16
+ expr.is_a?(Context) ? :context : :default
17
+ end
18
+ send(format, expr)
19
+ end
20
+
21
+ private
22
+
23
+ def string(expr)
24
+ "\"#{expr}\""
25
+ end
26
+
27
+ def symbol(expr)
28
+ expr.label
29
+ end
30
+
31
+ def list(expr)
32
+ "(#{expr.map { |el| call(el) }.join(" ")})"
33
+ end
34
+
35
+ def boolean(expr)
36
+ (expr == true).to_s
37
+ end
38
+
39
+ def lambda(expr)
40
+ list(
41
+ List
42
+ .new(expr.body.map { |e| call(e) })
43
+ .push(expr.params)
44
+ .push(SYMBOL_LAMBDA)
45
+ )
46
+ end
47
+
48
+ def context(expr)
49
+ context_bindings(expr).join("\n")
50
+ end
51
+
52
+ def null(_expr)
53
+ "NIL"
54
+ end
55
+
56
+ def default(expr)
57
+ expr
58
+ end
59
+
60
+ def context_bindings(expr, sep: "")
61
+ own =
62
+ expr
63
+ .bindings
64
+ .map { |tuple| "#{sep}#{call(tuple[0])}: #{call(tuple[1])}" }
65
+ return own if expr.fallback.nil?
66
+
67
+ own + context_bindings(expr.fallback, sep: "#{sep} ")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,198 @@
1
+ require "stringio"
2
+
3
+ module Campa
4
+ # rubocop: disable Metrics/ClassLength
5
+ class Reader
6
+ # rubocop: enable Metrics/ClassLength
7
+ def initialize(input)
8
+ @input = to_io_like(input)
9
+ next_char
10
+ end
11
+
12
+ def next
13
+ eat_separators
14
+ return read if !@input.eof?
15
+ return if @current_char.nil?
16
+
17
+ # Exhaust the reader if @input.eof? and !@current_char.nil?
18
+ read.tap { @current_char = nil }
19
+ end
20
+
21
+ private
22
+
23
+ SEPARATORS = ["\s", ","].freeze
24
+ BOOLS = %w[true false].freeze
25
+ BOOLS_START = %w[t f].freeze
26
+ CAST_INT = ->(str) { Integer(str) }
27
+ CAST_FLOAT = ->(str) { Float(str) }
28
+
29
+ def to_io_like(input)
30
+ return input if input.respond_to?(:getc) && input.respond_to?(:eof?)
31
+ return File.new(input) if File.file?(input)
32
+
33
+ # TODO: check if it is "castable" first,
34
+ StringIO.new(input)
35
+ end
36
+
37
+ def next_char
38
+ if @next_char.nil?
39
+ @current_char = @input.getc
40
+ else
41
+ @current_char = @next_char
42
+ @next_char = nil
43
+ end
44
+
45
+ @current_char
46
+ end
47
+
48
+ def peek
49
+ return @next_char if !@next_char.nil?
50
+
51
+ @next_char = @input.getc
52
+ end
53
+
54
+ def eat_separators
55
+ if @input.eof?
56
+ @current_char = nil if separator? || break?
57
+ return
58
+ end
59
+
60
+ next_char while separator? || break?
61
+ end
62
+
63
+ # rubocop: disable Metrics/MethodLength, Metrics/PerceivedComplexity
64
+ # rubocop: disable Style/EmptyCaseCondition, Metrics/CyclomaticComplexity
65
+ def read
66
+ case
67
+ when @current_char == "\""
68
+ read_string
69
+ when digit? || @current_char == "-" && digit?(peek)
70
+ read_number
71
+ when @current_char == "'"
72
+ read_quotation
73
+ when @current_char == "("
74
+ read_list
75
+ when boolean?
76
+ read_boolean
77
+ else
78
+ read_symbol
79
+ end
80
+ end
81
+ # rubocop: enable Metrics/MethodLength, Metrics/PerceivedComplexity
82
+ # rubocop: enable Style/EmptyCaseCondition, Metrics/CyclomaticComplexity
83
+
84
+ def read_string
85
+ return if @input.eof?
86
+
87
+ string = ""
88
+ # eats the opening "
89
+ next_char
90
+
91
+ while !@input.eof? && @current_char != "\""
92
+ string << @current_char
93
+ next_char
94
+ end
95
+ raise Error::MissingDelimiter, "\"" if @current_char != "\""
96
+
97
+ # eats the closing "
98
+ next_char
99
+
100
+ string
101
+ end
102
+
103
+ def read_number
104
+ number = @current_char
105
+ cast = CAST_INT
106
+
107
+ until @input.eof?
108
+ next_char
109
+ break if separator? || delimiter?
110
+
111
+ cast = CAST_FLOAT if @current_char == "."
112
+ number << @current_char
113
+ end
114
+
115
+ safe_cast(number, cast)
116
+ end
117
+
118
+ def safe_cast(number, cast)
119
+ cast.call(number)
120
+ rescue ArgumentError
121
+ raise Error::InvalidNumber, number
122
+ end
123
+
124
+ def read_quotation
125
+ # eats the ' char
126
+ next_char
127
+
128
+ List.new(SYMBOL_QUOTE, self.next)
129
+ end
130
+
131
+ def read_list
132
+ # eats the opening (
133
+ next_char
134
+
135
+ elements = []
136
+ while !@input.eof? && !delimiter?
137
+ token = self.next
138
+ elements << token
139
+ eat_separators if separator?
140
+ end
141
+ raise Error::MissingDelimiter, ")" if !delimiter?
142
+
143
+ # eats the closing )
144
+ next_char
145
+
146
+ List.new(*elements)
147
+ end
148
+
149
+ def read_boolean
150
+ boolean_value = @current_token
151
+ @current_token = nil
152
+ next_char
153
+ boolean_value == "true"
154
+ end
155
+
156
+ def read_symbol
157
+ label = @current_token || @current_char
158
+ @current_token = nil
159
+
160
+ until @input.eof?
161
+ next_char
162
+ break if separator? || delimiter? || break?
163
+
164
+ label << @current_char
165
+ end
166
+
167
+ # TODO: validate symbol (raise if invalid chars are present)
168
+ Symbol.new(label)
169
+ end
170
+
171
+ def separator?
172
+ SEPARATORS.include? @current_char
173
+ end
174
+
175
+ def break?
176
+ @current_char == "\n"
177
+ end
178
+
179
+ def delimiter?
180
+ @current_char == ")"
181
+ end
182
+
183
+ def digit?(char = nil)
184
+ char ||= @current_char
185
+ # TODO: should we force the encoding of source files?
186
+ # (since codepoints will be different depending on encoding).
187
+ !char.nil? && (char.ord >= 48 && char.ord <= 57)
188
+ end
189
+
190
+ def boolean?
191
+ return false if !BOOLS_START.include?(@current_char)
192
+
193
+ @current_token = @current_char
194
+ @current_token << next_char until @input.eof? || peek == " " || peek == ")"
195
+ BOOLS.include? @current_token
196
+ end
197
+ end
198
+ end
data/lib/campa/repl.rb ADDED
@@ -0,0 +1,75 @@
1
+ module Campa
2
+ class Repl
3
+ def initialize(evaler, context, reader: Reader)
4
+ @reader = reader
5
+ @evaler = evaler
6
+ @context = context
7
+ @environment = @context.push(Context.new)
8
+ @printer = Printer.new
9
+ end
10
+
11
+ # rubocop: disable Metrics/MethodLength
12
+ def run(input, output)
13
+ output.print "=> "
14
+ reader = @reader.new(input)
15
+
16
+ loop do
17
+ begin
18
+ token = reader.next
19
+ break if token.nil?
20
+
21
+ show(output, evaler.call(token, environment))
22
+ rescue ExecutionError => e
23
+ handle_exec_error(output, e)
24
+ rescue StandardError => e
25
+ handle_standard_error(output, e)
26
+ end
27
+
28
+ output.print "=> "
29
+ end
30
+ rescue Interrupt
31
+ output.puts "see you soon"
32
+ end
33
+ # rubocop: enable Metrics/MethodLength
34
+
35
+ private
36
+
37
+ attr_reader :evaler, :environment, :printer
38
+
39
+ def show(output, result)
40
+ output
41
+ .puts(
42
+ printer.call(
43
+ result
44
+ )
45
+ )
46
+ end
47
+
48
+ def handle_exec_error(output, exception)
49
+ [
50
+ "Execution Error: #{exception.class}",
51
+ " message: #{exception.message}",
52
+ " >> Runtime details:",
53
+ back_trace_to_s(exception),
54
+ ].each { |str| output.puts str }
55
+ end
56
+
57
+ def handle_standard_error(output, exception)
58
+ [
59
+ "FATAL!",
60
+ "Exeception error was raised at Runtime level",
61
+ "Runtime Error: #{exception.class}",
62
+ " message: #{exception.message}",
63
+ back_trace_to_s(exception),
64
+ ].each { |str| output.puts str }
65
+ end
66
+
67
+ def back_trace_to_s(exception)
68
+ exception
69
+ .backtrace[0..10]
70
+ .push("...")
71
+ .map { |s| " #{s}" }
72
+ .join("\n")
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,23 @@
1
+ module Campa
2
+ class Symbol
3
+ attr_reader :label
4
+
5
+ def initialize(label)
6
+ @label = label
7
+ end
8
+
9
+ def ==(other)
10
+ return false if !other.is_a?(self.class)
11
+
12
+ label == other.label
13
+ end
14
+
15
+ def eql?(other)
16
+ self == other && hash == other.hash
17
+ end
18
+
19
+ def hash
20
+ @hash ||= "Campa::Symbol_#{label}".hash
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Campa
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: campa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ricardo Valeriano
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-07-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zeitwerk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
27
+ description: A tiny lispey type of thing for learning purposes.
28
+ email:
29
+ - ricardo.valeriano@gmail.com
30
+ executables:
31
+ - campa
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - ".travis.yml"
39
+ - CHANGELOG.md
40
+ - Gemfile
41
+ - Gemfile.lock
42
+ - LICENSE.txt
43
+ - README.md
44
+ - Rakefile
45
+ - bin/console
46
+ - bin/setup
47
+ - campa.gemspec
48
+ - campa/core.cmp
49
+ - campa/example.cmp
50
+ - campa/test.cmp
51
+ - exe/campa
52
+ - lib/campa.rb
53
+ - lib/campa/cli.rb
54
+ - lib/campa/context.rb
55
+ - lib/campa/core/load.rb
56
+ - lib/campa/core/print.rb
57
+ - lib/campa/core/print_ln.rb
58
+ - lib/campa/core/test.rb
59
+ - lib/campa/core/test_report.rb
60
+ - lib/campa/error/arity.rb
61
+ - lib/campa/error/illegal_argument.rb
62
+ - lib/campa/error/invalid_number.rb
63
+ - lib/campa/error/missing_delimiter.rb
64
+ - lib/campa/error/not_a_function.rb
65
+ - lib/campa/error/not_found.rb
66
+ - lib/campa/error/parameters.rb
67
+ - lib/campa/error/reserved.rb
68
+ - lib/campa/error/resolution.rb
69
+ - lib/campa/evaler.rb
70
+ - lib/campa/execution_error.rb
71
+ - lib/campa/lambda.rb
72
+ - lib/campa/language.rb
73
+ - lib/campa/lisp/atom.rb
74
+ - lib/campa/lisp/cadr.rb
75
+ - lib/campa/lisp/car.rb
76
+ - lib/campa/lisp/cdr.rb
77
+ - lib/campa/lisp/cond.rb
78
+ - lib/campa/lisp/cons.rb
79
+ - lib/campa/lisp/core.rb
80
+ - lib/campa/lisp/defun.rb
81
+ - lib/campa/lisp/eq.rb
82
+ - lib/campa/lisp/label.rb
83
+ - lib/campa/lisp/lambda_fn.rb
84
+ - lib/campa/lisp/list_fn.rb
85
+ - lib/campa/lisp/quote.rb
86
+ - lib/campa/list.rb
87
+ - lib/campa/node.rb
88
+ - lib/campa/printer.rb
89
+ - lib/campa/reader.rb
90
+ - lib/campa/repl.rb
91
+ - lib/campa/symbol.rb
92
+ - lib/campa/version.rb
93
+ homepage: https://github.com/mistersourcerer/campa
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ homepage_uri: https://github.com/mistersourcerer/campa
98
+ source_code_uri: https://github.com/mistersourcerer/campa.
99
+ changelog_uri: https://github.com/mistersourcerer/campa/blob/main/CHANGELOG.md
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '3.0'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.2.15
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: A tiny lispey type of thing for learning purposes.
119
+ test_files: []