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,717 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
module IgniterLang
|
|
9
|
+
class AssemblyRefused < StandardError; end
|
|
10
|
+
|
|
11
|
+
class Assembler
|
|
12
|
+
ROOT = Pathname.new(File.expand_path("../..", __dir__))
|
|
13
|
+
DEFAULT_GOLDEN_DIR = ROOT / "experiments/source_to_semanticir_fixture/golden"
|
|
14
|
+
DEFAULT_OUT_DIR = ROOT / "experiments/igapp_assembler_proof/out"
|
|
15
|
+
|
|
16
|
+
# PROP-036: compiler_profile_id source contract constants.
|
|
17
|
+
# Used by validate_compiler_profile_source! only.
|
|
18
|
+
PROFILE_SOURCE_KIND = "compiler_profile_id_source"
|
|
19
|
+
PROFILE_SOURCE_NAMESPACE = "compiler_profile_unified"
|
|
20
|
+
PROFILE_SOURCE_ID_PATTERN = /\Acompiler_profile_unified\/sha256:[0-9a-f]{24,}\z/
|
|
21
|
+
PROFILE_SOURCE_SLOT_ORDER = %w[
|
|
22
|
+
core oof_registry fragment_registry escape_boundary contract_modifiers
|
|
23
|
+
temporal stream olap invariant assumptions evidence_observation pipeline
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
module Canonical
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def normalize(value)
|
|
30
|
+
case value
|
|
31
|
+
when Hash
|
|
32
|
+
value.keys.sort_by(&:to_s).each_with_object({}) do |key, out|
|
|
33
|
+
out[key.to_s] = normalize(value[key])
|
|
34
|
+
end
|
|
35
|
+
when Array
|
|
36
|
+
value.map { |item| normalize(item) }
|
|
37
|
+
when Symbol
|
|
38
|
+
value.to_s
|
|
39
|
+
else
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def json(value)
|
|
45
|
+
JSON.pretty_generate(normalize(value)) + "\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def hash(value)
|
|
49
|
+
"sha256:#{Digest::SHA256.hexdigest(JSON.generate(normalize(value)))}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def short_hash(value)
|
|
53
|
+
hash(value).split(":").last[0, 16]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def initialize(golden_dir: DEFAULT_GOLDEN_DIR, out_dir: DEFAULT_OUT_DIR)
|
|
58
|
+
@golden_dir = Pathname.new(golden_dir)
|
|
59
|
+
@out_dir = Pathname.new(out_dir)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def assemble_case(case_name, compiler_profile_source: nil)
|
|
63
|
+
report = read_json(@golden_dir / "#{case_name}.compilation_report.json")
|
|
64
|
+
refuse!(case_name, "pass_result=#{report.fetch("pass_result")}") unless report.fetch("pass_result") == "ok"
|
|
65
|
+
refuse!(case_name, "semantic_ir_ref missing") unless report.fetch("semantic_ir_ref").is_a?(String)
|
|
66
|
+
|
|
67
|
+
semantic_ir = read_json(@golden_dir / "#{case_name}.semantic_ir.json")
|
|
68
|
+
validate_refs!(case_name, report, semantic_ir)
|
|
69
|
+
validate_semantic_ir!(case_name, semantic_ir)
|
|
70
|
+
|
|
71
|
+
artifact = build_artifact(case_name, report, semantic_ir, compiler_profile_source: compiler_profile_source)
|
|
72
|
+
write_artifact(case_name, artifact)
|
|
73
|
+
artifact_summary(case_name, artifact)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def assemble_artifacts(case_name:, report:, semantic_ir:, target_dir:, compiler_profile_source: nil)
|
|
77
|
+
refuse!(case_name, "pass_result=#{report.fetch("pass_result")}") unless report.fetch("pass_result") == "ok"
|
|
78
|
+
refuse!(case_name, "semantic_ir_ref missing") unless report.fetch("semantic_ir_ref").is_a?(String)
|
|
79
|
+
validate_refs!(case_name, report, semantic_ir)
|
|
80
|
+
validate_semantic_ir!(case_name, semantic_ir)
|
|
81
|
+
|
|
82
|
+
artifact = build_artifact(case_name, report, semantic_ir, compiler_profile_source: compiler_profile_source)
|
|
83
|
+
target = Pathname.new(target_dir)
|
|
84
|
+
write_artifact_to(target, artifact)
|
|
85
|
+
artifact_summary_for_target(case_name, artifact, target)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def refuse_case(case_name)
|
|
89
|
+
report = read_json(@golden_dir / "#{case_name}.compilation_report.json")
|
|
90
|
+
assemble_case(case_name)
|
|
91
|
+
raise "expected #{case_name} to refuse, but it assembled"
|
|
92
|
+
rescue AssemblyRefused => e
|
|
93
|
+
target = @out_dir / "#{case_name}.igapp"
|
|
94
|
+
{
|
|
95
|
+
"case" => case_name,
|
|
96
|
+
"status" => "refused",
|
|
97
|
+
"pass_result" => report.fetch("pass_result"),
|
|
98
|
+
"reason" => e.message,
|
|
99
|
+
"wrote_igapp" => target.exist?
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def read_json(path)
|
|
106
|
+
JSON.parse(File.read(path))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def refuse!(case_name, reason)
|
|
110
|
+
raise AssemblyRefused, "#{case_name}: #{reason}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# PROP-036: validate a compiler_profile_id_source object before assembler use.
|
|
114
|
+
# Raises AssemblyRefused with compiler_profile_source.* reason text on any
|
|
115
|
+
# invalid input. Must not emit loader/report status values.
|
|
116
|
+
def validate_compiler_profile_source!(case_name, source)
|
|
117
|
+
unless source.is_a?(Hash)
|
|
118
|
+
refuse!(case_name, "compiler_profile_source.malformed: source must be a Hash")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
kind = source["kind"]
|
|
122
|
+
unless kind == PROFILE_SOURCE_KIND
|
|
123
|
+
refuse!(case_name, "compiler_profile_source.wrong_kind: #{kind.inspect}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
status = source["status"]
|
|
127
|
+
unless status == "finalized"
|
|
128
|
+
refuse!(case_name, "compiler_profile_source.unfinalized: status=#{status.inspect}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
namespace = source["profile_namespace"]
|
|
132
|
+
unless namespace == PROFILE_SOURCE_NAMESPACE
|
|
133
|
+
refuse!(case_name, "compiler_profile_source.unsupported_namespace: #{namespace.inspect}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
cid = source["compiler_profile_id"]
|
|
137
|
+
unless cid.is_a?(String) && cid.match?(PROFILE_SOURCE_ID_PATTERN)
|
|
138
|
+
refuse!(case_name, "compiler_profile_source.malformed_id: #{cid.inspect}")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
slot_order = source["slot_order"]
|
|
142
|
+
unless slot_order == PROFILE_SOURCE_SLOT_ORDER
|
|
143
|
+
refuse!(case_name, "compiler_profile_source.slot_order_mismatch")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Reconstruct finalization payload and verify digest + id consistency.
|
|
147
|
+
payload = {
|
|
148
|
+
"profile_namespace" => source["profile_namespace"],
|
|
149
|
+
"format_version" => source["format_version"],
|
|
150
|
+
"descriptor_digest" => source["descriptor_digest"],
|
|
151
|
+
"profile_kind" => source["profile_kind"],
|
|
152
|
+
"slot_order" => source["slot_order"],
|
|
153
|
+
"slot_assignments" => source.fetch("slot_assignments", {})
|
|
154
|
+
}
|
|
155
|
+
payload_hex = Digest::SHA256.hexdigest(JSON.generate(Canonical.normalize(payload)))
|
|
156
|
+
expected_payload_digest = "sha256:#{payload_hex}"
|
|
157
|
+
expected_profile_id = "#{PROFILE_SOURCE_NAMESPACE}/sha256:#{payload_hex[0, 24]}"
|
|
158
|
+
|
|
159
|
+
unless source["finalization_payload_digest"] == expected_payload_digest
|
|
160
|
+
refuse!(case_name, "compiler_profile_source.id_digest_mismatch: finalization_payload_digest")
|
|
161
|
+
end
|
|
162
|
+
unless cid == expected_profile_id
|
|
163
|
+
refuse!(case_name, "compiler_profile_source.id_digest_mismatch: compiler_profile_id")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if source["runtime_authority_granted"] == true
|
|
167
|
+
refuse!(case_name, "compiler_profile_source.runtime_authority_forbidden")
|
|
168
|
+
end
|
|
169
|
+
if source["dispatch_migration_authorized"] == true
|
|
170
|
+
refuse!(case_name, "compiler_profile_source.dispatch_migration_forbidden")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_refs!(case_name, report, semantic_ir)
|
|
175
|
+
refuse!(case_name, "SemanticIR kind=#{semantic_ir.fetch("kind", nil)}") unless semantic_ir.fetch("kind") == "semantic_ir_program"
|
|
176
|
+
refuse!(case_name, "semantic_ir_ref mismatch") unless report.fetch("semantic_ir_ref") == semantic_ir.fetch("program_id")
|
|
177
|
+
return if semantic_ir.fetch("compilation_report_ref") == report.fetch("program_id")
|
|
178
|
+
|
|
179
|
+
refuse!(case_name, "compilation_report_ref mismatch")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_semantic_ir!(case_name, semantic_ir)
|
|
183
|
+
semantic_ir.fetch("contracts").each do |contract|
|
|
184
|
+
refuse!(case_name, "OOF contract emitted: #{contract.fetch("contract_name")}") if contract.fetch("fragment_class") == "oof"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
return unless JSON.generate(semantic_ir).include?("stdlib.numeric.")
|
|
188
|
+
|
|
189
|
+
refuse!(case_name, "unresolved stdlib.numeric operator in SemanticIR")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_artifact(case_name, report, semantic_ir, compiler_profile_source: nil)
|
|
193
|
+
# PROP-036: validate source object and extract id before building hash material.
|
|
194
|
+
# validate_compiler_profile_source! raises AssemblyRefused on any invalid input.
|
|
195
|
+
compiler_profile_id = if compiler_profile_source
|
|
196
|
+
validate_compiler_profile_source!(case_name, compiler_profile_source)
|
|
197
|
+
compiler_profile_source.fetch("compiler_profile_id")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
contracts = semantic_ir.fetch("contracts").map { |contract| contract_file(contract) }
|
|
201
|
+
contract_ids = contracts.map { |contract| contract.fetch("contract_id") }.sort
|
|
202
|
+
fragment_classes = contracts.map { |contract| contract.fetch("fragment_class") }.uniq
|
|
203
|
+
fragment_class = fragment_classes.length == 1 ? fragment_classes.first : "mixed"
|
|
204
|
+
requirements = requirements_for(semantic_ir)
|
|
205
|
+
classified_ast = classified_ast_for(report, semantic_ir, contract_ids, fragment_class)
|
|
206
|
+
diagnostics = { "diagnostics" => report.fetch("diagnostics") }
|
|
207
|
+
compatibility_metadata = compatibility_metadata_for(report, semantic_ir)
|
|
208
|
+
|
|
209
|
+
artifact_material = {
|
|
210
|
+
"semantic_ir_program" => semantic_ir,
|
|
211
|
+
"contracts" => contracts,
|
|
212
|
+
"compilation_report" => report,
|
|
213
|
+
"requirements" => requirements,
|
|
214
|
+
"diagnostics" => diagnostics,
|
|
215
|
+
"classified_ast" => classified_ast,
|
|
216
|
+
"compatibility_metadata" => compatibility_metadata
|
|
217
|
+
}
|
|
218
|
+
# PROP-036: inject compiler_profile_id into hash material BEFORE artifact_hash
|
|
219
|
+
# is computed. Adding it after Canonical.hash is called is forbidden.
|
|
220
|
+
artifact_material["compiler_profile_id"] = compiler_profile_id if compiler_profile_id
|
|
221
|
+
|
|
222
|
+
artifact_hash = Canonical.hash(artifact_material)
|
|
223
|
+
contracts = contracts.map { |contract| contract.merge("artifact_hash" => artifact_hash) }
|
|
224
|
+
fragment_summary = fragment_summary_for(contracts)
|
|
225
|
+
contract_index = contract_index_for(contracts)
|
|
226
|
+
|
|
227
|
+
manifest = {
|
|
228
|
+
"kind" => "igapp_manifest",
|
|
229
|
+
"format_version" => "0.1.0",
|
|
230
|
+
"format" => "igapp_dir",
|
|
231
|
+
"program_id" => semantic_ir.fetch("program_id"),
|
|
232
|
+
"artifact_hash" => artifact_hash,
|
|
233
|
+
"language_version" => semantic_ir.fetch("format_version"),
|
|
234
|
+
"grammar_version" => semantic_ir.fetch("grammar_version"),
|
|
235
|
+
"schema_version" => "0.1.0",
|
|
236
|
+
"compiled_at" => "2026-05-06T00:00:00Z",
|
|
237
|
+
"assembler" => "igapp-assembler-proof-stage1-v0",
|
|
238
|
+
"semantic_ir_ref" => report.fetch("semantic_ir_ref"),
|
|
239
|
+
"compilation_report_ref" => semantic_ir.fetch("compilation_report_ref"),
|
|
240
|
+
"source_hash" => semantic_ir.fetch("source_hash"),
|
|
241
|
+
"source_path" => semantic_ir.fetch("source_path"),
|
|
242
|
+
"contracts" => contract_ids,
|
|
243
|
+
"contract_refs" => semantic_ir.fetch("contracts").to_h do |contract|
|
|
244
|
+
[contract.fetch("contract_name"), contract.fetch("contract_ref")]
|
|
245
|
+
end,
|
|
246
|
+
"fragment_class" => fragment_class,
|
|
247
|
+
"fragment_summary" => fragment_summary,
|
|
248
|
+
"contract_index" => contract_index,
|
|
249
|
+
"schema_descriptor" => { "trait_bounds" => [], "migrations" => [] },
|
|
250
|
+
"warnings" => [],
|
|
251
|
+
"diagnostics" => report.fetch("diagnostics")
|
|
252
|
+
}
|
|
253
|
+
# PROP-036: top-level manifest field; only present when a valid source is supplied.
|
|
254
|
+
manifest["compiler_profile_id"] = compiler_profile_id if compiler_profile_id
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
"case" => case_name,
|
|
258
|
+
"manifest" => manifest,
|
|
259
|
+
"semantic_ir_program" => semantic_ir,
|
|
260
|
+
"contracts" => contracts,
|
|
261
|
+
"compilation_report" => report,
|
|
262
|
+
"requirements" => requirements,
|
|
263
|
+
"diagnostics" => diagnostics,
|
|
264
|
+
"classified_ast" => classified_ast,
|
|
265
|
+
"projections" => { "projections" => [] },
|
|
266
|
+
"compatibility_metadata" => compatibility_metadata
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def contract_file(contract_ir)
|
|
271
|
+
contract_id = contract_ir.fetch("contract_name")
|
|
272
|
+
input_ports = ports(contract_ir.fetch("inputs"))
|
|
273
|
+
output_ports = ports(contract_ir.fetch("outputs"))
|
|
274
|
+
semantic_nodes = contract_ir.fetch("nodes")
|
|
275
|
+
compute_nodes = semantic_nodes.filter_map do |node|
|
|
276
|
+
next unless compute_node?(node)
|
|
277
|
+
|
|
278
|
+
{
|
|
279
|
+
"node_id" => "node_#{node.fetch("name")}",
|
|
280
|
+
"name" => node.fetch("name"),
|
|
281
|
+
"kind" => node.fetch("kind"),
|
|
282
|
+
"fragment_class" => node.fetch("fragment"),
|
|
283
|
+
"type_tag" => type_name(node.fetch("type")),
|
|
284
|
+
"lifecycle" => "session",
|
|
285
|
+
"obs_kind" => "value_observation",
|
|
286
|
+
"dependencies" => node.fetch("deps").map { |dep| "input:#{dep}" },
|
|
287
|
+
"expression" => compat_expr(node.fetch("expr"))
|
|
288
|
+
}
|
|
289
|
+
end
|
|
290
|
+
temporal_nodes = semantic_nodes.filter_map do |node|
|
|
291
|
+
next unless temporal_node?(node)
|
|
292
|
+
|
|
293
|
+
temporal_node_file(node)
|
|
294
|
+
end
|
|
295
|
+
stream_nodes = semantic_nodes.filter_map do |node|
|
|
296
|
+
next unless stream_node?(node)
|
|
297
|
+
|
|
298
|
+
stream_node_file(node)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
result = {
|
|
302
|
+
"contract_id" => contract_id,
|
|
303
|
+
"source_contract_ref" => contract_ir.fetch("contract_ref"),
|
|
304
|
+
"name" => contract_id,
|
|
305
|
+
"fragment_class" => contract_ir.fetch("fragment_class"),
|
|
306
|
+
"escape_set" => contract_ir.fetch("escape_boundaries"),
|
|
307
|
+
"lifecycle" => "session",
|
|
308
|
+
"input_ports" => input_ports,
|
|
309
|
+
"output_ports" => output_ports,
|
|
310
|
+
"compute_nodes" => compute_nodes,
|
|
311
|
+
"type_signature" => {
|
|
312
|
+
"inputs" => input_ports.to_h { |port| [port.fetch("name"), port.fetch("type_tag")] },
|
|
313
|
+
"outputs" => output_ports.to_h { |port| [port.fetch("name"), port.fetch("type_tag")] }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
result["temporal_nodes"] = temporal_nodes unless temporal_nodes.empty?
|
|
317
|
+
result["stream_nodes"] = stream_nodes unless stream_nodes.empty?
|
|
318
|
+
result
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def compute_node?(node)
|
|
322
|
+
node.key?("expr") && node.key?("type")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def temporal_node?(node)
|
|
326
|
+
%w[temporal_input_node temporal_access_node].include?(node.fetch("kind", nil))
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def stream_node?(node)
|
|
330
|
+
%w[stream_input_node window_decl_node fold_stream_node].include?(node.fetch("kind", nil))
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def temporal_node_file(node)
|
|
334
|
+
result = {
|
|
335
|
+
"node_id" => "node_#{node.fetch("name")}",
|
|
336
|
+
"name" => node.fetch("name"),
|
|
337
|
+
"kind" => node.fetch("kind"),
|
|
338
|
+
"fragment_class" => node.fetch("fragment", node.fetch("node_fragment_class", "temporal")),
|
|
339
|
+
"node_fragment_class" => node.fetch("node_fragment_class"),
|
|
340
|
+
"value_fragment_class" => node.fetch("value_fragment_class"),
|
|
341
|
+
"lifecycle" => node.fetch("lifecycle", "session"),
|
|
342
|
+
"obs_kind" => temporal_obs_kind(node),
|
|
343
|
+
"dependencies" => node.fetch("deps", []).map { |dep| "input:#{dep}" },
|
|
344
|
+
"required_capability" => node.fetch("required_capability"),
|
|
345
|
+
"required_caps" => node.fetch("required_caps", [node.fetch("required_capability")]),
|
|
346
|
+
"axis" => node.fetch("axis", node.fetch("temporal_axis", nil))
|
|
347
|
+
}
|
|
348
|
+
result["type_tag"] = type_name(node.fetch("type")) if node.key?("type")
|
|
349
|
+
result["result_type_tag"] = type_name(node.fetch("result_type")) if node.key?("result_type")
|
|
350
|
+
result["store_ref"] = node.fetch("store_ref") if node.key?("store_ref")
|
|
351
|
+
result["source_ref"] = node.fetch("source_ref") if node.key?("source_ref")
|
|
352
|
+
result["temporal_axis"] = node.fetch("temporal_axis") if node.key?("temporal_axis")
|
|
353
|
+
result["coordinate_refs"] = node.fetch("coordinate_refs") if node.key?("coordinate_refs")
|
|
354
|
+
result["as_of_ref"] = node.fetch("as_of_ref") if node.key?("as_of_ref")
|
|
355
|
+
result["valid_time_ref"] = node.fetch("valid_time_ref") if node.key?("valid_time_ref")
|
|
356
|
+
result["transaction_time_ref"] = node.fetch("transaction_time_ref") if node.key?("transaction_time_ref")
|
|
357
|
+
result["evidence_policy"] = node.fetch("evidence_policy") if node.key?("evidence_policy")
|
|
358
|
+
result
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def temporal_obs_kind(node)
|
|
362
|
+
node.fetch("kind") == "temporal_input_node" ? "temporal_source_observation" : "temporal_access_observation"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def stream_node_file(node)
|
|
366
|
+
name = node.fetch("name", node.fetch("ref", nil))
|
|
367
|
+
result = {
|
|
368
|
+
"node_id" => "node_#{name}",
|
|
369
|
+
"name" => name,
|
|
370
|
+
"kind" => node.fetch("kind"),
|
|
371
|
+
"fragment_class" => node.fetch("fragment", node.fetch("result_fragment", "stream")),
|
|
372
|
+
"lifecycle" => "window",
|
|
373
|
+
"obs_kind" => stream_obs_kind(node),
|
|
374
|
+
"dependencies" => node.fetch("deps", []).map { |dep| "input:#{dep}" }
|
|
375
|
+
}
|
|
376
|
+
result["type_tag"] = type_name(node.fetch("type")) if node.key?("type")
|
|
377
|
+
result["result_type_tag"] = type_name(node.fetch("result_type")) if node.key?("result_type")
|
|
378
|
+
%w[
|
|
379
|
+
window_ref ref key window_kind bounded size period idle on_close stream_ref init fn_ref
|
|
380
|
+
bound event_binding escape_capability result_fragment
|
|
381
|
+
].each do |key|
|
|
382
|
+
result[key] = node.fetch(key) if node.key?(key)
|
|
383
|
+
end
|
|
384
|
+
result
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def stream_obs_kind(node)
|
|
388
|
+
node.fetch("kind") == "window_decl_node" ? "stream_window_observation" : "stream_replay_metadata"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def fragment_summary_for(contracts)
|
|
392
|
+
fragment_classes = contracts.map { |contract| contract.fetch("fragment_class") }.uniq.sort
|
|
393
|
+
{
|
|
394
|
+
"fragment_classes" => fragment_classes,
|
|
395
|
+
"max_fragment_class" => max_fragment_class(fragment_classes),
|
|
396
|
+
"precedence_high_to_low" => fragment_precedence
|
|
397
|
+
}
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def max_fragment_class(fragment_classes)
|
|
401
|
+
fragment_precedence.find { |fragment| fragment_classes.include?(fragment) } || "core"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def fragment_precedence
|
|
405
|
+
%w[oof temporal stream escape core]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def contract_index_for(contracts)
|
|
409
|
+
contracts.sort_by { |contract| contract.fetch("contract_id") }.to_h do |contract|
|
|
410
|
+
entry = {
|
|
411
|
+
"contract_ref" => contract.fetch("source_contract_ref"),
|
|
412
|
+
"contract_path" => "contracts/#{snake_case(contract.fetch("contract_id"))}.json",
|
|
413
|
+
"fragment_class" => contract.fetch("fragment_class")
|
|
414
|
+
}
|
|
415
|
+
entry["temporal"] = temporal_contract_index(contract) if contract.fetch("fragment_class") == "temporal"
|
|
416
|
+
[contract.fetch("contract_id"), entry]
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def temporal_contract_index(contract)
|
|
421
|
+
temporal_nodes = contract.fetch("temporal_nodes", [])
|
|
422
|
+
access_nodes = temporal_nodes.select { |node| node.fetch("kind") == "temporal_access_node" }
|
|
423
|
+
coordinates = access_nodes.flat_map { |node| temporal_coordinates_for(contract, node) }
|
|
424
|
+
axes = coordinates.map { |coordinate| coordinate.fetch("axis") }.uniq
|
|
425
|
+
required_caps = (
|
|
426
|
+
contract.fetch("escape_set", []).flat_map { |boundary| boundary.fetch("required_caps", []) } +
|
|
427
|
+
temporal_nodes.flat_map { |node| node.fetch("required_caps", []) }
|
|
428
|
+
).uniq.sort
|
|
429
|
+
hint_axis = access_nodes.map { |node| node.fetch("axis", node.fetch("temporal_axis", nil)) }.compact.uniq
|
|
430
|
+
{
|
|
431
|
+
"axes" => axes.sort_by { |axis| temporal_axis_sort_key(axis) },
|
|
432
|
+
"required_capabilities" => required_caps,
|
|
433
|
+
"coordinates" => coordinates,
|
|
434
|
+
"cache_key_schema_hint" => {
|
|
435
|
+
"schema" => "runtime-cache-key-v1",
|
|
436
|
+
"fragment" => "TEMPORAL",
|
|
437
|
+
"axis" => hint_axis.length == 1 ? hint_axis.first : "mixed",
|
|
438
|
+
"coordinate_names" => coordinates.map { |coord| coord.fetch("name") }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def temporal_coordinates_for(contract, access_node)
|
|
444
|
+
coordinate_refs = access_node.fetch("coordinate_refs", {})
|
|
445
|
+
coordinate_refs.map do |axis_name, input_name|
|
|
446
|
+
{
|
|
447
|
+
"name" => input_name,
|
|
448
|
+
"axis" => coordinate_axis(access_node, axis_name),
|
|
449
|
+
"source_ref" => "input:#{input_name}",
|
|
450
|
+
"type" => input_type(contract, input_name)
|
|
451
|
+
}
|
|
452
|
+
end.sort_by { |coordinate| temporal_axis_sort_key(coordinate.fetch("axis")) }
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def coordinate_axis(access_node, axis_name)
|
|
456
|
+
access_axis = access_node.fetch("axis", access_node.fetch("temporal_axis", nil))
|
|
457
|
+
access_axis == "bitemporal" ? axis_name : access_axis
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def temporal_axis_sort_key(axis)
|
|
461
|
+
%w[valid_time transaction_time bitemporal].index(axis) || 99
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def input_type(contract, input_name)
|
|
465
|
+
port = contract.fetch("input_ports").find { |input| input.fetch("name") == input_name }
|
|
466
|
+
port ? port.fetch("type_tag") : "Unknown"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def ports(port_irs)
|
|
470
|
+
port_irs.map do |port|
|
|
471
|
+
{
|
|
472
|
+
"name" => port.fetch("name"),
|
|
473
|
+
"type_tag" => type_name(port.fetch("type")),
|
|
474
|
+
"lifecycle" => port.fetch("lifecycle"),
|
|
475
|
+
"required" => true
|
|
476
|
+
}
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def compat_expr(expr)
|
|
481
|
+
case expr.fetch("kind")
|
|
482
|
+
when "call"
|
|
483
|
+
{
|
|
484
|
+
"kind" => "apply",
|
|
485
|
+
"operator" => expr.fetch("fn"),
|
|
486
|
+
"operands" => expr.fetch("args").map { |arg| compat_expr(arg) }
|
|
487
|
+
}
|
|
488
|
+
when "ref"
|
|
489
|
+
{ "kind" => "ref", "name" => expr.fetch("name") }
|
|
490
|
+
when "literal"
|
|
491
|
+
{ "kind" => "literal", "value" => expr.fetch("value"), "type_tag" => type_name(expr.fetch("resolved_type")) }
|
|
492
|
+
when "field_access"
|
|
493
|
+
{
|
|
494
|
+
"kind" => "field_access",
|
|
495
|
+
"object" => compat_expr(expr.fetch("object")),
|
|
496
|
+
"field" => expr.fetch("field"),
|
|
497
|
+
"type_tag" => type_name(expr.fetch("resolved_type"))
|
|
498
|
+
}
|
|
499
|
+
else
|
|
500
|
+
expr
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def type_name(type)
|
|
505
|
+
return type if type.is_a?(String)
|
|
506
|
+
return type.to_s unless type.is_a?(Hash)
|
|
507
|
+
|
|
508
|
+
if type.key?("constructor")
|
|
509
|
+
element = type.fetch("element_type", nil)
|
|
510
|
+
return element ? "#{type.fetch("constructor")}[#{type_name(element)}]" : type.fetch("constructor")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
params = type.fetch("params", [])
|
|
514
|
+
return type.fetch("name") if params.empty?
|
|
515
|
+
|
|
516
|
+
"#{type.fetch("name")}[#{params.map { |param| type_name(param) }.join(",")}]"
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def requirements_for(semantic_ir)
|
|
520
|
+
boundaries = semantic_ir.fetch("contracts").flat_map { |contract| contract.fetch("escape_boundaries", []) }
|
|
521
|
+
required_caps = boundaries.flat_map { |boundary| boundary.fetch("required_caps", []) }.uniq.sort
|
|
522
|
+
fragments = semantic_ir.fetch("contracts").map { |contract| contract.fetch("fragment_class") }.uniq.sort
|
|
523
|
+
temporal_nodes = semantic_ir.fetch("contracts").flat_map { |contract| contract.fetch("nodes", []) }
|
|
524
|
+
.select { |node| temporal_node?(node) }
|
|
525
|
+
temporal_access_nodes = temporal_nodes.select { |node| node.fetch("kind") == "temporal_access_node" }
|
|
526
|
+
temporal_axes = temporal_nodes.map { |node| node.fetch("axis", node.fetch("temporal_axis", nil)) }.compact.uniq.sort
|
|
527
|
+
temporal_caps = required_caps & %w[history_read bihistory_read]
|
|
528
|
+
stream_caps = required_caps & %w[stream_input]
|
|
529
|
+
stream_nodes = semantic_ir.fetch("contracts").flat_map { |contract| contract.fetch("nodes", []) }
|
|
530
|
+
.select { |node| stream_node?(node) }
|
|
531
|
+
stream_windows = stream_nodes.select { |node| node.fetch("kind") == "window_decl_node" }
|
|
532
|
+
|
|
533
|
+
{
|
|
534
|
+
"temporal" => {
|
|
535
|
+
"requires_as_of" => temporal_caps.any?,
|
|
536
|
+
"requires_valid_time" => temporal_caps.any?,
|
|
537
|
+
"requires_transaction_time" => required_caps.include?("bihistory_read"),
|
|
538
|
+
"requires_replay" => required_caps.include?("bihistory_read"),
|
|
539
|
+
"requires_snapshot" => false,
|
|
540
|
+
"min_consistency" => "strong",
|
|
541
|
+
"axes" => temporal_axes,
|
|
542
|
+
"coordinate_refs" => temporal_access_nodes.map do |node|
|
|
543
|
+
{
|
|
544
|
+
"node" => node.fetch("name"),
|
|
545
|
+
"axis" => node.fetch("axis", node.fetch("temporal_axis")),
|
|
546
|
+
"coordinates" => node.fetch("coordinate_refs", {})
|
|
547
|
+
}
|
|
548
|
+
end,
|
|
549
|
+
"windows" => stream_windows.map { |node| stream_window_requirement(node) },
|
|
550
|
+
"slices" => []
|
|
551
|
+
},
|
|
552
|
+
"lifecycle" => {
|
|
553
|
+
"min_lifecycle" => "local",
|
|
554
|
+
"has_audit" => temporal_caps.any?,
|
|
555
|
+
"has_window" => stream_caps.any?
|
|
556
|
+
},
|
|
557
|
+
"fragments" => fragments,
|
|
558
|
+
"capabilities" => {
|
|
559
|
+
"required_caps" => required_caps,
|
|
560
|
+
"effect_kinds" => effect_kinds_for(boundaries)
|
|
561
|
+
},
|
|
562
|
+
"effects" => [],
|
|
563
|
+
"ffi" => [],
|
|
564
|
+
"required_tbackend_caps" => {
|
|
565
|
+
"read_as_of" => temporal_caps.any?,
|
|
566
|
+
"append_atomic" => false,
|
|
567
|
+
"replay_enabled" => required_caps.include?("bihistory_read"),
|
|
568
|
+
"snapshot_enabled" => false,
|
|
569
|
+
"compact_enabled" => false,
|
|
570
|
+
"subscribe_enabled" => false,
|
|
571
|
+
"consistency" => "strong"
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def effect_kinds_for(boundaries)
|
|
577
|
+
boundaries.flat_map { |boundary| boundary.fetch("produces", []) }.uniq.sort
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def stream_window_requirement(node)
|
|
581
|
+
result = {
|
|
582
|
+
"ref" => node.fetch("ref"),
|
|
583
|
+
"kind" => node.fetch("window_kind", nil),
|
|
584
|
+
"bounded" => node.fetch("bounded", false)
|
|
585
|
+
}
|
|
586
|
+
%w[size period idle on_close].each do |key|
|
|
587
|
+
result[key] = node.fetch(key) if node.key?(key)
|
|
588
|
+
end
|
|
589
|
+
result.compact
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def classified_ast_for(report, semantic_ir, contract_ids, fragment_class)
|
|
593
|
+
{
|
|
594
|
+
"kind" => "classified_program",
|
|
595
|
+
"format_version" => "0.1.0",
|
|
596
|
+
"program_id" => semantic_ir.fetch("program_id"),
|
|
597
|
+
"source_hash" => semantic_ir.fetch("source_hash"),
|
|
598
|
+
"source_path" => semantic_ir.fetch("source_path"),
|
|
599
|
+
"pass_result" => report.fetch("pass_result"),
|
|
600
|
+
"semantic_ir_ref" => report.fetch("semantic_ir_ref"),
|
|
601
|
+
"compilation_report_ref" => semantic_ir.fetch("compilation_report_ref"),
|
|
602
|
+
"fragment_class" => fragment_class,
|
|
603
|
+
"oof_count" => 0,
|
|
604
|
+
"contracts" => contract_ids,
|
|
605
|
+
"loadable_contracts" => contract_ids
|
|
606
|
+
}
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def compatibility_metadata_for(report, semantic_ir)
|
|
610
|
+
metadata = {
|
|
611
|
+
"kind" => "igapp_compatibility_metadata",
|
|
612
|
+
"format_version" => "0.1.0",
|
|
613
|
+
"canonical_semantic_ir_ref" => semantic_ir.fetch("program_id"),
|
|
614
|
+
"compilation_report_ref" => report.fetch("program_id"),
|
|
615
|
+
"loader_shape" => "runtime_machine_memory_proof.prop0191_direct_v0",
|
|
616
|
+
"canonical_artifact" => "semantic_ir_program.json",
|
|
617
|
+
"runtime_compatibility_artifact" => nil,
|
|
618
|
+
"notes" => [
|
|
619
|
+
"semantic_ir_program.json preserves PROP-019.1 envelope",
|
|
620
|
+
"RuntimeMachine proof loader reads semantic_ir_program.json directly"
|
|
621
|
+
]
|
|
622
|
+
}
|
|
623
|
+
if temporal_artifact?(semantic_ir)
|
|
624
|
+
metadata["runtime_execution"] = {
|
|
625
|
+
"status" => "unsupported",
|
|
626
|
+
"guard_policy" => "load_accept_evaluate_refuse",
|
|
627
|
+
"guard_at" => "evaluate",
|
|
628
|
+
"load" => {
|
|
629
|
+
"decision" => "accept_for_inspection",
|
|
630
|
+
"requires_contract_index" => true
|
|
631
|
+
},
|
|
632
|
+
"evaluate" => {
|
|
633
|
+
"decision" => "refuse_temporal_contract",
|
|
634
|
+
"reason_code" => "runtime.temporal_execution_unsupported"
|
|
635
|
+
},
|
|
636
|
+
"reason" => "temporal SemanticIR assembly proof preserves artifact shape only; RuntimeMachine temporal execution is out of scope"
|
|
637
|
+
}
|
|
638
|
+
metadata["notes"] += [
|
|
639
|
+
"temporal_input_node and temporal_access_node are preserved as non-compute contract nodes",
|
|
640
|
+
"temporal runtime execution requires a separate RuntimeMachine temporal adapter/hook slice"
|
|
641
|
+
]
|
|
642
|
+
end
|
|
643
|
+
metadata
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def temporal_artifact?(semantic_ir)
|
|
647
|
+
semantic_ir.fetch("contracts").any? do |contract|
|
|
648
|
+
contract.fetch("nodes", []).any? { |node| temporal_node?(node) }
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def write_artifact(case_name, artifact)
|
|
653
|
+
target = @out_dir / "#{case_name}.igapp"
|
|
654
|
+
write_artifact_to(target, artifact)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def write_artifact_to(target, artifact)
|
|
658
|
+
FileUtils.rm_rf(target)
|
|
659
|
+
FileUtils.mkdir_p(target / "contracts")
|
|
660
|
+
|
|
661
|
+
write_json(target / "manifest.json", artifact.fetch("manifest"))
|
|
662
|
+
write_json(target / "semantic_ir_program.json", artifact.fetch("semantic_ir_program"))
|
|
663
|
+
write_json(target / "compilation_report.json", artifact.fetch("compilation_report"))
|
|
664
|
+
write_json(target / "requirements.json", artifact.fetch("requirements"))
|
|
665
|
+
write_json(target / "diagnostics.json", artifact.fetch("diagnostics"))
|
|
666
|
+
write_json(target / "classified_ast.json", artifact.fetch("classified_ast"))
|
|
667
|
+
write_json(target / "projections.json", artifact.fetch("projections"))
|
|
668
|
+
write_json(target / "compatibility_metadata.json", artifact.fetch("compatibility_metadata"))
|
|
669
|
+
artifact.fetch("contracts").each do |contract|
|
|
670
|
+
write_json(target / "contracts/#{snake_case(contract.fetch("contract_id"))}.json", contract)
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def write_json(path, value)
|
|
675
|
+
File.write(path, Canonical.json(value))
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def snake_case(value)
|
|
679
|
+
value.gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def artifact_summary(case_name, artifact)
|
|
683
|
+
{
|
|
684
|
+
"case" => case_name,
|
|
685
|
+
"status" => "assembled",
|
|
686
|
+
"igapp_dir" => (@out_dir / "#{case_name}.igapp").relative_path_from(ROOT).to_s,
|
|
687
|
+
"program_id" => artifact.fetch("manifest").fetch("program_id"),
|
|
688
|
+
"artifact_hash" => artifact.fetch("manifest").fetch("artifact_hash"),
|
|
689
|
+
"semantic_ir_ref" => artifact.fetch("manifest").fetch("semantic_ir_ref"),
|
|
690
|
+
"compilation_report_ref" => artifact.fetch("manifest").fetch("compilation_report_ref"),
|
|
691
|
+
"contracts" => artifact.fetch("manifest").fetch("contracts"),
|
|
692
|
+
"files" => artifact_files(case_name)
|
|
693
|
+
}
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def artifact_summary_for_target(case_name, artifact, target)
|
|
697
|
+
{
|
|
698
|
+
"case" => case_name,
|
|
699
|
+
"status" => "assembled",
|
|
700
|
+
"igapp_dir" => target.to_s,
|
|
701
|
+
"program_id" => artifact.fetch("manifest").fetch("program_id"),
|
|
702
|
+
"artifact_hash" => artifact.fetch("manifest").fetch("artifact_hash"),
|
|
703
|
+
"semantic_ir_ref" => artifact.fetch("manifest").fetch("semantic_ir_ref"),
|
|
704
|
+
"compilation_report_ref" => artifact.fetch("manifest").fetch("compilation_report_ref"),
|
|
705
|
+
"contracts" => artifact.fetch("manifest").fetch("contracts"),
|
|
706
|
+
"files" => target.find.select(&:file?).map { |path| path.relative_path_from(target).to_s }.sort
|
|
707
|
+
}
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def artifact_files(case_name)
|
|
711
|
+
target = @out_dir / "#{case_name}.igapp"
|
|
712
|
+
target.find.select(&:file?).map { |path| path.relative_path_from(target).to_s }.sort
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
IgappAssembler = Assembler
|
|
717
|
+
end
|