Skip to content

Commit

Permalink
Tags use only one # again
Browse files Browse the repository at this point in the history
Pack macros now lack the implicit positional.
  • Loading branch information
gilch committed Dec 23, 2023
1 parent f72893a commit 9bcfc03
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 65 deletions.
101 changes: 47 additions & 54 deletions docs/primer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@ Reader Tags
+++++++++++

Besides a few built-ins,
reader macros in Lissp consist of a special symbol ending with ``#``\ s,
reader macros in Lissp consist of a special symbol ending with a ``#``,
called a *tag*,
followed by additional argument forms.

Expand Down Expand Up @@ -1446,8 +1446,23 @@ just like there's no run-time overhead for using a hex literal instead of decima
Multiary Tags
+++++++++++++

Reader tags may take multiple arguments.
You indicate how many with the number of trailing ``#``\ s.
Reader tags may also take keyword arguments indicated by keyword prefixes,
which can be helpful quick refinements for functions with optional arguments,
without the need to create a new reader macro for each specialization.

.. code-block:: REPL
#> builtins..int#.#"21" ; Normal base ten
>>> (21)
21
#> base=builtins..int#6 .#"21" ; base six via optional base= kwarg
>>> (13)
13
The special prefixes ``*=`` and ``**=`` unpack the argument at that position,
either as positional arguments or keyword arguments, respectively.
An empty prefix (``=``) indicates an additional positional argument.

.. code-block:: REPL
Expand All @@ -1460,7 +1475,7 @@ You indicate how many with the number of trailing ``#``\ s.
... )
Fraction(2, 3)
#> fractions..Fraction## 2 3 ; Notice the extra #.
#> =fractions..Fraction# 2 3 ; Notice the =.
>>> __import__('pickle').loads( # Fraction(2, 3)
... b'cfractions\n'
... b'Fraction\n'
Expand All @@ -1469,72 +1484,50 @@ You indicate how many with the number of trailing ``#``\ s.
... )
Fraction(2, 3)
Reader tags may also take keyword arguments indicated by keyword prefixes,
which can be helpful quick refinements for functions with optional arguments,
without the need to create a new reader macro for each specialization.
Multiple prefixes are allowed.
They pull from the reader stream in the order written.
(The final argument for the tag itself is always positional.)

.. code-block:: REPL
#> builtins..int#.#"21" ; Normal base ten
>>> (21)
21
Unqualified Tags
++++++++++++++++

#> base=builtins..int##6 .#"21" ; base six via optional base= kwarg
>>> (13)
13
Sometimes tags have no qualifier.
Three such tags are built into the reader:
inject ``.#``, discard ``_#``, and gensym ``$#``.

The special prefixes ``*=`` and ``**=`` unpack the agument at that position,
either as positional arguments or keyword arguments, respectively.
Prefixes pull from the reader stream in the order written.
Each prefix requires another ``#``.
Any leftover ``#``\ s each pull a positional argument after that.
An empty prefix (``=``) indicates a single positional argument.
These can be used to put positional arguments between or before kwargs.
The reader will also check the current module's ``_macro_`` namespace (if it has one)
for attributes ending in ``#`` (i.e. ``QzHASH_``)
when it encounters an unqualified tag.
Prefixes should not be included in the attribute name.
It is possible to use a tag name containing ``=``\ s.
but they must each be escaped with a ``\``.

Pack Objects
++++++++++++

Try to avoid using more than about 3 or 4 ``#``\ s in a tag,
because that gets hard to read.
You typically won't need more than that.
For too many homogeneous arguments,
(i.e., with the same semantic type)
consider using ``*=`` applied to a tuple instead.
For too many heterogeneous arguments, consider ``**=``.
For complicated expressions,
consider using inject (``.#``) on tuple expressions instead of using tags.

A tag can be empty if it has at least one prefix,
even the empty prefix (``=``).
An empty tag creates a `Pack` object,
which contains any args and kwargs given.
When a reader tag pulls one, it automatically unpacks it.

`Pack`\ s are used to order and group tag arguments in a hierarchical way,
for improved legibility.
They're another way to avoid using too many ``#``\ s in a row.
They allow you to write the keywords immediately before their values,
instead of up front.
When another reader tag pulls one as a positional argument, it automatically unpacks it.

`Pack`\ s are only meant for reader tags.
They should be consumed immediately at read time,
and are only allowed to survive past that for debugging purposes.

Unqualified Tags
++++++++++++++++

Sometimes tags have no qualifier.
Three such tags are built into the reader:
inject ``.#``, discard ``_#``, and gensym ``$#``.

The reader will also check the current module's ``_macro_`` namespace (if it has one)
for attributes ending in ``#`` (i.e. ``QzHASH_``)
when it encounters an unqualified tag.
The ``#`` is only in an attribute name to distinguish them from normal compile-time macros,
not to indicate arity.
Prefixes should not be included in the attribute name either.
It is possible to use a tag name containing ``=`` or extra ``#``\ s,
but they must be escaped with a ``\``.
`Pack`\ s are used to order and group tag arguments in a hierarchical way,
for improved legibility.
They allow you to write the keywords immediately before their values,
instead of up front.
They're also a way to avoid using too many positional prefixes (``=``\ s) in a row,
which can get hard to read.
You typically won't need more than ``===``.
For too many homogeneous arguments,
(i.e., with the same semantic type)
consider using ``*=`` applied to a tuple instead.
For too many heterogeneous arguments, consider ``**=``.
For complicated expressions,
consider using inject (``.#``) on tuple expressions instead of using tags.

Discard
+++++++
Expand Down
10 changes: 5 additions & 5 deletions src/hissp/macros.lissp
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior.
;; >>> b'Fully-qualified b# macro at read time.'
;; b'Fully-qualified b# macro at read time.'
;;
;; #> H:##b"Read-time b# via alias."
;; #> =H:#b"Read-time b# via alias."
;; >>> b'Read-time b# via alias.'
;; b'Read-time b# via alias.'
;;
Expand Down Expand Up @@ -963,8 +963,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior.

.. code-block:: REPL

#> @##str.swapcase
#..@##str.title
#> =@#str.swapcase
#..=@#str.title
#..(define spam 'spam) ; Unlike Python def, not always a function.
>>> # hissp.macros.._macro_.define
... __import__('builtins').globals().update(
Expand Down Expand Up @@ -2828,7 +2828,7 @@ Creates a lambda of arity {X} containing a `^*#<QzHAT_QzSTAR_QzHASH_>`

.. code-block:: REPL

#> (op#add 5 file=spy##sys..stdout(op#mul 7 3))
#> (op#add 5 file=spy#sys..stdout(op#mul 7 3))
>>> __import__('operator').add(
... (5),
... # hissp.._macro_._spy
Expand Down Expand Up @@ -2861,7 +2861,7 @@ Creates a lambda of arity {X} containing a `^*#<QzHAT_QzSTAR_QzHASH_>`

.. code-block:: REPL

#> file=time##sys..stdout(time..sleep .05)
#> file=time#sys..stdout(time..sleep .05)
>>> # hissp.macros.._macro_.let
... (lambda _QzPMWTVFTZz_time=__import__('time').time_ns:
... # hissp.macros.._macro_.letQz_from
Expand Down
11 changes: 5 additions & 6 deletions src/hissp/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
|['`,]
|[.][#]
# Any atom that ends in ``#``, but not ``.#`` or ``\#``.
|(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#]+
|(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#]
)
|(?P<string>
[#]? # raw?
Expand Down Expand Up @@ -436,12 +436,11 @@ def _get_counter(self) -> int:

def _custom_macro(self, form, tag: str):
assert tag.endswith("#")
arity = tag.replace(R"\#", "").count("#")
arity = tag.replace(R"\=", "").count("=")
*keywords, label = re.findall(r"((?:[^=\\]|\\.)*[=#])", tag)
arity += bool(label)
assert arity > 0
*keywords, label = re.findall(r"((?:[^=\\]|\\.)*(?:=|#+))", tag)
if len(keywords) > arity:
raise SyntaxError(f"Not enough # for each = in {tag!r}")
label = label[:-arity]
label = label[:-1]
label = force_munge(self.escape(label))
label = re.sub(r"(^\.)", lambda m: force_qz_encode(m[1]), label)
fn: Fn[[str], Fn] = self._fully_qualified if ".." in label else self._local
Expand Down

0 comments on commit 9bcfc03

Please sign in to comment.