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.
- eal/__init__.py +3 -0
- eal/abstractions.py +144 -0
- eal/acquisition_coordination.py +80 -0
- eal/api_load_methods.py +77 -0
- eal/aspic.py +478 -0
- eal/aspic_compiler.py +445 -0
- eal/aspic_export.py +309 -0
- eal/builtin_methods.py +115 -0
- eal/catalogue.py +384 -0
- eal/cli.py +204 -0
- eal/client_transports.py +69 -0
- eal/collection_identity.py +28 -0
- eal/collection_scheduler.py +72 -0
- eal/command_process.py +118 -0
- eal/command_supervisor.py +112 -0
- eal/composition.py +531 -0
- eal/credentials.py +33 -0
- eal/dialectic.py +321 -0
- eal/discovery.py +127 -0
- eal/evaluator.py +619 -0
- eal/expressions.py +404 -0
- eal/extensions.py +38 -0
- eal/formatter.py +156 -0
- eal/generated/EALLexer.py +434 -0
- eal/generated/EALParser.py +6559 -0
- eal/generated/EALVisitor.py +393 -0
- eal/generated/__init__.py +1 -0
- eal/host.py +179 -0
- eal/host_redaction.py +51 -0
- eal/knowledge.py +96 -0
- eal/limits.py +88 -0
- eal/mcp_guard.py +56 -0
- eal/methods.py +463 -0
- eal/model.py +266 -0
- eal/model_context.py +55 -0
- eal/modes.py +121 -0
- eal/observation_reuse.py +128 -0
- eal/operation_contracts.py +25 -0
- eal/operations.py +155 -0
- eal/packets.py +604 -0
- eal/parser.py +407 -0
- eal/planning.py +136 -0
- eal/propositions.py +231 -0
- eal/reachability.py +85 -0
- eal/reasoning/__init__.py +25 -0
- eal/reasoning/abductive.py +45 -0
- eal/reasoning/analogical.py +41 -0
- eal/reasoning/causal.py +46 -0
- eal/reasoning/counterfactual.py +64 -0
- eal/reasoning/deductive.py +72 -0
- eal/reasoning/inductive.py +29 -0
- eal/reasoning/strategy.py +18 -0
- eal/reasoning/structured.py +11 -0
- eal/reasoning/temporal.py +47 -0
- eal/reasoning/validation.py +90 -0
- eal/registered_assessment.py +181 -0
- eal/runtime.py +454 -0
- eal/sampled_negative.py +131 -0
- eal/scope_transfer.py +59 -0
- eal/semantics.py +656 -0
- eal/server.py +79 -0
- eal/server_auth.py +37 -0
- eal/server_settings.py +258 -0
- eal/source_printer.py +129 -0
- eal/store.py +272 -0
- eal/tool_acquisition.py +383 -0
- engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
- engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
- engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
- engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
- engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
- 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
|