Skip to content

Commit a5ff88a

Browse files
committed
Merge branch 'pmderodat/lkt-error-nodes' into 'master'
Lkt lowering: add checks for error nodes See merge request eng/libadalang/langkit!857
2 parents 65e2dab + ca2c4bd commit a5ff88a

File tree

18 files changed

+154
-125
lines changed

18 files changed

+154
-125
lines changed

langkit/compile_context.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
from __future__ import annotations
1313

1414
from collections import defaultdict
15-
from contextlib import contextmanager
15+
from contextlib import AbstractContextManager, contextmanager
1616
from enum import Enum
1717
from functools import reduce
1818
import importlib
1919
import os
2020
from os import path
21-
from typing import (Any, Callable, ContextManager, Dict, List, Optional, Set,
21+
from typing import (Any, Callable, Dict, Iterator, List, Optional, Set,
2222
TYPE_CHECKING, Tuple, Union, cast)
2323

2424
from funcy import lzip
@@ -852,19 +852,30 @@ def actual_version(self) -> str:
852852
def actual_build_date(self) -> str:
853853
return self.build_date or "undefined"
854854

855-
def lkt_context(self, lkt_node: L.LktNode) -> ContextManager[None]:
855+
@staticmethod
856+
def lkt_context(
857+
lkt_node: L.LktNode | None
858+
) -> AbstractContextManager[None]:
856859
"""
857860
Context manager to set the diagnostic context to the given node.
858861
859862
:param lkt_node: Node to use as a reference for this diagnostic
860-
context.
863+
context. If it is ``None``, leave the diagnostic context unchanged.
861864
"""
862-
# Invalid type passed here will fail much later and only if a
863-
# check_source_language call fails. To ease debugging, check that
864-
# "lkt_node" has the right type here.
865-
assert isinstance(lkt_node, L.LktNode)
865+
if lkt_node is None:
866+
@contextmanager
867+
def null_ctx_mgr() -> Iterator[None]:
868+
yield
869+
870+
return null_ctx_mgr()
871+
872+
else:
873+
# Invalid type passed here will fail much later and only if a
874+
# check_source_language call fails. To ease debugging, check that
875+
# "lkt_node" has the right type here.
876+
assert isinstance(lkt_node, L.LktNode)
866877

867-
return diagnostic_context(Location.from_lkt_node(lkt_node))
878+
return diagnostic_context(Location.from_lkt_node(lkt_node))
868879

869880
@staticmethod
870881
def lkt_doc(full_decl):

langkit/compiled_types.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

33
from collections import OrderedDict
4+
from contextlib import AbstractContextManager
45
from dataclasses import dataclass
56
import difflib
67
from itertools import count, takewhile
78
import pipes
89
from typing import (
9-
Callable, ClassVar, ContextManager, Dict, Iterator, List, Optional as Opt,
10-
Sequence, Set, TYPE_CHECKING, Tuple, Union, ValuesView
10+
Callable, ClassVar, Dict, Iterator, List, Optional as Opt, Sequence, Set,
11+
TYPE_CHECKING, Tuple, Union, ValuesView
1112
)
1213

1314
from langkit import names
@@ -381,7 +382,7 @@ def uses_entity_info(self) -> bool:
381382
return self._uses_entity_info
382383

383384
@property
384-
def diagnostic_context(self) -> ContextManager[None]:
385+
def diagnostic_context(self) -> AbstractContextManager[None]:
385386
return diagnostic_context(self.location)
386387

387388
@property

langkit/dsl.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from __future__ import annotations
22

3+
from contextlib import AbstractContextManager
34
import itertools
45
from typing import (
5-
Any, ContextManager, Dict, List, Optional as Opt, TYPE_CHECKING, Tuple,
6-
Type, Union
6+
Any, Dict, List, Optional as Opt, TYPE_CHECKING, Tuple, Type, Union
77
)
88

99
from langkit.compiled_types import (
@@ -49,7 +49,7 @@ def array(cls):
4949
'{}.array'.format(cls._name.camel))
5050

5151
@classmethod
52-
def _diagnostic_context(cls) -> ContextManager[None]:
52+
def _diagnostic_context(cls) -> AbstractContextManager[None]:
5353
return diagnostic_context(cls._location)
5454

5555
@staticmethod

langkit/envs.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
from __future__ import annotations
1010

11+
from contextlib import AbstractContextManager
1112
from enum import Enum
1213
from funcy import lsplit_by
1314
from itertools import count
14-
from typing import ContextManager, Dict, List, Optional, Type, cast, overload
15+
from typing import Dict, List, Optional, Type, cast, overload
1516

1617
from langkit import names
1718
from langkit.compile_context import CompileCtx, get_context
@@ -252,7 +253,7 @@ def count(cls: Type[EnvAction], sequence: List[EnvAction]) -> int:
252253
self.actions = self.pre_actions + self.post_actions
253254

254255
@property
255-
def diagnostic_context(self) -> ContextManager[None]:
256+
def diagnostic_context(self) -> AbstractContextManager[None]:
256257
"""
257258
Diagnostic context for env specs.
258259
"""

langkit/expressions/structs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3+
from contextlib import AbstractContextManager
34
import inspect
4-
from typing import (Any as _Any, ContextManager, Dict, List, Optional,
5-
Sequence, Tuple)
5+
from typing import Any as _Any, Dict, List, Optional, Sequence, Tuple
66

77
import funcy
88

@@ -861,7 +861,7 @@ def __init__(self,
861861
self.node_data: AbstractNodeData
862862

863863
@property
864-
def diagnostic_context(self) -> ContextManager[None]:
864+
def diagnostic_context(self) -> AbstractContextManager[None]:
865865
return diagnostic_context(self.location)
866866

867867
def resolve_field(self) -> AbstractNodeData:

langkit/lexer/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import annotations
22

33
from collections import defaultdict
4+
from contextlib import AbstractContextManager
45
from itertools import count
56
import re
6-
from typing import (Any, ContextManager, Dict, Iterator, List, Optional,
7-
Sequence, Set, TYPE_CHECKING, Tuple, Type, Union, cast)
7+
from typing import (Any, Dict, Iterator, List, Optional, Sequence, Set,
8+
TYPE_CHECKING, Tuple, Type, Union, cast)
89

910
from langkit.compile_context import CompileCtx, get_context
1011
from langkit.diagnostics import (
@@ -321,7 +322,7 @@ def signature(self) -> tuple:
321322
sorted(t.signature for t in self.tokens))
322323

323324
@property
324-
def diagnostic_context(self) -> ContextManager[None]:
325+
def diagnostic_context(self) -> AbstractContextManager[None]:
325326
assert self.location is not None
326327
return diagnostic_context(self.location)
327328

langkit/lkt_lowering.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,6 +2241,28 @@ def check_trait(trait_ref: Optional[L.LktNode],
22412241
'Inheritting from an enum node is forbidden'
22422242
)
22432243

2244+
with self.ctx.lkt_context(error_node_trait_ref):
2245+
# Determine whether this node is abstract. Remember that base enum
2246+
# node types are abstract (it is their derivations that are
2247+
# concrete).
2248+
is_abstract = (
2249+
not isinstance(annotations, NodeAnnotations)
2250+
or annotations.abstract
2251+
)
2252+
if is_abstract and is_error_node:
2253+
error("Error nodes cannot be abstract")
2254+
2255+
# Determine whether this node is synthetic
2256+
is_synthetic = annotations.synthetic
2257+
if is_synthetic and is_error_node:
2258+
error("Error nodes cannot be synthetic")
2259+
2260+
if base_type and base_type.is_list and is_error_node:
2261+
error("Error nodes cannot be lists")
2262+
2263+
if is_token_node and is_error_node:
2264+
error("Error nodes cannot be token nodes")
2265+
22442266
# Lower fields. Regular nodes can hold all types of fields, but token
22452267
# nodes and enum nodes can hold only user field and properties.
22462268
allowed_field_types = (
@@ -2271,11 +2293,10 @@ def check_trait(trait_ref: Optional[L.LktNode],
22712293
doc=self.ctx.lkt_doc(decl.parent),
22722294
base=base_type,
22732295
fields=fields,
2274-
is_abstract=(not isinstance(annotations, NodeAnnotations)
2275-
or annotations.abstract),
2296+
is_abstract=is_abstract,
22762297
is_token_node=is_token_node,
22772298
is_error_node=is_error_node,
2278-
is_synthetic=annotations.synthetic,
2299+
is_synthetic=is_synthetic,
22792300
has_abstract_list=annotations.has_abstract_list,
22802301
is_enum_node=is_enum_node,
22812302
is_bool_node=is_bool_node,
@@ -2292,6 +2313,18 @@ def check_trait(trait_ref: Optional[L.LktNode],
22922313
qualifier=annotations.qualifier
22932314
)
22942315

2316+
# Reject non-null fields for error nodes. Non-null fields can come from
2317+
# this node's own declaration, or they can come from inheritance.
2318+
if is_error_node:
2319+
error_msg = "Error nodes can only have null fields"
2320+
for f in result.get_parse_fields(include_inherited=True):
2321+
if not (f.null or f.abstract):
2322+
if f.struct != result:
2323+
error(f"{error_msg}: {f.qualname} is not null")
2324+
else:
2325+
with f.diagnostic_context:
2326+
error(error_msg)
2327+
22952328
return result
22962329

22972330
def create_enum_node_alternatives(

langkit/parsers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@
2323
from __future__ import annotations
2424

2525
from collections import OrderedDict
26-
from contextlib import contextmanager
26+
from contextlib import AbstractContextManager, contextmanager
2727
import difflib
2828
from funcy import keep
2929
import inspect
3030
from itertools import count
3131
from typing import (
32-
Any, Callable, ContextManager, Dict, Iterator, List as _List, Optional,
33-
Sequence, Set, TYPE_CHECKING, Tuple, Type, Union
32+
Any, Callable, Dict, Iterator, List as _List, Optional, Sequence, Set,
33+
TYPE_CHECKING, Tuple, Type, Union
3434
)
3535

3636
import funcy
@@ -270,7 +270,7 @@ def __init__(self,
270270
If we loaded a Lkt unit, mapping of all grammar rules it contains.
271271
"""
272272

273-
def context(self) -> ContextManager[None]:
273+
def context(self) -> AbstractContextManager[None]:
274274
return diagnostic_context(self.location)
275275

276276
def _add_rule(self, name: str, parser: Parser, doc: str = "") -> None:
@@ -762,7 +762,7 @@ def set_location(self, location: Location) -> None:
762762
c.set_location(self.location)
763763

764764
@property
765-
def diagnostic_context(self) -> ContextManager[None]:
765+
def diagnostic_context(self) -> AbstractContextManager[None]:
766766
"""
767767
Helper that will return a diagnostic context manager with parameters
768768
set for the grammar definition.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import lexer_example
2+
3+
@with_lexer(foo_lexer)
4+
grammar foo_grammar {
5+
@main_rule main_rule <- or(Example(@example) | skip(ErrorDecl))
6+
}
7+
8+
@abstract class FooNode implements Node[FooNode] {
9+
}
10+
11+
@has_abstract_list class Example : FooNode implements TokenNode {
12+
}
Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,37 @@
1-
== test_abstract ==
2-
test.py:XXX: error: Error nodes cannot be abstract
1+
== test_abstract.lkt ==
2+
test_abstract.lkt:3:52: error: Error nodes cannot be abstract
3+
3 | @abstract class BaseErrorDecl : FooNode implements ErrorNode {
4+
| ^^^^^^^^^
35

4-
== test_bad_type ==
5-
test.py:XXX: error: The "error_node" field, when present, must contain a boolean
66

7-
== test_derived_not_error ==
8-
test.py:XXX: error: "error_node" annotation inconsistent with inherited node
7+
== test_error_and_list.lkt ==
8+
test_error_and_list.lkt:3:56: error: Error nodes cannot be lists
9+
3 | class ErrorDecl : ASTList[FooNode, Example] implements ErrorNode {
10+
| ^^^^^^^^^
911

10-
== test_error_and_list ==
11-
test.py:XXX: error: Error nodes cannot also be lists
1212

13-
== test_error_and_token ==
14-
test.py:XXX: error: Error nodes cannot also be token nodes
13+
== test_error_and_token.lkt ==
14+
test_error_and_token.lkt:3:38: error: Error nodes cannot be token nodes
15+
3 | class ErrorDecl : FooNode implements ErrorNode, TokenNode {
16+
| ^^^^^^^^^
1517

16-
== test_nonnull_field ==
17-
test.py:XXX: error: Only null fields allowed here
1818

19-
== test_synthetic ==
20-
test.py:XXX: error: Error nodes cannot be synthetic
19+
== test_nonnull_field.lkt ==
20+
test_nonnull_field.lkt:4:18: error: Error nodes can only have null fields
21+
4 | @parse_field f: Example
22+
| ^^^^^^^^^^
23+
24+
25+
== test_nonnull_inherited_field.lkt ==
26+
test_nonnull_inherited_field.lkt:7:1: error: Error nodes can only have null fields: BaseErrorDecl.f is not null
27+
7 | class ErrorDecl : BaseErrorDecl implements ErrorNode {
28+
| ^
29+
30+
31+
== test_synthetic.lkt ==
32+
test_synthetic.lkt:3:49: error: Error nodes cannot be synthetic
33+
3 | @synthetic class ErrorDecl : FooNode implements ErrorNode {
34+
| ^^^^^^^^^
35+
2136

2237
Done

0 commit comments

Comments
 (0)