campa 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +3 -1
- data/README.md +10 -10
- data/Rakefile +8 -1
- data/bin/console +1 -8
- data/bin/setup +0 -2
- data/lib/campa.rb +46 -1
- data/lib/campa/cli.rb +29 -6
- data/lib/campa/context.rb +53 -5
- data/lib/campa/core.rb +6 -0
- data/lib/campa/core/load.rb +9 -0
- data/lib/campa/core/print.rb +12 -0
- data/lib/campa/core/print_ln.rb +16 -0
- data/lib/campa/core/test.rb +32 -2
- data/lib/campa/core/test_report.rb +10 -0
- data/lib/campa/evaler.rb +24 -0
- data/lib/campa/execution_error.rb +3 -0
- data/lib/campa/lambda.rb +63 -0
- data/lib/campa/language.rb +8 -0
- data/lib/campa/lisp/core.rb +18 -8
- data/lib/campa/list.rb +43 -0
- data/lib/campa/node.rb +6 -0
- data/lib/campa/printer.rb +27 -9
- data/lib/campa/reader.rb +14 -0
- data/lib/campa/repl.rb +18 -0
- data/lib/campa/symbol.rb +11 -0
- data/lib/campa/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db71a3db30c9fe5be51a2033b53adba713c4a0dd5f7fdcc0b2ae64b471792df5
|
4
|
+
data.tar.gz: fe74a65e008ef2575d2daf4f741563305a98b0ae28ac2830883ec0de8feb4d48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef6569c0fa26a2dc886903c4689cd2ae667e0ca6a3110289c9b280a4b88602e12be2d605d88dfd09b46e046f39dc7124d3c6dcb91456eca75992455f72313674
|
7
|
+
data.tar.gz: 3974b56df43c387b40a7211d0c65b7ab554d66e5546a0a3e851fbbd6ad19e42d4201ee9f0735928b4b6fe044fd23c1e3b4fdaa75ec24e2ec23f86969cff33487
|
data/.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.cmp linguist-language=lisp
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
campa (0.1.
|
4
|
+
campa (0.1.3)
|
5
5
|
zeitwerk (~> 2.4)
|
6
6
|
|
7
7
|
GEM
|
@@ -63,6 +63,7 @@ GEM
|
|
63
63
|
simplecov-html (0.12.3)
|
64
64
|
simplecov_json_formatter (0.1.3)
|
65
65
|
unicode-display_width (2.0.0)
|
66
|
+
yard (0.9.26)
|
66
67
|
zeitwerk (2.4.2)
|
67
68
|
|
68
69
|
PLATFORMS
|
@@ -77,6 +78,7 @@ DEPENDENCIES
|
|
77
78
|
rubocop-rake
|
78
79
|
rubocop-rspec
|
79
80
|
simplecov
|
81
|
+
yard
|
80
82
|
|
81
83
|
BUNDLED WITH
|
82
84
|
2.2.20
|
data/README.md
CHANGED
@@ -56,7 +56,7 @@ more accessible.
|
|
56
56
|
This project implements **LISP** to the point
|
57
57
|
where all the functions exemplified by Graham's article
|
58
58
|
run successfully. Which means one could implement *Campa* on itself
|
59
|
-
(or they could just [go here](../
|
59
|
+
(or they could just [go here](../main/campa/core.cmp)
|
60
60
|
and see how it is done already).
|
61
61
|
|
62
62
|
## Usage
|
@@ -144,7 +144,7 @@ to quote an object is also implemented in the runtime.
|
|
144
144
|
### Implementation details
|
145
145
|
|
146
146
|
All those functions are implemented in Ruby
|
147
|
-
[and they live right here](../
|
147
|
+
[and they live right here](../3b43a21/lib/campa/lisp).
|
148
148
|
|
149
149
|
## Beyond the Roots
|
150
150
|
|
@@ -158,16 +158,16 @@ and the ones implemented on the runtime (in Ruby).
|
|
158
158
|
|
159
159
|
### Extras in Campa
|
160
160
|
|
161
|
-
- [(assert x y)](../
|
161
|
+
- [(assert x y)](../3b43a21/campa/test.cmp#L1)
|
162
162
|
Returns true if x and y are *eq*.
|
163
163
|
|
164
164
|
### Extras in Ruby (runtime)
|
165
165
|
|
166
|
-
- [(load a-file another-file)](../
|
166
|
+
- [(load a-file another-file)](../3b43a21/lib/campa/core/load.rb)
|
167
167
|
Read and evaluate the files given as arguments
|
168
|
-
- [(print "some" "stuff" 4 '("even" "lists"))](../
|
168
|
+
- [(print "some" "stuff" 4 '("even" "lists"))](../3b43a21/lib/campa/core/print.rb)
|
169
169
|
Print out a reader friendly representation of the given parameter(s).
|
170
|
-
- [(println stuffz here)](../
|
170
|
+
- [(println stuffz here)](../3b43a21/lib/campa/core/println.rb)
|
171
171
|
Same as print but add a line break (`\n`) after each parameter.
|
172
172
|
|
173
173
|
#### Tests
|
@@ -184,7 +184,7 @@ that starts with *test-* or *test_* (case insentive)
|
|
184
184
|
and return *true* for success
|
185
185
|
or *false* for failure.
|
186
186
|
The core implementation for *Campa* rely on this tool
|
187
|
-
and you can check out [some test examples here](../
|
187
|
+
and you can check out [some test examples here](../3b43a21/test/core_test.cmp).
|
188
188
|
|
189
189
|
$ campa test test/core_test.cmp
|
190
190
|
|
@@ -194,8 +194,8 @@ and you can check out [some test examples here](../blob/4fc244d41dd6d9/test/core
|
|
194
194
|
Internally this "framework" is comprised
|
195
195
|
of the two following functions:
|
196
196
|
|
197
|
-
- [(tests-run optional-name other-optional-name)](../
|
198
|
-
- [(tests-report (tests-run))](../
|
197
|
+
- [(tests-run optional-name other-optional-name)](../3b43a21/lib/campa/core/test.rb)
|
198
|
+
- [(tests-report (tests-run))](../3b43a21/lib/campa/core/test_report.rb)
|
199
199
|
|
200
200
|
##### (tests-run)
|
201
201
|
|
@@ -237,7 +237,7 @@ This is an easy way to integrate this tool
|
|
237
237
|
with CI environments or any type of "progressive" build.
|
238
238
|
|
239
239
|
An example of how this is used by
|
240
|
-
*Campa* implementation [can be found here](../
|
240
|
+
*Campa* implementation [can be found here](../3b43a21/Rakefile#L12).
|
241
241
|
|
242
242
|
|
243
243
|
## Development
|
data/Rakefile
CHANGED
@@ -20,4 +20,11 @@ rescue SystemExit => e
|
|
20
20
|
exit(1) if e.status != 0
|
21
21
|
end
|
22
22
|
|
23
|
-
|
23
|
+
require "yard"
|
24
|
+
YARD::Rake::YardocTask.new do |t|
|
25
|
+
# t.files = ['lib/**/*.rb', OTHER_PATHS] # optional
|
26
|
+
# t.options = ['--any', '--extra', '--opts'] # optional
|
27
|
+
# t.stats_options = ['--list-undoc'] # optional
|
28
|
+
end
|
29
|
+
|
30
|
+
task default: %i[spec rubocop campatest yard]
|
data/bin/console
CHANGED
@@ -3,13 +3,6 @@
|
|
3
3
|
|
4
4
|
require "bundler/setup"
|
5
5
|
require "campa"
|
6
|
-
|
7
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
-
# with your gem easier. You can also use a different console, if you like.
|
9
|
-
|
10
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
-
# require "pry"
|
12
|
-
# Pry.start
|
13
|
-
|
14
6
|
require "irb"
|
7
|
+
|
15
8
|
IRB.start(__FILE__)
|
data/bin/setup
CHANGED
data/lib/campa.rb
CHANGED
@@ -7,12 +7,57 @@ require "zeitwerk"
|
|
7
7
|
loader = Zeitwerk::Loader.for_gem
|
8
8
|
loader.setup
|
9
9
|
|
10
|
+
# Campa is a tiny LISP implementation.
|
11
|
+
#
|
12
|
+
# The "benchmark" for this is to cover the specification
|
13
|
+
# established by the Paul Graham's article
|
14
|
+
# {http://paulgraham.com/rootsoflisp.html The Roots of Lisp}.
|
15
|
+
#
|
16
|
+
# So the following functions are implemented
|
17
|
+
# in the runtime:
|
18
|
+
#
|
19
|
+
# - (atom something)
|
20
|
+
# - (car list)
|
21
|
+
# - (cdr list)
|
22
|
+
# - (cond (some-condition value) (another-condition another-value))
|
23
|
+
# - (cons 'first '(second third))
|
24
|
+
# - (defun fun-name (parameters list) 'body)
|
25
|
+
# - (eq a-thing another-thing)
|
26
|
+
# - (label meaning-of-life 42)
|
27
|
+
# - (quote (some stuff))
|
28
|
+
#
|
29
|
+
# Besides these core functions
|
30
|
+
# other two ones,
|
31
|
+
# also specified on The Roots of Lisp,
|
32
|
+
# are also implemented on tnis LISP:
|
33
|
+
#
|
34
|
+
# - (cadr list) - and any variation possible (caaar, cadadar...)
|
35
|
+
# - (list 'this 'creates 'a 'new 'list)
|
36
|
+
#
|
37
|
+
# Those are all the functions necessary
|
38
|
+
# to implement an eval function
|
39
|
+
# able to interprete LISP by itself.
|
40
|
+
#
|
41
|
+
# And to be sure that this is the case
|
42
|
+
# we have this implementation on {file:campa/core.cmp campa/core.cmp}.
|
10
43
|
module Campa
|
11
|
-
|
44
|
+
# caar, cddr, cadr, but not car or cdr
|
45
|
+
CR_REGEX = /\Ac((ad)|(a|d){2,})r$$/
|
46
|
+
|
47
|
+
# symbol to reference the "stdout" in a Campa execution context
|
12
48
|
SYMBOL_OUT = Symbol.new("__out__")
|
49
|
+
|
50
|
+
# symbol for the lambda function
|
13
51
|
SYMBOL_LAMBDA = Symbol.new("lambda")
|
52
|
+
|
53
|
+
# symbol for the quote function
|
14
54
|
SYMBOL_QUOTE = Symbol.new("quote")
|
15
55
|
|
56
|
+
# Returns a Pathname pointint to
|
57
|
+
# the root of the "gem".
|
58
|
+
#
|
59
|
+
# Useful for requiring and/or finding files
|
60
|
+
# that need to be read by the runtime.
|
16
61
|
def self.root
|
17
62
|
@root ||= Pathname.new File.expand_path(__dir__)
|
18
63
|
end
|
data/lib/campa/cli.rb
CHANGED
@@ -1,13 +1,40 @@
|
|
1
1
|
module Campa
|
2
|
+
# Implements the features available
|
3
|
+
# to the command `campa` shipped with this gem.
|
2
4
|
class Cli
|
3
|
-
|
5
|
+
# @param repl [#run] the repl to be run if no option is given to `campa`
|
6
|
+
# @param evaler [#eval] lisp/campa evaler to be used
|
7
|
+
# by the repl or file evaluation
|
8
|
+
# @param context [#push, #[], #[]=] the context provider
|
9
|
+
# for the current run.
|
10
|
+
# This is where the __out__ binding pointing to $stdout will be created.
|
11
|
+
# @param reader [#new] lisp/campa reader for when
|
12
|
+
# an existent file is passed to `campa`.
|
13
|
+
def initialize(repl: nil, evaler: nil, context: nil, reader: Campa::Reader)
|
4
14
|
@evaler = evaler || default_evaler
|
5
15
|
@context = context || default_context
|
6
|
-
@reader = reader
|
16
|
+
@reader = reader
|
7
17
|
|
8
18
|
@repl = repl || default_repl
|
9
19
|
end
|
10
20
|
|
21
|
+
# Execute some campa stuff
|
22
|
+
# depending on the options given in the command line.
|
23
|
+
#
|
24
|
+
# If no argument is given
|
25
|
+
# it will start a {Campa::Repl} session.
|
26
|
+
#
|
27
|
+
# When first argument is an existent file
|
28
|
+
# then it will evaluated using {Campa::Reader} and {Campa::Evaler}.
|
29
|
+
#
|
30
|
+
# If the CLI argument is anything else,
|
31
|
+
# this method tries to match it with {OPTIONS}
|
32
|
+
# and find the method to be executed.
|
33
|
+
# Current options for the CLI are:
|
34
|
+
#
|
35
|
+
# <b><i>campa test FILE1, FILE2</i></b>
|
36
|
+
# Uses {Campa::Core::Test} and {Campa::Core::TestReport} to evaluate
|
37
|
+
# the files given as options as *Campa* test code.
|
11
38
|
def execute(argv = nil, input: $stdin, out: $stdout)
|
12
39
|
return repl.run(input, out) if argv.nil? || argv.empty?
|
13
40
|
return evaluate(argv[0], input, out) if File.file?(argv[0])
|
@@ -58,9 +85,5 @@ module Campa
|
|
58
85
|
def default_context
|
59
86
|
@default_context ||= Campa::Language.new
|
60
87
|
end
|
61
|
-
|
62
|
-
def default_reader
|
63
|
-
@default_reader ||= Campa::Reader
|
64
|
-
end
|
65
88
|
end
|
66
89
|
end
|
data/lib/campa/context.rb
CHANGED
@@ -1,36 +1,84 @@
|
|
1
1
|
module Campa
|
2
|
+
# Represents a storage for all bindings
|
3
|
+
# in any <i>Campa</i> execution.
|
4
|
+
#
|
5
|
+
# All functions run inside their own context
|
6
|
+
# so when they are finished
|
7
|
+
# any binding created by it will be gone.
|
8
|
+
#
|
9
|
+
# The {Repl} also uses it's own {Context} for execution.
|
2
10
|
class Context
|
11
|
+
# @!visibility private
|
3
12
|
attr_accessor :fallback
|
4
13
|
|
14
|
+
# @param env [#[], #[]=] a Hash like or another {Context} object
|
15
|
+
# to be used as the underlying storage for this {Context}.
|
5
16
|
def initialize(env = {})
|
6
17
|
@env = env
|
7
18
|
end
|
8
19
|
|
20
|
+
# Creates a new binding between a given {Symbol} and an Object.
|
21
|
+
#
|
22
|
+
# @param symbol [Symbol]
|
23
|
+
# (actually, nothing guarantees it will be a symbol right now)
|
24
|
+
# to be bound to a value in this {Context}.
|
25
|
+
# @param value [Object] associated to a given {Symbol}
|
9
26
|
def []=(symbol, value)
|
10
27
|
env[symbol] = value
|
11
28
|
end
|
12
29
|
|
30
|
+
# Returns the value bound to a {Symbol}
|
31
|
+
#
|
32
|
+
# @param symbol [Symbol] for which we want to fetch the value
|
33
|
+
# in this {Context}
|
34
|
+
# @return Object
|
13
35
|
def [](symbol)
|
14
36
|
return env[symbol] if env.include?(symbol)
|
15
37
|
|
16
38
|
fallback[symbol] if !fallback.nil?
|
17
39
|
end
|
18
40
|
|
41
|
+
# Check if there is any binding for a {Symbol}
|
42
|
+
#
|
43
|
+
# @param symbol [Symbol] "label" for a (possibly) bound value
|
19
44
|
def include?(symbol)
|
20
45
|
env.include?(symbol) ||
|
21
46
|
(!fallback.nil? && fallback.include?(symbol))
|
22
47
|
end
|
23
48
|
|
49
|
+
# Creates a new {Context} assigning self as a #fallback to it.
|
50
|
+
#
|
51
|
+
# This means that if a {Symbol}
|
52
|
+
# is not present on the returned {Context},
|
53
|
+
# it will be searched on this one.
|
54
|
+
# Which allows for building some sort of stack of {Context}s.
|
55
|
+
#
|
56
|
+
# WARNING: The name <i>push</i> on this API is very questionable,
|
57
|
+
# I would like to change this to a visually more telling API.
|
58
|
+
#
|
59
|
+
# @param new_env [#[], #[]=] a Hash like or a {Context}
|
60
|
+
# that will serve as a fallback.
|
61
|
+
# @return {Context}
|
24
62
|
def push(new_env = {})
|
25
|
-
# Context is explicit here
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# and then we want a normal context when pushing to it
|
63
|
+
# Context is explicit here instead of self.class.new
|
64
|
+
# because we can inherit a context
|
65
|
+
# like the Lisp::Core does.
|
66
|
+
# In this case we want a normal context when pushing to it
|
30
67
|
# (and not a Lisp::Core).
|
31
68
|
Context.new(new_env).tap { |c| c.fallback = self }
|
32
69
|
end
|
33
70
|
|
71
|
+
# Return the current bindings in an Array.
|
72
|
+
#
|
73
|
+
# @example A simple {Context} with two bound {Symbol}
|
74
|
+
# ctx = Context.new(
|
75
|
+
# Symbol.new("lol") => "what time is it?",
|
76
|
+
# Symbol.new("bbq") => 420,
|
77
|
+
# )
|
78
|
+
# ctx.bindings #=> [
|
79
|
+
# [Symbol.new("lol"), "what time is it?"],
|
80
|
+
# [Symbol.new("bbq"), 420]
|
81
|
+
# ]
|
34
82
|
def bindings
|
35
83
|
@bindings ||= env.is_a?(Context) ? env.bindings : env.to_a
|
36
84
|
end
|
data/lib/campa/core.rb
ADDED
data/lib/campa/core/load.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
module Campa
|
2
2
|
module Core
|
3
|
+
# Implements a <i>Campa</i> function
|
4
|
+
# that reads ({Reader}) and evaluates ({Evaler})
|
5
|
+
# files with valid <i>Campa</i> code in the given {Context}.
|
3
6
|
class Load
|
4
7
|
def initialize
|
5
8
|
@evaler = Evaler.new
|
6
9
|
end
|
7
10
|
|
11
|
+
# @param paths [Array<String>] Strings representing paths to files
|
12
|
+
# to be evaled in a given context
|
13
|
+
# @param env [Context] where the files pointed by <i>paths</i>
|
14
|
+
# will be evaled
|
15
|
+
# @return [Object] value of the last form evaled from the last file
|
16
|
+
# given by <i>paths</i>
|
8
17
|
def call(*paths, env:)
|
9
18
|
verify_presence(paths)
|
10
19
|
paths.reduce(nil) do |_, file|
|
data/lib/campa/core/print.rb
CHANGED
@@ -1,6 +1,18 @@
|
|
1
1
|
module Campa
|
2
2
|
module Core
|
3
|
+
# <i>Campa</i> function that print "anything" to the $stdout.
|
3
4
|
class Print
|
5
|
+
# It uses {Printer} to transform an Object
|
6
|
+
# into a human readable form
|
7
|
+
# and sends it to $stdout.
|
8
|
+
#
|
9
|
+
# It is possible to override the preference for using $stdout
|
10
|
+
# by binding {SYMBOL_OUT} to a desired Object
|
11
|
+
# in the env given as a parameter to this method.
|
12
|
+
#
|
13
|
+
# @param stuff [Object] anything resulting from evaling a <i>Campa</i> expression
|
14
|
+
# @param env [Context] where {SYMBOL_OUT} will be searched
|
15
|
+
# to find an alternative to $stdout
|
4
16
|
def call(*stuff, env:)
|
5
17
|
string =
|
6
18
|
stuff
|
data/lib/campa/core/print_ln.rb
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
module Campa
|
2
2
|
module Core
|
3
|
+
# <i>Campa</i> function that print "anything" to the $stdout.
|
3
4
|
class PrintLn
|
5
|
+
# It uses {Printer} to transform an Object
|
6
|
+
# into a human readable form
|
7
|
+
# and sends it to $stdout.
|
8
|
+
#
|
9
|
+
# Uses <i>#puts</i> method in the output to add a new line
|
10
|
+
# after sending each String representation
|
11
|
+
# created via {Printer} be sent to the output.
|
12
|
+
#
|
13
|
+
# It is possible to override the preference for using $stdout
|
14
|
+
# by binding {SYMBOL_OUT} to a desired Object
|
15
|
+
# in the env given as a parameter to this method.
|
16
|
+
#
|
17
|
+
# @param stuff [Object] anything resulting from evaling a <i>Campa</i> expression
|
18
|
+
# @param env [Context] where {SYMBOL_OUT} will be searched
|
19
|
+
# to find an alternative to $stdout
|
4
20
|
def call(*stuff, env:)
|
5
21
|
out = env[SYMBOL_OUT] || $stdout
|
6
22
|
stuff.each { |s| out.puts(s.is_a?(String) ? s : printer.call(s)) }
|
data/lib/campa/core/test.rb
CHANGED
@@ -1,12 +1,40 @@
|
|
1
1
|
module Campa
|
2
2
|
module Core
|
3
|
+
# Searches functions with prefix test_ or test- (case insenstive)
|
4
|
+
# in a given context, invoke those and
|
5
|
+
# store their results in a Data Structure with the given form:
|
6
|
+
#
|
7
|
+
# (
|
8
|
+
# (success, (test-one, test-two)),
|
9
|
+
# (failures, (test-three, test-four))
|
10
|
+
# )
|
11
|
+
#
|
12
|
+
# In this example we are considering that
|
13
|
+
# functions <i>test-one</i> and <i>test-two</i> returned <b>true</b> and
|
14
|
+
# functions <i>test-three</i> and <i>test-four</i> returned <b>false</b>.
|
3
15
|
class Test
|
4
|
-
TEST_REGEXP = /\Atest(_|-)(.+)$/i
|
5
|
-
|
6
16
|
def initialize
|
7
17
|
@evaler = Campa::Evaler.new
|
8
18
|
end
|
9
19
|
|
20
|
+
# Execute functions named test-* or test_* (case insentive)
|
21
|
+
# and collect the results.
|
22
|
+
#
|
23
|
+
# The param <i>tests</i> can be used to filter
|
24
|
+
# specific tests to be executed.
|
25
|
+
# For a context where functions
|
26
|
+
# test-great-one, test-great-two and test-awful exists,
|
27
|
+
# if we want to execute only the <i>great</i> ones, we could do:
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# test = Test.new
|
31
|
+
# test.call("great", env: ctx)
|
32
|
+
#
|
33
|
+
# @param tests [Array<String>] if given will be used to "filter"
|
34
|
+
# functions by name
|
35
|
+
# @param env [Context] will be used to search functions
|
36
|
+
# named test-* or test_* (case insentive) and also
|
37
|
+
# to execute those functions
|
10
38
|
def call(*tests, env:)
|
11
39
|
summary = execute_all(tests, env)
|
12
40
|
List.new(
|
@@ -17,6 +45,8 @@ module Campa
|
|
17
45
|
|
18
46
|
private
|
19
47
|
|
48
|
+
TEST_REGEXP = /\Atest(_|-)(.+)$/i
|
49
|
+
|
20
50
|
attr_reader :evaler
|
21
51
|
|
22
52
|
def execute_all(tests, env)
|
@@ -1,6 +1,16 @@
|
|
1
1
|
module Campa
|
2
2
|
module Core
|
3
|
+
# Uses the result of a {Test#call} execution
|
4
|
+
# to show a human readable summary of a test execution.
|
3
5
|
class TestReport
|
6
|
+
# Receives the result of {Test#call}
|
7
|
+
# and shows a (hopefully) useful test summary.
|
8
|
+
#
|
9
|
+
# @param result [List] result of calling {Test#call}
|
10
|
+
# @param env [Context] where object bound to {SYMBOL_OUT} will be searched
|
11
|
+
# as an alternative to $stdout
|
12
|
+
# @return [Boolean] <b>true</b> if all tests were successful
|
13
|
+
# (<b>false</b> otherwhise).
|
4
14
|
def call(result, env:)
|
5
15
|
success, failures = %i[success failures].map { |t| filter(t, result) }
|
6
16
|
out = env[SYMBOL_OUT] || $stdout
|
data/lib/campa/evaler.rb
CHANGED
@@ -1,9 +1,24 @@
|
|
1
1
|
module Campa
|
2
|
+
# All the actual logic on how to evaluate
|
3
|
+
# the differente known forms
|
4
|
+
# in implemented in here.
|
2
5
|
class Evaler
|
3
6
|
def initialize
|
4
7
|
@printer = Printer.new
|
5
8
|
end
|
6
9
|
|
10
|
+
# Returns the result of a given form evaluation.
|
11
|
+
#
|
12
|
+
# The parameter expression is evaluated
|
13
|
+
# based on it's type.
|
14
|
+
# Primitives (like booleans, nil, strings...) are returned as is.
|
15
|
+
# {Symbol}s and {List}s are handled like this:
|
16
|
+
# - {Symbol}'s are searched in the given {Context} (env parameter).
|
17
|
+
# - {List}'s are considered function invocations.
|
18
|
+
#
|
19
|
+
# @param expression Can be any known form.
|
20
|
+
# @param env [#[], #[]=] Hash or {Context} containing the bindings
|
21
|
+
# for the current <i>Campa</i> execution.
|
7
22
|
def call(expression, env = {})
|
8
23
|
context = self.context(env)
|
9
24
|
|
@@ -17,6 +32,15 @@ module Campa
|
|
17
32
|
end
|
18
33
|
end
|
19
34
|
|
35
|
+
# Receives a {Reader} object
|
36
|
+
# and evaluate all forms returned
|
37
|
+
# by each <i>#next</i> call.
|
38
|
+
# Uses {#call} to do the actual evaluation.
|
39
|
+
#
|
40
|
+
# @param reader [Reader] representing the source code to be evaluated
|
41
|
+
# @param env [Context] to evaluate the code
|
42
|
+
# @return [Object] the result of evaluating the last form
|
43
|
+
# available in the given {Reader}
|
20
44
|
def eval(reader, env = {})
|
21
45
|
context = self.context(env)
|
22
46
|
|
data/lib/campa/lambda.rb
CHANGED
@@ -1,7 +1,49 @@
|
|
1
1
|
module Campa
|
2
|
+
# Represents an anonymous function
|
3
|
+
# that will be executed in a given {Context}.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# # given the representation of
|
7
|
+
# # ((lambda (something) (print something)) "hello world")
|
8
|
+
# lbd = Lambda.new(
|
9
|
+
# [Symbol.new("something")],
|
10
|
+
# [List.new(Symbol.new("print"), Symbol.new("something"))]
|
11
|
+
# )
|
12
|
+
#
|
13
|
+
# # sends "hello world" to the $stdout and returns
|
14
|
+
# lbd.call("hello world") #=> nil
|
15
|
+
#
|
16
|
+
# @example working with closures:
|
17
|
+
#
|
18
|
+
# # given the representation of
|
19
|
+
# # (label meaning 42)
|
20
|
+
# # ((lambda (time) (print "time: " time " meaning of life: " meaning)) 420)
|
21
|
+
# ctx = Context.new(Symbol.new("meaning") => 42)
|
22
|
+
#
|
23
|
+
# lbd = Lambda.new(
|
24
|
+
# [Symbol.new("time")],
|
25
|
+
# [
|
26
|
+
# List.new(
|
27
|
+
# Symbol.new("print"),
|
28
|
+
# "time: ", Symbol.new("time"),
|
29
|
+
# ", meaning of life: ", Symbol.new("meaning")
|
30
|
+
# )
|
31
|
+
# ],
|
32
|
+
# ctx
|
33
|
+
# )
|
34
|
+
#
|
35
|
+
# # sends "time: 420, meaning of life: 42" to $stdout and returns
|
36
|
+
# lbd.call(420) #=> nil
|
2
37
|
class Lambda
|
3
38
|
attr_reader :params, :body, :closure
|
4
39
|
|
40
|
+
# @param params [Array<Symbol>] {Symbol}s naming the parameters
|
41
|
+
# that a lambda can receive when being invoked
|
42
|
+
# @param body [Array<Object>] expressions composing the body,
|
43
|
+
# they are executed one by one in order
|
44
|
+
# in the given {Context}
|
45
|
+
# @param closure [Context] used as a fallback for the {Context}
|
46
|
+
# given when the {Lambda} is executed
|
5
47
|
def initialize(params, body, closure = Context.new)
|
6
48
|
@params = params
|
7
49
|
@body = Array(body)
|
@@ -9,6 +51,21 @@ module Campa
|
|
9
51
|
@evaler = Evaler.new
|
10
52
|
end
|
11
53
|
|
54
|
+
# Executes the expressions contained in the {#body}
|
55
|
+
# one by one using as {Context} the parameter <i>env:</i>
|
56
|
+
# on this method.
|
57
|
+
#
|
58
|
+
# The <i>env:</i> param will be used here
|
59
|
+
# as a fallback to a brand new {Context}
|
60
|
+
# created in the moment of the invocation.
|
61
|
+
# This isolates the {Context} passed as a parameter
|
62
|
+
# of any mutations that would be created
|
63
|
+
# by the {Lambda} if there is any binding during the execution.
|
64
|
+
#
|
65
|
+
# @param args [Array<Object>] the values that will be bound
|
66
|
+
# to each of the {#params} given to the constructor
|
67
|
+
# @param env [Context] for the execution of a lambda
|
68
|
+
# @return [Object] result of evaluating the last expression on {#body}
|
12
69
|
def call(*args, env:)
|
13
70
|
raise arity_error(args) if params.to_a.length != args.length
|
14
71
|
|
@@ -20,6 +77,12 @@ module Campa
|
|
20
77
|
end
|
21
78
|
end
|
22
79
|
|
80
|
+
# Stablishes equality between {Lambda} objects
|
81
|
+
# by comparing {#params} and {#body}
|
82
|
+
#
|
83
|
+
# @param other [Lambda] another {Lambda}
|
84
|
+
# @return [Boolean] <b>true</b> if {#params} names and {#body} expressions
|
85
|
+
# are <i>#==</i> in both {Lambda}.
|
23
86
|
def ==(other)
|
24
87
|
return false if !other.is_a?(Campa::Lambda)
|
25
88
|
|
data/lib/campa/language.rb
CHANGED
@@ -1,4 +1,12 @@
|
|
1
1
|
module Campa
|
2
|
+
# {Context} wrapping the core bindings
|
3
|
+
# that form the core for <b>Campa</b> (language).
|
4
|
+
#
|
5
|
+
# It extends {Lisp::Core} to add
|
6
|
+
# - (test-run ...)
|
7
|
+
# - (tests-report)
|
8
|
+
# - (print ...)
|
9
|
+
# - (println ...)
|
2
10
|
class Language < Lisp::Core
|
3
11
|
def initialize
|
4
12
|
super
|
data/lib/campa/lisp/core.rb
CHANGED
@@ -1,6 +1,24 @@
|
|
1
1
|
module Campa
|
2
2
|
module Lisp
|
3
|
+
# {Context} representing the base for the minimal Lisp
|
4
|
+
# stablished by <i>Paul Graham's: <b>The Roots of Lisp</b></i>.
|
5
|
+
#
|
6
|
+
# Since this is a {Context}
|
7
|
+
# it can be instantiated and use as a parameter
|
8
|
+
# to any function invocation that
|
9
|
+
# want's to use <i>Lisp</i> functions on it's body.
|
10
|
+
# And also, it can be used as a fallback
|
11
|
+
# so if we want to "extend" <i>Lisp</i>
|
12
|
+
# we could Inherit it or {#push} a new {Context} on it.
|
3
13
|
class Core < Context
|
14
|
+
def initialize
|
15
|
+
super Hash[
|
16
|
+
CORE_FUNCS_MAP.map { |label, handler| [sym(label), handler.new] }
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
4
22
|
CORE_FUNCS_MAP = {
|
5
23
|
"quote" => Quote,
|
6
24
|
"atom" => Atom,
|
@@ -19,14 +37,6 @@ module Campa
|
|
19
37
|
"load" => Campa::Core::Load,
|
20
38
|
}.freeze
|
21
39
|
|
22
|
-
def initialize
|
23
|
-
super Hash[
|
24
|
-
CORE_FUNCS_MAP.map { |label, handler| [sym(label), handler.new] }
|
25
|
-
]
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
40
|
def sym(label)
|
31
41
|
Campa::Symbol.new(label)
|
32
42
|
end
|
data/lib/campa/list.rb
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
module Campa
|
2
|
+
# A minimalist implementation of a Linked List.
|
3
|
+
#
|
4
|
+
# An instance of it represents a function ivocation
|
5
|
+
# in the context of a {Evaler#call} or a {Evaler#eval} call.
|
6
|
+
#
|
7
|
+
# The importance of adding the adjective <i>"minimalist"</i>
|
8
|
+
# in this description
|
9
|
+
# is to give already the idea that this implementation
|
10
|
+
# does not come with some algorithms (deleting, for example)
|
11
|
+
# since for this also minimal Lisp implementation
|
12
|
+
# this won't be necessary.
|
2
13
|
class List
|
3
14
|
include Enumerable
|
4
15
|
|
5
16
|
EMPTY = new
|
6
17
|
|
18
|
+
# @param elements [Array<Object>] elements linked on the current {List}
|
7
19
|
def initialize(*elements)
|
8
20
|
@first = nil
|
9
21
|
@head = nil
|
@@ -12,24 +24,45 @@ module Campa
|
|
12
24
|
with elements
|
13
25
|
end
|
14
26
|
|
27
|
+
# Creates a new {List} with the parameter passed
|
28
|
+
# in front of it.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# List.new(2, 3).push(1) #=> List.new(1, 2, 3)
|
32
|
+
#
|
33
|
+
# @param element [Object] any thing to be pushed into the {List}
|
34
|
+
# @return [List] new {List} with new element in front of it
|
15
35
|
def push(element)
|
16
36
|
self.class.new.tap do |l|
|
17
37
|
l.first = Node.new(value: element, next_node: first)
|
18
38
|
end
|
19
39
|
end
|
20
40
|
|
41
|
+
# First element of the current {List}.
|
42
|
+
#
|
43
|
+
# Return <i>nil</i> if the current list is {EMPTY}.
|
44
|
+
#
|
45
|
+
# @return [Object, nil]
|
21
46
|
def head
|
22
47
|
return nil if self == EMPTY
|
23
48
|
|
24
49
|
first.value
|
25
50
|
end
|
26
51
|
|
52
|
+
# Creates a new list
|
53
|
+
# with all elements of the current one BUT the head.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# List.new(1, 2, 3).tail #=> List.new(2, 3)
|
57
|
+
#
|
58
|
+
# @return [List] with the "rest" of the elements on this {List}
|
27
59
|
def tail
|
28
60
|
return EMPTY if first.nil? || first.next_node.nil?
|
29
61
|
|
30
62
|
self.class.new.tap { |l| l.first = @first.next_node }
|
31
63
|
end
|
32
64
|
|
65
|
+
# Yields each element starting from head.
|
33
66
|
def each(&block)
|
34
67
|
return if self == EMPTY
|
35
68
|
|
@@ -37,6 +70,11 @@ module Campa
|
|
37
70
|
tail.each(&block) if tail != EMPTY
|
38
71
|
end
|
39
72
|
|
73
|
+
# Stablishes equality by comparing each element in the {List}.
|
74
|
+
#
|
75
|
+
#
|
76
|
+
# @param other [List] to be compared
|
77
|
+
# @return [Boolean] <b>true</b> if of both {List} have equal (<i>#==</i>) elements
|
40
78
|
def ==(other)
|
41
79
|
return false if !other.is_a?(List)
|
42
80
|
|
@@ -56,6 +94,11 @@ module Campa
|
|
56
94
|
end
|
57
95
|
end
|
58
96
|
|
97
|
+
# Uses {Printer} to represent the current {List}
|
98
|
+
# in a human readable form.
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# List.new(1, 2, 3).inspect #=> "(1 2 3)"
|
59
102
|
def inspect
|
60
103
|
@printer ||= Printer.new
|
61
104
|
@printer.call(self)
|
data/lib/campa/node.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
module Campa
|
2
|
+
# A node containing a value to form a linked {List}.
|
2
3
|
class Node
|
3
4
|
attr_accessor :next_node
|
4
5
|
attr_reader :value
|
5
6
|
|
7
|
+
# @param value [Object] actual value in the {List} node
|
8
|
+
# @param next_node [Node] next node linked in the {List} chain,
|
9
|
+
# this {Node} is the last if <i>next_node:</i> is <i>nil</i>
|
6
10
|
def initialize(value:, next_node: nil)
|
7
11
|
@value = value
|
8
12
|
@next_node = next_node
|
9
13
|
end
|
10
14
|
|
15
|
+
# @param other [Node] to be compared
|
16
|
+
# @return [Boolean] <b>true</b> if {#value} is <i>#==</i> on both {Node}
|
11
17
|
def ==(other)
|
12
18
|
return false if !other.is_a?(Node)
|
13
19
|
|
data/lib/campa/printer.rb
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
module Campa
|
2
|
+
# Represents a <i>Campa</i> expression
|
3
|
+
# in a human readable form.
|
4
|
+
#
|
5
|
+
# In general it tries to create a representation
|
6
|
+
# that is valid <i>Campa</i> code
|
7
|
+
# to make it easy(ier) to copy and paste stuff
|
8
|
+
# from the REPL to a file.
|
9
|
+
#
|
10
|
+
# @example some results produced by {Printer}
|
11
|
+
# printer = Printer.new
|
12
|
+
#
|
13
|
+
# printer.call("lol") #=> "lol"
|
14
|
+
# printer.call("lol") #=> 123
|
15
|
+
# printer.call(List.new("bbq", 420, List.new("yes"))) #=> ("bbq" 420 ("yes"))
|
2
16
|
class Printer
|
17
|
+
# @param expr [Object] <i>Campa</i> expression to be respresented in
|
18
|
+
# human readable form
|
19
|
+
# @return [String] human readable form (almost always valid code)
|
20
|
+
# of an Expression
|
21
|
+
def call(expr)
|
22
|
+
format = FORMATS.fetch(expr.class) do
|
23
|
+
expr.is_a?(Context) ? :context : :default
|
24
|
+
end
|
25
|
+
send(format, expr)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
3
30
|
FORMATS = {
|
4
31
|
String => :string,
|
5
32
|
Symbol => :symbol,
|
@@ -11,15 +38,6 @@ module Campa
|
|
11
38
|
NilClass => :null,
|
12
39
|
}.freeze
|
13
40
|
|
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
41
|
def string(expr)
|
24
42
|
"\"#{expr}\""
|
25
43
|
end
|
data/lib/campa/reader.rb
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|
require "stringio"
|
2
2
|
|
3
3
|
module Campa
|
4
|
+
# Reads strings or files into <i>Campa</i> expressions.
|
4
5
|
# rubocop: disable Metrics/ClassLength
|
5
6
|
class Reader
|
6
7
|
# rubocop: enable Metrics/ClassLength
|
8
|
+
|
9
|
+
# Given a String, a file pointer or any <i>#getc</i>, <i>#eof?</i>
|
10
|
+
# it allows fetch every valid <i>Campa</i> form from it.
|
11
|
+
#
|
12
|
+
# If the String is a valid file path
|
13
|
+
# it will be converted into a file pointer.
|
14
|
+
# Anything else will be converted into a StringIO
|
15
|
+
#
|
16
|
+
# @param input [String, (#getc, #eof?)]
|
7
17
|
def initialize(input)
|
8
18
|
@input = to_io_like(input)
|
9
19
|
next_char
|
10
20
|
end
|
11
21
|
|
22
|
+
# Return the next <i>Campa</i> form available
|
23
|
+
# in the underlying io like object.
|
24
|
+
#
|
25
|
+
# @return [Object] next available <i>Campa</i> form
|
12
26
|
def next
|
13
27
|
eat_separators
|
14
28
|
return read if !@input.eof?
|
data/lib/campa/repl.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
module Campa
|
2
2
|
class Repl
|
3
|
+
# It creates a new {Context} that uses one
|
4
|
+
# given as a parameter to this contructor to evaluate
|
5
|
+
# expressions typed in the <b>REPL</b>.
|
6
|
+
#
|
7
|
+
# @param evaler [Evaler] used to extract value from expressions
|
8
|
+
# @param context [Context] against which expressions will be evaled
|
9
|
+
# @param reader [Reader] to return expressions to be evaled
|
3
10
|
def initialize(evaler, context, reader: Reader)
|
4
11
|
@reader = reader
|
5
12
|
@evaler = evaler
|
@@ -8,6 +15,17 @@ module Campa
|
|
8
15
|
@printer = Printer.new
|
9
16
|
end
|
10
17
|
|
18
|
+
# Uses the <i>#getc</i> method from the parameter <i>input</i>
|
19
|
+
# to create a (probably blocking) loop to evaluate code "live"
|
20
|
+
# creating a <b>REPL</b> session.
|
21
|
+
#
|
22
|
+
# Captures the Interrupt exception to break the loop
|
23
|
+
# and finish the current <b>REPL</b> session.
|
24
|
+
#
|
25
|
+
# @param input [IO] an IO like object from where tokens will be extracted
|
26
|
+
# @param output [IO] IO like object to receive the output
|
27
|
+
# from evaluating the forms
|
28
|
+
#
|
11
29
|
# rubocop: disable Metrics/MethodLength
|
12
30
|
def run(input, output)
|
13
31
|
output.print "=> "
|
data/lib/campa/symbol.rb
CHANGED
@@ -1,21 +1,32 @@
|
|
1
1
|
module Campa
|
2
|
+
# Represents the name to which a value is bound in a specific {Context}.
|
3
|
+
#
|
4
|
+
# Implements the necessary interface to be used with success
|
5
|
+
# as key in Hash objects.
|
2
6
|
class Symbol
|
3
7
|
attr_reader :label
|
4
8
|
|
9
|
+
# @param label [String] name to be bound to a value in a given {Context}
|
5
10
|
def initialize(label)
|
6
11
|
@label = label
|
7
12
|
end
|
8
13
|
|
14
|
+
# @param other [Symbol] another {Symbol} to be compared
|
15
|
+
# @return [Boolean] <b>true</b> if both {#label} are <i>#==</i>
|
9
16
|
def ==(other)
|
10
17
|
return false if !other.is_a?(self.class)
|
11
18
|
|
12
19
|
label == other.label
|
13
20
|
end
|
14
21
|
|
22
|
+
# @param other [Symbol] another {Symbol} to be compared
|
23
|
+
# @return [Boolean] <b>true</b> if both {Symbol} are <i>#==</i>
|
24
|
+
# and both {#hash} are <i>#==</i>
|
15
25
|
def eql?(other)
|
16
26
|
self == other && hash == other.hash
|
17
27
|
end
|
18
28
|
|
29
|
+
# @return [String] String based on <i>label#hash</i>.
|
19
30
|
def hash
|
20
31
|
@hash ||= "Campa::Symbol_#{label}".hash
|
21
32
|
end
|
data/lib/campa/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: campa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ricardo Valeriano
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -32,6 +32,7 @@ executables:
|
|
32
32
|
extensions: []
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
|
+
- ".gitattributes"
|
35
36
|
- ".gitignore"
|
36
37
|
- ".rspec"
|
37
38
|
- ".rubocop.yml"
|
@@ -52,6 +53,7 @@ files:
|
|
52
53
|
- lib/campa.rb
|
53
54
|
- lib/campa/cli.rb
|
54
55
|
- lib/campa/context.rb
|
56
|
+
- lib/campa/core.rb
|
55
57
|
- lib/campa/core/load.rb
|
56
58
|
- lib/campa/core/print.rb
|
57
59
|
- lib/campa/core/print_ln.rb
|