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,821 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module IgniterLang
|
|
6
|
+
class TypeChecker
|
|
7
|
+
DEFAULT_VERSION = "typed-pass-executable-proof-v0"
|
|
8
|
+
|
|
9
|
+
def initialize(typechecker_version: DEFAULT_VERSION)
|
|
10
|
+
@typechecker_version = typechecker_version
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def typecheck(classified_program)
|
|
14
|
+
@type_shapes = type_shapes(classified_program)
|
|
15
|
+
@assumption_registry = classified_program.fetch("assumption_registry", [])
|
|
16
|
+
@type_shapes["Assumption"] = assumption_shape if assumptions_present?(classified_program)
|
|
17
|
+
@assumption_errors = assumption_errors_by_name(@assumption_registry)
|
|
18
|
+
@olap_env = olap_env(classified_program.fetch("olap_points", []))
|
|
19
|
+
@olap_errors = olap_declaration_errors(@olap_env)
|
|
20
|
+
typed_contracts = classified_program.fetch("contracts").map do |contract|
|
|
21
|
+
typecheck_contract(contract)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
result = {
|
|
25
|
+
"kind" => "typed_program",
|
|
26
|
+
"typechecker_version" => @typechecker_version,
|
|
27
|
+
"program_id" => program_id(classified_program),
|
|
28
|
+
"classified_program_id" => classified_program.fetch("program_id"),
|
|
29
|
+
"source_path" => classified_program.fetch("source_path"),
|
|
30
|
+
"source_hash" => classified_program.fetch("source_hash"),
|
|
31
|
+
"grammar_version" => classified_program.fetch("grammar_version"),
|
|
32
|
+
"module" => classified_program.fetch("module"),
|
|
33
|
+
"type_env" => @type_shapes,
|
|
34
|
+
"contracts" => typed_contracts,
|
|
35
|
+
"type_errors" => typed_contracts.flat_map { |contract| contract.fetch("type_errors") },
|
|
36
|
+
"semantic_ir_ref" => nil
|
|
37
|
+
}
|
|
38
|
+
result["assumption_registry"] = @assumption_registry unless @assumption_registry.empty?
|
|
39
|
+
result["olap_points"] = @olap_env.values.map { |decl| decl.fetch("semantic_node") } unless @olap_env.empty?
|
|
40
|
+
type_warnings = typed_contracts.flat_map { |contract| contract.fetch("type_warnings", []) }
|
|
41
|
+
result["type_warnings"] = type_warnings unless type_warnings.empty?
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def program_id(classified_program)
|
|
48
|
+
seed = [
|
|
49
|
+
classified_program.fetch("program_id"),
|
|
50
|
+
classified_program.fetch("source_hash"),
|
|
51
|
+
@typechecker_version
|
|
52
|
+
].join("|")
|
|
53
|
+
"typed_pass/#{Digest::SHA256.hexdigest(seed)[0, 16]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def type_shapes(classified_program)
|
|
57
|
+
classified_program.fetch("type_declarations").each_with_object({}) do |type, shapes|
|
|
58
|
+
shapes[type.fetch("name")] = type.fetch("fields", []).each_with_object({}) do |field, fields|
|
|
59
|
+
fields[field.fetch("name")] = type_ir(normalize_type(field.fetch("type_annotation")))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def typecheck_contract(classified_contract)
|
|
65
|
+
declared_oofs = classified_contract.fetch("oof_log")
|
|
66
|
+
assumption_refs = classified_contract.fetch("assumption_refs", [])
|
|
67
|
+
type_errors = declared_oofs + @olap_errors + assumption_refs.flat_map { |name| @assumption_errors.fetch(name, []) }
|
|
68
|
+
type_warnings = []
|
|
69
|
+
symbol_types = {}
|
|
70
|
+
typed_decls = []
|
|
71
|
+
invariant_effects = [] # [{"name" => ..., "effect" => "warns"|"uncertain"|"metric"}] for output propagation
|
|
72
|
+
|
|
73
|
+
classified_contract.fetch("declarations").each do |decl|
|
|
74
|
+
case decl.fetch("kind")
|
|
75
|
+
when "input"
|
|
76
|
+
type = type_ir(decl.fetch("type_annotation"))
|
|
77
|
+
symbol_types[decl.fetch("name")] = type
|
|
78
|
+
typed_decls << typed_decl(decl, type, nil, [])
|
|
79
|
+
when "read"
|
|
80
|
+
type = type_ir(decl.fetch("type_annotation"))
|
|
81
|
+
symbol_types[decl.fetch("name")] = type
|
|
82
|
+
typed_decls << typed_decl(decl, type, nil, [])
|
|
83
|
+
when "stream"
|
|
84
|
+
# stream declarations are ESCAPE; register their type for body-escape checks
|
|
85
|
+
type = decl.key?("type_annotation") ? type_ir(decl.fetch("type_annotation")) : type_ir("Unknown")
|
|
86
|
+
symbol_types[decl.fetch("name")] = type
|
|
87
|
+
typed_decls << typed_decl(decl, type, nil, [])
|
|
88
|
+
when "window"
|
|
89
|
+
typed_decls << typed_decl(decl, type_ir("Window"), nil, [])
|
|
90
|
+
when "fold_stream"
|
|
91
|
+
# OOF-S3: ESCAPE construct (stream ref) inside fold_stream accumulator function body
|
|
92
|
+
stream_symbols = stream_symbol_names(classified_contract)
|
|
93
|
+
check_fold_stream_body(decl, stream_symbols, type_errors)
|
|
94
|
+
result_type = fold_stream_result_type(decl)
|
|
95
|
+
symbol_types[decl.fetch("name")] = result_type
|
|
96
|
+
typed_decls << typed_decl(decl, result_type, decl.fetch("expr", nil), decl.fetch("deps", []))
|
|
97
|
+
when "uses_assumptions"
|
|
98
|
+
type = type_ir("Assumption")
|
|
99
|
+
symbol_types[decl.fetch("name")] = type
|
|
100
|
+
typed_decls << typed_decl(decl, type, nil, [])
|
|
101
|
+
when "invariant"
|
|
102
|
+
# TINV-1/2/3: Resolve predicate_ref, validate overridable_with, compute output_effect
|
|
103
|
+
check_invariant(decl, symbol_types, type_errors, invariant_effects)
|
|
104
|
+
typed_decls << typed_decl_invariant(decl, symbol_types)
|
|
105
|
+
when "compute"
|
|
106
|
+
typed_expr = infer_expr(decl.fetch("expr"), symbol_types, type_errors, type_warnings, decl.fetch("name"))
|
|
107
|
+
validate_declared_olap_type(decl, typed_expr, type_errors)
|
|
108
|
+
symbol_types[decl.fetch("name")] = typed_expr.fetch("resolved_type")
|
|
109
|
+
typed_decls << typed_decl(decl, typed_expr.fetch("resolved_type"), typed_expr, typed_expr.fetch("deps"))
|
|
110
|
+
when "output"
|
|
111
|
+
expected = type_ir(decl.fetch("type_annotation"))
|
|
112
|
+
actual = symbol_types.fetch(decl.fetch("name"), type_ir("Unknown"))
|
|
113
|
+
if type_name(actual) != type_name(expected) && !blocking_rule_present?(type_errors)
|
|
114
|
+
type_errors << type_mismatch(expected, actual, decl.fetch("name"))
|
|
115
|
+
end
|
|
116
|
+
# TINV-4: propagate invariant output effects to output nodes
|
|
117
|
+
typed_decls << typed_decl_output(decl, expected, invariant_effects)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
status = type_errors.empty? ? "accepted" : "blocked"
|
|
122
|
+
result = {
|
|
123
|
+
"kind" => "typed_contract",
|
|
124
|
+
"contract_id" => classified_contract.fetch("contract_id"),
|
|
125
|
+
"name" => classified_contract.fetch("name"),
|
|
126
|
+
"modifier" => classified_contract.fetch("modifier", "pure"),
|
|
127
|
+
"status" => status,
|
|
128
|
+
"fragment_class" => classified_contract.fetch("fragment_class"),
|
|
129
|
+
"symbols" => symbol_types.keys.sort.map do |name|
|
|
130
|
+
{ "name" => name, "type" => symbol_types.fetch(name), "resolved" => type_name(symbol_types.fetch(name)) != "Unknown" }
|
|
131
|
+
end,
|
|
132
|
+
"declarations" => typed_decls,
|
|
133
|
+
"type_errors" => dedupe_errors(type_errors)
|
|
134
|
+
}
|
|
135
|
+
result["assumption_refs"] = assumption_refs unless assumption_refs.empty?
|
|
136
|
+
warnings = dedupe_errors(type_warnings)
|
|
137
|
+
result["type_warnings"] = warnings unless warnings.empty?
|
|
138
|
+
result
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def typed_decl(decl, type, expr, deps)
|
|
142
|
+
result = {
|
|
143
|
+
"decl_id" => decl.fetch("decl_id"),
|
|
144
|
+
"kind" => decl.fetch("kind"),
|
|
145
|
+
"name" => decl.fetch("name"),
|
|
146
|
+
"fragment_class" => decl.fetch("fragment_class"),
|
|
147
|
+
"type" => type,
|
|
148
|
+
"deps" => deps
|
|
149
|
+
}
|
|
150
|
+
result["expr"] = expr if expr
|
|
151
|
+
result["semantic_node"] = expr.fetch("semantic_node") if expr&.key?("semantic_node")
|
|
152
|
+
%w[node_fragment_class value_fragment_class required_capability temporal_axis].each do |key|
|
|
153
|
+
result[key] = decl.fetch(key) if decl.key?(key)
|
|
154
|
+
end
|
|
155
|
+
%w[from lifecycle].each do |key|
|
|
156
|
+
result[key] = decl.fetch(key) if decl.key?(key)
|
|
157
|
+
end
|
|
158
|
+
%w[bound options window_ref key window_kind size period idle on_close fn_ref init stream_ref].each do |key|
|
|
159
|
+
result[key] = decl.fetch(key) if decl.key?(key)
|
|
160
|
+
end
|
|
161
|
+
result
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def assumption_shape
|
|
165
|
+
{
|
|
166
|
+
"kind" => type_ir("Symbol"),
|
|
167
|
+
"statement" => type_ir("String"),
|
|
168
|
+
"strength" => type_ir("Decimal"),
|
|
169
|
+
"source" => type_ir("String")
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def assumptions_present?(classified_program)
|
|
174
|
+
@assumption_registry.any? ||
|
|
175
|
+
classified_program.fetch("contracts").any? { |contract| contract.fetch("assumption_refs", []).any? }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def assumption_errors_by_name(registry)
|
|
179
|
+
registry.each_with_object({}) do |entry, errors|
|
|
180
|
+
strength = entry.fetch("fields", {}).fetch("strength", nil)
|
|
181
|
+
next if strength.nil? || valid_assumption_strength?(strength)
|
|
182
|
+
|
|
183
|
+
errors[entry.fetch("name")] ||= []
|
|
184
|
+
errors[entry.fetch("name")] << oof(
|
|
185
|
+
"TASSUMP-1",
|
|
186
|
+
"assumption strength must be between 0.0 and 1.0",
|
|
187
|
+
"assumption:#{entry.fetch("name")}"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def valid_assumption_strength?(strength)
|
|
193
|
+
strength.is_a?(Numeric) && strength >= 0.0 && strength <= 1.0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def infer_expr(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
197
|
+
case expr.fetch("kind")
|
|
198
|
+
when "literal"
|
|
199
|
+
type = type_ir(expr.fetch("type_tag"))
|
|
200
|
+
typed_expr("literal", type, [], "value" => expr.fetch("value"), "literal_type" => literal_type(type_name(type)))
|
|
201
|
+
when "symbol"
|
|
202
|
+
typed_expr("symbol", type_ir("Symbol"), [], "value" => expr.fetch("value"))
|
|
203
|
+
when "ref"
|
|
204
|
+
name = expr.fetch("name")
|
|
205
|
+
type = symbol_types.fetch(name, @olap_env.fetch(name, {}).fetch("type", type_ir("Unknown")))
|
|
206
|
+
type_errors << oof("OOF-P1", "Unresolved symbol: #{name}", node_name) if type_name(type) == "Unknown" && !rule_present?(type_errors, "OOF-P1")
|
|
207
|
+
typed_expr("ref", type, [name], "name" => name)
|
|
208
|
+
when "field_access"
|
|
209
|
+
object = infer_expr(expr.fetch("object"), symbol_types, type_errors, type_warnings, node_name)
|
|
210
|
+
object_type = type_name(object.fetch("resolved_type"))
|
|
211
|
+
field_type = @type_shapes.fetch(object_type, {})[expr.fetch("field")] || type_ir("Unknown")
|
|
212
|
+
if type_name(field_type) == "Unknown"
|
|
213
|
+
type_errors << oof("OOF-P1", "Unresolved field: #{object_type}.#{expr.fetch("field")}", node_name)
|
|
214
|
+
end
|
|
215
|
+
typed_expr(
|
|
216
|
+
"field_access",
|
|
217
|
+
field_type,
|
|
218
|
+
object.fetch("deps"),
|
|
219
|
+
"object" => object,
|
|
220
|
+
"field" => expr.fetch("field")
|
|
221
|
+
)
|
|
222
|
+
when "binary_op"
|
|
223
|
+
infer_binary(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
224
|
+
when "call"
|
|
225
|
+
infer_call(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
226
|
+
when "index_access"
|
|
227
|
+
infer_index_access(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
228
|
+
else
|
|
229
|
+
type_errors << oof("OOF-TY0", "Unsupported expression kind: #{expr.fetch("kind")}", node_name)
|
|
230
|
+
typed_expr("unsupported", type_ir("Unknown"), [], "source_kind" => expr.fetch("kind"))
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def infer_call(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
235
|
+
fn = expr.fetch("fn")
|
|
236
|
+
args = expr.fetch("args")
|
|
237
|
+
case fn
|
|
238
|
+
when "history_at"
|
|
239
|
+
infer_history_at(fn, args, symbol_types, type_errors, type_warnings, node_name)
|
|
240
|
+
when "bihistory_at"
|
|
241
|
+
infer_bihistory_at(fn, args, symbol_types, type_errors, type_warnings, node_name)
|
|
242
|
+
when "olap_rollup"
|
|
243
|
+
infer_olap_rollup(fn, args, symbol_types, type_errors, type_warnings, node_name)
|
|
244
|
+
else
|
|
245
|
+
type_errors << oof("OOF-TY0", "Unknown function: #{fn}", node_name)
|
|
246
|
+
typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def infer_history_at(fn, args, symbol_types, type_errors, type_warnings, node_name)
|
|
251
|
+
if args.length < 2
|
|
252
|
+
type_errors << oof_alias("OOF-H1", "history_at requires as_of argument", node_name, ["OOF-TM1"])
|
|
253
|
+
return typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
history_ref = infer_expr(args[0], symbol_types, type_errors, type_warnings, node_name)
|
|
257
|
+
as_of_ref = infer_expr(args[1], symbol_types, type_errors, type_warnings, node_name)
|
|
258
|
+
unless type_name(as_of_ref.fetch("resolved_type")) == "DateTime" ||
|
|
259
|
+
type_name(as_of_ref.fetch("resolved_type")) == "Unknown"
|
|
260
|
+
type_errors << oof_alias("OOF-BT1", "history_at: as_of must be DateTime, got #{type_name(as_of_ref.fetch("resolved_type"))}", node_name, ["OOF-TM3"])
|
|
261
|
+
end
|
|
262
|
+
result_type = option_type_from(history_ref.fetch("resolved_type"))
|
|
263
|
+
typed_expr(
|
|
264
|
+
"call",
|
|
265
|
+
result_type,
|
|
266
|
+
history_ref.fetch("deps") + as_of_ref.fetch("deps"),
|
|
267
|
+
"fn" => fn,
|
|
268
|
+
"args" => [history_ref, as_of_ref],
|
|
269
|
+
"semantic_node" => temporal_access_node(node_name, "valid_time", history_ref, [as_of_ref], result_type)
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def infer_bihistory_at(fn, args, symbol_types, type_errors, type_warnings, node_name)
|
|
274
|
+
if args.length < 2
|
|
275
|
+
type_errors << oof_alias("OOF-BT2", "bihistory_at requires valid_time (vt) argument", node_name, ["OOF-TM4"])
|
|
276
|
+
return typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
277
|
+
end
|
|
278
|
+
if args.length < 3
|
|
279
|
+
type_errors << oof_alias("OOF-BT3", "bihistory_at requires transaction_time (tt) argument", node_name, ["OOF-TM5"])
|
|
280
|
+
return typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
history_ref = infer_expr(args[0], symbol_types, type_errors, type_warnings, node_name)
|
|
284
|
+
vt_ref = infer_expr(args[1], symbol_types, type_errors, type_warnings, node_name)
|
|
285
|
+
tt_ref = infer_expr(args[2], symbol_types, type_errors, type_warnings, node_name)
|
|
286
|
+
[vt_ref, tt_ref].each_with_index do |axis_ref, idx|
|
|
287
|
+
axis_name = idx.zero? ? "valid_time" : "transaction_time"
|
|
288
|
+
unless type_name(axis_ref.fetch("resolved_type")) == "DateTime" ||
|
|
289
|
+
type_name(axis_ref.fetch("resolved_type")) == "Unknown"
|
|
290
|
+
type_errors << oof_alias("OOF-BT4", "bihistory_at: #{axis_name} must be DateTime, got #{type_name(axis_ref.fetch("resolved_type"))}", node_name, ["OOF-TM6"])
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
result_type = option_type_from(history_ref.fetch("resolved_type"))
|
|
294
|
+
typed_expr(
|
|
295
|
+
"call",
|
|
296
|
+
result_type,
|
|
297
|
+
history_ref.fetch("deps") + vt_ref.fetch("deps") + tt_ref.fetch("deps"),
|
|
298
|
+
"fn" => fn,
|
|
299
|
+
"args" => [history_ref, vt_ref, tt_ref],
|
|
300
|
+
"semantic_node" => temporal_access_node(node_name, "bitemporal", history_ref, [vt_ref, tt_ref], result_type)
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def temporal_access_node(node_name, axis, history_ref, axis_refs, result_type)
|
|
305
|
+
capability = axis == "bitemporal" ? "bihistory_read" : "history_read"
|
|
306
|
+
result = {
|
|
307
|
+
"kind" => "temporal_access_node",
|
|
308
|
+
"name" => node_name,
|
|
309
|
+
"source_ref" => history_ref.fetch("name", nil),
|
|
310
|
+
"axis" => axis,
|
|
311
|
+
"temporal_axis" => axis,
|
|
312
|
+
"history_ref" => history_ref.fetch("name", nil),
|
|
313
|
+
"axis_refs" => axis_refs.map { |ref| ref.fetch("name", nil) }.compact,
|
|
314
|
+
"coordinate_refs" => temporal_coordinate_refs(axis, axis_refs),
|
|
315
|
+
"result_type" => result_type,
|
|
316
|
+
"node_fragment_class" => "temporal",
|
|
317
|
+
"value_fragment_class" => "core",
|
|
318
|
+
"required_capability" => capability,
|
|
319
|
+
"required_caps" => [capability],
|
|
320
|
+
"deps" => history_ref.fetch("deps", []) + axis_refs.flat_map { |ref| ref.fetch("deps", []) },
|
|
321
|
+
"evidence_policy" => axis == "bitemporal" ? "link_selected_event_observation" : "link_selected_append_observation",
|
|
322
|
+
"fragment" => "temporal"
|
|
323
|
+
}
|
|
324
|
+
if axis == "bitemporal"
|
|
325
|
+
result["valid_time_ref"] = axis_refs[0]&.fetch("name", nil)
|
|
326
|
+
result["transaction_time_ref"] = axis_refs[1]&.fetch("name", nil)
|
|
327
|
+
else
|
|
328
|
+
result["as_of_ref"] = axis_refs[0]&.fetch("name", nil)
|
|
329
|
+
end
|
|
330
|
+
result
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def temporal_coordinate_refs(axis, axis_refs)
|
|
334
|
+
if axis == "bitemporal"
|
|
335
|
+
{
|
|
336
|
+
"valid_time" => axis_refs[0]&.fetch("name", nil),
|
|
337
|
+
"transaction_time" => axis_refs[1]&.fetch("name", nil)
|
|
338
|
+
}
|
|
339
|
+
else
|
|
340
|
+
{ "as_of" => axis_refs[0]&.fetch("name", nil) }
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def infer_index_access(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
345
|
+
object = expr.fetch("object")
|
|
346
|
+
return unsupported_index_access(expr, type_errors, node_name) unless object.fetch("kind") == "ref"
|
|
347
|
+
|
|
348
|
+
olap_name = object.fetch("name")
|
|
349
|
+
olap_decl = @olap_env[olap_name]
|
|
350
|
+
return unsupported_index_access(expr, type_errors, node_name) unless olap_decl
|
|
351
|
+
|
|
352
|
+
index = expr.fetch("index")
|
|
353
|
+
unless index.fetch("kind") == "slice_record"
|
|
354
|
+
type_errors << oof("OOF-O4", "OLAPPoint access requires a dimension slice record", node_name)
|
|
355
|
+
return typed_expr("index_access", type_ir("Unknown"), [olap_name], "object" => typed_expr("ref", olap_decl.fetch("type"), [olap_name], "name" => olap_name))
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
slices = index.fetch("fields")
|
|
359
|
+
dims = olap_decl.fetch("dimensions")
|
|
360
|
+
missing = dims.keys.sort - slices.keys.sort
|
|
361
|
+
missing.each do |dim|
|
|
362
|
+
type_errors << oof("OOF-O4", "OLAPPoint access missing required dimension: #{dim}", node_name)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
typed_slices = slices.keys.sort.map do |dim|
|
|
366
|
+
expected = dims[dim]
|
|
367
|
+
value = infer_expr(slices.fetch(dim), symbol_types, type_errors, type_warnings, node_name)
|
|
368
|
+
if expected && !unknown?(value.fetch("resolved_type")) && !same_type?(expected, value.fetch("resolved_type"))
|
|
369
|
+
type_errors << oof("OOF-O5", "OLAPPoint dimension '#{dim}' expected #{type_display(expected)}, got #{type_display(value.fetch("resolved_type"))}", node_name)
|
|
370
|
+
end
|
|
371
|
+
{
|
|
372
|
+
"dim" => dim,
|
|
373
|
+
"value" => value,
|
|
374
|
+
"value_ref" => slice_value_ref(slices.fetch(dim)),
|
|
375
|
+
"expected_type" => expected || type_ir("Unknown")
|
|
376
|
+
}
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
semantic_node = olap_access_node(node_name, olap_decl, typed_slices)
|
|
380
|
+
typed_expr(
|
|
381
|
+
"index_access",
|
|
382
|
+
olap_decl.fetch("measure_type"),
|
|
383
|
+
typed_slices.flat_map { |slice| slice.fetch("value").fetch("deps") },
|
|
384
|
+
"object" => typed_expr("ref", olap_decl.fetch("type"), [olap_name], "name" => olap_name),
|
|
385
|
+
"slices" => typed_slices,
|
|
386
|
+
"semantic_node" => semantic_node
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def unsupported_index_access(expr, type_errors, node_name)
|
|
391
|
+
type_errors << oof("OOF-TY0", "Unsupported index access", node_name)
|
|
392
|
+
typed_expr("index_access", type_ir("Unknown"), [], "source_kind" => expr.fetch("kind"))
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def infer_olap_rollup(fn, args, symbol_types, type_errors, type_warnings, node_name)
|
|
396
|
+
if args.length < 2
|
|
397
|
+
type_errors << oof("OOF-O4", "olap_rollup requires point and dimension arguments", node_name)
|
|
398
|
+
return typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
olap_ref = args[0]
|
|
402
|
+
dim_arg = args[1]
|
|
403
|
+
unless olap_ref.fetch("kind") == "ref" && dim_arg.fetch("kind") == "symbol"
|
|
404
|
+
type_errors << oof("OOF-O4", "olap_rollup requires a named OLAPPoint and dimension symbol", node_name)
|
|
405
|
+
return typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
olap_decl = @olap_env[olap_ref.fetch("name")]
|
|
409
|
+
unless olap_decl
|
|
410
|
+
type_errors << oof("OOF-P1", "Unresolved symbol: #{olap_ref.fetch("name")}", node_name)
|
|
411
|
+
return typed_expr("call", type_ir("Unknown"), [], "fn" => fn, "args" => [])
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
dim = dim_arg.fetch("value")
|
|
415
|
+
unless olap_decl.fetch("dimensions").key?(dim)
|
|
416
|
+
type_errors << oof("OOF-O4", "OLAPPoint access missing required dimension: #{dim}", node_name)
|
|
417
|
+
end
|
|
418
|
+
unless olap_decl.fetch("indexed").include?(dim) || explicit_scatter_gather?(args)
|
|
419
|
+
type_warnings << oof("OOF-O2", "rollup over non-indexed dimension may be slow; add to indexed:", node_name).merge("severity" => "warning")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
remaining_dims = olap_decl.fetch("dimensions").reject { |name, _type| name == dim }
|
|
423
|
+
result_type = olap_type(olap_decl.fetch("measure_type"), remaining_dims)
|
|
424
|
+
typed_expr(
|
|
425
|
+
"call",
|
|
426
|
+
result_type,
|
|
427
|
+
[olap_decl.fetch("name")],
|
|
428
|
+
"fn" => fn,
|
|
429
|
+
"args" => [
|
|
430
|
+
typed_expr("ref", olap_decl.fetch("type"), [olap_decl.fetch("name")], "name" => olap_decl.fetch("name")),
|
|
431
|
+
typed_expr("symbol", type_ir("Symbol"), [], "value" => dim)
|
|
432
|
+
]
|
|
433
|
+
)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def option_type_from(history_type)
|
|
437
|
+
inner = history_type.fetch("params", []).first
|
|
438
|
+
inner_name = inner.is_a?(Hash) ? inner.fetch("name", "Unknown") : (inner || "Unknown")
|
|
439
|
+
{ "name" => "Option", "params" => [{ "name" => inner_name, "params" => [] }] }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def infer_binary(expr, symbol_types, type_errors, type_warnings, node_name)
|
|
443
|
+
left = infer_expr(expr.fetch("left"), symbol_types, type_errors, type_warnings, node_name)
|
|
444
|
+
right = infer_expr(expr.fetch("right"), symbol_types, type_errors, type_warnings, node_name)
|
|
445
|
+
operator, result_type = operator_type(expr.fetch("op"), left.fetch("resolved_type"), right.fetch("resolved_type"), type_errors, node_name)
|
|
446
|
+
typed_expr(
|
|
447
|
+
"call",
|
|
448
|
+
result_type,
|
|
449
|
+
left.fetch("deps") + right.fetch("deps"),
|
|
450
|
+
"fn" => operator,
|
|
451
|
+
"args" => [left, right]
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def operator_type(op, left, right, type_errors, node_name)
|
|
456
|
+
left_name = type_name(left)
|
|
457
|
+
right_name = type_name(right)
|
|
458
|
+
case op
|
|
459
|
+
when "+"
|
|
460
|
+
type_errors << type_mismatch(type_ir("Integer"), type_ir("#{left_name}+#{right_name}"), node_name) unless unknown?(left, right) || left_name == "Integer" && right_name == "Integer"
|
|
461
|
+
["stdlib.integer.add", type_ir("Integer")]
|
|
462
|
+
when ">"
|
|
463
|
+
type_errors << type_mismatch(type_ir("Integer"), type_ir("#{left_name}+#{right_name}"), node_name) unless unknown?(left, right) || left_name == "Integer" && right_name == "Integer"
|
|
464
|
+
["stdlib.integer.gt", type_ir("Bool")]
|
|
465
|
+
when "&&"
|
|
466
|
+
type_errors << type_mismatch(type_ir("Bool"), type_ir("#{left_name}+#{right_name}"), node_name) unless unknown?(left, right) || left_name == "Bool" && right_name == "Bool"
|
|
467
|
+
["stdlib.bool.and", type_ir("Bool")]
|
|
468
|
+
else
|
|
469
|
+
type_errors << oof("OOF-TY0", "Unsupported operator: #{op}", node_name)
|
|
470
|
+
["stdlib.unsupported.#{op}", type_ir("Unknown")]
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def typed_expr(kind, type, deps, extra)
|
|
475
|
+
{ "kind" => kind }.merge(extra).merge("resolved_type" => type, "deps" => deps.uniq)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def type_ir(annotation)
|
|
479
|
+
return annotation.dup if annotation.is_a?(Hash) && annotation.key?("name")
|
|
480
|
+
|
|
481
|
+
name = annotation.is_a?(Hash) ? annotation.fetch("name", "Unknown") : annotation.to_s
|
|
482
|
+
params = annotation.is_a?(Hash) ? annotation.fetch("params", []).map { |p| type_ir(p) } : []
|
|
483
|
+
{ "name" => name, "params" => params }
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def dims_record_type(dims)
|
|
487
|
+
{
|
|
488
|
+
"name" => "DimsRecord",
|
|
489
|
+
"params" => [],
|
|
490
|
+
"dims" => dims.transform_values { |type| type_ir(type) }
|
|
491
|
+
}
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def olap_type(measure_type, dims)
|
|
495
|
+
{
|
|
496
|
+
"name" => "OLAPPoint",
|
|
497
|
+
"params" => [measure_type, dims_record_type(dims)]
|
|
498
|
+
}
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def olap_env(olap_points)
|
|
502
|
+
olap_points.each_with_object({}) do |point, env|
|
|
503
|
+
dimensions = point.fetch("dimensions", {}).transform_values { |type| type_ir(type) }
|
|
504
|
+
measure_type = type_ir(point.fetch("measure", point.fetch("measure_type", "Unknown")))
|
|
505
|
+
name = point.fetch("name")
|
|
506
|
+
semantic_node = {
|
|
507
|
+
"kind" => "olap_point_decl",
|
|
508
|
+
"name" => name,
|
|
509
|
+
"dimensions" => dimensions.transform_values { |type| type_display(type) },
|
|
510
|
+
"measure_type" => type_display(measure_type),
|
|
511
|
+
"granularity" => point.fetch("granularity", {}),
|
|
512
|
+
"source_ref" => point.fetch("source_ref", nil),
|
|
513
|
+
"indexed" => point.fetch("indexed", [])
|
|
514
|
+
}
|
|
515
|
+
env[name] = {
|
|
516
|
+
"name" => name,
|
|
517
|
+
"dimensions" => dimensions,
|
|
518
|
+
"measure_type" => measure_type,
|
|
519
|
+
"granularity" => point.fetch("granularity", {}),
|
|
520
|
+
"source" => point.fetch("source", nil),
|
|
521
|
+
"source_ref" => point.fetch("source_ref", nil),
|
|
522
|
+
"seeded_data" => point.fetch("seeded_data", false),
|
|
523
|
+
"indexed" => point.fetch("indexed", []),
|
|
524
|
+
"type" => olap_type(measure_type, dimensions),
|
|
525
|
+
"semantic_node" => semantic_node
|
|
526
|
+
}
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def olap_declaration_errors(olap_env)
|
|
531
|
+
olap_env.values.filter_map do |point|
|
|
532
|
+
next if point.fetch("source") || point.fetch("source_ref") || point.fetch("seeded_data")
|
|
533
|
+
|
|
534
|
+
oof("OOF-O3", "OLAPPoint must declare a source function or be populated via stream snapshot", point.fetch("name"))
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def validate_declared_olap_type(decl, typed_expr, type_errors)
|
|
539
|
+
annotation = decl["type_annotation"]
|
|
540
|
+
return unless annotation.is_a?(Hash) && annotation.fetch("name", nil) == "OLAPPoint"
|
|
541
|
+
|
|
542
|
+
expected = type_ir(annotation)
|
|
543
|
+
actual_node = typed_expr.fetch("semantic_node", nil)
|
|
544
|
+
return unless actual_node
|
|
545
|
+
|
|
546
|
+
measure = expected.fetch("params", []).fetch(0, type_ir("Unknown"))
|
|
547
|
+
if type_display(measure) != actual_node.dig("result_type", "measure")
|
|
548
|
+
type_errors << oof("OOF-TY0", "OLAPPoint measure expected #{type_display(measure)}, got #{actual_node.dig("result_type", "measure")}", decl.fetch("name"))
|
|
549
|
+
end
|
|
550
|
+
dims = dims_from_type(expected)
|
|
551
|
+
actual_dims = actual_node.dig("result_type", "dims_record", "dims") || {}
|
|
552
|
+
dims.each do |dim, expected_type|
|
|
553
|
+
actual_type = actual_dims[dim]
|
|
554
|
+
next if actual_type.nil? || type_display(expected_type) == actual_type
|
|
555
|
+
|
|
556
|
+
type_errors << oof("OOF-O5", "OLAPPoint dimension '#{dim}' expected #{type_display(expected_type)}, got #{actual_type}", decl.fetch("name"))
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def dims_from_type(type)
|
|
561
|
+
dims_record = type.fetch("params", []).find { |param| param.is_a?(Hash) && (param.fetch("kind", nil) == "dims_record" || param.fetch("name", nil) == "DimsRecord") }
|
|
562
|
+
return {} unless dims_record
|
|
563
|
+
|
|
564
|
+
dims_record.fetch("dims", {}).transform_values { |dim_type| type_ir(dim_type) }
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def olap_access_node(node_name, olap_decl, typed_slices)
|
|
568
|
+
{
|
|
569
|
+
"kind" => "olap_access_node",
|
|
570
|
+
"name" => node_name,
|
|
571
|
+
"olap_ref" => olap_decl.fetch("name"),
|
|
572
|
+
"slices" => typed_slices.map do |slice|
|
|
573
|
+
{
|
|
574
|
+
"dim" => slice.fetch("dim"),
|
|
575
|
+
"value_ref" => slice.fetch("value_ref"),
|
|
576
|
+
"value_type" => type_display(slice.fetch("value").fetch("resolved_type"))
|
|
577
|
+
}
|
|
578
|
+
end,
|
|
579
|
+
"operation" => "point",
|
|
580
|
+
"result_type" => {
|
|
581
|
+
"constructor" => "OLAPPoint",
|
|
582
|
+
"measure" => type_display(olap_decl.fetch("measure_type")),
|
|
583
|
+
"dims_record" => {
|
|
584
|
+
"kind" => "dims_record",
|
|
585
|
+
"dims" => olap_decl.fetch("dimensions").transform_values { |type| type_display(type) }
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
"resolved_type" => type_display(olap_decl.fetch("measure_type"))
|
|
589
|
+
}
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def slice_value_ref(expr)
|
|
593
|
+
expr.fetch("kind") == "ref" ? expr.fetch("name") : nil
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def explicit_scatter_gather?(args)
|
|
597
|
+
args.any? { |arg| arg.fetch("kind", nil) == "symbol" && arg.fetch("value") == "scatter_gather" }
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def same_type?(expected, actual)
|
|
601
|
+
type_display(expected) == type_display(actual)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def type_display(type)
|
|
605
|
+
return type.to_s unless type.is_a?(Hash)
|
|
606
|
+
|
|
607
|
+
params = type.fetch("params", [])
|
|
608
|
+
return type.fetch("name") if params.empty?
|
|
609
|
+
|
|
610
|
+
rendered = params.map { |param| param.is_a?(Hash) ? type_display(param) : param.to_s }.join(",")
|
|
611
|
+
"#{type.fetch("name")}[#{rendered}]"
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def type_name(type)
|
|
615
|
+
type.fetch("name")
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def normalize_type(type)
|
|
619
|
+
type.is_a?(Hash) ? type.fetch("name") : type.to_s
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def literal_type(name)
|
|
623
|
+
{
|
|
624
|
+
"Integer" => "int",
|
|
625
|
+
"Float" => "float",
|
|
626
|
+
"String" => "string",
|
|
627
|
+
"Bool" => "bool",
|
|
628
|
+
"Nil" => "nil"
|
|
629
|
+
}.fetch(name, name.downcase)
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def unknown?(*types)
|
|
633
|
+
types.any? { |type| type_name(type) == "Unknown" }
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def type_mismatch(expected, actual, node)
|
|
637
|
+
oof("OOF-TY0", "Type mismatch: expected #{type_name(expected)}, got #{type_name(actual)}", node)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def oof(rule, message, node_name)
|
|
641
|
+
{ "rule" => rule, "message" => message, "node" => node_name, "line" => nil }
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def oof_alias(rule, message, node_name, aliases)
|
|
645
|
+
oof(rule, message, node_name).merge("aliases" => aliases)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def rule_present?(errors, rule)
|
|
649
|
+
errors.any? { |entry| entry.fetch("rule") == rule }
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def blocking_rule_present?(errors)
|
|
653
|
+
%w[OOF-P1 OOF-CE4 OOF-OS2 OOF-H1 OOF-BT1 OOF-BT2 OOF-BT3 OOF-BT4 OOF-TM1 OOF-TM3 OOF-TM4 OOF-TM5 OOF-TM6 OOF-S3 OOF-O3 OOF-O4 OOF-O5 OOF-IV3].any? { |rule| rule_present?(errors, rule) }
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# OOF-IV helpers -------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
# TC-INV-1: resolve predicate_ref, check Bool type.
|
|
659
|
+
# TC-INV-2: validate overridable_with semantics.
|
|
660
|
+
# TC-INV-3: compute output_effect and record for output propagation.
|
|
661
|
+
def check_invariant(decl, symbol_types, type_errors, invariant_effects)
|
|
662
|
+
predicate_ref = decl.fetch("predicate_ref", nil)
|
|
663
|
+
severity = decl.fetch("severity", "error")
|
|
664
|
+
name = decl.fetch("name")
|
|
665
|
+
|
|
666
|
+
# TC-INV-1: predicate must resolve to Bool
|
|
667
|
+
if predicate_ref
|
|
668
|
+
pred_type = symbol_types.fetch(predicate_ref, type_ir("Unknown"))
|
|
669
|
+
unless type_name(pred_type) == "Bool" || type_name(pred_type) == "Unknown"
|
|
670
|
+
type_errors << oof("OOF-IV3", "invariant predicate must be Bool, got #{type_name(pred_type)}", name)
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# TC-INV-2: overridable_with on :error is OOF-I4 (dynamic/inferred case; parser catches static)
|
|
675
|
+
overridable_with = decl.fetch("overridable_with", nil)
|
|
676
|
+
if overridable_with && severity == "error"
|
|
677
|
+
type_errors << oof("OOF-I4", ":error invariants cannot be overridden", name)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# TC-INV-3: record output effect for TINV-4 propagation
|
|
681
|
+
effect = invariant_output_effect(severity)
|
|
682
|
+
invariant_effects << { "name" => name, "effect" => effect } if %w[warns uncertain metric].include?(effect)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Typed node for an invariant declaration.
|
|
686
|
+
def typed_decl_invariant(decl, symbol_types)
|
|
687
|
+
predicate_ref = decl.fetch("predicate_ref", nil)
|
|
688
|
+
pred_type = predicate_ref ? symbol_types.fetch(predicate_ref, type_ir("Unknown")) : type_ir("Unknown")
|
|
689
|
+
output_effect = invariant_output_effect(decl.fetch("severity", "error"))
|
|
690
|
+
result = {
|
|
691
|
+
"decl_id" => decl.fetch("decl_id"),
|
|
692
|
+
"kind" => "invariant",
|
|
693
|
+
"name" => decl.fetch("name"),
|
|
694
|
+
"fragment_class" => decl.fetch("fragment_class"),
|
|
695
|
+
"predicate_ref" => predicate_ref,
|
|
696
|
+
"predicate_type" => pred_type,
|
|
697
|
+
"severity" => decl.fetch("severity", "error"),
|
|
698
|
+
"label" => decl.fetch("label", nil),
|
|
699
|
+
"message" => decl.fetch("message", nil),
|
|
700
|
+
"overridable_with" => decl.fetch("overridable_with", nil),
|
|
701
|
+
"output_effect" => output_effect,
|
|
702
|
+
"type" => type_ir("Bool"),
|
|
703
|
+
"deps" => predicate_ref ? [predicate_ref] : []
|
|
704
|
+
}
|
|
705
|
+
result["source_span"] = decl.fetch("source_span") if decl.key?("source_span")
|
|
706
|
+
result["source_metadata"] = decl.fetch("source_metadata") if decl.key?("source_metadata")
|
|
707
|
+
result["threshold"] = decl.fetch("threshold") if decl.key?("threshold")
|
|
708
|
+
result["threshold_ms"] = decl.fetch("threshold_ms") if decl.key?("threshold_ms")
|
|
709
|
+
result
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Typed output decl with invariant effect propagation (TINV-4).
|
|
713
|
+
def typed_decl_output(decl, type, invariant_effects)
|
|
714
|
+
result = {
|
|
715
|
+
"decl_id" => decl.fetch("decl_id"),
|
|
716
|
+
"kind" => "output",
|
|
717
|
+
"name" => decl.fetch("name"),
|
|
718
|
+
"fragment_class" => decl.fetch("fragment_class"),
|
|
719
|
+
"type" => type,
|
|
720
|
+
"deps" => decl.fetch("deps")
|
|
721
|
+
}
|
|
722
|
+
warnings_from = invariant_effects.select { |e| e["effect"] == "warns" }.map { |e| e["name"] }
|
|
723
|
+
uncertain_from = invariant_effects.select { |e| e["effect"] == "uncertain" }.map { |e| e["name"] }
|
|
724
|
+
metrics_from = invariant_effects.select { |e| e["effect"] == "metric" }.map { |e| e["name"] }
|
|
725
|
+
result["warnings_from"] = warnings_from unless warnings_from.empty?
|
|
726
|
+
result["uncertain_from"] = uncertain_from unless uncertain_from.empty?
|
|
727
|
+
result["metrics_from"] = metrics_from unless metrics_from.empty?
|
|
728
|
+
result
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# Maps severity to the output_effect string (per PROP-025 §3 / spec track Part 3).
|
|
732
|
+
def invariant_output_effect(severity)
|
|
733
|
+
case severity
|
|
734
|
+
when "error" then "blocks"
|
|
735
|
+
when "warn" then "warns"
|
|
736
|
+
when "soft" then "uncertain"
|
|
737
|
+
when "metric" then "metric"
|
|
738
|
+
else "blocks"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# OOF-S3 helpers -------------------------------------------------------
|
|
743
|
+
|
|
744
|
+
# Collect the names of all stream-kind symbols in the classified contract.
|
|
745
|
+
def stream_symbol_names(classified_contract)
|
|
746
|
+
classified_contract.fetch("symbols", []).filter_map do |sym|
|
|
747
|
+
sym.fetch("name") if sym.fetch("kind") == "stream"
|
|
748
|
+
end.to_set
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Walk the fold_stream accumulator lambda body and emit OOF-S3 for any
|
|
752
|
+
# ref that names a stream symbol (ESCAPE construct inside CORE-required fn).
|
|
753
|
+
def check_fold_stream_body(decl, stream_symbols, type_errors)
|
|
754
|
+
return if stream_symbols.empty?
|
|
755
|
+
return unless decl.fetch("expr", nil)&.fetch("kind", nil) == "call"
|
|
756
|
+
|
|
757
|
+
call = decl.fetch("expr")
|
|
758
|
+
lambda_arg = call.fetch("args", []).find { |arg| arg.fetch("kind", nil) == "lambda" }
|
|
759
|
+
return unless lambda_arg
|
|
760
|
+
|
|
761
|
+
body = lambda_arg.fetch("body", nil)
|
|
762
|
+
return unless body
|
|
763
|
+
|
|
764
|
+
lambda_params = lambda_arg.fetch("params", []).map(&:to_s).to_set
|
|
765
|
+
escape_refs = collect_escape_refs(body, stream_symbols, lambda_params)
|
|
766
|
+
escape_refs.each do |ref_name|
|
|
767
|
+
type_errors << oof(
|
|
768
|
+
"OOF-S3",
|
|
769
|
+
"fold_stream accumulator must be CORE - found ESCAPE: #{ref_name}",
|
|
770
|
+
decl.fetch("name")
|
|
771
|
+
)
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# Recursively collect ref-names from the body AST that are stream symbols
|
|
776
|
+
# but NOT lambda parameters (those shadow the outer stream names).
|
|
777
|
+
def collect_escape_refs(node, stream_symbols, lambda_params)
|
|
778
|
+
return [] unless node.is_a?(Hash)
|
|
779
|
+
|
|
780
|
+
case node.fetch("kind", nil)
|
|
781
|
+
when "ref"
|
|
782
|
+
name = node.fetch("name")
|
|
783
|
+
stream_symbols.include?(name) && !lambda_params.include?(name) ? [name] : []
|
|
784
|
+
when "lambda"
|
|
785
|
+
# Nested lambda: extend lambda_params with inner params to avoid false positives
|
|
786
|
+
inner_params = lambda_params + node.fetch("params", []).map(&:to_s)
|
|
787
|
+
collect_escape_refs(node.fetch("body", {}), stream_symbols, inner_params)
|
|
788
|
+
when "binary_op"
|
|
789
|
+
collect_escape_refs(node.fetch("left", {}), stream_symbols, lambda_params) +
|
|
790
|
+
collect_escape_refs(node.fetch("right", {}), stream_symbols, lambda_params)
|
|
791
|
+
when "call"
|
|
792
|
+
node.fetch("args", []).flat_map { |arg| collect_escape_refs(arg, stream_symbols, lambda_params) } +
|
|
793
|
+
collect_escape_refs(node.fetch("object", {}), stream_symbols, lambda_params)
|
|
794
|
+
when "field_access"
|
|
795
|
+
collect_escape_refs(node.fetch("object", {}), stream_symbols, lambda_params)
|
|
796
|
+
else
|
|
797
|
+
# Walk all Hash values for any other node kinds
|
|
798
|
+
node.values.flat_map { |v| v.is_a?(Hash) ? collect_escape_refs(v, stream_symbols, lambda_params) : [] }
|
|
799
|
+
end.uniq
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Determine the fold_stream result type from the init literal or annotation.
|
|
803
|
+
# Returns Unknown if the init expression does not carry a type_tag.
|
|
804
|
+
def fold_stream_result_type(decl)
|
|
805
|
+
expr = decl.fetch("expr", nil)
|
|
806
|
+
return type_ir("Unknown") unless expr&.fetch("kind", nil) == "call"
|
|
807
|
+
|
|
808
|
+
args = expr.fetch("args", [])
|
|
809
|
+
init_arg = args[1] # args[0]=stream_ref, args[1]=init, args[2]=lambda
|
|
810
|
+
return type_ir("Unknown") unless init_arg&.fetch("kind", nil) == "literal"
|
|
811
|
+
|
|
812
|
+
type_ir(init_arg.fetch("type_tag", "Unknown"))
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def dedupe_errors(errors)
|
|
816
|
+
errors.uniq { |entry| [entry.fetch("rule"), entry.fetch("message"), entry.fetch("node"), entry.fetch("line")] }
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
Typechecker = TypeChecker
|
|
821
|
+
end
|