igniter_lang 0.1.0.alpha.1
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/README.md +65 -0
- data/RELEASE_NOTES.md +137 -0
- data/bin/igc +7 -0
- data/lib/igniter_lang/assembler.rb +717 -0
- data/lib/igniter_lang/classifier.rb +405 -0
- data/lib/igniter_lang/cli.rb +76 -0
- data/lib/igniter_lang/compilation_report.rb +99 -0
- data/lib/igniter_lang/compiler_orchestrator.rb +362 -0
- data/lib/igniter_lang/compiler_profile_contract_validator.rb +286 -0
- data/lib/igniter_lang/compiler_result.rb +77 -0
- data/lib/igniter_lang/diagnostics.rb +125 -0
- data/lib/igniter_lang/fragment_registry_compatibility_adapter.rb +129 -0
- data/lib/igniter_lang/internal_profile_assembly.rb +199 -0
- data/lib/igniter_lang/internal_profile_assembly_source_packet.rb +175 -0
- data/lib/igniter_lang/internal_profile_static_data_carrier.rb +286 -0
- data/lib/igniter_lang/oof_fragment_registry.rb +802 -0
- data/lib/igniter_lang/parser.rb +1736 -0
- data/lib/igniter_lang/runtime_smoke.rb +80 -0
- data/lib/igniter_lang/semanticir_emitter.rb +847 -0
- data/lib/igniter_lang/temporal_access_runtime.rb +437 -0
- data/lib/igniter_lang/temporal_executor.rb +457 -0
- data/lib/igniter_lang/typechecker.rb +821 -0
- data/lib/igniter_lang/version.rb +5 -0
- data/lib/igniter_lang.rb +27 -0
- metadata +72 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module IgniterLang
|
|
6
|
+
class Classifier
|
|
7
|
+
DEFAULT_VERSION = "classifier-pass-executable-proof-v0"
|
|
8
|
+
|
|
9
|
+
def initialize(classifier_version: DEFAULT_VERSION)
|
|
10
|
+
@classifier_version = classifier_version
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def classify(parsed_program, sample_input:)
|
|
14
|
+
assumption_registry = assumption_registry(parsed_program)
|
|
15
|
+
contracts = parsed_program.fetch("contracts").map do |contract|
|
|
16
|
+
classify_contract(parsed_program, contract, sample_input, assumption_registry)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
result = {
|
|
20
|
+
"kind" => "classified_program",
|
|
21
|
+
"classifier_version" => @classifier_version,
|
|
22
|
+
"program_id" => program_id(parsed_program),
|
|
23
|
+
"source_path" => parsed_program.fetch("source_path"),
|
|
24
|
+
"source_hash" => parsed_program.fetch("source_hash"),
|
|
25
|
+
"grammar_version" => parsed_program.fetch("grammar_version"),
|
|
26
|
+
"module" => parsed_program.fetch("module"),
|
|
27
|
+
"type_declarations" => type_declarations(parsed_program),
|
|
28
|
+
"contracts" => contracts,
|
|
29
|
+
"oof_log" => contracts.flat_map { |contract| contract.fetch("oof_log") },
|
|
30
|
+
"semantic_ir_ref" => nil
|
|
31
|
+
}
|
|
32
|
+
result["assumption_registry"] = assumption_registry.values unless assumption_registry.empty?
|
|
33
|
+
olap_points = parsed_program.fetch("olap_points", [])
|
|
34
|
+
result["olap_points"] = olap_points unless olap_points.empty?
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def type_declarations(parsed_program)
|
|
39
|
+
parsed_program.fetch("types", []).map do |type|
|
|
40
|
+
{
|
|
41
|
+
"kind" => "type",
|
|
42
|
+
"name" => type.fetch("name"),
|
|
43
|
+
"fields" => type.fetch("fields", []).map do |field|
|
|
44
|
+
{
|
|
45
|
+
"name" => field.fetch("name"),
|
|
46
|
+
"type_annotation" => normalize_type(field.fetch("type_annotation")),
|
|
47
|
+
"optional" => field.fetch("optional", false)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def program_id(parsed_program)
|
|
57
|
+
seed = [
|
|
58
|
+
parsed_program.fetch("source_path"),
|
|
59
|
+
parsed_program.fetch("grammar_version"),
|
|
60
|
+
parsed_program.fetch("source_hash"),
|
|
61
|
+
@classifier_version
|
|
62
|
+
].join("|")
|
|
63
|
+
"classifier_pass/#{Digest::SHA256.hexdigest(seed)[0, 16]}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def classify_contract(parsed_program, contract, sample_input, assumption_registry)
|
|
67
|
+
diagnostics = []
|
|
68
|
+
declarations = []
|
|
69
|
+
assumption_refs = []
|
|
70
|
+
symbol_fragments = {}
|
|
71
|
+
symbol_kinds = {}
|
|
72
|
+
compute_exprs = {}
|
|
73
|
+
window_declarations = []
|
|
74
|
+
fold_stream_stream_refs = Hash.new { |refs, stream_name| refs[stream_name] = [] }
|
|
75
|
+
parsed_program.fetch("olap_points", []).each do |point|
|
|
76
|
+
symbol_fragments[point.fetch("name")] = "escape"
|
|
77
|
+
symbol_kinds[point.fetch("name")] = "olap_point"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
contract.fetch("body").each do |node|
|
|
81
|
+
case node.fetch("kind")
|
|
82
|
+
when "input"
|
|
83
|
+
symbol_fragments[node.fetch("name")] = "core"
|
|
84
|
+
symbol_kinds[node.fetch("name")] = "input"
|
|
85
|
+
declarations << classified_decl(node, "core", [], [])
|
|
86
|
+
when "escape"
|
|
87
|
+
declarations << classified_decl(node, "escape", [], [])
|
|
88
|
+
when "stream"
|
|
89
|
+
symbol_fragments[node.fetch("name")] = "escape"
|
|
90
|
+
symbol_kinds[node.fetch("name")] = "stream"
|
|
91
|
+
declarations << classified_decl(node, "escape", [], [])
|
|
92
|
+
when "read"
|
|
93
|
+
fragment = temporal_type?(node["type_annotation"]) ? "temporal" : "escape"
|
|
94
|
+
symbol_fragments[node.fetch("name")] = fragment == "temporal" ? "core" : "escape"
|
|
95
|
+
symbol_kinds[node.fetch("name")] = fragment == "temporal" ? "temporal_read" : "read"
|
|
96
|
+
declarations << classified_decl(node, fragment, [], []).merge(value_fragment_metadata(fragment, node["type_annotation"]))
|
|
97
|
+
when "window"
|
|
98
|
+
window_declarations << node
|
|
99
|
+
declarations << classified_decl(node.merge("name" => node.fetch("label", "_window")), "escape", [], [])
|
|
100
|
+
when "uses_assumptions"
|
|
101
|
+
name = node.fetch("name")
|
|
102
|
+
assumption_refs << name
|
|
103
|
+
symbol_fragments[name] = "core"
|
|
104
|
+
symbol_kinds[name] = "assumption"
|
|
105
|
+
missing = assumption_registry.key?(name) ? [] : [name]
|
|
106
|
+
unless missing.empty?
|
|
107
|
+
diagnostics << oof(
|
|
108
|
+
"OOF-A1",
|
|
109
|
+
"contract '#{contract.fetch("name")}' uses assumptions '#{name}' but no " \
|
|
110
|
+
"assumption named '#{name}' is declared in this module",
|
|
111
|
+
"uses_assumptions:#{name}"
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
declarations << classified_decl(node, "epistemic", [], missing)
|
|
115
|
+
when "fold_stream"
|
|
116
|
+
bound = node.fetch("bound", nil)
|
|
117
|
+
result_fragment = bound ? "core" : "oof"
|
|
118
|
+
deps = expr_refs(node.fetch("expr", { "kind" => "literal", "value" => nil }))
|
|
119
|
+
deps.select { |dep| symbol_kinds[dep] == "stream" }.each do |stream_name|
|
|
120
|
+
fold_stream_stream_refs[stream_name] << node.fetch("name")
|
|
121
|
+
end
|
|
122
|
+
symbol_fragments[node.fetch("name")] = result_fragment
|
|
123
|
+
symbol_kinds[node.fetch("name")] = "fold_stream"
|
|
124
|
+
declarations << classified_decl(node, result_fragment, deps, [])
|
|
125
|
+
when "invariant"
|
|
126
|
+
deps = [node.fetch("predicate_ref", nil)].compact
|
|
127
|
+
missing = deps.reject { |dep| symbol_fragments.key?(dep) }
|
|
128
|
+
missing.each do |name|
|
|
129
|
+
diagnostics << oof("OOF-P1", "Unresolved symbol: #{name}", node.fetch("name"))
|
|
130
|
+
end
|
|
131
|
+
declarations << classified_decl(node, missing.empty? ? "core" : "oof", deps, missing)
|
|
132
|
+
.merge(invariant_author_fields(node))
|
|
133
|
+
.merge("source_metadata" => invariant_source_metadata(parsed_program, node))
|
|
134
|
+
when "compute"
|
|
135
|
+
deps = expr_refs(node.fetch("expr"))
|
|
136
|
+
missing = deps.reject { |dep| symbol_fragments.key?(dep) }
|
|
137
|
+
missing.each do |name|
|
|
138
|
+
diagnostics << oof("OOF-P1", "Unresolved symbol: #{name}", node.fetch("name"))
|
|
139
|
+
end
|
|
140
|
+
stream_deps = deps.select { |dep| symbol_kinds[dep] == "stream" }
|
|
141
|
+
stream_deps.each do |stream_name|
|
|
142
|
+
diagnostics << oof("OOF-S4", "Direct use of stream '#{stream_name}' is OOF - use fold_stream instead", node.fetch("name"))
|
|
143
|
+
end
|
|
144
|
+
upstream_oof = deps.any? { |dep| symbol_fragments[dep] == "oof" }
|
|
145
|
+
fragment = missing.empty? && stream_deps.empty? && !upstream_oof ? "core" : "oof"
|
|
146
|
+
symbol_fragments[node.fetch("name")] = fragment
|
|
147
|
+
symbol_kinds[node.fetch("name")] = "compute"
|
|
148
|
+
compute_exprs[node.fetch("name")] = node.fetch("expr")
|
|
149
|
+
declarations << classified_decl(node, fragment, deps, missing)
|
|
150
|
+
when "output"
|
|
151
|
+
name = node.fetch("name")
|
|
152
|
+
missing = symbol_fragments.key?(name) ? [] : [name]
|
|
153
|
+
diagnostics << oof("OOF-P1", "Unresolved output source: #{name}", name) unless missing.empty?
|
|
154
|
+
src_fragment = symbol_fragments.fetch(name, "oof")
|
|
155
|
+
fragment = missing.empty? && src_fragment == "core" ? "core" : "oof"
|
|
156
|
+
confidence_oof = confidence_as_bool_oof(node, compute_exprs[name])
|
|
157
|
+
diagnostics << confidence_oof if confidence_oof
|
|
158
|
+
fragment = "oof" if confidence_oof
|
|
159
|
+
declarations << classified_decl(node, fragment, [name], missing)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
diagnostics.concat(stream_missing_window_oofs(fold_stream_stream_refs, window_declarations))
|
|
164
|
+
diagnostics.concat(evidence_gate_oofs(contract, sample_input))
|
|
165
|
+
|
|
166
|
+
modifier = contract.fetch("modifier", "pure")
|
|
167
|
+
if modifier == "pure"
|
|
168
|
+
escape_decl = declarations.find { |decl| decl.fetch("fragment_class") == "escape" }
|
|
169
|
+
if escape_decl
|
|
170
|
+
diagnostics << oof(
|
|
171
|
+
"OOF-M1",
|
|
172
|
+
"pure contract '#{contract.fetch("name")}' cannot declare escape capabilities; " \
|
|
173
|
+
"use 'observed' for read-only external access",
|
|
174
|
+
contract.fetch("name")
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
contract_fragment = contract_fragment_for(declarations, diagnostics, modifier: modifier)
|
|
180
|
+
|
|
181
|
+
result = {
|
|
182
|
+
"kind" => "classified_contract",
|
|
183
|
+
"contract_id" => contract_id(parsed_program, contract),
|
|
184
|
+
"name" => contract.fetch("name"),
|
|
185
|
+
"modifier" => modifier,
|
|
186
|
+
"fragment_class" => contract_fragment,
|
|
187
|
+
"symbols" => symbol_table(symbol_kinds, symbol_fragments),
|
|
188
|
+
"declarations" => declarations,
|
|
189
|
+
"dependency_graph" => dependency_graph(declarations),
|
|
190
|
+
"oof_log" => diagnostics
|
|
191
|
+
}
|
|
192
|
+
result["assumption_refs"] = assumption_refs.uniq unless assumption_refs.empty?
|
|
193
|
+
result
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def contract_fragment_for(declarations, diagnostics, modifier: "pure")
|
|
197
|
+
return "oof" unless diagnostics.empty?
|
|
198
|
+
return "core" if declarations.all? { |decl| decl.fetch("fragment_class") == "core" }
|
|
199
|
+
return "temporal" if declarations.any? { |decl| decl.fetch("fragment_class") == "temporal" } &&
|
|
200
|
+
declarations.none? { |decl| decl.fetch("fragment_class") == "oof" }
|
|
201
|
+
return "escape" if (modifier != "pure" || declarations.any? { |decl| decl.fetch("fragment_class") == "escape" }) &&
|
|
202
|
+
declarations.none? { |decl| decl.fetch("fragment_class") == "oof" }
|
|
203
|
+
return "epistemic" if declarations.any? { |decl| decl.fetch("fragment_class") == "epistemic" } &&
|
|
204
|
+
declarations.none? { |decl| decl.fetch("fragment_class") == "oof" }
|
|
205
|
+
|
|
206
|
+
"oof"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def assumption_registry(parsed_program)
|
|
210
|
+
parsed_program.fetch("assumptions", []).each_with_object({}) do |assumption, registry|
|
|
211
|
+
name = assumption.fetch("name")
|
|
212
|
+
registry[name] = {
|
|
213
|
+
"kind" => "assumption_entry",
|
|
214
|
+
"name" => name,
|
|
215
|
+
"fields" => assumption.fetch("fields", {}),
|
|
216
|
+
"declared_in_module" => parsed_program.fetch("module")
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def stream_missing_window_oofs(fold_stream_stream_refs, window_declarations)
|
|
222
|
+
return [] unless window_declarations.empty?
|
|
223
|
+
|
|
224
|
+
fold_stream_stream_refs.keys.sort.map do |stream_name|
|
|
225
|
+
oof("OOF-S2", "stream '#{stream_name}' has no window - every stream must declare a window", stream_name)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def contract_id(parsed_program, contract)
|
|
230
|
+
[parsed_program.fetch("module"), contract.fetch("name")].compact.join(".")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def classified_decl(node, fragment, deps, missing)
|
|
234
|
+
result = {
|
|
235
|
+
"decl_id" => decl_id(node),
|
|
236
|
+
"kind" => node.fetch("kind"),
|
|
237
|
+
"name" => node.fetch("name"),
|
|
238
|
+
"fragment_class" => fragment,
|
|
239
|
+
"deps" => deps,
|
|
240
|
+
"missing_refs" => missing
|
|
241
|
+
}
|
|
242
|
+
result["type_annotation"] = normalized_type_annotation(node["type_annotation"]) if node.key?("type_annotation")
|
|
243
|
+
if node.key?("expr")
|
|
244
|
+
result["expr_kind"] = node.fetch("expr").fetch("kind")
|
|
245
|
+
result["expr"] = node.fetch("expr")
|
|
246
|
+
end
|
|
247
|
+
%w[bound options].each do |key|
|
|
248
|
+
result[key] = node.fetch(key) if node.key?(key)
|
|
249
|
+
end
|
|
250
|
+
result
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def invariant_author_fields(node)
|
|
254
|
+
%w[predicate_ref severity label message overridable_with source_span threshold threshold_ms].each_with_object({}) do |key, result|
|
|
255
|
+
result[key] = node.fetch(key) if node.key?(key)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def invariant_source_metadata(parsed_program, node)
|
|
260
|
+
{
|
|
261
|
+
"kind" => "invariant",
|
|
262
|
+
"source_path" => parsed_program.fetch("source_path", nil),
|
|
263
|
+
"source_span" => node.fetch("source_span", nil),
|
|
264
|
+
"name" => node.fetch("name"),
|
|
265
|
+
"severity" => node.fetch("severity", "error"),
|
|
266
|
+
"label" => node.fetch("label", nil),
|
|
267
|
+
"message" => node.fetch("message", nil)
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def value_fragment_metadata(fragment, type)
|
|
272
|
+
return {} unless fragment == "temporal"
|
|
273
|
+
|
|
274
|
+
type_name = normalize_type(type)
|
|
275
|
+
{
|
|
276
|
+
"node_fragment_class" => "temporal",
|
|
277
|
+
"value_fragment_class" => "core",
|
|
278
|
+
"required_capability" => temporal_capability(type_name),
|
|
279
|
+
"temporal_axis" => temporal_axis(type_name)
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def temporal_capability(type_name)
|
|
284
|
+
type_name == "BiHistory" ? "bihistory_read" : "history_read"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def temporal_axis(type_name)
|
|
288
|
+
type_name == "BiHistory" ? "bitemporal" : "valid_time"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def decl_id(node)
|
|
292
|
+
"#{node.fetch("kind")}:#{node.fetch("name")}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def symbol_table(symbol_kinds, symbol_fragments)
|
|
296
|
+
symbol_kinds.keys.sort.map do |name|
|
|
297
|
+
{
|
|
298
|
+
"name" => name,
|
|
299
|
+
"kind" => symbol_kinds.fetch(name),
|
|
300
|
+
"fragment_class" => symbol_fragments.fetch(name)
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def dependency_graph(declarations)
|
|
306
|
+
declaration_ids = declarations.map { |decl| decl.fetch("decl_id") }
|
|
307
|
+
symbol_producers = declarations.each_with_object({}) do |decl, index|
|
|
308
|
+
next unless %w[input compute].include?(decl.fetch("kind"))
|
|
309
|
+
|
|
310
|
+
index[decl.fetch("name")] = decl.fetch("decl_id")
|
|
311
|
+
end
|
|
312
|
+
edges = declarations.flat_map do |decl|
|
|
313
|
+
decl.fetch("deps").filter_map do |dep|
|
|
314
|
+
from = symbol_producers[dep]
|
|
315
|
+
next unless from
|
|
316
|
+
|
|
317
|
+
{ "from" => from, "to" => decl.fetch("decl_id"), "kind" => "symbol" }
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
{ "nodes" => declaration_ids, "edges" => edges }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def expr_refs(expr)
|
|
324
|
+
return [] unless expr.is_a?(Hash)
|
|
325
|
+
unless expr.key?("kind")
|
|
326
|
+
return expr.values.flat_map do |value|
|
|
327
|
+
case value
|
|
328
|
+
when Hash then expr_refs(value)
|
|
329
|
+
when Array then value.flat_map { |item| expr_refs(item) }
|
|
330
|
+
else []
|
|
331
|
+
end
|
|
332
|
+
end.uniq
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
case expr.fetch("kind")
|
|
336
|
+
when "ref"
|
|
337
|
+
[expr.fetch("name")]
|
|
338
|
+
when "field_access"
|
|
339
|
+
expr_refs(expr.fetch("object"))
|
|
340
|
+
when "binary_op"
|
|
341
|
+
expr_refs(expr.fetch("left")) + expr_refs(expr.fetch("right"))
|
|
342
|
+
when "call"
|
|
343
|
+
expr.fetch("args", []).flat_map { |arg| expr_refs(arg) }
|
|
344
|
+
when "literal", "symbol"
|
|
345
|
+
[]
|
|
346
|
+
else
|
|
347
|
+
expr.values.flat_map { |value| value.is_a?(Hash) ? expr_refs(value) : [] }
|
|
348
|
+
end.uniq
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def confidence_as_bool_oof(output_node, expr)
|
|
352
|
+
return nil unless normalize_type(output_node.fetch("type_annotation")) == "Bool"
|
|
353
|
+
return nil unless confidence_label_expr?(expr)
|
|
354
|
+
|
|
355
|
+
oof("OOF-CE4", "ConfidenceLabel cannot be used as Bool", output_node.fetch("name"))
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def confidence_label_expr?(expr)
|
|
359
|
+
return false unless expr
|
|
360
|
+
return true if expr.fetch("kind") == "field_access" && expr.fetch("field") == "confidence_label"
|
|
361
|
+
|
|
362
|
+
false
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def evidence_gate_oofs(contract, sample_input)
|
|
366
|
+
return [] unless evidence_alert_contract?(contract)
|
|
367
|
+
|
|
368
|
+
alert = sample_input.fetch("alert", {})
|
|
369
|
+
diagnostics = []
|
|
370
|
+
if alert.fetch("signal_count", 0) < 1 || alert.fetch("claim_count", 0) < 1
|
|
371
|
+
diagnostics << oof(
|
|
372
|
+
"OOF-OS2",
|
|
373
|
+
"EvidenceLinkedAlert requires non-empty signal_refs and claim_refs",
|
|
374
|
+
contract.fetch("name")
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
diagnostics
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def evidence_alert_contract?(contract)
|
|
381
|
+
contract.fetch("body").any? do |node|
|
|
382
|
+
node.fetch("kind") == "input" &&
|
|
383
|
+
normalize_type(node.fetch("type_annotation")) == "EvidenceLinkedAlertInput"
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def normalize_type(type)
|
|
388
|
+
type.is_a?(Hash) ? type.fetch("name") : type.to_s
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def normalized_type_annotation(type)
|
|
392
|
+
return type unless type.is_a?(Hash)
|
|
393
|
+
|
|
394
|
+
type
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def temporal_type?(type)
|
|
398
|
+
%w[History BiHistory].include?(normalize_type(type))
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def oof(rule, message, node_name)
|
|
402
|
+
{ "rule" => rule, "message" => message, "node" => node_name, "line" => nil }
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
require_relative "../igniter_lang"
|
|
7
|
+
|
|
8
|
+
module IgniterLang
|
|
9
|
+
module CLI
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
USAGE = "Usage: igc compile SOURCE --out OUT.igapp " \
|
|
13
|
+
"[--compiler-profile-source PATH.json]"
|
|
14
|
+
|
|
15
|
+
def run(argv)
|
|
16
|
+
command = argv.shift
|
|
17
|
+
unless command == "compile"
|
|
18
|
+
warn USAGE
|
|
19
|
+
return false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
source_path, out_path, profile_source_path = parse_compile_args(argv)
|
|
23
|
+
compiler_profile_source = load_profile_source(profile_source_path) if profile_source_path
|
|
24
|
+
orchestration = IgniterLang.compile(
|
|
25
|
+
source_path: source_path,
|
|
26
|
+
out_path: out_path,
|
|
27
|
+
compiler_profile_source: compiler_profile_source
|
|
28
|
+
)
|
|
29
|
+
puts JSON.pretty_generate(CompilerResult.public_result(orchestration.fetch("result")))
|
|
30
|
+
orchestration.fetch("status") == "ok"
|
|
31
|
+
rescue ArgumentError => e
|
|
32
|
+
warn e.message
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_compile_args(argv)
|
|
37
|
+
source = argv.shift
|
|
38
|
+
raise ArgumentError, USAGE unless source
|
|
39
|
+
|
|
40
|
+
out_flag = argv.shift
|
|
41
|
+
out = argv.shift
|
|
42
|
+
raise ArgumentError, USAGE unless out_flag == "--out" && out
|
|
43
|
+
|
|
44
|
+
profile_source_path = nil
|
|
45
|
+
until argv.empty?
|
|
46
|
+
flag = argv.shift
|
|
47
|
+
case flag
|
|
48
|
+
when "--compiler-profile-source"
|
|
49
|
+
path = argv.shift
|
|
50
|
+
raise ArgumentError, "--compiler-profile-source requires PATH.json" unless path
|
|
51
|
+
raise ArgumentError, "unsupported argument for igc compile" if profile_source_path
|
|
52
|
+
|
|
53
|
+
profile_source_path = Pathname.new(path)
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "unsupported argument for igc compile"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
[Pathname.new(source), Pathname.new(out), profile_source_path]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def load_profile_source(path)
|
|
63
|
+
raise ArgumentError, "compiler profile source path not found" unless path.exist?
|
|
64
|
+
raise ArgumentError, "compiler profile source path must be a regular file" unless path.file?
|
|
65
|
+
|
|
66
|
+
parsed = JSON.parse(path.read)
|
|
67
|
+
raise ArgumentError, "compiler profile source JSON must be an object" unless parsed.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
parsed
|
|
70
|
+
rescue Errno::EACCES
|
|
71
|
+
raise ArgumentError, "compiler profile source path is not readable"
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
raise ArgumentError, "compiler profile source file must contain valid JSON"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "diagnostics"
|
|
4
|
+
|
|
5
|
+
module IgniterLang
|
|
6
|
+
module CompilationReport
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def parse_failure(format_version:, parsed:, source_path:)
|
|
10
|
+
{
|
|
11
|
+
"kind" => "compilation_report",
|
|
12
|
+
"format_version" => format_version,
|
|
13
|
+
"program_id" => "compilation_report/parse_error",
|
|
14
|
+
"grammar_version" => parsed.fetch("grammar_version"),
|
|
15
|
+
"source_hash" => parsed.fetch("source_hash"),
|
|
16
|
+
"source_path" => source_path.to_s,
|
|
17
|
+
"pass_result" => "error",
|
|
18
|
+
"stages" => {
|
|
19
|
+
"parse" => "error",
|
|
20
|
+
"classify" => "skipped",
|
|
21
|
+
"typecheck" => "skipped",
|
|
22
|
+
"emit" => "skipped"
|
|
23
|
+
},
|
|
24
|
+
"diagnostics" => Diagnostics.from_parse_errors(parsed.fetch("parse_errors")),
|
|
25
|
+
"semantic_ir_ref" => nil
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def runtime_smoke_failure(report:, smoke:, source_path:)
|
|
30
|
+
report.merge(
|
|
31
|
+
"pass_result" => "error",
|
|
32
|
+
"source_path" => source_path.to_s,
|
|
33
|
+
"diagnostics" => report.fetch("diagnostics", []) + Diagnostics.from_runtime_smoke(smoke)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def internal_error(format_version:, source_path:, rule:, error:)
|
|
38
|
+
{
|
|
39
|
+
"kind" => "compilation_report",
|
|
40
|
+
"format_version" => format_version,
|
|
41
|
+
"program_id" => "compilation_report/#{rule}",
|
|
42
|
+
"grammar_version" => "unknown",
|
|
43
|
+
"source_hash" => nil,
|
|
44
|
+
"source_path" => source_path.to_s,
|
|
45
|
+
"pass_result" => "error",
|
|
46
|
+
"stages" => {
|
|
47
|
+
"parse" => "unknown",
|
|
48
|
+
"classify" => "unknown",
|
|
49
|
+
"typecheck" => "unknown",
|
|
50
|
+
"emit" => "unknown"
|
|
51
|
+
},
|
|
52
|
+
"diagnostics" => internal_error_diagnostics(rule, error),
|
|
53
|
+
"semantic_ir_ref" => nil
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def enrich(report:, parsed:)
|
|
58
|
+
contract_name = parsed.fetch("contracts", []).fetch(0, {}).fetch("name", nil)
|
|
59
|
+
report.merge(
|
|
60
|
+
"diagnostics" => Diagnostics.enrich(
|
|
61
|
+
report.fetch("diagnostics", []),
|
|
62
|
+
category: diagnostic_category_for(report),
|
|
63
|
+
contract: contract_name
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def with_compiler_profile_contract_validation(report:, validation:)
|
|
69
|
+
return report unless validation
|
|
70
|
+
|
|
71
|
+
report.merge(
|
|
72
|
+
"compiler_profile_contract_validation" => validation.merge("report_only" => true)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def diagnostic_category_for(report)
|
|
77
|
+
stages = report.fetch("stages", {})
|
|
78
|
+
return "typechecker_oof" if stages.fetch("typecheck", nil) == "oof"
|
|
79
|
+
return "emitter_error" if stages.fetch("emit", nil) == "error"
|
|
80
|
+
|
|
81
|
+
"classifier_oof"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def internal_error_diagnostics(rule, error)
|
|
85
|
+
return Diagnostics.from_assembler_refusal(error) if rule == "assembler_refused"
|
|
86
|
+
|
|
87
|
+
Diagnostics.enrich(
|
|
88
|
+
[
|
|
89
|
+
{
|
|
90
|
+
"rule" => rule,
|
|
91
|
+
"severity" => "error",
|
|
92
|
+
"message" => "#{error.class}: #{error.message}"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
category: "emitter_error"
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|