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
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: []