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
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
|
data/lib/campa/reader.rb
ADDED
@@ -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
|
data/lib/campa/symbol.rb
ADDED
@@ -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
|
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: []
|