kapusta 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/.rspec +2 -0
- data/Gemfile +10 -0
- data/README.md +58 -0
- data/Rakefile +10 -0
- data/bin/console +8 -0
- data/bin/setup +4 -0
- data/examples/accumulator.kap +16 -0
- data/examples/ackermann.kap +7 -0
- data/examples/anagram.kap +13 -0
- data/examples/binary-search.kap +16 -0
- data/examples/block-sort.kap +3 -0
- data/examples/calc.kap +10 -0
- data/examples/counter.kap +19 -0
- data/examples/describe.kap +9 -0
- data/examples/destructure.kap +4 -0
- data/examples/doto.kap +2 -0
- data/examples/egg-count.kap +10 -0
- data/examples/even-squares.kap +7 -0
- data/examples/exceptions.kap +14 -0
- data/examples/factorial.kap +8 -0
- data/examples/fib.kap +4 -0
- data/examples/fizzbuzz.kap +7 -0
- data/examples/gcd.kap +5 -0
- data/examples/greet.kap +2 -0
- data/examples/hashfn.kap +4 -0
- data/examples/kwargs.kap +1 -0
- data/examples/leap-year.kap +5 -0
- data/examples/match.kap +9 -0
- data/examples/min-max.kap +11 -0
- data/examples/module-header.kap +6 -0
- data/examples/palindrome.kap +8 -0
- data/examples/pangram.kap +9 -0
- data/examples/pcall.kap +9 -0
- data/examples/pipeline.kap +6 -0
- data/examples/points.kap +9 -0
- data/examples/primes.kap +8 -0
- data/examples/raindrops.kap +13 -0
- data/examples/record.kap +6 -0
- data/examples/regex.kap +9 -0
- data/examples/ruby-eval.kap +1 -0
- data/examples/safe-lookup.kap +6 -0
- data/examples/scopes.kap +18 -0
- data/examples/shapes.kap +9 -0
- data/examples/squares.kap +3 -0
- data/examples/stack.kap +19 -0
- data/examples/sum.kap +3 -0
- data/examples/tset.kap +4 -0
- data/examples/two-sum.kap +17 -0
- data/exe/kapfmt +6 -0
- data/exe/kapusta +6 -0
- data/kapfmt +4 -0
- data/kapusta.gemspec +25 -0
- data/lib/kapusta/ast.rb +76 -0
- data/lib/kapusta/cli.rb +61 -0
- data/lib/kapusta/compiler/emitter/bindings.rb +178 -0
- data/lib/kapusta/compiler/emitter/collections.rb +245 -0
- data/lib/kapusta/compiler/emitter/control_flow.rb +168 -0
- data/lib/kapusta/compiler/emitter/expressions.rb +107 -0
- data/lib/kapusta/compiler/emitter/interop.rb +277 -0
- data/lib/kapusta/compiler/emitter/patterns.rb +105 -0
- data/lib/kapusta/compiler/emitter/support.rb +169 -0
- data/lib/kapusta/compiler/emitter.rb +45 -0
- data/lib/kapusta/compiler/normalizer.rb +122 -0
- data/lib/kapusta/compiler/runtime.rb +583 -0
- data/lib/kapusta/compiler.rb +47 -0
- data/lib/kapusta/env.rb +42 -0
- data/lib/kapusta/formatter.rb +685 -0
- data/lib/kapusta/reader.rb +215 -0
- data/lib/kapusta/support.rb +7 -0
- data/lib/kapusta/version.rb +5 -0
- data/lib/kapusta.rb +30 -0
- data/spec/cli_spec.rb +77 -0
- data/spec/examples_spec.rb +258 -0
- data/spec/formatter_spec.rb +176 -0
- data/spec/spec_helper.rb +12 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f08d47c985c44cde4016b3f4188f1a59a4a4b72ae752d3876af8ab7c66c12069
|
|
4
|
+
data.tar.gz: d9b96c75f4a99de5bbdb9e7bdbdf258bfda5bf7791d13ffe2d115a655627f8b0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f936f5546667301b2bf9a96f0a16f8b61f6ea0b6fefc6c10a87c8a794b60b376274d2672b24e2a23b269adc3d932d418442dfdbce6d8957569a6ae426d0a3a4f
|
|
7
|
+
data.tar.gz: fb523b8572d114ee9e5b87c4a993607b9ab610a82f3aa0886a1465064877d956768fde168f7e58e2cfe2f31cddc8efddf82473d6addeab97f7c112d8d1fadcbb
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Kapusta
|
|
2
|
+
|
|
3
|
+
Kapusta is a Lisp for the Ruby runtime.
|
|
4
|
+
|
|
5
|
+
It is inspired by Fennel. It is not intended to be production-ready like Clojure: that would be a lot of work, and Ruby is already a rich, elegant language.
|
|
6
|
+
|
|
7
|
+
Instead, Kapusta aims to bring some of the simplicity and joy of Lisp to Ruby. Where Lua is intentionally minimal, and Fennel follows that design for good reason, Kapusta exists mostly for fun. You can use it for small apps, LeetCode, DragonRuby, or maybe even Rails.
|
|
8
|
+
|
|
9
|
+
For more information about Kapusta, see the official Fennel documentation and tutorials.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
exe/kapusta examples/fizzbuzz.kap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
exe/kapusta --compile examples/fizzbuzz.kap > examples/fizzbuzz.rb
|
|
21
|
+
ruby examples/fizzbuzz.rb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Comparison with Fennel
|
|
25
|
+
|
|
26
|
+
Kapusta keeps most core Fennel forms. The main differences come from Ruby's runtime and object model.
|
|
27
|
+
|
|
28
|
+
| Fennel | Kapusta |
|
|
29
|
+
|---------------------------------------|-------------------------------------------------------|
|
|
30
|
+
| Lua stdlib | Ruby stdlib |
|
|
31
|
+
| `:foo` is a Lua string | `:foo` is a Ruby symbol |
|
|
32
|
+
| `(. xs 1)` is the first element | `(. xs 0)` is the first element |
|
|
33
|
+
| `string.format`, `table.insert`, etc. | use Ruby methods and stdlib instead |
|
|
34
|
+
| `values` uses Lua multiple returns | `values` lowers to a Ruby array, usually destructured |
|
|
35
|
+
| `with-open`, `tail!` | not provided |
|
|
36
|
+
|
|
37
|
+
Kapusta-specific additions:
|
|
38
|
+
|
|
39
|
+
- `module` and `class` for Ruby host structure, including file-header forms
|
|
40
|
+
- `ivar` / `cvar` / `gvar` escape hatches
|
|
41
|
+
- `try` / `catch` / `finally` plus `raise` for exceptions
|
|
42
|
+
- `(ruby "...")` raw host escape hatch
|
|
43
|
+
- a trailing symbol-keyed hash is emitted as Ruby keyword arguments
|
|
44
|
+
- a final function literal argument is emitted as a Ruby block
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
See `examples/`.
|
|
49
|
+
|
|
50
|
+
## Formatting
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
exe/kapfmt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Syntax highlight
|
|
57
|
+
|
|
58
|
+
For Vim you can use https://git.sr.ht/~m15a/vim-fennel-syntax
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
(class Accumulator)
|
|
2
|
+
|
|
3
|
+
(fn initialize [start]
|
|
4
|
+
(set (ivar total) start))
|
|
5
|
+
|
|
6
|
+
(fn add! [n]
|
|
7
|
+
(set (ivar total) (+ (ivar total) n))
|
|
8
|
+
self)
|
|
9
|
+
|
|
10
|
+
(fn value []
|
|
11
|
+
(ivar total))
|
|
12
|
+
|
|
13
|
+
(let [acc (Accumulator.new 10)]
|
|
14
|
+
(acc.add! 5)
|
|
15
|
+
(acc.add! 7)
|
|
16
|
+
(puts (acc.value)))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
(fn normalize-word [word]
|
|
2
|
+
(let [lower (word.downcase)
|
|
3
|
+
chars (lower.chars)
|
|
4
|
+
sorted (chars.sort)]
|
|
5
|
+
(sorted.join)))
|
|
6
|
+
|
|
7
|
+
(fn anagram? [a b]
|
|
8
|
+
(= (normalize-word a)
|
|
9
|
+
(normalize-word b)))
|
|
10
|
+
|
|
11
|
+
(puts (anagram? "listen" "silent"))
|
|
12
|
+
(puts (anagram? "apple" "papel"))
|
|
13
|
+
(puts (anagram? "hello" "world"))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
(fn binary-search [xs target]
|
|
2
|
+
(var lo 0)
|
|
3
|
+
(var hi (- (length xs) 1))
|
|
4
|
+
(var answer nil)
|
|
5
|
+
(while (and (<= lo hi) (= answer nil))
|
|
6
|
+
(let [mid (: (/ (+ lo hi) 2) :floor)
|
|
7
|
+
guess (. xs mid)]
|
|
8
|
+
(if (= guess target) (set answer mid)
|
|
9
|
+
(< guess target) (set lo (+ mid 1))
|
|
10
|
+
(set hi (- mid 1)))))
|
|
11
|
+
answer)
|
|
12
|
+
|
|
13
|
+
(let [found (binary-search [1 3 5 7 9 11] 7)
|
|
14
|
+
missing (binary-search [1 3 5 7 9 11] 2)]
|
|
15
|
+
(puts found)
|
|
16
|
+
(puts (missing.inspect)))
|
data/examples/calc.kap
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
(fn eval-expr [expr]
|
|
2
|
+
(case expr
|
|
3
|
+
[:num n] n
|
|
4
|
+
[:add a b] (+ (eval-expr a) (eval-expr b))
|
|
5
|
+
[:sub a b] (- (eval-expr a) (eval-expr b))
|
|
6
|
+
[:mul a b] (* (eval-expr a) (eval-expr b))
|
|
7
|
+
[:div a b] (/ (eval-expr a) (eval-expr b))
|
|
8
|
+
_ (raise (ArgumentError.new (.. "unknown op: " (expr.inspect))))))
|
|
9
|
+
|
|
10
|
+
(puts (eval-expr [:add [:num 2] [:mul [:num 3] [:num 4]]]))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
(class Counter)
|
|
2
|
+
|
|
3
|
+
(fn initialize [start]
|
|
4
|
+
(set (ivar n) start))
|
|
5
|
+
|
|
6
|
+
(fn tick []
|
|
7
|
+
(set (ivar n) (+ (ivar n) 1))
|
|
8
|
+
(ivar n))
|
|
9
|
+
|
|
10
|
+
(fn value []
|
|
11
|
+
(ivar n))
|
|
12
|
+
|
|
13
|
+
(fn self.zero []
|
|
14
|
+
(Counter.new 0))
|
|
15
|
+
|
|
16
|
+
(let [c (Counter.new 10)]
|
|
17
|
+
(c.tick)
|
|
18
|
+
(c.tick)
|
|
19
|
+
(puts (c.value)))
|
data/examples/doto.kap
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
(fn require-score [s]
|
|
2
|
+
(if (= s "oops")
|
|
3
|
+
(raise (ArgumentError.new "not a number"))
|
|
4
|
+
(Integer s)))
|
|
5
|
+
|
|
6
|
+
(fn parse-score [s]
|
|
7
|
+
(try (require-score s)
|
|
8
|
+
(catch ArgumentError e
|
|
9
|
+
(.. "bad: " s))
|
|
10
|
+
(finally
|
|
11
|
+
(puts (.. "seen: " s)))))
|
|
12
|
+
|
|
13
|
+
(each [_ s (ipairs ["12" "oops"])]
|
|
14
|
+
(puts (parse-score s)))
|
data/examples/fib.kap
ADDED
data/examples/gcd.kap
ADDED
data/examples/greet.kap
ADDED
data/examples/hashfn.kap
ADDED
data/examples/kwargs.kap
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(puts (format "%<name>s has %<count>d tasks" {:name "Ada" :count 3}))
|
data/examples/match.kap
ADDED
data/examples/pcall.kap
ADDED
data/examples/points.kap
ADDED
data/examples/primes.kap
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
(fn raindrops [n]
|
|
2
|
+
(local div? #(= 0 (% $1 $2)))
|
|
3
|
+
(local empty? #(= 0 (length $1)))
|
|
4
|
+
(local drops [])
|
|
5
|
+
(local add-drop #(drops.push $1))
|
|
6
|
+
(when (div? n 3) (add-drop "Pling"))
|
|
7
|
+
(when (div? n 5) (add-drop "Plang"))
|
|
8
|
+
(when (div? n 7) (add-drop "Plong"))
|
|
9
|
+
(if (empty? drops)
|
|
10
|
+
(tostring n)
|
|
11
|
+
(drops.join)))
|
|
12
|
+
|
|
13
|
+
(print (raindrops 15))
|
data/examples/record.kap
ADDED
data/examples/regex.kap
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
(fn parse-date [s]
|
|
2
|
+
(let [re (ruby "/\\A(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})\\z/")]
|
|
3
|
+
(case (re.match s)
|
|
4
|
+
nil nil
|
|
5
|
+
m (m.named-captures))))
|
|
6
|
+
|
|
7
|
+
(each [_ s (ipairs ["2026-04-23" "hello" "1999-12-31"])]
|
|
8
|
+
(let [parsed (parse-date s)]
|
|
9
|
+
(puts (.. s " -> " (parsed.inspect)))))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(puts (ruby "[1, 2, 3].map { _1 * 10 }.join('-')"))
|
data/examples/scopes.kap
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
(class ScopeCounter)
|
|
2
|
+
|
|
3
|
+
(set (cvar total) 0)
|
|
4
|
+
|
|
5
|
+
(fn add! [n]
|
|
6
|
+
(set (cvar total) (+ (cvar total) n))
|
|
7
|
+
(set (gvar last-total) (cvar total))
|
|
8
|
+
(cvar total))
|
|
9
|
+
|
|
10
|
+
(fn self.total []
|
|
11
|
+
(cvar total))
|
|
12
|
+
|
|
13
|
+
(let [a (ScopeCounter.new)
|
|
14
|
+
b (ScopeCounter.new)]
|
|
15
|
+
(puts (a.add! 5))
|
|
16
|
+
(puts (b.add! 4))
|
|
17
|
+
(puts (ScopeCounter.total))
|
|
18
|
+
(puts (gvar last-total)))
|
data/examples/shapes.kap
ADDED
data/examples/stack.kap
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
(class Stack)
|
|
2
|
+
|
|
3
|
+
(fn initialize []
|
|
4
|
+
(set (ivar xs) []))
|
|
5
|
+
|
|
6
|
+
(fn push! [x]
|
|
7
|
+
(let [xs (ivar xs)]
|
|
8
|
+
(xs.push x))
|
|
9
|
+
self)
|
|
10
|
+
|
|
11
|
+
(fn pop! []
|
|
12
|
+
(let [xs (ivar xs)]
|
|
13
|
+
(xs.pop)))
|
|
14
|
+
|
|
15
|
+
(fn empty? []
|
|
16
|
+
(= 0 (length (ivar xs))))
|
|
17
|
+
|
|
18
|
+
(fn size []
|
|
19
|
+
(length (ivar xs)))
|
data/examples/sum.kap
ADDED
data/examples/tset.kap
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
(fn two-sum [xs target]
|
|
2
|
+
(var i 0)
|
|
3
|
+
(var answer nil)
|
|
4
|
+
(while (and (< i (length xs)) (= answer nil))
|
|
5
|
+
(var j (+ i 1))
|
|
6
|
+
(while (and (< j (length xs)) (= answer nil))
|
|
7
|
+
(when (= (+ (. xs i) (. xs j)) target) (set answer [i j]))
|
|
8
|
+
(set j (+ j 1)))
|
|
9
|
+
(set i (+ i 1)))
|
|
10
|
+
answer)
|
|
11
|
+
|
|
12
|
+
(let [first-pair (two-sum [2 7 11 15] 9)
|
|
13
|
+
second-pair (two-sum [3 2 4] 6)
|
|
14
|
+
missing-pair (two-sum [1 2 3] 10)]
|
|
15
|
+
(puts (first-pair.inspect))
|
|
16
|
+
(puts (second-pair.inspect))
|
|
17
|
+
(puts (missing-pair.inspect)))
|
data/exe/kapfmt
ADDED
data/exe/kapusta
ADDED
data/kapfmt
ADDED
data/kapusta.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/kapusta/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'kapusta'
|
|
7
|
+
spec.version = Kapusta::VERSION
|
|
8
|
+
spec.authors = ['Evgenii Morozov']
|
|
9
|
+
|
|
10
|
+
spec.summary = 'A Lisp for the Ruby runtime'
|
|
11
|
+
spec.description = 'Kapusta is a Lisp for the Ruby runtime.'
|
|
12
|
+
spec.required_ruby_version = '>= 3.1'
|
|
13
|
+
|
|
14
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
15
|
+
|
|
16
|
+
spec.files = Dir.chdir(__dir__) do
|
|
17
|
+
`git ls-files -z`.split("\x0").select do |path|
|
|
18
|
+
path.start_with?('bin/', 'docs/', 'examples/', 'exe/', 'lib/', 'spec/') ||
|
|
19
|
+
%w[.rspec Gemfile README.md Rakefile kapfmt kapusta.gemspec].include?(path)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
spec.bindir = 'exe'
|
|
23
|
+
spec.executables = %w[kapfmt kapusta]
|
|
24
|
+
spec.require_paths = ['lib']
|
|
25
|
+
end
|
data/lib/kapusta/ast.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class Sym
|
|
5
|
+
attr_reader :name
|
|
6
|
+
|
|
7
|
+
def initialize(name)
|
|
8
|
+
@name = name.to_s
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
@name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect
|
|
16
|
+
"#<Sym #{@name}>"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(Sym) && other.name == @name
|
|
21
|
+
end
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
@name.hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dotted?
|
|
29
|
+
@name != '.' && @name.include?('.')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def segments
|
|
33
|
+
@name.split('.')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Vec
|
|
38
|
+
attr_reader :items
|
|
39
|
+
|
|
40
|
+
def initialize(items)
|
|
41
|
+
@items = items
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class HashLit
|
|
46
|
+
attr_reader :pairs
|
|
47
|
+
|
|
48
|
+
def initialize(pairs)
|
|
49
|
+
@pairs = pairs
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all_sym_keys?
|
|
53
|
+
@pairs.all? { |key, _| key.is_a?(Symbol) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class List
|
|
58
|
+
attr_reader :items
|
|
59
|
+
|
|
60
|
+
def initialize(items)
|
|
61
|
+
@items = items
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def head
|
|
65
|
+
@items.first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rest
|
|
69
|
+
@items[1..] || []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def empty?
|
|
73
|
+
@items.empty?
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|