From e16f6f5562f4e87b4db052735eb8b187c88cd567 Mon Sep 17 00:00:00 2001 From: gilch Date: Wed, 18 Sep 2024 22:23:02 -0600 Subject: [PATCH] Update API docs for glossary (Includes some renames and type annotations.) --- docs/command_line_reference.rst | 6 +- docs/glossary.rst | 126 ++++++++++++++++++++------------ src/hissp/__init__.py | 14 ++-- src/hissp/compiler.py | 86 +++++++++++++--------- src/hissp/macros.lissp | 117 +++++++++++++++-------------- src/hissp/munger.py | 6 +- src/hissp/reader.py | 66 +++++++++-------- src/hissp/repl.py | 4 +- 8 files changed, 243 insertions(+), 182 deletions(-) diff --git a/docs/command_line_reference.rst b/docs/command_line_reference.rst index e2abf6dff..865662da2 100644 --- a/docs/command_line_reference.rst +++ b/docs/command_line_reference.rst @@ -1,4 +1,4 @@ -.. Copyright 2020, 2021, 2022, 2023 Matthew Egan Odendahl +.. Copyright 2020, 2021, 2022, 2023, 2024 Matthew Egan Odendahl SPDX-License-Identifier: CC-BY-SA-4.0 Command Line Reference @@ -45,7 +45,7 @@ For example, using `hissp.reader.transpile`, a package name, and module names, .. code-block:: console - $ alias lisspt="lissp -c '(hissp..transpile : :* ([#1:] sys..argv))'" + $ alias lisspt="lissp -c '(hissp..transpile : :* [##1:] sys..argv)'" $ lisspt pkg foo # Transpiles pkg/foo.lissp to pkg/foo.py in a package context. $ lisspt pkg.sub foo # Transpiles pkg/sub/foo.lissp to .py in subpackage context. $ lisspt "" foo bar # foo.lissp, bar.lissp to foo.py, bar.py without a package. @@ -54,7 +54,7 @@ or using `hissp.reader.transpile_file`, a file name, and a package name, .. code-block:: console - $ alias lissptf="lissp -c '(hissp.reader..transpile_file : :* ([#1:] sys..argv))'" + $ alias lissptf="lissp -c '(hissp.reader..transpile_file : :* [##1:] sys..argv)'" $ lissptf spam.lissp # Transpile a single file without a package. $ cd pkg $ lissptf eggs.lissp pkg # must declare the package name diff --git a/docs/glossary.rst b/docs/glossary.rst index 8f3312a16..d02be6d26 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -13,15 +13,16 @@ Glossary or the location of such a form. "Top" here means the top of the syntax tree rather than the top of the file. Each top-level form is a compilation unit. - A macro defined in a top-level form cannot affect other - `subform`\ s in the same top-level form, + A macro defined in a top-level form cannot affect any other + `subform` in the same top-level form, because by the time it exists, the top-level form has already been compiled. - `Injection`\ s of Python statements are only valid at the top level. + `Injection` of a Python statement is only valid at the top level. doorstop A `discarded item` used to "hold open" a bracket trail. - Typically ``_#/`` in Lissp. + Typically ``_#/`` in Lissp, + but may be a `Unicode token` with a comment. abstract syntax tree ast @@ -50,6 +51,8 @@ Glossary and passes it to Python's shell for evaluation and printing, then repeats. Used for inspection, online help, interactive development, and debugging. + A subREPL is just a REPL started from within a REPL, + analogous to a subshell. token A lexical unit of Lissp. @@ -61,21 +64,25 @@ Glossary A type of `token` which by itself results in a `parsed object` when passed to the Lissp parser. These are what the reader considers nouns. - Typically emitted as `atom`\ s, + Typically emitted as an `atom`, but may instead be arguments to a `tagging token`. parsed object The result of parsing an `object token`; - or a `tagging token` with its arguments; + or a `tagging token` with its arguments (except the `discard tag`); or a balanced pair of parentheses (open ``(`` and close ``)`` tokens), with their contents (if any). - Typically emitted as `form`\ s, + Typically emitted as a `form`, but may instead be arguments to a `tagging token`. tagging token A type of `token` that, when read, consumes one or more `parsed object`\ s and results in a `parsed object`. These can be thought of as reader adjectives. + They serve a function similar to reader macros in other Lisps, + but operate on the `parsed object` stream (parser level), + not the raw character stream (lexer level). + Think Clojure/EDN tags, not Common Lisp reader macros. special tag One of the built-in unary `tagging token`\ s @@ -87,15 +94,15 @@ Glossary each starting with a semicolon (``;``) character. Comment tokens read as `hissp.reader.Comment` instances, a type that is normally discarded by the reader. - However, they can still be arguments for `tagging token`\ s + However, they can still be arguments for a `tagging token`. unicode token An `object token` that begins and ends with a quotation mark (``"``) character. They may contain newline characters like Python's triple-quoted string literals, as is typical of Lisps. Internal quotation marks must be escaped with a preceding reverse solidus - (``\``) character. These read as `str atom`\ s containing - a Python string literal wrapped in parentheses. + (``\``) character. It reads as a `string literal fragment`, specifically, + a `str atom` containing a Python string literal wrapped in parentheses. str atom An `atom` of type `str`. Usually represents a `Python fragment`. @@ -106,7 +113,7 @@ Glossary A `Python fragment` which `ast.literal_eval` would evaluate to an object of type `str`. Not all `str atom`\ s are string literal fragments; - It must contain a Python string literal expression. + it must contain a Python string literal expression. `hissp.reader.is_string_literal` tests for string literal fragments. hissp string @@ -136,7 +143,7 @@ Glossary These are tuples beginning with either a ``quote`` or ``lambda`` `str atom`. They look like function calls but act more like macros, in that arguments are not all evaluated first. - While `control word`\ s are `form`\ s + While a `control word` is a `form` and can have special interpretations in certain contexts, they are not considered special forms. `module handle`\ s also have a processing rule in the compiler, @@ -155,10 +162,10 @@ Glossary Either a `Python injection` or a `Hissp injection`, depending on context. python injection - The technique of writing `Python fragment`\ s + The technique of writing a `Python fragment` rather than allowing the Hissp machinery to do it for you, - or the fragments so used or the `fragment atom`\ s containing them. - `text macro`\ s work via Python injection. + or the fragments so used or the `fragment atom` containing one. + A `text macro` works via Python injection. Injection is discouraged because it bypasses a lot of Hissp's machinery, and is opaque to code-walking macros, making them less useful or risking errors. @@ -166,8 +173,8 @@ Glossary Injection transcends that limitation. Injection of identifiers is considered standard in Hissp, so is not discourarged. - Lissp's `Unicode token`\ s read as `string literal fragment`\ s, - rather than as `quote`\ d `str atom`\ s, + A Lissp `Unicode token` reads as a `string literal fragment`, + rather than as a `quote`\ d `str atom`, making them an example of injection as well. This usage is standard in Lissp. @@ -175,15 +182,16 @@ Glossary Any `atom` of non-standard type (or the use thereof), i.e., anything the compiler doesn't have a literal notation for, which it would have to attempt to emit as a `pickle expression`. - This includes instances of standard types without a literal notation, - e.g., `math.nan` or collections containing nonstandard elements or cycles. + This includes instances of standard types without a literal notation + (e.g., `float` is a standard type, but `math.nan` has no literal) + or collections containing nonstandard elements or cycles. A macroexpansion may be an injection. Besides macroexpansions, in readerless mode, this almost always requires the use of non-literal notation, (i.e., notation not accepted by `ast.literal_eval`). In Lissp, this almost always requires the use of a `tagging token`. - (a notable exception is a float literal big enough in magnitude to overflow to ``inf`` or ``-inf``, - e.g., ``1e999``. + (A notable exception is a float literal big enough in magnitude to overflow to + ``inf`` or ``-inf``, e.g., ``1e999``. The compiler still considers this nonstandard because that's not its `repr`, and would emit a `pickle expression` for it.) Basic container types containing only standard elements do not count as injections, @@ -205,49 +213,53 @@ Glossary A `str atom` that is not a `control word`, especially if it does not simply contain an identifier or literal. So called because the compiler's usual interpretation is to emit the contents directly, - although there is a preprocessing step for `module handle`\ s. + although there is a preprocessing step for imports (see `module handle`). fragment token An `object token` that begins and ends with a vertical line (``|``) character. Internal vertical lines must be escaped as two vertical lines (``||``). - These read directly as `str atom`\ s, - which typically become a `fragment atom`, hence the name. + It reads directly as a `str atom`, + which typically becomes a `fragment atom`, hence the name. In the case that the fragment token begins with ``|:``, it becomes a `control word` instead. control token An `object token` that begins with a colon ``:`` character. - These read as `control word`\ s. + It reads as a `control word`, a type of `str atom`. control word A `str atom` that begins with a colon ``:`` character. These normally compile directly to Python string literals with the same contents (including the leading colon), but may have special interpretation in some contexts. + (Both Python and other Lisps use the term "`keyword`", + but they mean `different things`, + including Lisp's equivalent concept.) bare token An `object token` without the initial character marking it as a `comment token` (``;``), `Unicode token` (``"``), `fragment token` (``|``), or `control token` (``:``). - These are either `literal token`\ s, or failing that, `symbol token`\ s. + It is either a `literal token`, or failing that, a `symbol token`. literal token A `bare token` that is a valid Python literal, as determined by `ast.literal_eval`, but not of a container type. - These read as `atom`\ s of that type. + It reads as an `atom` of that type. symbol token A `bare token` that is not a `literal token`. - These are subject to `munging` and read as `symbol`\ s. + These are subject to `munging` and read as a `symbol`, + a type of `str atom`. symbol A `module handle` or a `Python fragment` containing an identifier. (Possibly with `qualification`.) - Symbols are always `str atom`\ s. + A symbols is always a `str atom`. munging The process of replacing characters invalid in a Python identifier - with "Quotez" equivalents. + with `Quotez` equivalents. Primarily used to make a `symbol token` into a `str atom` containing a valid Python identifier (a `symbol`). The munging machinery is in :mod:`hissp.munger`. @@ -256,16 +268,20 @@ Glossary The `munger`'s character replacement format. It's the character's Unicode name wrapped in ``Qz`` and ``_``. (Spaces become ``x`` and hyphens become ``h``.) - Characters without names use their base 10 ordinals instead. + Characters without names use their hexadecimal ordinals instead. Some ASCII characters use the short names from `TO_NAME` instead. + The `gensym` hashes and ``hissp.compiler.MAYBE`` use the same + ``Qz{}_`` wrapper. kwarg token A single-argument `tagging token` ending in an equals sign (``=``) and read as a `hissp.reader.Kwarg` instance. + Used as keyword arguments for a `tag`. stararg token - One of ``*=`` or ``**=``. A `special tag` which read as a - `hissp.reader.Kwarg` instance. + One of ``*=`` or ``**=``. A `special tag` which reads as a + `hissp.reader.Kwarg` instance. Used as unpacking positional + or keyword arguments (respectively) to a `tag`. tag tag token @@ -306,7 +322,7 @@ Glossary template A `template quote` and its argument, - a domain-specific language (DSL) for creating `form`\ s, + a domain-specific language (DSL) for creating a `form`, supporting tuple interpolation, `gensym`\ s, and automatic `full qualification`. Can also be used for data, not just code. @@ -316,7 +332,7 @@ Glossary qualification partial qualification partially qualified identifier - A `str atom` containing a dot-separated identifier path + A `str atom` containing a ``.``-separated identifier path prepended to an identifier is a qualified identifier. Compiles to Python attribute access syntax. If this is the path from the containing module, the result is a `qualified name`. @@ -324,13 +340,22 @@ Glossary if qualification is not full, it's partial. A `qualified name` is partial qualification, but partial qualification is not necessarily a `qualified name`, - since the path may start from some namespace other than the module globals. + because the path may start from some namespace other than the module globals. The qualifier part is everything but the last segment. Qualification is the process of adding a qualifier or the state of having a qualifier. + `hissp.macros._macro_.alias` produces a `tag` to abbreviate a qualifier. + + unqualified + An identifier without a `qualifier`. An unqualified `tag` is only + valid if it's available as an attribute in the current module's + ``_macro_`` namespace, and similarly for an unqualified `macro`. + A unqualified variable is valid if it's in the `builtins` module, + a global of the current module, + or a variable in the current lexical scope. module handle - A `str atom` containing a dot-separated path ending in a dot, + A `str atom` containing a ``.``-separated path ending in a ``.``, representing an import path for a module. Any segments before the module name are package names. E.g., ``foo.bar.baz.`` or ``foo.``. @@ -339,7 +364,7 @@ Glossary full qualifier full qualification fully-qualified identifier - A `module handle` prepended to a `qualified name` and separated with a dot + A `module handle` prepended to a `qualified name` and separated with a ``.`` is a fully-qualified identifier; it's the path of attribute access from the full import path of the module, which is enough to get a reference to the object from anywhere. @@ -354,13 +379,19 @@ Glossary ``,``. A `special tag` only valid in a `template`. Its argument is directly interpolated rather than quoted first. + splice + splicing unquote + ``,@``. A `special tag` only valid in a `template`. + Its argument is interpolated and unpacked rather than quoted first. + quote hard quote ``'``. A `special tag` abbreviating the ``quote`` `special form`. Sometimes called a "hard quote" to distinguish it from the `template quote`. inject tag - ``.#``. A `special tag` which evaluates the next `parsed object`. + ``.#``. A `special tag` which evaluates the next + `parsed object` and returns its result. So named because it's typically used to make an `injection`, although it can result in an object of any type. @@ -368,7 +399,7 @@ Glossary discarded item ``_#``. A `special tag` used to structurally disable parts of code during development, - for commentary, or as a `doorstop`, + for commentary, or as a `doorstop`. The argument to a discard tag is the discarded item. It is unique among `tagging token`\ s in that it doesn't return a `parsed object` at all. @@ -384,12 +415,15 @@ Glossary gensym tag ``$#``. A `special tag` only valid in a `template` for creating a `gensym`. - Prepends a gensym hash to its argument, or replaces ``$`` characters with it. - A gensym hash is unique to the template it was created in. - This prevents accidental name collisions in `macro expansion`\ s. + Prepends a `gensym hash` to its argument, or replaces ``$`` characters with it. gensym + gensym hash A generated `symbol`. These are produced by the `gensym tag`. + A gensym hash is unique to the template it was created in. + This prevents accidental name collisions in `macro expansion`\ s. + A gensym hash is mostly used for local variables because + they can't be disambiguated with a `full qualifier`. macro expansion expansion @@ -418,12 +452,12 @@ Glossary read time The phase before compilation proper that translates Lissp to Hissp: - when the reader runs and when `tagging token`\ s are activated. + when the `reader` runs and when `tagging token`\ s are activated. text macro - A `macro` that `expands ` to a `str atom`. + A `macro` that `expands ` to a `str atom` instead of some other `form`, - especially the `str atom` doesn't simply contain a string literal + especially if the `str atom` doesn't simply contain a string literal or (possibly qualified) identifier. Effectively, they return Python code, rather than Hissp, diff --git a/src/hissp/__init__.py b/src/hissp/__init__.py index 675ebc729..25a2a5c71 100644 --- a/src/hissp/__init__.py +++ b/src/hissp/__init__.py @@ -39,7 +39,7 @@ * `hissp.repl.interact`, as well as the `hissp.macros._macro_` namespace, making all the bundled -macros available with the shorter ``hissp.._macro_`` qualifier. +`macros` available with the shorter ``hissp.._macro_`` `qualifier`. """ from hissp.compiler import Compiler, readerless from hissp.munger import demunge, munge @@ -54,7 +54,7 @@ def prelude(env): - """Lissp prelude shorthand tag. + """Lissp prelude shorthand `tag`. Usage: ``hissp..prelude#env``, which expands to @@ -70,7 +70,7 @@ def prelude(env): def alias(abbreviation, qualifier="hissp.macros.._macro_"): - """Lissp alias shorthand tag. + """Lissp alias shorthand `tag`. Usage: ``hissp..alias## abbreviation qualifier``, which expands to @@ -80,7 +80,7 @@ def alias(abbreviation, qualifier="hissp.macros.._macro_"): (hissp.macros.._macro_.alias abbreviation qualifier) The single-argument form - ``hissp..alias#abbreviation`` aliases the bundled macro qualifier. + ``hissp..alias#abbreviation`` aliases the bundled macro `qualifier`. See `hissp.macros._macro_.alias`. """ @@ -88,7 +88,7 @@ def alias(abbreviation, qualifier="hissp.macros.._macro_"): def refresh(module): - """REPL convenience tag to recompile and reload a module. + """`REPL` convenience `tag` to recompile and reload a module. Usage: ``hissp..refresh#foo.`` where ``foo.`` evaluates to a module. @@ -118,14 +118,14 @@ def refresh(module): def subrepl(module): - """Convenience tag to start a Lissp subREPL in the given module. + """Convenience `tag` to start a Lissp subREPL in the given module. Usage: ``hissp..subrepl#foo.`` where ``foo.`` evaluates to a module. Won't re-enter current module. Prints `__name__` on subREPL exit. (Exit a subREPL using EOF.) - See also: `refresh`, `hissp.repl.interact`. + See also: `refresh`, `hissp.repl.interact`, :term:`repl`. """ return ( (('lambda',(':','module',module,) diff --git a/src/hissp/compiler.py b/src/hissp/compiler.py index a088131e8..cae9ee2a8 100644 --- a/src/hissp/compiler.py +++ b/src/hissp/compiler.py @@ -18,7 +18,17 @@ from pprint import pformat from traceback import format_exc from types import MappingProxyType, ModuleType -from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple, TypeVar, Union +from typing import ( + Any, + Dict, + Iterable, + List, + NewType, + Optional, + Tuple, + TypeVar, + Union, +) from warnings import warn PAIR_WORDS = {":*": "*", ":**": "**", ":?": ""} @@ -38,16 +48,21 @@ instead of its defining environment. Rather than pass in an implicit argument to all macros, it's available here. -`readerless` uses this automatically. +`readerless` and `macroexpand` use this automatically. """ MAX_PROTOCOL = pickle.HIGHEST_PROTOCOL """ +Compiler pickle protocol limit. + When there is no known literal syntax for an `atom`, the compiler emits a `pickle.loads` expression as a fallback. This is the highest pickle protocol it's allowed to use. The compiler may use Protocol 0 instead when it has a shorter `repr`, due to the inefficient escapes required for non-printing bytes. + +A lower number may be necessary if the compiled output is expected to +run on an earlier Python version than the compiler. """ @@ -187,7 +202,7 @@ def tuple(self, form: Tuple) -> str: @_trace def special(self, form: Tuple) -> str: - """Try to compile as special form, else :meth:`invocation`. + """Try to compile as a `special form`, else :meth:`invocation`. The two special forms are ``quote`` and `lambda `. @@ -245,8 +260,9 @@ def lambda_(self, form: Tuple) -> str: The special `control word`\ s ``:*`` and ``:**`` designate the remainder of the positional and keyword parameters, respectively. + Note this body evaluates expressions in sequence, for side - effects. + effects: >>> print(readerless( ... ('lambda', (':',':*','args',':**','kwargs',) @@ -356,9 +372,9 @@ def expand_macro(self, form: Tuple) -> Union[str, Sentinel]: @classmethod def get_macro(cls, symbol, env: Dict[str, Any]): - """Returns the macro function for a symbol given the namespace. + """Returns the macro function for ``symbol`` given the ``env``. - Returns ``None`` if symbol isn't a macro. + Returns ``None`` if ``symbol`` isn't a macro identifier. """ if type(symbol) is not str or symbol.startswith(":"): return None @@ -396,10 +412,14 @@ def call(self, form: Iterable) -> str: Compile call form. Any tuple that is not quoted, ``()``, or a `special` form or - `macro` is a run-time call. + `macro` is a run-time call form. + It has three parts: + + ( : ). + + Each argument pairs with a keyword or `control word` target. + The ``:?`` target passes positionally (implied for singles). - Like Python, it has three parts: - ( : ). For example: >>> print(readerless( @@ -413,7 +433,7 @@ def call(self, form: Iterable) -> str: sep=':', end='\n\n') - Either or may be empty: + Either or may be empty: >>> readerless(('foo',':',),) 'foo()' @@ -424,7 +444,7 @@ def call(self, form: Iterable) -> str: foo( bar=baz) - The ``:`` is optional if the part is empty: + The ``:`` is optional if the part is empty: >>> readerless(('foo',),) 'foo()' @@ -432,10 +452,8 @@ def call(self, form: Iterable) -> str: foo( bar) - The part has implicit pairs; there must be an even number. - - Use the control words ``:*`` and ``:**`` for iterable and - mapping unpacking: + Use the ``:*`` and ``:**`` targets for position and + keyword unpacking, respectively: >>> print(readerless( ... ('print',':',':*',[1,2], 'a',3, ':*',[4], ':**',{'sep':':','end':'\n\n'},), @@ -446,11 +464,10 @@ def call(self, form: Iterable) -> str: *[4], **{'sep': ':', 'end': '\n\n'}) - Unlike other control words, these can be repeated, - but (as in Python) a ``*`` is not allowed to follow ``**``. - Method calls are similar to function calls: + (. : ). + A method on the first (self) argument is assumed if the function name starts with a dot: @@ -528,7 +545,7 @@ def atomic(self, form) -> str: R""" Compile forms that evaluate to themselves. - Emits a literal if possible, otherwise falls back to `pickle`: + Returns a literal if possible, otherwise falls back to `pickle`: >>> readerless(-4.2j) '((-0-4.2j))' @@ -595,31 +612,32 @@ def pickle(self, form) -> str: return f"# {r}\n__import__({pickle.__name__!r}).loads({code})" @staticmethod - def linenos(form): - lines = form.split("\n") + def linenos(code: str): + """Adds line numbers to code for error messages.""" + lines = code.split("\n") digits = len(str(len(lines))) return "\n".join(f"{i:0{digits}} {line}" for i, line in enumerate(lines, 1)) - def eval(self, form: str, form_number: int) -> Tuple[str, ...]: - """Execute compiled form, but only if evaluate mode is enabled.""" + def eval(self, code: str, form_number: int) -> Tuple[str, ...]: + """Execute compiled code, but only if evaluate mode is enabled.""" try: if self.evaluate: filename = ( f"" ) - exec(compile(form, filename, "exec"), self.env) + exec(compile(code, filename, "exec"), self.env) except Exception as e: exc = format_exc() if self.env.get("__name__") == "__main__": self.abort = exc else: warn( - f"\n {e} when evaluating form:\n{form}\n\n{exc}", PostCompileWarning + f"\n {e} when evaluating form:\n{code}\n\n{exc}", PostCompileWarning ) - return form, "# " + exc.replace("\n", "\n# ") - return (form,) + return code, "# " + exc.replace("\n", "\n# ") + return (code,) def _join_args(*args): @@ -681,16 +699,12 @@ def macroexpand(form, env: Optional[Dict[str, Any]] = None): form = expanded -def _identity(x): - return x - - def macroexpand_all( form, env: Optional[Dict[str, Any]] = None, *, - preprocess=_identity, - postprocess=_identity, + preprocess=lambda x: x, + postprocess=lambda x: x, ): """Recursively macroexpand everything possible from the outside-in. @@ -715,7 +729,7 @@ def macroexpand_all( return "lambda", _pexpand(exp[1], env), *(macroexpand_all(e, env) for e in exp[2:]) -def _pexpand(params, env: Optional[Dict[str, Any]]): +def _pexpand(params, env: Optional[Dict[str, Any]]) -> tuple: if ":" not in params: return params singles, pairs = parse_params(params) @@ -726,7 +740,7 @@ def _pexpand(params, env: Optional[Dict[str, Any]]): return *singles, ":", *chain.from_iterable(pairs.items()) -def parse_params(params): +def parse_params(params) -> Tuple[tuple, Dict[str, Any]]: """Parses a lambda form's `params` into a tuple of singles and a dict of pairs.""" iparams = iter(params) singles = tuple(takewhile(lambda x: x != ":", iparams)) diff --git a/src/hissp/macros.lissp b/src/hissp/macros.lissp index b71080d5b..664eda9e4 100644 --- a/src/hissp/macros.lissp +++ b/src/hissp/macros.lissp @@ -3,17 +3,18 @@ "Hissp's bundled macros. -To help keep macro definitions and expansions manageable in complexity, -these macros lack some of the extra features their equivalents have in -Python or in other Lisps. These are not intended to be a standard -library for general use, but do bring Hissp up to a basic standard of -utility without adding dependencies, which may suffice in some cases. +To help keep macro definitions and `expansion`\\ s manageable in +complexity, these macros lack some of the extra features their +equivalents have in Python or in other Lisps. These are not intended to +be a standard library for general use, but do bring Hissp up to a basic +standard of utility without adding dependencies, which may suffice in +some cases. Because of deliberate design restrictions, there are no dependencies in -their expansions either, meaning compiled Hissp code utilizing the -bundled macros need not have Hissp installed to work, only the Python -standard library. All helper code must therefore be inlined, resulting -in larger expansions than might otherwise be necessary. +their expansions either, meaning the compiled output of the bundled +macros need not have Hissp installed to work, only the Python standard +library. All helper code must therefore be inlined, resulting in larger +expansions than might otherwise be necessary. They also have no prerequisite initialization, beyond what is available in a standard Python module. For example, a ``_macro_`` namespace need @@ -22,21 +23,23 @@ presence of ``_macro_`` (at compile time) in its expansion context, and inline the initialization code when required. As a convenience, the bundled macros are automatically made available -unqualified in the Lissp `REPL`, but this does not apply to modules. A +`unqualified` in the Lissp `REPL`, but this does not apply to modules. A Hissp module with better alternatives need not use the bundled macros at all. -They also eschew any expansions of subexpressions to non-atomic Python -code strings, relying only on the built-in special forms ``quote`` and -``lambda``, which makes their expansions compatible with advanced -rewriting macros that process the Hissp expansions of other macros. (The --``[#`` tags are something of an exception, as one subexpression is -written in Python to begin with.) Note that this need not apply to -inlined helper functions that contain no user code, because this is no -worse than using an opaque name imported from somewhere else. `if-else -` is one such example, expanding to a lambda containing a -Python `conditional expression`, immediately called with the -subexpressions as arguments. +They also eschew expansions of any subexpression to a `Python injection` +(except the standard `symbol` or `string literal fragment` cases), +relying only on the built-in special forms ``quote`` and ``lambda``, +which makes their expansions compatible with advanced rewriting macros +that process the Hissp expansions of other macros. (The -``[#`` tags are +something of an exception, as one subexpression is written in Python to +begin with.) + +This doesn't apply to inlined helper functions that contain no user +code, because that's no worse than using an opaque name imported from +somewhere else. `if-else ` is one such example, expanding to +a lambda containing an injected `conditional expression `, +immediately called with the subexpressions as arguments. " ;;; This module is not necessarily a good example of how you should @@ -321,15 +324,15 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. `(progn ,@body))) (defmacro fun (qualname params : maybe-docstring () :* body) - "A lambda enhanced with a qualname and optionally a docstring. + "A lambda enhanced with a `qualified name` and optionally a docstring. Hissp's (and Python's) lambda syntax do not have docstrings. Named lambdas improve `REPL` transparency and error messages, at the cost of some configuration overhead to set the name in the three places Python requires. - Used by `defun`. Not recommended for otherwise anonymous functions due - to the additional overhead. + Used by `defmacro` and `defun`. Not recommended for otherwise + anonymous functions due to the additional overhead. " (let*from ((name : :* _xs) (reversed (.split qualname '. 1)) (doc top) (if-else (hissp.reader..is_hissp_string maybe-docstring) @@ -392,7 +395,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; Define the real defmacro using the bootstrap macros. (defmacro defmacro (name parameters : :* body) - "Creates a new macro for the current module. + "Creates a new `macro function` for the current module. If there's no local ``_macro_`` namespace (at compile time), adds code to create one using `types.SimpleNamespace` (at runtime), if it's @@ -469,9 +472,9 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro define (qualname value) "Assigns an attribute the value. - The namespace part of the qualname may be fully qualified or start - from a name in scope. An empty namespace part assigns an attribute of - the current module. + The qualname may be a `fully-qualified identifier` or start from a + name in scope. Assigns an attribute of the current module (a global) + if there's no `qualifier`. .. code-block:: REPL @@ -497,7 +500,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro defun (qualname params : :* body) "`define` a `fun` with the same name. - See `deftypeonce` for defining methods with defun. + See `deftypeonce` for how to define methods. See also: `def`, `define`, `defmacro`, `:@## `. " @@ -556,6 +559,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. >>> CACHE namespace(x=42) + See also: `deftypeonce`, `hissp.refresh`. + " `(unless ,(let-from (ns _dot attr) (.rpartition qualname ".") (if-else ns @@ -563,23 +568,23 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. `(operator..contains (globals) ',attr))) (define ,qualname ,value))) -(defmacro deftypeonce (qualname : bases () :* once-decorators) +(defmacro deftypeonce (qualname : bases () :* once_decorators) 'hissp.reader..Comment.contents# - ;; Defines a type (class), unless it exists. + ;; Defines a `type` (`class`), unless it exists. ;; ;; Add class attributes afterward using `define` or `defun`, and class ;; decorators above with `:@## ` or afterward ;; using `zap@ `. These run again on module reload and patch ;; in existing instances. ;; - ;; The ``once-decorators`` apply before any external ones, in the + ;; The ``once_decorators`` apply before any external ones, in the ;; order written (first applies first), unless the type exists (not ;; reapplied on reloads). Beware that type attributes defined - ;; afterward will not be available for the ``once-decorators`` to + ;; afterward will not be available for the ``once_decorators`` to ;; operate upon. A decorator can add attributes for subsequent ;; decorators to operate upon, however, and a decorator may be a ;; lambda defined in line. It is possible to add arbitrary attributes - ;; this way, but remember that ``once-decorators`` don't run again on + ;; this way, but remember that ``once_decorators`` don't run again on ;; reloads, so changes here cannot simply be reloaded with the module ;; the way attributes defined afterward can. ;; @@ -703,7 +708,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... (2)) ;; Point2D(1, 2) ;; - ;; Also supports kwds in the bases tuple for + ;; Also supports keyword arguments in the bases tuple for ;; `object.__init_subclass__`. Separate with a ``:``. ;; ;; .. code-block:: REPL @@ -773,14 +778,14 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... )) ;; {'a': 1, 'b': 2} ;; - ;; See also: `attach`, `type`, :keyword:`class`, `types.new_class`. + ;; See also: `attach`, `types.new_class`, `defonce`. ;; (let (ibases (iter bases) name |qualname.rpartition('.')[-1]|) `(defonce ,qualname ,(functools..reduce (lambda (cls f) `(,f ,cls)) - once-decorators + once_decorators `(type ',name (|| ,@(itertools..takewhile (lambda x (operator..ne x ':)) ibases) ||) @@ -1160,7 +1165,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. `(lambda ,'XYZW ,expr)) (defmacro alias (abbreviation qualifier) - <#;Defines a tag abbreviation of a qualifier. + <#;Defines a `tag` abbreviation of a `qualifier`. ;; ;; .. code-block:: REPL ;; @@ -1237,7 +1242,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; The bundled `op# ` and `i# ` tags are aliases ;; for `operator` and `itertools`, respectively. ;; - ;; See also: `prelude`, `attach`. + ;; See also: `prelude`, `attach`, `hissp.alias`. + ;; ;; `(defmacro ,(.format "{}{}" abbreviation '#) ($#attr : :* $#args :** $#kwargs) @@ -1507,6 +1513,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro set\[\# (lookup items value) <#;``set[###`` 'setsub' Injection. Subscription with assignment. ;; + ;; Returns the value assigned (not the collection updated). + ;; ;; .. code-block:: REPL ;; ;; #> (define spam (list "0000")) @@ -1515,7 +1523,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... spam=list( ;; ... ('0000'))) ;; - ;; #> set[###-1] spam set[###::2] spam "ab" + ;; #> set[###-1] spam set[###::2] spam "ab" ; Chained assignment. ;; >>> ( ;; ... lambda _Qzuwg7puku__items, ;; ... _Qzuwg7puku__value=( @@ -1640,9 +1648,9 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro set@ (qualname value) <#;``set@`` 'setat' Assigns an attribute, returns the value. ;; - ;; The namespace part of the qualname may be fully qualified or start + ;; The namespace part of the ``qualname`` may be fully qualified or start ;; from a name in scope. An empty namespace part sets an attribute of the - ;; current module. + ;; current module (a global). ;; ;; Mnemonic: set @tribute. ;; @@ -1809,7 +1817,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro doto (self : :* invocations) <#;Configure an object. ;; - ;; Calls multiple 'methods' on one 'self'. + ;; Calls multiple "methods" on one "self". ;; ;; Evaluates the given ``self``, then injects it as the first argument to ;; a sequence of invocations. Returns ``self``. @@ -1880,7 +1888,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ([{'a'}, 'bc'], 'de') ;; ;; See also: `-\<>> `, `X# `, - ;; `\!# `. + ;; `\!# `, `en# `. ;; (functools..reduce XY# |(Y[0],X,*Y[1:],)| (map X# |X if type(X) is tuple else (X,)| forms) @@ -1927,7 +1935,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... ':<>') ;; ('de', ['bc', {'a'}], 'fg', ':<>') ;; - ;; See also: `-> `. + ;; See also: `-> `, `en# `. ;; (functools..reduce XY#(let (i (iter Y)) `(,@(i#takewhile X#(op#ne X :<>) i) ,X ,@i)) @@ -1989,8 +1997,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro cond (: :* pairs) <#;Multiple condition branching. ;; - ;; Pairs are implied by position. Default is ``()``, use something always - ;; truthy to change it, like ``:else`` or `True`. For example, + ;; Pairs are implied by position. Default is ``()``. Use something always + ;; truthy to change it, like ``:else`` or `True`. For example: ;; ;; .. code-block:: REPL ;; @@ -2319,9 +2327,10 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. (defmacro throw* (: :* exception) <#;``throw*`` 'throw star' Creates a closed generator and calls ``.throw``. ;; - ;; Despite PEP 3109, .throw still seems to accept multiple arguments. - ;; Avoid using this form except when implementing throw method overrides. - ;; Prefer `throw` instead. + ;; Despite `PEP 3109 `_, ``.throw`` + ;; still seems to accept multiple arguments. Avoid using this form + ;; except when implementing throw method overrides. Prefer `throw` + ;; instead. ;; ;; The 3-arg form is deprecated as of Python 3.12. ;; @@ -2761,10 +2770,10 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; dependencies in the compiled output. ;; ;; Mainly intended for single-file scripts that can't have dependencies, - ;; or similarly constrained environments (e.g. embedded, readerless). - ;; There, the first form should be ``(hissp.._macro_.prelude)``, - ;; which is also implied in ``$ lissp -c`` commands. - ;; (See the `hissp.prelude` shorthand for Lissp.) + ;; or similarly constrained environments (e.g., embedded, + ;; `readerless mode`). There, the first form should be + ;; ``(hissp.._macro_.prelude)``, which is also implied in ``$ lissp -c`` + ;; commands. (See the `hissp.prelude` shorthand for Lissp.) ;; ;; Larger projects with access to functional and macro libraries need not ;; use this prelude at all. diff --git a/src/hissp/munger.py b/src/hissp/munger.py index 2034264e5..85de30909 100644 --- a/src/hissp/munger.py +++ b/src/hissp/munger.py @@ -182,8 +182,10 @@ def demunge(s: str) -> str: Characters can be encoded in one of three ways: Short names, Unicode names, and ordinals. - ``demunge`` will decode any of these, even though :func:`munge` will - consistently pick only one of these for any given character. + ``demunge`` will decode any of these. Even though :func:`munge` will + consistently pick only one of these for any given character, + which Unicode characters have names depends on the Python version. + ``demunge`` will also leave the remaining text as-is, along with any invalid Quotez. diff --git a/src/hissp/reader.py b/src/hissp/reader.py index 08247a9cd..f169ed821 100644 --- a/src/hissp/reader.py +++ b/src/hissp/reader.py @@ -178,9 +178,9 @@ def position(self, pos: int) -> Tuple[str, int, int, str]: class Comment: - """Parsed object for a comment token (line comment block). + """`Parsed object` class for a `comment token` (line comment block). - The reader normally discards these, but reader macros can use them. + The reader normally discards these, but they can be `tag` arguments. """ def __init__(self, token): @@ -199,9 +199,9 @@ def __repr__(self): class Kwarg: - """Contains a read-time keyword argument for reader macros. + """Contains a read-time keyword argument for a `tag`. - Normally made with kwarg tags, but can be constructed directly. + Normally made with a `kwarg token`, but can be constructed directly. """ def __init__(self, k, v): @@ -219,30 +219,30 @@ class Lissp: Wraps around a Hissp compiler instance. Parses Lissp tokens into Hissp syntax trees. - The special tags are handled here. They are + The `special tag`\ s are handled here. They are .. list-table:: * - ``'`` - `quote` * - :literal:`\`` (backtick) - - template quote (starts a `template`) + - `template quote` (starts a `template`) * - ``_#`` - - discard + - `discard tag` * - ``.#`` - - inject (evaluate at read time and use resulting object) + - `inject tag` (evaluate at read time) - Plus the three built-in template helper tags, which are only + Plus the three built-in template helpers, which are only valid inside a template. .. list-table:: * - ``,`` - - unquote + - `unquote` * - ``,@`` - - splice unquote + - `splice` * - ``$#`` - - `gensym` + - `gensym tag` Special tags are reserved by the reader and cannot be reassigned. """ @@ -528,7 +528,7 @@ def _continue(self): @staticmethod def bare(v): - """Preprocesses bare tokens. Handles escapes and munging.""" + """Preprocesses a `bare token`. Handles escapes and munging.""" if "\\" != v[0]: with suppress(ValueError, SyntaxError): if not hasattr(x := ast.literal_eval(Lissp.escape(v)), "__contains__"): @@ -548,12 +548,13 @@ def _check_depth(self): def is_hissp_string(form) -> bool: """Determines if form would directly represent a string in Hissp. + (A `Hissp string`.) - Allows "readerless mode"-style strings: ('quote','foo',) - and any string literal in a Hissp-level str: '"foo"' - (including the "('foo')" form produced by the Lissp reader). + Allows `readerless mode`-style strings: ``('quote','foo',)`` + and any `string literal fragment`: ``'"foo"'`` + (including the ``"('foo')"`` form produced by the Lissp reader). - Macros often produce strings in one of these forms, via ``'`` or + Macros often produce strings in one of these forms, via `quote` or `repr` on a string object. """ return ( @@ -569,17 +570,18 @@ def is_lissp_string(form) -> bool: Determines if form could have been read from a Lissp string literal. It's not enough to check if the form has a string type. - Several token types such as control words, symbols, and Python - injections, read in as strings. Macros may need to distinguish these - cases. + Several token types such as a `control token`, `symbol token`, or + `fragment token`, read in as a `str atom`. Macros may need to + distinguish these cases. """ return type(form) is str and form.startswith("(") and bool(is_string_literal(form)) def is_string_literal(form) -> Optional[bool]: """Determines if `ast.literal_eval` on form produces a string. + (A `string literal fragment`.) - False if it produces something else or None if it raises Exception. + ``False`` if it produces something else or ``None`` if it raises. """ with suppress(Exception): return type(ast.literal_eval(form)) is str @@ -589,8 +591,8 @@ def is_qualifiable(symbol): """Determines if symbol can be qualified with a module. Can't be ``quote``, ``__import__``, any Python reserved word, a - prefix auto-gensym, already qualified, method syntax, or a module - handle; and must be a valid identifier or attribute identifier. + prefix auto-`gensym`, already qualified, method syntax, or a `module + handle`; and must be a valid identifier or attribute identifier. """ return ( symbol not in {"quote", "__import__"} @@ -603,7 +605,7 @@ def is_qualifiable(symbol): def transpile(package: Optional[str], *modules: str): """Transpiles the named Python modules from Lissp. - A .lissp file of the same name must be present in the module's + A ``.lissp`` file of the same name must be present in the module's location. The Python modules are overwritten. Missing modules are created. If the package is "" or ``None``, `transpile` writes non- packaged modules to the current working directory instead. @@ -614,21 +616,21 @@ def transpile(package: Optional[str], *modules: str): def transpile_packaged(resource: str, package: str): - """Locates & transpiles a packaged .lissp resource file to .py.""" + """Locates & transpiles a packaged ``.lissp`` resource file to ``.py``.""" with resources.path(package, resource) as path: transpile_file(path, package) def transpile_file(path: Union[Path, str], package: Optional[str] = None): - """Transpiles a single .lissp file to .py in the same location. + """Transpiles a single ``.lissp`` file to ``.py`` in the same location. - Code in .lissp files is executed upon compilation. This is necessary - because macro definitions can alter the compilation of subsequent - top-level forms. A packaged Lissp file must know its package at - compile time to handle templates and macros correctly. + Code in ``.lissp`` files is executed upon compilation. This is + necessary because macro definitions can alter the compilation of + subsequent top-level forms. A packaged Lissp file must know its + package at compile time to handle templates and macros correctly. - After the .py file is written, `__file__` will be set to it, if it - doesn't exist already. + After the ``.py`` file is written, `__file__` will be set to it, if + it doesn't exist already. """ path = Path(path).resolve(strict=True) qualname = f"{package or ''}{'.' if package else ''}{PurePath(path.name).stem}" diff --git a/src/hissp/repl.py b/src/hissp/repl.py index 28a2ba401..0eaca1315 100644 --- a/src/hissp/repl.py +++ b/src/hissp/repl.py @@ -28,7 +28,7 @@ class LisspREPL(InteractiveConsole): which is useful for debugging other modules. See `hissp.subrepl`. - Call interact() to start. + Call :func:`interact` to start. """ # locals shadows the builtin, but that's the name in the superclass. @@ -103,7 +103,7 @@ def main(__main__): """`REPL` command-line entry point. `hissp.macros._macro_` is copied into the module namespace, - making the bundled macros immediately available unqualified. + making the bundled `macros` immediately available `unqualified`. """ repl = LisspREPL(locals=__main__.__dict__) import hissp.macros # Here so repl can import before compilation.