Skip to content

Commit 6e8ddec

Browse files
authored
Add metadata to functions (#347)
* Add async/await support to Basilisp * Parsing and generating * Testssss * Bust that cache * I dunno man whatever * Specify cache location specifically * Ugh * Ok go back to the old version * Small formatting change * Include function meta on all functions * Fix linting and mypy errors * Stuff * Fix async bug * Fix mypy errors * Tests for function with meta * Retain function meta through fn macro * Test multi-arity fn
1 parent fb8456b commit 6e8ddec

File tree

7 files changed

+299
-60
lines changed

7 files changed

+299
-60
lines changed

src/basilisp/core/__init__.lpy

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,14 +2183,15 @@
21832183
(fn-arity-with-destructuring body)
21842184

21852185
(seq? (first body))
2186-
(map fn-arity-with-destructuring body)
2186+
(apply list (map fn-arity-with-destructuring body))
21872187

21882188
:else
21892189
body)]
2190-
(if name
2191-
`(fn* ~name
2192-
~@arities)
2193-
`(fn* ~@arities))))
2190+
(as-> arities $
2191+
(cond->> $ name (cons name))
2192+
(cons 'fn* $)
2193+
(cond-> $
2194+
(meta &form) (with-meta (meta &form))))))
21942195

21952196
(defn destructure
21962197
"Take a [binding expr] pair (as from a let block) and produce all of the

src/basilisp/lang/compiler/generator.py

Lines changed: 127 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@ def _is_redefable(v: Var) -> bool:
430430
_FIND_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.find_safe")
431431
_COLLECT_ARGS_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._collect_args")
432432
_COERCE_SEQ_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}.to_seq")
433+
_BASILISP_FN_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._basilisp_fn")
434+
_FN_WITH_ATTRS_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._with_attrs")
433435
_TRAMPOLINE_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._trampoline")
434436
_TRAMPOLINE_ARGS_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._TrampolineArgs")
435437

@@ -568,6 +570,14 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
568570
assert isinstance(node.init, Fn)
569571
def_ast = _fn_to_py_ast(ctx, node.init, def_name=defsym.name)
570572
is_defn = True
573+
elif (
574+
node.init.op == NodeOp.WITH_META
575+
and isinstance(node.init, WithMeta)
576+
and node.init.expr.op == NodeOp.FN
577+
):
578+
assert isinstance(node.init, WithMeta)
579+
def_ast = _with_meta_to_py_ast(ctx, node.init, def_name=defsym.name)
580+
is_defn = True
571581
else:
572582
def_ast = gen_py_ast(ctx, node.init)
573583
else:
@@ -684,6 +694,9 @@ def _synthetic_do_to_py_ast(ctx: GeneratorContext, node: Do) -> GeneratedPyAST:
684694
)
685695

686696

697+
MetaNode = Union[Const, MapNode]
698+
699+
687700
def __fn_name(s: Optional[str]) -> str:
688701
"""Generate a safe Python function name from a function name symbol.
689702
If no symbol is provided, generate a name with a default prefix."""
@@ -730,9 +743,32 @@ def __fn_args_to_py_ast(
730743
return fn_args, varg, fn_body_ast
731744

732745

746+
def __fn_meta(
747+
ctx: GeneratorContext, meta_node: Optional[MetaNode] = None
748+
) -> Tuple[Iterable[ast.AST], Iterable[ast.AST]]:
749+
if meta_node is not None:
750+
meta_ast = gen_py_ast(ctx, meta_node)
751+
return (
752+
meta_ast.dependencies,
753+
[
754+
ast.Call(
755+
func=_FN_WITH_ATTRS_FN_NAME,
756+
args=[],
757+
keywords=[ast.keyword(arg="meta", value=meta_ast.node)],
758+
)
759+
],
760+
)
761+
else:
762+
return (), ()
763+
764+
733765
@_with_ast_loc_deps
734766
def __single_arity_fn_to_py_ast(
735-
ctx: GeneratorContext, node: Fn, method: FnMethod, def_name: Optional[str] = None
767+
ctx: GeneratorContext,
768+
node: Fn,
769+
method: FnMethod,
770+
def_name: Optional[str] = None,
771+
meta_node: Optional[MetaNode] = None,
736772
) -> GeneratedPyAST:
737773
"""Return a Python AST node for a function with a single arity."""
738774
assert node.op == NodeOp.FN
@@ -753,34 +789,56 @@ def __single_arity_fn_to_py_ast(
753789
fn_args, varg, fn_body_ast = __fn_args_to_py_ast(
754790
ctx, method.params, method.body
755791
)
792+
meta_deps, meta_decorators = __fn_meta(ctx, meta_node)
756793
return GeneratedPyAST(
757794
node=ast.Name(id=py_fn_name, ctx=ast.Load()),
758-
dependencies=[
759-
py_fn_node(
760-
name=py_fn_name,
761-
args=ast.arguments(
762-
args=fn_args,
763-
kwarg=None,
764-
vararg=varg,
765-
kwonlyargs=[],
766-
defaults=[],
767-
kw_defaults=[],
768-
),
769-
body=fn_body_ast,
770-
decorator_list=[_TRAMPOLINE_FN_NAME]
771-
if ctx.recur_point.has_recur
772-
else [],
773-
returns=None,
795+
dependencies=list(
796+
chain(
797+
meta_deps,
798+
[
799+
py_fn_node(
800+
name=py_fn_name,
801+
args=ast.arguments(
802+
args=fn_args,
803+
kwarg=None,
804+
vararg=varg,
805+
kwonlyargs=[],
806+
defaults=[],
807+
kw_defaults=[],
808+
),
809+
body=fn_body_ast,
810+
decorator_list=list(
811+
chain(
812+
meta_decorators,
813+
[_BASILISP_FN_FN_NAME],
814+
[_TRAMPOLINE_FN_NAME]
815+
if ctx.recur_point.has_recur
816+
else [],
817+
)
818+
),
819+
returns=None,
820+
)
821+
],
774822
)
775-
],
823+
),
776824
)
777825

778826

779-
def __multi_arity_dispatch_fn(
827+
def __handle_async_return(node: ast.AST) -> ast.Return:
828+
return ast.Return(value=ast.Await(value=node))
829+
830+
831+
def __handle_return(node: ast.AST) -> ast.Return:
832+
return ast.Return(value=node)
833+
834+
835+
def __multi_arity_dispatch_fn( # pylint: disable=too-many-arguments,too-many-locals
836+
ctx: GeneratorContext,
780837
name: str,
781838
arity_map: Dict[int, str],
782839
default_name: Optional[str] = None,
783840
max_fixed_arity: Optional[int] = None,
841+
meta_node: Optional[MetaNode] = None,
784842
is_async: bool = False,
785843
) -> GeneratedPyAST:
786844
"""Return the Python AST nodes for a argument-length dispatch function
@@ -803,6 +861,9 @@ def fn(*args):
803861
dispatch_keys.append(ast.Num(k))
804862
dispatch_vals.append(ast.Name(id=v, ctx=ast.Load()))
805863

864+
# Async functions should return await, otherwise just return
865+
handle_return = __handle_async_return if is_async else __handle_return
866+
806867
nargs_name = genname("nargs")
807868
method_name = genname("method")
808869
body = [
@@ -833,8 +894,8 @@ def fn(*args):
833894
comparators=[ast.Name(id=method_name, ctx=ast.Load())],
834895
),
835896
body=[
836-
ast.Return(
837-
value=ast.Call(
897+
handle_return(
898+
ast.Call(
838899
func=ast.Name(id=method_name, ctx=ast.Load()),
839900
args=[
840901
ast.Starred(
@@ -858,8 +919,8 @@ def fn(*args):
858919
comparators=[ast.Num(max_fixed_arity)],
859920
),
860921
body=[
861-
ast.Return(
862-
value=ast.Call(
922+
handle_return(
923+
ast.Call(
863924
func=ast.Name(id=default_name, ctx=ast.Load()),
864925
args=[
865926
ast.Starred(
@@ -891,28 +952,34 @@ def fn(*args):
891952
]
892953

893954
py_fn_node = ast.AsyncFunctionDef if is_async else ast.FunctionDef
955+
meta_deps, meta_decorators = __fn_meta(ctx, meta_node)
894956
return GeneratedPyAST(
895957
node=ast.Name(id=name, ctx=ast.Load()),
896-
dependencies=[
897-
ast.Assign(
898-
targets=[ast.Name(id=dispatch_map_name, ctx=ast.Store())],
899-
value=ast.Dict(keys=dispatch_keys, values=dispatch_vals),
900-
),
901-
py_fn_node(
902-
name=name,
903-
args=ast.arguments(
904-
args=[],
905-
kwarg=None,
906-
vararg=ast.arg(arg=_MULTI_ARITY_ARG_NAME, annotation=None),
907-
kwonlyargs=[],
908-
defaults=[],
909-
kw_defaults=[],
910-
),
911-
body=body,
912-
decorator_list=[],
913-
returns=None,
914-
),
915-
],
958+
dependencies=chain(
959+
[
960+
ast.Assign(
961+
targets=[ast.Name(id=dispatch_map_name, ctx=ast.Store())],
962+
value=ast.Dict(keys=dispatch_keys, values=dispatch_vals),
963+
)
964+
],
965+
meta_deps,
966+
[
967+
py_fn_node(
968+
name=name,
969+
args=ast.arguments(
970+
args=[],
971+
kwarg=None,
972+
vararg=ast.arg(arg=_MULTI_ARITY_ARG_NAME, annotation=None),
973+
kwonlyargs=[],
974+
defaults=[],
975+
kw_defaults=[],
976+
),
977+
body=body,
978+
decorator_list=list(chain(meta_decorators, [_BASILISP_FN_FN_NAME])),
979+
returns=None,
980+
)
981+
],
982+
),
916983
)
917984

918985

@@ -922,6 +989,7 @@ def __multi_arity_fn_to_py_ast( # pylint: disable=too-many-locals
922989
node: Fn,
923990
methods: Collection[FnMethod],
924991
def_name: Optional[str] = None,
992+
meta_node: Optional[MetaNode] = None,
925993
) -> GeneratedPyAST:
926994
"""Return a Python AST node for a function with multiple arities."""
927995
assert node.op == NodeOp.FN
@@ -974,10 +1042,13 @@ def __multi_arity_fn_to_py_ast( # pylint: disable=too-many-locals
9741042
)
9751043

9761044
dispatch_fn_ast = __multi_arity_dispatch_fn(
1045+
ctx,
9771046
py_fn_name,
9781047
arity_to_name,
9791048
default_name=rest_arity_name,
9801049
max_fixed_arity=node.max_fixed_arity,
1050+
meta_node=meta_node,
1051+
is_async=node.is_async,
9811052
)
9821053

9831054
return GeneratedPyAST(
@@ -988,16 +1059,21 @@ def __multi_arity_fn_to_py_ast( # pylint: disable=too-many-locals
9881059

9891060
@_with_ast_loc
9901061
def _fn_to_py_ast(
991-
ctx: GeneratorContext, node: Fn, def_name: Optional[str] = None
1062+
ctx: GeneratorContext,
1063+
node: Fn,
1064+
def_name: Optional[str] = None,
1065+
meta_node: Optional[MetaNode] = None,
9921066
) -> GeneratedPyAST:
9931067
"""Return a Python AST Node for a `fn` expression."""
9941068
assert node.op == NodeOp.FN
9951069
if len(node.methods) == 1:
9961070
return __single_arity_fn_to_py_ast(
997-
ctx, node, next(iter(node.methods)), def_name=def_name
1071+
ctx, node, next(iter(node.methods)), def_name=def_name, meta_node=meta_node
9981072
)
9991073
else:
1000-
return __multi_arity_fn_to_py_ast(ctx, node, node.methods, def_name=def_name)
1074+
return __multi_arity_fn_to_py_ast(
1075+
ctx, node, node.methods, def_name=def_name, meta_node=meta_node
1076+
)
10011077

10021078

10031079
@_with_ast_loc_deps
@@ -1674,9 +1750,6 @@ def _maybe_host_form_to_py_ast(
16741750
#########################
16751751

16761752

1677-
MetaNode = Union[Const, MapNode]
1678-
1679-
16801753
@_with_ast_loc
16811754
def _map_to_py_ast(
16821755
ctx: GeneratorContext, node: MapNode, meta_node: Optional[MetaNode] = None
@@ -1816,22 +1889,24 @@ def _py_tuple_to_py_ast(ctx: GeneratorContext, node: PyTuple) -> GeneratedPyAST:
18161889

18171890

18181891
_WITH_META_EXPR_HANDLER = { # type: ignore
1819-
NodeOp.FN: None, # TODO: function with meta
1892+
NodeOp.FN: _fn_to_py_ast,
18201893
NodeOp.MAP: _map_to_py_ast,
18211894
NodeOp.SET: _set_to_py_ast,
18221895
NodeOp.VECTOR: _vec_to_py_ast,
18231896
}
18241897

18251898

1826-
def _with_meta_to_py_ast(ctx: GeneratorContext, node: WithMeta) -> GeneratedPyAST:
1899+
def _with_meta_to_py_ast(
1900+
ctx: GeneratorContext, node: WithMeta, **kwargs
1901+
) -> GeneratedPyAST:
18271902
"""Generate a Python AST node for Python interop method calls."""
18281903
assert node.op == NodeOp.WITH_META
18291904

18301905
handle_expr = _WITH_META_EXPR_HANDLER.get(node.expr.op)
18311906
assert (
18321907
handle_expr is not None
18331908
), "No expression handler for with-meta child node type"
1834-
return handle_expr(ctx, node.expr, meta_node=node.meta) # type: ignore
1909+
return handle_expr(ctx, node.expr, meta_node=node.meta, **kwargs) # type: ignore
18351910

18361911

18371912
#################

src/basilisp/lang/compiler/parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,8 @@ def __fn_method_ast( # pylint: disable=too-many-branches,too-many-locals
707707
return method
708708

709709

710-
def _fn_ast( # pylint: disable=too-many-branches # noqa: MC0001
710+
@_with_meta # noqa: MC0001
711+
def _fn_ast( # pylint: disable=too-many-branches
711712
ctx: ParserContext, form: Union[llist.List, lseq.Seq]
712713
) -> Fn:
713714
assert form.first == SpecialForm.FN

0 commit comments

Comments
 (0)