Skip to content

Commit 548ef18

Browse files
committed
Merge branch 'sync/pmderodat/can_reach' into 'master'
Turn "can_reach" into a real property, customizable by language specs Closes AdaCore#645 See merge request eng/libadalang/langkit!830
2 parents 04cedfb + 85ade9b commit 548ef18

File tree

21 files changed

+291
-46
lines changed

21 files changed

+291
-46
lines changed

langkit/compile_context.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,39 @@ def properties_logging(self):
12951295
"""
12961296
return any(prop.activate_tracing for prop in self.all_properties)
12971297

1298+
def check_can_reach_signature(self) -> None:
1299+
"""
1300+
Check that the root node's "can_reach" signature is conforming.
1301+
"""
1302+
from langkit.compiled_types import T
1303+
1304+
# If the language spec does not create one, the initialisation of the
1305+
# root node is supposed to create an automatic "can_reach" property.
1306+
assert self.root_grammar_class is not None
1307+
fields = self.root_grammar_class.get_abstract_node_data_dict()
1308+
can_reach = fields["can_reach"]
1309+
1310+
qualname = can_reach.qualname
1311+
args = can_reach.natural_arguments
1312+
1313+
with can_reach.diagnostic_context:
1314+
check_source_language(
1315+
can_reach.is_property,
1316+
f"{qualname} must be a property",
1317+
)
1318+
check_source_language(
1319+
can_reach.type.matches(T.Bool),
1320+
f"{qualname} must return a boolean",
1321+
)
1322+
check_source_language(
1323+
len(args) == 1 and args[0].type.matches(T.root_node),
1324+
f"{qualname} must take one argument: a bare node",
1325+
)
1326+
check_source_language(
1327+
not can_reach.uses_entity_info,
1328+
f"{qualname} cannot use entities",
1329+
)
1330+
12981331
def compute_properties_callgraphs(self) -> None:
12991332
"""
13001333
Compute forwards and backwards properties callgraphs.
@@ -1564,7 +1597,15 @@ def warn(unused_set, message):
15641597
sorted_set = sorted(
15651598
(p.qualname, p)
15661599
for p in unused_set
1567-
if not p.is_internal and not p.artificial
1600+
if (
1601+
# Never warn about implicitly referenced properties:
1602+
# internal properties, artificial properties and
1603+
# "can_reach" (can come from the language spec but used by
1604+
# lexical env lookups).
1605+
not p.is_internal
1606+
and not p.artificial
1607+
and p.indexing_name != "can_reach"
1608+
)
15681609
)
15691610
for _, p in sorted_set:
15701611
with p.diagnostic_context:
@@ -2058,6 +2099,8 @@ def lower_grammar_rules(ctx):
20582099
CompileCtx.compute_uses_entity_info_attr),
20592100
GlobalPass('compute uses envs attribute',
20602101
CompileCtx.compute_uses_envs_attr),
2102+
GlobalPass('check can_reach signature',
2103+
CompileCtx.check_can_reach_signature),
20612104
EnvSpecPass('check env specs', EnvSpec.check_spec),
20622105
GlobalPass('compute is reachable attribute',
20632106
CompileCtx.compute_is_reachable_attr),

langkit/compiled_types.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,6 +2632,10 @@ def __init__(
26322632
:param dsl_name: Name used to represent this type at the DSL level.
26332633
Useful to format diagnostics.
26342634
"""
2635+
from langkit.expressions import Property
2636+
2637+
actual_fields = list(fields)
2638+
26352639
self.raw_name = name
26362640
self.kwless_raw_name = (self.raw_name + names.Name('Node')
26372641
if is_keyword(self.raw_name) else
@@ -2708,17 +2712,44 @@ def __init__(
27082712
# Now we have an official root node type, we can create its builtin
27092713
# fields.
27102714
if is_root:
2711-
fields = self.builtin_properties() + fields
2712-
self._init_fields(fields)
2715+
# If the language spec does not define one, create a default
2716+
# "can_reach" property.
2717+
if not any(
2718+
(n if isinstance(n, str) else n.lower) == "can_reach"
2719+
for n, _ in actual_fields
2720+
):
2721+
from langkit.expressions import No, Self
2722+
can_reach = Property(
2723+
public=False,
2724+
type=T.Bool,
2725+
expr=(
2726+
lambda from_node=T.root_node:
2727+
# If there is no from_node node, assume we can access
2728+
# everything. Also assume than from_node can reach Self
2729+
# if both do not belong to the same unit.
2730+
(from_node == No(T.root_node))
2731+
| (Self.unit != from_node.unit) # type: ignore
2732+
| (Self < from_node)
2733+
),
2734+
)
2735+
# Provide a dummy location for GDB helpers
2736+
can_reach.location = Location(__file__)
2737+
can_reach.artificial = True
2738+
actual_fields.append(("can_reach", can_reach))
2739+
2740+
# Always add builtin properties first
2741+
actual_fields = self.builtin_properties() + actual_fields
2742+
2743+
self._init_fields(actual_fields)
27132744

27142745
# Encode all field names for nodes so that there is no name collision
27152746
# when considering all fields from all nodes.
2716-
for f in fields:
2747+
for f in actual_fields:
27172748
if isinstance(f, BaseField):
27182749
f._internal_name = self.name + f.name
27192750

27202751
# Make sure that all user fields for nodes are private
2721-
for _, f_v in fields:
2752+
for _, f_v in actual_fields:
27222753
with f_v.diagnostic_context:
27232754
check_source_language(
27242755
not f_v.is_user_field or
@@ -2756,7 +2787,7 @@ def __init__(
27562787
if env_spec:
27572788
env_spec.ast_node = self
27582789

2759-
self.env_spec = env_spec
2790+
self.env_spec: Opt[EnvSpec] = env_spec
27602791
"""
27612792
EnvSpec instance corresponding to this node.
27622793
"""

langkit/dsl_unparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,7 @@ def to_emit_dispatcher(prop):
14921492
${emit_field(field)}$hl
14931493
% endfor
14941494
% for prop in properties:
1495-
% if not prop.is_internal and not is_builtin_prop(prop) and not prop.is_artificial_dispatcher:
1495+
% if not prop.is_internal and not is_builtin_prop(prop) and not prop.is_artificial_dispatcher and not prop.artificial:
14961496
${emit_prop(prop, walker)}$hl
14971497
% endif
14981498
% endfor

langkit/expressions/base.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2516,22 +2516,6 @@ def bind(self, dynvar, value, expr):
25162516
return bind_expr
25172517

25182518

2519-
@auto_attr
2520-
def can_reach(self, node, from_node):
2521-
"""
2522-
Return whether `node` can reach `from_node` (two AST nodes), from a
2523-
sequential viewpoint. If elements are declared in different units, it will
2524-
always return True, eg this does not handle general visibility issues, just
2525-
sequentiality of declarations.
2526-
"""
2527-
# TODO: this could and should be a built-in property rather than an
2528-
# expression.
2529-
node_expr = construct(node, T.root_node)
2530-
from_node_expr = construct(from_node, T.root_node)
2531-
return CallExpr('Node_Can_Reach', 'Can_Reach', T.Bool,
2532-
[node_expr, from_node_expr], abstract_expr=self)
2533-
2534-
25352519
class SelfVariable(AbstractVariable):
25362520

25372521
_singleton = None

langkit/templates/pkg_implementation_body_ada.mako

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3974,15 +3974,7 @@ package body ${ada_lib_name}.Implementation is
39743974

39753975
function Can_Reach (El, From : ${T.root_node.name}) return Boolean is
39763976
begin
3977-
-- Since this function is only used to implement sequential semantics in
3978-
-- envs, we consider that elements coming from different units are
3979-
-- always visible for each other, and let the user implement language
3980-
-- specific visibility rules in the DSL.
3981-
if El = null or else From = null or else El.Unit /= From.Unit then
3982-
return True;
3983-
end if;
3984-
3985-
return El.Token_Start_Index < From.Token_Start_Index;
3977+
return ${T.root_node.kwless_raw_name}_P_Can_Reach (El, From);
39863978
end Can_Reach;
39873979

39883980
-----------------

langkit/templates/pkg_implementation_spec_ada.mako

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ private package ${ada_lib_name}.Implementation is
273273
-- The combine function on environments metadata does a boolean Or on every
274274
-- boolean component of the env metadata.
275275

276-
function Can_Reach (El, From : ${T.root_node.name}) return Boolean;
276+
function Can_Reach (El, From : ${T.root_node.name}) return Boolean
277+
with Inline;
277278
-- Return whether El can reach From, from a sequential viewpoint. If
278279
-- elements are declared in different units, it will always return True,
279280
-- eg this does not handle general visibility issues, just sequentiality of
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import lexer_example
2+
3+
@with_lexer(foo_lexer)
4+
grammar foo_grammar {
5+
@main_rule main_rule <- list*(def)
6+
def <- Def("def" name)
7+
name <- Name(@identifier)
8+
}
9+
10+
@abstract class FooNode implements Node[FooNode] {
11+
12+
fun can_reach(origin: FooNode): Bool =
13+
# "foo" defs can never be reached: "foo" Def nodes should never appear
14+
# in the result of any lexical env lookups.
15+
match node {
16+
case d : Def => not (d.name.symbol = "foo")
17+
case _ => true
18+
}
19+
20+
@export fun lookup(n: Symbol): Array[FooNode] =
21+
node.node_env().get(n, from=node)
22+
}
23+
24+
class Def : FooNode {
25+
@parse_field name: Name
26+
}
27+
28+
class Name : FooNode implements TokenNode {
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import sys
2+
3+
import libfoolang
4+
5+
6+
print("main.py: Running...")
7+
8+
ctx = libfoolang.AnalysisContext()
9+
unit = ctx.get_from_buffer(
10+
"main.txt",
11+
b"""
12+
def foo
13+
def bar
14+
def baz
15+
""",
16+
)
17+
if unit.diagnostics:
18+
for d in unit.diagnostics:
19+
print(d)
20+
sys.exit(1)
21+
22+
for name in ("foo", "bar", "baz", "unknown"):
23+
print(f"Looking for {name}:")
24+
for n in unit.root.p_lookup(name):
25+
print(f" {n.text}")
26+
print("end")
27+
28+
print("main.py: Done.")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
main.py: Running...
2+
Looking for foo:
3+
end
4+
Looking for bar:
5+
def bar
6+
end
7+
Looking for baz:
8+
def baz
9+
end
10+
Looking for unknown:
11+
end
12+
main.py: Done.
13+
Done
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
Check that providing a custom "can_reach" has expected effects on env lookups.
3+
"""
4+
5+
from langkit.dsl import ASTNode, Field, T
6+
from langkit.envs import EnvSpec, add_to_env_kv
7+
from langkit.expressions import Self, ignore, langkit_property
8+
9+
from utils import build_and_run
10+
11+
12+
class FooNode(ASTNode):
13+
14+
@langkit_property()
15+
def can_reach(origin=T.FooNode):
16+
ignore(origin)
17+
# "foo" defs can never be reached: "foo" Def nodes should never appear
18+
# in the result of any lexical env lookups.
19+
return Self.match(
20+
lambda d=T.Def: d.name.symbol != "foo",
21+
lambda _: True,
22+
)
23+
24+
@langkit_property(public=True)
25+
def lookup(n=T.Symbol):
26+
return Self.node_env.get(n, from_node=Self)
27+
28+
29+
class Def(FooNode):
30+
name = Field(type=T.Name)
31+
env_spec = EnvSpec(add_to_env_kv(Self.name.symbol, Self))
32+
33+
34+
class Name(FooNode):
35+
token_node = True
36+
37+
38+
build_and_run(lkt_file="expected_concrete_syntax.lkt", py_script="main.py")
39+
print("Done")

0 commit comments

Comments
 (0)