kapusta 0.1.5 → 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.
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.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