engineering-argument-language 3.2.3__py3-none-any.whl

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.
Files changed (72) hide show
  1. eal/__init__.py +3 -0
  2. eal/abstractions.py +144 -0
  3. eal/acquisition_coordination.py +80 -0
  4. eal/api_load_methods.py +77 -0
  5. eal/aspic.py +478 -0
  6. eal/aspic_compiler.py +445 -0
  7. eal/aspic_export.py +309 -0
  8. eal/builtin_methods.py +115 -0
  9. eal/catalogue.py +384 -0
  10. eal/cli.py +204 -0
  11. eal/client_transports.py +69 -0
  12. eal/collection_identity.py +28 -0
  13. eal/collection_scheduler.py +72 -0
  14. eal/command_process.py +118 -0
  15. eal/command_supervisor.py +112 -0
  16. eal/composition.py +531 -0
  17. eal/credentials.py +33 -0
  18. eal/dialectic.py +321 -0
  19. eal/discovery.py +127 -0
  20. eal/evaluator.py +619 -0
  21. eal/expressions.py +404 -0
  22. eal/extensions.py +38 -0
  23. eal/formatter.py +156 -0
  24. eal/generated/EALLexer.py +434 -0
  25. eal/generated/EALParser.py +6559 -0
  26. eal/generated/EALVisitor.py +393 -0
  27. eal/generated/__init__.py +1 -0
  28. eal/host.py +179 -0
  29. eal/host_redaction.py +51 -0
  30. eal/knowledge.py +96 -0
  31. eal/limits.py +88 -0
  32. eal/mcp_guard.py +56 -0
  33. eal/methods.py +463 -0
  34. eal/model.py +266 -0
  35. eal/model_context.py +55 -0
  36. eal/modes.py +121 -0
  37. eal/observation_reuse.py +128 -0
  38. eal/operation_contracts.py +25 -0
  39. eal/operations.py +155 -0
  40. eal/packets.py +604 -0
  41. eal/parser.py +407 -0
  42. eal/planning.py +136 -0
  43. eal/propositions.py +231 -0
  44. eal/reachability.py +85 -0
  45. eal/reasoning/__init__.py +25 -0
  46. eal/reasoning/abductive.py +45 -0
  47. eal/reasoning/analogical.py +41 -0
  48. eal/reasoning/causal.py +46 -0
  49. eal/reasoning/counterfactual.py +64 -0
  50. eal/reasoning/deductive.py +72 -0
  51. eal/reasoning/inductive.py +29 -0
  52. eal/reasoning/strategy.py +18 -0
  53. eal/reasoning/structured.py +11 -0
  54. eal/reasoning/temporal.py +47 -0
  55. eal/reasoning/validation.py +90 -0
  56. eal/registered_assessment.py +181 -0
  57. eal/runtime.py +454 -0
  58. eal/sampled_negative.py +131 -0
  59. eal/scope_transfer.py +59 -0
  60. eal/semantics.py +656 -0
  61. eal/server.py +79 -0
  62. eal/server_auth.py +37 -0
  63. eal/server_settings.py +258 -0
  64. eal/source_printer.py +129 -0
  65. eal/store.py +272 -0
  66. eal/tool_acquisition.py +383 -0
  67. engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
  68. engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
  69. engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
  70. engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
  71. engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
  72. engineering_argument_language-3.2.3.dist-info/top_level.txt +1 -0
eal/composition.py ADDED
@@ -0,0 +1,531 @@
1
+ """Lexical binding and hygienic, identity-preserving argument composition."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, replace
5
+ import re
6
+
7
+ from .limits import bounded, current_limits
8
+ from .model import (Program, Diagnostic, Block, Declaration, Environment, Tool, Evidence, Assumption,
9
+ Reasoning, Claim, Argument, Objection, Pattern, Application, PatternBinding,
10
+ ArgumentationDirective, ArgumentOrigin, Context, Module, SourceImport,
11
+ ArgumentBlock, PatternGuard)
12
+
13
+ TABLES = {Environment: 'environments', Tool: 'tools', Evidence: 'evidence', Assumption: 'assumptions',
14
+ Reasoning: 'reasoning', Claim: 'claims', Argument: 'arguments', Objection: 'objections',
15
+ Pattern: 'patterns', Application: 'applications'}
16
+ KINDS = {cls: name[:-1] if name.endswith('s') else name for cls, name in TABLES.items()}
17
+ KINDS[Evidence] = 'evidence'
18
+ PARAMETER_KINDS = frozenset({'claim', 'reasoning', 'evidence', 'assumption', 'environment', 'tool'})
19
+ _OPERATION = re.compile(r'(head|tail)\(([^()]+)\)\Z')
20
+
21
+
22
+ def qualify(prefix, name):
23
+ return prefix + '.' + name if prefix else name
24
+
25
+
26
+ def candidates(prefix, name):
27
+ while prefix:
28
+ yield qualify(prefix, name)
29
+ prefix = prefix.rpartition('.')[0]
30
+ yield name
31
+
32
+
33
+ def value_references(value):
34
+ """References and their required types; predicate paths are data, not symbols."""
35
+ if isinstance(value, Evidence):
36
+ yield 'tool', value.tool, 'tool', False
37
+ yield 'environment', value.environment, 'environment', False
38
+ elif isinstance(value, (Claim, Assumption)):
39
+ yield 'environment', value.environment, 'environment', False
40
+ if isinstance(value, Assumption):
41
+ yield 'validation', value.validation, 'evidence', False
42
+ elif isinstance(value, Reasoning):
43
+ for ref in value.backing:
44
+ yield 'backing', ref, 'evidence', True
45
+ if value.transfer:
46
+ yield 'transfer', value.transfer.source, 'environment', False
47
+ yield 'transfer', value.transfer.target, 'environment', False
48
+ yield 'transfer', value.transfer.assumption, 'assumption', False
49
+ elif isinstance(value, (Argument, Pattern)):
50
+ yield 'conclusion', value.conclusion, 'claim', False
51
+ yield 'reasoning', value.reasoning, 'reasoning', False
52
+ for attr, kind in (('evidence', 'evidence'), ('assumptions', 'assumption'), ('premises', 'claim')):
53
+ for ref in getattr(value, attr):
54
+ yield attr, ref, kind, True
55
+ if value.binding is not None:
56
+ yield 'binding', value.binding, 'evidence', False
57
+ elif isinstance(value, Objection):
58
+ yield 'target', value.target, value.target_kind, False
59
+ for ref in value.evidence:
60
+ yield 'evidence', ref, 'evidence', True
61
+ for ref in value.premises:
62
+ yield 'premises', ref, 'claim', True
63
+
64
+
65
+ @dataclass
66
+ class _Record:
67
+ value: object
68
+ prefix: str
69
+ bindings: dict
70
+ span: object
71
+ pattern: str | None = None
72
+ application: str | None = None
73
+ depth: int = 0
74
+ stack: tuple = ()
75
+ guards: frozenset = frozenset()
76
+ source_file: str | None = None
77
+
78
+
79
+ class _Lowerer:
80
+ def __init__(self, program):
81
+ self.program = program
82
+ self.records = {name: {} for name in TABLES.values()}
83
+ self.symbols = {}
84
+ self.directives = []
85
+ self.namespaces = set()
86
+ self.locations = {}
87
+ self.generated = []
88
+ self.duplicates = []
89
+ self.errors = []
90
+ self.expanded = 0
91
+ self.references = 0
92
+ self.checked_patterns = set()
93
+ self.source_files = {}
94
+ self.calls = []
95
+
96
+ def error(self, code, message, owner=None, span=None, expected=None, actual=None):
97
+ self.errors.append(Diagnostic(code, message, owner, span or self.locations.get(owner), expected, actual))
98
+
99
+ def register(self, value, prefix, bindings, span, pattern=None, application=None, depth=0, stack=(), guards=frozenset(), source_file=None):
100
+ if len(self.symbols) + len(self.directives) >= current_limits().declarations:
101
+ self.error('resource_limit', 'Declaration budget exceeded', application or getattr(value, 'name', None), span)
102
+ return
103
+ if isinstance(value, ArgumentationDirective):
104
+ self.directives.append(_Record(value, prefix, bindings, span, pattern, application, depth, stack, guards))
105
+ return
106
+ name = qualify(prefix, value.name)
107
+ table = TABLES[type(value)]
108
+ # A compact application and its single generated argument share an ID.
109
+ compact = isinstance(value, Argument) and value.origin is not None and name in self.records['applications']
110
+ if (name in self.symbols and not compact) or name in self.namespaces:
111
+ self.duplicates.append(name)
112
+ self.symbols[name] = KINDS[type(value)] if not compact else 'argument'
113
+ self.records[table][name] = _Record(replace(value, name=name), prefix, bindings, span,
114
+ pattern, application, depth, stack, guards, source_file)
115
+ self.locations[name] = span
116
+ if source_file:
117
+ self.source_files[name] = source_file
118
+ if application and not isinstance(value, Application):
119
+ self.generated.append(name)
120
+ for _, ref, _, sequence in value_references(value):
121
+ bound = bindings.get(ref)
122
+ self.references += len(bound) if sequence and type(bound) is tuple else 1
123
+ if self.references > current_limits().expanded_references:
124
+ self.error('resource_limit', 'Expanded reference budget exceeded', application, span)
125
+
126
+ def block(self, block, prefix='', bindings=None, pattern=None, application=None, depth=0, stack=(), guards=frozenset()):
127
+ if depth > current_limits().expansion_depth:
128
+ self.error('resource_limit', 'Composition depth budget exceeded', application or prefix)
129
+ return
130
+ bindings = bindings or {}
131
+ for declaration in block.declarations:
132
+ value, span = declaration.value, declaration.span
133
+ if isinstance(value, Context):
134
+ self.block(value.body, prefix, bindings, pattern, application, depth + 1, stack, guards)
135
+ elif isinstance(value, (Module, SourceImport)):
136
+ namespace = qualify(prefix, value.name)
137
+ if namespace in self.namespaces or namespace in self.symbols:
138
+ self.error('duplicate_symbol', f'Namespace {namespace!r} is declared more than once', namespace, span)
139
+ self.namespaces.add(namespace)
140
+ self.block(value.body, namespace, bindings, pattern, application, depth + 1, stack, guards)
141
+ elif isinstance(value, ArgumentBlock):
142
+ namespace = qualify(prefix, value.name)
143
+ self.block(value.body, namespace, bindings, pattern, application, depth + 1, stack, guards)
144
+ flow = replace(value.conclusion, name=value.name,
145
+ origin=ArgumentOrigin(pattern, application) if application else None)
146
+ # Its final flow resolves against the block's own lexical scope.
147
+ self.register(flow, prefix, bindings, span, pattern, application, depth, stack, guards, declaration.source_file)
148
+ record = self.records['arguments'].get(namespace)
149
+ if record:
150
+ record.prefix = namespace
151
+ elif isinstance(value, PatternGuard):
152
+ if pattern is None:
153
+ self.error('invalid_pattern_guard', 'when is a structural list guard inside a pattern', prefix, span)
154
+ continue
155
+ values = bindings.get(value.parameter)
156
+ if type(values) is not tuple:
157
+ self.error('pattern_guard_type', 'when requires a typed list parameter', application, span)
158
+ continue
159
+ selected = value.body if values else value.otherwise
160
+ if selected:
161
+ self.block(selected, prefix, bindings, pattern, application, depth + 1, stack,
162
+ guards | {value.parameter} if values else guards)
163
+ else:
164
+ if application and isinstance(value, Argument):
165
+ value = replace(value, origin=ArgumentOrigin(pattern, application))
166
+ self.register(value, prefix, bindings, span, pattern, application, depth, stack, guards, declaration.source_file)
167
+
168
+ def resolve(self, ref, record, sequence=False):
169
+ operation = _OPERATION.fullmatch(ref)
170
+ if operation:
171
+ name = operation.group(2)
172
+ value = record.bindings.get(name)
173
+ if type(value) is not tuple:
174
+ self.error('pattern_reference_kind', f'{operation.group(1)} requires a list parameter', record.application or record.value.name,
175
+ record.span)
176
+ return () if sequence else ref
177
+ if operation.group(1) == 'tail':
178
+ value = value[1:]
179
+ elif value:
180
+ value = value[0]
181
+ else:
182
+ self.error('empty_pattern_head', 'head requires a nonempty guarded list', record.application, record.span)
183
+ return () if sequence else ref
184
+ elif ref in record.bindings:
185
+ value = record.bindings[ref]
186
+ else:
187
+ value = next((name for name in candidates(record.prefix, ref) if name in self.symbols), ref)
188
+ if sequence:
189
+ return value if type(value) is tuple else (value,)
190
+ if type(value) is tuple:
191
+ self.error('pattern_reference_kind', 'A scalar reference cannot consume a list', record.application or record.value.name,
192
+ record.span)
193
+ return ref
194
+ return value
195
+
196
+ def resolve_values(self, values, record):
197
+ return tuple(ref for value in values for ref in self.resolve(value, record, True))
198
+
199
+ def resolved(self, record):
200
+ value = record.value
201
+ if isinstance(value, Evidence):
202
+ return replace(value, tool=self.resolve(value.tool, record), environment=self.resolve(value.environment, record))
203
+ if isinstance(value, Claim):
204
+ return replace(value, environment=self.resolve(value.environment, record))
205
+ if isinstance(value, Assumption):
206
+ return replace(value, environment=self.resolve(value.environment, record), validation=self.resolve(value.validation, record))
207
+ if isinstance(value, Reasoning):
208
+ transfer = value.transfer
209
+ if transfer:
210
+ transfer = replace(transfer, source=self.resolve(transfer.source, record), target=self.resolve(transfer.target, record),
211
+ assumption=self.resolve(transfer.assumption, record))
212
+ return replace(value, backing=self.resolve_values(value.backing, record), transfer=transfer)
213
+ if isinstance(value, Argument):
214
+ return replace(value, conclusion=self.resolve(value.conclusion, record), reasoning=self.resolve(value.reasoning, record),
215
+ evidence=self.resolve_values(value.evidence, record), assumptions=self.resolve_values(value.assumptions, record),
216
+ premises=self.resolve_values(value.premises, record),
217
+ binding=self.resolve(value.binding, record) if value.binding else None)
218
+ if isinstance(value, Objection):
219
+ return replace(value, target=self.resolve(value.target, record), evidence=self.resolve_values(value.evidence, record),
220
+ premises=self.resolve_values(value.premises, record))
221
+ if isinstance(value, Application):
222
+ bindings = tuple(replace(item, reference=self.resolve_values(item.reference, record) if type(item.reference) is tuple
223
+ else self.resolve(item.reference, record, _OPERATION.fullmatch(item.reference) is not None
224
+ and item.reference.startswith('tail('))) for item in value.arguments)
225
+ pattern = next((name for name in candidates(record.prefix, value.pattern) if name in self.records['patterns']), value.pattern)
226
+ return replace(value, pattern=pattern, arguments=bindings)
227
+ if isinstance(value, ArgumentationDirective):
228
+ return replace(value, name=self.resolve(value.name, record), other=self.resolve(value.other, record) if value.other else None)
229
+ return value
230
+
231
+ def template_locals(self, block, prefix=''):
232
+ result = {}
233
+ for declaration in block.declarations:
234
+ value = declaration.value
235
+ if isinstance(value, Context):
236
+ result.update(self.template_locals(value.body, prefix))
237
+ elif isinstance(value, (Module, SourceImport, ArgumentBlock)):
238
+ child = qualify(prefix, value.name)
239
+ result.update(self.template_locals(value.body, child))
240
+ if isinstance(value, ArgumentBlock):
241
+ result[child] = 'argument'
242
+ elif isinstance(value, PatternGuard):
243
+ result.update(self.template_locals(value.body, prefix))
244
+ if value.otherwise:
245
+ result.update(self.template_locals(value.otherwise, prefix))
246
+ elif isinstance(value, ArgumentationDirective):
247
+ continue
248
+ else:
249
+ result[qualify(prefix, value.name)] = KINDS[type(value)]
250
+ return result
251
+
252
+ def check_template(self, record):
253
+ pattern = record.value
254
+ if pattern.name in self.checked_patterns:
255
+ return
256
+ self.checked_patterns.add(pattern.name)
257
+ parameters = {}
258
+ for parameter in pattern.parameters:
259
+ if parameter.name in parameters:
260
+ self.error('duplicate_pattern_parameter', f'Parameter {parameter.name!r} is declared more than once', pattern.name)
261
+ parameters[parameter.name] = parameter.kind
262
+ if parameter.kind.removesuffix('[]') not in PARAMETER_KINDS:
263
+ self.error('invalid_pattern_parameter_kind', f'Unknown parameter kind {parameter.kind!r}', pattern.name)
264
+ if pattern.decreases is not None and not parameters.get(pattern.decreases, '').endswith('[]'):
265
+ self.error('invalid_recursion_measure', 'decreases must name a typed list parameter', pattern.name)
266
+ locals = self.template_locals(pattern.body) if pattern.body else {}
267
+ local_patterns = {}
268
+ def definitions(block, prefix=''):
269
+ for item in block.declarations:
270
+ value = item.value
271
+ if isinstance(value, Pattern):
272
+ local_patterns[qualify(prefix, value.name)] = value
273
+ elif isinstance(value, (Context, Module, ArgumentBlock)):
274
+ definitions(value.body, qualify(prefix, value.name) if not isinstance(value, Context) else prefix)
275
+ elif isinstance(value, PatternGuard):
276
+ definitions(value.body, prefix)
277
+ if value.otherwise:
278
+ definitions(value.otherwise, prefix)
279
+ if pattern.body:
280
+ definitions(pattern.body)
281
+ def application_kinds(block, prefix=''):
282
+ for item in block.declarations:
283
+ value = item.value
284
+ if isinstance(value, Application):
285
+ local = next((name for name in candidates(prefix, value.pattern) if name in local_patterns), None)
286
+ global_name = next((name for name in candidates(record.prefix, value.pattern) if name in self.records['patterns']), None)
287
+ called = local_patterns[local] if local is not None else self.records['patterns'][global_name].value if global_name else None
288
+ if called is not None and called.body is None:
289
+ locals[qualify(prefix, value.name)] = 'argument'
290
+ elif isinstance(value, (Context, Module, SourceImport, ArgumentBlock, PatternGuard)):
291
+ child = qualify(prefix, value.name) if isinstance(value, (Module, SourceImport, ArgumentBlock)) else prefix
292
+ application_kinds(value.body, child)
293
+ if isinstance(value, PatternGuard) and value.otherwise:
294
+ application_kinds(value.otherwise, prefix)
295
+ application_kinds(pattern.body)
296
+ for name in locals:
297
+ if name in parameters:
298
+ self.error('generated_symbol_collision', f'Local {name!r} shadows a pattern parameter', pattern.name)
299
+
300
+ def reference(ref, kind, sequence, prefix):
301
+ operation = _OPERATION.fullmatch(ref)
302
+ name = operation.group(2) if operation else ref
303
+ actual = parameters.get(name)
304
+ if actual is not None:
305
+ if operation:
306
+ if not actual.endswith('[]'):
307
+ self.error('pattern_reference_kind', f'{operation.group(1)} requires a list parameter', pattern.name)
308
+ return
309
+ actual = actual if operation.group(1) == 'tail' else actual[:-2]
310
+ else:
311
+ actual = next((locals[name_] for name_ in candidates(prefix, name) if name_ in locals), None)
312
+ if actual is None:
313
+ self.error('unbound_pattern_reference', f'Reference {name!r} must name a typed parameter or local declaration; global capture is forbidden',
314
+ pattern.name, expected=kind, actual='undeclared parameter')
315
+ elif actual != kind and not (sequence and actual == kind + '[]'):
316
+ self.error('pattern_reference_kind', f'Reference {name!r} has kind {actual!r}; expected {kind!r}', pattern.name,
317
+ expected=kind, actual=actual)
318
+
319
+ def check_value(value, prefix):
320
+ for _, ref, kind, sequence in value_references(value):
321
+ reference(ref, kind, sequence, prefix)
322
+
323
+ def walk(block, prefix='', guards=frozenset()):
324
+ for declaration in block.declarations:
325
+ value = declaration.value
326
+ if isinstance(value, Context):
327
+ walk(value.body, prefix, guards)
328
+ elif isinstance(value, (Module, SourceImport)):
329
+ walk(value.body, qualify(prefix, value.name), guards)
330
+ elif isinstance(value, ArgumentBlock):
331
+ child = qualify(prefix, value.name)
332
+ walk(value.body, child, guards)
333
+ check_value(value.conclusion, child)
334
+ elif isinstance(value, PatternGuard):
335
+ if not parameters.get(value.parameter, '').endswith('[]'):
336
+ self.error('pattern_guard_type', 'when requires a typed list parameter', pattern.name)
337
+ walk(value.body, prefix, guards | {value.parameter})
338
+ if value.otherwise:
339
+ walk(value.otherwise, prefix, guards)
340
+ elif isinstance(value, Pattern):
341
+ # Nested definitions remain closed in their own parameters.
342
+ nested = _Record(replace(value, name=qualify(pattern.name, value.name)), record.prefix, {}, declaration.span)
343
+ self.check_template(nested)
344
+ elif isinstance(value, Application):
345
+ target = next((name for name in candidates(record.prefix, value.pattern) if name in self.records['patterns']), None)
346
+ called = self.records['patterns'][target].value if target else None
347
+ local = next((name for name in candidates(prefix, value.pattern) if name in local_patterns), None)
348
+ if local is not None:
349
+ called = replace(local_patterns[local], name=qualify(pattern.name, local))
350
+ elif value.pattern in (pattern.name, pattern.name.rpartition('.')[2] or pattern.name):
351
+ called = pattern
352
+ if called:
353
+ supplied = {item.name: item.reference for item in value.arguments}
354
+ kinds = {param.name: param.kind for param in called.parameters}
355
+ if len(supplied) != len(value.arguments):
356
+ self.error('duplicate_pattern_argument', 'A parameter is supplied more than once', pattern.name)
357
+ for item in value.arguments:
358
+ expected = kinds.get(item.name)
359
+ if expected is None:
360
+ self.error('unknown_pattern_argument', f'Unknown parameter {item.name!r}', pattern.name)
361
+ continue
362
+ refs = item.reference if type(item.reference) is tuple else (item.reference,)
363
+ if type(item.reference) is tuple and not expected.endswith('[]'):
364
+ self.error('pattern_argument_kind', f'Parameter {item.name!r} requires a scalar reference', pattern.name)
365
+ if type(item.reference) is str and expected.endswith('[]'):
366
+ op = _OPERATION.fullmatch(item.reference)
367
+ actual = parameters.get(op.group(2) if op else item.reference, '')
368
+ if not actual.endswith('[]') or op and op.group(1) == 'head':
369
+ self.error('pattern_argument_kind', f'Parameter {item.name!r} requires a list reference or explicit list', pattern.name)
370
+ for ref in refs:
371
+ reference(ref, expected.removesuffix('[]'), expected.endswith('[]'), prefix)
372
+ for missing in kinds.keys() - supplied.keys():
373
+ self.error('missing_pattern_argument', f'Pattern {called.name!r} requires {missing!r}', pattern.name)
374
+ self.calls.append((pattern.name, called.name, parameters, supplied, guards))
375
+ if called.name == pattern.name:
376
+ measure = pattern.decreases
377
+ if not measure or measure not in guards or supplied.get(measure) != 'tail(' + measure + ')':
378
+ self.error('nonterminating_pattern', 'A recursive call must be guarded by the decreasing list and pass its tail', pattern.name)
379
+ elif value.pattern not in locals:
380
+ self.error('unknown_pattern', f'Unknown pattern {value.pattern!r}', pattern.name)
381
+ elif not isinstance(value, ArgumentationDirective):
382
+ check_value(value, prefix)
383
+
384
+ if pattern.body:
385
+ walk(pattern.body)
386
+ else:
387
+ if not (pattern.evidence or pattern.assumptions or pattern.premises):
388
+ self.error('empty_pattern', 'An argument pattern requires support', pattern.name)
389
+ check_value(pattern, '')
390
+
391
+ def expand(self, record):
392
+ application = self.resolved(record)
393
+ existing = self.records['arguments'].get(application.name)
394
+ if existing is not None and existing.value.origin is None:
395
+ self.error('generated_symbol_collision', 'An application cannot replace an authored argument', application.name)
396
+ return
397
+ self.records['applications'][application.name].value = application
398
+ self.records['applications'][application.name].bindings = {}
399
+ # Its binding references are now fully qualified; prevent resolving twice.
400
+ self.records['applications'][application.name].prefix = ''
401
+ template = self.records['patterns'].get(application.pattern)
402
+ if template is None:
403
+ self.error('unknown_pattern', f'Unknown pattern {application.pattern!r}', application.name)
404
+ return
405
+ pattern = template.value
406
+ self.check_template(template)
407
+ if any(error.declaration == pattern.name for error in self.errors):
408
+ self.error('invalid_pattern', f'Pattern {pattern.name!r} has an invalid definition', application.name)
409
+ return
410
+ self.expanded += 1
411
+ if self.expanded > current_limits().applications:
412
+ self.error('resource_limit', 'Pattern application budget exceeded', application.name)
413
+ return
414
+ parameters = {parameter.name: parameter.kind for parameter in pattern.parameters}
415
+ bindings = {}
416
+ before = len(self.errors)
417
+ for item in application.arguments:
418
+ if item.name in bindings:
419
+ self.error('duplicate_pattern_argument', f'Parameter {item.name!r} is supplied twice', application.name)
420
+ bindings[item.name] = item.reference
421
+ expected = parameters.get(item.name)
422
+ if expected is None:
423
+ self.error('unknown_pattern_argument', f'Unknown parameter {item.name!r}', application.name)
424
+ continue
425
+ many = expected.endswith('[]')
426
+ if many != (type(item.reference) is tuple):
427
+ self.error('pattern_argument_kind', f'Parameter {item.name!r} requires {expected}', application.name)
428
+ continue
429
+ values = item.reference if many else (item.reference,)
430
+ for value in values:
431
+ self.references += 1
432
+ if self.symbols.get(value) != expected.removesuffix('[]'):
433
+ self.error('pattern_argument_kind', f'Parameter {item.name!r} requires {expected}; {value!r} is {self.symbols.get(value, "unknown reference")}',
434
+ application.name, expected=expected, actual=self.symbols.get(value, 'unknown reference'))
435
+ for missing in parameters.keys() - bindings.keys():
436
+ self.error('missing_pattern_argument', f'Pattern {pattern.name!r} requires {missing!r}', application.name)
437
+ if self.references > current_limits().expanded_references:
438
+ self.error('resource_limit', 'Expanded reference budget exceeded', application.name)
439
+ if len(self.errors) != before:
440
+ return
441
+ measure = len(bindings[pattern.decreases]) if pattern.decreases else None
442
+ ancestor = next((entry for entry in reversed(record.stack) if entry[0] == pattern.name), None)
443
+ if ancestor and (measure is None or ancestor[1] is None or measure >= ancestor[1]):
444
+ self.error('nonterminating_pattern', 'Recursive composition must strictly decrease its declared finite list', application.name)
445
+ return
446
+ if ancestor and (record.pattern is None or not self.records['patterns'][record.pattern].value.decreases
447
+ or self.records['patterns'][record.pattern].value.decreases not in record.guards):
448
+ self.error('nonterminating_pattern', 'A recursive call must occur inside its nonempty list guard', application.name)
449
+ return
450
+ depth = record.depth + 1
451
+ stack = record.stack + ((pattern.name, measure),)
452
+ if pattern.body:
453
+ self.block(pattern.body, application.name, bindings, pattern.name, application.name, depth, stack)
454
+ else:
455
+ value = Argument(application.name, pattern.conclusion, pattern.reasoning, pattern.evidence,
456
+ pattern.assumptions, pattern.premises, pattern.binding,
457
+ ArgumentOrigin(pattern.name, application.name))
458
+ self.register(value, '', bindings, record.span, pattern.name, application.name, depth, stack, source_file=record.source_file)
459
+
460
+ def run(self):
461
+ self.block(self.program.authored)
462
+ for record in list(self.records['patterns'].values()):
463
+ self.check_template(record)
464
+ graph = {}
465
+ for caller, called, *_ in self.calls:
466
+ graph.setdefault(caller, set()).add(called)
467
+ def reaches(start, goal):
468
+ pending, seen = [start], set()
469
+ while pending:
470
+ node = pending.pop()
471
+ if node == goal:
472
+ return True
473
+ if node not in seen:
474
+ seen.add(node)
475
+ pending.extend(graph.get(node, ()))
476
+ return False
477
+ for caller, called, parameters, supplied, guards in self.calls:
478
+ if reaches(called, caller):
479
+ source = self.records['patterns'].get(caller)
480
+ target = self.records['patterns'].get(called)
481
+ if source and target:
482
+ a, b = source.value.decreases, target.value.decreases
483
+ if not a or not b or a not in guards or supplied.get(b) != 'tail(' + a + ')':
484
+ self.error('nonterminating_pattern', 'Every call in a recursive component must pass a guarded decreasing tail', caller)
485
+ done = set()
486
+ if len(self.records['applications']) > current_limits().applications:
487
+ self.error('resource_limit', 'Pattern application budget exceeded')
488
+ done.update(self.records['applications'])
489
+ while True:
490
+ pending = [record for name, record in self.records['applications'].items() if name not in done]
491
+ if not pending:
492
+ break
493
+ for record in pending:
494
+ done.add(record.value.name)
495
+ self.expand(record)
496
+ if any(error.code == 'resource_limit' for error in self.errors):
497
+ # Exhaustion never exposes a seemingly complete partial expansion.
498
+ for records in self.records.values():
499
+ for name in list(records):
500
+ if records[name].application:
501
+ del records[name]
502
+ self.generated = []
503
+ tables = {name: {key: self.resolved(record) for key, record in records.items()}
504
+ for name, records in self.records.items()}
505
+ directives = tuple(self.resolved(record) for record in self.directives)
506
+ def body_count(block):
507
+ count = 0
508
+ for declaration in block.declarations:
509
+ value = declaration.value
510
+ if isinstance(value, Context):
511
+ count += body_count(value.body)
512
+ else:
513
+ count += 1
514
+ if isinstance(value, (Module, SourceImport, ArgumentBlock, PatternGuard, Pattern)) and getattr(value, 'body', None):
515
+ count += body_count(value.body)
516
+ if isinstance(value, PatternGuard) and value.otherwise:
517
+ count += body_count(value.otherwise)
518
+ return count
519
+ definitions = sum(body_count(pattern.body) if pattern.body else 1 for pattern in tables['patterns'].values())
520
+ count = sum(len(table) for table in tables.values()) + definitions + len(directives)
521
+ return replace(self.program, **tables, argumentation_directives=directives, duplicates=tuple(self.duplicates),
522
+ locations=self.locations, generated=tuple(self.generated), lowering_diagnostics=tuple(self.errors),
523
+ declaration_count=count, source_files=self.source_files)
524
+
525
+
526
+ @bounded
527
+ def lower_program(program):
528
+ if program.authored is None:
529
+ from .abstractions import lower_patterns
530
+ return lower_patterns(program)
531
+ return _Lowerer(program).run()
eal/credentials.py ADDED
@@ -0,0 +1,33 @@
1
+ """Read and validate operator-owned opaque bearer credentials without disclosure."""
2
+
3
+ from collections.abc import Mapping
4
+ import os
5
+ import re
6
+
7
+
8
+ _ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*", re.ASCII)
9
+
10
+
11
+ def validate_bearer_token(token: str) -> None:
12
+ """Require a token that both HTTP server and client can represent exactly."""
13
+ if (not isinstance(token, str) or not token
14
+ or any(character.isspace() or not 33 <= ord(character) <= 126 for character in token)):
15
+ raise ValueError("HTTP bearer token must contain only non-whitespace printable ASCII characters")
16
+
17
+
18
+ def read_bearer_token(token_env: str | None,
19
+ environ: Mapping[str, str] | None = None) -> str | None:
20
+ """Read one credential; diagnostics identify the variable without its value."""
21
+ if token_env is None:
22
+ return None
23
+ if not isinstance(token_env, str) or not _ENV_NAME.fullmatch(token_env):
24
+ raise ValueError("token_env must name an environment variable")
25
+ environ = os.environ if environ is None else environ
26
+ token = environ.get(token_env)
27
+ if not isinstance(token, str) or not token:
28
+ raise ValueError(f"HTTP bearer token environment variable {token_env} is missing or empty")
29
+ try:
30
+ validate_bearer_token(token)
31
+ except ValueError:
32
+ raise ValueError(f"HTTP bearer token environment variable {token_env} contains invalid characters") from None
33
+ return token