Skip to content

Commit

Permalink
LSP: add call inlays for complex literals (#25202)
Browse files Browse the repository at this point in the history
Closes #25115.

This just involves adding some smarter AST inspection to detect what we
want to treat like "literals" for the purposes of call inlays.
Specifically, I've opted for the following:

1. Complex number literals should only have 'two' components; in the
same way that `1+1` is not a literal, neither is `1 + 1i + 1`. On the
other hand, `1 + 1i` _is_ a literal.
2. The numbers can be in any order: `1 + 1i` or `1i + 1`. This is a bit
liberal, but I think it's reasonable.
3. Double negation is not allowed, neither for real/imaginary numbers
nor complex literals.
  * So, `-4` is a "literal", but `- -4` is not.
  * Similarly, `4 - (-i)` is not a literal.

I've un-xfailed the test that locked this down, and added a separate
test for negative numbers.

Reviewed by @jabraham17 -- thanks!

## Testing
- [x] `make test-cls`
  • Loading branch information
DanilaFe authored Jun 10, 2024
2 parents 504241a + 20c47e3 commit 324cb1a
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 3 deletions.
55 changes: 54 additions & 1 deletion tools/chpl-language-server/src/chpl-language-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,59 @@
import argparse
import configargparse

REAL_NUMBERIC = (chapel.RealLiteral, chapel.IntLiteral, chapel.UintLiteral)
NUMERIC = REAL_NUMBERIC + (chapel.ImagLiteral,)


def is_basic_literal_like(node: chapel.AstNode) -> Optional[chapel.Literal]:
"""
Check for "basic" literals: basically, 1, "hello", -42, etc.
Returns the "underlying" literal removing surrounding AST (1, "hello", 42).
This helps do type comparisons in more complex checks. If the node is
not a basic literal, returns None.
"""
if isinstance(node, chapel.Literal):
return node

if (
isinstance(node, chapel.OpCall)
and node.op() == "-"
and node.num_actuals() == 1
):
# Do not recurse; do not consider --42 as a basic literal.
act = node.actual(0)
if isinstance(act, NUMERIC):
return act

return None


def is_literal_like(node: chapel.AstNode) -> bool:
if is_basic_literal_like(node):
return True

if isinstance(node, chapel.OpCall):
# A complex number is far from a literal in the AST; in fact, it
# potentially has as many as 4 AST nodes: -1 + 2i has a unary negation,
# an addition, and two "pure" literals.
op = node.op()
if (op == "+" or op == "-") and node.num_actuals() == 2:
# The left element can be a 'basic literal-like', like 1 or -42i.
# But the right element shouldn't have any operators, otherwise
# we'd get somethig that looks like 1 - -42i. So we just
# use the right argument directly.
left = is_basic_literal_like(node.actual(0))
right = node.actual(1)

real_number = REAL_NUMBERIC
imag_number = chapel.ImagLiteral
if isinstance(left, real_number) and isinstance(right, imag_number):
return True
if isinstance(left, imag_number) and isinstance(right, real_number):
return True

return False


def decl_kind(decl: chapel.NamedDecl) -> Optional[SymbolKind]:
if isinstance(decl, chapel.Module) and decl.kind() != "implicit":
Expand Down Expand Up @@ -1222,7 +1275,7 @@ def get_call_inlays(
# tuples. We don't need hints for those.
continue

if not isinstance(act, chapel.core.Literal):
if not is_literal_like(act):
# Only show named arguments for literals.
continue

Expand Down
38 changes: 36 additions & 2 deletions tools/chpl-language-server/test/call_inlays.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ async def test_call_inlays(client: LanguageClient):


@pytest.mark.asyncio
@pytest.mark.xfail
async def test_call_inlays_complex(client: LanguageClient):
"""
Ensure that call inlays are shown for complex literals in call expressions.
Expand All @@ -84,9 +83,44 @@ async def test_call_inlays_complex(client: LanguageClient):
file = """
proc foo(a) {}
foo(3i+10);
foo(3i+10.0);
foo(3.0i+10.0);
foo(3.0i+10);
foo(10+3i);
foo(10.0+3i);
foo(10.0+3.0i);
foo(10+3.0i);
// Not 'literals' in our sense:
foo(3i + 4i);
foo(1 + 1);
foo((-1) - (-i));
"""

inlays = [(pos((1, 4)), "a = ", None)]
inlays = [(pos((i, 4)), "a = ", None) for i in range(1, 9)]

async with source_file(client, file) as doc:
await check_inlay_hints(client, doc, rng((0, 0), endpos(file)), inlays)


@pytest.mark.asyncio
async def test_call_inlays_negative(client: LanguageClient):
"""
Ensure that call inlays are shown for negative literals in call expressions.
"""

file = """
proc foo(a) {}
foo(-42);
foo(-42.0);
foo(-42i);
foo(-42.0i);
// Not 'literals' in our sense:
foo(- -42);
"""

inlays = [(pos((i, 4)), "a = ", None) for i in range(1, 5)]

async with source_file(client, file) as doc:
await check_inlay_hints(client, doc, rng((0, 0), endpos(file)), inlays)
Expand Down

0 comments on commit 324cb1a

Please sign in to comment.