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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +75 -0
- data/CONTRIBUTING.md +123 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/Rakefile +11 -0
- data/bin/konpeito +6 -0
- data/konpeito.gemspec +43 -0
- data/lib/konpeito/ast/typed_ast.rb +620 -0
- data/lib/konpeito/ast/visitor.rb +78 -0
- data/lib/konpeito/cache/cache_manager.rb +230 -0
- data/lib/konpeito/cache/dependency_graph.rb +192 -0
- data/lib/konpeito/cache.rb +8 -0
- data/lib/konpeito/cli/base_command.rb +187 -0
- data/lib/konpeito/cli/build_command.rb +220 -0
- data/lib/konpeito/cli/check_command.rb +104 -0
- data/lib/konpeito/cli/config.rb +231 -0
- data/lib/konpeito/cli/deps_command.rb +128 -0
- data/lib/konpeito/cli/doctor_command.rb +340 -0
- data/lib/konpeito/cli/fmt_command.rb +199 -0
- data/lib/konpeito/cli/init_command.rb +312 -0
- data/lib/konpeito/cli/lsp_command.rb +40 -0
- data/lib/konpeito/cli/run_command.rb +150 -0
- data/lib/konpeito/cli/test_command.rb +248 -0
- data/lib/konpeito/cli/watch_command.rb +212 -0
- data/lib/konpeito/cli.rb +301 -0
- data/lib/konpeito/codegen/builtin_methods.rb +229 -0
- data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
- data/lib/konpeito/codegen/debug_info.rb +352 -0
- data/lib/konpeito/codegen/inliner.rb +486 -0
- data/lib/konpeito/codegen/jvm_backend.rb +197 -0
- data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
- data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
- data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
- data/lib/konpeito/codegen/monomorphizer.rb +359 -0
- data/lib/konpeito/codegen/profile_runtime.c +341 -0
- data/lib/konpeito/codegen/profiler.rb +99 -0
- data/lib/konpeito/compiler.rb +592 -0
- data/lib/konpeito/dependency_resolver.rb +296 -0
- data/lib/konpeito/diagnostics/collector.rb +127 -0
- data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
- data/lib/konpeito/diagnostics/renderer.rb +144 -0
- data/lib/konpeito/formatter/formatter.rb +1214 -0
- data/lib/konpeito/hir/builder.rb +7167 -0
- data/lib/konpeito/hir/nodes.rb +2465 -0
- data/lib/konpeito/lsp/document_manager.rb +820 -0
- data/lib/konpeito/lsp/server.rb +183 -0
- data/lib/konpeito/lsp/transport.rb +38 -0
- data/lib/konpeito/parser/prism_adapter.rb +65 -0
- data/lib/konpeito/platform.rb +103 -0
- data/lib/konpeito/profile/report.rb +136 -0
- data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
- data/lib/konpeito/stdlib/compression/compression.rb +72 -0
- data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
- data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
- data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
- data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
- data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
- data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
- data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
- data/lib/konpeito/stdlib/http/extconf.rb +19 -0
- data/lib/konpeito/stdlib/http/http.rb +125 -0
- data/lib/konpeito/stdlib/http/http.rbs +57 -0
- data/lib/konpeito/stdlib/http/http_native.c +440 -0
- data/lib/konpeito/stdlib/json/extconf.rb +17 -0
- data/lib/konpeito/stdlib/json/json.rb +44 -0
- data/lib/konpeito/stdlib/json/json.rbs +33 -0
- data/lib/konpeito/stdlib/json/json_native.c +286 -0
- data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
- data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
- data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
- data/lib/konpeito/stdlib/ui/ui.rb +318 -0
- data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
- data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
- data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
- data/lib/konpeito/type_checker/inferrer.rb +565 -0
- data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
- data/lib/konpeito/type_checker/type_resolver.rb +276 -0
- data/lib/konpeito/type_checker/types.rb +1434 -0
- data/lib/konpeito/type_checker/unification.rb +323 -0
- data/lib/konpeito/ui/animation/animated_state.rb +80 -0
- data/lib/konpeito/ui/animation/easing.rb +59 -0
- data/lib/konpeito/ui/animation/value_tween.rb +66 -0
- data/lib/konpeito/ui/app.rb +379 -0
- data/lib/konpeito/ui/box.rb +38 -0
- data/lib/konpeito/ui/castella.rb +70 -0
- data/lib/konpeito/ui/castella_native.rb +76 -0
- data/lib/konpeito/ui/chart/area_chart.rb +305 -0
- data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
- data/lib/konpeito/ui/chart/base_chart.rb +210 -0
- data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
- data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
- data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
- data/lib/konpeito/ui/chart/line_chart.rb +289 -0
- data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
- data/lib/konpeito/ui/chart/scales.rb +77 -0
- data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
- data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
- data/lib/konpeito/ui/column.rb +271 -0
- data/lib/konpeito/ui/core.rb +2199 -0
- data/lib/konpeito/ui/dsl.rb +443 -0
- data/lib/konpeito/ui/frame.rb +171 -0
- data/lib/konpeito/ui/frame_native.rb +494 -0
- data/lib/konpeito/ui/markdown/ast.rb +124 -0
- data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
- data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
- data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
- data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
- data/lib/konpeito/ui/markdown/parser.rb +805 -0
- data/lib/konpeito/ui/markdown/renderer.rb +639 -0
- data/lib/konpeito/ui/markdown/theme.rb +165 -0
- data/lib/konpeito/ui/render_node.rb +260 -0
- data/lib/konpeito/ui/row.rb +207 -0
- data/lib/konpeito/ui/spacer.rb +18 -0
- data/lib/konpeito/ui/style.rb +799 -0
- data/lib/konpeito/ui/theme.rb +563 -0
- data/lib/konpeito/ui/themes/material.rb +35 -0
- data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
- data/lib/konpeito/ui/widgets/button.rb +103 -0
- data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
- data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
- data/lib/konpeito/ui/widgets/container.rb +91 -0
- data/lib/konpeito/ui/widgets/data_table.rb +667 -0
- data/lib/konpeito/ui/widgets/divider.rb +29 -0
- data/lib/konpeito/ui/widgets/image.rb +105 -0
- data/lib/konpeito/ui/widgets/input.rb +485 -0
- data/lib/konpeito/ui/widgets/markdown.rb +57 -0
- data/lib/konpeito/ui/widgets/modal.rb +163 -0
- data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
- data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
- data/lib/konpeito/ui/widgets/net_image.rb +100 -0
- data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
- data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
- data/lib/konpeito/ui/widgets/slider.rb +133 -0
- data/lib/konpeito/ui/widgets/switch.rb +84 -0
- data/lib/konpeito/ui/widgets/tabs.rb +157 -0
- data/lib/konpeito/ui/widgets/text.rb +110 -0
- data/lib/konpeito/ui/widgets/tree.rb +426 -0
- data/lib/konpeito/version.rb +5 -0
- data/lib/konpeito.rb +109 -0
- data/test_native_array.rb +172 -0
- data/test_native_array_class.rb +197 -0
- data/test_native_class.rb +151 -0
- data/tools/konpeito-asm/build.sh +65 -0
- data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
- data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
- data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
- data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
- 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,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
|