Skip to content

Commit 93be150

Browse files
authored
Only generate globally unique parameter names for inline functions (#1229)
Fixes #1212
1 parent 88cfb0b commit 93be150

File tree

10 files changed

+112
-110
lines changed

10 files changed

+112
-110
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
* Added `afor` and `awith` macros to support async Python interop (#1179, #1181)
1010

1111
### Changed
12-
* Single arity functions can be tagged with `^:allow-unsafe-names` to preserve their parameter names (#1212)
12+
* Function parameter names will not be automatically generated with unique suffixes unless the function meta key `^:safe-py-params` is provided (#1212)
1313

1414
### Fixed
1515
* Fix an issue where the compiler would generate an illegal `return` statement for asynchronous generators (#1180)

docs/compiler.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ Functions not meeting these criteria will trigger compile time errors if they ar
177177

178178
Users should consider inlining primarily a Basilisp internal feature and use it extremely sparingly in user code.
179179

180+
.. warning::
181+
182+
Due to the nature of how inline functions are applied, there is a potential for name clashes between the inline function parameter names and names defined elsewhere in the containing Python module.
183+
Therefore, it is recommended for any inline function to set the meta key ``^:safe-py-params`` to ensure that the compiler generates globally unique Python parameter names.
184+
For inline functions generated automatically by the compiler, this setting is automatically enabled.
185+
186+
.. code-block::
187+
188+
^:safe-py-params (fn [a b] ...)
189+
180190
.. _compiler_debugging:
181191

182192
Debugging

docs/pyinterop.rst

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,6 @@ When compiled, a ``kebab-case`` identifier always becomes a ``snake_case`` ident
2222

2323
The Basilisp compiler munges *all* unsafe Basilisp identifiers to safe Python identifiers, but other cases are unlikely to appear in standard Python interop usage.
2424

25-
.. note::
26-
27-
By default, the compiler munges function parameter names and makes them globally unique by appending a monotically increasing number suffix to support function inlining. To enable interop with Python libraries that rely on preserved parameter names, you can use the ```^:allow-unsafe-names`` metadata key to retain the (munged) parameter names. This flag only applies to single-arity Basilisp functions.
28-
29-
.. code-block::
30-
31-
(defn ^:allow-unsafe-names afun [a b] ...)
32-
3325
.. _python_builtins:
3426

3527
Python Builtins
@@ -316,7 +308,7 @@ These decorators are applied from right to left, similar to how Python decorator
316308
;;; Decorators with arguments, and order of application (right to left)
317309
;;
318310
;; example decorator
319-
(defn mult-x-decorator
311+
(defn mult-x-decorator
320312
[x]
321313
(fn [f]
322314
(fn [] (* (f) x))))
@@ -327,7 +319,7 @@ These decorators are applied from right to left, similar to how Python decorator
327319
;;; defasync support
328320
;;
329321
;; example async decorator
330-
(defn add-7-async-decorator
322+
(defn add-7-async-decorator
331323
[f]
332324
^:async (fn [] (+ (await (f)) 7)))
333325

src/basilisp/core.lpy

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -307,27 +307,27 @@
307307
;; Define inline functions for earlier core functions now that we have all the
308308
;; functionality required to do so.
309309

310-
(.alter-meta #'first assoc :inline (fn [seq] `(basilisp.lang.runtime/first ~seq)))
311-
(.alter-meta #'rest assoc :inline (fn [seq] `(basilisp.lang.runtime/rest ~seq)))
312-
(.alter-meta #'next assoc :inline (fn [seq] `(basilisp.lang.runtime/next ~seq)))
313-
(.alter-meta #'second assoc :inline (fn [seq] `(first (rest ~seq))))
314-
(.alter-meta #'ffirst assoc :inline (fn [seq] `(first (first ~seq))))
315-
(.alter-meta #'identity assoc :inline (fn [o] `~o))
316-
(.alter-meta #'instance? assoc :inline (fn [class obj] `(python/isinstance ~obj ~class)))
317-
(.alter-meta #'boolean? assoc :inline (fn [o] `(instance? python/bool ~o)))
318-
(.alter-meta #'float? assoc :inline (fn [o] `(instance? python/float ~o)))
319-
(.alter-meta #'integer? assoc :inline (fn [o] `(instance? python/int ~o)))
320-
(.alter-meta #'string? assoc :inline (fn [o] `(instance? python/str ~o)))
321-
(.alter-meta #'symbol? assoc :inline (fn [o] `(instance? basilisp.lang.symbol/Symbol ~o)))
322-
(.alter-meta #'keyword? assoc :inline (fn [o] `(instance? basilisp.lang.keyword/Keyword ~o)))
323-
(.alter-meta #'list? assoc :inline (fn [o] `(instance? basilisp.lang.interfaces/IPersistentList ~o)))
324-
(.alter-meta #'map? assoc :inline (fn [o] `(instance? basilisp.lang.interfaces/IPersistentMap ~o)))
325-
(.alter-meta #'set? assoc :inline (fn [o] `(instance? basilisp.lang.interfaces/IPersistentSet ~o)))
326-
(.alter-meta #'vector? assoc :inline (fn [o] `(instance? basilisp.lang.interfaces/IPersistentVector ~o)))
327-
(.alter-meta #'seq? assoc :inline (fn [o] `(instance? basilisp.lang.interfaces/ISeq ~o)))
328-
(.alter-meta #'seq assoc :inline (fn [o] `(basilisp.lang.runtime/to-seq ~o)))
329-
(.alter-meta #'set assoc :inline (fn [coll] `(basilisp.lang.runtime/to-set ~coll)))
330-
(.alter-meta #'vec assoc :inline (fn [coll] `(basilisp.lang.runtime/vector ~coll)))
310+
(.alter-meta #'first assoc :inline ^:safe-py-params (fn [seq] `(basilisp.lang.runtime/first ~seq)))
311+
(.alter-meta #'rest assoc :inline ^:safe-py-params (fn [seq] `(basilisp.lang.runtime/rest ~seq)))
312+
(.alter-meta #'next assoc :inline ^:safe-py-params (fn [seq] `(basilisp.lang.runtime/next ~seq)))
313+
(.alter-meta #'second assoc :inline ^:safe-py-params (fn [seq] `(first (rest ~seq))))
314+
(.alter-meta #'ffirst assoc :inline ^:safe-py-params (fn [seq] `(first (first ~seq))))
315+
(.alter-meta #'identity assoc :inline ^:safe-py-params (fn [o] `~o))
316+
(.alter-meta #'instance? assoc :inline ^:safe-py-params (fn [class obj] `(python/isinstance ~obj ~class)))
317+
(.alter-meta #'boolean? assoc :inline ^:safe-py-params (fn [o] `(instance? python/bool ~o)))
318+
(.alter-meta #'float? assoc :inline ^:safe-py-params (fn [o] `(instance? python/float ~o)))
319+
(.alter-meta #'integer? assoc :inline ^:safe-py-params (fn [o] `(instance? python/int ~o)))
320+
(.alter-meta #'string? assoc :inline ^:safe-py-params (fn [o] `(instance? python/str ~o)))
321+
(.alter-meta #'symbol? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.symbol/Symbol ~o)))
322+
(.alter-meta #'keyword? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.keyword/Keyword ~o)))
323+
(.alter-meta #'list? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.interfaces/IPersistentList ~o)))
324+
(.alter-meta #'map? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.interfaces/IPersistentMap ~o)))
325+
(.alter-meta #'set? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.interfaces/IPersistentSet ~o)))
326+
(.alter-meta #'vector? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.interfaces/IPersistentVector ~o)))
327+
(.alter-meta #'seq? assoc :inline ^:safe-py-params (fn [o] `(instance? basilisp.lang.interfaces/ISeq ~o)))
328+
(.alter-meta #'seq assoc :inline ^:safe-py-params (fn [o] `(basilisp.lang.runtime/to-seq ~o)))
329+
(.alter-meta #'set assoc :inline ^:safe-py-params (fn [coll] `(basilisp.lang.runtime/to-set ~coll)))
330+
(.alter-meta #'vec assoc :inline ^:safe-py-params (fn [coll] `(basilisp.lang.runtime/vector ~coll)))
331331

332332
(def
333333
^{:macro true
@@ -4796,7 +4796,7 @@
47964796
as the result. Returns the new value.
47974797

47984798
.. note::
4799-
4799+
48004800
Due to Basilisp's :ref:`Direct Linking Optimization <direct_linking>`, changes to
48014801
a Var's root value may not be reflected in the code unless the Var is dynamic.
48024802
To ensure updates propagate, set the Var's `^:redef` metadata, which disables
@@ -6068,7 +6068,7 @@
60686068
generated function from right to left.
60696069

60706070
.. note::
6071-
6071+
60726072
The ``name`` metadata (i.e., ``(fn ^{...} <name> ...)``) takes
60736073
precedence over the ``form`` metadata (i.e., ``^{...} (fn <name?> ...)``)
60746074
when both specify the same key.

src/basilisp/lang/compiler/analyzer.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
SYM_CLASSMETHOD_META_KEY,
5757
SYM_DEFAULT_META_KEY,
5858
SYM_DYNAMIC_META_KEY,
59+
SYM_GEN_SAFE_PYTHON_PARAM_NAMES_META_KEY,
5960
SYM_INLINE_META_KW,
6061
SYM_KWARGS_META_KEY,
6162
SYM_MACRO_META_KEY,
@@ -1070,7 +1071,7 @@ def _def_ast( # pylint: disable=too-many-locals,too-many-statements
10701071
"Cannot have a user-generated inline function and an automatically "
10711072
"generated inline function"
10721073
)
1073-
var.meta.assoc(SYM_INLINE_META_KW, init.inline_fn) # type: ignore[union-attr]
1074+
var.alter_meta(lambda m: m.assoc(SYM_INLINE_META_KW, init.inline_fn)) # type: ignore[misc]
10741075
def_meta = def_meta.assoc(SYM_INLINE_META_KW, init.inline_fn.form) # type: ignore[union-attr]
10751076

10761077
if tag_ast is not None and any(
@@ -1570,7 +1571,7 @@ def __deftype_or_reify_method_node_from_arities(
15701571
)
15711572

15721573

1573-
def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-locals # noqa: MC0001
1574+
def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-locals
15741575
form: ISeq, ctx: AnalyzerContext, special_form: sym.Symbol
15751576
) -> tuple[list[DefTypeBase], list[DefTypeMember]]:
15761577
"""Roll up `deftype*` and `reify*` declared bases and method implementations."""
@@ -2210,11 +2211,12 @@ def _inline_fn_ast(
22102211
*([sym.symbol(genname(f"{name.name}-inline"))] if name is not None else []),
22112212
vec.vector(binding.form for binding in inline_arity.params),
22122213
macroed_form,
2214+
meta=lmap.map({SYM_GEN_SAFE_PYTHON_PARAM_NAMES_META_KEY: True}),
22132215
)
22142216
return _fn_ast(inline_fn_form, ctx)
22152217

22162218

2217-
@_with_meta # noqa: MC0001
2219+
@_with_meta
22182220
def _fn_ast( # pylint: disable=too-many-locals,too-many-statements
22192221
form: Union[llist.PersistentList, ISeq], ctx: AnalyzerContext
22202222
) -> Fn:
@@ -3601,7 +3603,7 @@ def __resolve_namespaced_symbol_in_ns(
36013603
return None
36023604

36033605

3604-
def __resolve_namespaced_symbol( # pylint: disable=too-many-branches # noqa: MC0001
3606+
def __resolve_namespaced_symbol( # pylint: disable=too-many-branches
36053607
ctx: AnalyzerContext, form: sym.Symbol
36063608
) -> Union[Const, HostField, MaybeClass, MaybeHostForm, VarRef]:
36073609
"""Resolve a namespaced symbol into a Python name or Basilisp Var."""

src/basilisp/lang/compiler/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class SpecialForm:
3737

3838
SYM_ABSTRACT_META_KEY = kw.keyword("abstract")
3939
SYM_ABSTRACT_MEMBERS_META_KEY = kw.keyword("abstract-members")
40-
SYM_ALLOW_UNSAFE_NAMES_META_KEY = kw.keyword("allow-unsafe-names")
40+
SYM_GEN_SAFE_PYTHON_PARAM_NAMES_META_KEY = kw.keyword("safe-py-params")
4141
SYM_ASYNC_META_KEY = kw.keyword("async")
4242
SYM_KWARGS_META_KEY = kw.keyword("kwargs")
4343
SYM_PRIVATE_META_KEY = kw.keyword("private")

src/basilisp/lang/compiler/generator.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
INTERFACE_KW,
3838
OPERATOR_ALIAS,
3939
REST_KW,
40-
SYM_ALLOW_UNSAFE_NAMES_META_KEY,
4140
SYM_DYNAMIC_META_KEY,
41+
SYM_GEN_SAFE_PYTHON_PARAM_NAMES_META_KEY,
4242
SYM_REDEF_META_KEY,
4343
VAR_IS_PROTOCOL_META_KEY,
4444
)
@@ -654,13 +654,12 @@ def with_lineno_and_col(
654654
MetaNode = Union[Const, MapNode]
655655

656656

657-
def _is_allow_unsafe_names(fn_meta_node: Optional[MetaNode]) -> bool:
658-
"""Return True if the `fn_meta_node` has the meta key set to
659-
retain functio parameter names.
660-
661-
"""
657+
def _should_gen_safe_python_param_names(fn_meta_node: Optional[MetaNode]) -> bool:
658+
"""Return True if the `fn_meta_node` has the meta key set to generate globally
659+
unique function parameter names."""
662660
return (
663-
bool(fn_meta_node.form.val_at(SYM_ALLOW_UNSAFE_NAMES_META_KEY, False)) is True
661+
bool(fn_meta_node.form.val_at(SYM_GEN_SAFE_PYTHON_PARAM_NAMES_META_KEY, False))
662+
is True
664663
if fn_meta_node is not None and isinstance(fn_meta_node.form, IPersistentMap)
665664
else False
666665
)
@@ -1659,7 +1658,7 @@ def __fn_args_to_py_ast(
16591658
ctx: GeneratorContext,
16601659
params: Iterable[Binding],
16611660
body: Do,
1662-
allow_unsafe_param_names: bool = True,
1661+
should_generate_safe_names: bool = False,
16631662
) -> tuple[list[ast.arg], Optional[ast.arg], list[ast.stmt], Iterable[PyASTNode]]:
16641663
"""Generate a list of Python AST nodes from function method parameters.
16651664
@@ -1675,7 +1674,11 @@ def __fn_args_to_py_ast(
16751674
assert binding.init is None, ":fn nodes cannot have binding :inits"
16761675
assert varg is None, "Must have at most one variadic arg"
16771676
arg_name = munge(binding.name)
1678-
if not allow_unsafe_param_names:
1677+
# Always generate a unique name for bindings named "_" since those are
1678+
# typically ignored parameters. Python doesn't allow duplicate param
1679+
# names (even including "_"), so this is a hack to support something
1680+
# Clojure allows.
1681+
if should_generate_safe_names or binding.name == "_":
16791682
arg_name = genname(arg_name)
16801683

16811684
arg_tag: Optional[ast.expr]
@@ -1822,14 +1825,12 @@ def __single_arity_fn_to_py_ast( # pylint: disable=too-many-locals
18221825
sym.symbol(lisp_fn_name), py_fn_name, LocalType.FN
18231826
)
18241827

1825-
# check if we should preserve the original parameter names
1826-
allow_unsafe_param_names = _is_allow_unsafe_names(meta_node)
1827-
18281828
fn_args, varg, fn_body_ast, fn_def_deps = __fn_args_to_py_ast(
18291829
ctx,
18301830
method.params,
18311831
method.body,
1832-
allow_unsafe_param_names=allow_unsafe_param_names,
1832+
# check if we should preserve the original parameter names
1833+
should_generate_safe_names=_should_gen_safe_python_param_names(meta_node),
18331834
)
18341835
meta_deps, meta_decorators = __fn_meta(ctx, meta_node)
18351836

@@ -2138,7 +2139,13 @@ def __multi_arity_fn_to_py_ast( # pylint: disable=too-many-locals
21382139
)
21392140

21402141
fn_args, varg, fn_body_ast, fn_def_deps = __fn_args_to_py_ast(
2141-
ctx, arity.params, arity.body
2142+
ctx,
2143+
arity.params,
2144+
arity.body,
2145+
# check if we should preserve the original parameter names
2146+
should_generate_safe_names=_should_gen_safe_python_param_names(
2147+
meta_node
2148+
),
21422149
)
21432150
all_arity_def_deps.extend(fn_def_deps)
21442151

src/basilisp/lang/reader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,7 +1644,7 @@ def _resolve_tagged_literal(
16441644
raise ctx.syntax_error(e.message).with_traceback(e.__traceback__) from None
16451645

16461646

1647-
def _read_reader_macro(ctx: ReaderContext) -> LispReaderForm: # noqa: MC0001
1647+
def _read_reader_macro(ctx: ReaderContext) -> LispReaderForm:
16481648
"""Return a data structure evaluated as a reader macro from the input stream."""
16491649
start = ctx.reader.advance()
16501650
assert start == "#"
@@ -1731,7 +1731,7 @@ def _read_next_consuming_whitespace(ctx: ReaderContext) -> LispReaderForm:
17311731
return _read_next(ctx)
17321732

17331733

1734-
def _read_next(ctx: ReaderContext) -> LispReaderForm: # noqa: C901 MC0001
1734+
def _read_next(ctx: ReaderContext) -> LispReaderForm: # noqa: C901
17351735
"""Read the next full form from the input stream."""
17361736
reader = ctx.reader
17371737
char = reader.peek()

tests/basilisp/compiler_test.py

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2776,6 +2776,50 @@ def test_fn_method_allows_empty_body(
27762776
assert callable(fvar.value)
27772777
assert None is fvar.value()
27782778

2779+
@pytest.mark.parametrize(
2780+
"code,args",
2781+
[
2782+
("(defn test_dfn1a [a b] 5)", ["a", "b"]),
2783+
("(def test_dfn2 (fn [a b & c] 5))", ["a", "b", "c"]),
2784+
("(def test_dfn3 (fn [a b {:as c}] 5))", ["a", "b", "c"]),
2785+
],
2786+
)
2787+
def test_fn_argument_names_unchanged(
2788+
self, lcompile: CompileFn, code: str, args: list[str]
2789+
):
2790+
fvar = lcompile(code)
2791+
fn_args = list(inspect.signature(fvar.deref()).parameters.keys())
2792+
assert fn_args == args
2793+
2794+
@pytest.mark.parametrize(
2795+
"code",
2796+
[
2797+
"(defn ^:safe-py-params test_dfn0 [a b] 5)",
2798+
"(def ^:safe-py-params test_dfn2 (fn [a b & c] 5))",
2799+
"(def test_dfn2 ^:safe-py-params (fn [a b & c] 5))",
2800+
"(def test_dfn2 ^:safe-py-params (fn [a b {:as c}] 5))",
2801+
],
2802+
)
2803+
def test_fn_argument_names_globally_unique(self, lcompile: CompileFn, code: str):
2804+
fvar = lcompile(code)
2805+
args = list(inspect.signature(fvar.deref()).parameters.keys())
2806+
assert all(
2807+
re.fullmatch(r"[a-z]_\d+", arg) for arg in args
2808+
), f"unexpected argument names {args}"
2809+
2810+
def test_fn_argument_names_unchanged_except_ignored_argument(
2811+
self, lcompile: CompileFn
2812+
):
2813+
code = "(fn [a b _ _] 5)"
2814+
fvar = lcompile(code)
2815+
fn_args = set(inspect.signature(fvar).parameters.keys())
2816+
assert len(fn_args) == 4
2817+
assert {"a", "b"}.issubset(fn_args)
2818+
remaining_args = fn_args.difference({"a", "b"})
2819+
assert all(
2820+
re.fullmatch(r"__\d+", arg) for arg in remaining_args
2821+
), f"unexpected argument names {remaining_args}"
2822+
27792823
def test_single_arity_fn(self, lcompile: CompileFn, ns: runtime.Namespace):
27802824
code = """
27812825
(def string-upper (fn* string-upper [s] (.upper s)))
@@ -7222,57 +7266,3 @@ def test_yield_as_coroutine_with_multiple_yields(
72227266
assert kw.keyword("coroutine-value") == state.deref()
72237267
assert None is next(coro, None)
72247268
assert kw.keyword("done") == state.deref()
7225-
7226-
7227-
def test_defn_argument_names(lcompile: CompileFn):
7228-
# By default, function parameter names are made globally unique.
7229-
#
7230-
# For example, notice how defn generate parameter names in the
7231-
# pattern <parameter-name>_<monotonically-increasing-number> to
7232-
# ensure global uniqueness.
7233-
code = """
7234-
(defn test_dfn0 [a b] 5)
7235-
"""
7236-
fvar = lcompile(code)
7237-
args = list(inspect.signature(fvar.deref()).parameters.keys())
7238-
assert all(
7239-
re.fullmatch(p, s) for p, s in zip([r"a_\d+", r"b_\d+"], args)
7240-
), f"unexpected argument names {args}"
7241-
7242-
code = """
7243-
(defn ^:allow-unsafe-names test_dfn1a [a b] 5)
7244-
"""
7245-
fvar = lcompile(code)
7246-
args = list(inspect.signature(fvar.deref()).parameters.keys())
7247-
assert args == ["a", "b"]
7248-
7249-
code = """
7250-
(defn ^{:allow-unsafe-names false} test_dfn1b [a b] 5)
7251-
"""
7252-
fvar = lcompile(code)
7253-
args = list(inspect.signature(fvar.deref()).parameters.keys())
7254-
assert all(
7255-
re.fullmatch(p, s) for p, s in zip([r"a_\d+", r"b_\d+"], args)
7256-
), f"unexpected argument names {args}"
7257-
7258-
code = """
7259-
(def ^:allow-unsafe-names test_dfn2 (fn [a b & c] 5))
7260-
"""
7261-
fvar = lcompile(code)
7262-
args = list(inspect.signature(fvar.deref()).parameters.keys())
7263-
assert args == ["a", "b", "c"]
7264-
7265-
code = """
7266-
(def test_dfn3 ^:allow-unsafe-names (fn [a b & c] 5))
7267-
"""
7268-
fvar = lcompile(code)
7269-
args = list(inspect.signature(fvar.deref()).parameters.keys())
7270-
assert args == ["a", "b", "c"]
7271-
7272-
code = """
7273-
^{:kwargs :collect}
7274-
(defn ^:allow-unsafe-names test_dfn4 [a b {:as xyz}] 5)
7275-
"""
7276-
fvar = lcompile(code)
7277-
args = list(inspect.signature(fvar.deref()).parameters.keys())
7278-
assert args == ["a", "b", "xyz"]

0 commit comments

Comments
 (0)