kapusta 0.3.0 → 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: 179b8b97d775c6bff99bd3d5ac3e1df1f6ab8c17637b6e16f9d688ade7f9dd06
4
- data.tar.gz: 899899c13c9b1d4f85216df5d3989807d874a9ec862be94d48989f851b2d8111
3
+ metadata.gz: 01cc980a37ff63b56db021edb64f7ed9b4381569bb82e8e4ada099a2b17ba19e
4
+ data.tar.gz: 035f0f4a38d5d9fad65f4a53e8343bbcad4b681bd33dfa975f7c9c638a83ccb4
5
5
  SHA512:
6
- metadata.gz: 69247136466e119860e5fee232b0bb21fa40b9d23986f09051a4acccda476a9c42b5e3e63e1382367df6c3008876af2f6a69a9b6fd2bfc0e05e1c9e69ca6a8b0
7
- data.tar.gz: f65709e841e372a1dcc4c9a453d2251d58fb4fffbecd51fd1f0bcfb7c0423dc51109d6533e1b85d9b39bbc82cc994dcf64bdb5aad586c2b6476858db8e320d25
6
+ metadata.gz: caaeb31630a5c80607fe6c2907548c2874f177add0bdbd3555e72039dd598aaf481f91778043f6b81cd511c9ac8609e48f61c474766dac9b3e8a2fa41d408970
7
+ data.tar.gz: 5417ad8088be602618c3662660af820be6dbf15eb27d8300b1c542e3889fbfe1813d51ca7306f988a630616d1857f333141bf20b964d0869c2caa8c3eba869e3
data/README.md CHANGED
@@ -49,7 +49,7 @@ See `examples/bank-account.kap` and `examples/use_bank_account.rb`.
49
49
 
50
50
  ## Examples
51
51
 
52
- 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/).
53
53
 
54
54
  ```fennel
55
55
  (fn ack [m n]
@@ -90,7 +90,6 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
90
90
  | `values` uses Lua multiple returns | `values` lowers to a Ruby array, usually destructured |
91
91
  | `(print x)` is Lua's `print` (bare) | `(print x)` is Ruby's `p` (inspect-style) |
92
92
  | `with-open`, `tail!` | not provided |
93
- | macros | not provided for now |
94
93
 
95
94
  Kapusta-specific additions:
96
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")
@@ -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]
@@ -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
@@ -96,11 +96,45 @@ module Kapusta
96
96
  when '/' then emit_div(args, env, current_scope)
97
97
  when '%' then args.map { |arg| parenthesize(emit_expr(arg, env, current_scope)) }.join(' % ')
98
98
  when 'print' then emit_print(args, env, current_scope)
99
+ when 'quasi-sym' then "Kapusta::Sym.new(#{emit_expr(args[0], env, current_scope)})"
100
+ when 'quasi-list' then "Kapusta::List.new([#{args.map { |a| emit_expr(a, env, current_scope) }.join(', ')}])"
101
+ when 'quasi-list-tail' then emit_quasi_list_tail(args, env, current_scope)
102
+ when 'quasi-vec' then "Kapusta::Vec.new([#{args.map { |a| emit_expr(a, env, current_scope) }.join(', ')}])"
103
+ when 'quasi-vec-tail' then emit_quasi_vec_tail(args, env, current_scope)
104
+ when 'quasi-hash' then emit_quasi_hash(args, env, current_scope)
105
+ when 'quasi-gensym' then emit_quasi_gensym(args[0], env, current_scope)
106
+ when 'macro', 'macros', 'import-macros'
107
+ emit_error!("#{name} must appear at the top level and is consumed by the macro expander")
99
108
  else
100
109
  emit_error!("unknown special form: #{name}")
101
110
  end
102
111
  end
103
112
 
113
+ def emit_quasi_list_tail(args, env, current_scope)
114
+ head_items = args[0]
115
+ tail_expr = emit_expr(args[1], env, current_scope)
116
+ head_code = head_items.items.map { |item| emit_expr(item, env, current_scope) }.join(', ')
117
+ "Kapusta::List.new([#{head_code}, *#{parenthesize(tail_expr)}])"
118
+ end
119
+
120
+ def emit_quasi_vec_tail(args, env, current_scope)
121
+ head_items = args[0]
122
+ tail_expr = emit_expr(args[1], env, current_scope)
123
+ head_code = head_items.items.map { |item| emit_expr(item, env, current_scope) }.join(', ')
124
+ "Kapusta::Vec.new([#{head_code}, *#{parenthesize(tail_expr)}])"
125
+ end
126
+
127
+ def emit_quasi_gensym(arg, env, current_scope)
128
+ "Kapusta::Compiler::MacroExpander.fresh_gensym(#{emit_expr(arg, env, current_scope)})"
129
+ end
130
+
131
+ def emit_quasi_hash(args, env, current_scope)
132
+ pairs = args.each_slice(2).map do |key, value|
133
+ "[#{emit_expr(key, env, current_scope)}, #{emit_expr(value, env, current_scope)}]"
134
+ end
135
+ "Kapusta::HashLit.new([#{pairs.join(', ')}])"
136
+ end
137
+
104
138
  def emit_concat(args, env, current_scope)
105
139
  return '""' if args.empty?
106
140
 
@@ -79,8 +79,7 @@ module Kapusta
79
79
  if sub.is_a?(Sym)
80
80
  raise PatternNotTranslatable if sub.name == '_'
81
81
 
82
- bind_name = sub.name.start_with?('?') ? sub.name.delete_prefix('?') : sub.name
83
- ruby_name = define_local(current_env, bind_name)
82
+ ruby_name = define_local(current_env, sub.name)
84
83
  lines << "#{ruby_name} = #{access}"
85
84
  else
86
85
  sub_code, current_env = try_emit_native_pattern_bind(sub, access, current_env) ||
@@ -96,8 +95,7 @@ module Kapusta
96
95
  when Sym
97
96
  return ['_', env, nil] if pattern.name == '_'
98
97
 
99
- bind_name = pattern.name.start_with?('?') ? pattern.name.delete_prefix('?') : pattern.name
100
- ruby_name = define_local(env, bind_name)
98
+ ruby_name = define_local(env, pattern.name)
101
99
  [ruby_name, env, nil]
102
100
  when Vec
103
101
  inner = []
@@ -137,8 +135,7 @@ module Kapusta
137
135
 
138
136
  return '*' if sym.name == '_'
139
137
 
140
- bind_name = sym.name.start_with?('?') ? sym.name.delete_prefix('?') : sym.name
141
- "*#{define_local(env, bind_name)}"
138
+ "*#{define_local(env, sym.name)}"
142
139
  end
143
140
 
144
141
  class PatternNotTranslatable < StandardError; end
@@ -179,12 +176,11 @@ module Kapusta
179
176
  return '_' if name == '_'
180
177
 
181
178
  if nil_allowing_pattern_name?(name)
182
- bind_name = name.start_with?('?') ? name.delete_prefix('?') : name
183
- raise PatternNotTranslatable if state[:bound_names].key?(bind_name)
179
+ raise PatternNotTranslatable if state[:bound_names].key?(name)
184
180
 
185
- state[:bound_names][bind_name] = true
186
- state[:binding_names] << bind_name
187
- sanitize_local(bind_name)
181
+ state[:bound_names][name] = true
182
+ state[:binding_names] << name
183
+ sanitize_local(name)
188
184
  else
189
185
  binding = mode == :match ? env.lookup_if_defined(name) : nil
190
186
  if state[:bound_names].key?(name)
@@ -263,7 +263,8 @@ module Kapusta
263
263
 
264
264
  def sanitize_local(name)
265
265
  base = Kapusta.kebab_to_snake(name.respond_to?(:name) ? name.name : name)
266
- base = base.gsub('?', '_q').gsub('!', '_bang')
266
+ base = base.sub(/\A\?/, 'q_').gsub('?', '_q')
267
+ base = base.sub(/\A!/, 'bang_').gsub('!', '_bang')
267
268
  base = base.gsub(/[^a-zA-Z0-9_]/, '_')
268
269
  if base.empty? || base.match?(/\A\d/) || base.match?(/\A[A-Z]/) || self.class::RUBY_KEYWORDS.include?(base)
269
270
  base = "_#{base}"