diff --git a/docs/lissp_whirlwind_tour.rst b/docs/lissp_whirlwind_tour.rst index 7a263dbe5..3f985bd56 100644 --- a/docs/lissp_whirlwind_tour.rst +++ b/docs/lissp_whirlwind_tour.rst @@ -897,7 +897,7 @@ Lissp Whirlwind Tour ;;; represent it directly. Tags apply to the next parsed Hissp object ;;; at read time, before the Hissp compiler sees it, and thus before ;;; they are compiled and evaluated. Tags end in # except for a few - ;;; builtins-- ' ! ` , ,@ + ;;; builtins-- ' ` , ,@ ;;;; 11.1 Quote @@ -1327,7 +1327,7 @@ Lissp Whirlwind Tour ;; Remember, a gensym hash prefix is an alternative to qualification - ;; for locals. (Thus, templates don't qualified them.) + ;; for locals. (Thus, templates don't qualify them.) #> (setattr _macro_ #.. 'once-triple #.. (lambda x @@ -1886,203 +1886,3 @@ Lissp Whirlwind Tour ;; Statement injections work at the top level only. #> .#"from operator import *" ;All your operator are belong to us. >>> from operator import * - - - ;;;; 15.4 Extra (!), the Final Builtin Reader Macro - - ;;; Reader macros have one primary argument, but additional arguments - ;;; can be passed in with the extra macro !. A tag consumes the next - ;;; parsed object, and if it's an Extra, consumes one again. Thus, - ;;; extras must be written between the tag and primary, but because - ;;; they're often optional refinements, which are easier to define as - ;;; trailing optional parameters in Python functions, they get passed in - ;;; after the primary. - - #> (setattr _macro_ 'L\# en#list) ; (help _macro_.en\#) - #.. - >>> setattr( - ... _macro_, - ... 'LQzHASH_', - ... (lambda *_Qz6RFWTTVXz_xs: - ... list( - ... _Qz6RFWTTVXz_xs))) - - - #> L#primary - >>> ['primary'] - ['primary'] - - #> L#!1 primary - >>> ['primary', 1] - ['primary', 1] - - - ;; Alias can work on reader macros too! - #> (hissp.._macro_.alias M: hissp.._macro_) - >>> # hissp.._macro_.alias - ... # hissp.macros.._macro_.defmacro - ... # hissp.macros.._macro_.let - ... (lambda _QzAW22OE5Kz_fn=(lambda _QzARAQTXTEz_prime,_QzARAQTXTEz_reader=None,*_QzARAQTXTEz_args:( - ... 'Aliases ``hissp.._macro_`` as ``MQzCOLON_#``.', - ... # hissp.macros.._macro_.ifQz_else - ... (lambda b,c,a:c()if b else a())( - ... _QzARAQTXTEz_reader, - ... (lambda : - ... __import__('builtins').getattr( - ... __import__('hissp')._macro_, - ... ('{}{}').format( - ... _QzARAQTXTEz_reader, - ... # hissp.macros.._macro_.ifQz_else - ... (lambda b,c,a:c()if b else a())( - ... 'hissp.._macro_'.endswith( - ... '._macro_'), - ... (lambda :'QzHASH_'), - ... (lambda :('')))))( - ... _QzARAQTXTEz_prime, - ... *_QzARAQTXTEz_args)), - ... (lambda : - ... ('{}.{}').format( - ... 'hissp.._macro_', - ... _QzARAQTXTEz_prime))))[-1]):( - ... __import__('builtins').setattr( - ... _QzAW22OE5Kz_fn, - ... '__doc__', - ... 'Aliases ``hissp.._macro_`` as ``MQzCOLON_#``.'), - ... __import__('builtins').setattr( - ... _QzAW22OE5Kz_fn, - ... '__qualname__', - ... ('.').join( - ... ('_macro_', - ... 'MQzCOLON_QzHASH_',))), - ... __import__('builtins').setattr( - ... __import__('operator').getitem( - ... __import__('builtins').globals(), - ... '_macro_'), - ... 'MQzCOLON_QzHASH_', - ... _QzAW22OE5Kz_fn))[-1])() - - #> M:#!b"Read-time b# via alias." ;Extra arg for alias with (!) - >>> b'Read-time b# via alias.' - b'Read-time b# via alias.' - - - #> L# !1 !2 primary ;Note the order! - >>> ['primary', 1, 2] - ['primary', 1, 2] - - #> .#(en#list "primary" 1 2) ;Inject. Note the order. - >>> ['primary', 1, 2] - ['primary', 1, 2] - - - #> !1 ;! is for a single Extra. - >>> __import__('pickle').loads( # Extra([1]) - ... b'ccopyreg\n' - ... b'_reconstructor\n' - ... b'(chissp.reader\n' - ... b'Extra\n' - ... b'cbuiltins\n' - ... b'tuple\n' - ... b'(I1\n' - ... b'ttR.' - ... ) - Extra([1]) - - #> hissp.reader..Extra#(: :? 0 :* (1 2 3)) ; but Extra can have multiple elements. - >>> __import__('pickle').loads( # Extra([':', ':?', 0, ':*', (1, 2, 3)]) - ... b'ccopyreg\n' - ... b'_reconstructor\n' - ... b'(chissp.reader\n' - ... b'Extra\n' - ... b'cbuiltins\n' - ... b'tuple\n' - ... b'(V:\n' - ... b'V:?\n' - ... b'I0\n' - ... b'V:*\n' - ... b'(I1\n' - ... b'I2\n' - ... b'I3\n' - ... b'tttR.' - ... ) - Extra([':', ':?', 0, ':*', (1, 2, 3)]) - - #> !!!1 2 3 ;Extras can have extras. - >>> __import__('pickle').loads( # Extra([1, 2, 3]) - ... b'ccopyreg\n' - ... b'_reconstructor\n' - ... b'(chissp.reader\n' - ... b'Extra\n' - ... b'cbuiltins\n' - ... b'tuple\n' - ... b'(I1\n' - ... b'I2\n' - ... b'I3\n' - ... b'ttR.' - ... ) - Extra([1, 2, 3]) - - - #> L#!: !:* !(0 1 2) !:? !3 primary ;Unpacking works like calls. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - #> L#!0 !: !:* !(1 2 3)primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - #> L#hissp.reader..Extra#(0 : :* (1 2 3))primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - - #> (setattr _macro_ 'E\# hissp.reader..Extra) - >>> setattr( - ... _macro_, - ... 'EQzHASH_', - ... __import__('hissp.reader',fromlist='?').Extra) - - - #> L# !0 E#(1 2) !3 primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - #> L#E#(0 : :* (1 2 3))primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - - ;; Kwargs also work like calls. - #> builtins..dict#() - >>> {} - {} - - #> builtins..dict#!: !spam !1 !foo !2 !:** !.#(dict : eggs 3 bar 4)() - >>> {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - - #> builtins..dict#E#(: spam 1 foo 2 :** .#(dict : eggs 3 bar 4))() - >>> {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - - #> builtins..dict#!: !!spam 1 !!foo 2 !!:** .#(dict : eggs 3 bar 4)() - >>> {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - - - ;; Yeah, you can nest these if you have to. - #> L# !x - #.. !L# !1 L# !A - #.. inner - #.. !y - #..outer - >>> ['outer', 'x', [['inner', 'A'], 1], 'y'] - ['outer', 'x', [['inner', 'A'], 1], 'y'] - - - ;; The compiler will evaluate tuples no matter how the reader produces them. - #> builtins..tuple#L# !"Hello" !"World!" print - >>> print( - ... ('Hello'), - ... ('World!')) - Hello World! diff --git a/docs/macro_tutorial.rst b/docs/macro_tutorial.rst index f6284f5ca..0bd8eeb39 100644 --- a/docs/macro_tutorial.rst +++ b/docs/macro_tutorial.rst @@ -713,7 +713,7 @@ Now we're shorter than Python: But we're also less general. We can change the expression, but we've hardcoded the parameters to it. -The fixed parameter name is fine as long unless it shadows a `nonlocal ` we need, +The fixed parameter name is fine unless it shadows a `nonlocal ` we need, but what if we needed two parameters? Could we make a macro for that? diff --git a/docs/primer.rst b/docs/primer.rst index d33a1a8d7..1e81f7997 100644 --- a/docs/primer.rst +++ b/docs/primer.rst @@ -757,7 +757,7 @@ The ``:*`` can likewise act as a separator starting the keyword-only arguments, and can likewise be paired with ``:?``. The normal parameters in between these can be passed in either as positional arguments -or as keyword arguments. +or as keyword arguments (kwargs). The ``:*`` can instead pair with a parameter name, which collects the remainder of the positional arguments into a tuple. @@ -1367,14 +1367,13 @@ Unfortunately, there are some objects even pickle can't handle. Hissp had to give up with an error this time. -Qualified Reader Macros -+++++++++++++++++++++++ +Reader Tags ++++++++++++ Besides a few built-ins, -reader macros in Lissp consist of a symbol ending with a ``#``, +reader macros in Lissp consist of a special symbol ending with ``#``\ s, called a *tag*, -followed by another form, -called its *primary*. +followed by additional argument forms. A function named by a `qualified identifier`_ is invoked on the form, and the reader embeds the resulting object into the output Hissp: @@ -1444,12 +1443,64 @@ then there is no run-time overhead for the alternative notation, because it's compiled to ``(81)``, just like there's no run-time overhead for using a hex literal instead of decimal in Python. -Sometimes tags can be unqualified. -Three tags are built into the reader: +Multiary Tags ++++++++++++++ + +Reader tags may take multiple arguments. +You indicate how many with the number of trailing ``#``\ s. + +.. code-block:: REPL + + #> fractions..Fraction# .#"2/3" ; Two thirds. + >>> __import__('pickle').loads( # Fraction(2, 3) + ... b'cfractions\n' + ... b'Fraction\n' + ... b'(V2/3\n' + ... b'tR.' + ... ) + Fraction(2, 3) + + #> fractions..Fraction## 2 3 ; Notice the extra #. + >>> __import__('pickle').loads( # Fraction(2, 3) + ... b'cfractions\n' + ... b'Fraction\n' + ... b'(V2/3\n' + ... b'tR.' + ... ) + Fraction(2, 3) + +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 + >>> (21) + 21 + + #> builtins..int## base=#6 .#"21" ; base 6, via base=# kwarg tag + >>> (13) + 13 + +The helper tags ``*=#`` and ``**=#`` unpack the argument at that position, +either as positional arguments or keyword arguments, respectively. + +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. +It is possible to use a tag name containing extra ``#``\ s, +or ending in ``=#`` if escaped with a ``\``. Discard +++++++ @@ -1470,7 +1521,6 @@ Templates +++++++++ Besides ``'``, which we've already seen, -and ``!``, which we'll cover later, Lissp has three other built-in reader macros that don't require a ``#``: * ````` template quote @@ -1593,7 +1643,7 @@ Gensyms +++++++ The built-in tag ``$#`` creates a *generated symbol* -(gensym) based on the given primary symbol. +(gensym) based on the given symbol. Within a template, the same gensym name always makes the same gensym: .. code-block:: REPL @@ -1640,56 +1690,6 @@ which can sometimes happen when they are very short. By default, the hash is a prefix, but you can mark some other location for it using a $. -Extra -+++++ - -The final built-in reader macro ``!`` -is used to pass extra arguments to other reader macros. -None of Lissp's built-in reader macros use it -(although some of the `bundled macros ` do), -but extras 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 - - #> builtins..int#!6 .#"21" ; base six via optional base arg - >>> (13) - 13 - -A reader macro can have more than one extra. - -Note that since extras are often optional arguments, -they're passed in *after* the reader macro's primary argument, -even though they're written first. - -.. code-block:: REPL - - #> builtins..range# !0 !-1 20 - >>> __import__('pickle').loads( # range(20, 0, -1) - ... b'cbuiltins\n' - ... b'range\n' - ... b'(I20\n' - ... b'I0\n' - ... b'I-1\n' - ... b'tR.' - ... ) - range(20, 0, -1) - -Pass in keyword arguments by pairing with a name after ``:``, -like calls. ``:*`` and ``:**`` unpacking also work here. - -.. code-block:: REPL - - #> builtins..int# !: !base !6 .#"21" - >>> (13) - 13 - -See the section on Extras in the `lissp_whirlwind_tour` for more examples. - Macros ====== diff --git a/docs/style_guide.rst b/docs/style_guide.rst index 8db011ae1..56ad42647 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 @@ -771,14 +771,14 @@ comment from the previous line. Unlike inline comments, margin comment continuation lines need not have code on their line. -Be careful with comments around detached reader tags!. +Be careful with comments around detached reader tags! Comment tokens are normally discarded by the reader in Lissp, but they are a valid target for reader macros, 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 their primary, so this doesn't come up, -but e.g. the bundled decorator macro `@#!` typically is not.) +(Usually, tags are attached to one argument, so this doesn't come up, +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, @@ -804,7 +804,7 @@ Lissp parses comments in blocks, so multiline comments used as reader arguments nearly always use a form/group comment starting with two semicolons and a space as described below. But with a single ``;``, they must follow code on the same line, -typically the reader tag itself, or an `Extra` macro ``!``. +typically the reader tag itself. In the rare case neither is valid (if the macro is counting the semicolons), then it's a margin comment. Indent it to the margin. @@ -1036,123 +1036,6 @@ if it's not obvious from the identifier. This way, all three name versions (munged, demunged, and pronounced) will appear in generated docs. -Reader Macros -::::::::::::: - -Reader macros should not be separated from each other -or from their primary argument with whitespace. - -.. code-block:: Lissp - - ' builtins..repr# .# (lambda :) ;Bad. - 'builtins..repr#.#(lambda :) ;Preferred. - -Separating the tag with a space is acceptable when the primary starts with a ``|`` character, -because ``#|`` starts a block comment in other Lisp dialects. -Any editor not specialized for Lissp may get confused. -``#\|`` is an alternative. - -If a primary argument spans multiple lines, -it's acceptable to separate with a newline, -but be careful not to accidentally put a comment in between, -unless you explicitly discard it. - -.. code-block:: Lissp - - _# ; Bad. Comments are valid reader macro arguments! - ((lambda abc ;This wasn't discarded! - (frobnicate a b c)) - arg) - - _# - ;; Bad. This comment would have been discarded anyway. - ((lambda abc ;But this wasn't discarded! - (frobnicate a b c)) - arg) - - _#_# - ;; OK. This actually works. - ((lambda abc ;This was discarded too. - (frobnicate a b c)) - arg) - - ;; OK. Put the tag after the comment on its own line. - _# - ((lambda abc - (frobnicate a b c)) - arg) - - _#((lambda abc - (frobnicate a b c)) ;Bad. Wrong indentation! - arg) - - _#((lambda abc ;Preferred. No separation, good indents. - (frobnicate a b c)) - arg) - - ;; OK. Composed macros can group. Primary spanned multiple lines. - `', - ((lambda abc - (frobnicate a b c)) - arg) - - `',((lambda abc ;Preferred. No separation. - (frobnicate a b c)) - arg) - -Extras may always be separated from the tag, -but only imply groups of extras with whitespace if they are semantically grouped. - -.. code-block:: Lissp - - builtins..int#!6 .#"21" ;Preferred. Spacing not required. - builtins..int# !6 "21" ;OK. Extras may always be separated. - - 'foo#!(spam)!(eggs)bar ;Preferred. Spacing not required. - 'foo# !(spam) !(eggs) bar ;OK. Extras may always be separated. - 'foo# !(spam)!(eggs) bar ;Bad if grouping not meaningful. - 'foo#!(spam) !(eggs) bar ;Bad for the same reason. - -You can also imply groups by stacking bangs, -but no more than three in a row. - -.. code-block:: Lissp - - builtins..dict# !: !foo !2 !bar !4 () ;OK. Grouped by extra space. - builtins..dict#!: !foo!2 !bar!4() ;Bad. {'fooQzBANG_2': 'barQzBANG_4'} - builtins..dict# !!!: foo 2 !! bar 4 () ;OK. Meaningful breaks, no more than !!!. - builtins..dict#!: !!foo 2 !!bar 4() ;Preferred. Pairs grouped by stacking. - builtins..dict#!!!!!: foo 2 bar 4 () ;Bad. Have to count bangs. - -Align extras spanning lines like tuple contents. - -.. code-block:: Lissp - - ;; Extras aligned with the first extra. - foo#!spam - !eggs - !ham - bar ;Primary isn't an extra. Aligned with tag. - - ;; Extras aligned with the first extra. - foo# - !spam - !eggs - !ham - bar - - ;; Indent recursively. - foo#!spam - !bar#!sausage - !bacon - :tomato - !eggs - :beans - - ;; Don't dangle brackets! - (print <<#;Hello, World! - _#/) - Identifiers =========== diff --git a/src/hissp/macros.lissp b/src/hissp/macros.lissp index 41d0bc71e..e395b79e2 100644 --- a/src/hissp/macros.lissp +++ b/src/hissp/macros.lissp @@ -514,7 +514,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... b=(2))) ;; {'a': 1, 'b': 2} ;; - ;; See also: `attach`, `type`, `@#!`, :keyword:`class`, + ;; See also: `attach`, `type`, `@#`, :keyword:`class`, ;; `types.new_class` ;; (let (ibases (iter bases)) @@ -672,7 +672,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. See also, `attach`, `python-grammar:assignment_expression`. " - ;; TODO: consider using extras for initial contents. + ;; TODO: consider using kwargs for initial contents. `(let (,'my (types..SimpleNamespace)) ,e)) @@ -823,34 +823,29 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; >>> # hissp.._macro_.alias ;; ... # hissp.macros.._macro_.defmacro ;; ... # hissp.macros.._macro_.let - ;; ... (lambda _QzAW22OE5Kz_fn=(lambda _QzARAQTXTEz_prime,_QzARAQTXTEz_reader=None,*_QzARAQTXTEz_args:( + ;; ... (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())( - ;; ... _QzARAQTXTEz_reader, + ;; ... _QzE4JATHEUz_args, ;; ... (lambda : ;; ... __import__('builtins').getattr( ;; ... __import__('hissp')._macro_, ;; ... ('{}{}').format( - ;; ... _QzARAQTXTEz_reader, - ;; ... # hissp.macros.._macro_.ifQz_else - ;; ... (lambda b,c,a:c()if b else a())( - ;; ... 'hissp.._macro_'.endswith( - ;; ... '._macro_'), - ;; ... (lambda :'QzHASH_'), - ;; ... (lambda :('')))))( - ;; ... _QzARAQTXTEz_prime, - ;; ... *_QzARAQTXTEz_args)), + ;; ... _QzE4JATHEUz_attr, + ;; ... 'QzHASH_'))( + ;; ... *_QzE4JATHEUz_args, + ;; ... **_QzE4JATHEUz_kwargs)), ;; ... (lambda : ;; ... ('{}.{}').format( ;; ... 'hissp.._macro_', - ;; ... _QzARAQTXTEz_prime))))[-1]):( + ;; ... _QzE4JATHEUz_attr))))[-1]):( ;; ... __import__('builtins').setattr( - ;; ... _QzAW22OE5Kz_fn, + ;; ... _Qz2D5FNHXZz_fn, ;; ... '__doc__', ;; ... 'Aliases ``hissp.._macro_`` as ``HQzCOLON_#``.'), ;; ... __import__('builtins').setattr( - ;; ... _QzAW22OE5Kz_fn, + ;; ... _Qz2D5FNHXZz_fn, ;; ... '__qualname__', ;; ... ('.').join( ;; ... ('_macro_', @@ -860,7 +855,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... __import__('builtins').globals(), ;; ... '_macro_'), ;; ... 'HQzCOLON_QzHASH_', - ;; ... _QzAW22OE5Kz_fn))[-1])() + ;; ... _Qz2D5FNHXZz_fn))[-1])() ;; ;; #> 'H:#alias ;; >>> 'hissp.._macro_.alias' @@ -879,7 +874,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." ;Extra arg for alias with (!) + ;; #> H:##b"Read-time b# via alias." ;; >>> b'Read-time b# via alias.' ;; b'Read-time b# via alias.' ;; @@ -889,16 +884,14 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; See also: `prelude`, `attach`. ;; `(defmacro ,(.format "{}{}" abbreviation '#) - ($#prime : $#reader None :* $#args) + ($#attr : :* $#args :** $#kwargs) ',(.format "Aliases ``{}`` as ``{}#``." qualifier abbreviation) - (if-else $#reader - ((getattr ,qualifier (.format "{}{}" - $#reader - (if-else (.endswith ',qualifier ','._macro_) - ','# - ""))) - $#prime : :* $#args) - (.format "{}.{}" ',qualifier $#prime)))) + (if-else $#args + ((getattr ,qualifier ,(if-else (.endswith qualifier '._macro_) + `(.format "{}{}" ,'$#attr ','#) + '$#attr)) + : :* $#args :** $#kwargs) + (.format "{}.{}" ',qualifier $#attr)))) (alias i itertools.) (alias op operator.) @@ -946,8 +939,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; `(op#itemgetter ,e)) -(defmacro @\# (definition decoration) - "``@#!`` 'decorator' applies ``decoration`` to a global and reassigns. +(defmacro @\# (decoration definition) + "``@#`` 'decorator' applies ``decoration`` to a global and reassigns. ``definition`` form must assign a global identified by its first arg. Expands to a `define`, meaning decorators can stack. @@ -958,8 +951,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( @@ -2823,7 +2816,7 @@ Creates a lambda of arity {X} containing a `^*#` .. code-block:: REPL - #> (op#add 5 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 @@ -2856,7 +2849,7 @@ Creates a lambda of arity {X} containing a `^*#` .. code-block:: REPL - #> 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 b70a35d2b..cd1254b90 100644 --- a/src/hissp/reader.py +++ b/src/hissp/reader.py @@ -19,7 +19,7 @@ from contextlib import contextmanager, nullcontext, suppress from functools import reduce from importlib import import_module, resources -from itertools import chain, takewhile +from itertools import chain from keyword import iskeyword as _iskeyword from pathlib import Path, PurePath from pprint import pformat @@ -72,10 +72,10 @@ |(?P\)) |(?P ,@ - |['`,!] + |['`,] |[.][#] # Any atom that ends in ``#``, but not ``.#`` or ``\#``. - |(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#] + |(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#]+ ) |(?P [#]? # raw? @@ -176,14 +176,18 @@ def __repr__(self): return f"Comment({self.token!r})" -class Extra(tuple): - """Designates Extra read-time arguments for reader macros. +class Kwarg: + """Contains a read-time keyword argument for reader macros. - Normally made with the ``!`` macro, but can be constructed directly. + Normally made with kwarg tags, but can be constructed directly. """ + def __init__(self, k, v): + self.k = k + self.v = v + def __repr__(self): - return f"Extra({list(self)!r})" + return f"Kwarg({self.k}, {self.v})" class Lissp: @@ -290,7 +294,7 @@ def _macro(self, v): ",": self.unquote_context, ",@": self.unquote_context, }.get(v, nullcontext)(): - yield self.parse_macro(v, *self._extras(p, v)) + yield self.parse_macro(v, self._pull(v, p)) @contextmanager def gensym_context(self): @@ -315,19 +319,16 @@ def unquote_context(self): finally: self.context.pop() - def _extras(self, p, v): - extras = [] + def _pull(self, v, p): depth = len(self.depth) nondrop = self._filter_drop() try: - while isinstance(form := next(nondrop), Extra): - extras.extend(form) + return next(nondrop) except StopIteration: e = SoftSyntaxError if len(self.depth) == depth else SyntaxError raise e(f"Reader macro {v!r} missing argument.", self.position(p)) from None - return form, extras - def parse_macro(self, tag: str, form, extras): + def parse_macro(self, tag: str, form): # fmt: off R"""Apply a reader macro to a form. @@ -337,8 +338,6 @@ def parse_macro(self, tag: str, form, extras): * - ``'`` - `quote` - * - ``!`` - - `Extra` * - :literal:`\`` (backtick) - template quote (starts a `template`) * - ``_#`` @@ -361,19 +360,14 @@ def parse_macro(self, tag: str, form, extras): The built-in macros are reserved by the reader and cannot be reassigned. """ - def case(s): - if (b := tag == s) and extras: - raise SyntaxError(f"Extra for {s!r} reader macro.") - return b - if case("'"): return "quote", form - if tag == "!": return Extra([*extras, form]) - if case("`"): return self.template(form) - if case(","): return _Unquote(":?", form) - if case(",@"): return _Unquote(":*", form) - if case("_#"): return DROP - if case("$#"): return self.gensym(form) - if case(".#"): return eval(readerless(form, self.ns), self.ns) - return self._custom_macro(form, tag, extras) + if tag == "'": return "quote", form + if tag == "`": return self.template(form) + if tag == ",": return _Unquote(":?", form) + if tag == ",@": return _Unquote(":*", form) + if tag == "_#": return DROP + if tag == "$#": return self.gensym(form) + if tag == ".#": return eval(readerless(form, self.ns), self.ns) + return self._custom_macro(form, tag) # fmt: on def template(self, form): @@ -440,14 +434,40 @@ def _get_counter(self) -> int: return self.counters[-1] return self.counters[index] - def _custom_macro(self, form, tag: str, extras): + def _custom_macro(self, form, tag: str): assert tag.endswith("#") - tag = force_munge(self.escape(tag[:-1])) - tag = re.sub(r"(^\.)", lambda m: force_qz_encode(m[1]), tag) - fn: Fn[[str], Fn] = self._fully_qualified if ".." in tag else self._local + if re.fullmatch(r"(?:[^\\]|\\.)+=#", tag): + return Kwarg(tag[:-2], form) + arity = tag.replace(R"\#", "").count("#") + assert arity > 0 + 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 + p = self._pos + args = [] + kwargs = {} + depth = len(self.depth) with self.compiler.macro_context(): - args, kwargs = parse_extras(extras) - return fn(tag)(form, *args, **kwargs) + 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(x) + if i == arity: + break + else: + e = SoftSyntaxError if len(self.depth) == depth else SyntaxError + raise e( + f"Reader tag {tag!r} missing argument.", self.position(p) + ) from None + return fn(label)(*args, **kwargs) @staticmethod def _fully_qualified(tag: str) -> Fn: @@ -546,21 +566,6 @@ def is_string_literal(form) -> Optional[bool]: return type(ast.literal_eval(form)) is str -def parse_extras(extras): - it = iter(extras) - args = [*takewhile(lambda x: x != ":", it)] - kwargs = {} - for k in it: - # fmt: off - v = next(it) - if k == ":?": args.append(v) - elif k == ":*": args.extend(v) - elif k == ":**": kwargs.update(v) - else: kwargs[k] = v - # fmt: on - return args, kwargs - - def is_qualifiable(symbol): """Determines if symbol can be qualified with a module. diff --git a/tests/test_reader.py b/tests/test_reader.py index 3562058e4..dc7ffb6fe 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -156,28 +156,10 @@ def test_bad_macro(self): with self.assertRaises(SyntaxError): next(self.parser.reads("foo#bar")) - def test_bad_extra(self): - for m in {"'", "`", "_#", ".#"}: - with self.subTest(macro=m), self.assertRaises(SyntaxError): - next(self.parser.reads(f"{m}!x()")) - - def test_bad_template_extra(self): - for m in {",", ",@", "$#"}: - with self.subTest(macro=m), self.assertRaises(SyntaxError): - next(self.parser.reads(f"`({m}!x y)")) - def test_reader_missing(self): with self.assertRaises(SyntaxError): next(self.parser.reads("(x#)")) - def test_reader_no_primary(self): - with self.assertRaises(SyntaxError): - next(self.parser.reads("(x# !2)")) - - def test_reader_soft_no_primary(self): - with self.assertRaises(SyntaxError): - next(self.parser.reads("(x# !2")) - def test_reader_initial_dot(self): msg = r"Unknown reader macro 'QzFULLxSTOP_foo'." with self.assertRaisesRegex(SyntaxError, msg):