kapusta 0.8.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: f64991d78295bfc4a633136af86607f2d9536f3e113ae9ea819a0c5708837427
4
- data.tar.gz: 0d656fcf3afefbb2c89acad7a0fa4de8a57f8a484c2ce887e5bc70e7493c71ea
3
+ metadata.gz: 520158590f1244ac17f70b58ded945f66be00060f6da89cebb366fefaf27b62e
4
+ data.tar.gz: 6773e0faa320f23cb15106e710f380a39093c9fc6e56cead8941c28e0ccf16c5
5
5
  SHA512:
6
- metadata.gz: a27e51a26db8069c998d7e0b586beb4fda1505c2f13f572b82bbbda92bc79dbe6e3d4e308547877a0e455cd56b5c9f1c226830ca2645664dae65b47b552dc3a1
7
- data.tar.gz: e6e675b9be4492d7bf75e341915bfd5e022a733240a2b7c4139c2904a99577207a7b4d182208ad1706c31bcded3ebaf3a50e03d18f27eafc68373b96141ae8b0
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
@@ -43,6 +43,7 @@ COMPATIBLE = %w[
43
43
  shapes.kap
44
44
  squares.kap
45
45
  sum.kap
46
+ thread-styles.kap
46
47
  tic-tac-toe.kap
47
48
  underscore-patterns.kap
48
49
  ].freeze
@@ -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,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,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
@@ -104,19 +104,29 @@ module Kapusta
104
104
  end
105
105
 
106
106
  def thread_short(forms, position)
107
- forms[1..].reduce(forms.first) do |memo, form|
108
- temp = thread_temp
109
- List.new([
110
- Sym.new('let'),
111
- Vec.new([temp, memo]),
112
- List.new([
113
- Sym.new('if'),
114
- List.new([Sym.new('='), temp, nil]),
115
- nil,
116
- thread_step(temp, form, position)
117
- ])
118
- ])
107
+ steps = forms[1..]
108
+ return forms.first if steps.empty?
109
+
110
+ prev_temp = thread_temp
111
+ binding_items = [prev_temp, forms.first]
112
+ body = nil
113
+ last_index = steps.length - 1
114
+ steps.each_with_index do |form, i|
115
+ guarded = List.new([
116
+ Sym.new('if'),
117
+ List.new([Sym.new('='), prev_temp, nil]),
118
+ nil,
119
+ thread_step(prev_temp, form, position)
120
+ ])
121
+ if i == last_index
122
+ body = guarded
123
+ else
124
+ temp = thread_temp
125
+ binding_items.push(temp, guarded)
126
+ prev_temp = temp
127
+ end
119
128
  end
129
+ List.new([Sym.new('let'), Vec.new(binding_items), body])
120
130
  end
121
131
 
122
132
  def thread_step(memo, form, position)
@@ -178,7 +178,7 @@ module Kapusta
178
178
 
179
179
  case form
180
180
  when Comment then form.text
181
- when List then render_list(form, indent, top_level:)
181
+ when List then form.sigil ? render_sigil(form) : render_list(form, indent, top_level:)
182
182
  when Vec then render_vec(form, indent, layout:, top_level:, force_expand:)
183
183
  when HashLit then render_hash(form, indent)
184
184
  when Quasiquote then render_prefix('`', form.form, indent, force_expand:)
@@ -189,6 +189,13 @@ module Kapusta
189
189
  end
190
190
  end
191
191
 
192
+ SIGIL_PREFIXES = { ivar: '@', cvar: '@@', gvar: '$' }.freeze
193
+ private_constant :SIGIL_PREFIXES
194
+
195
+ def render_sigil(list)
196
+ "#{SIGIL_PREFIXES.fetch(list.sigil)}#{list.items[1].name}"
197
+ end
198
+
192
199
  def render_prefix(prefix, inner, indent, force_expand: false)
193
200
  rendered = render(inner, indent + prefix.length, force_expand:)
194
201
  lines = rendered.lines(chomp: true)
@@ -221,6 +228,7 @@ module Kapusta
221
228
 
222
229
  "{#{rendered.join(' ')}}"
223
230
  when List
231
+ return render_sigil(form) if form.sigil
224
232
  return if contains_comments?(form.items)
225
233
  return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
226
234
  return if multiline_in_source?(form)
@@ -16,7 +16,7 @@ module Kapusta
16
16
  end
17
17
  end
18
18
 
19
- SKIPPED_HEADS = %w[macros ivar cvar gvar quasi-sym quasi-list
19
+ SKIPPED_HEADS = %w[macros quasi-sym quasi-list
20
20
  quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym].freeze
21
21
 
22
22
  DISPATCHERS = {
@@ -42,7 +42,10 @@ module Kapusta
42
42
  'class' => :walk_module_class,
43
43
  'hashfn' => :walk_hashfn,
44
44
  'macro' => :walk_macro_def,
45
- 'import-macros' => :walk_import_macros
45
+ 'import-macros' => :walk_import_macros,
46
+ 'ivar' => :walk_sigil_form,
47
+ 'cvar' => :walk_sigil_form,
48
+ 'gvar' => :walk_sigil_form
46
49
  }.freeze
47
50
 
48
51
  attr_reader :bindings, :references, :root_scope
@@ -58,7 +61,9 @@ module Kapusta
58
61
  @references = []
59
62
  @scope_seq = 0
60
63
  @root_scope = make_scope(nil, :file)
64
+ @gvar_scope = make_scope(nil, :gvars)
61
65
  @in_module_or_class = 0
66
+ @sigil_scope_stack = [make_sigil_scopes]
62
67
  end
63
68
 
64
69
  def walk_top(forms)
@@ -132,7 +137,7 @@ module Kapusta
132
137
  name_sym, supers, = split_class_args(form.items[1..] || [])
133
138
  supers&.items&.each { |item| walk_form(item, scope) }
134
139
  add_constant_binding(name_sym, scope, :class) if name_sym.is_a?(Sym)
135
- inside_module_or_class do
140
+ inside_class do
136
141
  remaining_forms.each { |item| walk_form(item, scope) }
137
142
  end
138
143
  end
@@ -154,6 +159,21 @@ module Kapusta
154
159
  @in_module_or_class -= 1
155
160
  end
156
161
 
162
+ def inside_class
163
+ inside_module_or_class do
164
+ @sigil_scope_stack.push(make_sigil_scopes)
165
+ begin
166
+ yield
167
+ ensure
168
+ @sigil_scope_stack.pop
169
+ end
170
+ end
171
+ end
172
+
173
+ def make_sigil_scopes
174
+ { ivar: make_scope(nil, :ivars), cvar: make_scope(nil, :cvars) }
175
+ end
176
+
157
177
  def walk_form(form, scope)
158
178
  case form
159
179
  when List then walk_list(form, scope)
@@ -291,6 +311,10 @@ module Kapusta
291
311
  target = list.items[1]
292
312
  value = list.items[2]
293
313
  walk_form(value, scope) if value
314
+ if target.is_a?(List)
315
+ walk_form(target, scope)
316
+ return
317
+ end
294
318
  return unless target.is_a?(Sym) && !target.dotted?
295
319
 
296
320
  existing = scope.lookup(target.name)
@@ -301,6 +325,29 @@ module Kapusta
301
325
  end
302
326
  end
303
327
 
328
+ def walk_sigil_form(list, _scope)
329
+ return if list.items.length < 2
330
+
331
+ inner = list.items[1]
332
+ return unless inner.is_a?(Sym)
333
+
334
+ kind = list.head.name.to_sym
335
+ target_scope = sigil_target_scope(kind)
336
+ existing = target_scope.bindings[inner.name]
337
+ if existing
338
+ add_reference(inner, target_scope, existing)
339
+ else
340
+ add_binding(inner, target_scope, kind)
341
+ end
342
+ end
343
+
344
+ def sigil_target_scope(kind)
345
+ case kind
346
+ when :ivar, :cvar then @sigil_scope_stack.last.fetch(kind)
347
+ when :gvar then @gvar_scope
348
+ end
349
+ end
350
+
304
351
  def walk_fn(list, scope)
305
352
  items = list.items
306
353
  if items[1].is_a?(Vec)
@@ -472,8 +519,11 @@ module Kapusta
472
519
 
473
520
  add_constant_binding(name_sym, scope, kind) if name_sym.is_a?(Sym)
474
521
 
475
- inside_module_or_class do
476
- list.items[body_start..]&.each { |form| walk_form(form, scope) }
522
+ body = list.items[body_start..] || []
523
+ if kind == :class
524
+ inside_class { body.each { |form| walk_form(form, scope) } }
525
+ else
526
+ inside_module_or_class { body.each { |form| walk_form(form, scope) } }
477
527
  end
478
528
  end
479
529
 
@@ -95,6 +95,8 @@ module Kapusta
95
95
  when '#' then read_hashfn
96
96
  when '`' then read_quasiquote
97
97
  when ',' then read_unquote
98
+ when '@' then peek_at(@pos + 1) == '@' ? read_sigil(:cvar, 2) : read_sigil(:ivar, 1)
99
+ when '$' then read_sigil(:gvar, 1)
98
100
  when *CLOSING_DELIMS then raise unexpected_closing_delim(peek)
99
101
  else
100
102
  read_atom
@@ -117,6 +119,32 @@ module Kapusta
117
119
  Quasiquote.new(read_form)
118
120
  end
119
121
 
122
+ def read_sigil(kind, prefix_length)
123
+ return read_atom unless sigil_id_start?(peek_at(@pos + prefix_length))
124
+
125
+ position = source_position
126
+ prefix_length.times { advance }
127
+ name_position = source_position
128
+ name_start = @pos
129
+ advance until delim?(peek) || peek == '.'
130
+ inner = Sym.new(@src[name_start...@pos])
131
+ inner.line = name_position[0]
132
+ inner.column = name_position[1]
133
+ list = List.new([Sym.new(kind.to_s), inner])
134
+ list.sigil = kind
135
+ list.line = position[0]
136
+ list.column = position[1]
137
+ list
138
+ end
139
+
140
+ def peek_at(pos)
141
+ @src[pos]
142
+ end
143
+
144
+ def sigil_id_start?(char)
145
+ !char.nil? && char.match?(/[A-Za-z_]/)
146
+ end
147
+
120
148
  def read_unquote
121
149
  advance
122
150
  if peek == '@'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.0'
5
5
  end
@@ -563,4 +563,17 @@ RSpec.describe 'examples' do
563
563
  it 'macros-import-whole.kap' do
564
564
  expect(run_example('macros-import-whole.kap')).to eq("7\n")
565
565
  end
566
+
567
+ it 'parking-system.kap' do
568
+ expect(run_example('parking-system.kap')).to eq("true\ntrue\nfalse\nfalse\n")
569
+ end
570
+
571
+ it 'hit-counter.kap' do
572
+ expect(run_example('hit-counter.kap')).to eq(<<~OUT)
573
+ 1
574
+ 2
575
+ 3
576
+ "alice"
577
+ OUT
578
+ end
566
579
  end
data/spec/lsp_spec.rb CHANGED
@@ -587,6 +587,92 @@ RSpec.describe Kapusta::LSP do
587
587
  end
588
588
  end
589
589
 
590
+ it 'renames an @ivar across methods within a class' do
591
+ text = "(class C)\n(fn one [] (set @counter 1))\n(fn two [] @counter)\n"
592
+ responses = run(
593
+ frame_initialize,
594
+ frame_did_open('file:///x.kap', text),
595
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'counter'), new_name: 'total')
596
+ )
597
+ changes = result_for(responses)['result']['documentChanges']
598
+
599
+ expect(changes.length).to eq(1)
600
+ edits = changes.first['edits']
601
+ expect(edits.map { |e| e['newText'] }).to eq(%w[total total])
602
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
603
+ .to contain_exactly([1, 17], [2, 12])
604
+ end
605
+
606
+ it 'renames @ivar without touching a same-named fn parameter or value reference' do
607
+ text = "(class C)\n(fn init [val] (set @counter val))\n"
608
+ responses = run(
609
+ frame_initialize,
610
+ frame_did_open('file:///x.kap', text),
611
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'counter'), new_name: 'total')
612
+ )
613
+ changes = result_for(responses)['result']['documentChanges']
614
+
615
+ expect(changes.length).to eq(1)
616
+ edits = changes.first['edits']
617
+ expect(edits.length).to eq(1)
618
+ expect(edits.first['newText']).to eq('total')
619
+ expect(edits.first['range']['start']).to eq('line' => 1, 'character' => 21)
620
+ end
621
+
622
+ it 'renames a $gvar within a file' do
623
+ text = "(set $last 1)\n(print $last)\n"
624
+ responses = run(
625
+ frame_initialize,
626
+ frame_did_open('file:///x.kap', text),
627
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'last'), new_name: 'latest')
628
+ )
629
+ changes = result_for(responses)['result']['documentChanges']
630
+
631
+ expect(changes.length).to eq(1)
632
+ edits = changes.first['edits']
633
+ expect(edits.map { |e| e['newText'] }).to eq(%w[latest latest])
634
+ end
635
+
636
+ it 'keeps @x and @@x in separate sigil namespaces' do
637
+ text = "(class C)\n(set @@flag 1)\n(fn show [] (set @flag 2))\n"
638
+ ivar_position = { line: 2, character: 18 }
639
+ responses = run(
640
+ frame_initialize,
641
+ frame_did_open('file:///x.kap', text),
642
+ frame_rename(uri: 'file:///x.kap', **ivar_position, new_name: 'mark')
643
+ )
644
+ changes = result_for(responses)['result']['documentChanges']
645
+
646
+ expect(changes.length).to eq(1)
647
+ edits = changes.first['edits']
648
+ expect(edits.length).to eq(1)
649
+ expect(edits.first['newText']).to eq('mark')
650
+ expect(edits.first['range']['start']).to eq('line' => 2, 'character' => 18)
651
+ end
652
+
653
+ it 'jumps to the first @@cvar binding from a later use site' do
654
+ text = "(class C)\n(set @@total 0)\n(fn add [] (set @@total (+ @@total 1)))\n"
655
+ use_index = text.rindex('@@total') + 2
656
+ prefix = text[0...use_index]
657
+ last_nl = prefix.rindex("\n")
658
+ pos = { line: prefix.count("\n"), character: last_nl ? use_index - last_nl - 1 : use_index }
659
+
660
+ responses = run(
661
+ frame_initialize,
662
+ frame_did_open('file:///x.kap', text),
663
+ frame_definition(uri: 'file:///x.kap', **pos)
664
+ )
665
+ result = result_for(responses)['result']
666
+
667
+ expect(result).to eq(
668
+ 'uri' => 'file:///x.kap',
669
+ 'range' => {
670
+ 'start' => { 'line' => 1, 'character' => 7 },
671
+ 'end' => { 'line' => 1, 'character' => 12 }
672
+ }
673
+ )
674
+ end
675
+
590
676
  it 'escapes file URIs built during workspace scans' do
591
677
  Dir.mktmpdir do |dir|
592
678
  nested = File.join(dir, 'space dir')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kapusta
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -57,6 +57,7 @@ files:
57
57
  - examples/greet.kap
58
58
  - examples/happy-number.kap
59
59
  - examples/hashfn.kap
60
+ - examples/hit-counter.kap
60
61
  - examples/import-helpers.kapm
61
62
  - examples/kwargs.kap
62
63
  - examples/leap-year.kap
@@ -81,6 +82,7 @@ files:
81
82
  - examples/packet-router.kap
82
83
  - examples/palindrome.kap
83
84
  - examples/pangram.kap
85
+ - examples/parking-system.kap
84
86
  - examples/pcall.kap
85
87
  - examples/pipeline.kap
86
88
  - examples/pivot-index.kap
@@ -103,6 +105,7 @@ files:
103
105
  - examples/stack.kap
104
106
  - examples/subtract-product-sum.kap
105
107
  - examples/sum.kap
108
+ - examples/thread-styles.kap
106
109
  - examples/threading.kap
107
110
  - examples/tic-tac-toe.kap
108
111
  - examples/tset.kap