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/parser.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""ANTLR recognition into a typed lexical tree, followed by checked lowering."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
|
|
8
|
+
from antlr4 import CommonTokenStream, InputStream, ParserRuleContext
|
|
9
|
+
from antlr4.error.ErrorListener import ErrorListener
|
|
10
|
+
|
|
11
|
+
from .generated.EALLexer import EALLexer
|
|
12
|
+
from .generated.EALParser import EALParser
|
|
13
|
+
from .generated.EALVisitor import EALVisitor
|
|
14
|
+
from .limits import current_limits, using_limits
|
|
15
|
+
from .model import (Application, Argument, ArgumentationDirective, Assumption, Claim, Environment, Evidence,
|
|
16
|
+
Pattern, PatternBinding, PatternParameter, Reasoning, Objection,
|
|
17
|
+
Predicate, Program, Proposition, SourceSpan, Tool, Expression,
|
|
18
|
+
Block, Declaration, Module, SourceImport, Context, ArgumentBlock,
|
|
19
|
+
PatternGuard, ScopeTransfer)
|
|
20
|
+
from .expressions import make_predicate
|
|
21
|
+
|
|
22
|
+
MAX_SOURCE_BYTES = 1024 * 1024
|
|
23
|
+
MAX_TOKENS = 100_000
|
|
24
|
+
DEFAULT_FIELDS = {
|
|
25
|
+
'tool': ('version',),
|
|
26
|
+
'evidence': ('environment', 'tool', 'max_age', 'kind', 'input'),
|
|
27
|
+
'assumption': ('environment', 'validate', 'valid_from', 'valid_until'),
|
|
28
|
+
'claim': ('environment',),
|
|
29
|
+
'reasoning': ('method',),
|
|
30
|
+
'proposition': ('subject', 'quantity', 'unit', 'scope', 'valid_from', 'valid_until', 'query'),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EALSyntaxError(ValueError):
|
|
35
|
+
"""Recognition or structural lowering failed; no recovered tree is executed."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Errors(ErrorListener):
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.errors = []
|
|
41
|
+
|
|
42
|
+
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
|
|
43
|
+
self.errors.append(f'{line}:{column + 1}: {msg}')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _string(node):
|
|
47
|
+
value = json.loads(node.getText())
|
|
48
|
+
try:
|
|
49
|
+
value.encode('utf-8')
|
|
50
|
+
except UnicodeError as exc:
|
|
51
|
+
symbol = getattr(node, 'symbol', node)
|
|
52
|
+
raise EALSyntaxError(f'{symbol.line}:{symbol.column + 1}: String contains an unpaired Unicode surrogate') from exc
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _span(ctx):
|
|
57
|
+
text = ctx.stop.text or ''
|
|
58
|
+
lines = text.split('\n')
|
|
59
|
+
return SourceSpan(ctx.start.line, ctx.start.column + 1, ctx.stop.line + len(lines) - 1,
|
|
60
|
+
ctx.stop.column + len(text) + 1 if len(lines) == 1 else len(lines[-1]) + 1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _references(ctx):
|
|
64
|
+
return tuple(reference.getText() for reference in ctx.reference()) if ctx else ()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _ASTBuilder(EALVisitor):
|
|
68
|
+
def __init__(self, resolver=None, origin=''):
|
|
69
|
+
self.defaults = {}
|
|
70
|
+
self.resolver = resolver
|
|
71
|
+
self.origin = origin
|
|
72
|
+
self.imports = {}
|
|
73
|
+
self.import_sources = {}
|
|
74
|
+
self.import_stack = []
|
|
75
|
+
self.total_bytes = 0
|
|
76
|
+
self.total_tokens = 0
|
|
77
|
+
|
|
78
|
+
def block(self, declarations):
|
|
79
|
+
return Block(tuple(self.visit(declaration) for declaration in declarations))
|
|
80
|
+
|
|
81
|
+
def visitDeclaration(self, ctx):
|
|
82
|
+
child = ctx.getChild(0)
|
|
83
|
+
return Declaration(self.visit(child), _span(child), self.origin or None)
|
|
84
|
+
|
|
85
|
+
def visitModuleDecl(self, ctx):
|
|
86
|
+
return Module(ctx.qualifiedName().getText(), self.block(ctx.declaration()))
|
|
87
|
+
|
|
88
|
+
def visitContextDecl(self, ctx):
|
|
89
|
+
additions = {}
|
|
90
|
+
for attribute in ctx.contextAttribute():
|
|
91
|
+
name = attribute.start.text
|
|
92
|
+
if name in additions:
|
|
93
|
+
self.fail(attribute, f'Duplicate context default {name!r}')
|
|
94
|
+
if attribute.qualifiedName():
|
|
95
|
+
value = attribute.qualifiedName().getText()
|
|
96
|
+
elif attribute.identifier():
|
|
97
|
+
value = attribute.identifier().getText()
|
|
98
|
+
elif attribute.signedNumber():
|
|
99
|
+
value = float(attribute.signedNumber().getText())
|
|
100
|
+
elif attribute.jsonValue():
|
|
101
|
+
value = self.visit(attribute.jsonValue())
|
|
102
|
+
else:
|
|
103
|
+
value = _string(attribute.STRING())
|
|
104
|
+
additions[name] = value
|
|
105
|
+
previous = self.defaults
|
|
106
|
+
self.defaults = {**previous, **additions}
|
|
107
|
+
try:
|
|
108
|
+
return Context(additions, self.block(ctx.declaration()))
|
|
109
|
+
finally:
|
|
110
|
+
self.defaults = previous
|
|
111
|
+
|
|
112
|
+
def visitImportDecl(self, ctx):
|
|
113
|
+
if self.resolver is None:
|
|
114
|
+
self.fail(ctx, 'Source imports require a trusted host source resolver')
|
|
115
|
+
requested = _string(ctx.STRING())
|
|
116
|
+
if not requested or '\\' in requested or PurePosixPath(requested).is_absolute():
|
|
117
|
+
self.fail(ctx, 'Imports require a relative POSIX source path')
|
|
118
|
+
parts = []
|
|
119
|
+
for part in (PurePosixPath(self.origin).parent / requested).parts:
|
|
120
|
+
if part == '..':
|
|
121
|
+
if not parts:
|
|
122
|
+
self.fail(ctx, 'Import escapes the source root')
|
|
123
|
+
parts.pop()
|
|
124
|
+
elif part != '.':
|
|
125
|
+
parts.append(part)
|
|
126
|
+
path = '/'.join(parts)
|
|
127
|
+
if path in self.import_stack or path == self.origin:
|
|
128
|
+
self.fail(ctx, f'Source import cycle at {path!r}')
|
|
129
|
+
if len(self.import_stack) >= current_limits().expansion_depth:
|
|
130
|
+
self.fail(ctx, 'Source import depth budget exceeded')
|
|
131
|
+
try:
|
|
132
|
+
if path not in self.import_sources:
|
|
133
|
+
self.import_sources[path] = self.resolver(path)
|
|
134
|
+
source = self.import_sources[path]
|
|
135
|
+
except (ValueError, OSError, KeyError) as exc:
|
|
136
|
+
self.fail(ctx, f'Cannot resolve source import {path!r}: {exc}')
|
|
137
|
+
previous_origin, previous_defaults = self.origin, self.defaults
|
|
138
|
+
self.origin, self.defaults = path, {}
|
|
139
|
+
self.import_stack.append(path)
|
|
140
|
+
try:
|
|
141
|
+
tree = self.recognise(source)
|
|
142
|
+
if _string(tree.STRING()) != 'EAL/3':
|
|
143
|
+
self.fail(ctx, 'An imported source must declare EAL/3')
|
|
144
|
+
body = self.block(tree.declaration())
|
|
145
|
+
finally:
|
|
146
|
+
self.import_stack.pop()
|
|
147
|
+
self.origin, self.defaults = previous_origin, previous_defaults
|
|
148
|
+
digest = hashlib.sha256(source.encode('utf-8')).hexdigest()
|
|
149
|
+
self.imports[path] = digest
|
|
150
|
+
return SourceImport(ctx.identifier().getText(), path, body, digest)
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def fail(ctx, message):
|
|
154
|
+
raise EALSyntaxError(f'{ctx.start.line}:{ctx.start.column + 1}: {message}')
|
|
155
|
+
|
|
156
|
+
def fields(self, ctx, wrappers, kind, required):
|
|
157
|
+
result, predicates = {}, []
|
|
158
|
+
for wrapper in wrappers:
|
|
159
|
+
child = wrapper.getChild(0)
|
|
160
|
+
field = child if isinstance(child, ParserRuleContext) else wrapper
|
|
161
|
+
name = field.start.text
|
|
162
|
+
if name == 'require':
|
|
163
|
+
predicates.append(self.visit(field))
|
|
164
|
+
continue
|
|
165
|
+
if name in result:
|
|
166
|
+
self.fail(field, f'Duplicate field {name!r}')
|
|
167
|
+
if name in ('proposition', 'transfer'):
|
|
168
|
+
value = self.visit(field)
|
|
169
|
+
elif name == 'result':
|
|
170
|
+
value = make_predicate(self.visit(field.expression()))
|
|
171
|
+
elif hasattr(field, 'qualifiedName'):
|
|
172
|
+
value = field.qualifiedName().getText()
|
|
173
|
+
elif hasattr(field, 'identifier'):
|
|
174
|
+
value = field.identifier().getText()
|
|
175
|
+
elif hasattr(field, 'jsonValue'):
|
|
176
|
+
value = self.visit(field.jsonValue())
|
|
177
|
+
elif hasattr(field, 'referenceList'):
|
|
178
|
+
value = _references(field.referenceList())
|
|
179
|
+
elif hasattr(field, 'signedNumber'):
|
|
180
|
+
value = float(field.signedNumber().getText())
|
|
181
|
+
else:
|
|
182
|
+
value = _string(field.STRING())
|
|
183
|
+
result[name] = value
|
|
184
|
+
for name in DEFAULT_FIELDS.get(kind, ()):
|
|
185
|
+
if name not in result and name in self.defaults:
|
|
186
|
+
result[name] = self.defaults[name]
|
|
187
|
+
missing = set(required) - result.keys()
|
|
188
|
+
if missing:
|
|
189
|
+
self.fail(ctx, 'Missing field after context inheritance: ' + ', '.join(sorted(missing)))
|
|
190
|
+
result['predicates'] = tuple(predicates)
|
|
191
|
+
if kind == 'evidence' and not predicates:
|
|
192
|
+
self.fail(ctx, 'Evidence requires at least one require predicate')
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
def groups(self, ctx, rule):
|
|
196
|
+
result = {}
|
|
197
|
+
for group in getattr(ctx, rule)():
|
|
198
|
+
name = group.start.text
|
|
199
|
+
if name in result:
|
|
200
|
+
self.fail(group, f'Duplicate support group {name!r}')
|
|
201
|
+
result[name] = _references(group)
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def argument(self, name, ctx):
|
|
205
|
+
groups = self.groups(ctx.support(), 'supportGroup')
|
|
206
|
+
return Argument(name, ctx.conclusionRef.getText(), ctx.reasoningRef.getText(),
|
|
207
|
+
groups.get('evidence', ()), groups.get('assumptions', ()), groups.get('premises', ()),
|
|
208
|
+
ctx.bindingRef.getText() if ctx.bindingRef else None)
|
|
209
|
+
|
|
210
|
+
def visitArgumentationDirective(self, ctx):
|
|
211
|
+
kind = ctx.start.text
|
|
212
|
+
names = ctx.qualifiedName()
|
|
213
|
+
text = ctx.signedNumber().getText() if kind == 'rank' else None
|
|
214
|
+
return ArgumentationDirective(kind, names[0].getText(), names[1].getText() if kind in ('contrary', 'prefer') else None,
|
|
215
|
+
int(text) if text is not None and text.isdigit() else None,
|
|
216
|
+
_string(ctx.STRING()), _span(ctx), self.origin or None)
|
|
217
|
+
|
|
218
|
+
def visitEnvironmentDecl(self, ctx):
|
|
219
|
+
return Environment(ctx.qualifiedName().getText(), tuple(self.visit(p) for p in ctx.predicate()))
|
|
220
|
+
|
|
221
|
+
def visitToolDecl(self, ctx):
|
|
222
|
+
f = self.fields(ctx, ctx.versionField(), 'tool', ('version',))
|
|
223
|
+
return Tool(ctx.qualifiedName().getText(), f['version'])
|
|
224
|
+
|
|
225
|
+
def visitEvidenceDecl(self, ctx):
|
|
226
|
+
f = self.fields(ctx, ctx.evidenceField(), 'evidence', ('tool', 'kind', 'environment', 'max_age'))
|
|
227
|
+
return Evidence(ctx.qualifiedName().getText(), f['tool'], f['kind'], f['environment'], f['max_age'],
|
|
228
|
+
f.get('input', {}), f['predicates'])
|
|
229
|
+
|
|
230
|
+
def visitAssumptionDecl(self, ctx):
|
|
231
|
+
f = self.fields(ctx, ctx.assumptionField(), 'assumption', ('statement', 'environment', 'validate'))
|
|
232
|
+
return Assumption(ctx.qualifiedName().getText(), f['statement'], f['environment'], f['validate'],
|
|
233
|
+
f.get('valid_from'), f.get('valid_until'))
|
|
234
|
+
|
|
235
|
+
def visitReasoningDecl(self, ctx):
|
|
236
|
+
f = self.fields(ctx, ctx.reasoningField(), 'reasoning', ('method', 'rationale'))
|
|
237
|
+
return Reasoning(ctx.qualifiedName().getText(), f['method'], f['rationale'], f.get('backing', ()),
|
|
238
|
+
f['predicates'], f.get('transfer'))
|
|
239
|
+
|
|
240
|
+
def visitTransferField(self, ctx):
|
|
241
|
+
names = [name.getText() for name in ctx.qualifiedName()]
|
|
242
|
+
return ScopeTransfer(*names, _string(ctx.STRING()))
|
|
243
|
+
|
|
244
|
+
def visitClaimDecl(self, ctx):
|
|
245
|
+
f = self.fields(ctx, ctx.claimField(), 'claim', ('statement', 'environment'))
|
|
246
|
+
return Claim(ctx.qualifiedName().getText(), f['statement'], f['environment'], f.get('proposition'))
|
|
247
|
+
|
|
248
|
+
def visitPropositionDecl(self, ctx):
|
|
249
|
+
names = ('subject', 'quantity', 'unit', 'scope', 'valid_from', 'valid_until', 'result', 'query')
|
|
250
|
+
f = self.fields(ctx, ctx.propositionField(), 'proposition', names)
|
|
251
|
+
return Proposition(*(f[name] for name in names))
|
|
252
|
+
|
|
253
|
+
def visitArgumentDecl(self, ctx):
|
|
254
|
+
name = ctx.qualifiedName().getText()
|
|
255
|
+
flow = self.argument(name, ctx.argumentFlow())
|
|
256
|
+
return ArgumentBlock(name, self.block(ctx.declaration()), flow) if ctx.declaration() or ctx.getChild(2).getText() == '{' else flow
|
|
257
|
+
|
|
258
|
+
def visitPatternDecl(self, ctx):
|
|
259
|
+
parameters = tuple(self.visit(parameter) for parameter in ctx.patternParameter())
|
|
260
|
+
decreasing = ctx.identifier().getText() if ctx.identifier() else None
|
|
261
|
+
if ctx.argumentFlow():
|
|
262
|
+
flow = self.argument(ctx.qualifiedName().getText(), ctx.argumentFlow())
|
|
263
|
+
return Pattern(flow.name, parameters, flow.conclusion, flow.reasoning, flow.evidence,
|
|
264
|
+
flow.assumptions, flow.premises, flow.binding, decreases=decreasing)
|
|
265
|
+
return Pattern(ctx.qualifiedName().getText(), parameters, '', '', (), (), (),
|
|
266
|
+
body=self.block(ctx.declaration()), decreases=decreasing)
|
|
267
|
+
|
|
268
|
+
def visitPatternParameter(self, ctx):
|
|
269
|
+
kind = ctx.parameterKind().getText() + ('[]' if '[' in ctx.getText() else '')
|
|
270
|
+
return PatternParameter(ctx.identifier().getText(), kind)
|
|
271
|
+
|
|
272
|
+
def visitApplicationDecl(self, ctx):
|
|
273
|
+
names = ctx.qualifiedName()
|
|
274
|
+
return Application(names[0].getText(), names[1].getText(),
|
|
275
|
+
tuple(self.visit(binding) for binding in ctx.patternBinding()))
|
|
276
|
+
|
|
277
|
+
def visitPatternBinding(self, ctx):
|
|
278
|
+
values = ctx.bindingValue()
|
|
279
|
+
refs = _references(values)
|
|
280
|
+
reference = refs if values.getText().startswith('[') else refs[0]
|
|
281
|
+
return PatternBinding(ctx.identifier().getText(), reference)
|
|
282
|
+
|
|
283
|
+
def visitPatternGuard(self, ctx):
|
|
284
|
+
before, after = [], []
|
|
285
|
+
target = before
|
|
286
|
+
for child in ctx.children:
|
|
287
|
+
if child.getText() == 'else':
|
|
288
|
+
target = after
|
|
289
|
+
elif isinstance(child, EALParser.DeclarationContext):
|
|
290
|
+
target.append(child)
|
|
291
|
+
return PatternGuard(ctx.identifier().getText(), self.block(before),
|
|
292
|
+
self.block(after) if 'else' in [child.getText() for child in ctx.children] else None)
|
|
293
|
+
|
|
294
|
+
def visitObjectionDecl(self, ctx):
|
|
295
|
+
groups = self.groups(ctx.objectionSupport(), 'objectionGroup')
|
|
296
|
+
return Objection(ctx.qualifiedName().getText(), ctx.targetKind().getText(), ctx.reference().getText(),
|
|
297
|
+
groups.get('evidence', ()), groups.get('premises', ()))
|
|
298
|
+
|
|
299
|
+
def visitPredicate(self, ctx):
|
|
300
|
+
return make_predicate(self.visit(ctx.expression()))
|
|
301
|
+
|
|
302
|
+
def visitExpression(self, ctx):
|
|
303
|
+
return self.visit(ctx.orExpression())
|
|
304
|
+
|
|
305
|
+
def binary(self, ctx):
|
|
306
|
+
result = self.visit(ctx.getChild(0))
|
|
307
|
+
for index in range(2, ctx.getChildCount(), 2):
|
|
308
|
+
symbol = ctx.getChild(index - 1).getText()
|
|
309
|
+
right = self.visit(ctx.getChild(index))
|
|
310
|
+
left = result
|
|
311
|
+
if symbol in ('==', '!=', '<', '<=', '>', '>=') and left.kind == 'literal' and type(left.value) is str:
|
|
312
|
+
left = Expression('field', left.value)
|
|
313
|
+
result = Expression('binary', symbol, (left, right))
|
|
314
|
+
return result
|
|
315
|
+
|
|
316
|
+
visitOrExpression = binary
|
|
317
|
+
visitAndExpression = binary
|
|
318
|
+
visitComparisonExpression = binary
|
|
319
|
+
visitAdditiveExpression = binary
|
|
320
|
+
visitMultiplicativeExpression = binary
|
|
321
|
+
|
|
322
|
+
def visitUnaryExpression(self, ctx):
|
|
323
|
+
return (Expression('unary', ctx.start.text, (self.visit(ctx.unaryExpression()),))
|
|
324
|
+
if ctx.unaryExpression() else self.visit(ctx.primaryExpression()))
|
|
325
|
+
|
|
326
|
+
def visitPrimaryExpression(self, ctx):
|
|
327
|
+
if ctx.qualifiedName():
|
|
328
|
+
name = ctx.qualifiedName().getText()
|
|
329
|
+
if '(' not in [child.getText() for child in ctx.children]:
|
|
330
|
+
return Expression('field', name)
|
|
331
|
+
arguments = tuple(self.visit(expr) for expr in ctx.expression())
|
|
332
|
+
if name == 'field':
|
|
333
|
+
if len(arguments) != 1 or arguments[0].kind != 'literal' or type(arguments[0].value) is not str:
|
|
334
|
+
self.fail(ctx, 'field requires one literal property path')
|
|
335
|
+
return Expression('field', arguments[0].value)
|
|
336
|
+
return Expression('call', name, arguments)
|
|
337
|
+
if ctx.jsonValue():
|
|
338
|
+
return Expression('literal', self.visit(ctx.jsonValue()))
|
|
339
|
+
return self.visit(ctx.expression(0))
|
|
340
|
+
|
|
341
|
+
def visitJsonScalar(self, ctx):
|
|
342
|
+
return _string(ctx.STRING()) if ctx.STRING() else json.loads(ctx.getText())
|
|
343
|
+
|
|
344
|
+
def visitJsonValue(self, ctx):
|
|
345
|
+
return self.visit(ctx.getChild(0))
|
|
346
|
+
|
|
347
|
+
def visitJsonObject(self, ctx):
|
|
348
|
+
names = [_string(name) for name in ctx.STRING()]
|
|
349
|
+
if len(set(names)) != len(names):
|
|
350
|
+
self.fail(ctx, 'Duplicate JSON object key')
|
|
351
|
+
return dict(zip(names, (self.visit(value) for value in ctx.jsonValue())))
|
|
352
|
+
|
|
353
|
+
def visitJsonArray(self, ctx):
|
|
354
|
+
return [self.visit(value) for value in ctx.jsonValue()]
|
|
355
|
+
|
|
356
|
+
def recognise(self, source):
|
|
357
|
+
if type(source) is not str:
|
|
358
|
+
raise EALSyntaxError('Source must be a string')
|
|
359
|
+
try:
|
|
360
|
+
encoded = source.encode('utf-8')
|
|
361
|
+
except UnicodeError as exc:
|
|
362
|
+
raise EALSyntaxError('Source must be valid Unicode') from exc
|
|
363
|
+
self.total_bytes += len(encoded)
|
|
364
|
+
if self.total_bytes > current_limits().source_bytes:
|
|
365
|
+
raise EALSyntaxError(f'Source bundle exceeds {current_limits().source_bytes} bytes')
|
|
366
|
+
listener = _Errors()
|
|
367
|
+
lexer = EALLexer(InputStream(source))
|
|
368
|
+
lexer.removeErrorListeners(); lexer.addErrorListener(listener)
|
|
369
|
+
tokens = CommonTokenStream(lexer)
|
|
370
|
+
tokens.fill()
|
|
371
|
+
self.total_tokens += len(tokens.tokens)
|
|
372
|
+
if self.total_tokens > current_limits().source_tokens:
|
|
373
|
+
raise EALSyntaxError(f'Source bundle exceeds {current_limits().source_tokens} tokens')
|
|
374
|
+
parser = EALParser(tokens)
|
|
375
|
+
parser.removeErrorListeners(); parser.addErrorListener(listener)
|
|
376
|
+
try:
|
|
377
|
+
tree = parser.program()
|
|
378
|
+
except RecursionError as exc:
|
|
379
|
+
raise EALSyntaxError('Source nesting exceeds parser resources') from exc
|
|
380
|
+
if listener.errors:
|
|
381
|
+
raise EALSyntaxError('\n'.join(listener.errors[:20]))
|
|
382
|
+
return tree
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def parse(source: str, *, limits=None, resolver=None, origin='') -> Program:
|
|
386
|
+
"""Recognise, resolve lexical names and expand closed typed patterns.
|
|
387
|
+
|
|
388
|
+
A resolver supplies source text only. Filesystem access is owned by the host;
|
|
389
|
+
parsing and evaluation never execute collectors or imported Python code.
|
|
390
|
+
"""
|
|
391
|
+
with using_limits(limits or current_limits()):
|
|
392
|
+
builder = _ASTBuilder(resolver, origin)
|
|
393
|
+
tree = builder.recognise(source)
|
|
394
|
+
try:
|
|
395
|
+
body = builder.block(tree.declaration())
|
|
396
|
+
except EALSyntaxError:
|
|
397
|
+
raise
|
|
398
|
+
except (RecursionError, ValueError, OverflowError) as exc:
|
|
399
|
+
raise EALSyntaxError(f'Invalid source data: {exc}') from exc
|
|
400
|
+
encoded = source.encode('utf-8')
|
|
401
|
+
digest = hashlib.sha256(encoded).hexdigest()
|
|
402
|
+
if builder.imports:
|
|
403
|
+
digest = hashlib.sha256(json.dumps({'source': digest, 'imports': builder.imports},
|
|
404
|
+
sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
|
405
|
+
program = Program(_string(tree.STRING()), digest, authored=body, imports=builder.imports)
|
|
406
|
+
from .composition import lower_program
|
|
407
|
+
return lower_program(program)
|
eal/planning.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Plan the evidence needed to assess a claim's complete argument graph.
|
|
2
|
+
|
|
3
|
+
Planning reads a validated EAL/3 programme. It has no collection, storage or
|
|
4
|
+
reasoning effects: the host decides how to execute the selected requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .model import Program
|
|
13
|
+
from .semantics import objection_scopes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ClaimClosure:
|
|
18
|
+
"""Declarations which may affect a claim under the authored calculus."""
|
|
19
|
+
|
|
20
|
+
claims: frozenset[str]
|
|
21
|
+
arguments: frozenset[str]
|
|
22
|
+
objections: frozenset[str]
|
|
23
|
+
evidence: frozenset[str]
|
|
24
|
+
assumptions: frozenset[str]
|
|
25
|
+
reasoning: frozenset[str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class PlannedCall:
|
|
30
|
+
"""A declared acquisition request before its host binding is resolved."""
|
|
31
|
+
|
|
32
|
+
evidence_id: str
|
|
33
|
+
tool: str
|
|
34
|
+
tool_version: str
|
|
35
|
+
evidence_kind: str
|
|
36
|
+
environment: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class EvidencePlan:
|
|
41
|
+
"""Complete claim dependencies and collection requests in source order."""
|
|
42
|
+
|
|
43
|
+
claim: str
|
|
44
|
+
closure: ClaimClosure
|
|
45
|
+
calls: tuple[PlannedCall, ...]
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def evidence_ids(self) -> tuple[str, ...]:
|
|
49
|
+
return tuple(call.evidence_id for call in self.calls)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def estimated_calls(self) -> int:
|
|
53
|
+
"""Maximum collector invocations without host-approved reuse."""
|
|
54
|
+
return len(self.calls)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EvidencePlanner:
|
|
58
|
+
"""Indexed traversal of support, attack and defence dependencies.
|
|
59
|
+
|
|
60
|
+
Every alternative derivation is included. Objection premise claims enter
|
|
61
|
+
the same claim worklist, and objections to objections are followed to a
|
|
62
|
+
fixed point. Scope checks match the authored dialectic. The programme
|
|
63
|
+
must already have passed semantic validation before this planner is used.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, program: Program):
|
|
67
|
+
self.program = program
|
|
68
|
+
by_conclusion: dict[str, list[str]] = {}
|
|
69
|
+
for name, argument in program.arguments.items():
|
|
70
|
+
by_conclusion.setdefault(argument.conclusion, []).append(name)
|
|
71
|
+
self._by_conclusion = by_conclusion
|
|
72
|
+
by_target: dict[tuple[str, str], list[str]] = {}
|
|
73
|
+
for name, objection in program.objections.items():
|
|
74
|
+
by_target.setdefault((objection.target_kind, objection.target), []).append(name)
|
|
75
|
+
self._by_target = by_target
|
|
76
|
+
self._scopes = objection_scopes(program)
|
|
77
|
+
|
|
78
|
+
def plan(self, target: str) -> EvidencePlan:
|
|
79
|
+
if target not in self.program.claims:
|
|
80
|
+
raise ValueError(f"Unknown claim {target!r}")
|
|
81
|
+
|
|
82
|
+
program = self.program
|
|
83
|
+
claims: set[str] = set()
|
|
84
|
+
arguments: set[str] = set()
|
|
85
|
+
objections: set[str] = set()
|
|
86
|
+
evidence: set[str] = set()
|
|
87
|
+
assumptions: set[str] = set()
|
|
88
|
+
reasoning: set[str] = set()
|
|
89
|
+
pending = deque([("claim", target)])
|
|
90
|
+
|
|
91
|
+
def attacks(kind: str, identifier: str, environment: str) -> None:
|
|
92
|
+
for objection_id in self._by_target.get((kind, identifier), ()):
|
|
93
|
+
if self._scopes[objection_id] == {environment}:
|
|
94
|
+
pending.append(("objection", objection_id))
|
|
95
|
+
|
|
96
|
+
while pending:
|
|
97
|
+
kind, name = pending.popleft()
|
|
98
|
+
if kind == "claim":
|
|
99
|
+
if name in claims:
|
|
100
|
+
continue
|
|
101
|
+
claims.add(name)
|
|
102
|
+
environment = program.claims[name].environment
|
|
103
|
+
pending.extend(("argument", item) for item in self._by_conclusion.get(name, ()))
|
|
104
|
+
attacks("claim", name, environment)
|
|
105
|
+
elif kind == "argument":
|
|
106
|
+
if name in arguments:
|
|
107
|
+
continue
|
|
108
|
+
arguments.add(name)
|
|
109
|
+
argument = program.arguments[name]
|
|
110
|
+
environment = program.claims[argument.conclusion].environment
|
|
111
|
+
evidence.update(argument.evidence)
|
|
112
|
+
reasoning.add(argument.reasoning)
|
|
113
|
+
evidence.update(program.reasoning[argument.reasoning].backing)
|
|
114
|
+
assumptions.update(argument.assumptions)
|
|
115
|
+
evidence.update(program.assumptions[item].validation for item in argument.assumptions)
|
|
116
|
+
pending.extend(("claim", item) for item in argument.premises)
|
|
117
|
+
attacks("argument", name, environment)
|
|
118
|
+
attacks("reasoning", argument.reasoning, environment)
|
|
119
|
+
for item in argument.assumptions:
|
|
120
|
+
attacks("assumption", item, environment)
|
|
121
|
+
else:
|
|
122
|
+
if name in objections:
|
|
123
|
+
continue
|
|
124
|
+
objections.add(name)
|
|
125
|
+
objection = program.objections[name]
|
|
126
|
+
evidence.update(objection.evidence)
|
|
127
|
+
pending.extend(("claim", item) for item in objection.premises)
|
|
128
|
+
attacks("objection", name, next(iter(self._scopes[name])))
|
|
129
|
+
|
|
130
|
+
closure = ClaimClosure(*(frozenset(group) for group in
|
|
131
|
+
(claims, arguments, objections, evidence, assumptions, reasoning)))
|
|
132
|
+
calls = tuple(PlannedCall(name, program.evidence[name].tool,
|
|
133
|
+
program.tools[program.evidence[name].tool].version,
|
|
134
|
+
program.evidence[name].kind, program.evidence[name].environment)
|
|
135
|
+
for name in program.evidence if name in evidence)
|
|
136
|
+
return EvidencePlan(target, closure, calls)
|