Skip to content

Commit 747b977

Browse files
committed
refactoring parser.correctness and adding context to check_correctnessand and check_parse_correctness error messages
1 parent b372483 commit 747b977

14 files changed

Lines changed: 207 additions & 588 deletions

src/hyperbase/builders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from hyperbase.constants import ATOM_ENCODE_TABLE
88
from hyperbase.hyperedge import Atom, Hyperedge, UniqueAtom
9-
from hyperbase.parsers.parse_result import ParseResult
9+
from hyperbase.parsers.result import ParseResult
1010

1111

1212
def str_to_atom(s: str) -> str:

src/hyperbase/cli/repl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@
2525
from hyperbase.constants import EdgeType
2626
from hyperbase.hyperedge import Atom, Hyperedge
2727
from hyperbase.parsers import Parser, get_parser, list_parsers
28-
from hyperbase.parsers.badness import badness_check
29-
from hyperbase.parsers.parse_result import ParseResult
30-
from hyperbase.parsers.repl_api import (
28+
from hyperbase.parsers.correctness import check_parse_correctness
29+
from hyperbase.parsers.repl import (
3130
CommandHandler,
3231
DiagnosticsProvider,
3332
PostResultHook,
3433
PreResultHook,
3534
ReplContext,
3635
StatsProvider,
3736
)
37+
from hyperbase.parsers.result import ParseResult
3838

3939
SETTINGS_FILE = Path.home() / ".hyperbase_repl_settings.json"
4040

@@ -2001,7 +2001,7 @@ def _render_badness(self, ctx: ReplContext) -> None:
20012001
_edge = hedge(ctx.edge)
20022002
if _edge is None:
20032003
return
2004-
badness_errors = badness_check(_edge, ctx.tokens)
2004+
badness_errors = check_parse_correctness(_edge, ctx.tokens)
20052005

20062006
self.console.print()
20072007
if not badness_errors:

src/hyperbase/correctness.py

Lines changed: 159 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
from hyperbase.hyperedge import Atom, Hyperedge
1111

1212

13-
def check_correctness(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
14-
"""Check correctness of a hyperedge, returning errors keyed by subedge."""
13+
def check_correctness(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str, int]]]:
14+
"""Check correctness of a hyperedge, returning errors keyed by subedge.
15+
16+
Each error is ``(code, message, severity)``. Correctness failures are hard
17+
grammar violations, so they all carry severity ``0`` (the most serious).
18+
"""
1519
if edge.atom:
1620
return _check_atom(edge) # type: ignore[arg-type]
1721
return _check_edge(edge)
1822

1923

20-
def _check_atom(atom: Atom) -> dict[Hyperedge, list[tuple[str, str]]]:
21-
output: dict[Hyperedge, list[tuple[str, str]]] = {}
24+
def _check_atom(atom: Atom) -> dict[Hyperedge, list[tuple[str, str, int]]]:
25+
output: dict[Hyperedge, list[tuple[str, str, int]]] = {}
2226
errors: list[tuple[str, str]] = []
2327

2428
at = atom.mtype()
@@ -30,16 +34,16 @@ def _check_atom(atom: Atom) -> dict[Hyperedge, list[tuple[str, str]]]:
3034
EdgeType.TRIGGER,
3135
EdgeType.CONJUNCTION,
3236
}:
33-
errors.append(("bad-atom-type", f"{at} is not a valid atom type"))
37+
errors.append(("bad-atom-type", f"atom '{atom}' has invalid type {at}"))
3438

3539
if len(errors) > 0:
36-
output[atom] = errors
40+
output[atom] = [(code, msg, 0) for code, msg in errors]
3741

3842
return output
3943

4044

41-
def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
42-
output: dict[Hyperedge, list[tuple[str, str]]] = {}
45+
def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str, int]]]:
46+
output: dict[Hyperedge, list[tuple[str, str, int]]] = {}
4347
errors: list[tuple[str, str]] = []
4448

4549
ct = edge[0].mtype()
@@ -51,40 +55,54 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
5155
EdgeType.TRIGGER,
5256
EdgeType.CONJUNCTION,
5357
}:
54-
errors.append(("conn-bad-type", f"connector has incorrect type: {ct}"))
58+
errors.append(
59+
(
60+
"conn-bad-type",
61+
f"connector '{edge[0]}' of '{edge}' has incorrect type: {ct}",
62+
)
63+
)
5564
# check if modifier structure is correct
5665
if ct == EdgeType.MODIFIER:
5766
if len(edge) != 2:
58-
errors.append(("mod-1-arg", "modifiers can only have one argument"))
67+
errors.append(
68+
("mod-1-arg", f"modifier edge '{edge}' can only have one argument")
69+
)
5970
# check if builder structure is correct
6071
elif ct == EdgeType.BUILDER:
6172
if len(edge) != 3:
62-
errors.append(("build-2-args", "builders can only have two arguments"))
73+
errors.append(
74+
("build-2-args", f"builder edge '{edge}' can only have two arguments")
75+
)
6376
for arg in edge[1:]:
6477
at = arg.mtype()
6578
if at != EdgeType.CONCEPT:
66-
e = f"builder argument {arg!s} has incorrect type: {at}"
79+
e = f"builder argument '{arg}' of '{edge}' has incorrect type: {at}"
6780
errors.append(("build-arg-bad-type", e))
6881
# check if trigger structure is correct
6982
elif ct == EdgeType.TRIGGER:
7083
if len(edge) != 2:
71-
errors.append(("trig-1-arg", "triggers can only have one arguments"))
84+
errors.append(
85+
("trig-1-arg", f"trigger edge '{edge}' can only have one argument")
86+
)
7287
for arg in edge[1:]:
7388
at = arg.mtype()
7489
if at not in {EdgeType.CONCEPT, EdgeType.RELATION}:
75-
e = f"trigger argument {arg!s} has incorrect type: {at}"
90+
e = f"trigger argument '{arg}' of '{edge}' has incorrect type: {at}"
7691
errors.append(("trig-bad-arg-type", e))
7792
# check if predicate structure is correct
7893
elif ct == EdgeType.PREDICATE:
7994
for arg in edge[1:]:
8095
at = arg.mtype()
8196
if at not in {EdgeType.CONCEPT, EdgeType.RELATION, EdgeType.SPECIFIER}:
82-
e = f"predicate argument {arg!s} has incorrect type: {at}"
97+
e = f"predicate argument '{arg}' of '{edge}' has incorrect type: {at}"
8398
errors.append(("pred-arg-bad-type", e))
8499
# check if conjunction structure is correct
85100
elif ct == EdgeType.CONJUNCTION and len(edge) < 3:
86101
errors.append(
87-
("conj-2-args-min", "conjunctions must have at least two arguments")
102+
(
103+
"conj-2-args-min",
104+
f"conjunction edge '{edge}' must have at least two arguments",
105+
)
88106
)
89107

90108
# check argrole counts
@@ -98,8 +116,8 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
98116
errors.append(
99117
(
100118
"pred-bad-arg-role",
101-
f"{ar} is not a valid argument role "
102-
"for connector of type P",
119+
f"'{ar}' is not a valid argument role for "
120+
f"predicate connector '{edge[0]}' in '{edge}'",
103121
)
104122
)
105123
elif ct == EdgeType.BUILDER:
@@ -108,16 +126,17 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
108126
errors.append(
109127
(
110128
"build-bad-arg-role",
111-
f"{ar} is not a valid argument role "
112-
"for connector of type B",
129+
f"'{ar}' is not a valid argument role for "
130+
f"builder connector '{edge[0]}' in '{edge}'",
113131
)
114132
)
115133

116134
if len(ars) != len(edge) - 1:
117135
errors.append(
118136
(
119137
"bad-num-argroles",
120-
"number of argroles must match number of arguments",
138+
f"number of argroles on connector '{edge[0]}' must match "
139+
f"number of arguments in '{edge}'",
121140
)
122141
)
123142

@@ -127,24 +146,140 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
127146
errors.append(
128147
(
129148
f"argrole-{role}-1-max",
130-
f"argrole {role} can only be used once",
149+
f"argrole '{role}' can only be used once in '{edge}'",
131150
)
132151
)
133152
else:
134153
errors.append(
135154
(
136155
"no-argroles",
137-
"Connectors of type P or B must have argument roles",
156+
f"connector '{edge[0]}' of '{edge}' (type P or B) must have "
157+
"argument roles",
138158
)
139159
)
140160
except RuntimeError:
141161
# malformed edges are detected elsewhere
142162
pass
143163

144164
if len(errors) > 0:
145-
output[edge] = errors
165+
output[edge] = [(code, msg, 0) for code, msg in errors]
146166

147167
for subedge in edge:
148168
output.update(check_correctness(subedge))
149169

150170
return output
171+
172+
173+
def check_structural_quality(
174+
edge: Hyperedge,
175+
) -> dict[Hyperedge, list[tuple[str, str, int]]]:
176+
"""Soft, edge-only quality checks on argroles, modifiers and junctions.
177+
178+
Unlike :func:`check_correctness` (hard grammar rules), these flag
179+
structures that are valid but unidiomatic. The third tuple value is a
180+
severity (lower is worse): ``2`` for argrole problems, ``3`` for modifier
181+
and junction issues.
182+
"""
183+
errors: dict[Hyperedge, list[tuple[str, str, int]]] = {}
184+
185+
def _visit(current_edge: Hyperedge) -> None:
186+
if not current_edge or current_edge.atom:
187+
return
188+
189+
current_errors: list[tuple[str, str, int]] = []
190+
191+
# Argrole checks
192+
try:
193+
ars = current_edge.argroles()
194+
ar_counts: Counter[str] = Counter()
195+
for ar in ars:
196+
if ar not in "masox":
197+
current_errors.append(
198+
(
199+
"bad-argrole",
200+
f"Bad argument role '{ar}' on connector "
201+
f"'{current_edge[0]}' in '{current_edge}'. "
202+
"Should be one of 'masox'.",
203+
2,
204+
)
205+
)
206+
ar_counts[ar] += 1
207+
except Exception:
208+
pass
209+
210+
# Modifier checks
211+
try:
212+
connector_type = current_edge[0].type()
213+
if len(current_edge) >= 2:
214+
target_mt = current_edge[1].mt
215+
if connector_type in {"Ma", "Md", "Mq", "Mp"} and target_mt != "C":
216+
current_errors.append(
217+
(
218+
f"bad-{connector_type.lower()}-target",
219+
f"Modifier '{current_edge}' of type '{connector_type}' "
220+
"should only be applied to concepts (type 'C'), but its "
221+
f"target '{current_edge[1]}' has type '{target_mt}'.",
222+
3,
223+
)
224+
)
225+
elif connector_type == "Mm" and target_mt != "P":
226+
current_errors.append(
227+
(
228+
"bad-mm-target",
229+
f"Modifier '{current_edge}' of type 'Mm' should only be "
230+
"applied to predicates (type 'P'), but its target "
231+
f"'{current_edge[1]}' has type '{target_mt}'.",
232+
3,
233+
)
234+
)
235+
elif connector_type == "Mn" and target_mt not in {"C", "P"}:
236+
current_errors.append(
237+
(
238+
"bad-mn-target",
239+
f"Modifier '{current_edge}' of type 'Mn' should only be "
240+
"applied to concepts (type 'C') or predicates (type 'P'), "
241+
f"but its target '{current_edge[1]}' has type "
242+
f"'{target_mt}'.",
243+
3,
244+
)
245+
)
246+
except Exception:
247+
pass
248+
249+
# Junction checks
250+
try:
251+
if current_edge[0].mt == "J":
252+
types = {child.mt for child in current_edge[1:]}
253+
if str(current_edge[0]) == ":/J/.":
254+
if types != {"R"} and types != {"C"} and types != {"C", "R"}:
255+
current_errors.append(
256+
(
257+
"bad-colon-junction-types",
258+
f"Arguments of junction '{current_edge}' should "
259+
"ideally be of either type 'R' or 'C'.",
260+
3,
261+
)
262+
)
263+
else:
264+
if len(types) > 1 and types != {"C", "R"}:
265+
current_errors.append(
266+
(
267+
"bad-junction-types",
268+
f"Arguments of junction '{current_edge}' should "
269+
"ideally be all of the same type or a combination of "
270+
"'R' and 'C' types.",
271+
3,
272+
)
273+
)
274+
except Exception:
275+
pass
276+
277+
if current_errors:
278+
errors[current_edge] = current_errors
279+
280+
for child in current_edge:
281+
_visit(child)
282+
283+
if edge:
284+
_visit(edge)
285+
return errors

src/hyperbase/hyperedge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ def arguments_with_role(self, argrole: str) -> list[Hyperedge]:
428428
edges.append(self[pos + 1])
429429
return edges
430430

431-
def check_correctness(self) -> dict[Hyperedge, list[tuple[str, str]]]:
431+
def check_correctness(self) -> dict[Hyperedge, list[tuple[str, str, int]]]:
432432
from hyperbase.correctness import check_correctness
433433

434434
return check_correctness(self)

src/hyperbase/loaders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from hyperbase.builders import hedge
99
from hyperbase.hyperedge import Hyperedge
10-
from hyperbase.parsers.parse_result import ParseResult
10+
from hyperbase.parsers.result import ParseResult
1111

1212

1313
def load_edges(

src/hyperbase/parsers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from importlib.metadata import EntryPoint, entry_points
22
from typing import Any
33

4-
from hyperbase.parsers.parse_result import ParseResult
54
from hyperbase.parsers.parser import Parser
6-
from hyperbase.parsers.repl_api import ReplContext
5+
from hyperbase.parsers.repl import ReplContext
6+
from hyperbase.parsers.result import ParseResult
77

88

99
def list_parsers() -> dict[str, EntryPoint]:

0 commit comments

Comments
 (0)