konpeito 0.1.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.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +75 -0
  4. data/CONTRIBUTING.md +123 -0
  5. data/LICENSE +21 -0
  6. data/README.md +257 -0
  7. data/Rakefile +11 -0
  8. data/bin/konpeito +6 -0
  9. data/konpeito.gemspec +43 -0
  10. data/lib/konpeito/ast/typed_ast.rb +620 -0
  11. data/lib/konpeito/ast/visitor.rb +78 -0
  12. data/lib/konpeito/cache/cache_manager.rb +230 -0
  13. data/lib/konpeito/cache/dependency_graph.rb +192 -0
  14. data/lib/konpeito/cache.rb +8 -0
  15. data/lib/konpeito/cli/base_command.rb +187 -0
  16. data/lib/konpeito/cli/build_command.rb +220 -0
  17. data/lib/konpeito/cli/check_command.rb +104 -0
  18. data/lib/konpeito/cli/config.rb +231 -0
  19. data/lib/konpeito/cli/deps_command.rb +128 -0
  20. data/lib/konpeito/cli/doctor_command.rb +340 -0
  21. data/lib/konpeito/cli/fmt_command.rb +199 -0
  22. data/lib/konpeito/cli/init_command.rb +312 -0
  23. data/lib/konpeito/cli/lsp_command.rb +40 -0
  24. data/lib/konpeito/cli/run_command.rb +150 -0
  25. data/lib/konpeito/cli/test_command.rb +248 -0
  26. data/lib/konpeito/cli/watch_command.rb +212 -0
  27. data/lib/konpeito/cli.rb +301 -0
  28. data/lib/konpeito/codegen/builtin_methods.rb +229 -0
  29. data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
  30. data/lib/konpeito/codegen/debug_info.rb +352 -0
  31. data/lib/konpeito/codegen/inliner.rb +486 -0
  32. data/lib/konpeito/codegen/jvm_backend.rb +197 -0
  33. data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
  34. data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
  35. data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
  36. data/lib/konpeito/codegen/monomorphizer.rb +359 -0
  37. data/lib/konpeito/codegen/profile_runtime.c +341 -0
  38. data/lib/konpeito/codegen/profiler.rb +99 -0
  39. data/lib/konpeito/compiler.rb +592 -0
  40. data/lib/konpeito/dependency_resolver.rb +296 -0
  41. data/lib/konpeito/diagnostics/collector.rb +127 -0
  42. data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
  43. data/lib/konpeito/diagnostics/renderer.rb +144 -0
  44. data/lib/konpeito/formatter/formatter.rb +1214 -0
  45. data/lib/konpeito/hir/builder.rb +7167 -0
  46. data/lib/konpeito/hir/nodes.rb +2465 -0
  47. data/lib/konpeito/lsp/document_manager.rb +820 -0
  48. data/lib/konpeito/lsp/server.rb +183 -0
  49. data/lib/konpeito/lsp/transport.rb +38 -0
  50. data/lib/konpeito/parser/prism_adapter.rb +65 -0
  51. data/lib/konpeito/platform.rb +103 -0
  52. data/lib/konpeito/profile/report.rb +136 -0
  53. data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
  54. data/lib/konpeito/stdlib/compression/compression.rb +72 -0
  55. data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
  56. data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
  57. data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
  58. data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
  59. data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
  60. data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
  61. data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
  62. data/lib/konpeito/stdlib/http/extconf.rb +19 -0
  63. data/lib/konpeito/stdlib/http/http.rb +125 -0
  64. data/lib/konpeito/stdlib/http/http.rbs +57 -0
  65. data/lib/konpeito/stdlib/http/http_native.c +440 -0
  66. data/lib/konpeito/stdlib/json/extconf.rb +17 -0
  67. data/lib/konpeito/stdlib/json/json.rb +44 -0
  68. data/lib/konpeito/stdlib/json/json.rbs +33 -0
  69. data/lib/konpeito/stdlib/json/json_native.c +286 -0
  70. data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
  71. data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
  72. data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
  73. data/lib/konpeito/stdlib/ui/ui.rb +318 -0
  74. data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
  75. data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
  76. data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
  77. data/lib/konpeito/type_checker/inferrer.rb +565 -0
  78. data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
  79. data/lib/konpeito/type_checker/type_resolver.rb +276 -0
  80. data/lib/konpeito/type_checker/types.rb +1434 -0
  81. data/lib/konpeito/type_checker/unification.rb +323 -0
  82. data/lib/konpeito/ui/animation/animated_state.rb +80 -0
  83. data/lib/konpeito/ui/animation/easing.rb +59 -0
  84. data/lib/konpeito/ui/animation/value_tween.rb +66 -0
  85. data/lib/konpeito/ui/app.rb +379 -0
  86. data/lib/konpeito/ui/box.rb +38 -0
  87. data/lib/konpeito/ui/castella.rb +70 -0
  88. data/lib/konpeito/ui/castella_native.rb +76 -0
  89. data/lib/konpeito/ui/chart/area_chart.rb +305 -0
  90. data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
  91. data/lib/konpeito/ui/chart/base_chart.rb +210 -0
  92. data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
  93. data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
  94. data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
  95. data/lib/konpeito/ui/chart/line_chart.rb +289 -0
  96. data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
  97. data/lib/konpeito/ui/chart/scales.rb +77 -0
  98. data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
  99. data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
  100. data/lib/konpeito/ui/column.rb +271 -0
  101. data/lib/konpeito/ui/core.rb +2199 -0
  102. data/lib/konpeito/ui/dsl.rb +443 -0
  103. data/lib/konpeito/ui/frame.rb +171 -0
  104. data/lib/konpeito/ui/frame_native.rb +494 -0
  105. data/lib/konpeito/ui/markdown/ast.rb +124 -0
  106. data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
  107. data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
  108. data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
  109. data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
  110. data/lib/konpeito/ui/markdown/parser.rb +805 -0
  111. data/lib/konpeito/ui/markdown/renderer.rb +639 -0
  112. data/lib/konpeito/ui/markdown/theme.rb +165 -0
  113. data/lib/konpeito/ui/render_node.rb +260 -0
  114. data/lib/konpeito/ui/row.rb +207 -0
  115. data/lib/konpeito/ui/spacer.rb +18 -0
  116. data/lib/konpeito/ui/style.rb +799 -0
  117. data/lib/konpeito/ui/theme.rb +563 -0
  118. data/lib/konpeito/ui/themes/material.rb +35 -0
  119. data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
  120. data/lib/konpeito/ui/widgets/button.rb +103 -0
  121. data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
  122. data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
  123. data/lib/konpeito/ui/widgets/container.rb +91 -0
  124. data/lib/konpeito/ui/widgets/data_table.rb +667 -0
  125. data/lib/konpeito/ui/widgets/divider.rb +29 -0
  126. data/lib/konpeito/ui/widgets/image.rb +105 -0
  127. data/lib/konpeito/ui/widgets/input.rb +485 -0
  128. data/lib/konpeito/ui/widgets/markdown.rb +57 -0
  129. data/lib/konpeito/ui/widgets/modal.rb +163 -0
  130. data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
  131. data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
  132. data/lib/konpeito/ui/widgets/net_image.rb +100 -0
  133. data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
  134. data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
  135. data/lib/konpeito/ui/widgets/slider.rb +133 -0
  136. data/lib/konpeito/ui/widgets/switch.rb +84 -0
  137. data/lib/konpeito/ui/widgets/tabs.rb +157 -0
  138. data/lib/konpeito/ui/widgets/text.rb +110 -0
  139. data/lib/konpeito/ui/widgets/tree.rb +426 -0
  140. data/lib/konpeito/version.rb +5 -0
  141. data/lib/konpeito.rb +109 -0
  142. data/test_native_array.rb +172 -0
  143. data/test_native_array_class.rb +197 -0
  144. data/test_native_class.rb +151 -0
  145. data/tools/konpeito-asm/build.sh +65 -0
  146. data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
  147. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  148. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  149. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  150. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  151. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  152. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  153. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  154. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  155. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  156. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  157. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  158. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  159. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  160. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  161. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  162. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
  163. data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
  164. data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
  165. data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
  166. data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
  167. data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
  168. data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
  169. data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
  170. data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
  171. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
  172. data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
  173. data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
  174. data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
  175. data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
  176. data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
  177. data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
  178. data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
  179. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
  180. metadata +267 -0
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Konpeito
8
+ module Cache
9
+ # Manages compilation cache for incremental builds.
10
+ # Caches AST and type inference results per file.
11
+ class CacheManager
12
+ MANIFEST_FILE = "manifest.json"
13
+ AST_DIR = "ast"
14
+ TYPES_DIR = "types"
15
+
16
+ attr_reader :cache_dir, :dependency_graph
17
+
18
+ def initialize(cache_dir: ".konpeito_cache")
19
+ @cache_dir = File.expand_path(cache_dir)
20
+ @manifest = nil
21
+ @dependency_graph = nil
22
+ @dirty = false
23
+
24
+ ensure_cache_dirs
25
+ load_manifest
26
+ end
27
+
28
+ # Calculate SHA256 hash of file content
29
+ def file_hash(path)
30
+ return nil unless File.exist?(path)
31
+
32
+ Digest::SHA256.file(path).hexdigest
33
+ end
34
+
35
+ # Check if a file needs recompilation
36
+ def needs_recompile?(path)
37
+ path = normalize_path(path)
38
+ current_hash = file_hash(path)
39
+ return true unless current_hash
40
+
41
+ cached_hash = @manifest["files"][path]&.dig("hash")
42
+ current_hash != cached_hash
43
+ end
44
+
45
+ # Get cached AST for a file
46
+ def get_ast(path)
47
+ path = normalize_path(path)
48
+ return nil if needs_recompile?(path)
49
+
50
+ ast_file = ast_cache_path(path)
51
+ return nil unless File.exist?(ast_file)
52
+
53
+ begin
54
+ Marshal.load(File.binread(ast_file))
55
+ rescue StandardError
56
+ nil
57
+ end
58
+ end
59
+
60
+ # Store AST in cache
61
+ def put_ast(path, ast)
62
+ path = normalize_path(path)
63
+ ast_file = ast_cache_path(path)
64
+
65
+ FileUtils.mkdir_p(File.dirname(ast_file))
66
+ File.binwrite(ast_file, Marshal.dump(ast))
67
+
68
+ update_file_entry(path)
69
+ end
70
+
71
+ # Get cached type inference results
72
+ def get_types(path)
73
+ path = normalize_path(path)
74
+ return nil if needs_recompile?(path)
75
+
76
+ types_file = types_cache_path(path)
77
+ return nil unless File.exist?(types_file)
78
+
79
+ begin
80
+ Marshal.load(File.binread(types_file))
81
+ rescue StandardError
82
+ nil
83
+ end
84
+ end
85
+
86
+ # Store type inference results
87
+ def put_types(path, data)
88
+ path = normalize_path(path)
89
+ types_file = types_cache_path(path)
90
+
91
+ FileUtils.mkdir_p(File.dirname(types_file))
92
+ File.binwrite(types_file, Marshal.dump(data))
93
+
94
+ update_file_entry(path)
95
+ end
96
+
97
+ # Invalidate cache for a file and all its dependents
98
+ def invalidate(path)
99
+ path = normalize_path(path)
100
+
101
+ # Get all affected files
102
+ affected = [path] + @dependency_graph.get_all_dependents(path)
103
+
104
+ affected.each do |file|
105
+ invalidate_single(file)
106
+ end
107
+
108
+ @dirty = true
109
+ end
110
+
111
+ # Register a dependency
112
+ def add_dependency(from, to)
113
+ @dependency_graph.add_dependency(from, to)
114
+ @dirty = true
115
+ end
116
+
117
+ # Clear dependencies for a file (called before re-analyzing)
118
+ def clear_dependencies(path)
119
+ @dependency_graph.clear_dependencies(path)
120
+ @dirty = true
121
+ end
122
+
123
+ # Get files that need recompilation given changed files
124
+ def get_recompile_order(changed_paths)
125
+ @dependency_graph.invalidation_order(changed_paths)
126
+ end
127
+
128
+ # Clear all cache
129
+ def clean!
130
+ FileUtils.rm_rf(@cache_dir)
131
+ ensure_cache_dirs
132
+ @manifest = create_empty_manifest
133
+ @dependency_graph = DependencyGraph.new
134
+ @dirty = true
135
+ save_manifest
136
+ end
137
+
138
+ # Save manifest to disk
139
+ def save_manifest
140
+ return unless @dirty
141
+
142
+ @manifest["dependency_graph"] = @dependency_graph.to_h
143
+ @manifest["updated_at"] = Time.now.iso8601
144
+
145
+ manifest_path = File.join(@cache_dir, MANIFEST_FILE)
146
+ File.write(manifest_path, JSON.pretty_generate(@manifest))
147
+ @dirty = false
148
+ end
149
+
150
+ # Get all cached files
151
+ def cached_files
152
+ @manifest["files"].keys
153
+ end
154
+
155
+ # Check if cache exists and is valid
156
+ def cache_exists?
157
+ File.exist?(File.join(@cache_dir, MANIFEST_FILE))
158
+ end
159
+
160
+ private
161
+
162
+ def normalize_path(path)
163
+ File.expand_path(path)
164
+ end
165
+
166
+ def ensure_cache_dirs
167
+ FileUtils.mkdir_p(@cache_dir)
168
+ FileUtils.mkdir_p(File.join(@cache_dir, AST_DIR))
169
+ FileUtils.mkdir_p(File.join(@cache_dir, TYPES_DIR))
170
+ end
171
+
172
+ def load_manifest
173
+ manifest_path = File.join(@cache_dir, MANIFEST_FILE)
174
+
175
+ if File.exist?(manifest_path)
176
+ begin
177
+ data = JSON.parse(File.read(manifest_path))
178
+ @manifest = data
179
+ @dependency_graph = DependencyGraph.from_h(data["dependency_graph"])
180
+ rescue JSON::ParserError
181
+ @manifest = create_empty_manifest
182
+ @dependency_graph = DependencyGraph.new
183
+ end
184
+ else
185
+ @manifest = create_empty_manifest
186
+ @dependency_graph = DependencyGraph.new
187
+ end
188
+ end
189
+
190
+ def create_empty_manifest
191
+ {
192
+ "version" => Konpeito::VERSION,
193
+ "created_at" => Time.now.iso8601,
194
+ "updated_at" => Time.now.iso8601,
195
+ "files" => {},
196
+ "dependency_graph" => {}
197
+ }
198
+ end
199
+
200
+ def update_file_entry(path)
201
+ @manifest["files"][path] = {
202
+ "hash" => file_hash(path),
203
+ "cached_at" => Time.now.iso8601
204
+ }
205
+ @dirty = true
206
+ end
207
+
208
+ def invalidate_single(path)
209
+ @manifest["files"].delete(path)
210
+
211
+ # Remove cached files
212
+ ast_file = ast_cache_path(path)
213
+ types_file = types_cache_path(path)
214
+
215
+ FileUtils.rm_f(ast_file)
216
+ FileUtils.rm_f(types_file)
217
+ end
218
+
219
+ def ast_cache_path(path)
220
+ hash = Digest::SHA256.hexdigest(path)
221
+ File.join(@cache_dir, AST_DIR, "#{hash}.ast")
222
+ end
223
+
224
+ def types_cache_path(path)
225
+ hash = Digest::SHA256.hexdigest(path)
226
+ File.join(@cache_dir, TYPES_DIR, "#{hash}.types")
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Konpeito
4
+ module Cache
5
+ # Tracks file dependencies for incremental compilation.
6
+ # Maintains both forward (file -> dependencies) and reverse (file -> dependents) graphs.
7
+ class DependencyGraph
8
+ def initialize
9
+ @forward = {} # path -> Set of paths this file depends on
10
+ @reverse = {} # path -> Set of paths that depend on this file
11
+ end
12
+
13
+ # Add a dependency: from depends on to
14
+ # Example: main.rb requires utils.rb -> add_dependency("main.rb", "utils.rb")
15
+ def add_dependency(from, to)
16
+ from = normalize_path(from)
17
+ to = normalize_path(to)
18
+
19
+ @forward[from] ||= Set.new
20
+ @forward[from] << to
21
+
22
+ @reverse[to] ||= Set.new
23
+ @reverse[to] << from
24
+ end
25
+
26
+ # Get all files that depend on the given path (direct dependents only)
27
+ def get_direct_dependents(path)
28
+ path = normalize_path(path)
29
+ @reverse[path]&.to_a || []
30
+ end
31
+
32
+ # Get all files that depend on the given path (transitively)
33
+ def get_all_dependents(path)
34
+ path = normalize_path(path)
35
+ result = Set.new
36
+ queue = [path]
37
+
38
+ while queue.any?
39
+ current = queue.shift
40
+ dependents = @reverse[current] || Set.new
41
+
42
+ dependents.each do |dep|
43
+ unless result.include?(dep)
44
+ result << dep
45
+ queue << dep
46
+ end
47
+ end
48
+ end
49
+
50
+ result.to_a
51
+ end
52
+
53
+ # Get all dependencies of the given path (what this file depends on)
54
+ def get_dependencies(path)
55
+ path = normalize_path(path)
56
+ @forward[path]&.to_a || []
57
+ end
58
+
59
+ # Given a set of changed files, return all files that need recompilation
60
+ # in dependency order (dependencies before dependents)
61
+ def invalidation_order(changed_paths)
62
+ changed_paths = changed_paths.map { |p| normalize_path(p) }
63
+
64
+ # Collect all affected files (changed + their dependents)
65
+ affected = Set.new(changed_paths)
66
+ changed_paths.each do |path|
67
+ get_all_dependents(path).each { |dep| affected << dep }
68
+ end
69
+
70
+ # Topological sort: dependencies before dependents
71
+ topological_sort(affected.to_a)
72
+ end
73
+
74
+ # Check if path has any registered dependencies
75
+ def has_dependencies?(path)
76
+ path = normalize_path(path)
77
+ @forward.key?(path) && @forward[path].any?
78
+ end
79
+
80
+ # Remove a file from the graph
81
+ def remove(path)
82
+ path = normalize_path(path)
83
+
84
+ # Remove from forward graph
85
+ if @forward[path]
86
+ @forward[path].each do |dep|
87
+ @reverse[dep]&.delete(path)
88
+ end
89
+ @forward.delete(path)
90
+ end
91
+
92
+ # Remove from reverse graph
93
+ if @reverse[path]
94
+ @reverse[path].each do |dependent|
95
+ @forward[dependent]&.delete(path)
96
+ end
97
+ @reverse.delete(path)
98
+ end
99
+ end
100
+
101
+ # Clear all dependencies for a file (but keep dependents)
102
+ def clear_dependencies(path)
103
+ path = normalize_path(path)
104
+
105
+ if @forward[path]
106
+ @forward[path].each do |dep|
107
+ @reverse[dep]&.delete(path)
108
+ end
109
+ @forward[path] = Set.new
110
+ end
111
+ end
112
+
113
+ # Clear all data
114
+ def clear!
115
+ @forward.clear
116
+ @reverse.clear
117
+ end
118
+
119
+ # Serialize to hash for persistence
120
+ def to_h
121
+ {
122
+ "forward" => @forward.transform_values(&:to_a),
123
+ "reverse" => @reverse.transform_values(&:to_a)
124
+ }
125
+ end
126
+
127
+ # Deserialize from hash
128
+ def self.from_h(data)
129
+ graph = new
130
+ return graph unless data
131
+
132
+ (data["forward"] || {}).each do |path, deps|
133
+ graph.instance_variable_get(:@forward)[path] = Set.new(deps)
134
+ end
135
+ (data["reverse"] || {}).each do |path, deps|
136
+ graph.instance_variable_get(:@reverse)[path] = Set.new(deps)
137
+ end
138
+ graph
139
+ end
140
+
141
+ # Get all known files
142
+ def all_files
143
+ (@forward.keys + @reverse.keys).uniq
144
+ end
145
+
146
+ private
147
+
148
+ def normalize_path(path)
149
+ File.expand_path(path)
150
+ end
151
+
152
+ # Topological sort using Kahn's algorithm
153
+ # Returns files in dependency order: dependencies before dependents
154
+ def topological_sort(files)
155
+ return files if files.empty?
156
+
157
+ # Build in-degree map (only for files in our set)
158
+ # in_degree[file] = number of dependencies of file that are also in our set
159
+ in_degree = {}
160
+ files.each { |f| in_degree[f] = 0 }
161
+
162
+ files.each do |file|
163
+ (@forward[file] || Set.new).each do |dep|
164
+ # file depends on dep, so file has an incoming edge from dep
165
+ # Increment in_degree for file (not dep) so dependencies come first
166
+ in_degree[file] += 1 if in_degree.key?(dep)
167
+ end
168
+ end
169
+
170
+ # Start with files that have no dependencies (in-degree 0)
171
+ queue = files.select { |f| in_degree[f] == 0 }
172
+ result = []
173
+
174
+ while queue.any?
175
+ current = queue.shift
176
+ result << current
177
+
178
+ (@reverse[current] || Set.new).each do |dependent|
179
+ next unless in_degree.key?(dependent)
180
+
181
+ in_degree[dependent] -= 1
182
+ queue << dependent if in_degree[dependent] == 0
183
+ end
184
+ end
185
+
186
+ # If there are remaining files, there's a cycle - add them anyway
187
+ remaining = files - result
188
+ result + remaining
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Konpeito
4
+ module Cache
5
+ autoload :CacheManager, "konpeito/cache/cache_manager"
6
+ autoload :DependencyGraph, "konpeito/cache/dependency_graph"
7
+ end
8
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Konpeito
6
+ module Commands
7
+ # Base class for all CLI commands
8
+ class BaseCommand
9
+ attr_reader :args, :options, :config
10
+
11
+ def initialize(args, config: nil)
12
+ @args = args.dup
13
+ @config = config || Config.new
14
+ @options = default_options
15
+ end
16
+
17
+ # Override in subclasses
18
+ def run
19
+ raise NotImplementedError, "Subclasses must implement #run"
20
+ end
21
+
22
+ # Override in subclasses to provide command name
23
+ def self.command_name
24
+ raise NotImplementedError, "Subclasses must implement .command_name"
25
+ end
26
+
27
+ # Override in subclasses to provide command description
28
+ def self.description
29
+ raise NotImplementedError, "Subclasses must implement .description"
30
+ end
31
+
32
+ protected
33
+
34
+ # Override in subclasses
35
+ def default_options
36
+ {
37
+ verbose: false,
38
+ color: $stderr.tty?
39
+ }
40
+ end
41
+
42
+ # Override in subclasses
43
+ def setup_option_parser(opts)
44
+ opts.on("-v", "--verbose", "Verbose output") do
45
+ options[:verbose] = true
46
+ end
47
+
48
+ opts.on("--no-color", "Disable colored output") do
49
+ options[:color] = false
50
+ end
51
+
52
+ opts.on("-h", "--help", "Show this help") do
53
+ puts opts
54
+ exit
55
+ end
56
+ end
57
+
58
+ def parse_options!
59
+ parser = OptionParser.new do |opts|
60
+ opts.banner = banner
61
+ opts.separator ""
62
+ opts.separator "Options:"
63
+ setup_option_parser(opts)
64
+ end
65
+
66
+ parser.parse!(args)
67
+ end
68
+
69
+ def banner
70
+ "Usage: konpeito #{self.class.command_name} [options]"
71
+ end
72
+
73
+ def puts_verbose(message)
74
+ puts message if options[:verbose]
75
+ end
76
+
77
+ # Cargo-style structured output: right-aligned tag + message
78
+ # Output goes to $stderr so stdout remains clean for program output
79
+ def emit(tag, message)
80
+ if options[:color]
81
+ $stderr.puts " \e[1;32m%12s\e[0m %s" % [tag, message]
82
+ else
83
+ $stderr.puts " %12s %s" % [tag, message]
84
+ end
85
+ end
86
+
87
+ # Emit a warning line
88
+ def emit_warn(tag, message)
89
+ if options[:color]
90
+ $stderr.puts " \e[1;33m%12s\e[0m %s" % [tag, message]
91
+ else
92
+ $stderr.puts " %12s %s" % [tag, message]
93
+ end
94
+ end
95
+
96
+ # Emit an error line
97
+ def emit_error(tag, message)
98
+ if options[:color]
99
+ $stderr.puts " \e[1;31m%12s\e[0m %s" % [tag, message]
100
+ else
101
+ $stderr.puts " %12s %s" % [tag, message]
102
+ end
103
+ end
104
+
105
+ # Format a file size for display
106
+ def format_size(bytes)
107
+ if bytes >= 1024 * 1024
108
+ "%.1f MB" % (bytes / (1024.0 * 1024))
109
+ elsif bytes >= 1024
110
+ "%d KB" % (bytes / 1024)
111
+ else
112
+ "%d B" % bytes
113
+ end
114
+ end
115
+
116
+ def error(message)
117
+ $stderr.puts "Error: #{message}"
118
+ exit 1
119
+ end
120
+
121
+ def display_diagnostics(diagnostics)
122
+ return if diagnostics.empty?
123
+
124
+ renderer = Diagnostics::DiagnosticRenderer.new(
125
+ color: options[:color],
126
+ io: $stderr
127
+ )
128
+ renderer.render_all(diagnostics)
129
+ end
130
+
131
+ def display_dependency_error(error)
132
+ renderer = Diagnostics::DiagnosticRenderer.new(
133
+ color: options[:color],
134
+ io: $stderr
135
+ )
136
+
137
+ if error.cycle
138
+ span = if error.from_file
139
+ Diagnostics::SourceSpan.new(
140
+ file_path: error.from_file,
141
+ start_line: 1,
142
+ start_column: 0
143
+ )
144
+ end
145
+ diagnostic = Diagnostics::Diagnostic.circular_dependency(
146
+ cycle: error.cycle,
147
+ span: span
148
+ )
149
+ renderer.render(diagnostic)
150
+ elsif error.missing_file
151
+ span = if error.from_file
152
+ Diagnostics::SourceSpan.new(
153
+ file_path: error.from_file,
154
+ start_line: error.line || 1,
155
+ start_column: 0,
156
+ source: error.from_file && File.exist?(error.from_file) ? File.read(error.from_file) : nil
157
+ )
158
+ end
159
+ diagnostic = Diagnostics::Diagnostic.file_not_found(
160
+ path: error.missing_file,
161
+ span: span
162
+ )
163
+ renderer.render(diagnostic)
164
+ else
165
+ $stderr.puts "Error: #{error.message}"
166
+ end
167
+ end
168
+
169
+ def default_output_name(source_file, format: :cruby_ext, target: :native)
170
+ base = File.basename(source_file, ".rb")
171
+
172
+ if target == :jvm
173
+ "#{base}.jar"
174
+ else
175
+ case format
176
+ when :cruby_ext
177
+ "#{base}#{Platform.shared_lib_extension}"
178
+ when :standalone
179
+ base
180
+ else
181
+ "#{base}#{Platform.shared_lib_extension}"
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end