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
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'kapusta/formatter'
5
+ require 'stringio'
6
+ require 'tmpdir'
7
+
8
+ def capture_stdout
9
+ previous_stdout = $stdout
10
+ $stdout = StringIO.new
11
+ yield
12
+ $stdout.string
13
+ ensure
14
+ $stdout = previous_stdout
15
+ end
16
+
17
+ def capture_stderr
18
+ previous_stderr = $stderr
19
+ $stderr = StringIO.new
20
+ yield
21
+ $stderr.string
22
+ ensure
23
+ $stderr = previous_stderr
24
+ end
25
+
26
+ def with_stdin(input)
27
+ previous_stdin = $stdin
28
+ $stdin = StringIO.new(input)
29
+ yield
30
+ ensure
31
+ $stdin = previous_stdin
32
+ end
33
+
34
+ RSpec.describe Kapusta::Formatter do
35
+ repo_root = File.expand_path('..', __dir__)
36
+ example_idempotence_paths = Dir.glob(File.join(repo_root, 'examples/**/*.kap')).map do |path|
37
+ path.delete_prefix("#{repo_root}/")
38
+ end.freeze
39
+
40
+ it 'formats source with the built-in printer even without fnlfmt in PATH' do
41
+ Dir.mktmpdir do |dir|
42
+ path = File.join(dir, 'sample.kap')
43
+ File.write(path, <<~KAP)
44
+ (let [uri (URI.join (ivar base-uri) (.. "/posts/" id)) body (Net.HTTP.get uri) post (JSON.parse body {:symbolize-names true}) {: title : author} post] (values title author))
45
+ KAP
46
+
47
+ previous_path = ENV.fetch('PATH', nil)
48
+ ENV['PATH'] = ''
49
+
50
+ output = capture_stdout do
51
+ expect(described_class.new([path]).run).to eq(0)
52
+ end
53
+
54
+ expect(output).to eq(<<~KAP)
55
+ (let [uri (URI.join (ivar base-uri) (.. "/posts/" id))
56
+ body (Net.HTTP.get uri)
57
+ post (JSON.parse body {:symbolize-names true})
58
+ {: title : author} post]
59
+ (values title author))
60
+ KAP
61
+ ensure
62
+ ENV['PATH'] = previous_path
63
+ end
64
+ end
65
+
66
+ it 'rewrites files in place with --fix' do
67
+ Dir.mktmpdir do |dir|
68
+ path = File.join(dir, 'sample.kap')
69
+ File.write(path, <<~KAP)
70
+ (let [words ["red" "green" "blue" "black" "olive"]](-> words (: :select (fn [w] (< (length w) 5))) (: :map (fn [w] (w.upcase))) (: :sort) (: :each (fn [w] (puts w)))))
71
+ KAP
72
+
73
+ expect(described_class.new(['--fix', path]).run).to eq(0)
74
+ expect(File.read(path)).to eq(<<~KAP)
75
+ (let [words ["red" "green" "blue" "black" "olive"]]
76
+ (-> words
77
+ (: :select (fn [w] (< (length w) 5)))
78
+ (: :map (fn [w] (w.upcase)))
79
+ (: :sort)
80
+ (: :each (fn [w] (puts w)))))
81
+ KAP
82
+ end
83
+ end
84
+
85
+ it 'reports dirty files with --check' do
86
+ Dir.mktmpdir do |dir|
87
+ path = File.join(dir, 'sample.kap')
88
+ File.write(path, '(fn tick [](set (ivar n) (+ (ivar n) 1))(ivar n))')
89
+
90
+ error_output = capture_stderr do
91
+ expect(described_class.new(['--check', path]).run).to eq(1)
92
+ end
93
+
94
+ expect(error_output).to include("Not formatted: #{path}")
95
+ end
96
+ end
97
+
98
+ it 'reads stdin when the input path is -' do
99
+ output = with_stdin("(let [name (or (. ARGV 0) \"world\")](puts (.. \"Hello, \" name \"!\")))\n") do
100
+ capture_stdout do
101
+ expect(described_class.new(['-']).run).to eq(0)
102
+ end
103
+ end
104
+
105
+ expect(output).to eq(<<~KAP)
106
+ (let [name (or (. ARGV 0) "world")]
107
+ (puts (.. "Hello, " name "!")))
108
+ KAP
109
+ end
110
+
111
+ it 'checks stdin when the input path is -' do
112
+ error_output = with_stdin("(fn tick [](set (ivar n) (+ (ivar n) 1))(ivar n))\n") do
113
+ capture_stderr do
114
+ expect(described_class.new(['--check', '-']).run).to eq(1)
115
+ end
116
+ end
117
+
118
+ expect(error_output).to include('Not formatted: -')
119
+ end
120
+
121
+ it 'rejects --fix with stdin' do
122
+ error_output = with_stdin("(+ 1 2)\n") do
123
+ capture_stderr do
124
+ expect(described_class.new(['--fix', '-']).run).to eq(1)
125
+ end
126
+ end
127
+
128
+ expect(error_output).to include('Cannot use --fix with stdin (-).')
129
+ end
130
+
131
+ it 'rejects comments instead of dropping them' do
132
+ Dir.mktmpdir do |dir|
133
+ path = File.join(dir, 'sample.kap')
134
+ File.write(path, "(fn main [] ; comment\n (print 1))\n")
135
+
136
+ error_output = capture_stderr do
137
+ expect(described_class.new([path]).run).to eq(1)
138
+ end
139
+
140
+ expect(error_output).to include('kapfmt does not support comments yet.')
141
+ end
142
+ end
143
+
144
+ it 'formats let bindings with hanging pair alignment' do
145
+ Dir.mktmpdir do |dir|
146
+ path = File.join(dir, 'sample.kap')
147
+ File.write(path, <<~KAP)
148
+ (let [[a b c] [1 2 3] {: name : age} {:name "Ada" :age 36}]
149
+ (print (+ a b c))
150
+ (print name age))
151
+ KAP
152
+
153
+ output = capture_stdout do
154
+ expect(described_class.new([path]).run).to eq(0)
155
+ end
156
+
157
+ expect(output).to eq(<<~KAP)
158
+ (let [[a b c] [1 2 3]
159
+ {: name : age} {:name "Ada" :age 36}]
160
+ (print (+ a b c))
161
+ (print name age))
162
+ KAP
163
+ end
164
+ end
165
+
166
+ example_idempotence_paths.each do |relative_path|
167
+ it "keeps #{relative_path} unchanged" do
168
+ path = File.expand_path("../#{relative_path}", __dir__)
169
+ source = File.read(path)
170
+
171
+ formatted = described_class.new([]).send(:format_source, source)
172
+
173
+ expect(formatted).to eq(source)
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'kapusta'
5
+
6
+ RSpec.configure do |config|
7
+ config.disable_monkey_patching!
8
+ config.example_status_persistence_file_path = '.rspec_status'
9
+ config.order = :random
10
+
11
+ Kernel.srand config.seed
12
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kapusta
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Evgenii Morozov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-04-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Kapusta is a Lisp for the Ruby runtime.
14
+ email:
15
+ executables:
16
+ - kapfmt
17
+ - kapusta
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rspec"
22
+ - Gemfile
23
+ - README.md
24
+ - Rakefile
25
+ - bin/console
26
+ - bin/setup
27
+ - examples/accumulator.kap
28
+ - examples/ackermann.kap
29
+ - examples/anagram.kap
30
+ - examples/binary-search.kap
31
+ - examples/block-sort.kap
32
+ - examples/calc.kap
33
+ - examples/counter.kap
34
+ - examples/describe.kap
35
+ - examples/destructure.kap
36
+ - examples/doto.kap
37
+ - examples/egg-count.kap
38
+ - examples/even-squares.kap
39
+ - examples/exceptions.kap
40
+ - examples/factorial.kap
41
+ - examples/fib.kap
42
+ - examples/fizzbuzz.kap
43
+ - examples/gcd.kap
44
+ - examples/greet.kap
45
+ - examples/hashfn.kap
46
+ - examples/kwargs.kap
47
+ - examples/leap-year.kap
48
+ - examples/match.kap
49
+ - examples/min-max.kap
50
+ - examples/module-header.kap
51
+ - examples/palindrome.kap
52
+ - examples/pangram.kap
53
+ - examples/pcall.kap
54
+ - examples/pipeline.kap
55
+ - examples/points.kap
56
+ - examples/primes.kap
57
+ - examples/raindrops.kap
58
+ - examples/record.kap
59
+ - examples/regex.kap
60
+ - examples/ruby-eval.kap
61
+ - examples/safe-lookup.kap
62
+ - examples/scopes.kap
63
+ - examples/shapes.kap
64
+ - examples/squares.kap
65
+ - examples/stack.kap
66
+ - examples/sum.kap
67
+ - examples/tset.kap
68
+ - examples/two-sum.kap
69
+ - exe/kapfmt
70
+ - exe/kapusta
71
+ - kapfmt
72
+ - kapusta.gemspec
73
+ - lib/kapusta.rb
74
+ - lib/kapusta/ast.rb
75
+ - lib/kapusta/cli.rb
76
+ - lib/kapusta/compiler.rb
77
+ - lib/kapusta/compiler/emitter.rb
78
+ - lib/kapusta/compiler/emitter/bindings.rb
79
+ - lib/kapusta/compiler/emitter/collections.rb
80
+ - lib/kapusta/compiler/emitter/control_flow.rb
81
+ - lib/kapusta/compiler/emitter/expressions.rb
82
+ - lib/kapusta/compiler/emitter/interop.rb
83
+ - lib/kapusta/compiler/emitter/patterns.rb
84
+ - lib/kapusta/compiler/emitter/support.rb
85
+ - lib/kapusta/compiler/normalizer.rb
86
+ - lib/kapusta/compiler/runtime.rb
87
+ - lib/kapusta/env.rb
88
+ - lib/kapusta/formatter.rb
89
+ - lib/kapusta/reader.rb
90
+ - lib/kapusta/support.rb
91
+ - lib/kapusta/version.rb
92
+ - spec/cli_spec.rb
93
+ - spec/examples_spec.rb
94
+ - spec/formatter_spec.rb
95
+ - spec/spec_helper.rb
96
+ homepage:
97
+ licenses: []
98
+ metadata:
99
+ rubygems_mfa_required: 'true'
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.1'
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.5.22
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: A Lisp for the Ruby runtime
119
+ test_files: []