kapusta 0.1.5 → 0.2.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -4
  3. data/examples/bank-account.kap +21 -0
  4. data/examples/baseball-game.kap +11 -0
  5. data/examples/best-time-to-buy-sell-stock.kap +12 -0
  6. data/examples/climbing-stairs.kap +13 -0
  7. data/examples/doto-hygiene.kap +5 -0
  8. data/examples/happy-number.kap +20 -0
  9. data/examples/length-of-last-word.kap +7 -0
  10. data/examples/majority-element.kap +11 -0
  11. data/examples/maximum-subarray.kap +12 -0
  12. data/examples/move-zeroes.kap +13 -0
  13. data/examples/plus-one.kap +14 -0
  14. data/examples/reverse-integer.kap +13 -0
  15. data/examples/roman-to-integer.kap +17 -0
  16. data/examples/stack.kap +27 -10
  17. data/examples/two-sum-hash.kap +17 -0
  18. data/examples/use_bank_account.rb +13 -0
  19. data/examples/valid-parentheses-1.kap +19 -0
  20. data/examples/valid-parentheses-2.kap +8 -0
  21. data/examples/zoo-animal-1.kap +5 -0
  22. data/examples/zoo-animal-inheritance-2.kap +8 -0
  23. data/lib/kapusta/ast.rb +33 -3
  24. data/lib/kapusta/compiler/emitter/bindings.rb +35 -25
  25. data/lib/kapusta/compiler/emitter/control_flow.rb +15 -7
  26. data/lib/kapusta/compiler/emitter/expressions.rb +13 -13
  27. data/lib/kapusta/compiler/emitter/interop.rb +91 -40
  28. data/lib/kapusta/compiler/emitter/patterns.rb +14 -11
  29. data/lib/kapusta/compiler/emitter/support.rb +13 -11
  30. data/lib/kapusta/compiler/emitter.rb +1 -5
  31. data/lib/kapusta/compiler/normalizer.rb +41 -17
  32. data/lib/kapusta/compiler/runtime.rb +0 -152
  33. data/lib/kapusta/env.rb +21 -6
  34. data/lib/kapusta/reader.rb +27 -3
  35. data/lib/kapusta/version.rb +1 -1
  36. data/lib/kapusta.rb +62 -1
  37. data/spec/cli_spec.rb +25 -2
  38. data/spec/examples_spec.rb +257 -81
  39. data/spec/reader_spec.rb +26 -0
  40. metadata +23 -8
  41. data/examples/inheritance.kap +0 -13
@@ -4,106 +4,11 @@ module Kapusta
4
4
  module Compiler
5
5
  module Runtime
6
6
  HELPER_DEPENDENCIES = {
7
- stringify: %i[repr],
8
- print_values: %i[stringify],
9
- concat: %i[stringify],
10
- method_path_value: %i[kebab_to_snake],
11
- set_method_path: %i[kebab_to_snake],
12
- get_ivar: %i[kebab_to_snake],
13
- set_ivar: %i[kebab_to_snake],
14
- get_cvar: %i[current_class_scope kebab_to_snake],
15
- set_cvar: %i[current_class_scope kebab_to_snake],
16
- get_gvar: %i[kebab_to_snake],
17
- set_gvar: %i[kebab_to_snake],
18
7
  destructure: %i[destructure_into],
19
8
  match_pattern: %i[match_pattern_into]
20
9
  }.freeze
21
10
 
22
11
  HELPER_SOURCES = {
23
- kebab_to_snake: <<~RUBY.chomp,
24
- def kap_kebab_to_snake(name)
25
- name.tr('-', '_')
26
- end
27
- RUBY
28
- call: <<~'RUBY'.chomp,
29
- def kap_call(callee, positional, kwargs = nil, block = nil)
30
- raise "not callable: #{callee.inspect}" unless callee.respond_to?(:call)
31
-
32
- if block
33
- kwargs ? callee.call(*positional, **kwargs, &block) : callee.call(*positional, &block)
34
- else
35
- kwargs ? callee.call(*positional, **kwargs) : callee.call(*positional)
36
- end
37
- end
38
- RUBY
39
- send_call: <<~RUBY.chomp,
40
- def kap_send_call(receiver, method_name, positional, kwargs = nil, block = nil)
41
- if block
42
- if kwargs
43
- receiver.public_send(method_name, *positional, **kwargs, &block)
44
- else
45
- receiver.public_send(method_name, *positional, &block)
46
- end
47
- elsif kwargs
48
- receiver.public_send(method_name, *positional, **kwargs)
49
- else
50
- receiver.public_send(method_name, *positional)
51
- end
52
- end
53
- RUBY
54
- invoke_self: <<~RUBY.chomp,
55
- def kap_invoke_self(receiver, method_name, positional, kwargs = nil, block = nil)
56
- if block
57
- if kwargs
58
- receiver.send(method_name, *positional, **kwargs, &block)
59
- else
60
- receiver.send(method_name, *positional, &block)
61
- end
62
- else
63
- kwargs ? receiver.send(method_name, *positional, **kwargs) : receiver.send(method_name, *positional)
64
- end
65
- end
66
- RUBY
67
- stringify: <<~RUBY.chomp,
68
- def kap_stringify(value)
69
- case value
70
- when nil then 'nil'
71
- when Array, Hash then kap_repr(value)
72
- else value.to_s
73
- end
74
- end
75
- RUBY
76
- repr: <<~'RUBY'.chomp,
77
- def kap_repr(value)
78
- case value
79
- when nil then 'nil'
80
- when true, false then value.to_s
81
- when String, Symbol then value.inspect
82
- when Array
83
- "[#{value.map { |item| kap_repr(item) }.join(', ')}]"
84
- when Hash
85
- "{#{value.map { |key, item| "#{kap_repr(key)}=>#{kap_repr(item)}" }.join(', ')}}"
86
- else
87
- value.inspect
88
- end
89
- end
90
- RUBY
91
- print_values: <<~'RUBY'.chomp,
92
- def kap_print_values(*values)
93
- $stdout.puts(values.map { |value| kap_stringify(value) }.join("\t"))
94
- nil
95
- end
96
- RUBY
97
- concat: <<~RUBY.chomp,
98
- def kap_concat(values)
99
- values.map { |value| kap_stringify(value) }.join
100
- end
101
- RUBY
102
- get_path: <<~RUBY.chomp,
103
- def kap_get_path(obj, keys)
104
- keys.reduce(obj) { |acc, key| acc[key] }
105
- end
106
- RUBY
107
12
  qget_path: <<~RUBY.chomp,
108
13
  def kap_qget_path(obj, keys)
109
14
  keys.each do |key|
@@ -114,63 +19,6 @@ module Kapusta
114
19
  obj
115
20
  end
116
21
  RUBY
117
- set_path: <<~RUBY.chomp,
118
- def kap_set_path(obj, keys, value)
119
- target = obj
120
- keys[0...-1].each { |key| target = target[key] }
121
- target[keys.last] = value
122
- end
123
- RUBY
124
- method_path_value: <<~RUBY.chomp,
125
- def kap_method_path_value(base, segments)
126
- segments.reduce(base) { |obj, segment| obj.public_send(kap_kebab_to_snake(segment).to_sym) }
127
- end
128
- RUBY
129
- set_method_path: <<~'RUBY'.chomp,
130
- def kap_set_method_path(base, segments, value)
131
- target = base
132
- segments[0...-1].each do |segment|
133
- target = target.public_send(kap_kebab_to_snake(segment).to_sym)
134
- end
135
- setter = "#{kap_kebab_to_snake(segments.last)}="
136
- target.public_send(setter.to_sym, value)
137
- end
138
- RUBY
139
- current_class_scope: <<~RUBY.chomp,
140
- def kap_current_class_scope(receiver)
141
- receiver.is_a?(Module) ? receiver : receiver.class
142
- end
143
- RUBY
144
- get_ivar: <<~'RUBY'.chomp,
145
- def kap_get_ivar(receiver, name)
146
- receiver.instance_variable_get("@#{kap_kebab_to_snake(name)}")
147
- end
148
- RUBY
149
- set_ivar: <<~'RUBY'.chomp,
150
- def kap_set_ivar(receiver, name, value)
151
- receiver.instance_variable_set("@#{kap_kebab_to_snake(name)}", value)
152
- end
153
- RUBY
154
- get_cvar: <<~'RUBY'.chomp,
155
- def kap_get_cvar(receiver, name)
156
- kap_current_class_scope(receiver).class_variable_get("@@#{kap_kebab_to_snake(name)}")
157
- end
158
- RUBY
159
- set_cvar: <<~'RUBY'.chomp,
160
- def kap_set_cvar(receiver, name, value)
161
- kap_current_class_scope(receiver).class_variable_set("@@#{kap_kebab_to_snake(name)}", value)
162
- end
163
- RUBY
164
- get_gvar: <<~'RUBY'.chomp,
165
- def kap_get_gvar(name)
166
- Kernel.eval("$#{kap_kebab_to_snake(name)}", binding, __FILE__, __LINE__)
167
- end
168
- RUBY
169
- set_gvar: <<~'RUBY'.chomp,
170
- def kap_set_gvar(name, value)
171
- Kernel.eval("$#{kap_kebab_to_snake(name)} = value", binding, __FILE__, __LINE__)
172
- end
173
- RUBY
174
22
  ensure_module: <<~RUBY.chomp,
175
23
  def kap_ensure_module(holder, path)
176
24
  segments = path.split('.')
data/lib/kapusta/env.rb CHANGED
@@ -10,12 +10,13 @@ module Kapusta
10
10
  end
11
11
 
12
12
  def define(name, value)
13
- @vars[name] = value
13
+ @vars[binding_key(name)] = value
14
14
  end
15
15
 
16
16
  def lookup(name)
17
- if @vars.key?(name)
18
- @vars[name]
17
+ key = binding_key(name)
18
+ if @vars.key?(key)
19
+ @vars[key]
19
20
  elsif @parent
20
21
  @parent.lookup(name)
21
22
  else
@@ -23,8 +24,17 @@ module Kapusta
23
24
  end
24
25
  end
25
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
+
26
36
  def defined?(name)
27
- @vars.key?(name) || @parent&.defined?(name)
37
+ @vars.key?(binding_key(name)) || @parent&.defined?(name)
28
38
  end
29
39
 
30
40
  def ruby_name_defined?(name)
@@ -36,8 +46,9 @@ module Kapusta
36
46
  end
37
47
 
38
48
  def set_existing!(name, value)
39
- if @vars.key?(name)
40
- @vars[name] = value
49
+ key = binding_key(name)
50
+ if @vars.key?(key)
51
+ @vars[key] = value
41
52
  elsif @parent
42
53
  @parent.set_existing!(name, value)
43
54
  else
@@ -51,6 +62,10 @@ module Kapusta
51
62
 
52
63
  private
53
64
 
65
+ def binding_key(name)
66
+ name.respond_to?(:binding_key) ? name.binding_key : name
67
+ end
68
+
54
69
  def binding_ruby_name(value)
55
70
  value.respond_to?(:ruby_name) ? value.ruby_name : value
56
71
  end
@@ -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,6 +223,25 @@ 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'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.1.5'
4
+ VERSION = '0.2.1'
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