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,362 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
require_relative "assembler"
|
|
8
|
+
require_relative "classifier"
|
|
9
|
+
require_relative "compilation_report"
|
|
10
|
+
require_relative "compiler_profile_contract_validator"
|
|
11
|
+
require_relative "compiler_result"
|
|
12
|
+
require_relative "parser"
|
|
13
|
+
require_relative "semanticir_emitter"
|
|
14
|
+
require_relative "typechecker"
|
|
15
|
+
|
|
16
|
+
module IgniterLang
|
|
17
|
+
class CompilerOrchestrator
|
|
18
|
+
FORMAT_VERSION = SemanticIREmitter::FORMAT_VERSION
|
|
19
|
+
STRICT_REQUIREMENT_KIND = "compiler_profile_contract_strict_requirement"
|
|
20
|
+
STRICT_REQUIREMENT_MODE = "strict_contract_digest"
|
|
21
|
+
CONTRACT_DIGEST_MISMATCH_CODE =
|
|
22
|
+
"compiler_profile_contract.contract_digest_mismatch"
|
|
23
|
+
CONTRACT_DIGEST_REFUSAL_CODE =
|
|
24
|
+
"compiler_profile_contract_refusal.contract_digest_mismatch"
|
|
25
|
+
STRICT_REQUIREMENT_MALFORMED_CODE =
|
|
26
|
+
"compiler_profile_contract_refusal.strict_requirement_malformed"
|
|
27
|
+
STRICT_REQUIREMENT_SOURCES = ["proof_local_gate", "internal_test_seam"].freeze
|
|
28
|
+
|
|
29
|
+
def initialize(
|
|
30
|
+
classifier: Classifier.new,
|
|
31
|
+
typechecker: TypeChecker.new,
|
|
32
|
+
emitter: SemanticIREmitter.new,
|
|
33
|
+
assembler: Assembler.new,
|
|
34
|
+
compiler_profile_contract_provider: nil,
|
|
35
|
+
compiler_profile_contract_strict_requirement: nil
|
|
36
|
+
)
|
|
37
|
+
@classifier = classifier
|
|
38
|
+
@typechecker = typechecker
|
|
39
|
+
@emitter = emitter
|
|
40
|
+
@assembler = assembler
|
|
41
|
+
@compiler_profile_contract_provider = compiler_profile_contract_provider
|
|
42
|
+
@compiler_profile_contract_strict_requirement = compiler_profile_contract_strict_requirement
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def compile(
|
|
46
|
+
source_path:,
|
|
47
|
+
out_path:,
|
|
48
|
+
sample_input: nil,
|
|
49
|
+
sample_input_resolver: nil,
|
|
50
|
+
runtime_smoke: nil,
|
|
51
|
+
compiler_profile_source: nil
|
|
52
|
+
)
|
|
53
|
+
source_path = Pathname.new(source_path)
|
|
54
|
+
out_path = Pathname.new(out_path)
|
|
55
|
+
parsed = ParsedProgram.parse(File.read(source_path), source_path: source_path.to_s).to_h
|
|
56
|
+
return parse_failure(parsed, source_path, out_path) unless parsed.fetch("parse_errors").empty?
|
|
57
|
+
|
|
58
|
+
resolved_sample_input = sample_input || resolve_sample_input(parsed, sample_input_resolver)
|
|
59
|
+
classified = @classifier.classify(parsed, sample_input: resolved_sample_input)
|
|
60
|
+
typed = @typechecker.typecheck(classified)
|
|
61
|
+
compilation = @emitter.emit_typed(typed)
|
|
62
|
+
report = CompilationReport.enrich(
|
|
63
|
+
report: compilation.fetch("compilation_report"),
|
|
64
|
+
parsed: parsed
|
|
65
|
+
)
|
|
66
|
+
semantic_ir = compilation.fetch("semantic_ir")
|
|
67
|
+
report_for_assembly = report
|
|
68
|
+
|
|
69
|
+
if report.fetch("pass_result") == "ok"
|
|
70
|
+
validation = compiler_profile_contract_validation(
|
|
71
|
+
source_path: source_path,
|
|
72
|
+
out_path: out_path,
|
|
73
|
+
parsed_program: parsed,
|
|
74
|
+
compiler_profile_source: compiler_profile_source
|
|
75
|
+
)
|
|
76
|
+
report = CompilationReport.with_compiler_profile_contract_validation(
|
|
77
|
+
report: report,
|
|
78
|
+
validation: validation
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return refusal(report, source_path, out_path) unless report.fetch("pass_result") == "ok"
|
|
83
|
+
|
|
84
|
+
strict_terminal = compiler_profile_contract_strict_terminal(
|
|
85
|
+
report: report,
|
|
86
|
+
source_path: source_path
|
|
87
|
+
)
|
|
88
|
+
return strict_terminal if strict_terminal
|
|
89
|
+
|
|
90
|
+
# PROP-036: compiler_profile_source is passed unchanged to the assembler.
|
|
91
|
+
# The orchestrator is a transport boundary only — it does not derive, load,
|
|
92
|
+
# discover, default, finalize, or validate profiles. Assembler validation
|
|
93
|
+
# remains authoritative. Nil preserves legacy_optional behavior.
|
|
94
|
+
assembled = @assembler.assemble_artifacts(
|
|
95
|
+
case_name: case_name_for(source_path, parsed),
|
|
96
|
+
report: report_for_assembly,
|
|
97
|
+
semantic_ir: semantic_ir,
|
|
98
|
+
target_dir: out_path,
|
|
99
|
+
compiler_profile_source: compiler_profile_source
|
|
100
|
+
)
|
|
101
|
+
smoke = runtime_smoke&.call(out_path: out_path, sample_input: resolved_sample_input)
|
|
102
|
+
|
|
103
|
+
if smoke && !smoke.fetch("trusted")
|
|
104
|
+
smoke_report = CompilationReport.runtime_smoke_failure(
|
|
105
|
+
report: report,
|
|
106
|
+
smoke: smoke,
|
|
107
|
+
source_path: source_path
|
|
108
|
+
)
|
|
109
|
+
return refusal(smoke_report, source_path, out_path, status: "runtime_smoke_failed")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
"status" => "ok",
|
|
114
|
+
"result" => CompilerResult.ok(
|
|
115
|
+
format_version: FORMAT_VERSION,
|
|
116
|
+
semantic_ir: semantic_ir,
|
|
117
|
+
source_path: source_path,
|
|
118
|
+
report: report,
|
|
119
|
+
igapp_path: out_path,
|
|
120
|
+
contracts: assembled.fetch("contracts"),
|
|
121
|
+
runtime_smoke: smoke
|
|
122
|
+
),
|
|
123
|
+
"parsed_program" => parsed,
|
|
124
|
+
"classified_program" => classified,
|
|
125
|
+
"typed_program" => typed,
|
|
126
|
+
"semantic_ir" => semantic_ir,
|
|
127
|
+
"compilation_report" => report,
|
|
128
|
+
"assembled" => assembled,
|
|
129
|
+
"sample_input" => resolved_sample_input
|
|
130
|
+
}
|
|
131
|
+
rescue AssemblyRefused => e
|
|
132
|
+
report = CompilationReport.internal_error(
|
|
133
|
+
format_version: FORMAT_VERSION,
|
|
134
|
+
source_path: source_path,
|
|
135
|
+
rule: "assembler_refused",
|
|
136
|
+
error: e
|
|
137
|
+
)
|
|
138
|
+
refusal(report, source_path, out_path, status: "assembler_refused")
|
|
139
|
+
rescue => e
|
|
140
|
+
report = CompilationReport.internal_error(
|
|
141
|
+
format_version: FORMAT_VERSION,
|
|
142
|
+
source_path: source_path,
|
|
143
|
+
rule: "compiler_error",
|
|
144
|
+
error: e
|
|
145
|
+
)
|
|
146
|
+
refusal(report, source_path, out_path, status: "error")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def parse_failure(parsed, source_path, out_path)
|
|
152
|
+
report = CompilationReport.parse_failure(
|
|
153
|
+
format_version: FORMAT_VERSION,
|
|
154
|
+
parsed: parsed,
|
|
155
|
+
source_path: source_path
|
|
156
|
+
)
|
|
157
|
+
refusal(report, source_path, out_path, status: "error")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def refusal(report, source_path, out_path, status: "oof")
|
|
161
|
+
report_path = report_path_for(out_path)
|
|
162
|
+
write_json(report_path, report)
|
|
163
|
+
{
|
|
164
|
+
"status" => status,
|
|
165
|
+
"result" => CompilerResult.refusal(
|
|
166
|
+
format_version: FORMAT_VERSION,
|
|
167
|
+
status: status,
|
|
168
|
+
report: report,
|
|
169
|
+
source_path: source_path,
|
|
170
|
+
report_path: report_path
|
|
171
|
+
),
|
|
172
|
+
"compilation_report" => report,
|
|
173
|
+
"report_path" => report_path.to_s
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def report_path_for(out_path)
|
|
178
|
+
raw = out_path.to_s
|
|
179
|
+
if raw.end_with?(".igapp")
|
|
180
|
+
Pathname.new(raw.delete_suffix(".igapp") + ".compilation_report.json")
|
|
181
|
+
else
|
|
182
|
+
Pathname.new("#{raw}.compilation_report.json")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def write_json(path, value)
|
|
187
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
|
188
|
+
File.write(path, "#{JSON.pretty_generate(value)}\n")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def compiler_profile_contract_validation(source_path:, out_path:, parsed_program:, compiler_profile_source:)
|
|
192
|
+
return nil unless @compiler_profile_contract_provider.respond_to?(:call)
|
|
193
|
+
|
|
194
|
+
contract = @compiler_profile_contract_provider.call(
|
|
195
|
+
source_path: source_path,
|
|
196
|
+
out_path: out_path,
|
|
197
|
+
parsed_program: parsed_program,
|
|
198
|
+
compiler_profile_source: compiler_profile_source
|
|
199
|
+
)
|
|
200
|
+
return nil unless contract.is_a?(Hash)
|
|
201
|
+
|
|
202
|
+
CompilerProfileContractValidator.validate(contract)
|
|
203
|
+
rescue
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def compiler_profile_contract_strict_terminal(report:, source_path:)
|
|
208
|
+
return nil if @compiler_profile_contract_strict_requirement.nil?
|
|
209
|
+
|
|
210
|
+
requirement = validate_compiler_profile_contract_strict_requirement(
|
|
211
|
+
@compiler_profile_contract_strict_requirement
|
|
212
|
+
)
|
|
213
|
+
unless requirement.fetch("valid")
|
|
214
|
+
diagnostic = strict_requirement_malformed_diagnostic(
|
|
215
|
+
requirement.fetch("reason")
|
|
216
|
+
)
|
|
217
|
+
return strict_configuration_error(
|
|
218
|
+
report: report,
|
|
219
|
+
source_path: source_path,
|
|
220
|
+
diagnostic: diagnostic
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
validation = report.fetch("compiler_profile_contract_validation", nil)
|
|
225
|
+
return nil unless validation.is_a?(Hash)
|
|
226
|
+
|
|
227
|
+
diagnostic_codes = Array(validation["diagnostic_codes"])
|
|
228
|
+
return nil unless diagnostic_codes.include?(CONTRACT_DIGEST_MISMATCH_CODE)
|
|
229
|
+
|
|
230
|
+
strict_refusal(
|
|
231
|
+
report: report,
|
|
232
|
+
source_path: source_path,
|
|
233
|
+
diagnostic: contract_digest_mismatch_refusal_diagnostic
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def validate_compiler_profile_contract_strict_requirement(requirement)
|
|
238
|
+
unless requirement.is_a?(Hash)
|
|
239
|
+
return invalid_strict_requirement(
|
|
240
|
+
"expected compiler_profile_contract_strict_requirement hash"
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
unless requirement["kind"] == STRICT_REQUIREMENT_KIND
|
|
245
|
+
return invalid_strict_requirement(
|
|
246
|
+
"expected compiler_profile_contract_strict_requirement kind"
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
unless requirement["mode"] == STRICT_REQUIREMENT_MODE
|
|
250
|
+
return invalid_strict_requirement("unsupported strict requirement mode")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
source = requirement["source"]
|
|
254
|
+
unless STRICT_REQUIREMENT_SOURCES.include?(source)
|
|
255
|
+
return invalid_strict_requirement("unsupported strict validation source")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
candidates = Array(requirement["refusal_candidates"])
|
|
259
|
+
unless candidates.include?(CONTRACT_DIGEST_MISMATCH_CODE)
|
|
260
|
+
return invalid_strict_requirement("missing contract_digest_mismatch refusal candidate")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
unless requirement["recompute_unavailable_policy"] == "fail_open_report_only"
|
|
264
|
+
return invalid_strict_requirement("unsupported recompute_unavailable_policy")
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
unless requirement["compile_refusal_authorized"] == false
|
|
268
|
+
return invalid_strict_requirement("compile_refusal_authorized marker must remain false")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
{ "valid" => true }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def invalid_strict_requirement(reason)
|
|
275
|
+
{ "valid" => false, "reason" => reason }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def strict_refusal(report:, source_path:, diagnostic:)
|
|
279
|
+
{
|
|
280
|
+
"status" => "refused",
|
|
281
|
+
"result" => CompilerResult.strict_terminal(
|
|
282
|
+
format_version: FORMAT_VERSION,
|
|
283
|
+
status: "refused",
|
|
284
|
+
report: report,
|
|
285
|
+
source_path: source_path,
|
|
286
|
+
diagnostics: [diagnostic]
|
|
287
|
+
),
|
|
288
|
+
"compilation_report" => report
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def strict_configuration_error(report:, source_path:, diagnostic:)
|
|
293
|
+
{
|
|
294
|
+
"status" => "configuration_error",
|
|
295
|
+
"result" => CompilerResult.strict_terminal(
|
|
296
|
+
format_version: FORMAT_VERSION,
|
|
297
|
+
status: "configuration_error",
|
|
298
|
+
report: report,
|
|
299
|
+
source_path: source_path,
|
|
300
|
+
diagnostics: [diagnostic]
|
|
301
|
+
),
|
|
302
|
+
"compilation_report" => report
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def contract_digest_mismatch_refusal_diagnostic
|
|
307
|
+
{
|
|
308
|
+
"code" => CONTRACT_DIGEST_REFUSAL_CODE,
|
|
309
|
+
"message" => "Strict compiler profile contract validation refused compilation " \
|
|
310
|
+
"because contract_digest does not match canonical contract material.",
|
|
311
|
+
"path" => "compiler_profile_contract_validation.contract_digest",
|
|
312
|
+
"evidence_code" => CONTRACT_DIGEST_MISMATCH_CODE
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def strict_requirement_malformed_diagnostic(_reason)
|
|
317
|
+
{
|
|
318
|
+
"code" => STRICT_REQUIREMENT_MALFORMED_CODE,
|
|
319
|
+
"message" => "Malformed strict compiler profile contract requirement produced " \
|
|
320
|
+
"configuration_error before assembly.",
|
|
321
|
+
"path" => "compiler_profile_contract_strict_requirement",
|
|
322
|
+
"evidence_code" => nil
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def resolve_sample_input(parsed, sample_input_resolver)
|
|
327
|
+
return sample_input_resolver.call(parsed) if sample_input_resolver
|
|
328
|
+
|
|
329
|
+
default_sample_input(parsed.fetch("contracts").fetch(0, {}))
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def default_sample_input(contract)
|
|
333
|
+
contract.fetch("body", []).each_with_object({}) do |node, inputs|
|
|
334
|
+
next unless node.fetch("kind") == "input"
|
|
335
|
+
|
|
336
|
+
inputs[node.fetch("name")] = sample_value_for(node.fetch("type_annotation"))
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def sample_value_for(type_annotation)
|
|
341
|
+
type_name = if type_annotation.is_a?(Hash)
|
|
342
|
+
type_annotation.fetch("name", "Unknown")
|
|
343
|
+
else
|
|
344
|
+
type_annotation.to_s
|
|
345
|
+
end
|
|
346
|
+
case type_name
|
|
347
|
+
when "Integer" then 1
|
|
348
|
+
when "Float" then 1.0
|
|
349
|
+
when "Bool" then true
|
|
350
|
+
when "String" then "synthetic"
|
|
351
|
+
else {}
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def case_name_for(source_path, parsed)
|
|
356
|
+
basename = File.basename(source_path.to_s, ".ig")
|
|
357
|
+
return basename unless basename.empty?
|
|
358
|
+
|
|
359
|
+
parsed.fetch("contracts").fetch(0).fetch("name").downcase
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "set"
|
|
6
|
+
|
|
7
|
+
module IgniterLang
|
|
8
|
+
module CompilerProfileContractValidator
|
|
9
|
+
RESULT_KIND = "compiler_profile_contract_validation_result"
|
|
10
|
+
FORMAT_VERSION = "0.1.0"
|
|
11
|
+
DEFAULT_DIGEST_REFERENCE_POLICY = :prop038_24_plus
|
|
12
|
+
|
|
13
|
+
REQUIRED_SLOTS = %w[core oof_registry fragment_registry escape_boundary].freeze
|
|
14
|
+
OPTIONAL_SLOTS = %w[
|
|
15
|
+
contract_modifiers temporal stream olap invariant assumptions evidence_observation pipeline
|
|
16
|
+
].freeze
|
|
17
|
+
ALL_SLOTS = (REQUIRED_SLOTS + OPTIONAL_SLOTS).freeze
|
|
18
|
+
|
|
19
|
+
DESCRIPTOR_DIGEST_PATTERN = /\Acompiler_profile_descriptor\/sha256:[0-9a-f]{24,}\z/
|
|
20
|
+
FINALIZATION_PAYLOAD_DIGEST_PATTERN = /\Asha256:[0-9a-f]{64}\z/
|
|
21
|
+
CONTRACT_DIGEST_PREFIX = "compiler_profile_contract/sha256:"
|
|
22
|
+
CONTRACT_DIGEST_PATTERN = /\Acompiler_profile_contract\/sha256:[0-9a-f]{24,}\z/
|
|
23
|
+
SUPPORTED_DIGEST_REFERENCE_POLICIES = %w[prop038_24_plus].freeze
|
|
24
|
+
CANONICAL_CONTRACT_FIELDS = %w[
|
|
25
|
+
kind
|
|
26
|
+
format_version
|
|
27
|
+
profile_namespace
|
|
28
|
+
profile_kind
|
|
29
|
+
compiler_profile_id
|
|
30
|
+
descriptor_digest
|
|
31
|
+
finalization_payload_digest
|
|
32
|
+
required_slot_schema
|
|
33
|
+
slot_order
|
|
34
|
+
slot_assignments
|
|
35
|
+
strict_registries
|
|
36
|
+
ordered_rule_graph
|
|
37
|
+
non_authority
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
def self.validate(contract, digest_reference_policy: DEFAULT_DIGEST_REFERENCE_POLICY)
|
|
41
|
+
policy = digest_reference_policy.to_s
|
|
42
|
+
diagnostics = []
|
|
43
|
+
|
|
44
|
+
unless contract.is_a?(Hash)
|
|
45
|
+
diagnostics << diagnostic("wrong_kind", "expected compiler_profile_contract", "kind")
|
|
46
|
+
return result(diagnostics, policy)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
diagnostics << diagnostic("wrong_kind", "expected compiler_profile_contract", "kind") unless contract["kind"] == "compiler_profile_contract"
|
|
50
|
+
diagnostics << diagnostic("unsupported_format_version", "expected format_version 0.1.0", "format_version") unless contract["format_version"] == FORMAT_VERSION
|
|
51
|
+
diagnostics << diagnostic("descriptor_digest_invalid", "descriptor_digest must be compiler_profile_descriptor/sha256:<hex>", "descriptor_digest") unless contract["descriptor_digest"].to_s.match?(DESCRIPTOR_DIGEST_PATTERN)
|
|
52
|
+
diagnostics << diagnostic("finalization_payload_digest_invalid", "finalization_payload_digest must be sha256:<64 hex>", "finalization_payload_digest") unless contract["finalization_payload_digest"].to_s.match?(FINALIZATION_PAYLOAD_DIGEST_PATTERN)
|
|
53
|
+
|
|
54
|
+
contract_digest_recomputable = validate_contract_digest_shape(diagnostics, contract, policy)
|
|
55
|
+
|
|
56
|
+
slot_order = Array(contract["slot_order"])
|
|
57
|
+
slot_assignments = contract["slot_assignments"] || {}
|
|
58
|
+
Array(contract.dig("required_slot_schema", "required_slots")).each do |slot|
|
|
59
|
+
unless slot_order.include?(slot) && slot_assignments.key?(slot)
|
|
60
|
+
diagnostics << diagnostic("missing_required_slot", "required slot #{slot.inspect} is missing from slot_order or slot_assignments", "slot_assignments.#{slot}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
strict_registries = contract["strict_registries"] || {}
|
|
65
|
+
strict_registries.each do |registry_name, entries|
|
|
66
|
+
seen = {}
|
|
67
|
+
Array(entries).each do |entry|
|
|
68
|
+
key = entry["key"]
|
|
69
|
+
if seen.key?(key)
|
|
70
|
+
diagnostics << diagnostic("duplicate_strict_key", "strict registry #{registry_name} has duplicate key #{key.inspect}", "strict_registries.#{registry_name}.#{key}")
|
|
71
|
+
end
|
|
72
|
+
seen[key] = true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
rules = Array(contract.dig("ordered_rule_graph", "rules"))
|
|
77
|
+
rule_ids = rules.map { |rule| rule["rule_id"] }
|
|
78
|
+
rules.each do |rule|
|
|
79
|
+
(Array(rule["before"]) + Array(rule["after"])).each do |ref|
|
|
80
|
+
unless rule_ids.include?(ref)
|
|
81
|
+
diagnostics << diagnostic("missing_rule_reference", "ordered rule #{rule["rule_id"]} references missing rule #{ref.inspect}", "ordered_rule_graph.rules.#{rule["rule_id"]}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
cycle = find_rule_cycle(rules)
|
|
87
|
+
diagnostics << diagnostic("rule_cycle", "ordered rule graph contains cycle: #{cycle.join(" -> ")}", "ordered_rule_graph.rules") if cycle
|
|
88
|
+
|
|
89
|
+
non_authority = contract["non_authority"] || {}
|
|
90
|
+
diagnostics << diagnostic("runtime_authority_forbidden", "compiler profile contract cannot grant runtime authority", "non_authority.runtime_authority_granted") if non_authority["runtime_authority_granted"]
|
|
91
|
+
diagnostics << diagnostic("dispatch_migration_forbidden", "compiler profile contract cannot authorize dispatch migration", "non_authority.dispatch_migration_authorized") if non_authority["dispatch_migration_authorized"]
|
|
92
|
+
|
|
93
|
+
validate_contract_digest_match(diagnostics, contract) if contract_digest_recomputable
|
|
94
|
+
|
|
95
|
+
result(diagnostics, policy)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class << self
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def validate_contract_digest_shape(diagnostics, contract, policy)
|
|
102
|
+
unless SUPPORTED_DIGEST_REFERENCE_POLICIES.include?(policy)
|
|
103
|
+
diagnostics << diagnostic(
|
|
104
|
+
"contract_digest_policy_unsupported",
|
|
105
|
+
"unsupported contract_digest policy #{policy.inspect}",
|
|
106
|
+
"digest_reference_policy"
|
|
107
|
+
)
|
|
108
|
+
return false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
unless contract["contract_digest"].to_s.match?(CONTRACT_DIGEST_PATTERN)
|
|
112
|
+
diagnostics << diagnostic(
|
|
113
|
+
"contract_digest_invalid",
|
|
114
|
+
"contract_digest must be compiler_profile_contract/sha256:<24+ lowercase hex>",
|
|
115
|
+
"contract_digest"
|
|
116
|
+
)
|
|
117
|
+
return false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate_contract_digest_match(diagnostics, contract)
|
|
124
|
+
declared_hex = contract["contract_digest"].to_s.delete_prefix(CONTRACT_DIGEST_PREFIX)
|
|
125
|
+
computed_hex = recomputed_contract_digest_hex(contract)
|
|
126
|
+
return if computed_hex.start_with?(declared_hex)
|
|
127
|
+
|
|
128
|
+
diagnostics << diagnostic(
|
|
129
|
+
"contract_digest_mismatch",
|
|
130
|
+
"declared contract_digest does not match recomputed canonical contract digest",
|
|
131
|
+
"contract_digest"
|
|
132
|
+
)
|
|
133
|
+
rescue
|
|
134
|
+
diagnostics << diagnostic(
|
|
135
|
+
"contract_digest_recompute_unavailable",
|
|
136
|
+
"contract digest recompute requested but canonicalization is unavailable",
|
|
137
|
+
"contract_digest"
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def diagnostic(code, message, path = nil)
|
|
142
|
+
{
|
|
143
|
+
"code" => "compiler_profile_contract.#{code}",
|
|
144
|
+
"message" => message,
|
|
145
|
+
"path" => path
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def result(diagnostics, policy)
|
|
150
|
+
{
|
|
151
|
+
"kind" => RESULT_KIND,
|
|
152
|
+
"format_version" => FORMAT_VERSION,
|
|
153
|
+
"valid" => diagnostics.empty?,
|
|
154
|
+
"diagnostics" => diagnostics,
|
|
155
|
+
"diagnostic_codes" => diagnostics.map { |diagnostic| diagnostic.fetch("code") },
|
|
156
|
+
"digest_reference_policy" => policy,
|
|
157
|
+
"compiler_integrated" => false,
|
|
158
|
+
"compile_refusal_authorized" => false
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def recomputed_contract_digest_hex(contract)
|
|
163
|
+
Digest::SHA256.hexdigest(canonical_contract_json(contract))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def canonical_contract_json(contract)
|
|
167
|
+
JSON.generate(canonical_contract_material(contract))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def canonical_contract_material(contract)
|
|
171
|
+
material = CANONICAL_CONTRACT_FIELDS.to_h do |field|
|
|
172
|
+
[field, canonicalize_for_digest(contract[field])]
|
|
173
|
+
end
|
|
174
|
+
material["strict_registries"] = canonical_strict_registries(contract["strict_registries"] || {})
|
|
175
|
+
material["ordered_rule_graph"] = canonical_ordered_rule_graph(contract["ordered_rule_graph"] || {})
|
|
176
|
+
canonicalize_for_digest(material)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def canonical_strict_registries(registries)
|
|
180
|
+
unless registries.is_a?(Hash)
|
|
181
|
+
raise TypeError, "strict_registries must be a Hash for canonicalization"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
registries.keys.sort_by(&:to_s).to_h do |key|
|
|
185
|
+
registry_name = key.to_s
|
|
186
|
+
entries = Array(registries[key]).map { |entry| canonicalize_for_digest(entry) }
|
|
187
|
+
[
|
|
188
|
+
registry_name,
|
|
189
|
+
entries.sort_by do |entry|
|
|
190
|
+
[
|
|
191
|
+
entry.fetch("key", "").to_s,
|
|
192
|
+
entry.fetch("owner_slot", "").to_s,
|
|
193
|
+
entry.fetch("rule_ref", "").to_s
|
|
194
|
+
]
|
|
195
|
+
end
|
|
196
|
+
]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def canonical_ordered_rule_graph(graph)
|
|
201
|
+
unless graph.is_a?(Hash)
|
|
202
|
+
raise TypeError, "ordered_rule_graph must be a Hash for canonicalization"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
rules = Array(graph["rules"]).map do |rule|
|
|
206
|
+
canonical_rule(rule)
|
|
207
|
+
end
|
|
208
|
+
{ "rules" => rules.sort_by { |rule| rule.fetch("rule_id", "").to_s } }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def canonical_rule(rule)
|
|
212
|
+
unless rule.is_a?(Hash)
|
|
213
|
+
raise TypeError, "ordered rule must be a Hash for canonicalization"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
normalized = rule.keys.sort_by(&:to_s).to_h do |key|
|
|
217
|
+
key_name = key.to_s
|
|
218
|
+
value = if key_name == "before" || key_name == "after"
|
|
219
|
+
Array(rule[key]).map(&:to_s).uniq.sort
|
|
220
|
+
else
|
|
221
|
+
canonicalize_for_digest(rule[key])
|
|
222
|
+
end
|
|
223
|
+
[key_name, value]
|
|
224
|
+
end
|
|
225
|
+
normalized["before"] = [] unless normalized.key?("before")
|
|
226
|
+
normalized["after"] = [] unless normalized.key?("after")
|
|
227
|
+
normalized
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def canonicalize_for_digest(value)
|
|
231
|
+
case value
|
|
232
|
+
when Hash
|
|
233
|
+
value.keys.sort_by(&:to_s).to_h do |key|
|
|
234
|
+
[key.to_s, canonicalize_for_digest(value[key])]
|
|
235
|
+
end
|
|
236
|
+
when Array
|
|
237
|
+
value.map { |entry| canonicalize_for_digest(entry) }
|
|
238
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
239
|
+
value
|
|
240
|
+
else
|
|
241
|
+
raise TypeError, "unsupported canonical contract value #{value.class}"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def find_rule_cycle(rules)
|
|
246
|
+
ids = rules.map { |rule| rule.fetch("rule_id") }
|
|
247
|
+
edges = Hash.new { |hash, key| hash[key] = [] }
|
|
248
|
+
rules.each do |rule|
|
|
249
|
+
rule.fetch("before", []).each { |target| edges[rule.fetch("rule_id")] << target }
|
|
250
|
+
rule.fetch("after", []).each { |source| edges[source] << rule.fetch("rule_id") }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
visiting = Set.new
|
|
254
|
+
visited = Set.new
|
|
255
|
+
stack = []
|
|
256
|
+
|
|
257
|
+
visit = lambda do |id|
|
|
258
|
+
return nil if visited.include?(id)
|
|
259
|
+
if visiting.include?(id)
|
|
260
|
+
cycle_start = stack.index(id) || 0
|
|
261
|
+
return stack[cycle_start..] + [id]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
visiting << id
|
|
265
|
+
stack << id
|
|
266
|
+
edges[id].each do |target|
|
|
267
|
+
next unless ids.include?(target)
|
|
268
|
+
|
|
269
|
+
cycle = visit.call(target)
|
|
270
|
+
return cycle if cycle
|
|
271
|
+
end
|
|
272
|
+
stack.pop
|
|
273
|
+
visiting.delete(id)
|
|
274
|
+
visited << id
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
ids.each do |id|
|
|
279
|
+
cycle = visit.call(id)
|
|
280
|
+
return cycle if cycle
|
|
281
|
+
end
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|