Skip to content

Commit 4cbf5b4

Browse files
authored
Move interop functionality to compiler (#308)
* Move interop functionality to compiler * Remove unused reader branch * One other test
1 parent bd7ca53 commit 4cbf5b4

File tree

5 files changed

+49
-146
lines changed

5 files changed

+49
-146
lines changed

src/basilisp/lang/compiler.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ def _assert_recur_is_tail(ctx: CompilerContext, form: lseq.Seq) -> None: # noqa
810810
}:
811811
_assert_no_recur(ctx, child)
812812
else:
813-
_assert_recur_is_tail(ctx, child)
813+
_assert_no_recur(ctx, child)
814814
else:
815815
if isinstance(child, lseq.Seqable):
816816
_assert_no_recur(ctx, child.seq())
@@ -1664,7 +1664,9 @@ def _resolve_macro_sym(ctx: CompilerContext, form: sym.Symbol) -> Optional[Var]:
16641664
return ctx.current_ns.find(form)
16651665

16661666

1667-
def _list_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
1667+
def _list_ast( # pylint: disable=too-many-locals
1668+
ctx: CompilerContext, form: llist.List
1669+
) -> ASTStream:
16681670
"""Generate a stream of Python AST nodes for a source code list.
16691671
16701672
Being the basis of any Lisp language, Lists have a lot of special cases
@@ -1722,6 +1724,24 @@ def _list_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
17221724
) from e
17231725
return
17241726

1727+
# Handle interop calls and properties generated dynamically (e.g.
1728+
# by a macro)
1729+
if first.name.startswith(".-"):
1730+
assert first.ns is None, "Interop property symbols may not have a namespace"
1731+
prop_name = sym.symbol(first.name[2:])
1732+
target = runtime.nth(form, 1)
1733+
yield from _interop_prop_ast(ctx, llist.l(_INTEROP_PROP, target, prop_name))
1734+
return
1735+
elif first.name.startswith("."):
1736+
assert first.ns is None, "Interop call symbols may not have a namespace"
1737+
attr_name = sym.symbol(first.name[1:])
1738+
rest = form.rest
1739+
target = rest.first
1740+
args = rest.rest
1741+
interop_form = llist.l(_INTEROP_CALL, target, attr_name, *args)
1742+
yield from _interop_call_ast(ctx, interop_form)
1743+
return
1744+
17251745
elems_nodes, elems = _collection_literal_ast(ctx, form)
17261746

17271747
# Quoted list
@@ -2083,16 +2103,10 @@ def _to_ast( # pylint: disable=too-many-branches
20832103
elif isinstance(form, str):
20842104
yield _node(ast.Str(form))
20852105
return
2086-
elif isinstance(form, bool):
2106+
elif isinstance(form, (bool, type(None))):
20872107
yield _node(ast.NameConstant(form))
20882108
return
2089-
elif isinstance(form, type(None)):
2090-
yield _node(ast.NameConstant(None))
2091-
return
2092-
elif isinstance(form, float):
2093-
yield _node(ast.Num(form))
2094-
return
2095-
elif isinstance(form, (complex, int)):
2109+
elif isinstance(form, (complex, float, int)):
20962110
yield _node(ast.Num(form))
20972111
return
20982112
elif isinstance(form, datetime):

src/basilisp/lang/reader.py

Lines changed: 9 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from basilisp.lang.typing import LispForm, IterableLispForm
3939
from basilisp.util import Maybe
4040

41-
ns_name_chars = re.compile(r"\w|-|\+|\*|\?|/|\=|\\|!|&|%|>|<|\$")
41+
ns_name_chars = re.compile(r"\w|-|\+|\*|\?|/|\=|\\|!|&|%|>|<|\$|\.")
4242
alphanumeric_chars = re.compile(r"\w")
4343
begin_num_chars = re.compile(r"[0-9\-]")
4444
num_chars = re.compile("[0-9]")
@@ -319,9 +319,6 @@ def _read_namespaced(
319319
elif allowed_suffix is not None and token == allowed_suffix:
320320
reader.next_token()
321321
name.append(token)
322-
elif not has_ns and token == ".":
323-
reader.next_token()
324-
name.append(token)
325322
else:
326323
break
327324

@@ -370,96 +367,11 @@ def _consume_whitespace(reader: StreamReader) -> None:
370367
token = reader.advance()
371368

372369

373-
def _read_interop(ctx: ReaderContext, end_token: str) -> llist.List:
374-
"""Read a Python interop call or property access.
375-
376-
The instance member access syntax permits the following iterations:
377-
378-
(. instance member & args)
379-
(.member instance & args)
380-
381-
(. instance -property)
382-
(.-property instance)
383-
384-
This function always dynamically rewrites everything into one of two
385-
canonical formats:
386-
387-
(. instance member & args)
388-
(.- instance property)
389-
390-
By using just two canonical forms, it will be much easier to parse
391-
and compile Python interop code."""
392-
reader = ctx.reader
393-
start = reader.advance()
394-
assert start == "."
395-
seq: List[LispForm] = []
396-
397-
token = reader.peek()
398-
if whitespace_chars.match(token):
399-
instance = _read_next_consuming_comment(ctx)
400-
member = _read_next_consuming_comment(ctx)
401-
402-
# There are cases (particularly with macros) where we may
403-
# not have a symbol in this spot. In those cases, we need
404-
# to expect the author used the correct form in the first
405-
# place.
406-
if isinstance(member, symbol.Symbol) and member.name.startswith("-"):
407-
seq.append(_INTEROP_PROP)
408-
member = symbol.symbol(member.name[1:])
409-
else:
410-
seq.append(_INTEROP_CALL)
411-
412-
seq.append(instance)
413-
seq.append(member)
414-
elif token == "-":
415-
reader.advance()
416-
seq.append(_INTEROP_PROP)
417-
418-
# If whitespace immediately follows, this is the form
419-
# (.- object member ...), otherwise it is (.-member object ...).
420-
# We need to support both, as the former is more commonly
421-
# the format which will appear in macros.
422-
if whitespace_chars.match(reader.peek()):
423-
instance = _read_next_consuming_comment(ctx)
424-
member = _read_next_consuming_comment(ctx)
425-
else:
426-
member = _read_next_consuming_comment(ctx)
427-
if not isinstance(member, symbol.Symbol):
428-
raise SyntaxError(f"Expected Symbol; found {type(member)}")
429-
instance = _read_next_consuming_comment(ctx)
430-
431-
seq.append(instance)
432-
seq.append(member)
433-
else:
434-
assert not whitespace_chars.match(token)
435-
seq.append(_INTEROP_CALL)
436-
member = _read_next_consuming_comment(ctx)
437-
instance = _read_next_consuming_comment(ctx)
438-
if not isinstance(member, symbol.Symbol):
439-
raise SyntaxError(f"Expected Symbol; found {type(member)}")
440-
seq.append(instance)
441-
seq.append(member)
442-
443-
while True:
444-
token = reader.peek()
445-
if token == "":
446-
raise SyntaxError(f"Unexpected EOF in list")
447-
if token == end_token:
448-
reader.next_token()
449-
return llist.list(seq)
450-
elem = _read_next(ctx)
451-
if elem is COMMENT or isinstance(elem, Comment):
452-
continue
453-
seq.append(elem)
454-
455-
456370
@_with_loc
457371
def _read_list(ctx: ReaderContext) -> llist.List:
458372
"""Read a list element from the input stream."""
459373
start = ctx.reader.advance()
460374
assert start == "("
461-
if ctx.reader.peek() == ".":
462-
return _read_interop(ctx, ")")
463375
return _read_coll(ctx, llist.list, ")", "list")
464376

465377

@@ -661,6 +573,14 @@ def _read_sym(ctx: ReaderContext) -> MaybeSymbol:
661573
ns, name = _read_namespaced(ctx, allowed_suffix="#")
662574
if not ctx.is_syntax_quoted and name.endswith("#"):
663575
raise SyntaxError("Gensym may not appear outside syntax quote")
576+
if ns is not None:
577+
if any(map(lambda s: len(s) == 0, ns.split("."))):
578+
raise SyntaxError(
579+
"All '.' separated segments of a namespace "
580+
"must contain at least one character."
581+
)
582+
if name.startswith(".") and ns is not None:
583+
raise SyntaxError("Symbols starting with '.' may not have a namespace")
664584
if ns is None:
665585
if name == "nil":
666586
return None

tests/basilisp/compiler_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,9 +568,9 @@ def test_interop_call(ns: runtime.Namespace):
568568

569569
def test_interop_prop(ns: runtime.Namespace):
570570
assert lcompile("(.-ns 'some.ns/sym)") == "some.ns"
571-
assert lcompile("(. 'some.ns/sym -ns)") == "some.ns"
571+
assert lcompile("(.- 'some.ns/sym ns)") == "some.ns"
572572
assert lcompile("(.-name 'some.ns/sym)") == "sym"
573-
assert lcompile("(. 'some.ns/sym -name)") == "sym"
573+
assert lcompile("(.- 'some.ns/sym name)") == "sym"
574574

575575
with pytest.raises(AttributeError):
576576
lcompile("(.-fake 'some.ns/sym)")

tests/basilisp/core_macros_test.lpy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
(is (= :a (-> :a)))
3131
(is (= 2 (-> 1 inc)))
3232
(is (= 1 (-> 1 inc dec)))
33-
(is (= 4 (-> 10 inc (- 7)))))
33+
(is (= 4 (-> 10 inc (- 7))))
34+
(is (= "HI" (-> "hi" .upper)))
35+
(is (= "Hi" (-> "HI" .lower .capitalize))))
3436

3537
(deftest ->>-test
3638
(is (= :a (->> :a)))

tests/basilisp/reader_test.py

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def test_symbol():
268268
assert sym.symbol("<body>") == read_str_first("<body>")
269269
assert sym.symbol("*muffs*") == read_str_first("*muffs*")
270270
assert sym.symbol("yay!") == read_str_first("yay!")
271+
assert sym.symbol(".interop") == read_str_first(".interop")
272+
assert sym.symbol("ns.name") == read_str_first("ns.name")
271273

272274
assert sym.symbol("sym", ns="ns") == read_str_first("ns/sym")
273275
assert sym.symbol("sym", ns="qualified.ns") == read_str_first("qualified.ns/sym")
@@ -290,6 +292,15 @@ def test_symbol():
290292
with pytest.raises(reader.SyntaxError):
291293
read_str_first("/sym")
292294

295+
with pytest.raises(reader.SyntaxError):
296+
read_str_first(".second.ns/name")
297+
298+
with pytest.raises(reader.SyntaxError):
299+
read_str_first("ns..third/name")
300+
301+
with pytest.raises(reader.SyntaxError):
302+
read_str_first("ns.second/.interop")
303+
293304
with pytest.raises(reader.SyntaxError):
294305
# This will raise because the default pushback depth of the
295306
# reader.StreamReader instance used by the reader is 5, so
@@ -658,50 +669,6 @@ def test_var():
658669
)
659670

660671

661-
def test_interop_call():
662-
assert llist.l(sym.symbol("."), "STRING", sym.symbol("lower")) == read_str_first(
663-
'(. "STRING" lower)'
664-
)
665-
assert llist.l(sym.symbol("."), "STRING", sym.symbol("lower")) == read_str_first(
666-
'(.lower "STRING")'
667-
)
668-
assert llist.l(
669-
sym.symbol("."), "www.google.com", sym.symbol("split"), "."
670-
) == read_str_first('(.split "www.google.com" ".")')
671-
assert llist.l(
672-
sym.symbol("."), "www.google.com", sym.symbol("split"), "."
673-
) == read_str_first('(. "www.google.com" split ".")')
674-
675-
assert llist.l(
676-
sym.symbol("."),
677-
sym.symbol("obj"),
678-
llist.l(
679-
sym.symbol("unquote"), llist.l(sym.symbol("quote"), sym.symbol("method"))
680-
),
681-
) == read_str_first("(. obj (unquote (quote method)))")
682-
683-
with pytest.raises(reader.SyntaxError):
684-
read_str_first('(."non-symbol" symbol)')
685-
686-
687-
def test_interop_prop():
688-
assert llist.l(
689-
sym.symbol(".-"), sym.symbol("sym"), sym.symbol("name")
690-
) == read_str_first("(. sym -name)")
691-
assert llist.l(
692-
sym.symbol(".-"), sym.symbol("encoder"), sym.symbol("algorithm")
693-
) == read_str_first("(.-algorithm encoder)")
694-
assert llist.l(
695-
sym.symbol(".-"), sym.symbol("name"), sym.symbol("sym")
696-
) == read_str_first("(.- name sym)")
697-
assert llist.l(sym.symbol(".-"), sym.symbol("name"), "string") == read_str_first(
698-
'(.- name "string")'
699-
)
700-
701-
with pytest.raises(reader.SyntaxError):
702-
read_str_first('(.-"string" sym)')
703-
704-
705672
def test_meta():
706673
def issubmap(m, sub):
707674
for k, subv in sub.items():

0 commit comments

Comments
 (0)