kapusta 0.2.4 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef91159e2623e199fc391fa483440fa9f8ca76f273fbc31caeb0b07e891ee92c
4
- data.tar.gz: 5468818f0ad4b0509a31635d886f3374009e3e8499a197d82f99da22c53e3a12
3
+ metadata.gz: 01cc980a37ff63b56db021edb64f7ed9b4381569bb82e8e4ada099a2b17ba19e
4
+ data.tar.gz: 035f0f4a38d5d9fad65f4a53e8343bbcad4b681bd33dfa975f7c9c638a83ccb4
5
5
  SHA512:
6
- metadata.gz: 7bcc51b164c21357c2aa2ef52e4e544cba0116ca8d8b6b1c02ecb55dc62697508a3cbb22d4c56a0a7cf0da77f53d7500827ecd2debe0760fec34ebe4d992be07
7
- data.tar.gz: 40b1b709b5e083b94584424f9fb2ad7c3ec6908786ea80051173df8edaa380b9b5b76d0c485a6377f7e77bbed5799633501204d7a39acf0b919eb74197fabcf2
6
+ metadata.gz: caaeb31630a5c80607fe6c2907548c2874f177add0bdbd3555e72039dd598aaf481f91778043f6b81cd511c9ac8609e48f61c474766dac9b3e8a2fa41d408970
7
+ data.tar.gz: 5417ad8088be602618c3662660af820be6dbf15eb27d8300b1c542e3889fbfe1813d51ca7306f988a630616d1857f333141bf20b964d0869c2caa8c3eba869e3
data/README.md CHANGED
@@ -8,6 +8,12 @@ Instead, Kapusta aims to bring some of the simplicity and joy of Lisp to Ruby. W
8
8
 
9
9
  For more information about Kapusta, see the official Fennel documentation and tutorials.
10
10
 
11
+ ## Features
12
+
13
+ 1. Compiles to readable Ruby.
14
+ 2. Compiled `.rb` files don't depend on Kapusta. Run with plain `ruby`, or load `.kap` files at runtime via `require 'kapusta'`.
15
+ 3. Two-way Ruby interop.
16
+
11
17
  ## Usage
12
18
 
13
19
  ```
@@ -43,7 +49,7 @@ See `examples/bank-account.kap` and `examples/use_bank_account.rb`.
43
49
 
44
50
  ## Examples
45
51
 
46
- See [`examples/`](https://github.com/evmorov/kapusta/tree/main/examples).
52
+ See [`examples/`](https://github.com/evmorov/kapusta/tree/main/examples/) and [`examples-compiled/`](https://github.com/evmorov/kapusta/tree/main/examples-compiled/).
47
53
 
48
54
  ```fennel
49
55
  (fn ack [m n]
@@ -84,7 +90,6 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
84
90
  | `values` uses Lua multiple returns | `values` lowers to a Ruby array, usually destructured |
85
91
  | `(print x)` is Lua's `print` (bare) | `(print x)` is Ruby's `p` (inspect-style) |
86
92
  | `with-open`, `tail!` | not provided |
87
- | macros | not provided for now |
88
93
 
89
94
  Kapusta-specific additions:
90
95
 
data/bin/check-all ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ echo "== rspec =="
5
+ bundle exec rspec
6
+
7
+ echo "== rubocop -A =="
8
+ bundle exec rubocop -A
9
+
10
+ echo "== kapfmt =="
11
+ for file in examples/*.kap; do ./exe/kapfmt --fix "$file"; done
12
+
13
+ echo "== fennel parity =="
14
+ bin/fennel-parity
15
+
16
+ echo "== compile examples =="
17
+ bin/compile-examples
18
+
19
+ echo "== Success! =="
data/bin/fennel-parity ADDED
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'open3'
5
+
6
+ ROOT = File.expand_path('..', __dir__)
7
+ EXAMPLES = File.join(ROOT, 'examples')
8
+ KAPUSTA = File.join(ROOT, 'exe', 'kapusta')
9
+ KAPFMT = File.join(ROOT, 'exe', 'kapfmt')
10
+
11
+ COMPATIBLE = %w[
12
+ ackermann.kap
13
+ anonymous-greeter.kap
14
+ climbing-stairs.kap
15
+ describe.kap
16
+ destructure.kap
17
+ factorial.kap
18
+ fib.kap
19
+ fizzbuzz.kap
20
+ gcd.kap
21
+ hashfn.kap
22
+ leap-year.kap
23
+ macros-dbg.kap
24
+ macros-multi.kap
25
+ macros-swap.kap
26
+ macros-thrice-if.kap
27
+ macros-unless.kap
28
+ macros-when-let.kap
29
+ match.kap
30
+ min-max.kap
31
+ or-patterns.kap
32
+ packet-router.kap
33
+ points.kap
34
+ primes.kap
35
+ safe-lookup.kap
36
+ shapes.kap
37
+ squares.kap
38
+ sum.kap
39
+ tic-tac-toe.kap
40
+ underscore-patterns.kap
41
+ ].freeze
42
+
43
+ def run(cmd, file)
44
+ out, err, status = Open3.capture3(cmd, file, chdir: EXAMPLES)
45
+ [out, err, status]
46
+ rescue StandardError => e
47
+ ['', e.message, nil]
48
+ end
49
+
50
+ def strip_outer_quotes(line)
51
+ if line.length >= 2 && line.start_with?('"') && line.end_with?('"')
52
+ line[1..-2]
53
+ else
54
+ line
55
+ end
56
+ end
57
+
58
+ def normalize_kapusta(out)
59
+ out.lines.map { |l| strip_outer_quotes(l.chomp) }
60
+ end
61
+
62
+ def normalize_fennel(out)
63
+ # Lua's `print` joins multiple args with TAB on one line; Ruby's `p`
64
+ # prints each on its own line. Split tabs so the two layouts line up.
65
+ out.lines.flat_map { |l| l.chomp.split("\t") }
66
+ end
67
+
68
+ def unified_diff(label_kap, kap_lines, label_fnl, fnl_lines)
69
+ max = [kap_lines.length, fnl_lines.length].max
70
+ rows = []
71
+ (0...max).each do |i|
72
+ la = kap_lines[i]
73
+ lb = fnl_lines[i]
74
+ next if la == lb
75
+
76
+ rows << format(' line %<n>3d %<a>s: %<la>s',
77
+ n: i + 1, a: label_kap, la: la.inspect)
78
+ rows << format(' %<b>s: %<lb>s',
79
+ b: label_fnl, lb: lb.inspect)
80
+ end
81
+ rows.join("\n")
82
+ end
83
+
84
+ def check_format(path)
85
+ k_out, k_err, k_status = run(KAPFMT, path)
86
+ f_out, f_err, f_status = run('fnlfmt', path)
87
+
88
+ return "kapfmt exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
89
+ return "fnlfmt exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
90
+ return if k_out == f_out
91
+
92
+ k_lines = k_out.lines.map(&:chomp)
93
+ f_lines = f_out.lines.map(&:chomp)
94
+ "format differs:\n#{unified_diff('kapfmt', k_lines, 'fnlfmt', f_lines)}"
95
+ end
96
+
97
+ def check_run(path)
98
+ k_out, k_err, k_status = run(KAPUSTA, path)
99
+ f_out, f_err, f_status = run('fennel', path)
100
+
101
+ return "kapusta exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
102
+ return "fennel exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
103
+
104
+ k_lines = normalize_kapusta(k_out)
105
+ f_lines = normalize_fennel(f_out)
106
+ return if k_lines == f_lines
107
+
108
+ if k_lines.length == f_lines.length
109
+ "output differs:\n#{unified_diff('kapusta', k_lines, 'fennel', f_lines)}"
110
+ else
111
+ "line count differs (kapusta=#{k_lines.length}, fennel=#{f_lines.length})"
112
+ end
113
+ end
114
+
115
+ def check(name)
116
+ path = File.join(EXAMPLES, name)
117
+ return [:miss, "missing file: #{path}"] unless File.exist?(path)
118
+
119
+ fmt_reason = check_format(path)
120
+ return [:fail, fmt_reason] if fmt_reason
121
+
122
+ run_reason = check_run(path)
123
+ return [:fail, run_reason] if run_reason
124
+
125
+ [:ok, nil]
126
+ end
127
+
128
+ puts "Checking #{COMPATIBLE.size} compatible examples (kapfmt vs fnlfmt, kapusta vs fennel)...\n\n"
129
+
130
+ failures = []
131
+ COMPATIBLE.each do |name|
132
+ status, reason = check(name)
133
+ case status
134
+ when :ok
135
+ puts "OK #{name}"
136
+ when :miss
137
+ failures << [name, reason]
138
+ puts "MISS #{name}"
139
+ when :fail
140
+ failures << [name, reason]
141
+ puts "FAIL #{name}"
142
+ end
143
+ end
144
+
145
+ puts
146
+ if failures.empty?
147
+ puts "All #{COMPATIBLE.size} compatible examples produce matching output."
148
+ exit 0
149
+ else
150
+ puts "#{failures.size} of #{COMPATIBLE.size} examples failed:\n\n"
151
+ failures.each do |name, reason|
152
+ puts " * #{name}"
153
+ reason.each_line { |l| puts " #{l.rstrip}" }
154
+ puts
155
+ end
156
+ exit 1
157
+ end
@@ -0,0 +1,9 @@
1
+ (macro dbg [expr]
2
+ `(let [v# ,expr]
3
+ (print "dbg" v#)
4
+ v#))
5
+
6
+ (let [result (dbg (+ 1 2 3))]
7
+ (print "result" result))
8
+
9
+ (dbg (* 10 (+ 2 3)))
@@ -0,0 +1,12 @@
1
+ (macros {:my-min (fn [x y]
2
+ `(let [x# ,x
3
+ y# ,y]
4
+ (if (< x# y#) x# y#)))
5
+ :my-max (fn [x y]
6
+ `(let [x# ,x
7
+ y# ,y]
8
+ (if (< x# y#) y# x#)))})
9
+
10
+ (print (my-min 10 20))
11
+ (print (my-max 10 20))
12
+ (print (my-min 7 7))
@@ -0,0 +1,9 @@
1
+ (macro swap! [a b]
2
+ `(let [tmp# ,a]
3
+ (set ,a ,b)
4
+ (set ,b tmp#)))
5
+
6
+ (var x 1)
7
+ (var y 2)
8
+ (swap! x y)
9
+ (print x y)
@@ -0,0 +1,18 @@
1
+ (macro thrice-if [condition result]
2
+ (fn step [i]
3
+ (if (< 0 i)
4
+ `(if ,condition
5
+ (do
6
+ ,result
7
+ ,(step (- i 1))))))
8
+
9
+ (step 3))
10
+
11
+ (var counter 0)
12
+
13
+ (fn ready? [] (< counter 5))
14
+
15
+ (fn tick [] (set counter (+ counter 1)) (print "tick" counter))
16
+
17
+ (thrice-if (ready?) (tick))
18
+ (print "final" counter)
@@ -0,0 +1,7 @@
1
+ (macro unless [condition & body]
2
+ `(if (not ,condition)
3
+ (do
4
+ ,(unpack body))))
5
+
6
+ (unless false (print "shown") (print "also shown"))
7
+ (unless true (print "hidden"))
@@ -0,0 +1,7 @@
1
+ (macro when-let [[name val] & body]
2
+ `(let [,name ,val]
3
+ (when ,name ,(unpack body))))
4
+
5
+ (when-let [x (+ 1 2)] (print "got" x))
6
+ (when-let [y nil] (print "should not print"))
7
+ (print "done")
@@ -0,0 +1,9 @@
1
+ (fn manhattan [edge]
2
+ (let [{:from [x1 y1] :to [x2 y2]} edge]
3
+ (+ (: (- x1 x2) :abs) (: (- y1 y2) :abs))))
4
+
5
+ (fn total-distance [edges]
6
+ (accumulate [total 0 _ edge (ipairs edges)]
7
+ (+ total (manhattan edge))))
8
+
9
+ (print (total-distance [{:from [0 0] :to [3 4]} {:from [1 1] :to [4 5]}]))
@@ -1,15 +1,12 @@
1
1
  (fn inbox-line [user event]
2
2
  (match event
3
3
  [:score user points] (.. "score:" points)
4
- [:profile user ?city] (if city (.. "city:" city) "city:nil")
4
+ [:profile user ?city] (if ?city (.. "city:" ?city) "city:nil")
5
5
  _ "other"))
6
6
 
7
7
  (fn score-delta [user event]
8
8
  (case event
9
- (where (or [:bonus (= user) points] [:score (= user) points])
10
- (> points 0)
11
- (< points 10))
12
- points
9
+ (where (or [:bonus (= user) p] [:score (= user) p]) (> p 0) (< p 10)) p
13
10
  _ 0))
14
11
 
15
12
  (fn packet-kind [packet]
@@ -0,0 +1,14 @@
1
+ (fn subtract-product-sum [n]
2
+ (var x n)
3
+ (var product 1)
4
+ (var sum 0)
5
+ (while (> x 0)
6
+ (let [d (% x 10)]
7
+ (set product (* product d))
8
+ (set sum (+ sum d))
9
+ (set x (: (/ x 10) :floor))))
10
+ (- product sum))
11
+
12
+ (print (subtract-product-sum 234))
13
+ (print (subtract-product-sum 4421))
14
+ (print (subtract-product-sum 1))
@@ -6,13 +6,8 @@
6
6
  [["O" _ _] ["O" _ _] ["O" _ _]] "O"
7
7
  _ "draw"))
8
8
 
9
- (each
10
- [
11
- _
12
- board
13
- (ipairs [
14
- [["X" "X" "X"] ["O" "" ""] ["" "O" ""]]
15
- [["O" "X" "X"] ["O" "" "X"] ["O" "" ""]]
16
- [["X" "O" ""] ["" "X" "O"] ["" "" "X"]]
17
- [["X" "O" "X"] ["O" "X" "O"] ["O" "X" "O"]]])]
9
+ (each [_ board (ipairs [[["X" "X" "X"] ["O" "" ""] ["" "O" ""]]
10
+ [["O" "X" "X"] ["O" "" "X"] ["O" "" ""]]
11
+ [["X" "O" ""] ["" "X" "O"] ["" "" "X"]]
12
+ [["X" "O" "X"] ["O" "X" "O"] ["O" "X" "O"]]])]
18
13
  (print (winner board)))
@@ -0,0 +1,22 @@
1
+ (macro floor-div [a b]
2
+ `(: (/ ,a ,b) :floor))
3
+
4
+ (macro divide-out! [v d]
5
+ `(while (= 0 (% ,v ,d))
6
+ (set ,v (floor-div ,v ,d))))
7
+
8
+ (fn ugly? [n]
9
+ (var x n)
10
+ (if (<= n 0)
11
+ false
12
+ (do
13
+ (divide-out! x 2)
14
+ (divide-out! x 3)
15
+ (divide-out! x 5)
16
+ (= x 1))))
17
+
18
+ (print (ugly? 6))
19
+ (print (ugly? 1))
20
+ (print (ugly? 14))
21
+ (print (ugly? 0))
22
+ (print (ugly? 30))
data/lib/kapusta/ast.rb CHANGED
@@ -9,6 +9,8 @@ module Kapusta
9
9
  end
10
10
  end
11
11
 
12
+ BlankLine = Class.new
13
+
12
14
  class Sym
13
15
  attr_reader :name
14
16
 
@@ -74,17 +76,25 @@ module Kapusta
74
76
 
75
77
  class Vec
76
78
  attr_reader :items
79
+ attr_accessor :multiline_source
77
80
 
78
81
  def initialize(items)
79
82
  @items = items
83
+ @multiline_source = false
84
+ end
85
+
86
+ def to_ary
87
+ @items
80
88
  end
81
89
  end
82
90
 
83
91
  class HashLit
84
92
  attr_reader :entries
93
+ attr_accessor :multiline_source
85
94
 
86
95
  def initialize(entries)
87
96
  @entries = entries
97
+ @multiline_source = false
88
98
  end
89
99
 
90
100
  def pairs
@@ -98,9 +108,11 @@ module Kapusta
98
108
 
99
109
  class List
100
110
  attr_reader :items
111
+ attr_accessor :multiline_source
101
112
 
102
113
  def initialize(items)
103
114
  @items = items
115
+ @multiline_source = false
104
116
  end
105
117
 
106
118
  def head
@@ -115,4 +127,34 @@ module Kapusta
115
127
  @items.empty?
116
128
  end
117
129
  end
130
+
131
+ class AutoGensym < Sym
132
+ def inspect
133
+ "#<AutoGensym #{@name}#>"
134
+ end
135
+ end
136
+
137
+ class Quasiquote
138
+ attr_reader :form
139
+
140
+ def initialize(form)
141
+ @form = form
142
+ end
143
+ end
144
+
145
+ class Unquote
146
+ attr_reader :form
147
+
148
+ def initialize(form)
149
+ @form = form
150
+ end
151
+ end
152
+
153
+ class UnquoteSplice
154
+ attr_reader :form
155
+
156
+ def initialize(form)
157
+ @form = form
158
+ end
159
+ end
118
160
  end
@@ -281,12 +281,23 @@ module Kapusta
281
281
  else
282
282
  define_local(env, target.name)
283
283
  end
284
- ["#{ruby_name} = #{value_code}", env]
284
+ [emit_assignment(ruby_name, value_code), env]
285
285
  else
286
286
  [emit_set_target(target, value_code, env, current_scope), env]
287
287
  end
288
288
  end
289
289
 
290
+ def emit_assignment(lhs, value_code)
291
+ prefix = "#{lhs} "
292
+ if value_code.start_with?(prefix) &&
293
+ (m = value_code[prefix.length..].match(/\A(\S+) (.*)\z/m)) &&
294
+ !m[1].include?('=')
295
+ "#{lhs} #{m[1]}= #{m[2]}"
296
+ else
297
+ "#{lhs} = #{value_code}"
298
+ end
299
+ end
300
+
290
301
  def emit_set_expr(args, env, current_scope)
291
302
  target = args[0]
292
303
  value_code = emit_expr(args[1], env, current_scope)
@@ -302,7 +313,7 @@ module Kapusta
302
313
  last = segments.last
303
314
  snake = Kapusta.kebab_to_snake(last)
304
315
  if direct_method_name?(last)
305
- "#{receiver}.#{snake} = #{value_code}"
316
+ emit_assignment("#{receiver}.#{snake}", value_code)
306
317
  else
307
318
  "#{receiver}.public_send(:\"#{snake}=\", #{value_code})"
308
319
  end
@@ -310,7 +321,7 @@ module Kapusta
310
321
  binding = env.lookup(target.name)
311
322
  emit_error!("cannot set method binding: #{target.name}") if method_binding?(binding)
312
323
 
313
- "#{binding} = #{value_code}"
324
+ emit_assignment(binding, value_code)
314
325
  end
315
326
  when List
316
327
  head = target.head
@@ -319,13 +330,13 @@ module Kapusta
319
330
  keys = target.items[2..].map { |item| emit_expr(item, env, current_scope) }
320
331
  receiver = simple_expression?(object_code) ? object_code : parenthesize(object_code)
321
332
  prefix = keys[0...-1].map { |k| "[#{k}]" }.join
322
- "#{receiver}#{prefix}[#{keys.last}] = #{value_code}"
333
+ emit_assignment("#{receiver}#{prefix}[#{keys.last}]", value_code)
323
334
  elsif head.is_a?(Sym) && head.name == 'ivar'
324
- "@#{Kapusta.kebab_to_snake(target.items[1].name)} = #{value_code}"
335
+ emit_assignment("@#{Kapusta.kebab_to_snake(target.items[1].name)}", value_code)
325
336
  elsif head.is_a?(Sym) && head.name == 'cvar'
326
- "@@#{Kapusta.kebab_to_snake(target.items[1].name)} = #{value_code}"
337
+ emit_assignment("@@#{Kapusta.kebab_to_snake(target.items[1].name)}", value_code)
327
338
  elsif head.is_a?(Sym) && head.name == 'gvar'
328
- "$#{global_name(target.items[1].name)} = #{value_code}"
339
+ emit_assignment("$#{global_name(target.items[1].name)}", value_code)
329
340
  else
330
341
  emit_error!("bad set target: #{target.inspect}")
331
342
  end