Skip to content

Commit 460caff

Browse files
authored
Add support for the :param-tags reader metadata syntax (#1116)
Fixes #1111
1 parent 7cc260f commit 460caff

File tree

5 files changed

+141
-89
lines changed

5 files changed

+141
-89
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ 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 the `:param-tags` reader metadata syntax `^[tag ...]` from Clojure 1.12 (#1111)
10+
811
### Changed
912
* Types generated by `reify` may optionally be marked as `^:mutable` now to prevent `attrs.exceptions.FrozenInstanceError`s being thrown when mutating methods inherited from the supertype(s) are called (#1088)
1013

docs/differencesfromclojure.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ Host interoperability features generally match those of Clojure.
192192
Type Hinting
193193
^^^^^^^^^^^^
194194

195-
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.
195+
Type hints may be applied anywhere they are supported in Clojure (as the ``:tag`` or ``:param-tags`` metadata keys), but the compiler does not currently use them for any purpose.
196196
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.
197197
Particularly in the case of function arguments and return values, these tags maybe introspected from the Python :external:py:mod:`inspect` module.
198198
There is no need for type hints anywhere in Basilisp right now, however.

docs/reader.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ Metadata applied to a form must be one of: :ref:`maps`, :ref:`symbols`, :ref:`ke
417417

418418
* Symbol metadata will be normalized to a Map with the symbol as the value for the key ``:tag``.
419419
* Keyword metadata will be normalized to a Map with the keyword as the key with the value of ``true``.
420+
* Vector metadata will be normalized to a Map with the vector as the value for the key ``:param-tags``.
420421
* Map metadata will not be modified when it is read.
421422

422423
.. seealso::

src/basilisp/lang/reader.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
READER_END_LINE_KW = kw.keyword("end-line", ns="basilisp.lang.reader")
8484
READER_END_COL_KW = kw.keyword("end-col", ns="basilisp.lang.reader")
8585

86+
READER_TAG_KW = kw.keyword("tag")
87+
READER_PARAM_TAGS_KW = kw.keyword("param-tags")
88+
8689
READER_COND_FORM_KW = kw.keyword("form")
8790
READER_COND_SPLICING_KW = kw.keyword("splicing?")
8891

@@ -1077,11 +1080,13 @@ def _read_meta(ctx: ReaderContext) -> IMeta:
10771080

10781081
meta_map: Optional[lmap.PersistentMap[LispForm, LispForm]]
10791082
if isinstance(meta, sym.Symbol):
1080-
meta_map = lmap.map({kw.keyword("tag"): meta})
1083+
meta_map = lmap.map({READER_TAG_KW: meta})
10811084
elif isinstance(meta, kw.Keyword):
10821085
meta_map = lmap.map({meta: True})
10831086
elif isinstance(meta, lmap.PersistentMap):
10841087
meta_map = meta
1088+
elif isinstance(meta, vec.PersistentVector):
1089+
meta_map = lmap.map({READER_PARAM_TAGS_KW: meta})
10851090
else:
10861091
raise ctx.syntax_error(
10871092
f"Expected symbol, keyword, or map for metadata, not {type(meta)}"

tests/basilisp/reader_test.py

Lines changed: 130 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,101 +1309,144 @@ def test_var():
13091309
)
13101310

13111311

1312-
def test_meta():
1313-
def issubmap(m, sub):
1312+
class TestMetadata:
1313+
@staticmethod
1314+
def assert_is_submap(m, sub):
13141315
for k, subv in sub.items():
13151316
try:
13161317
mv = m[k]
1317-
return subv == mv
1318+
if subv != mv:
1319+
pytest.fail(f"Map key {k}: {mv} != {subv}")
13181320
except KeyError:
1319-
return False
1320-
return False
1321+
pytest.fail(f"Missing key {k}")
1322+
return True
13211323

1322-
s = read_str_first("^str s")
1323-
assert s == sym.symbol("s")
1324-
assert issubmap(s.meta, lmap.map({kw.keyword("tag"): sym.symbol("str")}))
1325-
assert issubmap(s.meta, lmap.map({reader.READER_LINE_KW: 1}))
1326-
assert issubmap(s.meta, lmap.map({reader.READER_END_LINE_KW: 1}))
1327-
assert issubmap(s.meta, lmap.map({reader.READER_COL_KW: 5}))
1328-
assert issubmap(s.meta, lmap.map({reader.READER_END_COL_KW: 6}))
1329-
1330-
s = read_str_first("^:dynamic *ns*")
1331-
assert s == sym.symbol("*ns*")
1332-
assert issubmap(s.meta, lmap.map({kw.keyword("dynamic"): True}))
1333-
1334-
s = read_str_first('^{:doc "If true, assert."} *assert*')
1335-
assert s == sym.symbol("*assert*")
1336-
assert issubmap(s.meta, lmap.map({kw.keyword("doc"): "If true, assert."}))
1337-
1338-
v = read_str_first("^:has-meta [:a]")
1339-
assert v == vec.v(kw.keyword("a"))
1340-
assert issubmap(v.meta, lmap.map({kw.keyword("has-meta"): True}))
1341-
1342-
l = read_str_first("^:has-meta (:a)")
1343-
assert l == llist.l(kw.keyword("a"))
1344-
assert issubmap(l.meta, lmap.map({kw.keyword("has-meta"): True}))
1345-
1346-
m = read_str_first('^:has-meta {:key "val"}')
1347-
assert m == lmap.map({kw.keyword("key"): "val"})
1348-
assert issubmap(m.meta, lmap.map({kw.keyword("has-meta"): True}))
1349-
1350-
t = read_str_first("^:has-meta #{:a}")
1351-
assert t == lset.s(kw.keyword("a"))
1352-
assert issubmap(t.meta, lmap.map({kw.keyword("has-meta"): True}))
1353-
1354-
s = read_str_first('^:dynamic ^{:doc "If true, assert."} *assert*')
1355-
assert s == sym.symbol("*assert*")
1356-
assert issubmap(
1357-
s.meta,
1358-
lmap.map({kw.keyword("dynamic"): True, kw.keyword("doc"): "If true, assert."}),
1324+
@pytest.mark.parametrize(
1325+
"s,form,expected_meta",
1326+
[
1327+
(
1328+
"^str s",
1329+
sym.symbol("s"),
1330+
lmap.map(
1331+
{
1332+
kw.keyword("tag"): sym.symbol("str"),
1333+
reader.READER_LINE_KW: 1,
1334+
reader.READER_END_LINE_KW: 1,
1335+
reader.READER_COL_KW: 5,
1336+
reader.READER_END_COL_KW: 6,
1337+
}
1338+
),
1339+
),
1340+
(
1341+
"^:dynamic *ns*",
1342+
sym.symbol("*ns*"),
1343+
lmap.map({kw.keyword("dynamic"): True}),
1344+
),
1345+
(
1346+
'^{:doc "If true, assert."} *assert*',
1347+
sym.symbol("*assert*"),
1348+
lmap.map({kw.keyword("doc"): "If true, assert."}),
1349+
),
1350+
(
1351+
"^[] {}",
1352+
lmap.EMPTY,
1353+
lmap.map({kw.keyword("param-tags"): vec.EMPTY}),
1354+
),
1355+
(
1356+
'^[:a b "c"] {}',
1357+
lmap.EMPTY,
1358+
lmap.map(
1359+
{
1360+
kw.keyword("param-tags"): vec.v(
1361+
kw.keyword("a"), sym.symbol("b"), "c"
1362+
)
1363+
}
1364+
),
1365+
),
1366+
(
1367+
"^:has-meta [:a]",
1368+
vec.v(kw.keyword("a")),
1369+
lmap.map({kw.keyword("has-meta"): True}),
1370+
),
1371+
(
1372+
"^:has-meta (:a)",
1373+
llist.l(kw.keyword("a")),
1374+
lmap.map({kw.keyword("has-meta"): True}),
1375+
),
1376+
(
1377+
'^:has-meta {:key "val"}',
1378+
lmap.map({kw.keyword("key"): "val"}),
1379+
lmap.map({kw.keyword("has-meta"): True}),
1380+
),
1381+
(
1382+
"^:has-meta #{:a}",
1383+
lset.s(kw.keyword("a")),
1384+
lmap.map({kw.keyword("has-meta"): True}),
1385+
),
1386+
(
1387+
'^:dynamic ^{:doc "If true, assert."} ^python/bool ^[:dynamic :muffs] *assert*',
1388+
sym.symbol("*assert*"),
1389+
lmap.map(
1390+
{
1391+
kw.keyword("dynamic"): True,
1392+
kw.keyword("doc"): "If true, assert.",
1393+
kw.keyword("tag"): sym.symbol("bool", ns="python"),
1394+
kw.keyword("param-tags"): vec.v(
1395+
kw.keyword("dynamic"), kw.keyword("muffs")
1396+
),
1397+
}
1398+
),
1399+
),
1400+
(
1401+
"^{:always true} ^{:always false} *assert*",
1402+
sym.symbol("*assert*"),
1403+
lmap.map({kw.keyword("always"): True}),
1404+
),
1405+
],
13591406
)
1407+
def test_legal_reader_metadata(
1408+
self, s: str, form, expected_meta: lmap.PersistentMap
1409+
):
1410+
v = read_str_first(s)
1411+
assert v == form
1412+
self.assert_is_submap(v.meta, expected_meta)
13601413

1361-
s = read_str_first("^{:always true} ^{:always false} *assert*")
1362-
assert s == sym.symbol("*assert*")
1363-
assert issubmap(s.meta, lmap.map({kw.keyword("always"): True}))
1364-
1365-
1366-
def test_invalid_meta_structure():
1367-
with pytest.raises(reader.SyntaxError):
1368-
read_str_first("^35233 {}")
1369-
1370-
with pytest.raises(reader.SyntaxError):
1371-
read_str_first("^583.28 {}")
1372-
1373-
with pytest.raises(reader.SyntaxError):
1374-
read_str_first("^true {}")
1375-
1376-
with pytest.raises(reader.SyntaxError):
1377-
read_str_first("^false {}")
1378-
1379-
with pytest.raises(reader.SyntaxError):
1380-
read_str_first("^nil {}")
1381-
1382-
with pytest.raises(reader.SyntaxError):
1383-
read_str_first('^"String value" {}')
1384-
1385-
1386-
def test_invalid_meta_attachment():
1387-
with pytest.raises(reader.SyntaxError):
1388-
read_str_first("^:has-meta 35233")
1389-
1390-
with pytest.raises(reader.SyntaxError):
1391-
read_str_first("^:has-meta 583.28")
1392-
1393-
with pytest.raises(reader.SyntaxError):
1394-
read_str_first("^:has-meta :i-am-a-keyword")
1395-
1396-
with pytest.raises(reader.SyntaxError):
1397-
read_str_first("^:has-meta true")
1398-
1399-
with pytest.raises(reader.SyntaxError):
1400-
read_str_first("^:has-meta false")
1401-
1402-
with pytest.raises(reader.SyntaxError):
1403-
read_str_first("^:has-meta nil")
1414+
@pytest.mark.parametrize(
1415+
"s",
1416+
[
1417+
"^35233 {}",
1418+
"^583.28 {}",
1419+
"^12.6J {}",
1420+
"^22/7 {}",
1421+
"^12.6M {}",
1422+
"^true {}",
1423+
"^false {}",
1424+
"^nil {}",
1425+
'^"String value" {}',
1426+
],
1427+
)
1428+
def test_syntax_error_attaching_unsupported_type_as_metadata(self, s: str):
1429+
with pytest.raises(reader.SyntaxError):
1430+
read_str_first(s)
14041431

1405-
with pytest.raises(reader.SyntaxError):
1406-
read_str_first('^:has-meta "String value"')
1432+
@pytest.mark.parametrize(
1433+
"s",
1434+
[
1435+
"^:has-meta 35233",
1436+
"^:has-meta 583.28",
1437+
"^:has-meta 12.6J",
1438+
"^:has-meta 22/7",
1439+
"^:has-meta 12.6M",
1440+
"^:has-meta :i-am-a-keyword",
1441+
"^:has-meta true",
1442+
"^:has-meta false",
1443+
"^:has-meta nil",
1444+
'^:has-meta "String value"',
1445+
],
1446+
)
1447+
def test_syntax_error_attaching_metadata_to_unsupported_type(self, s: str):
1448+
with pytest.raises(reader.SyntaxError):
1449+
read_str_first(s)
14071450

14081451

14091452
def test_comment_reader_macro():

0 commit comments

Comments
 (0)