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,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Konpeito
|
|
4
|
+
# Known standard library names that can be loaded at runtime via rb_require
|
|
5
|
+
# This list is derived from Ruby's bundled RBS stdlib definitions
|
|
6
|
+
KNOWN_STDLIB_LIBRARIES = %w[
|
|
7
|
+
json fileutils find pathname tempfile timeout uri yaml
|
|
8
|
+
digest openssl socket stringio csv date time set
|
|
9
|
+
net/http net/https net/ftp net/smtp net/pop net/imap
|
|
10
|
+
securerandom base64 benchmark erb logger optparse
|
|
11
|
+
pp prettyprint pstore monitor mutex_m thwait
|
|
12
|
+
tsort weakref shellwords abbrev ostruct open-uri
|
|
13
|
+
singleton forwardable delegate observable ripper
|
|
14
|
+
drb cgi webrick etc
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# Wrapper for merged AST that provides ProgramNode-like interface
|
|
18
|
+
class MergedAST
|
|
19
|
+
attr_reader :statements, :locals
|
|
20
|
+
|
|
21
|
+
def initialize(base_ast, merged_statements)
|
|
22
|
+
@locals = base_ast.locals
|
|
23
|
+
@statements = MergedStatements.new(merged_statements)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def is_a?(klass)
|
|
27
|
+
return true if klass == Prism::ProgramNode
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def compact_child_nodes
|
|
32
|
+
[@statements]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# TypedNode.node_type derives from class name — mimic Prism::ProgramNode
|
|
36
|
+
def class
|
|
37
|
+
Prism::ProgramNode
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class MergedStatements
|
|
42
|
+
attr_reader :body
|
|
43
|
+
|
|
44
|
+
def initialize(body)
|
|
45
|
+
@body = body
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def compact_child_nodes
|
|
49
|
+
@body
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def is_a?(klass)
|
|
53
|
+
return true if klass == Prism::StatementsNode
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# TypedNode.node_type derives from class name — mimic Prism::StatementsNode
|
|
58
|
+
def class
|
|
59
|
+
Prism::StatementsNode
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class DependencyResolver
|
|
64
|
+
attr_reader :resolved_files, :rbs_paths, :stdlib_requires, :runtime_native_extensions
|
|
65
|
+
|
|
66
|
+
def initialize(base_paths: [], verbose: false, cache_manager: nil)
|
|
67
|
+
@base_paths = base_paths
|
|
68
|
+
@verbose = verbose
|
|
69
|
+
@cache_manager = cache_manager
|
|
70
|
+
@resolved_files = {} # path => AST
|
|
71
|
+
@resolving = Set.new # For circular dependency detection
|
|
72
|
+
@rbs_paths = [] # Auto-detected RBS paths
|
|
73
|
+
@resolve_order = [] # Order of resolved files (for AST merging)
|
|
74
|
+
@stdlib_requires = [] # Stdlib libraries to load at runtime
|
|
75
|
+
@runtime_native_extensions = [] # Native extensions loaded at runtime (not for linking)
|
|
76
|
+
@cache_hits = 0 # Statistics for verbose output
|
|
77
|
+
@cache_misses = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Resolve all dependencies starting from entry_file
|
|
81
|
+
# Returns [merged_ast, rbs_paths, stdlib_requires, runtime_native_extensions]
|
|
82
|
+
def resolve(entry_file)
|
|
83
|
+
entry_path = File.expand_path(entry_file)
|
|
84
|
+
resolve_file(entry_path)
|
|
85
|
+
|
|
86
|
+
# Log cache statistics in verbose mode
|
|
87
|
+
if @verbose && @cache_manager && (@cache_hits > 0 || @cache_misses > 0)
|
|
88
|
+
log "Cache: #{@cache_hits} hits, #{@cache_misses} misses"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
merged_ast = merge_asts
|
|
92
|
+
[merged_ast, @rbs_paths.uniq, @stdlib_requires.uniq, @runtime_native_extensions.uniq]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def resolve_file(path)
|
|
98
|
+
return if @resolved_files.key?(path)
|
|
99
|
+
|
|
100
|
+
if @resolving.include?(path)
|
|
101
|
+
cycle = @resolving.to_a
|
|
102
|
+
cycle_start = cycle.index(path)
|
|
103
|
+
cycle_files = cycle[cycle_start..].map { |p| File.basename(p) }
|
|
104
|
+
cycle_files << File.basename(path) # Complete the cycle
|
|
105
|
+
cycle_path = cycle_files.join(" -> ")
|
|
106
|
+
raise DependencyError.new(
|
|
107
|
+
"Circular dependency detected: #{cycle_path}",
|
|
108
|
+
from_file: @resolving.to_a.last,
|
|
109
|
+
cycle: cycle_files
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless File.exist?(path)
|
|
114
|
+
raise DependencyError.new(
|
|
115
|
+
"File not found: #{path}",
|
|
116
|
+
missing_file: path
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
log "Resolving: #{path}"
|
|
121
|
+
@resolving.add(path)
|
|
122
|
+
|
|
123
|
+
# Try to use cached AST if available
|
|
124
|
+
ast = nil
|
|
125
|
+
if @cache_manager
|
|
126
|
+
cached_ast = @cache_manager.get_ast(path)
|
|
127
|
+
if cached_ast
|
|
128
|
+
log " (cached)"
|
|
129
|
+
ast = cached_ast
|
|
130
|
+
@cache_hits += 1
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse the file if not cached
|
|
135
|
+
unless ast
|
|
136
|
+
ast = Parser::PrismAdapter.parse_file(path)
|
|
137
|
+
@cache_misses += 1
|
|
138
|
+
|
|
139
|
+
# Store in cache
|
|
140
|
+
if @cache_manager
|
|
141
|
+
@cache_manager.put_ast(path, ast)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@resolved_files[path] = ast
|
|
146
|
+
|
|
147
|
+
# Check for corresponding RBS file (alongside .rb, or in types/ subdirectory)
|
|
148
|
+
rbs_path = path.sub(/\.rb$/, ".rbs")
|
|
149
|
+
if File.exist?(rbs_path)
|
|
150
|
+
log " Found RBS: #{rbs_path}"
|
|
151
|
+
@rbs_paths << rbs_path
|
|
152
|
+
else
|
|
153
|
+
dir = File.dirname(path)
|
|
154
|
+
# Check types/ subdirectory (common convention for separate type definitions)
|
|
155
|
+
types_rbs = File.join(dir, "types", File.basename(path, ".rb") + ".rbs")
|
|
156
|
+
if File.exist?(types_rbs)
|
|
157
|
+
log " Found RBS: #{types_rbs}"
|
|
158
|
+
@rbs_paths << types_rbs
|
|
159
|
+
else
|
|
160
|
+
# Check parent dir's types/ with subdirectory name as filename
|
|
161
|
+
# e.g., widgets/text.rb -> ../types/widgets.rbs
|
|
162
|
+
parent_dir = File.dirname(dir)
|
|
163
|
+
subdir_name = File.basename(dir)
|
|
164
|
+
parent_types_rbs = File.join(parent_dir, "types", subdir_name + ".rbs")
|
|
165
|
+
if File.exist?(parent_types_rbs) && !@rbs_paths.include?(parent_types_rbs)
|
|
166
|
+
log " Found RBS: #{parent_types_rbs}"
|
|
167
|
+
@rbs_paths << parent_types_rbs
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Clear existing dependencies for this file (in case of re-analysis)
|
|
173
|
+
if @cache_manager
|
|
174
|
+
@cache_manager.clear_dependencies(path)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Detect and resolve requires
|
|
178
|
+
requires = Parser::PrismAdapter.detect_requires(ast)
|
|
179
|
+
requires.each do |req|
|
|
180
|
+
dep_path = find_file(req[:name], from_file: path, is_relative: req[:type] == :require_relative)
|
|
181
|
+
if dep_path
|
|
182
|
+
# Register dependency: this file depends on dep_path
|
|
183
|
+
if @cache_manager
|
|
184
|
+
@cache_manager.add_dependency(path, dep_path)
|
|
185
|
+
end
|
|
186
|
+
resolve_file(dep_path)
|
|
187
|
+
elsif req[:type] == :require && stdlib_library?(req[:name])
|
|
188
|
+
# Known stdlib library - will be loaded at runtime via rb_require
|
|
189
|
+
log " Detected stdlib require: #{req[:name]}"
|
|
190
|
+
@stdlib_requires << req[:name]
|
|
191
|
+
elsif req[:type] == :require_relative
|
|
192
|
+
# Check if this points to a native extension (.bundle/.so/.dll)
|
|
193
|
+
if native_extension_exists?(req[:name], from_file: path)
|
|
194
|
+
log " Detected native extension (runtime load): #{req[:name]}"
|
|
195
|
+
# Track both the base name (for linker exclusion) and the absolute path
|
|
196
|
+
# (for rb_require in Init function).
|
|
197
|
+
# e.g. "../stdlib/ui/konpeito_ui" → base: "konpeito_ui", path: "/abs/path/to/konpeito_ui"
|
|
198
|
+
abs_path = File.expand_path(req[:name], File.dirname(path))
|
|
199
|
+
@runtime_native_extensions << { base: File.basename(req[:name]), path: abs_path }
|
|
200
|
+
else
|
|
201
|
+
# require_relative must be resolvable
|
|
202
|
+
raise DependencyError.new(
|
|
203
|
+
"Cannot resolve require_relative: #{req[:name]} from #{path}",
|
|
204
|
+
from_file: path,
|
|
205
|
+
line: req[:line],
|
|
206
|
+
missing_file: req[:name]
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
else
|
|
210
|
+
# Unknown require - likely a gem that must be required at runtime
|
|
211
|
+
raise DependencyError.new(
|
|
212
|
+
"Cannot resolve require: #{req[:name]}. " \
|
|
213
|
+
"If this is a gem, use require at runtime in Ruby code instead of compile-time.",
|
|
214
|
+
from_file: path,
|
|
215
|
+
line: req[:line],
|
|
216
|
+
missing_file: req[:name]
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
@resolving.delete(path)
|
|
222
|
+
@resolve_order << path
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def find_file(name, from_file:, is_relative:)
|
|
226
|
+
# Add .rb extension if not present
|
|
227
|
+
name_with_ext = name.end_with?(".rb") ? name : "#{name}.rb"
|
|
228
|
+
|
|
229
|
+
if is_relative
|
|
230
|
+
# require_relative: resolve relative to the current file
|
|
231
|
+
dir = File.dirname(from_file)
|
|
232
|
+
path = File.expand_path(name_with_ext, dir)
|
|
233
|
+
return path if File.exist?(path)
|
|
234
|
+
else
|
|
235
|
+
# require: search in base_paths and relative to entry file
|
|
236
|
+
search_paths = @base_paths + [File.dirname(from_file)]
|
|
237
|
+
search_paths.each do |base|
|
|
238
|
+
path = File.expand_path(name_with_ext, base)
|
|
239
|
+
return path if File.exist?(path)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
nil # Not found (may be stdlib)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def merge_asts
|
|
247
|
+
return @resolved_files.values.first if @resolve_order.size == 1
|
|
248
|
+
|
|
249
|
+
# Merge all ASTs in dependency order (dependencies first)
|
|
250
|
+
all_statements = []
|
|
251
|
+
|
|
252
|
+
@resolve_order.each do |path|
|
|
253
|
+
ast = @resolved_files[path]
|
|
254
|
+
next unless ast.is_a?(Prism::ProgramNode)
|
|
255
|
+
|
|
256
|
+
# Extract statements, filtering out require/require_relative calls
|
|
257
|
+
ast.statements.body.each do |stmt|
|
|
258
|
+
next if require_statement?(stmt)
|
|
259
|
+
all_statements << stmt
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Create a wrapper that provides the same interface as ProgramNode
|
|
264
|
+
first_ast = @resolved_files[@resolve_order.first]
|
|
265
|
+
MergedAST.new(first_ast, all_statements)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def require_statement?(stmt)
|
|
269
|
+
return false unless stmt.is_a?(Prism::CallNode)
|
|
270
|
+
return false unless stmt.receiver.nil?
|
|
271
|
+
%w[require require_relative].include?(stmt.name.to_s)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def log(message)
|
|
275
|
+
puts message if @verbose
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Check if a require_relative target points to a native extension (.bundle/.so/.dll)
|
|
279
|
+
def native_extension_exists?(name, from_file:)
|
|
280
|
+
dir = File.dirname(from_file)
|
|
281
|
+
base = File.expand_path(name, dir)
|
|
282
|
+
# Check for common native extension file extensions
|
|
283
|
+
%w[.bundle .so .dll .dylib].any? { |ext| File.exist?("#{base}#{ext}") }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Check if a name is a known stdlib library
|
|
287
|
+
def stdlib_library?(name)
|
|
288
|
+
# Normalize: convert dashes to underscores for matching
|
|
289
|
+
normalized = name.tr("-", "_")
|
|
290
|
+
KNOWN_STDLIB_LIBRARIES.include?(name) ||
|
|
291
|
+
KNOWN_STDLIB_LIBRARIES.include?(normalized) ||
|
|
292
|
+
KNOWN_STDLIB_LIBRARIES.include?(name.tr("_", "-"))
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "diagnostic"
|
|
4
|
+
require_relative "renderer"
|
|
5
|
+
|
|
6
|
+
module Konpeito
|
|
7
|
+
module Diagnostics
|
|
8
|
+
# Collects diagnostics from various compilation phases
|
|
9
|
+
class Collector
|
|
10
|
+
attr_reader :diagnostics
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@diagnostics = []
|
|
14
|
+
@source_cache = {} # file_path -> source content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Register source content for a file
|
|
18
|
+
def register_source(file_path, content)
|
|
19
|
+
@source_cache[file_path] = content
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get cached source content
|
|
23
|
+
def source_for(file_path)
|
|
24
|
+
@source_cache[file_path] ||= File.exist?(file_path) ? File.read(file_path) : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add a diagnostic
|
|
28
|
+
def add(diagnostic)
|
|
29
|
+
@diagnostics << diagnostic
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Create and add a type mismatch error
|
|
33
|
+
def type_mismatch(expected:, found:, node:, file_path:)
|
|
34
|
+
span = span_from_node(node, file_path)
|
|
35
|
+
add(Diagnostic.type_mismatch(expected: expected, found: found, span: span))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Create and add an undefined variable error
|
|
39
|
+
def undefined_variable(name:, node:, file_path:, similar: nil)
|
|
40
|
+
span = span_from_node(node, file_path)
|
|
41
|
+
add(Diagnostic.undefined_variable(name: name, span: span, similar: similar))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Create and add a parse error
|
|
45
|
+
def parse_error(message:, location:, file_path:)
|
|
46
|
+
span = span_from_location(location, file_path)
|
|
47
|
+
add(Diagnostic.parse_error(message: message, span: span))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Create and add a circular dependency error
|
|
51
|
+
def circular_dependency(cycle:, file_path:, line: 1)
|
|
52
|
+
span = SourceSpan.new(
|
|
53
|
+
file_path: file_path,
|
|
54
|
+
start_line: line,
|
|
55
|
+
start_column: 0,
|
|
56
|
+
source: source_for(file_path)
|
|
57
|
+
)
|
|
58
|
+
add(Diagnostic.circular_dependency(cycle: cycle, span: span))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Create and add a file not found error
|
|
62
|
+
def file_not_found(path:, from_file:, line: 1)
|
|
63
|
+
span = SourceSpan.new(
|
|
64
|
+
file_path: from_file,
|
|
65
|
+
start_line: line,
|
|
66
|
+
start_column: 0,
|
|
67
|
+
source: source_for(from_file)
|
|
68
|
+
)
|
|
69
|
+
add(Diagnostic.file_not_found(path: path, span: span))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Create and add a codegen error
|
|
73
|
+
def codegen_error(message:, node: nil, file_path: nil)
|
|
74
|
+
span = node && file_path ? span_from_node(node, file_path) : nil
|
|
75
|
+
add(Diagnostic.codegen_error(message: message, span: span))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Create and add an arity mismatch error
|
|
79
|
+
def arity_mismatch(expected:, found:, node:, file_path:)
|
|
80
|
+
span = span_from_node(node, file_path)
|
|
81
|
+
add(Diagnostic.arity_mismatch(expected: expected, found: found, span: span))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if there are any errors
|
|
85
|
+
def errors?
|
|
86
|
+
@diagnostics.any?(&:error?)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get only errors
|
|
90
|
+
def errors
|
|
91
|
+
@diagnostics.select(&:error?)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get only warnings
|
|
95
|
+
def warnings
|
|
96
|
+
@diagnostics.select(&:warning?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Render all diagnostics
|
|
100
|
+
def render(color: true, io: $stderr)
|
|
101
|
+
renderer = DiagnosticRenderer.new(color: color, io: io)
|
|
102
|
+
renderer.render_all(@diagnostics)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Clear all diagnostics
|
|
106
|
+
def clear
|
|
107
|
+
@diagnostics.clear
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def span_from_node(node, file_path)
|
|
113
|
+
return nil unless node.respond_to?(:location) && node.location
|
|
114
|
+
|
|
115
|
+
span_from_location(node.location, file_path)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def span_from_location(location, file_path)
|
|
119
|
+
SourceSpan.from_prism_location(
|
|
120
|
+
location,
|
|
121
|
+
file_path: file_path,
|
|
122
|
+
source: source_for(file_path)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Konpeito
|
|
4
|
+
module Diagnostics
|
|
5
|
+
# Represents a location in source code with ability to extract snippets
|
|
6
|
+
class SourceSpan
|
|
7
|
+
attr_reader :file_path, :start_line, :start_column, :end_line, :end_column
|
|
8
|
+
|
|
9
|
+
def initialize(file_path:, start_line:, start_column:, end_line: nil, end_column: nil, source: nil)
|
|
10
|
+
@file_path = file_path
|
|
11
|
+
@start_line = start_line
|
|
12
|
+
@start_column = start_column
|
|
13
|
+
@end_line = end_line || start_line
|
|
14
|
+
@end_column = end_column || start_column
|
|
15
|
+
@source = source
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Create from a Prism node location
|
|
19
|
+
def self.from_prism_location(location, file_path:, source: nil)
|
|
20
|
+
new(
|
|
21
|
+
file_path: file_path,
|
|
22
|
+
start_line: location.start_line,
|
|
23
|
+
start_column: location.start_column,
|
|
24
|
+
end_line: location.end_line,
|
|
25
|
+
end_column: location.end_column,
|
|
26
|
+
source: source
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get source lines with optional context
|
|
31
|
+
def snippet(context_lines: 2)
|
|
32
|
+
return nil unless source_lines
|
|
33
|
+
|
|
34
|
+
first_line = [@start_line - context_lines, 1].max
|
|
35
|
+
last_line = [@end_line + context_lines, source_lines.size].min
|
|
36
|
+
|
|
37
|
+
lines = []
|
|
38
|
+
(first_line..last_line).each do |line_num|
|
|
39
|
+
line_content = source_lines[line_num - 1] || ""
|
|
40
|
+
lines << { line_num: line_num, content: line_content, highlight: line_in_span?(line_num) }
|
|
41
|
+
end
|
|
42
|
+
lines
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get only the highlighted source text
|
|
46
|
+
def highlighted_text
|
|
47
|
+
return nil unless source_lines
|
|
48
|
+
|
|
49
|
+
if @start_line == @end_line
|
|
50
|
+
line = source_lines[@start_line - 1] || ""
|
|
51
|
+
line[@start_column...@end_column] || line[@start_column..]
|
|
52
|
+
else
|
|
53
|
+
# Multi-line span
|
|
54
|
+
lines = source_lines[(@start_line - 1)...@end_line]
|
|
55
|
+
return nil unless lines
|
|
56
|
+
|
|
57
|
+
result = []
|
|
58
|
+
lines.each_with_index do |line, idx|
|
|
59
|
+
if idx == 0
|
|
60
|
+
result << line[@start_column..]
|
|
61
|
+
elsif idx == lines.size - 1
|
|
62
|
+
result << line[0...@end_column]
|
|
63
|
+
else
|
|
64
|
+
result << line
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
result.join("\n")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_s
|
|
72
|
+
"#{@file_path}:#{@start_line}:#{@start_column}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def source_lines
|
|
78
|
+
return @source_lines if defined?(@source_lines)
|
|
79
|
+
|
|
80
|
+
@source_lines = if @source
|
|
81
|
+
@source.lines
|
|
82
|
+
elsif @file_path && File.exist?(@file_path)
|
|
83
|
+
File.read(@file_path).lines
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def line_in_span?(line_num)
|
|
88
|
+
line_num >= @start_line && line_num <= @end_line
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Represents a diagnostic label (additional annotation on source)
|
|
93
|
+
class Label
|
|
94
|
+
attr_reader :span, :message, :style
|
|
95
|
+
|
|
96
|
+
def initialize(span:, message:, style: :primary)
|
|
97
|
+
@span = span
|
|
98
|
+
@message = message
|
|
99
|
+
@style = style # :primary, :secondary
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Represents a single diagnostic message (error, warning, or note)
|
|
104
|
+
class Diagnostic
|
|
105
|
+
SEVERITIES = { error: 0, warning: 1, note: 2, help: 3 }.freeze
|
|
106
|
+
|
|
107
|
+
attr_reader :severity, :code, :message, :span, :labels, :notes, :suggestions
|
|
108
|
+
|
|
109
|
+
def initialize(severity:, code:, message:, span: nil, labels: [], notes: [], suggestions: [])
|
|
110
|
+
raise ArgumentError, "Invalid severity: #{severity}" unless SEVERITIES.key?(severity)
|
|
111
|
+
|
|
112
|
+
@severity = severity
|
|
113
|
+
@code = code
|
|
114
|
+
@message = message
|
|
115
|
+
@span = span
|
|
116
|
+
@labels = labels
|
|
117
|
+
@notes = notes
|
|
118
|
+
@suggestions = suggestions
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def error?
|
|
122
|
+
@severity == :error
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def warning?
|
|
126
|
+
@severity == :warning
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Create a type mismatch error
|
|
130
|
+
def self.type_mismatch(expected:, found:, span:, note_span: nil)
|
|
131
|
+
labels = [Label.new(span: span, message: "expected #{expected}, found #{found}")]
|
|
132
|
+
|
|
133
|
+
notes = []
|
|
134
|
+
if note_span
|
|
135
|
+
notes << "type was inferred here: #{note_span}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
new(
|
|
139
|
+
severity: :error,
|
|
140
|
+
code: "E001",
|
|
141
|
+
message: "type mismatch",
|
|
142
|
+
span: span,
|
|
143
|
+
labels: labels,
|
|
144
|
+
notes: notes
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Create an undefined variable error
|
|
149
|
+
def self.undefined_variable(name:, span:, similar: nil)
|
|
150
|
+
suggestions = []
|
|
151
|
+
if similar
|
|
152
|
+
suggestions << "did you mean `#{similar}`?"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
new(
|
|
156
|
+
severity: :error,
|
|
157
|
+
code: "E004",
|
|
158
|
+
message: "undefined variable `#{name}`",
|
|
159
|
+
span: span,
|
|
160
|
+
suggestions: suggestions
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Create an undefined method error
|
|
165
|
+
def self.undefined_method(name:, receiver_type:, span:)
|
|
166
|
+
new(
|
|
167
|
+
severity: :error,
|
|
168
|
+
code: "E005",
|
|
169
|
+
message: "undefined method `#{name}` for type #{receiver_type}",
|
|
170
|
+
span: span
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Create a parse error
|
|
175
|
+
def self.parse_error(message:, span:)
|
|
176
|
+
new(
|
|
177
|
+
severity: :error,
|
|
178
|
+
code: "E010",
|
|
179
|
+
message: message,
|
|
180
|
+
span: span
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Create a circular dependency error
|
|
185
|
+
def self.circular_dependency(cycle:, span:)
|
|
186
|
+
cycle_str = cycle.join(" -> ")
|
|
187
|
+
new(
|
|
188
|
+
severity: :error,
|
|
189
|
+
code: "E020",
|
|
190
|
+
message: "circular dependency detected",
|
|
191
|
+
span: span,
|
|
192
|
+
notes: ["dependency cycle: #{cycle_str}"]
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Create a file not found error
|
|
197
|
+
def self.file_not_found(path:, span:)
|
|
198
|
+
new(
|
|
199
|
+
severity: :error,
|
|
200
|
+
code: "E021",
|
|
201
|
+
message: "file not found: #{path}",
|
|
202
|
+
span: span
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Create a codegen error
|
|
207
|
+
def self.codegen_error(message:, span: nil)
|
|
208
|
+
new(
|
|
209
|
+
severity: :error,
|
|
210
|
+
code: "E030",
|
|
211
|
+
message: message,
|
|
212
|
+
span: span
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Create an arity mismatch error
|
|
217
|
+
def self.arity_mismatch(expected:, found:, span:)
|
|
218
|
+
new(
|
|
219
|
+
severity: :error,
|
|
220
|
+
code: "E002",
|
|
221
|
+
message: "wrong number of arguments (given #{found}, expected #{expected})",
|
|
222
|
+
span: span
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Create an occurs check error (infinite type)
|
|
227
|
+
def self.occurs_check(type_var:, type:, span:)
|
|
228
|
+
new(
|
|
229
|
+
severity: :error,
|
|
230
|
+
code: "E003",
|
|
231
|
+
message: "cannot construct infinite type: #{type_var} ~ #{type}",
|
|
232
|
+
span: span
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|