kapusta 0.2.4 → 0.3.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.
@@ -11,174 +11,261 @@ module Kapusta
11
11
  return ['', env] if pattern.name == '_'
12
12
 
13
13
  ruby_name = define_local(env, pattern)
14
- ["#{ruby_name} = #{value_code}", env]
14
+ return ["#{ruby_name} = #{value_code}", env]
15
+ end
16
+
17
+ native = try_emit_native_pattern_bind(pattern, value_code, env)
18
+ return native if native
19
+
20
+ emit_error!("destructure pattern this compiler cannot translate: #{pattern.inspect}")
21
+ end
22
+
23
+ def try_emit_native_pattern_bind(pattern, value_code, env)
24
+ case pattern
25
+ when Vec
26
+ try_emit_native_vec_bind(pattern, value_code, env)
27
+ when HashLit
28
+ try_emit_native_hash_bind(pattern, value_code, env)
29
+ end
30
+ rescue PatternNotTranslatable
31
+ nil
32
+ end
33
+
34
+ def try_emit_native_vec_bind(pattern, value_code, env)
35
+ parts = []
36
+ deferred = []
37
+ current_env = env
38
+ items = pattern.items
39
+ i = 0
40
+ while i < items.length
41
+ if items[i].is_a?(Sym) && items[i].name == '&'
42
+ sub = items[i + 1]
43
+ raise PatternNotTranslatable unless sub.is_a?(Sym)
44
+
45
+ parts << native_rest_target(sub, current_env)
46
+ i += 2
47
+ else
48
+ code, current_env, follow_up = native_destructure_target(items[i], current_env, allow_follow_up: true)
49
+ parts << code
50
+ deferred << follow_up if follow_up
51
+ i += 1
52
+ end
53
+ end
54
+ if deferred.empty?
55
+ ["#{parts.join(', ')} = #{value_code}", current_env]
15
56
  else
16
- bindings_var = temp('bindings')
17
- current_env = env
18
- lines = [
19
- "#{bindings_var} = #{runtime_call(:destructure, emit_pattern(pattern), value_code)}"
20
- ]
21
- pattern_names(pattern).each do |name|
22
- ruby_name = define_local(current_env, name)
23
- lines << "#{ruby_name} = #{bindings_var}.fetch(#{name.inspect})"
57
+ arr_var = simple_expression?(value_code) ? value_code : temp('array')
58
+ lines = []
59
+ lines << "#{arr_var} = #{value_code}" unless arr_var == value_code
60
+ lines << "#{parts.join(', ')} = #{arr_var}"
61
+ deferred.each do |follow_up|
62
+ sub_code, current_env = follow_up.call(current_env)
63
+ lines << sub_code
24
64
  end
25
65
  [lines.join("\n"), current_env]
26
66
  end
27
67
  end
28
68
 
29
- def emit_bindings_from_match(binding_names, bindings_var, env)
30
- current_env = env
69
+ def try_emit_native_hash_bind(pattern, value_code, env)
70
+ pairs = pattern.pairs
71
+ raise PatternNotTranslatable if pairs.empty?
72
+
73
+ temp_var = simple_expression?(value_code) ? value_code : temp('hash')
31
74
  lines = []
32
- binding_names.each do |name|
33
- ruby_name = define_local(current_env, name)
34
- lines << "#{ruby_name} = #{bindings_var}.fetch(#{name.inspect})"
75
+ lines << "#{temp_var} = #{value_code}" unless temp_var == value_code
76
+ current_env = env
77
+ pairs.each do |key, sub|
78
+ access = "#{temp_var}[#{key.inspect}]"
79
+ if sub.is_a?(Sym)
80
+ raise PatternNotTranslatable if sub.name == '_'
81
+
82
+ bind_name = sub.name.start_with?('?') ? sub.name.delete_prefix('?') : sub.name
83
+ ruby_name = define_local(current_env, bind_name)
84
+ lines << "#{ruby_name} = #{access}"
85
+ else
86
+ sub_code, current_env = try_emit_native_pattern_bind(sub, access, current_env) ||
87
+ raise(PatternNotTranslatable)
88
+ lines << sub_code
89
+ end
35
90
  end
36
91
  [lines.join("\n"), current_env]
37
92
  end
38
93
 
39
- def pattern_match_plan(pattern, env, mode:, allow_pins:)
40
- state = { bound_names: {}, binding_names: [] }
94
+ def native_destructure_target(pattern, env, allow_follow_up: false)
95
+ case pattern
96
+ when Sym
97
+ return ['_', env, nil] if pattern.name == '_'
98
+
99
+ bind_name = pattern.name.start_with?('?') ? pattern.name.delete_prefix('?') : pattern.name
100
+ ruby_name = define_local(env, bind_name)
101
+ [ruby_name, env, nil]
102
+ when Vec
103
+ inner = []
104
+ current = env
105
+ deferred = []
106
+ items = pattern.items
107
+ i = 0
108
+ while i < items.length
109
+ if items[i].is_a?(Sym) && items[i].name == '&'
110
+ inner << native_rest_target(items[i + 1], current)
111
+ i += 2
112
+ else
113
+ code, current, follow_up = native_destructure_target(items[i], current)
114
+ inner << code
115
+ deferred << follow_up if follow_up
116
+ i += 1
117
+ end
118
+ end
119
+ raise PatternNotTranslatable unless deferred.empty?
120
+
121
+ ["(#{inner.join(', ')})", current, nil]
122
+ when HashLit
123
+ raise PatternNotTranslatable unless allow_follow_up
124
+
125
+ slot = temp('slot')
126
+ follow_up = lambda do |outer_env|
127
+ try_emit_native_hash_bind(pattern, slot, outer_env) || raise(PatternNotTranslatable)
128
+ end
129
+ [slot, env, follow_up]
130
+ else
131
+ raise PatternNotTranslatable
132
+ end
133
+ end
134
+
135
+ def native_rest_target(sym, env)
136
+ raise PatternNotTranslatable unless sym.is_a?(Sym)
137
+
138
+ return '*' if sym.name == '_'
139
+
140
+ bind_name = sym.name.start_with?('?') ? sym.name.delete_prefix('?') : sym.name
141
+ "*#{define_local(env, bind_name)}"
142
+ end
143
+
144
+ class PatternNotTranslatable < StandardError; end
145
+
146
+ def native_pattern_plan(pattern, env, mode:, allow_pins:)
147
+ state = { bound_names: {}, binding_names: [], guards: [] }
148
+ ruby_pattern = compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
41
149
  {
42
- pattern: emit_match_pattern(pattern, env, mode:, allow_pins:, state:),
150
+ pattern: ruby_pattern,
151
+ guards: state[:guards],
43
152
  bindings: state[:binding_names]
44
153
  }
154
+ rescue PatternNotTranslatable
155
+ nil
45
156
  end
46
157
 
47
- def emit_match_pattern(pattern, env, mode:, allow_pins:, state:)
158
+ def compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
48
159
  case pattern
49
- when Sym
50
- emit_symbol_match_pattern(pattern, env, mode:, state:)
51
- when Vec
52
- emit_sequence_match_pattern(pattern.items, env, mode:, allow_pins:, state:)
53
- when HashLit
54
- pairs = pattern.pairs.map do |key, value|
55
- "[#{key.inspect}, #{emit_match_pattern(value, env, mode:, allow_pins:, state:)}]"
56
- end
57
- "[:hash, [#{pairs.join(', ')}]]"
160
+ when Sym then compile_native_symbol(pattern, env, mode:, state:)
161
+ when Vec then compile_native_sequence(pattern.items, env, mode:, allow_pins:, state:)
162
+ when HashLit then compile_native_hash(pattern, env, mode:, allow_pins:, state:)
58
163
  when List
59
164
  if pin_pattern?(pattern)
60
- emit_pin_match_pattern(pattern, env, mode:, allow_pins:)
165
+ compile_native_pin(pattern, env, mode:, allow_pins:)
61
166
  elsif or_pattern?(pattern)
62
- emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
63
- elsif where_pattern?(pattern)
64
- emit_error!('`where` is only valid as a case/match clause head')
167
+ compile_native_or(pattern, env, mode:, allow_pins:, state:)
65
168
  else
66
- emit_sequence_match_pattern(pattern.items, env, mode:, allow_pins:, state:)
169
+ compile_native_sequence(pattern.items, env, mode:, allow_pins:, state:)
67
170
  end
68
- when nil
69
- '[:lit, nil]'
70
- when Symbol, String, Numeric, true, false
71
- "[:lit, #{pattern.inspect}]"
72
- else
73
- emit_error!("bad pattern: #{pattern.inspect}")
171
+ when nil then 'nil'
172
+ when Symbol, String, Numeric, true, false then pattern.inspect
173
+ else raise PatternNotTranslatable
74
174
  end
75
175
  end
76
176
 
77
- def emit_symbol_match_pattern(pattern, env, mode:, state:)
177
+ def compile_native_symbol(pattern, env, mode:, state:)
78
178
  name = pattern.name
179
+ return '_' if name == '_'
79
180
 
80
- if name == '_'
81
- '[:wild]'
82
- elsif nil_allowing_pattern_name?(name)
181
+ if nil_allowing_pattern_name?(name)
83
182
  bind_name = name.start_with?('?') ? name.delete_prefix('?') : name
84
- emit_named_match_pattern(bind_name, env, mode:, state:, allow_nil: true, prefer_pin: false)
85
- else
86
- emit_named_match_pattern(name, env, mode:, state:, allow_nil: false, prefer_pin: true)
87
- end
88
- end
183
+ raise PatternNotTranslatable if state[:bound_names].key?(bind_name)
89
184
 
90
- def emit_named_match_pattern(name, env, mode:, state:, allow_nil:, prefer_pin:)
91
- binding = prefer_pin && mode == :match ? env.lookup_if_defined(name) : nil
92
- if state[:bound_names].key?(name)
93
- "[:ref, #{name.inspect}]"
94
- elsif binding
95
- "[:pin, #{binding_value_code(binding)}]"
185
+ state[:bound_names][bind_name] = true
186
+ state[:binding_names] << bind_name
187
+ sanitize_local(bind_name)
96
188
  else
97
- state[:bound_names][name] = true
98
- state[:binding_names] << name
99
- "[:bind, #{name.inspect}, #{allow_nil}]"
189
+ binding = mode == :match ? env.lookup_if_defined(name) : nil
190
+ if state[:bound_names].key?(name)
191
+ raise PatternNotTranslatable
192
+ elsif binding
193
+ "^(#{binding_value_code(binding)})"
194
+ else
195
+ state[:bound_names][name] = true
196
+ state[:binding_names] << name
197
+ ruby = sanitize_local(name)
198
+ state[:guards] << "!#{ruby}.nil?"
199
+ ruby
200
+ end
100
201
  end
101
202
  end
102
203
 
103
- def emit_sequence_match_pattern(items, env, mode:, allow_pins:, state:)
204
+ def compile_native_sequence(items, env, mode:, allow_pins:, state:)
104
205
  parts = []
206
+ has_rest = false
105
207
  i = 0
106
208
  while i < items.length
107
209
  if rest_pattern_marker?(items, i)
108
- parts << "[:rest, #{emit_match_pattern(items[i + 1], env, mode:, allow_pins:, state:)}]"
210
+ has_rest = true
211
+ sub = items[i + 1]
212
+ raise PatternNotTranslatable unless sub.is_a?(Sym)
213
+
214
+ if sub.name == '_'
215
+ parts << '*'
216
+ else
217
+ state[:bound_names][sub.name] = true
218
+ state[:binding_names] << sub.name
219
+ parts << "*#{sanitize_local(sub.name)}"
220
+ end
109
221
  i += 2
110
222
  else
111
- parts << emit_match_pattern(items[i], env, mode:, allow_pins:, state:)
223
+ parts << compile_native_pattern(items[i], env, mode:, allow_pins:, state:)
112
224
  i += 1
113
225
  end
114
226
  end
115
- "[:vec, [#{parts.join(', ')}]]"
227
+ parts << '*' unless has_rest
228
+ "[#{parts.join(', ')}]"
116
229
  end
117
230
 
118
- def emit_pin_match_pattern(pattern, env, mode:, allow_pins:)
119
- emit_error!('pin patterns are only supported inside `case` guards') unless allow_pins && mode == :case
231
+ def compile_native_hash(pattern, env, mode:, allow_pins:, state:)
232
+ pairs = pattern.pairs.map do |key, value|
233
+ raise PatternNotTranslatable unless key.is_a?(Symbol)
234
+
235
+ "#{key}: #{compile_native_pattern(value, env, mode:, allow_pins:, state:)}"
236
+ end
237
+ "{#{pairs.join(', ')}}"
238
+ end
239
+
240
+ def compile_native_pin(pattern, env, mode:, allow_pins:)
241
+ raise PatternNotTranslatable unless allow_pins && mode == :case
120
242
 
121
243
  name_sym = pattern.items[1]
122
- emit_error!("bad pin pattern: #{pattern.inspect}") unless name_sym.is_a?(Sym)
244
+ raise PatternNotTranslatable unless name_sym.is_a?(Sym)
123
245
 
124
246
  binding = env.lookup_if_defined(name_sym.name)
125
- emit_error!("cannot pin undefined name: #{name_sym.name}") unless binding
247
+ raise PatternNotTranslatable unless binding
126
248
 
127
- "[:pin, #{binding_value_code(binding)}]"
249
+ "^(#{binding_value_code(binding)})"
128
250
  end
129
251
 
130
- def emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
131
- initial_names = state[:binding_names].length
252
+ def compile_native_or(pattern, env, mode:, allow_pins:, state:)
132
253
  initial_bound = state[:bound_names].dup
133
- canonical_names = nil
254
+ initial_names = state[:binding_names].length
255
+ initial_guards = state[:guards].length
134
256
  variants = pattern.items[1..].map do |subpattern|
135
257
  alt_state = {
136
258
  bound_names: initial_bound.dup,
137
- binding_names: state[:binding_names].dup
259
+ binding_names: state[:binding_names].dup,
260
+ guards: state[:guards].dup
138
261
  }
139
- compiled = emit_match_pattern(subpattern, env, mode:, allow_pins:, state: alt_state)
140
- alt_names = alt_state[:binding_names][initial_names..]
141
- canonical_names ||= alt_names
142
- emit_error!('all `or` patterns must bind the same names') if canonical_names.sort != alt_names.sort
262
+ compiled = compile_native_pattern(subpattern, env, mode:, allow_pins:, state: alt_state)
263
+ raise PatternNotTranslatable if alt_state[:binding_names].length > initial_names
264
+ raise PatternNotTranslatable if alt_state[:guards].length > initial_guards
143
265
 
144
266
  compiled
145
267
  end
146
-
147
- canonical_names.each do |name|
148
- state[:bound_names][name] = true
149
- state[:binding_names] << name
150
- end
151
- "[:or, [#{variants.join(', ')}]]"
152
- end
153
-
154
- def emit_pattern(pattern)
155
- case pattern
156
- when Sym
157
- pattern.name == '_' ? '[:sym, "_"]' : "[:sym, #{pattern.name.inspect}]"
158
- when Vec
159
- parts = []
160
- items = pattern.items
161
- i = 0
162
- while i < items.length
163
- if items[i].is_a?(Sym) && items[i].name == '&'
164
- parts << "[:rest, #{emit_pattern(items[i + 1])}]"
165
- i += 2
166
- else
167
- parts << emit_pattern(items[i])
168
- i += 1
169
- end
170
- end
171
- "[:vec, [#{parts.join(', ')}]]"
172
- when HashLit
173
- pairs = pattern.pairs.map { |key, value| "[#{key.inspect}, #{emit_pattern(value)}]" }
174
- "[:hash, [#{pairs.join(', ')}]]"
175
- when nil
176
- '[:nil]'
177
- when Symbol, String, Numeric, true, false
178
- "[:lit, #{pattern.inspect}]"
179
- else
180
- emit_error!("bad pattern: #{pattern.inspect}")
181
- end
268
+ "(#{variants.join(' | ')})"
182
269
  end
183
270
 
184
271
  def pattern_names(pattern)
@@ -62,7 +62,7 @@ module Kapusta
62
62
  else
63
63
  name_sym, supers, = split_class_args(form.items[1..])
64
64
  body = emit_forms_with_headers(remaining_forms, env, :class, result: false)
65
- emit_direct_class_header(name_sym, supers, body) || emit_class_wrapper(name_sym, supers, env, body)
65
+ emit_direct_class_header(name_sym, supers, body, env) || emit_class_wrapper(name_sym, supers, env, body)
66
66
  end
67
67
  end
68
68
 
@@ -128,7 +128,7 @@ module Kapusta
128
128
  return [emit_for_statement(form.rest, env, current_scope), env]
129
129
  when 'each'
130
130
  code = emit_each_statement(form.rest, env, current_scope)
131
- return ["#{code}\nnil", env] if result_needed
131
+ return ["#{code}\nnil", env] if result_needed && current_scope != :toplevel
132
132
 
133
133
  return [code, env]
134
134
  end
@@ -219,19 +219,6 @@ module Kapusta
219
219
  source_name.is_a?(GeneratedSym)
220
220
  end
221
221
 
222
- def runtime_helper(name)
223
- helper = name.to_sym
224
- @runtime_helpers << helper unless @runtime_helpers.include?(helper)
225
- Runtime.helper_name(helper)
226
- end
227
-
228
- def runtime_call(name, *args)
229
- rendered_args = args.map do |arg|
230
- arg.is_a?(Array) ? "[#{arg.join(', ')}]" : arg || 'nil'
231
- end
232
- "#{runtime_helper(name)}(#{rendered_args.join(', ')})"
233
- end
234
-
235
222
  def method_binding?(binding)
236
223
  binding.is_a?(Env::MethodBinding)
237
224
  end
@@ -27,14 +27,12 @@ module Kapusta
27
27
  def initialize(path:)
28
28
  @path = path
29
29
  @temp_index = 0
30
- @runtime_helpers = []
31
30
  end
32
31
 
33
32
  def emit_file(forms)
34
33
  env = Env.new
35
34
  body = emit_forms_with_headers(forms, env, :toplevel)
36
- helpers = Runtime.helper_source(@runtime_helpers)
37
- [helpers, body].reject(&:empty?).join("\n\n") << "\n"
35
+ "#{body}\n"
38
36
  end
39
37
  end
40
38
  end
@@ -121,12 +121,12 @@ module Kapusta
121
121
  end
122
122
 
123
123
  def thread_temp
124
- gensym('kap_thread')
124
+ gensym('thread')
125
125
  end
126
126
 
127
127
  def doto(forms)
128
128
  value = forms.first
129
- temp = gensym('kap_doto')
129
+ temp = gensym('doto')
130
130
  body = forms[1..].map do |form|
131
131
  if form.is_a?(List)
132
132
  List.new([form.items[0], temp, *form.items[1..]])
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'error'
4
- require_relative 'compiler/runtime'
5
4
  require_relative 'compiler/normalizer'
6
5
  require_relative 'compiler/emitter'
7
6
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.2.4'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -12,10 +12,12 @@ module Kapusta
12
12
  @loaded_kapusta_features = {}
13
13
 
14
14
  def self.eval(source, path: '(eval)', **_opts)
15
+ install!
15
16
  Compiler.run(source, path:)
16
17
  end
17
18
 
18
19
  def self.dofile(path, **_opts)
20
+ install!
19
21
  source = File.read(path)
20
22
  self.eval(source, path:)
21
23
  end
@@ -25,6 +27,7 @@ module Kapusta
25
27
  end
26
28
 
27
29
  def self.require(feature, relative_to: nil)
30
+ install!
28
31
  feature = feature.to_s
29
32
  local_path = resolve_require_path(feature, relative_to:)
30
33
 
@@ -35,12 +38,33 @@ module Kapusta
35
38
  end
36
39
 
37
40
  def self.install!
38
- @install ||= begin
39
- Kernel.require 'rubygems'
40
- true
41
+ return if @installed
42
+
43
+ @installed = true
44
+ Kernel.module_eval do
45
+ alias_method :__kapusta_original_require_relative, :require_relative
46
+ private :__kapusta_original_require_relative
47
+
48
+ def require_relative(path)
49
+ kap_path = Kapusta.send(:resolve_kap_relative, path, caller_locations(1, 1).first)
50
+ return Kapusta.send(:require_kapusta_file, kap_path) if kap_path
51
+
52
+ __kapusta_original_require_relative(path)
53
+ end
41
54
  end
42
55
  end
43
56
 
57
+ def self.resolve_kap_relative(path, location)
58
+ return unless path.is_a?(String) && location
59
+
60
+ base_file = location.absolute_path || location.path
61
+ return unless base_file
62
+
63
+ full = File.expand_path(path, File.dirname(File.expand_path(base_file)))
64
+ candidates = full.end_with?('.kap') ? [full] : ["#{full}.kap"]
65
+ candidates.find { |c| File.file?(c) }
66
+ end
67
+
44
68
  def self.resolve_require_path(feature, relative_to:)
45
69
  return unless local_feature?(feature)
46
70
 
@@ -88,5 +112,5 @@ module Kapusta
88
112
  end
89
113
 
90
114
  private_class_method :resolve_require_path, :local_feature?, :require_base_dir,
91
- :existing_feature_path, :require_kapusta_file
115
+ :existing_feature_path, :require_kapusta_file, :resolve_kap_relative
92
116
  end
@@ -484,6 +484,10 @@ RSpec.describe 'examples' do
484
484
  expect(run_example('majority-element.kap')).to eq("3\n2\n1\n")
485
485
  end
486
486
 
487
+ it 'manhattan-distance.kap' do
488
+ expect(run_example('manhattan-distance.kap')).to eq("14\n")
489
+ end
490
+
487
491
  it 'plus-one.kap' do
488
492
  expect(run_example('plus-one.kap')).to eq(<<~OUT)
489
493
  [1, 2, 4]
@@ -492,4 +496,8 @@ RSpec.describe 'examples' do
492
496
  [1, 0, 0]
493
497
  OUT
494
498
  end
499
+
500
+ it 'subtract-product-sum.kap' do
501
+ expect(run_example('subtract-product-sum.kap')).to eq("15\n21\n0\n")
502
+ end
495
503
  end
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.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -57,6 +57,7 @@ files:
57
57
  - examples/leap-year.kap
58
58
  - examples/length-of-last-word.kap
59
59
  - examples/majority-element.kap
60
+ - examples/manhattan-distance.kap
60
61
  - examples/match.kap
61
62
  - examples/maximum-subarray.kap
62
63
  - examples/min-max.kap
@@ -84,6 +85,7 @@ files:
84
85
  - examples/single-number.kap
85
86
  - examples/squares.kap
86
87
  - examples/stack.kap
88
+ - examples/subtract-product-sum.kap
87
89
  - examples/sum.kap
88
90
  - examples/threading.kap
89
91
  - examples/tic-tac-toe.kap
@@ -112,7 +114,6 @@ files:
112
114
  - lib/kapusta/compiler/emitter/patterns.rb
113
115
  - lib/kapusta/compiler/emitter/support.rb
114
116
  - lib/kapusta/compiler/normalizer.rb
115
- - lib/kapusta/compiler/runtime.rb
116
117
  - lib/kapusta/env.rb
117
118
  - lib/kapusta/error.rb
118
119
  - lib/kapusta/formatter.rb