Skip to content

Commit 1a744a0

Browse files
authored
Apply :tag metadata to the Python AST (#680)
Fixes #354
1 parent 6125ebd commit 1a744a0

File tree

11 files changed

+372
-62
lines changed

11 files changed

+372
-62
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
* Added support for passing through `:tag` metadata to the generated Python AST (#354)
810

911
### Changed
1012
* Optimize calls to Python's `operator` module into their corresponding native operators (#754)

docs/differencesfromclojure.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ Host interoperability features generally match those of Clojure.
166166
Type Hinting
167167
^^^^^^^^^^^^
168168

169-
Type hints may be applied anywhere they are supported in Clojure, but the compiler does not currently use them.
170-
Support for attaching type hints to the Python AST is tracked in `#354 <https://github.com/basilisp-lang/basilisp/issues/354>`_\.
169+
Type hints may be applied anywhere they are supported in Clojure (as the ``:tag`` metadata key), but the compiler does not currently use them for any purpose.
170+
Tags provided for ``def`` names, function arguments and return values, and :lpy:form:`let` locals will be applied to the resulting Python AST by the compiler wherever possible.
171+
Particularly in the case of function arguments and return values, these tags maybe introspected from the Python `inspect <https://docs.python.org/3/library/inspect.html>`_ module.
171172
There is no need for type hints anywhere in Basilisp right now, however.
172173

173174
.. _compilation_differences:

docs/gettingstarted.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,5 @@ For systems where the shebang line allows arguments, you can use ``#!/usr/bin/en
131131

132132
.. code-block:: clojure
133133
134-
#!/usr/bin/env basilisp
134+
#!/usr/bin/env basilisp-run
135135
(println "Hello world!")

docs/pyinterop.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,37 @@ As you can see in the example above, this strategy fits neatly with the existing
211211
The ``:collect`` strategy is a better accompaniment to functions with positional arguments.
212212
With this strategy, Python keyword arguments are converted into a Basilisp map with de-munged keyword arguments and passed as the final positional argument of the function.
213213
You can use map destructuring on this final positional argument, just as you would with the map in the ``:apply`` case above.
214+
215+
Type Hinting
216+
------------
217+
218+
Basilisp supports passing type hints through to the underlying generated Python using type hints by applying the ``:tag`` metadata to certain syntax elements.
219+
220+
In Clojure, these tags are type declarations for certain primitive types.
221+
In Clojurescript, tags are type *hints* and they are only necessary in extremely limited circumstances to help the compiler.
222+
In Basilisp, tags are not used by the compiler at all.
223+
Instead, tags applied to function arguments and return values in Basilisp are applied to the underlying Python objects and are introspectable at runtime using the Python `inspect <https://docs.python.org/3/library/inspect.html>`_ standard library module.
224+
225+
Type hints may be applied to :lpy:form:`def` names, function arguments and return values, and :lpy:form:`let` local forms.
226+
227+
.. code-block:: clojure
228+
229+
(def ^python/str s "a string")
230+
231+
(defn upper
232+
^python/str [^python/str s]
233+
(.upper s))
234+
235+
(let [^python/int i 64]
236+
(* i 2))
237+
238+
.. note::
239+
240+
The reader applies ``:tag`` :ref:`metadata` automatically for symbols following the ``^`` symbol, but users may manually apply ``:tag`` metadata containing any valid expression.
241+
Python permits any valid expression in a variable annotation, so Basilisp likewise allows any valid expression.
242+
243+
.. warning::
244+
245+
Due to the complexity of supporting multi-arity functions in Python, only return annotations are preserved on the arity dispatch function.
246+
Return annotations are combined as by ``typing.Union``, so ``typing.Union[str, str] == str``.
247+
The annotations for individual arity arguments are preserved in their compiled form, but they are challenging to access programmatically.

src/basilisp/contrib/pytest/testrunner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(self, message: str, data: lmap.PersistentMap) -> None:
4444

4545
def __repr__(self):
4646
return (
47-
"basilisp.contrib.pytest..testrunner.TestFailuresInfo"
47+
"basilisp.contrib.pytest.testrunner.TestFailuresInfo"
4848
f"({self._msg}, {lrepr(self._data)})"
4949
)
5050

src/basilisp/core.lpy

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5565,8 +5565,9 @@
55655565
{:first f
55665566
:rest r})"
55675567
[body]
5568-
(let [args (first body)
5569-
body (rest body)
5568+
(let [args (first body)
5569+
arg-vec-meta (meta args)
5570+
body (rest body)
55705571

55715572
arg-groups (split-with (partial not= '&) args)
55725573
args (first arg-groups)
@@ -5582,9 +5583,11 @@
55825583
(mapcat destructure-binding)))
55835584

55845585
defs (map destructure-def args)
5585-
arg-vec (vec (concat
5586-
(map :name defs)
5587-
(map :name rest-defs)))
5586+
arg-vec (with-meta
5587+
(vec (concat
5588+
(map :name defs)
5589+
(map :name rest-defs)))
5590+
arg-vec-meta)
55885591
bindings (->> defs
55895592
(filter #(not= :symbol (:type %)))
55905593
(mapcat destructure-binding)

src/basilisp/lang/compiler/analyzer.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
SYM_PRIVATE_META_KEY,
6969
SYM_PROPERTY_META_KEY,
7070
SYM_STATICMETHOD_META_KEY,
71+
SYM_TAG_META_KEY,
7172
VAR_IS_PROTOCOL_META_KEY,
7273
SpecialForm,
7374
)
@@ -623,8 +624,8 @@ def has_meta_prop(o: Union[IMeta, Var]) -> bool:
623624

624625

625626
def _meta_getter(meta_kw: kw.Keyword) -> MetaGetter:
626-
"""Return a function which checks an object with metadata for a boolean
627-
value by meta_kw."""
627+
"""Return a function which checks an object with metadata for a value by
628+
meta_kw."""
628629

629630
def get_meta_prop(o: Union[IMeta, Var]) -> Any:
630631
return Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).value
@@ -641,6 +642,7 @@ def get_meta_prop(o: Union[IMeta, Var]) -> Any:
641642
_is_macro = _bool_meta_getter(SYM_MACRO_META_KEY)
642643
_is_no_inline = _bool_meta_getter(SYM_NO_INLINE_META_KEY)
643644
_inline_meta = _meta_getter(SYM_INLINE_META_KW)
645+
_tag_meta = _meta_getter(SYM_TAG_META_KEY)
644646

645647

646648
def _loc(form: Union[LispForm, ISeq]) -> Optional[Tuple[int, int, int, int]]:
@@ -785,6 +787,12 @@ def _call_args_ast(
785787
return args, kwargs
786788

787789

790+
def _tag_ast(form: Optional[LispForm], ctx: AnalyzerContext) -> Optional[Node]:
791+
if form is None:
792+
return None
793+
return _analyze_form(form, ctx)
794+
795+
788796
def _with_meta(gen_node):
789797
"""Wraps the node generated by gen_node in a :with-meta AST node if the
790798
original form has meta.
@@ -884,6 +892,8 @@ def _def_ast( # pylint: disable=too-many-locals,too-many-statements
884892
f"def names must be symbols, not {type(name)}", form=name
885893
)
886894

895+
tag_ast = _tag_ast(_tag_meta(name), ctx)
896+
887897
init_idx: Optional[int]
888898
children: vec.PersistentVector[kw.Keyword]
889899
if nelems == 2:
@@ -970,17 +980,26 @@ def _def_ast( # pylint: disable=too-many-locals,too-many-statements
970980
with ctx.expr_pos():
971981
init = _analyze_form(runtime.nth(form, init_idx), ctx)
972982

973-
# Attach the automatically generated inline function (if one exists) to the Var
974-
# and def metadata. We do not need to do this for user-provided inline
975-
# functions (for which `init.inline_fn` will be None) since those should
976-
# already be attached to the meta.
977-
if isinstance(init, Fn) and init.inline_fn is not None:
978-
assert isinstance(var.meta.val_at(SYM_INLINE_META_KW), bool), ( # type: ignore[union-attr]
979-
"Cannot have a user-generated inline function and an automatically "
980-
"generated inline function"
981-
)
982-
var.meta.assoc(SYM_INLINE_META_KW, init.inline_fn) # type: ignore[union-attr]
983-
def_meta = def_meta.assoc(SYM_INLINE_META_KW, init.inline_fn.form) # type: ignore[union-attr]
983+
if isinstance(init, Fn):
984+
# Attach the automatically generated inline function (if one exists) to the
985+
# Var and def metadata. We do not need to do this for user-provided inline
986+
# functions (for which `init.inline_fn` will be None) since those should
987+
# already be attached to the meta.
988+
if init.inline_fn is not None:
989+
assert isinstance(var.meta.val_at(SYM_INLINE_META_KW), bool), ( # type: ignore[union-attr]
990+
"Cannot have a user-generated inline function and an automatically "
991+
"generated inline function"
992+
)
993+
var.meta.assoc(SYM_INLINE_META_KW, init.inline_fn) # type: ignore[union-attr]
994+
def_meta = def_meta.assoc(SYM_INLINE_META_KW, init.inline_fn.form) # type: ignore[union-attr]
995+
996+
if tag_ast is not None and any(
997+
arity.tag is not None for arity in init.arities
998+
):
999+
raise AnalyzerException(
1000+
"def'ed Var :tag conflicts with defined function :tag",
1001+
form=form,
1002+
)
9841003
else:
9851004
init = None
9861005

@@ -992,6 +1011,7 @@ def _def_ast( # pylint: disable=too-many-locals,too-many-statements
9921011
doc=doc,
9931012
children=children,
9941013
env=def_node_env,
1014+
tag=tag_ast,
9951015
)
9961016

9971017
# We still have to compile the meta here down to Python source code, so
@@ -1800,6 +1820,7 @@ def _deftype_ast( # pylint: disable=too-many-locals
18001820
local=LocalType.FIELD,
18011821
is_assignable=is_mutable,
18021822
env=ctx.get_node_env(),
1823+
tag=_tag_ast(_tag_meta(field), ctx),
18031824
init=analyze_form(ctx, field_default)
18041825
if field_default is not __DEFTYPE_DEFAULT_SENTINEL
18051826
else None,
@@ -1852,6 +1873,7 @@ def __fn_method_ast( # pylint: disable=too-many-locals
18521873
raise AnalyzerException(
18531874
"function arity arguments must be a vector", form=params
18541875
)
1876+
return_tag = _tag_ast(_tag_meta(params), ctx)
18551877

18561878
has_vargs, vargs_idx = False, 0
18571879
param_nodes = []
@@ -1870,6 +1892,7 @@ def __fn_method_ast( # pylint: disable=too-many-locals
18701892
form=s,
18711893
name=s.name,
18721894
local=LocalType.ARG,
1895+
tag=_tag_ast(_tag_meta(s), ctx),
18731896
arg_id=i,
18741897
is_variadic=False,
18751898
env=ctx.get_node_env(),
@@ -1880,7 +1903,6 @@ def __fn_method_ast( # pylint: disable=too-many-locals
18801903
if has_vargs:
18811904
try:
18821905
vargs_sym = params[vargs_idx + 1]
1883-
18841906
if not isinstance(vargs_sym, sym.Symbol):
18851907
raise AnalyzerException(
18861908
"function rest parameter name must be a symbol", form=vargs_sym
@@ -1890,6 +1912,7 @@ def __fn_method_ast( # pylint: disable=too-many-locals
18901912
form=vargs_sym,
18911913
name=vargs_sym.name,
18921914
local=LocalType.ARG,
1915+
tag=_tag_ast(_tag_meta(vargs_sym), ctx),
18931916
arg_id=vargs_idx + 1,
18941917
is_variadic=True,
18951918
env=ctx.get_node_env(),
@@ -1911,6 +1934,7 @@ def __fn_method_ast( # pylint: disable=too-many-locals
19111934
form=form,
19121935
loop_id=fn_loop_id,
19131936
params=vec.vector(param_nodes),
1937+
tag=return_tag,
19141938
is_variadic=has_vargs,
19151939
fixed_arity=len(param_nodes) - int(has_vargs),
19161940
body=Do(
@@ -2472,6 +2496,7 @@ def _let_ast(form: ISeq, ctx: AnalyzerContext) -> Let:
24722496
form=name,
24732497
name=name.name,
24742498
local=LocalType.LET,
2499+
tag=_tag_ast(_tag_meta(name), ctx),
24752500
init=_analyze_form(value, ctx),
24762501
children=vec.v(INIT),
24772502
env=ctx.get_node_env(),

src/basilisp/lang/compiler/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class SpecialForm:
4949
SYM_NO_WARN_WHEN_UNUSED_META_KEY = kw.keyword("no-warn-when-unused")
5050
SYM_REDEF_META_KEY = kw.keyword("redef")
5151
SYM_STATICMETHOD_META_KEY = kw.keyword("staticmethod")
52+
SYM_TAG_META_KEY = kw.keyword("tag")
5253

5354
ARGLISTS_KW = kw.keyword("arglists")
5455
COL_KW = kw.keyword("col")

0 commit comments

Comments
 (0)