Skip to content

Commit a4dc689

Browse files
authored
Fix a bug with syntax quoted anonymous functions (#1162)
Fixes #1160
1 parent 08c473b commit a4dc689

File tree

4 files changed

+162
-52
lines changed

4 files changed

+162
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* Fix a bug where `#` characters were not legal in keywords and symbols (#1149)
1313
* Fix a bug where seqs were not considered valid input for matching clauses of the `case` macro (#1148)
1414
* Fix a bug where `py->lisp` did not keywordize string keys potentially containing namespaces (#1156)
15+
* Fix a bug where anonymous functions using the `#(...)` reader syntax were not properly expanded in a syntax quote (#1160)
1516

1617
## [v0.3.3]
1718
### Added

src/basilisp/lang/reader.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,25 +1183,33 @@ def _read_function(ctx: ReaderContext) -> llist.PersistentList:
11831183
if ctx.is_in_anon_fn:
11841184
raise ctx.syntax_error("Nested #() definitions not allowed")
11851185

1186+
current_ns = get_current_ns()
1187+
11861188
with ctx.in_anon_fn():
11871189
form = _read_list(ctx)
11881190
arg_set = set()
11891191

1190-
def arg_suffix(arg_num):
1192+
def arg_suffix(arg_num: Optional[str]) -> str:
11911193
if arg_num is None:
11921194
return "1"
11931195
elif arg_num == "&":
11941196
return "rest"
11951197
else:
11961198
return arg_num
11971199

1198-
def sym_replacement(arg_num):
1200+
def sym_replacement(arg_num: Optional[str]) -> sym.Symbol:
11991201
suffix = arg_suffix(arg_num)
1202+
if ctx.is_syntax_quoted:
1203+
suffix = f"{suffix}#"
12001204
return sym.symbol(f"arg-{suffix}")
12011205

12021206
def identify_and_replace(f):
12031207
if isinstance(f, sym.Symbol):
1204-
if f.ns is None:
1208+
# Checking against the current namespace is generally only used for
1209+
# when anonymous function definitions are syntax quoted. Arguments
1210+
# are resolved in terms of the current namespace, so we simply check
1211+
# if the symbol namespace matches the current runtime namespace.
1212+
if f.ns is None or f.ns == current_ns.name:
12051213
match = fn_macro_args.match(f.name)
12061214
if match is not None:
12071215
arg_num = match.group(2)
@@ -1217,9 +1225,10 @@ def identify_and_replace(f):
12171225
if len(numbered_args) > 0:
12181226
max_arg = max(numbered_args)
12191227
arg_list = [sym_replacement(str(i)) for i in range(1, max_arg + 1)]
1220-
if "rest" in arg_set:
1221-
arg_list.append(_AMPERSAND)
1222-
arg_list.append(sym_replacement("rest"))
1228+
1229+
if "rest" in arg_set:
1230+
arg_list.append(_AMPERSAND)
1231+
arg_list.append(sym_replacement("rest"))
12231232

12241233
return llist.l(_FN, vec.vector(arg_list), body)
12251234

tests/basilisp/compiler_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5870,6 +5870,51 @@ def test_syntax_quoting(test_ns: str, lcompile: CompileFn, resolver: reader.Reso
58705870
)
58715871

58725872

5873+
def test_syntax_quoting_anonymous_fns_with_single_arg(
5874+
test_ns: str, lcompile: CompileFn, resolver: reader.Resolver
5875+
):
5876+
single_arg_fn = lcompile("`#(println %)", resolver=resolver)
5877+
assert single_arg_fn.first == sym.symbol("fn*")
5878+
single_arg_vec = runtime.nth(single_arg_fn, 1)
5879+
assert isinstance(single_arg_vec, vec.PersistentVector)
5880+
single_arg = single_arg_vec[0]
5881+
assert isinstance(single_arg, sym.Symbol)
5882+
assert re.match(r"arg-1_\d+", single_arg.name) is not None
5883+
println_call = runtime.nth(single_arg_fn, 2)
5884+
assert runtime.nth(println_call, 1) == single_arg
5885+
5886+
5887+
def test_syntax_quoting_anonymous_fns_with_multiple_args(
5888+
test_ns: str, lcompile: CompileFn, resolver: reader.Resolver
5889+
):
5890+
multi_arg_fn = lcompile("`#(vector %1 %2 %3)", resolver=resolver)
5891+
assert multi_arg_fn.first == sym.symbol("fn*")
5892+
multi_arg_vec = runtime.nth(multi_arg_fn, 1)
5893+
assert isinstance(multi_arg_vec, vec.PersistentVector)
5894+
5895+
for arg in multi_arg_vec:
5896+
assert isinstance(arg, sym.Symbol)
5897+
assert re.match(r"arg-\d_\d+", arg.name) is not None
5898+
5899+
vector_call = runtime.nth(multi_arg_fn, 2)
5900+
assert vec.vector(runtime.nthrest(vector_call, 1)) == multi_arg_vec
5901+
5902+
5903+
def test_syntax_quoting_anonymous_fns_with_rest_arg(
5904+
test_ns: str, lcompile: CompileFn, resolver: reader.Resolver
5905+
):
5906+
rest_arg_fn = lcompile("`#(vec %&)", resolver=resolver)
5907+
assert rest_arg_fn.first == sym.symbol("fn*")
5908+
rest_arg_vec = runtime.nth(rest_arg_fn, 1)
5909+
assert isinstance(rest_arg_vec, vec.PersistentVector)
5910+
assert rest_arg_vec[0] == sym.symbol("&")
5911+
rest_arg = rest_arg_vec[1]
5912+
assert isinstance(rest_arg, sym.Symbol)
5913+
assert re.match(r"arg-rest_\d+", rest_arg.name) is not None
5914+
vec_call = runtime.nth(rest_arg_fn, 2)
5915+
assert runtime.nth(vec_call, 1) == rest_arg
5916+
5917+
58735918
class TestThrow:
58745919
def test_throw_not_enough_args(self, lcompile: CompileFn):
58755920
with pytest.raises(compiler.CompilerException):

tests/basilisp/reader_test.py

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,54 +1793,109 @@ def test_splicing_form_in_maps(self):
17931793
)
17941794

17951795

1796-
def test_function_reader_macro():
1797-
assert read_str_first("#()") == llist.l(sym.symbol("fn*"), vec.v(), None)
1798-
assert read_str_first("#(identity %)") == llist.l(
1799-
sym.symbol("fn*"),
1800-
vec.v(sym.symbol("arg-1")),
1801-
llist.l(sym.symbol("identity"), sym.symbol("arg-1")),
1802-
)
1803-
assert read_str_first("#(identity %1)") == llist.l(
1804-
sym.symbol("fn*"),
1805-
vec.v(sym.symbol("arg-1")),
1806-
llist.l(sym.symbol("identity"), sym.symbol("arg-1")),
1807-
)
1808-
assert read_str_first("#(identity %& %1)") == llist.l(
1809-
sym.symbol("fn*"),
1810-
vec.v(sym.symbol("arg-1"), sym.symbol("&"), sym.symbol("arg-rest")),
1811-
llist.l(sym.symbol("identity"), sym.symbol("arg-rest"), sym.symbol("arg-1")),
1812-
)
1813-
assert read_str_first("#(identity %3)") == llist.l(
1814-
sym.symbol("fn*"),
1815-
vec.v(sym.symbol("arg-1"), sym.symbol("arg-2"), sym.symbol("arg-3")),
1816-
llist.l(sym.symbol("identity"), sym.symbol("arg-3")),
1817-
)
1818-
assert read_str_first("#(identity %3 %&)") == llist.l(
1819-
sym.symbol("fn*"),
1820-
vec.v(
1821-
sym.symbol("arg-1"),
1822-
sym.symbol("arg-2"),
1823-
sym.symbol("arg-3"),
1824-
sym.symbol("&"),
1825-
sym.symbol("arg-rest"),
1826-
),
1827-
llist.l(sym.symbol("identity"), sym.symbol("arg-3"), sym.symbol("arg-rest")),
1828-
)
1829-
assert read_str_first("#(identity {:arg %})") == llist.l(
1830-
sym.symbol("fn*"),
1831-
vec.v(
1832-
sym.symbol("arg-1"),
1833-
),
1834-
llist.l(
1835-
sym.symbol("identity"), lmap.map({kw.keyword("arg"): sym.symbol("arg-1")})
1836-
),
1796+
class TestFunctionReaderMacro:
1797+
@pytest.mark.parametrize(
1798+
"code,v",
1799+
[
1800+
("#()", llist.l(sym.symbol("fn*"), vec.v(), None)),
1801+
(
1802+
"#(identity %)",
1803+
llist.l(
1804+
sym.symbol("fn*"),
1805+
vec.v(sym.symbol("arg-1")),
1806+
llist.l(sym.symbol("identity"), sym.symbol("arg-1")),
1807+
),
1808+
),
1809+
(
1810+
"#(identity %1)",
1811+
llist.l(
1812+
sym.symbol("fn*"),
1813+
vec.v(sym.symbol("arg-1")),
1814+
llist.l(sym.symbol("identity"), sym.symbol("arg-1")),
1815+
),
1816+
),
1817+
(
1818+
"#(identity %& %1)",
1819+
llist.l(
1820+
sym.symbol("fn*"),
1821+
vec.v(sym.symbol("arg-1"), sym.symbol("&"), sym.symbol("arg-rest")),
1822+
llist.l(
1823+
sym.symbol("identity"),
1824+
sym.symbol("arg-rest"),
1825+
sym.symbol("arg-1"),
1826+
),
1827+
),
1828+
),
1829+
(
1830+
"#(identity %3)",
1831+
llist.l(
1832+
sym.symbol("fn*"),
1833+
vec.v(
1834+
sym.symbol("arg-1"), sym.symbol("arg-2"), sym.symbol("arg-3")
1835+
),
1836+
llist.l(sym.symbol("identity"), sym.symbol("arg-3")),
1837+
),
1838+
),
1839+
(
1840+
"#(identity %3 %&)",
1841+
llist.l(
1842+
sym.symbol("fn*"),
1843+
vec.v(
1844+
sym.symbol("arg-1"),
1845+
sym.symbol("arg-2"),
1846+
sym.symbol("arg-3"),
1847+
sym.symbol("&"),
1848+
sym.symbol("arg-rest"),
1849+
),
1850+
llist.l(
1851+
sym.symbol("identity"),
1852+
sym.symbol("arg-3"),
1853+
sym.symbol("arg-rest"),
1854+
),
1855+
),
1856+
),
1857+
(
1858+
"#(identity {:arg %})",
1859+
llist.l(
1860+
sym.symbol("fn*"),
1861+
vec.v(
1862+
sym.symbol("arg-1"),
1863+
),
1864+
llist.l(
1865+
sym.symbol("identity"),
1866+
lmap.map({kw.keyword("arg"): sym.symbol("arg-1")}),
1867+
),
1868+
),
1869+
),
1870+
(
1871+
"#(vec %&)",
1872+
llist.l(
1873+
sym.symbol("fn*"),
1874+
vec.v(sym.symbol("&"), sym.symbol("arg-rest")),
1875+
llist.l(sym.symbol("vec"), sym.symbol("arg-rest")),
1876+
),
1877+
),
1878+
(
1879+
"#(vector %1 %&)",
1880+
llist.l(
1881+
sym.symbol("fn*"),
1882+
vec.v(sym.symbol("arg-1"), sym.symbol("&"), sym.symbol("arg-rest")),
1883+
llist.l(
1884+
sym.symbol("vector"),
1885+
sym.symbol("arg-1"),
1886+
sym.symbol("arg-rest"),
1887+
),
1888+
),
1889+
),
1890+
],
18371891
)
1892+
def test_function_reader_macro(self, code: str, v):
1893+
assert v == read_str_first(code)
18381894

1839-
with pytest.raises(reader.SyntaxError):
1840-
read_str_first("#(identity #(%1 %2))")
1841-
1842-
with pytest.raises(reader.SyntaxError):
1843-
read_str_first("#app/ermagrd [1 2 3]")
1895+
@pytest.mark.parametrize("code", ["#(identity #(%1 %2))", "#app/ermagrd [1 2 3]"])
1896+
def test_invalid_function_reader_macro(self, code: str):
1897+
with pytest.raises(reader.SyntaxError):
1898+
read_str_first(code)
18441899

18451900

18461901
def test_deref():

0 commit comments

Comments
 (0)