diff --git a/docs/primer.rst b/docs/primer.rst index 6beeec813..5e81dc4ca 100644 --- a/docs/primer.rst +++ b/docs/primer.rst @@ -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. @@ -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 @@ -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' @@ -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 +++++++ diff --git a/docs/style_guide.rst b/docs/style_guide.rst index 2d42a220e..01bcd363f 100644 --- a/docs/style_guide.rst +++ b/docs/style_guide.rst @@ -376,9 +376,9 @@ a form modifying the previous (e.g. decorating, attaching attributes), or adding it to a collection may be attached to it. However, in many of these cases, -the groups could be written as a single top-level form insead, +the groups could be written as a single top-level form instead, given the appropriate functions or macros. -E.g. `dict.update` (on `globals`), `let`, `@##`, `attach`, `doto`. +E.g. `dict.update` (on `globals`), `let`, `@#`, `attach`, `doto`. Try to avoid blank lines within forms. You may need them for separating groups whose elements span lines @@ -778,7 +778,7 @@ in which case they may be treated as literal values. Avoid using inline or margin comments as commentary between a tag and its target, as this can cause errors when they are instead treated as arguments. (Usually, tags are attached to one argument, so this doesn't come up, -but e.g. the bundled decorator macro `@##` typically is not.) +but e.g. the bundled decorator macro `@#` typically is not.) You may use a discarded string instead ``_#"NB foo"``. A good syntax highlighter specialized for Lissp may be able to indicate when a comment token is not discarded, diff --git a/src/hissp/macros.lissp b/src/hissp/macros.lissp index 7889d9c76..c7ad4921f 100644 --- a/src/hissp/macros.lissp +++ b/src/hissp/macros.lissp @@ -823,32 +823,25 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; >>> # hissp.._macro_.alias ;; ... # hissp.macros.._macro_.defmacro ;; ... # hissp.macros.._macro_.let - ;; ... (lambda _Qz2D5FNHXZz_fn=(lambda _QzE4JATHEUz_first,*_QzE4JATHEUz_rest,**_QzE4JATHEUz_kwargs:( + ;; ... (lambda _Qz2D5FNHXZz_fn=(lambda _QzE4JATHEUz_attr,*_QzE4JATHEUz_args,**_QzE4JATHEUz_kwargs:( ;; ... 'Aliases ``hissp.._macro_`` as ``HQzCOLON_#``.', ;; ... # hissp.macros.._macro_.ifQz_else ;; ... (lambda b,c,a:c()if b else a())( - ;; ... _QzE4JATHEUz_rest, + ;; ... (lambda X,Y:X or Y)( + ;; ... _QzE4JATHEUz_args, + ;; ... _QzE4JATHEUz_kwargs), ;; ... (lambda : - ;; ... # hissp.macros.._macro_.let - ;; ... (lambda _QzE4JATHEUz_rest=__import__('builtins').list( - ;; ... _QzE4JATHEUz_rest): - ;; ... __import__('builtins').getattr( - ;; ... __import__('hissp')._macro_, - ;; ... ('{}{}').format( - ;; ... _QzE4JATHEUz_first, - ;; ... # hissp.macros.._macro_.ifQz_else - ;; ... (lambda b,c,a:c()if b else a())( - ;; ... 'hissp.._macro_'.endswith( - ;; ... '._macro_'), - ;; ... (lambda :'QzHASH_'), - ;; ... (lambda :('')))))( - ;; ... _QzE4JATHEUz_rest.pop(), - ;; ... *_QzE4JATHEUz_rest, - ;; ... **_QzE4JATHEUz_kwargs))()), + ;; ... __import__('builtins').getattr( + ;; ... __import__('hissp')._macro_, + ;; ... ('{}{}').format( + ;; ... _QzE4JATHEUz_attr, + ;; ... 'QzHASH_'))( + ;; ... *_QzE4JATHEUz_args, + ;; ... **_QzE4JATHEUz_kwargs)), ;; ... (lambda : ;; ... ('{}.{}').format( ;; ... 'hissp.._macro_', - ;; ... _QzE4JATHEUz_first))))[-1]):( + ;; ... _QzE4JATHEUz_attr))))[-1]):( ;; ... __import__('builtins').setattr( ;; ... _Qz2D5FNHXZz_fn, ;; ... '__doc__', @@ -883,7 +876,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.' ;; @@ -893,17 +886,14 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; See also: `prelude`, `attach`. ;; `(defmacro ,(.format "{}{}" abbreviation '#) - ($#first : :* $#rest :** $#kwargs) + ($#attr : :* $#args :** $#kwargs) ',(.format "Aliases ``{}`` as ``{}#``." qualifier abbreviation) - (if-else $#rest - (let ($#rest (list $#rest)) - ((getattr ,qualifier (.format "{}{}" - $#first - (if-else (.endswith ',qualifier ','._macro_) - ','# - ""))) - (.pop $#rest) : :* $#rest :** $#kwargs)) - (.format "{}.{}" ',qualifier $#first)))) + (if-else (,'XY#.#"X or Y" $#args $#kwargs) + ((getattr ,qualifier ,(if-else (.endswith qualifier '._macro_) + `(.format "{}{}" ,'$#attr ','#) + '$#attr)) + : :* $#args :** $#kwargs) + (.format "{}.{}" ',qualifier $#attr)))) (alias i itertools.) (alias op operator.) @@ -963,8 +953,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( @@ -2828,7 +2818,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 file=spy#sys..stdout(op#mul 7 3)) >>> __import__('operator').add( ... (5), ... # hissp.._macro_._spy @@ -2861,7 +2851,7 @@ Creates a lambda of arity {X} containing a `^*#` .. 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 diff --git a/src/hissp/reader.py b/src/hissp/reader.py index 7952c475e..94ff4a126 100644 --- a/src/hissp/reader.py +++ b/src/hissp/reader.py @@ -75,7 +75,7 @@ |['`,] |[.][#] # Any atom that ends in ``#``, but not ``.#`` or ``\#``. - |(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#]+ + |(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#] ) |(?P [#]? # raw? @@ -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[:-1]) 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