Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
* Added support for the `:param-tags` reader metadata syntax `^[tag ...]` from Clojure 1.12 (#1111)

### Changed
* 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)

Expand Down
2 changes: 1 addition & 1 deletion docs/differencesfromclojure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Host interoperability features generally match those of Clojure.
Type Hinting
^^^^^^^^^^^^

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.
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.
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.
Particularly in the case of function arguments and return values, these tags maybe introspected from the Python :external:py:mod:`inspect` module.
There is no need for type hints anywhere in Basilisp right now, however.
Expand Down
1 change: 1 addition & 0 deletions docs/reader.rst
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ Metadata applied to a form must be one of: :ref:`maps`, :ref:`symbols`, :ref:`ke

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

.. seealso::
Expand Down
7 changes: 6 additions & 1 deletion src/basilisp/lang/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
READER_END_LINE_KW = kw.keyword("end-line", ns="basilisp.lang.reader")
READER_END_COL_KW = kw.keyword("end-col", ns="basilisp.lang.reader")

READER_TAG_KW = kw.keyword("tag")
READER_PARAM_TAGS_KW = kw.keyword("param-tags")

READER_COND_FORM_KW = kw.keyword("form")
READER_COND_SPLICING_KW = kw.keyword("splicing?")

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

meta_map: Optional[lmap.PersistentMap[LispForm, LispForm]]
if isinstance(meta, sym.Symbol):
meta_map = lmap.map({kw.keyword("tag"): meta})
meta_map = lmap.map({READER_TAG_KW: meta})
elif isinstance(meta, kw.Keyword):
meta_map = lmap.map({meta: True})
elif isinstance(meta, lmap.PersistentMap):
meta_map = meta
elif isinstance(meta, vec.PersistentVector):
meta_map = lmap.map({READER_PARAM_TAGS_KW: meta})
else:
raise ctx.syntax_error(
f"Expected symbol, keyword, or map for metadata, not {type(meta)}"
Expand Down
217 changes: 130 additions & 87 deletions tests/basilisp/reader_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1309,101 +1309,144 @@ def test_var():
)


def test_meta():
def issubmap(m, sub):
class TestMetadata:
@staticmethod
def assert_is_submap(m, sub):
for k, subv in sub.items():
try:
mv = m[k]
return subv == mv
if subv != mv:
pytest.fail(f"Map key {k}: {mv} != {subv}")
except KeyError:
return False
return False
pytest.fail(f"Missing key {k}")
return True

s = read_str_first("^str s")
assert s == sym.symbol("s")
assert issubmap(s.meta, lmap.map({kw.keyword("tag"): sym.symbol("str")}))
assert issubmap(s.meta, lmap.map({reader.READER_LINE_KW: 1}))
assert issubmap(s.meta, lmap.map({reader.READER_END_LINE_KW: 1}))
assert issubmap(s.meta, lmap.map({reader.READER_COL_KW: 5}))
assert issubmap(s.meta, lmap.map({reader.READER_END_COL_KW: 6}))

s = read_str_first("^:dynamic *ns*")
assert s == sym.symbol("*ns*")
assert issubmap(s.meta, lmap.map({kw.keyword("dynamic"): True}))

s = read_str_first('^{:doc "If true, assert."} *assert*')
assert s == sym.symbol("*assert*")
assert issubmap(s.meta, lmap.map({kw.keyword("doc"): "If true, assert."}))

v = read_str_first("^:has-meta [:a]")
assert v == vec.v(kw.keyword("a"))
assert issubmap(v.meta, lmap.map({kw.keyword("has-meta"): True}))

l = read_str_first("^:has-meta (:a)")
assert l == llist.l(kw.keyword("a"))
assert issubmap(l.meta, lmap.map({kw.keyword("has-meta"): True}))

m = read_str_first('^:has-meta {:key "val"}')
assert m == lmap.map({kw.keyword("key"): "val"})
assert issubmap(m.meta, lmap.map({kw.keyword("has-meta"): True}))

t = read_str_first("^:has-meta #{:a}")
assert t == lset.s(kw.keyword("a"))
assert issubmap(t.meta, lmap.map({kw.keyword("has-meta"): True}))

s = read_str_first('^:dynamic ^{:doc "If true, assert."} *assert*')
assert s == sym.symbol("*assert*")
assert issubmap(
s.meta,
lmap.map({kw.keyword("dynamic"): True, kw.keyword("doc"): "If true, assert."}),
@pytest.mark.parametrize(
"s,form,expected_meta",
[
(
"^str s",
sym.symbol("s"),
lmap.map(
{
kw.keyword("tag"): sym.symbol("str"),
reader.READER_LINE_KW: 1,
reader.READER_END_LINE_KW: 1,
reader.READER_COL_KW: 5,
reader.READER_END_COL_KW: 6,
}
),
),
(
"^:dynamic *ns*",
sym.symbol("*ns*"),
lmap.map({kw.keyword("dynamic"): True}),
),
(
'^{:doc "If true, assert."} *assert*',
sym.symbol("*assert*"),
lmap.map({kw.keyword("doc"): "If true, assert."}),
),
(
"^[] {}",
lmap.EMPTY,
lmap.map({kw.keyword("param-tags"): vec.EMPTY}),
),
(
'^[:a b "c"] {}',
lmap.EMPTY,
lmap.map(
{
kw.keyword("param-tags"): vec.v(
kw.keyword("a"), sym.symbol("b"), "c"
)
}
),
),
(
"^:has-meta [:a]",
vec.v(kw.keyword("a")),
lmap.map({kw.keyword("has-meta"): True}),
),
(
"^:has-meta (:a)",
llist.l(kw.keyword("a")),
lmap.map({kw.keyword("has-meta"): True}),
),
(
'^:has-meta {:key "val"}',
lmap.map({kw.keyword("key"): "val"}),
lmap.map({kw.keyword("has-meta"): True}),
),
(
"^:has-meta #{:a}",
lset.s(kw.keyword("a")),
lmap.map({kw.keyword("has-meta"): True}),
),
(
'^:dynamic ^{:doc "If true, assert."} ^python/bool ^[:dynamic :muffs] *assert*',
sym.symbol("*assert*"),
lmap.map(
{
kw.keyword("dynamic"): True,
kw.keyword("doc"): "If true, assert.",
kw.keyword("tag"): sym.symbol("bool", ns="python"),
kw.keyword("param-tags"): vec.v(
kw.keyword("dynamic"), kw.keyword("muffs")
),
}
),
),
(
"^{:always true} ^{:always false} *assert*",
sym.symbol("*assert*"),
lmap.map({kw.keyword("always"): True}),
),
],
)
def test_legal_reader_metadata(
self, s: str, form, expected_meta: lmap.PersistentMap
):
v = read_str_first(s)
assert v == form
self.assert_is_submap(v.meta, expected_meta)

s = read_str_first("^{:always true} ^{:always false} *assert*")
assert s == sym.symbol("*assert*")
assert issubmap(s.meta, lmap.map({kw.keyword("always"): True}))


def test_invalid_meta_structure():
with pytest.raises(reader.SyntaxError):
read_str_first("^35233 {}")

with pytest.raises(reader.SyntaxError):
read_str_first("^583.28 {}")

with pytest.raises(reader.SyntaxError):
read_str_first("^true {}")

with pytest.raises(reader.SyntaxError):
read_str_first("^false {}")

with pytest.raises(reader.SyntaxError):
read_str_first("^nil {}")

with pytest.raises(reader.SyntaxError):
read_str_first('^"String value" {}')


def test_invalid_meta_attachment():
with pytest.raises(reader.SyntaxError):
read_str_first("^:has-meta 35233")

with pytest.raises(reader.SyntaxError):
read_str_first("^:has-meta 583.28")

with pytest.raises(reader.SyntaxError):
read_str_first("^:has-meta :i-am-a-keyword")

with pytest.raises(reader.SyntaxError):
read_str_first("^:has-meta true")

with pytest.raises(reader.SyntaxError):
read_str_first("^:has-meta false")

with pytest.raises(reader.SyntaxError):
read_str_first("^:has-meta nil")
@pytest.mark.parametrize(
"s",
[
"^35233 {}",
"^583.28 {}",
"^12.6J {}",
"^22/7 {}",
"^12.6M {}",
"^true {}",
"^false {}",
"^nil {}",
'^"String value" {}',
],
)
def test_syntax_error_attaching_unsupported_type_as_metadata(self, s: str):
with pytest.raises(reader.SyntaxError):
read_str_first(s)

with pytest.raises(reader.SyntaxError):
read_str_first('^:has-meta "String value"')
@pytest.mark.parametrize(
"s",
[
"^:has-meta 35233",
"^:has-meta 583.28",
"^:has-meta 12.6J",
"^:has-meta 22/7",
"^:has-meta 12.6M",
"^:has-meta :i-am-a-keyword",
"^:has-meta true",
"^:has-meta false",
"^:has-meta nil",
'^:has-meta "String value"',
],
)
def test_syntax_error_attaching_metadata_to_unsupported_type(self, s: str):
with pytest.raises(reader.SyntaxError):
read_str_first(s)


def test_comment_reader_macro():
Expand Down