diff --git a/docs/primer.rst b/docs/primer.rst index 6beeec813..1e81f7997 100644 --- a/docs/primer.rst +++ b/docs/primer.rst @@ -1469,56 +1469,23 @@ You indicate how many with the number of trailing ``#``\ s. ... ) Fraction(2, 3) -Reader tags may also take keyword arguments indicated by keyword prefixes, +Reader tags may also take keyword arguments, +made with a helper kwarg tag ending in ``=#``, 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 + #> builtins..int# .#"21" ; Normal base ten >>> (21) 21 - #> base=builtins..int##6 .#"21" ; base six via optional base= kwarg + #> builtins..int## base=#6 .#"21" ; base 6, via base=# kwarg tag >>> (13) 13 -The special prefixes ``*=`` and ``**=`` unpack the agument at that position, +The helper tags ``*=#`` and ``**=#`` unpack the argument 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. - -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. - -`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 ++++++++++++++++ @@ -1532,9 +1499,8 @@ 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 ``\``. +It is possible to use a tag name containing extra ``#``\ s, +or ending in ``=#`` if escaped with a ``\``. Discard +++++++ diff --git a/src/hissp/macros.lissp b/src/hissp/macros.lissp index 7889d9c76..bb64baf62 100644 --- a/src/hissp/macros.lissp +++ b/src/hissp/macros.lissp @@ -2828,7 +2828,7 @@ Creates a lambda of arity {X} containing a `^*#` .. code-block:: REPL - #> (op#add 5 file=spy##sys..stdout(op#mul 7 3)) + #> (op#add 5 spy##file=#sys..stdout(op#mul 7 3)) >>> __import__('operator').add( ... (5), ... # hissp.._macro_._spy @@ -2861,7 +2861,7 @@ Creates a lambda of arity {X} containing a `^*#` .. code-block:: REPL - #> file=time##sys..stdout(time..sleep .05) + #> time##file=#sys..stdout(time..sleep .05) >>> # hissp.macros.._macro_.let ... (lambda _QzPMWTVFTZz_time=__import__('time').time_ns: ... # hissp.macros.._macro_.letQz_from diff --git a/src/hissp/reader.py b/src/hissp/reader.py index 7952c475e..5a1e088fc 100644 --- a/src/hissp/reader.py +++ b/src/hissp/reader.py @@ -176,18 +176,18 @@ def __repr__(self): return f"Comment({self.token!r})" -class Pack: - """Contains read-time arguments for reader macros. +class Kwarg: + """Contains a read-time keyword argument for reader macros. - Normally made with empty tags, but can be constructed directly. + Normally made with kwarg tags, but can be constructed directly. """ - def __init__(self, *args, **kwargs): - self.args = list(args) - self.kwargs = kwargs + def __init__(self, k, v): + self.k = k + self.v = v def __repr__(self): - return f"Pack(*{self.args},**{self.kwargs})" + return f"Kwarg({self.k}, {self.v})" class Lissp: @@ -436,12 +436,11 @@ def _get_counter(self) -> int: def _custom_macro(self, form, tag: str): assert tag.endswith("#") + if re.fullmatch(r"(?:[^\\]|\\.)+=#", tag): + return Kwarg(tag[:-2], form) arity = tag.replace(R"\#", "").count("#") 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 = tag[:-arity] 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 @@ -450,27 +449,17 @@ def _custom_macro(self, form, tag: str): kwargs = {} depth = len(self.depth) with self.compiler.macro_context(): - for i, [k, v] in enumerate( - zip_longest( - (k[:-1] for k in keywords), chain([form], self._filter_drop()) - ), - 1, - ): - if type(v) is Pack: - if k: - raise SyntaxError( - f"Can't apply prefix {k}= to {v}.", self.position(self._pos) - ) - args.extend(v.args) - kwargs.update(v.kwargs) - elif k == "*": - args.extend(v) - elif k == "**": - kwargs.update(v) - elif k: - kwargs[force_munge(self.escape(k))] = v + for i, x in enumerate(chain([form], self._filter_drop()), 1): + if type(x) is Kwarg: + k, v = x.k, x.v + if k == "*": + args.extend(v) + elif k == "**": + kwargs.update(v) + else: + kwargs[force_munge(self.escape(k))] = v else: - args.append(v) + args.append(x) if i == arity: break else: