kapusta 0.1.4 → 0.2.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.
@@ -4,175 +4,21 @@ module Kapusta
4
4
  module Compiler
5
5
  module Runtime
6
6
  HELPER_DEPENDENCIES = {
7
- print_values: %i[stringify],
8
- concat: %i[stringify],
9
- method_path_value: %i[kebab_to_snake],
10
- set_method_path: %i[kebab_to_snake],
11
- get_ivar: %i[kebab_to_snake],
12
- set_ivar: %i[kebab_to_snake],
13
- get_cvar: %i[current_class_scope kebab_to_snake],
14
- set_cvar: %i[current_class_scope kebab_to_snake],
15
- get_gvar: %i[kebab_to_snake],
16
- set_gvar: %i[kebab_to_snake],
17
7
  destructure: %i[destructure_into],
18
8
  match_pattern: %i[match_pattern_into]
19
9
  }.freeze
20
10
 
21
11
  HELPER_SOURCES = {
22
- kebab_to_snake: <<~RUBY.chomp,
23
- def kap_kebab_to_snake(name)
24
- name.tr('-', '_')
25
- end
26
- RUBY
27
- call: <<~'RUBY'.chomp,
28
- def kap_call(callee, positional, kwargs = nil, block = nil)
29
- raise "not callable: #{callee.inspect}" unless callee.respond_to?(:call)
30
-
31
- if block
32
- kwargs ? callee.call(*positional, **kwargs, &block) : callee.call(*positional, &block)
33
- else
34
- kwargs ? callee.call(*positional, **kwargs) : callee.call(*positional)
35
- end
36
- end
37
- RUBY
38
- send_call: <<~RUBY.chomp,
39
- def kap_send_call(receiver, method_name, positional, kwargs = nil, block = nil)
40
- if block
41
- if kwargs
42
- receiver.public_send(method_name, *positional, **kwargs, &block)
43
- else
44
- receiver.public_send(method_name, *positional, &block)
45
- end
46
- elsif kwargs
47
- receiver.public_send(method_name, *positional, **kwargs)
48
- else
49
- receiver.public_send(method_name, *positional)
50
- end
51
- end
52
- RUBY
53
- invoke_self: <<~RUBY.chomp,
54
- def kap_invoke_self(receiver, method_name, positional, kwargs = nil, block = nil)
55
- if block
56
- if kwargs
57
- receiver.send(method_name, *positional, **kwargs, &block)
58
- else
59
- receiver.send(method_name, *positional, &block)
60
- end
61
- else
62
- kwargs ? receiver.send(method_name, *positional, **kwargs) : receiver.send(method_name, *positional)
63
- end
64
- end
65
- RUBY
66
- stringify: <<~'RUBY'.chomp,
67
- def kap_stringify(value)
68
- render = nil
69
- render = lambda do |item|
70
- case item
71
- when nil then 'nil'
72
- when true then 'true'
73
- when false then 'false'
74
- when String, Symbol then item.inspect
75
- when Array
76
- "[#{item.map { |child| render.call(child) }.join(', ')}]"
77
- when Hash
78
- "{#{item.map { |key, child| "#{render.call(key)}=>#{render.call(child)}" }.join(', ')}}"
79
- else
80
- item.inspect
81
- end
82
- end
83
-
84
- case value
85
- when nil then 'nil'
86
- when true then 'true'
87
- when false then 'false'
88
- when Array, Hash then render.call(value)
89
- else value.to_s
90
- end
91
- end
92
- RUBY
93
- print_values: <<~'RUBY'.chomp,
94
- def kap_print_values(*values)
95
- $stdout.puts(values.map { |value| kap_stringify(value) }.join("\t"))
96
- nil
97
- end
98
- RUBY
99
- concat: <<~RUBY.chomp,
100
- def kap_concat(values)
101
- values.map { |value| kap_stringify(value) }.join
102
- end
103
- RUBY
104
- get_path: <<~RUBY.chomp,
105
- def kap_get_path(obj, keys)
106
- keys.reduce(obj) { |acc, key| acc[key] }
107
- end
108
- RUBY
109
12
  qget_path: <<~RUBY.chomp,
110
13
  def kap_qget_path(obj, keys)
111
14
  keys.each do |key|
112
- return nil if obj.nil?
15
+ return if obj.nil?
113
16
 
114
17
  obj = obj[key]
115
18
  end
116
19
  obj
117
20
  end
118
21
  RUBY
119
- set_path: <<~RUBY.chomp,
120
- def kap_set_path(obj, keys, value)
121
- target = obj
122
- keys[0...-1].each { |key| target = target[key] }
123
- target[keys.last] = value
124
- end
125
- RUBY
126
- method_path_value: <<~RUBY.chomp,
127
- def kap_method_path_value(base, segments)
128
- segments.reduce(base) { |obj, segment| obj.public_send(kap_kebab_to_snake(segment).to_sym) }
129
- end
130
- RUBY
131
- set_method_path: <<~'RUBY'.chomp,
132
- def kap_set_method_path(base, segments, value)
133
- target = base
134
- segments[0...-1].each do |segment|
135
- target = target.public_send(kap_kebab_to_snake(segment).to_sym)
136
- end
137
- setter = "#{kap_kebab_to_snake(segments.last)}="
138
- target.public_send(setter.to_sym, value)
139
- end
140
- RUBY
141
- current_class_scope: <<~RUBY.chomp,
142
- def kap_current_class_scope(receiver)
143
- receiver.is_a?(Module) ? receiver : receiver.class
144
- end
145
- RUBY
146
- get_ivar: <<~'RUBY'.chomp,
147
- def kap_get_ivar(receiver, name)
148
- receiver.instance_variable_get("@#{kap_kebab_to_snake(name)}")
149
- end
150
- RUBY
151
- set_ivar: <<~'RUBY'.chomp,
152
- def kap_set_ivar(receiver, name, value)
153
- receiver.instance_variable_set("@#{kap_kebab_to_snake(name)}", value)
154
- end
155
- RUBY
156
- get_cvar: <<~'RUBY'.chomp,
157
- def kap_get_cvar(receiver, name)
158
- kap_current_class_scope(receiver).class_variable_get("@@#{kap_kebab_to_snake(name)}")
159
- end
160
- RUBY
161
- set_cvar: <<~'RUBY'.chomp,
162
- def kap_set_cvar(receiver, name, value)
163
- kap_current_class_scope(receiver).class_variable_set("@@#{kap_kebab_to_snake(name)}", value)
164
- end
165
- RUBY
166
- get_gvar: <<~'RUBY'.chomp,
167
- def kap_get_gvar(name)
168
- Kernel.eval("$#{kap_kebab_to_snake(name)}", binding, __FILE__, __LINE__)
169
- end
170
- RUBY
171
- set_gvar: <<~'RUBY'.chomp,
172
- def kap_set_gvar(name, value)
173
- Kernel.eval("$#{kap_kebab_to_snake(name)} = value", binding, __FILE__, __LINE__)
174
- end
175
- RUBY
176
22
  ensure_module: <<~RUBY.chomp,
177
23
  def kap_ensure_module(holder, path)
178
24
  segments = path.split('.')
@@ -367,10 +213,13 @@ module Kapusta
367
213
 
368
214
  HELPER_SOURCES.each_key do |name|
369
215
  helper_method = :"kap_#{name}"
370
- define_singleton_method(name, instance_method(helper_method))
216
+ body = instance_method(helper_method)
217
+ define_singleton_method(helper_method, body)
218
+ define_singleton_method(name, body)
371
219
  helper_methods << helper_method
372
220
  end
373
221
 
222
+ private_class_method(*helper_methods)
374
223
  send(:private, *helper_methods)
375
224
  end
376
225
  end
data/lib/kapusta/env.rb CHANGED
@@ -2,18 +2,21 @@
2
2
 
3
3
  module Kapusta
4
4
  class Env
5
+ MethodBinding = Struct.new(:ruby_name)
6
+
5
7
  def initialize(parent = nil)
6
8
  @parent = parent
7
9
  @vars = {}
8
10
  end
9
11
 
10
12
  def define(name, value)
11
- @vars[name] = value
13
+ @vars[binding_key(name)] = value
12
14
  end
13
15
 
14
16
  def lookup(name)
15
- if @vars.key?(name)
16
- @vars[name]
17
+ key = binding_key(name)
18
+ if @vars.key?(key)
19
+ @vars[key]
17
20
  elsif @parent
18
21
  @parent.lookup(name)
19
22
  else
@@ -21,21 +24,31 @@ module Kapusta
21
24
  end
22
25
  end
23
26
 
27
+ def lookup_if_defined(name)
28
+ key = binding_key(name)
29
+ if @vars.key?(key)
30
+ @vars[key]
31
+ else
32
+ @parent&.lookup_if_defined(name)
33
+ end
34
+ end
35
+
24
36
  def defined?(name)
25
- @vars.key?(name) || @parent&.defined?(name)
37
+ @vars.key?(binding_key(name)) || @parent&.defined?(name)
26
38
  end
27
39
 
28
40
  def ruby_name_defined?(name)
29
- @vars.value?(name) || @parent&.ruby_name_defined?(name)
41
+ @vars.any? { |_source_name, value| binding_ruby_name(value) == name } || @parent&.ruby_name_defined?(name)
30
42
  end
31
43
 
32
44
  def local_ruby_name_defined?(name)
33
- @vars.value?(name)
45
+ @vars.any? { |_source_name, value| binding_ruby_name(value) == name }
34
46
  end
35
47
 
36
48
  def set_existing!(name, value)
37
- if @vars.key?(name)
38
- @vars[name] = value
49
+ key = binding_key(name)
50
+ if @vars.key?(key)
51
+ @vars[key] = value
39
52
  elsif @parent
40
53
  @parent.set_existing!(name, value)
41
54
  else
@@ -46,5 +59,15 @@ module Kapusta
46
59
  def child
47
60
  Env.new(self)
48
61
  end
62
+
63
+ private
64
+
65
+ def binding_key(name)
66
+ name.respond_to?(:binding_key) ? name.binding_key : name
67
+ end
68
+
69
+ def binding_ruby_name(value)
70
+ value.respond_to?(:ruby_name) ? value.ruby_name : value
71
+ end
49
72
  end
50
73
  end
@@ -177,15 +177,15 @@ module Kapusta
177
177
  when Sym
178
178
  form.name
179
179
  when Vec
180
- return nil if contains_comments?(form.items)
180
+ return if contains_comments?(form.items)
181
181
 
182
182
  "[#{form.items.map { |item| flat_render(item) }.join(' ')}]"
183
183
  when HashLit
184
- return nil if contains_comments?(form.entries)
184
+ return if contains_comments?(form.entries)
185
185
 
186
186
  "{#{form.pairs.map { |key, value| flat_hash_pair(key, value) }.join(' ')}}"
187
187
  when List
188
- return nil if contains_comments?(form.items)
188
+ return if contains_comments?(form.items)
189
189
  return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
190
190
 
191
191
  "(#{form.items.map { |item| flat_render(item) }.join(' ')})"
@@ -548,13 +548,12 @@ module Kapusta
548
548
 
549
549
  def render_hanging_pairwise_vec(vec)
550
550
  pairs = vec.items.each_slice(2).to_a
551
- rendered_pairs = pairs.map do |pair|
552
- left, right = pair
553
- return nil unless pair.length == 2
551
+ return unless pairs.all? { |pair| pair.length == 2 }
554
552
 
553
+ rendered_pairs = pairs.map do |left, right|
555
554
  render_binding_pair(left, right)
556
555
  end
557
- return nil if rendered_pairs.any?(&:nil?)
556
+ return if rendered_pairs.any?(&:nil?)
558
557
 
559
558
  lines = ["[#{rendered_pairs.first}"]
560
559
  continuation = ' ' * '(let ['.length
@@ -609,7 +608,7 @@ module Kapusta
609
608
  def render_pair(left, right, indent)
610
609
  left_rendered = flat_render(left) || render(left, indent)
611
610
  right_rendered = flat_render(right) || render(right, indent)
612
- return nil unless single_line?(left_rendered) && single_line?(right_rendered)
611
+ return unless single_line?(left_rendered) && single_line?(right_rendered)
613
612
 
614
613
  pair = "#{left_rendered} #{right_rendered}"
615
614
  fits?(pair, indent) ? pair : nil
@@ -617,12 +616,12 @@ module Kapusta
617
616
 
618
617
  def render_binding_pair(left, right)
619
618
  left_rendered = flat_render(left)
620
- return nil unless left_rendered
619
+ return unless left_rendered
621
620
 
622
621
  right_rendered = render(right, '(let ['.length + left_rendered.length + 1)
623
622
  first_line, *rest = right_rendered.lines(chomp: true)
624
623
  pair = "#{left_rendered} #{first_line}"
625
- return nil unless pair.length <= MAX_WIDTH
624
+ return unless pair.length <= MAX_WIDTH
626
625
 
627
626
  return pair if rest.empty?
628
627
 
@@ -8,6 +8,7 @@ module Kapusta
8
8
 
9
9
  WHITESPACE = [' ', "\t", "\n", "\r", "\f", "\v", ','].freeze
10
10
  DELIMS = ['(', ')', '[', ']', '{', '}', '"', ';'].freeze
11
+ CLOSING_DELIMS = [')', ']', '}'].freeze
11
12
 
12
13
  def self.read_all(source, preserve_comments: false)
13
14
  new(source, preserve_comments:).read_all
@@ -85,6 +86,7 @@ module Kapusta
85
86
  when '{' then read_hash
86
87
  when '"' then read_string
87
88
  when '#' then read_hashfn
89
+ when *CLOSING_DELIMS then raise unexpected_closing_delim(peek)
88
90
  else
89
91
  read_atom
90
92
  end
@@ -93,11 +95,12 @@ module Kapusta
93
95
  end
94
96
 
95
97
  def read_list
98
+ opening_position = source_position
96
99
  advance
97
100
  items = []
98
101
  loop do
99
102
  skip_ws
100
- raise Error, 'unclosed (' if eof?
103
+ raise unclosed_opening_delim('(', opening_position) if eof?
101
104
  break if peek == ')'
102
105
 
103
106
  items << read_next_item
@@ -107,11 +110,12 @@ module Kapusta
107
110
  end
108
111
 
109
112
  def read_vec
113
+ opening_position = source_position
110
114
  advance
111
115
  items = []
112
116
  loop do
113
117
  skip_ws
114
- raise Error, 'unclosed [' if eof?
118
+ raise unclosed_opening_delim('[', opening_position) if eof?
115
119
  break if peek == ']'
116
120
 
117
121
  items << read_next_item
@@ -121,12 +125,13 @@ module Kapusta
121
125
  end
122
126
 
123
127
  def read_hash
128
+ opening_position = source_position
124
129
  advance
125
130
  entries = []
126
131
  pending = []
127
132
  loop do
128
133
  skip_ws
129
- raise Error, 'unclosed {' if eof?
134
+ raise unclosed_opening_delim('{', opening_position) if eof?
130
135
  break if peek == '}'
131
136
 
132
137
  item = read_next_item
@@ -218,12 +223,31 @@ module Kapusta
218
223
  parse_atom(token)
219
224
  end
220
225
 
226
+ def unexpected_closing_delim(char)
227
+ line, column = source_position
228
+ Error.new("unexpected closing delimiter '#{char}' at line #{line}, column #{column}")
229
+ end
230
+
231
+ def unclosed_opening_delim(char, position)
232
+ line, column = position
233
+ Error.new("unclosed opening delimiter '#{char}' at line #{line}, column #{column}")
234
+ end
235
+
236
+ def source_position
237
+ prefix = @src[0...@pos]
238
+ line = prefix.count("\n") + 1
239
+ last_newline = prefix.rindex("\n")
240
+ column = last_newline ? prefix.length - last_newline : prefix.length + 1
241
+
242
+ [line, column]
243
+ end
244
+
221
245
  def parse_atom(token)
222
246
  return true if token == 'true'
223
247
  return false if token == 'false'
224
- return nil if token == 'nil'
225
- return token.to_i if token.match?(/\A-?\d+\z/)
226
- return token.to_f if token.match?(/\A-?\d+\.\d+\z/)
248
+ return if token == 'nil'
249
+ return Integer(token, 10) if token.match?(/\A-?\d+\z/)
250
+ return Float(token) if token.match?(/\A-?\d+\.\d+\z/)
227
251
 
228
252
  if token.start_with?(':') && token.length > 1
229
253
  Kapusta.kebab_to_snake(token[1..]).to_sym
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.1.4'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -9,6 +9,8 @@ require_relative 'kapusta/env'
9
9
  require_relative 'kapusta/compiler'
10
10
 
11
11
  module Kapusta
12
+ @loaded_kapusta_features = {}
13
+
12
14
  def self.eval(source, path: '(eval)', **_opts)
13
15
  Compiler.run(source, path:)
14
16
  end
@@ -22,10 +24,69 @@ module Kapusta
22
24
  Compiler.compile(source, path:)
23
25
  end
24
26
 
27
+ def self.require(feature, relative_to: nil)
28
+ feature = feature.to_s
29
+ local_path = resolve_require_path(feature, relative_to:)
30
+
31
+ return require_kapusta_file(local_path) if local_path&.end_with?('.kap')
32
+ return Kernel.require(local_path) if local_path
33
+
34
+ Kernel.require(feature)
35
+ end
36
+
25
37
  def self.install!
26
38
  @install ||= begin
27
- require 'rubygems'
39
+ Kernel.require 'rubygems'
28
40
  true
29
41
  end
30
42
  end
43
+
44
+ def self.resolve_require_path(feature, relative_to:)
45
+ return unless local_feature?(feature)
46
+
47
+ path =
48
+ if File.absolute_path?(feature)
49
+ feature
50
+ else
51
+ File.expand_path(feature, require_base_dir(relative_to))
52
+ end
53
+ existing_feature_path(path)
54
+ end
55
+
56
+ def self.local_feature?(feature)
57
+ feature.start_with?('./', '../') || File.absolute_path?(feature)
58
+ end
59
+
60
+ def self.require_base_dir(relative_to)
61
+ return Dir.pwd if relative_to.nil? || relative_to.start_with?('(')
62
+
63
+ File.dirname(File.expand_path(relative_to))
64
+ end
65
+
66
+ def self.existing_feature_path(path)
67
+ candidates =
68
+ if File.extname(path).empty?
69
+ [path, "#{path}.kap", "#{path}.rb"]
70
+ else
71
+ [path]
72
+ end
73
+
74
+ candidates.find { |candidate| File.file?(candidate) }
75
+ end
76
+
77
+ def self.require_kapusta_file(path)
78
+ expanded = File.realpath(path)
79
+ return false if @loaded_kapusta_features[expanded]
80
+
81
+ @loaded_kapusta_features[expanded] = true
82
+ dofile(expanded)
83
+ $LOADED_FEATURES << expanded unless $LOADED_FEATURES.include?(expanded)
84
+ true
85
+ rescue StandardError, ScriptError
86
+ @loaded_kapusta_features.delete(expanded) if expanded
87
+ raise
88
+ end
89
+
90
+ private_class_method :resolve_require_path, :local_feature?, :require_base_dir,
91
+ :existing_feature_path, :require_kapusta_file
31
92
  end
data/spec/cli_spec.rb CHANGED
@@ -39,7 +39,28 @@ RSpec.describe Kapusta::CLI do
39
39
 
40
40
  stdout, stderr, status = Open3.capture3(RbConfig.ruby, output_path)
41
41
 
42
- expected = "1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n16\n17\nFizz\n19\nBuzz\n"
42
+ expected = <<~OUT
43
+ 1
44
+ 2
45
+ "Fizz"
46
+ 4
47
+ "Buzz"
48
+ "Fizz"
49
+ 7
50
+ 8
51
+ "Fizz"
52
+ "Buzz"
53
+ 11
54
+ "Fizz"
55
+ 13
56
+ 14
57
+ "FizzBuzz"
58
+ 16
59
+ 17
60
+ "Fizz"
61
+ 19
62
+ "Buzz"
63
+ OUT
43
64
  expect(status.success?).to eq(true), stderr
44
65
  expect(stdout).to eq(expected)
45
66
  end
@@ -63,7 +84,9 @@ RSpec.describe Kapusta::CLI do
63
84
  described_class.start([path, 'Ada'])
64
85
  end
65
86
 
66
- expect(output).to eq("Hello, Ada!\n")
87
+ expect(output).to eq(<<~OUT)
88
+ "Hello, Ada!"
89
+ OUT
67
90
  end
68
91
 
69
92
  it 'prints the version with -v' do