kapusta 0.7.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f35530eebc269dfec62ee1f36f6cc11a252ee4590eeac961c9b47e4e49ca09
4
- data.tar.gz: a0ee60d21fb9a6066915d916e465bef09d9208c0469712673db35bea83efd7c4
3
+ metadata.gz: 520158590f1244ac17f70b58ded945f66be00060f6da89cebb366fefaf27b62e
4
+ data.tar.gz: 6773e0faa320f23cb15106e710f380a39093c9fc6e56cead8941c28e0ccf16c5
5
5
  SHA512:
6
- metadata.gz: 350e111e7cfe045723ea1982c18762727d64ea615219d904a79af2933874af5ce355076e242ed9c0bf5e91d135dbd4498b2b5ae107b05035f205e3ccb87bbb54
7
- data.tar.gz: 7e242c3fb28fb0d5d2839f7e3d6e5dca96dda90204e1a81024111e1fc5ef4f6c7abb2a6e3628e5894eac46ce9d1c10f21db80bcae63da3ec9b4874a747b1bba1
6
+ metadata.gz: 814d37c473287d2d1e54568c7d21c22d88703cd89da761100c1f20950e47cb18eb2ac4807aa596878ded72eb7049a6083d2b9de46bc76ce2ea55787f6993f61f
7
+ data.tar.gz: 3a15fc05522e617cf46e8709a55dc8497252503f2b995dffaa4f8995fc34a54e9bcc2a711cd501884203bf0113c7924f159377459de8f2524e9c371c018d8238
data/README.md CHANGED
@@ -104,7 +104,7 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
104
104
  Kapusta-specific additions:
105
105
 
106
106
  - `module` and `class` for Ruby host structure, including file-header forms
107
- - `ivar` (`@var`) / `cvar` (`@@var`) / `gvar` (`$var`) escape hatches
107
+ - `ivar` or `@var`) / `cvar` or `@@var` / `gvar` or `$var`
108
108
  - `try` / `catch` / `finally` plus `raise` for exceptions
109
109
  - `(ruby "...")` raw host escape hatch
110
110
  - a trailing symbol-keyed hash is emitted as Ruby keyword arguments
data/bin/fennel-parity CHANGED
@@ -24,6 +24,9 @@ COMPATIBLE = %w[
24
24
  hashfn.kap
25
25
  leap-year.kap
26
26
  macros-dbg.kap
27
+ macros-import-helpers.kap
28
+ macros-import-whole.kap
29
+ macros-import.kap
27
30
  macros-multi.kap
28
31
  macros-swap.kap
29
32
  macros-thrice-if.kap
@@ -40,17 +43,20 @@ COMPATIBLE = %w[
40
43
  shapes.kap
41
44
  squares.kap
42
45
  sum.kap
46
+ thread-styles.kap
43
47
  tic-tac-toe.kap
44
48
  underscore-patterns.kap
45
49
  ].freeze
46
50
 
47
- def run(cmd, file, chdir: EXAMPLES)
48
- out, err, status = Open3.capture3(cmd, file, chdir:)
51
+ def run(cmd, file, chdir: EXAMPLES, env: {})
52
+ out, err, status = Open3.capture3(env, cmd, file, chdir:)
49
53
  [out, err, status]
50
54
  rescue StandardError => e
51
55
  ['', e.message, nil]
52
56
  end
53
57
 
58
+ FENNEL_ENV = { 'FENNEL_MACRO_PATH' => './?.kapm;./?.kap' }.freeze
59
+
54
60
  def strip_outer_quotes(line)
55
61
  if line.length >= 2 && line.start_with?('"') && line.end_with?('"')
56
62
  line[1..-2]
@@ -100,7 +106,7 @@ end
100
106
 
101
107
  def check_run(path)
102
108
  k_out, k_err, k_status = run(KAPUSTA, path)
103
- f_out, f_err, f_status = run('fennel', path)
109
+ f_out, f_err, f_status = run('fennel', path, env: FENNEL_ENV)
104
110
 
105
111
  return "kapusta exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
106
112
  return "fennel exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
@@ -132,7 +138,7 @@ end
132
138
  def check_error(name)
133
139
  path = File.join(EXAMPLES_ERRORS, name)
134
140
  _, _, k_status = run(KAPUSTA, path, chdir: EXAMPLES_ERRORS)
135
- _, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS)
141
+ _, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS, env: FENNEL_ENV)
136
142
 
137
143
  return [:fail, 'fennel unexpectedly succeeded'] if f_status&.success?
138
144
  return [:fail, "kapusta unexpectedly succeeded (exit #{k_status&.exitstatus})"] if k_status&.success?
@@ -0,0 +1,17 @@
1
+ (class HitCounter)
2
+
3
+ (set @@total 0)
4
+
5
+ (fn initialize [name] (set @name name))
6
+
7
+ (fn hit []
8
+ (set @@total (+ @@total 1))
9
+ (set $last-hitter @name)
10
+ @@total)
11
+
12
+ (let [a (HitCounter.new "alice")
13
+ b (HitCounter.new "bob")]
14
+ (print (a.hit))
15
+ (print (b.hit))
16
+ (print (a.hit))
17
+ (print $last-hitter))
@@ -0,0 +1,9 @@
1
+ (local FACTOR 10)
2
+
3
+ (fn double [x]
4
+ (* x 2))
5
+
6
+ (fn scaled [x]
7
+ `(* ,x ,(double FACTOR)))
8
+
9
+ {: scaled}
@@ -0,0 +1,3 @@
1
+ (import-macros {: scaled} :import-helpers)
2
+
3
+ (print (scaled 3))
@@ -0,0 +1,5 @@
1
+ (import-macros mine :shared-macros)
2
+
3
+ (var n 5)
4
+ (mine.inc-by n 2)
5
+ (print n)
@@ -0,0 +1,6 @@
1
+ (import-macros {: inc-by} :shared-macros)
2
+
3
+ (var counter 0)
4
+ (inc-by counter 5)
5
+ (inc-by counter 3)
6
+ (print counter)
@@ -0,0 +1,18 @@
1
+ (class ParkingSystem)
2
+
3
+ (fn initialize [big medium small]
4
+ (set @big big)
5
+ (set @medium medium)
6
+ (set @small small))
7
+
8
+ (fn add-car [car-type]
9
+ (if (and (= car-type 1) (> @big 0)) (do (set @big (- @big 1)) true)
10
+ (and (= car-type 2) (> @medium 0)) (do (set @medium (- @medium 1)) true)
11
+ (and (= car-type 3) (> @small 0)) (do (set @small (- @small 1)) true)
12
+ false))
13
+
14
+ (let [parking (ParkingSystem.new 1 1 0)]
15
+ (print (parking.add-car 1))
16
+ (print (parking.add-car 2))
17
+ (print (parking.add-car 3))
18
+ (print (parking.add-car 1)))
@@ -0,0 +1,4 @@
1
+ (fn inc-by [target n]
2
+ `(set ,target (+ ,target ,n)))
3
+
4
+ {: inc-by}
@@ -0,0 +1,41 @@
1
+ (fn positive? [n] (> n 0))
2
+ (fn square [n] (* n n))
3
+ (fn add [x y] (+ x y))
4
+ (fn mul [x y] (* x y))
5
+ (fn nonzero [n] (if (= n 0) nil n))
6
+ (fn non-empty [s] (if (= s "") nil s))
7
+ (fn wrap [s] (.. ">>" s "<<"))
8
+ (fn shout [s] (.. s "!"))
9
+
10
+ (fn keep [pred xs]
11
+ (icollect [_ x (ipairs xs)]
12
+ (when (pred x) x)))
13
+
14
+ (fn map [f xs]
15
+ (icollect [_ x (ipairs xs)]
16
+ (f x)))
17
+
18
+ (fn join [sep xs]
19
+ (var s "")
20
+ (each [_ x (ipairs xs)]
21
+ (if (= s "")
22
+ (set s (.. x))
23
+ (set s (.. s sep x))))
24
+ s)
25
+
26
+ (let [scores [-2 3 -1 4 0 5]
27
+ report (->> scores
28
+ (keep positive?)
29
+ (map square)
30
+ (join ", "))
31
+ adjusted (-> 7 (add 3) (mul 2) (square))
32
+ ok (-?> "hello" (non-empty) (wrap) (shout))
33
+ bad (-?> "" (non-empty) (wrap) (shout))
34
+ live (-?>> 5 (nonzero) (mul 3) (add 1))
35
+ dead (-?>> 0 (nonzero) (mul 3) (add 1))]
36
+ (print report)
37
+ (print adjusted)
38
+ (print ok)
39
+ (print bad)
40
+ (print live)
41
+ (print dead))
data/lib/kapusta/ast.rb CHANGED
@@ -117,7 +117,7 @@ module Kapusta
117
117
 
118
118
  class List
119
119
  attr_reader :items
120
- attr_accessor :multiline_source, :line, :column
120
+ attr_accessor :multiline_source, :line, :column, :sigil
121
121
 
122
122
  def initialize(items)
123
123
  @items = items
@@ -1,27 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'macro_gensym'
4
+ require_relative 'macro_lowerer'
5
+ require_relative 'macro_importer'
6
+
3
7
  module Kapusta
4
8
  module Compiler
5
9
  class MacroExpander
6
10
  class Error < Kapusta::Error; end
7
11
 
8
- @gensym_counter = 0
9
-
10
12
  class << self
11
13
  def fresh_gensym(prefix)
12
- @gensym_counter += 1
13
- GeneratedSym.new("#{prefix}_g#{@gensym_counter}", @gensym_counter)
14
+ MacroGensym.fresh_gensym(prefix)
14
15
  end
15
16
 
16
17
  def fresh_local_gensym(prefix)
17
- @gensym_counter += 1
18
- GeneratedSym.new("#{prefix}_local_#{@gensym_counter}", @gensym_counter)
18
+ MacroGensym.fresh_local_gensym(prefix)
19
19
  end
20
20
  end
21
21
 
22
- def initialize(path: nil)
22
+ def initialize(path: nil, loading: nil)
23
23
  @macros = {}
24
24
  @path = path
25
+ @loading = loading || []
25
26
  end
26
27
 
27
28
  def expand_all(forms)
@@ -40,7 +41,8 @@ module Kapusta
40
41
  register_macros_form(form.rest)
41
42
  return []
42
43
  when 'import-macros'
43
- raise macro_error(:import_macros_unsupported, form)
44
+ handle_import_macros(form)
45
+ return []
44
46
  end
45
47
  end
46
48
  [expand(form)]
@@ -90,7 +92,8 @@ module Kapusta
90
92
  register_macros_form(list.rest)
91
93
  return List.new([Sym.new('do')])
92
94
  when 'import-macros'
93
- raise macro_error(:import_macros_unsupported, list)
95
+ handle_import_macros(list)
96
+ return List.new([Sym.new('do')])
94
97
  end
95
98
 
96
99
  key = lookup_key(name)
@@ -136,152 +139,61 @@ module Kapusta
136
139
  value.is_a?(List) && value.head.is_a?(Sym) && %w[fn lambda λ].include?(value.head.name)
137
140
  end
138
141
 
139
- def register(source_name, params, body)
140
- proc_obj = compile_macro(source_name, params, body)
141
- @macros[lookup_key(source_name)] = proc_obj
142
- end
143
-
144
- def compile_macro(name, params, body)
145
- lowering = Lowering.new
146
- lowered_body = body.map { |form| lowering.lower(form) }
147
- gensym_locals = lowering.collected_gensyms
148
-
149
- wrapped =
150
- if gensym_locals.empty?
151
- List.new([Sym.new('fn'), params, *lowered_body])
152
- else
153
- let_bindings = gensym_locals.flat_map do |gensym_sym, prefix|
154
- [gensym_sym, List.new([Sym.new('quasi-gensym'), prefix])]
155
- end
156
- inner = lowered_body.length == 1 ? lowered_body.first : List.new([Sym.new('do'), *lowered_body])
157
- List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
158
- end
159
-
160
- macro_path = @path || "(macro #{name})"
161
- ruby = Compiler.compile_forms([wrapped], path: macro_path)
162
- TOPLEVEL_BINDING.eval(ruby, macro_path, 1)
163
- end
164
-
165
- def invoke_macro(key, args)
166
- proc_obj = @macros[key]
167
- proc_obj.call(*args)
168
- end
169
-
170
- class Lowering
171
- def initialize
172
- @gensyms = {}
142
+ def handle_import_macros(form)
143
+ args = form.rest
144
+ destructure = args[0]
145
+ module_arg = args[1]
146
+ unless destructure.is_a?(HashLit) || destructure.is_a?(Sym)
147
+ raise macro_error(:import_macros_destructure_invalid, form)
173
148
  end
174
-
175
- def collected_gensyms
176
- @gensyms.map { |prefix, sym| [sym, prefix] }
149
+ unless module_arg.is_a?(Symbol) || module_arg.is_a?(String)
150
+ raise macro_error(:import_macros_module_invalid, form)
177
151
  end
178
152
 
179
- def lower(form)
180
- case form
181
- when Quasiquote then copy_position(lower_quasi(form.form), form)
182
- when Unquote, UnquoteSplice
183
- raise Error, Kapusta::Errors.format(:unquote_outside_quasiquote)
184
- when AutoGensym
185
- raise Error, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
186
- when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
187
- when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
188
- when HashLit
189
- copy_position(
190
- HashLit.new(form.entries.map do |entry|
191
- entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
192
- end),
193
- form
194
- )
195
- else
196
- form
197
- end
198
- end
199
-
200
- def copy_position(target, source)
201
- return target unless target.respond_to?(:line=) && source.respond_to?(:line)
202
-
203
- target.line ||= source.line
204
- target.column ||= source.column
205
- target
206
- end
207
-
208
- def lower_quasi(form)
209
- case form
210
- when AutoGensym then gensym_local_for(form.name)
211
- when Sym then List.new([Sym.new('quasi-sym'), form.name])
212
- when List then lower_quasi_list(form)
213
- when Vec then lower_quasi_vec(form)
214
- when HashLit then lower_quasi_hash(form)
215
- when Unquote then lower(form.form)
216
- when UnquoteSplice
217
- raise Error, Kapusta::Errors.format(:unquote_splice_outside_list)
218
- when Quasiquote
219
- raise Error, Kapusta::Errors.format(:nested_quasiquote)
220
- else
221
- form
222
- end
223
- end
224
-
225
- def lower_quasi_list(list)
226
- items = list.items
227
- return List.new([Sym.new('quasi-list')]) if items.empty?
228
-
229
- if (tail_expr = splice_tail(items))
230
- head_items = items[0...-1].map { |item| lower_quasi(item) }
231
- return List.new([Sym.new('quasi-list-tail'), Vec.new(head_items), tail_expr])
232
- end
233
-
234
- lowered_items = items.map { |item| lower_quasi_item(item) }
235
- List.new([Sym.new('quasi-list'), *lowered_items])
153
+ module_label = MacroImporter.module_label(module_arg)
154
+ exports = macro_importer.load(module_arg, form)
155
+ if destructure.is_a?(HashLit)
156
+ register_imported_macros(destructure, exports, module_label, form)
157
+ else
158
+ register_whole_module(destructure, exports)
236
159
  end
160
+ end
237
161
 
238
- def lower_quasi_vec(vec)
239
- items = vec.items
240
- if (tail_expr = splice_tail(items))
241
- head_items = items[0...-1].map { |item| lower_quasi(item) }
242
- return List.new([Sym.new('quasi-vec-tail'), Vec.new(head_items), tail_expr])
243
- end
244
-
245
- lowered_items = items.map { |item| lower_quasi_item(item) }
246
- List.new([Sym.new('quasi-vec'), *lowered_items])
247
- end
162
+ def macro_importer
163
+ @macro_importer ||= MacroImporter.new(path: @path, loading: @loading, error_class: Error)
164
+ end
248
165
 
249
- def lower_quasi_hash(hash)
250
- parts = []
251
- hash.entries.each do |entry|
252
- next unless entry.is_a?(Array)
166
+ def register_imported_macros(destructure, exports, module_label, form)
167
+ destructure.pairs.each do |key, target|
168
+ raise macro_error(:import_macros_destructure_invalid, form) unless target.is_a?(Sym)
253
169
 
254
- key, value = entry
255
- parts << lower_quasi(key) << lower_quasi(value)
256
- end
257
- List.new([Sym.new('quasi-hash'), *parts])
170
+ proc_obj = exports[key] ||
171
+ raise(macro_error(:import_macros_macro_not_found, form,
172
+ macro: key.to_s.tr('_', '-'), module: module_label))
173
+ @macros[lookup_key(target.name)] = proc_obj
258
174
  end
175
+ end
259
176
 
260
- def lower_quasi_item(item)
261
- if item.is_a?(Unquote) && unpack_call?(item.form)
262
- inner = lower(item.form.items[1])
263
- List.new([Sym.new('.'), inner, 0])
264
- else
265
- lower_quasi(item)
266
- end
177
+ def register_whole_module(bind_sym, exports)
178
+ exports.each do |export_key, proc_obj|
179
+ macro_name = "#{bind_sym.name}.#{export_key.to_s.tr('_', '-')}"
180
+ @macros[lookup_key(macro_name)] = proc_obj
267
181
  end
182
+ end
268
183
 
269
- def splice_tail(items)
270
- last = items.last
271
- return unless last
272
- return lower(last.form) if last.is_a?(UnquoteSplice)
273
- return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
274
-
275
- nil
276
- end
184
+ def register(source_name, params, body)
185
+ proc_obj = compile_macro(source_name, params, body)
186
+ @macros[lookup_key(source_name)] = proc_obj
187
+ end
277
188
 
278
- def unpack_call?(form)
279
- form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
280
- end
189
+ def compile_macro(name, params, body)
190
+ macro_path = @path || "(macro #{name})"
191
+ MacroLowerer.compile(params:, body:, path: macro_path, error_class: Error)
192
+ end
281
193
 
282
- def gensym_local_for(prefix)
283
- @gensyms[prefix] ||= MacroExpander.fresh_local_gensym(prefix)
284
- end
194
+ def invoke_macro(key, args)
195
+ proc_obj = @macros[key]
196
+ proc_obj.call(*args)
285
197
  end
286
198
  end
287
199
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ module Compiler
5
+ module MacroGensym
6
+ @counter = 0
7
+
8
+ class << self
9
+ def fresh_gensym(prefix)
10
+ @counter += 1
11
+ GeneratedSym.new("#{prefix}_g#{@counter}", @counter)
12
+ end
13
+
14
+ def fresh_local_gensym(prefix)
15
+ @counter += 1
16
+ GeneratedSym.new("#{prefix}_local_#{@counter}", @counter)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'macro_lowerer'
4
+
5
+ module Kapusta
6
+ module Compiler
7
+ class MacroImporter
8
+ EXTENSIONS = %w[kapm kap fnlm fnl].freeze
9
+
10
+ def self.module_label(module_arg)
11
+ module_arg.is_a?(Symbol) ? module_arg.to_s.tr('_', '-') : module_arg.to_s
12
+ end
13
+
14
+ def initialize(path: nil, loading: nil, error_class: Kapusta::Error)
15
+ @path = path
16
+ @loading = loading || []
17
+ @error_class = error_class
18
+ end
19
+
20
+ def load(module_arg, import_form)
21
+ module_label = self.class.module_label(module_arg)
22
+ absolute_path = resolve_macro_module(module_arg) ||
23
+ raise(import_error(:import_macros_module_not_found, import_form, module: module_label))
24
+
25
+ raise import_error(:import_macros_cycle, import_form, module: module_label) if @loading.include?(absolute_path)
26
+
27
+ load_macro_module(absolute_path, module_label, import_form)
28
+ end
29
+
30
+ private
31
+
32
+ def resolve_macro_module(module_arg)
33
+ snake_stem = module_arg.to_s
34
+ kebab_stem = snake_stem.tr('_', '-')
35
+ [kebab_stem, snake_stem].uniq.each do |stem|
36
+ EXTENSIONS.each do |ext|
37
+ candidate = File.expand_path("#{stem}.#{ext}", base_dir)
38
+ return candidate if File.file?(candidate)
39
+ end
40
+ end
41
+ nil
42
+ end
43
+
44
+ def base_dir
45
+ return Dir.pwd unless @path && !@path.start_with?('(')
46
+
47
+ File.dirname(File.expand_path(@path))
48
+ end
49
+
50
+ def load_macro_module(absolute_path, module_label, import_form)
51
+ @loading.push(absolute_path)
52
+ begin
53
+ source = File.read(absolute_path)
54
+ forms = Reader.read_all(source)
55
+ rescue Kapusta::Error => e
56
+ raise e.with_defaults(path: absolute_path)
57
+ end
58
+ unless forms.last.is_a?(HashLit)
59
+ raise import_error(:import_macros_module_no_exports, import_form, module: module_label)
60
+ end
61
+
62
+ processed = forms.map { |form| MacroLowerer.lower_module_form(form, error_class: @error_class) }
63
+ wrapper = List.new([List.new([Sym.new('fn'), Vec.new([]), *processed])])
64
+ ruby = Compiler.compile_forms([wrapper], path: absolute_path)
65
+ result = TOPLEVEL_BINDING.eval(ruby, absolute_path, 1)
66
+
67
+ return result if result.is_a?(Hash)
68
+
69
+ raise import_error(:import_macros_module_no_exports, import_form, module: module_label)
70
+ ensure
71
+ @loading.pop
72
+ end
73
+
74
+ def import_error(code, form, **args)
75
+ line = form.respond_to?(:line) ? form.line : nil
76
+ column = form.respond_to?(:column) ? form.column : nil
77
+ @error_class.new(Kapusta::Errors.format(code, **args), path: @path, line:, column:)
78
+ end
79
+ end
80
+ end
81
+ end