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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +10 -0
  4. data/README.md +58 -0
  5. data/Rakefile +10 -0
  6. data/bin/console +8 -0
  7. data/bin/setup +4 -0
  8. data/examples/accumulator.kap +16 -0
  9. data/examples/ackermann.kap +7 -0
  10. data/examples/anagram.kap +13 -0
  11. data/examples/binary-search.kap +16 -0
  12. data/examples/block-sort.kap +3 -0
  13. data/examples/calc.kap +10 -0
  14. data/examples/counter.kap +19 -0
  15. data/examples/describe.kap +9 -0
  16. data/examples/destructure.kap +4 -0
  17. data/examples/doto.kap +2 -0
  18. data/examples/egg-count.kap +10 -0
  19. data/examples/even-squares.kap +7 -0
  20. data/examples/exceptions.kap +14 -0
  21. data/examples/factorial.kap +8 -0
  22. data/examples/fib.kap +4 -0
  23. data/examples/fizzbuzz.kap +7 -0
  24. data/examples/gcd.kap +5 -0
  25. data/examples/greet.kap +2 -0
  26. data/examples/hashfn.kap +4 -0
  27. data/examples/kwargs.kap +1 -0
  28. data/examples/leap-year.kap +5 -0
  29. data/examples/match.kap +9 -0
  30. data/examples/min-max.kap +11 -0
  31. data/examples/module-header.kap +6 -0
  32. data/examples/palindrome.kap +8 -0
  33. data/examples/pangram.kap +9 -0
  34. data/examples/pcall.kap +9 -0
  35. data/examples/pipeline.kap +6 -0
  36. data/examples/points.kap +9 -0
  37. data/examples/primes.kap +8 -0
  38. data/examples/raindrops.kap +13 -0
  39. data/examples/record.kap +6 -0
  40. data/examples/regex.kap +9 -0
  41. data/examples/ruby-eval.kap +1 -0
  42. data/examples/safe-lookup.kap +6 -0
  43. data/examples/scopes.kap +18 -0
  44. data/examples/shapes.kap +9 -0
  45. data/examples/squares.kap +3 -0
  46. data/examples/stack.kap +19 -0
  47. data/examples/sum.kap +3 -0
  48. data/examples/tset.kap +4 -0
  49. data/examples/two-sum.kap +17 -0
  50. data/exe/kapfmt +6 -0
  51. data/exe/kapusta +6 -0
  52. data/kapfmt +4 -0
  53. data/kapusta.gemspec +25 -0
  54. data/lib/kapusta/ast.rb +76 -0
  55. data/lib/kapusta/cli.rb +61 -0
  56. data/lib/kapusta/compiler/emitter/bindings.rb +178 -0
  57. data/lib/kapusta/compiler/emitter/collections.rb +245 -0
  58. data/lib/kapusta/compiler/emitter/control_flow.rb +168 -0
  59. data/lib/kapusta/compiler/emitter/expressions.rb +107 -0
  60. data/lib/kapusta/compiler/emitter/interop.rb +277 -0
  61. data/lib/kapusta/compiler/emitter/patterns.rb +105 -0
  62. data/lib/kapusta/compiler/emitter/support.rb +169 -0
  63. data/lib/kapusta/compiler/emitter.rb +45 -0
  64. data/lib/kapusta/compiler/normalizer.rb +122 -0
  65. data/lib/kapusta/compiler/runtime.rb +583 -0
  66. data/lib/kapusta/compiler.rb +47 -0
  67. data/lib/kapusta/env.rb +42 -0
  68. data/lib/kapusta/formatter.rb +685 -0
  69. data/lib/kapusta/reader.rb +215 -0
  70. data/lib/kapusta/support.rb +7 -0
  71. data/lib/kapusta/version.rb +5 -0
  72. data/lib/kapusta.rb +30 -0
  73. data/spec/cli_spec.rb +77 -0
  74. data/spec/examples_spec.rb +258 -0
  75. data/spec/formatter_spec.rb +176 -0
  76. data/spec/spec_helper.rb +12 -0
  77. 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
@@ -0,0 +1,2 @@
1
+ --color
2
+ -I lib
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rubocop', require: false
10
+ end
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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
+ require 'rspec/core/rake_task'
6
+
7
+ RuboCop::RakeTask.new(:rubocop)
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'kapusta'
6
+ require 'irb'
7
+
8
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bundle install
@@ -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,7 @@
1
+ (fn ack [m n]
2
+ (if (= m 0) (+ n 1)
3
+ (= n 0) (ack (- m 1) 1)
4
+ (ack (- m 1) (ack m (- n 1)))))
5
+
6
+ (print (ack 2 3))
7
+ (print (ack 3 3))
@@ -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)))
@@ -0,0 +1,3 @@
1
+ (let [xs [3 1 2]
2
+ sorted (xs.sort (fn [a b] (b.<=> a)))]
3
+ (puts (sorted.join ", ")))
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)))
@@ -0,0 +1,9 @@
1
+ (fn describe [x]
2
+ (case x
3
+ 0 "zero"
4
+ 1 "one"
5
+ (where n (< n 0)) "negative"
6
+ _ "many"))
7
+
8
+ (each [_ n (ipairs [-3 0 1 2 99])]
9
+ (print n (describe n)))
@@ -0,0 +1,4 @@
1
+ (let [[a b c] [1 2 3]
2
+ {: name : age} {:name "Ada" :age 36}]
3
+ (print (+ a b c))
4
+ (print name age))
data/examples/doto.kap ADDED
@@ -0,0 +1,2 @@
1
+ (let [xs (doto [] (: :push 1) (: :push 2) (: :push 3))]
2
+ (puts (xs.join ", ")))
@@ -0,0 +1,10 @@
1
+ (fn egg-count [number]
2
+ (local odd? #(> (% $ 2) 0))
3
+ (var n number)
4
+ (var eggs 0)
5
+ (while (> n 0)
6
+ (when (odd? n) (set eggs (+ eggs 1)))
7
+ (set n (: (/ n 2) :floor)))
8
+ eggs)
9
+
10
+ (print (egg-count 30))
@@ -0,0 +1,7 @@
1
+ (let [
2
+ even-squares
3
+ (-> [1 2 3 4 5 6]
4
+ (: :select (fn [n] (n.even?)))
5
+ (: :map (fn [n] (* n n))))
6
+ ]
7
+ (puts (even-squares.join ", ")))
@@ -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)))
@@ -0,0 +1,8 @@
1
+ (fn factorial [n]
2
+ (case n
3
+ 0 1
4
+ 1 1
5
+ _ (* n (factorial (- n 1)))))
6
+
7
+ (each [_ n (ipairs [0 1 5 6 10])]
8
+ (print n (factorial n)))
data/examples/fib.kap ADDED
@@ -0,0 +1,4 @@
1
+ (fn fib [n]
2
+ (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))
3
+
4
+ (print (fib 10))
@@ -0,0 +1,7 @@
1
+ (for [n 1 20]
2
+ (let [d3? (= 0 (% n 3))
3
+ d5? (= 0 (% n 5))]
4
+ (if (and d3? d5?) (print "FizzBuzz")
5
+ d3? (print "Fizz")
6
+ d5? (print "Buzz")
7
+ (print n))))
data/examples/gcd.kap ADDED
@@ -0,0 +1,5 @@
1
+ (fn gcd [a b]
2
+ (if (= b 0) a (gcd b (% a b))))
3
+
4
+ (print (gcd 48 36))
5
+ (print (gcd 270 192))
@@ -0,0 +1,2 @@
1
+ (let [name (or (. ARGV 0) "world")]
2
+ (puts (.. "Hello, " name "!")))
@@ -0,0 +1,4 @@
1
+ (let [add #(+ $1 $2)
2
+ triple #(* 3 $)]
3
+ (print (add 2 3))
4
+ (print (triple 7)))
@@ -0,0 +1 @@
1
+ (puts (format "%<name>s has %<count>d tasks" {:name "Ada" :count 3}))
@@ -0,0 +1,5 @@
1
+ (fn leap-year? [y]
2
+ (let [div? #(= 0 (% $1 $2))]
3
+ (and (div? y 4) (or (not (div? y 100)) (div? y 400)))))
4
+
5
+ (print (leap-year? 2000))
@@ -0,0 +1,9 @@
1
+ (fn describe-user [user]
2
+ (match user
3
+ {: name :stats {: score}} (.. name ": " score)
4
+ {: name} (.. name ": no score")
5
+ _ "unknown"))
6
+
7
+ (puts (describe-user {:name "Ada" :stats {:score 9}}))
8
+ (puts (describe-user {:name "Lin"}))
9
+ (puts (describe-user {}))
@@ -0,0 +1,11 @@
1
+ (fn min-max [xs]
2
+ (let [[first & rest] xs]
3
+ (var lo first)
4
+ (var hi first)
5
+ (each [_ x (ipairs rest)]
6
+ (when (< x lo) (set lo x))
7
+ (when (> x hi) (set hi x)))
8
+ [lo hi]))
9
+
10
+ (let [[lo hi] (min-max [3 1 4 1 5 9 2 6])]
11
+ (print lo hi))
@@ -0,0 +1,6 @@
1
+ (module HeaderDemo)
2
+
3
+ (fn self.greet [name]
4
+ (.. "Hello, " name "!"))
5
+
6
+ (puts (self.greet "Ada"))
@@ -0,0 +1,8 @@
1
+ (fn palindrome? [s]
2
+ (let [lower (s.downcase)
3
+ normalized (lower.gsub (ruby "/[^a-z]/") "")]
4
+ (= normalized (normalized.reverse))))
5
+
6
+ (puts (palindrome? "racecar"))
7
+ (puts (palindrome? "A man, a plan, a canal: Panama"))
8
+ (puts (palindrome? "kapusta"))
@@ -0,0 +1,9 @@
1
+ (fn pangram? [s]
2
+ (let [lower (s.downcase)
3
+ letters (lower.gsub (ruby "/[^a-z]/") "")
4
+ chars (letters.chars)
5
+ uniq (chars.uniq)]
6
+ (= (uniq.length) 26)))
7
+
8
+ (puts (pangram? "The quick brown fox jumps over the lazy dog"))
9
+ (puts (pangram? "Hello, world"))
@@ -0,0 +1,9 @@
1
+ (let [[ok value] (pcall Integer "12")
2
+ [bad-ok error] (pcall Integer "oops")
3
+ [handled-ok handled] (xpcall Integer (fn [e] (e.message)) "oops")]
4
+ (puts ok)
5
+ (puts value)
6
+ (puts bad-ok)
7
+ (puts (error.class))
8
+ (puts handled-ok)
9
+ (puts handled))
@@ -0,0 +1,6 @@
1
+ (let [words ["red" "green" "blue" "black" "olive"]]
2
+ (-> words
3
+ (: :select (fn [w] (< (length w) 5)))
4
+ (: :map (fn [w] (w.upcase)))
5
+ (: :sort)
6
+ (: :each (fn [w] (puts w)))))
@@ -0,0 +1,9 @@
1
+ (fn point-kind [point]
2
+ (case point
3
+ [0 0] "origin"
4
+ [0 _] "y-axis"
5
+ [_ 0] "x-axis"
6
+ [_ _] "point"))
7
+
8
+ (each [_ point (ipairs [[0 0] [0 2] [3 0] [3 4]])]
9
+ (puts (point-kind point)))
@@ -0,0 +1,8 @@
1
+ (fn prime? [n]
2
+ (var ok true)
3
+ (for [d 2 (- n 1) &until (not ok)]
4
+ (when (= 0 (% n d)) (set ok false)))
5
+ (and (> n 1) ok))
6
+
7
+ (let [ps (fcollect [n 2 30] (when (prime? n) n))]
8
+ (each [_ p (ipairs ps)] (print p)))
@@ -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))
@@ -0,0 +1,6 @@
1
+ (fn format-record [record]
2
+ (let [{: name : role : tags} record]
3
+ (.. name " / " role " / " (tags.join ", "))))
4
+
5
+ (let [record {:name "Ada" :role "engineer" :tags ["ruby" "lisp"]}]
6
+ (puts (format-record record)))
@@ -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('-')"))
@@ -0,0 +1,6 @@
1
+ (let [user {:profile {:name "Ada"}}
2
+ missing {}
3
+ name (?. user :profile :name)
4
+ missing-name (?. missing :profile :name)]
5
+ (puts name)
6
+ (puts (missing-name.inspect)))
@@ -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)))
@@ -0,0 +1,9 @@
1
+ (fn area [shape]
2
+ (case shape
3
+ [:circle r] (* 3.14 r r)
4
+ [:square s] (* s s)
5
+ [:rect w h] (* w h)
6
+ _ 0))
7
+
8
+ (each [_ s (ipairs [[:circle 5] [:square 3] [:rect 2 4] [:dot]])]
9
+ (print (area s)))
@@ -0,0 +1,3 @@
1
+ (let [xs [1 2 3 4 5]
2
+ ys (icollect [_ x (ipairs xs)] (* x x))]
3
+ (each [_ y (ipairs ys)] (print y)))
@@ -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
@@ -0,0 +1,3 @@
1
+ (let [xs [10 20 30 40]
2
+ total (accumulate [s 0 _ x (ipairs xs)] (+ s x))]
3
+ (print total))
data/examples/tset.kap ADDED
@@ -0,0 +1,4 @@
1
+ (let [person {:name "Ada"}]
2
+ (tset person :city "Amsterdam")
3
+ (puts (person.inspect))
4
+ (puts (. person :city)))
@@ -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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/kapusta/formatter'
5
+
6
+ exit Kapusta::Formatter.new(ARGV).run
data/exe/kapusta ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/kapusta/cli'
5
+
6
+ Kapusta::CLI.start
data/kapfmt ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ load File.expand_path('exe/kapfmt', __dir__)
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
@@ -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