konpeito 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.
@@ -1,60 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../formatter/formatter"
4
-
5
3
  module Konpeito
6
4
  module Commands
7
- # Fmt command - format Ruby source files with built-in Prism-based formatter
5
+ # Fmt command - format Ruby source files via RuboCop
8
6
  class FmtCommand < BaseCommand
9
7
  def self.command_name
10
8
  "fmt"
11
9
  end
12
10
 
13
11
  def self.description
14
- "Format Ruby source files"
12
+ "Format Ruby source files (via RuboCop)"
15
13
  end
16
14
 
17
15
  def run
18
16
  parse_options!
19
17
 
20
- @files = args.empty? ? find_ruby_files : args
18
+ rubocop_args = build_rubocop_args
19
+ success = system("bundle", "exec", "rubocop", *rubocop_args)
20
+ exit 1 unless success
21
+ end
21
22
 
22
- if @files.empty?
23
- $stderr.puts "No Ruby files found to format."
24
- return
25
- end
23
+ # Visible for testing
24
+ def build_rubocop_args
25
+ rubocop_args = []
26
26
 
27
- emit("Formatting", "#{@files.size} file(s)...") unless options[:quiet]
28
-
29
- changed = 0
30
- unchanged = 0
31
- errored = 0
32
-
33
- @files.each do |file|
34
- result = format_file(file)
35
- case result
36
- when :changed
37
- changed += 1
38
- when :unchanged
39
- unchanged += 1
40
- when :error
41
- errored += 1
42
- end
27
+ if options[:check]
28
+ # Check mode: report violations without modifying files
29
+ else
30
+ # Default: auto-correct
31
+ rubocop_args << "-A"
43
32
  end
44
33
 
45
- unless options[:quiet]
46
- parts = []
47
- parts << "#{changed} changed" if changed > 0
48
- parts << "#{unchanged} unchanged" if unchanged > 0
49
- parts << "#{errored} error(s)" if errored > 0
50
- emit("Finished", parts.join(", "))
51
- end
34
+ rubocop_args << "--format" << "quiet" if options[:quiet]
35
+ rubocop_args << "--no-color" unless options[:color]
52
36
 
53
- if options[:check] && changed > 0
54
- exit 1
37
+ options[:exclude].each do |pattern|
38
+ rubocop_args << "--exclude" << pattern
55
39
  end
56
40
 
57
- exit 1 if errored > 0
41
+ rubocop_args.concat(args) unless args.empty?
42
+
43
+ rubocop_args
58
44
  end
59
45
 
60
46
  protected
@@ -64,7 +50,6 @@ module Konpeito
64
50
  verbose: false,
65
51
  color: $stderr.tty?,
66
52
  check: false,
67
- diff: false,
68
53
  quiet: false,
69
54
  exclude: []
70
55
  }
@@ -75,9 +60,8 @@ module Konpeito
75
60
  options[:check] = true
76
61
  end
77
62
 
78
- opts.on("--diff", "Show what would change (unified diff)") do
79
- options[:diff] = true
80
- options[:check] = true # diff implies check
63
+ opts.on("--diff", "Check formatting without modifying files (alias for --check)") do
64
+ options[:check] = true
81
65
  end
82
66
 
83
67
  opts.on("-q", "--quiet", "Suppress non-error output") do
@@ -99,101 +83,9 @@ module Konpeito
99
83
  konpeito fmt Format all Ruby files
100
84
  konpeito fmt src/main.rb Format specific file
101
85
  konpeito fmt --check Check without modifying
102
- konpeito fmt --diff Show what would change
86
+ konpeito fmt --diff Check without modifying (alias)
103
87
  BANNER
104
88
  end
105
-
106
- private
107
-
108
- def find_ruby_files
109
- default_exclude = ["vendor/", ".bundle/", ".konpeito_cache/", "tools/"]
110
- all_exclude = default_exclude + options[:exclude]
111
-
112
- Dir.glob("**/*.rb").reject do |f|
113
- all_exclude.any? { |pat| f.start_with?(pat) || File.fnmatch?(pat, f) }
114
- end
115
- end
116
-
117
- def format_file(file)
118
- unless File.exist?(file)
119
- $stderr.puts "Warning: #{file} not found, skipping"
120
- return :error
121
- end
122
-
123
- source = File.read(file)
124
- formatter = Formatter::Formatter.new(source, filepath: file)
125
- formatted = formatter.format
126
-
127
- if source == formatted
128
- return :unchanged
129
- end
130
-
131
- if options[:diff]
132
- show_diff(file, source, formatted)
133
- emit("Formatted", file) unless options[:quiet]
134
- return :changed
135
- end
136
-
137
- if options[:check]
138
- emit_warn("Unformatted", file) unless options[:quiet]
139
- return :changed
140
- end
141
-
142
- # Write formatted content
143
- File.write(file, formatted)
144
- emit("Formatted", file) unless options[:quiet]
145
- :changed
146
- rescue => e
147
- $stderr.puts "Error formatting #{file}: #{e.message}"
148
- :error
149
- end
150
-
151
- def show_diff(file, original, formatted)
152
- orig_lines = original.lines
153
- fmt_lines = formatted.lines
154
-
155
- # Simple unified diff
156
- $stdout.puts "--- #{file}"
157
- $stdout.puts "+++ #{file} (formatted)"
158
-
159
- # Find differing regions
160
- max_len = [orig_lines.size, fmt_lines.size].max
161
- i = 0
162
- while i < max_len
163
- if orig_lines[i] != fmt_lines[i]
164
- # Find the end of this diff hunk
165
- hunk_start = i
166
- while i < max_len && orig_lines[i] != fmt_lines[i]
167
- i += 1
168
- end
169
- hunk_end = i
170
-
171
- # Context
172
- ctx_start = [hunk_start - 3, 0].max
173
- ctx_end = [hunk_end + 3, max_len].min
174
-
175
- $stdout.puts "@@ -#{ctx_start + 1},#{hunk_end - ctx_start} +#{ctx_start + 1},#{hunk_end - ctx_start} @@"
176
-
177
- (ctx_start...ctx_end).each do |j|
178
- if j >= hunk_start && j < hunk_end
179
- if j < orig_lines.size && orig_lines[j]
180
- line = orig_lines[j].chomp
181
- $stdout.puts options[:color] ? "\e[31m-#{line}\e[0m" : "-#{line}"
182
- end
183
- if j < fmt_lines.size && fmt_lines[j]
184
- line = fmt_lines[j].chomp
185
- $stdout.puts options[:color] ? "\e[32m+#{line}\e[0m" : "+#{line}"
186
- end
187
- else
188
- line = (orig_lines[j] || fmt_lines[j] || "").chomp
189
- $stdout.puts " #{line}"
190
- end
191
- end
192
- else
193
- i += 1
194
- end
195
- end
196
- end
197
89
  end
198
90
  end
199
91
  end
@@ -48,6 +48,7 @@ module Konpeito
48
48
  classpath: nil,
49
49
  rbs_paths: config.rbs_paths.dup,
50
50
  require_paths: config.require_paths.dup,
51
+ inline_rbs: false,
51
52
  lib: false
52
53
  }
53
54
  end
@@ -69,6 +70,10 @@ module Konpeito
69
70
  options[:require_paths] << path
70
71
  end
71
72
 
73
+ opts.on("--inline", "Use inline RBS annotations (# @rbs, #:) from Ruby source") do
74
+ options[:inline_rbs] = true
75
+ end
76
+
72
77
  super
73
78
  end
74
79
 
@@ -78,6 +83,7 @@ module Konpeito
78
83
 
79
84
  Examples:
80
85
  konpeito run src/main.rb Build and run (native)
86
+ konpeito run --inline src/main.rb Build and run with inline RBS
81
87
  konpeito run --target jvm src/main.rb Build and run (JVM)
82
88
  BANNER
83
89
  end
@@ -104,11 +110,16 @@ module Konpeito
104
110
  def run_native(source_file)
105
111
  require "tmpdir"
106
112
 
107
- output_file = File.join(Dir.tmpdir, "konpeito_run_#{File.basename(source_file, '.rb')}#{Platform.shared_lib_extension}")
113
+ # Use a subdirectory to avoid conflicts, but keep the basename matching
114
+ # the Init_ function name (Ruby requires Init_<basename> to match the filename)
115
+ @run_tmpdir = File.join(Dir.tmpdir, "konpeito_run_#{Process.pid}")
116
+ FileUtils.mkdir_p(@run_tmpdir)
117
+ output_file = File.join(@run_tmpdir, "#{File.basename(source_file, '.rb')}#{Platform.shared_lib_extension}")
108
118
 
109
119
  build_args = ["-o", output_file]
110
120
  build_args << "-v" if options[:verbose]
111
121
  build_args << "--no-color" unless options[:color]
122
+ build_args << "--inline" if options[:inline_rbs]
112
123
  options[:rbs_paths].each { |p| build_args << "--rbs" << p }
113
124
  options[:require_paths].each { |p| build_args << "-I" << p }
114
125
  build_args << source_file
@@ -116,9 +127,11 @@ module Konpeito
116
127
  Commands::BuildCommand.new(build_args, config: config).run
117
128
 
118
129
  emit("Running", output_file)
119
- system("ruby", "-r", output_file, "-e", "")
130
+ # Run without Bundler environment so the compiled extension can load
131
+ # any installed gem (not just those in the current Gemfile)
132
+ run_without_bundler("ruby", "-r", output_file, "-e", "")
120
133
  ensure
121
- FileUtils.rm_f(output_file) if output_file && File.exist?(output_file)
134
+ FileUtils.rm_rf(@run_tmpdir) if @run_tmpdir && Dir.exist?(@run_tmpdir)
122
135
  end
123
136
 
124
137
  def build_classpath
@@ -135,10 +148,23 @@ module Konpeito
135
148
  parts.reject(&:empty?).join(Platform.classpath_separator)
136
149
  end
137
150
 
151
+ # Run a command without Bundler's environment restrictions.
152
+ # When konpeito is invoked via `bundle exec`, child processes inherit
153
+ # BUNDLE_GEMFILE/RUBYOPT which restrict gem access. The compiled extension
154
+ # may require gems not in the current Gemfile.
155
+ def run_without_bundler(*cmd)
156
+ if defined?(Bundler) && Bundler.respond_to?(:with_unbundled_env)
157
+ Bundler.with_unbundled_env { system(*cmd) }
158
+ else
159
+ system(*cmd)
160
+ end
161
+ end
162
+
138
163
  def build_jvm_args(source_file, classpath)
139
164
  build_args = ["--target", "jvm"]
140
165
  build_args << "-v" if options[:verbose]
141
166
  build_args << "--no-color" unless options[:color]
167
+ build_args << "--inline" if options[:inline_rbs]
142
168
  build_args += ["--classpath", classpath] unless classpath.empty?
143
169
  options[:rbs_paths].each { |p| build_args << "--rbs" << p }
144
170
  options[:require_paths].each { |p| build_args << "-I" << p }
data/lib/konpeito/cli.rb CHANGED
@@ -5,7 +5,6 @@ require_relative "cli/config"
5
5
  require_relative "cli/base_command"
6
6
  require_relative "cli/build_command"
7
7
  require_relative "cli/check_command"
8
- require_relative "cli/lsp_command"
9
8
  require_relative "cli/init_command"
10
9
  require_relative "cli/fmt_command"
11
10
  require_relative "cli/test_command"
@@ -13,6 +12,7 @@ require_relative "cli/watch_command"
13
12
  require_relative "cli/run_command"
14
13
  require_relative "cli/deps_command"
15
14
  require_relative "cli/doctor_command"
15
+ require_relative "cli/completion_command"
16
16
 
17
17
  module Konpeito
18
18
  # Main CLI router - dispatches to subcommands
@@ -21,13 +21,13 @@ module Konpeito
21
21
  "build" => Commands::BuildCommand,
22
22
  "run" => Commands::RunCommand,
23
23
  "check" => Commands::CheckCommand,
24
- "lsp" => Commands::LspCommand,
25
24
  "init" => Commands::InitCommand,
26
25
  "fmt" => Commands::FmtCommand,
27
26
  "test" => Commands::TestCommand,
28
27
  "watch" => Commands::WatchCommand,
29
28
  "deps" => Commands::DepsCommand,
30
- "doctor" => Commands::DoctorCommand
29
+ "doctor" => Commands::DoctorCommand,
30
+ "completion" => Commands::CompletionCommand
31
31
  }.freeze
32
32
 
33
33
  attr_reader :args
@@ -62,6 +62,10 @@ module Konpeito
62
62
  else
63
63
  # Unknown command
64
64
  $stderr.puts "Unknown command: #{command_name}"
65
+ suggestion = find_similar_command(command_name)
66
+ $stderr.puts " Did you mean: #{suggestion}?" if suggestion
67
+ $stderr.puts ""
68
+ $stderr.puts "Available commands: #{COMMANDS.keys.join(', ')}"
65
69
  $stderr.puts "Run 'konpeito --help' for usage information."
66
70
  exit 1
67
71
  end
@@ -69,6 +73,43 @@ module Konpeito
69
73
 
70
74
  private
71
75
 
76
+ def find_similar_command(input)
77
+ best = nil
78
+ best_distance = Float::INFINITY
79
+
80
+ COMMANDS.each_key do |name|
81
+ # Prefix match (e.g. "b" -> "build")
82
+ return name if name.start_with?(input) && input.length >= 1
83
+
84
+ d = levenshtein(input, name)
85
+ if d < best_distance && d <= 2
86
+ best_distance = d
87
+ best = name
88
+ end
89
+ end
90
+ best
91
+ end
92
+
93
+ def levenshtein(a, b)
94
+ m = a.length
95
+ n = b.length
96
+ return n if m == 0
97
+ return m if n == 0
98
+
99
+ d = Array.new(m + 1) { |i| i }
100
+ (1..n).each do |j|
101
+ prev = d[0]
102
+ d[0] = j
103
+ (1..m).each do |i|
104
+ cost = a[i - 1] == b[j - 1] ? 0 : 1
105
+ temp = d[i]
106
+ d[i] = [d[i] + 1, d[i - 1] + 1, prev + cost].min
107
+ prev = temp
108
+ end
109
+ end
110
+ d[m]
111
+ end
112
+
72
113
  def run_command(command_name, command_args)
73
114
  command_class = COMMANDS[command_name]
74
115
  config = Commands::Config.new
@@ -105,6 +146,7 @@ module Konpeito
105
146
  puts " konpeito test Run tests"
106
147
  puts " konpeito fmt Format source files"
107
148
  puts " konpeito doctor Check environment"
149
+ puts " konpeito completion zsh Generate shell completions"
108
150
  puts ""
109
151
  puts "Legacy mode (backwards compatible):"
110
152
  puts " konpeito source.rb Same as: konpeito build source.rb"
@@ -171,7 +213,6 @@ module Konpeito
171
213
  require_paths: [],
172
214
  color: $stderr.tty?,
173
215
  debug: false,
174
- lsp: false,
175
216
  profile: false,
176
217
  incremental: false,
177
218
  clean_cache: false
@@ -181,12 +222,6 @@ module Konpeito
181
222
  def run
182
223
  parse_options!
183
224
 
184
- # Start LSP server if requested
185
- if options[:lsp]
186
- Commands::LspCommand.new([], config: Commands::Config.new).run
187
- return
188
- end
189
-
190
225
  if args.empty?
191
226
  puts "Usage: konpeito [options] <source.rb>"
192
227
  puts "Run 'konpeito --help' for more information."
@@ -272,10 +307,6 @@ module Konpeito
272
307
  options[:color] = false
273
308
  end
274
309
 
275
- opts.on("--lsp", "Start Language Server Protocol server") do
276
- options[:lsp] = true
277
- end
278
-
279
310
  opts.on("--incremental", "Enable incremental compilation (cache unchanged files)") do
280
311
  options[:incremental] = true
281
312
  end
@@ -44,6 +44,22 @@ module Konpeito
44
44
 
45
45
  # String hash - exported (useful for Hash key optimization)
46
46
  hash: { c_func: "rb_str_hash", arity: 0, return_type: :Integer, conv: :simple },
47
+
48
+ # String repeat - exported
49
+ :* => { c_func: "rb_str_times", arity: 1, return_type: :String, conv: :simple },
50
+
51
+ # String freeze - exported
52
+ freeze: { c_func: "rb_str_freeze", arity: 0, return_type: :String, conv: :simple },
53
+
54
+ # String replace - exported
55
+ replace: { c_func: "rb_str_replace", arity: 1, return_type: :String, conv: :simple },
56
+
57
+ # String succ/next - exported
58
+ succ: { c_func: "rb_str_succ", arity: 0, return_type: :String, conv: :simple },
59
+ next: { c_func: "rb_str_succ", arity: 0, return_type: :String, conv: :simple },
60
+
61
+ # String inspect - exported
62
+ inspect: { c_func: "rb_str_inspect", arity: 0, return_type: :String, conv: :simple },
47
63
  },
48
64
 
49
65
  Array: {
@@ -65,7 +65,7 @@ module Konpeito
65
65
  unless @debug
66
66
  FileUtils.rm_f(obj_file)
67
67
  end
68
- FileUtils.rm_f(init_c_file)
68
+ # FileUtils.rm_f(init_c_file) # DEBUG: keep for inspection
69
69
  FileUtils.rm_f(init_obj_file)
70
70
  FileUtils.rm_f(profile_c_file) if profile_c_file
71
71
  FileUtils.rm_f(profile_obj_file) if profile_obj_file
@@ -241,11 +241,32 @@ module Konpeito
241
241
  lines << ""
242
242
  end
243
243
 
244
- # Load stdlib dependencies first
244
+ # Load stdlib/gem dependencies first
245
+ # Use rb_funcallv(rb_cObject, ...) instead of rb_require() or rb_funcallv(rb_mKernel, ...)
246
+ # rb_require() bypasses RubyGems gem activation entirely.
247
+ # rb_funcallv(rb_mKernel, ...) calls the Kernel singleton method which also
248
+ # fails to activate gems. rb_funcallv(rb_cObject, ...) calls require as an
249
+ # instance method on Object (which includes Kernel), going through RubyGems'
250
+ # full gem activation path.
245
251
  unless @stdlib_requires.empty?
246
- lines << " /* Load stdlib dependencies */"
252
+ lines << " /* Load dependencies */"
253
+ lines << " {"
254
+ lines << " ID require_id = rb_intern(\"require\");"
247
255
  @stdlib_requires.each do |lib_name|
248
- lines << " rb_require(\"#{lib_name}\");"
256
+ lines << " {"
257
+ lines << " VALUE args[] = { rb_str_new_cstr(\"#{lib_name}\") };"
258
+ lines << " rb_funcallv(rb_cObject, require_id, 1, args);"
259
+ lines << " }"
260
+ end
261
+ lines << " }"
262
+ lines << ""
263
+ end
264
+
265
+ # Load runtime native extensions (require_relative pointing to .bundle/.so)
266
+ unless @runtime_native_extensions.empty?
267
+ lines << " /* Load runtime native extensions */"
268
+ @runtime_native_extensions.each do |ext|
269
+ lines << " rb_require(\"#{ext[:path]}\");"
249
270
  end
250
271
  lines << ""
251
272
  end
@@ -266,7 +287,15 @@ module Konpeito
266
287
 
267
288
  # Use -1 arity for variadic functions
268
289
  arity = llvm_generator.variadic_functions[mangled_name] ? -1 : func.params.size - 1
269
- lines << " rb_define_method(#{module_var}, \"#{method_name}\", #{mangled_name}, #{arity});"
290
+
291
+ if module_def.module_function_methods&.include?(method_name)
292
+ # module_function: available as both public module method AND private instance method
293
+ lines << " rb_define_module_function(#{module_var}, \"#{method_name}\", #{mangled_name}, #{arity});"
294
+ elsif module_def.private_methods&.include?(method_name)
295
+ lines << " rb_define_private_method(#{module_var}, \"#{method_name}\", #{mangled_name}, #{arity});"
296
+ else
297
+ lines << " rb_define_method(#{module_var}, \"#{method_name}\", #{mangled_name}, #{arity});"
298
+ end
270
299
  end
271
300
 
272
301
  # Register singleton methods (def self.method)
@@ -292,6 +321,25 @@ module Konpeito
292
321
  end
293
322
  end
294
323
 
324
+ # Execute top-level include/extend/prepend before class definitions
325
+ # so that constants from included modules are available for superclass resolution
326
+ # (e.g., `include Kumiki; class CounterComponent < Component`)
327
+ unless hir.toplevel_includes.empty?
328
+ lines << " /* Top-level include/extend/prepend */"
329
+ hir.toplevel_includes.each do |kind, module_name|
330
+ module_expr = "rb_const_get(rb_cObject, rb_intern(\"#{module_name}\"))"
331
+ case kind
332
+ when :include
333
+ lines << " rb_include_module(rb_cObject, #{module_expr});"
334
+ when :extend
335
+ lines << " rb_extend_object(rb_cObject, #{module_expr});"
336
+ when :prepend
337
+ lines << " rb_prepend_module(rb_cObject, #{module_expr});"
338
+ end
339
+ end
340
+ lines << ""
341
+ end
342
+
295
343
  # Define NativeClasses with TypedData allocator
296
344
  native_classes.each do |class_name, class_type|
297
345
  lines.concat(generate_native_class_init(class_name, class_type))
@@ -429,6 +477,16 @@ module Konpeito
429
477
  lines << " rb_define_private_method(rb_cObject, \"#{func_def.name}\", #{mangled_name}, #{arity});"
430
478
  end
431
479
 
480
+ # Call top-level entry point if present.
481
+ # rb_vm_top_self() is not exported from libruby in Ruby 4.0; use rb_cObject instead.
482
+ # Top-level calls like module_function/private/public/include on Object are semantically
483
+ # equivalent to the same calls on the main object for our purposes.
484
+ has_main = hir.functions.any? { |f| f.name == "__main__" && !f.owner_class && !f.owner_module }
485
+ if has_main
486
+ lines << " /* Run top-level code */"
487
+ lines << " rn___main__(rb_cObject);"
488
+ end
489
+
432
490
  lines << "}"
433
491
  lines << ""
434
492
 
@@ -939,10 +997,22 @@ module Konpeito
939
997
  end
940
998
 
941
999
  if @rbs_loader
1000
+ stdlib_ui_dir = File.expand_path("../stdlib/ui", __dir__)
1001
+
942
1002
  @rbs_loader.all_ffi_libraries.each do |lib_name|
1003
+ link_name = lib_name.sub(/^lib/, "")
1004
+
1005
+ # On macOS the UI runtime ships as a Ruby .bundle (loadable module),
1006
+ # not as a .dylib. Bundles cannot be used as -l link targets; their
1007
+ # symbols are resolved at runtime via -undefined dynamic_lookup, so
1008
+ # we simply skip the -l flag for them.
1009
+ bundle_path = File.join(stdlib_ui_dir, "#{link_name}.bundle")
1010
+ if File.exist?(bundle_path) && RbConfig::CONFIG["host_os"] =~ /darwin/
1011
+ next
1012
+ end
1013
+
943
1014
  # Convert library name to linker flag
944
1015
  # "libm" -> "-lm", "libfoo" -> "-lfoo", "foo" -> "-lfoo"
945
- link_name = lib_name.sub(/^lib/, "")
946
1016
  flags << "-l#{link_name}"
947
1017
  end
948
1018
  end