lispython 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. lispython-0.6.0/CHANGELOG.md +447 -0
  2. {lispython-0.5.0 → lispython-0.6.0}/PKG-INFO +3 -1
  3. {lispython-0.5.0 → lispython-0.6.0}/README.md +2 -0
  4. {lispython-0.5.0 → lispython-0.6.0}/docs/macros.md +6 -5
  5. {lispython-0.5.0 → lispython-0.6.0}/docs/syntax/expressions.md +9 -0
  6. {lispython-0.5.0 → lispython-0.6.0}/docs/syntax/statements.md +2 -0
  7. {lispython-0.5.0 → lispython-0.6.0}/docs/version_macro.py +2 -1
  8. {lispython-0.5.0 → lispython-0.6.0}/pyproject.toml +40 -3
  9. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/__init__.py +1 -1
  10. lispython-0.6.0/src/lispy/core/builtins.py +127 -0
  11. lispython-0.6.0/src/lispy/core/compiler/expr.py +871 -0
  12. lispython-0.6.0/src/lispy/core/compiler/features.py +210 -0
  13. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/core/compiler/literal.py +126 -6
  14. lispython-0.6.0/src/lispy/core/compiler/stmt.py +1025 -0
  15. lispython-0.6.0/src/lispy/core/compiler/utils.py +270 -0
  16. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/core/errors.py +66 -0
  17. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/core/importer.py +18 -2
  18. lispython-0.6.0/src/lispy/core/macro.py +156 -0
  19. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/core/meta_functions.py +3 -0
  20. lispython-0.6.0/src/lispy/core/nodes.py +1129 -0
  21. lispython-0.6.0/src/lispy/core/parser.py +1952 -0
  22. lispython-0.6.0/src/lispy/core/utils.py +78 -0
  23. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/core_meta_functions.lpy +5 -0
  24. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/lsp/server.lpy +105 -46
  25. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/macros/sugar.lpy +13 -6
  26. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/tools.lpy +245 -63
  27. lispython-0.6.0/tests/conftest.py +37 -0
  28. lispython-0.6.0/tests/test_as_thread.py +240 -0
  29. {lispython-0.5.0 → lispython-0.6.0}/tests/test_ast_hygiene.py +1 -1
  30. {lispython-0.5.0 → lispython-0.6.0}/tests/test_cli_module.py +16 -0
  31. {lispython-0.5.0 → lispython-0.6.0}/tests/test_dx.py +2 -1
  32. lispython-0.6.0/tests/test_expr.py +365 -0
  33. {lispython-0.5.0 → lispython-0.6.0}/tests/test_gensym.py +10 -0
  34. {lispython-0.5.0 → lispython-0.6.0}/tests/test_include_meta.py +24 -4
  35. {lispython-0.5.0 → lispython-0.6.0}/tests/test_literal.py +16 -16
  36. lispython-0.6.0/tests/test_literal_unwrap.py +355 -0
  37. {lispython-0.5.0 → lispython-0.6.0}/tests/test_lsp.py +77 -1
  38. lispython-0.6.0/tests/test_macro_positions.py +581 -0
  39. {lispython-0.5.0 → lispython-0.6.0}/tests/test_meta_functions.py +3 -3
  40. lispython-0.6.0/tests/test_names.py +300 -0
  41. lispython-0.6.0/tests/test_nesting.py +1417 -0
  42. lispython-0.6.0/tests/test_nrepl.py +410 -0
  43. lispython-0.6.0/tests/test_parser.py +1786 -0
  44. lispython-0.6.0/tests/test_statement_in_expr.py +80 -0
  45. lispython-0.6.0/tests/test_stmt.py +852 -0
  46. {lispython-0.5.0 → lispython-0.6.0}/tests/test_version_features.py +18 -5
  47. {lispython-0.5.0 → lispython-0.6.0}/tests/utils.py +0 -1
  48. {lispython-0.5.0 → lispython-0.6.0}/uv.lock +2 -2
  49. lispython-0.5.0/.githooks/post-checkout +0 -119
  50. lispython-0.5.0/.pre-commit-config.yaml +0 -22
  51. lispython-0.5.0/.vscode/settings.json +0 -3
  52. lispython-0.5.0/CHANGELOG.md +0 -83
  53. lispython-0.5.0/src/lispy/core/builtins.py +0 -29
  54. lispython-0.5.0/src/lispy/core/compiler/expr.py +0 -558
  55. lispython-0.5.0/src/lispy/core/compiler/features.py +0 -85
  56. lispython-0.5.0/src/lispy/core/compiler/stmt.py +0 -706
  57. lispython-0.5.0/src/lispy/core/compiler/utils.py +0 -45
  58. lispython-0.5.0/src/lispy/core/macro.py +0 -127
  59. lispython-0.5.0/src/lispy/core/nodes.py +0 -581
  60. lispython-0.5.0/src/lispy/core/parser.py +0 -769
  61. lispython-0.5.0/src/lispy/core/utils.py +0 -32
  62. lispython-0.5.0/tests/test_as_thread.py +0 -49
  63. lispython-0.5.0/tests/test_expr.py +0 -57
  64. lispython-0.5.0/tests/test_literal_unwrap.py +0 -73
  65. lispython-0.5.0/tests/test_parser.py +0 -104
  66. lispython-0.5.0/tests/test_stmt.py +0 -294
  67. {lispython-0.5.0 → lispython-0.6.0}/.gitignore +0 -0
  68. {lispython-0.5.0 → lispython-0.6.0}/LICENSE.md +0 -0
  69. {lispython-0.5.0 → lispython-0.6.0}/docs/index.md +0 -0
  70. {lispython-0.5.0 → lispython-0.6.0}/docs/syntax/overview.md +0 -0
  71. {lispython-0.5.0 → lispython-0.6.0}/docs/usage/cli.md +0 -0
  72. {lispython-0.5.0 → lispython-0.6.0}/docs/usage/getting-started.md +0 -0
  73. {lispython-0.5.0 → lispython-0.6.0}/docs/why-lispy.md +0 -0
  74. {lispython-0.5.0 → lispython-0.6.0}/mkdocs.yml +0 -0
  75. {lispython-0.5.0 → lispython-0.6.0}/noxfile.py +0 -0
  76. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/core/compiler/__init__.py +0 -0
  77. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/lsp/__init__.py +0 -0
  78. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/lsp/__main__.py +0 -0
  79. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/macros/__init__.py +0 -0
  80. {lispython-0.5.0 → lispython-0.6.0}/src/lispy/macros/init.lpy +0 -0
  81. {lispython-0.5.0 → lispython-0.6.0}/tests/__init__.py +0 -0
  82. {lispython-0.5.0 → lispython-0.6.0}/tests/test_glued_head.py +0 -0
@@ -0,0 +1,447 @@
1
+ # Changelog
2
+
3
+ All notable changes to lispython are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
5
+ follows [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.6.0] - 2026-11-02
10
+
11
+ ### Added
12
+
13
+ - The nREPL `eval` op reads the standard `file`, `line` and `column` fields
14
+ (where the code sits in the editor's buffer, 1-based). Tracebacks and
15
+ compile errors then name that file with buffer line numbers and print the
16
+ source line, including for definitions evaluated in earlier requests and
17
+ for `load-file`.
18
+ - `(raise)` compiles to a bare `raise`, re-raising the exception being
19
+ handled.
20
+
21
+ ### Changed
22
+
23
+ - A string opened with `\"` in an f/t-string replacement field must be closed
24
+ by `\"` (`\"\"\"` for `\"\"\"`): a bare `"` in it, as its close or its
25
+ content, is a compile error at the opener, where it was accepted before
26
+ (`f"{(g \"a")}"`). Its `{x=}` echo (and a t-string's expression text) now
27
+ shows the source as written: `f"{(.get d \"k\")=}"` echoes
28
+ `(.get d \"k\")=`, where it echoed `(.get d "k")=`.
29
+ - On 3.12 a single-quoted literal's format spec continued after a line
30
+ break in its text is read as CPython 3.12 reads it: the line break and
31
+ what follows are no longer part of the spec (`f"{x:ab\n}"` formats with
32
+ `ab`, where it used `ab\n`), only blanks, comments and replacement fields
33
+ may follow before the `}`, and any other text there (`f"{x:a\nb}"`, which
34
+ was accepted) is CPython's "expecting '}', or format specs" error or its
35
+ tokenizer error.
36
+ - An nREPL eval without `file` is named `<nrepl-N>` (a new N per eval)
37
+ instead of `<nrepl>`, which can break a client that matches `<nrepl>` in
38
+ error text.
39
+ - On Python 3.13+, a line break in a single-quoted f/t-string's format spec
40
+ is a compile error at the break even when the field then closes
41
+ (`f"{x:ab\n}"`), as CPython 3.13+ refuse it; it compiled before.
42
+ - An invalid `line` or `column` (not an integer from 1 to
43
+ `NREPL-MAX-POSITION-LINE`, 1,000,000, or `NREPL-MAX-POSITION-COLUMN`,
44
+ 100,000) gets an `error` reply with the request id instead of being
45
+ accepted.
46
+ - Every name the compiler emits must be a Python name, or it is a compile
47
+ error at the symbol. Code that compiled and ran before is now refused:
48
+ a symbol whose mangled text is no Python identifier (`foo!`, `a.`, as in
49
+ `(= foo! 1)`), an operator whose mangled text is no identifier used as a
50
+ value (`(f +)`, `(print ~)`), and a hard keyword in any name slot:
51
+ attributes (`(. a if)`, `(.if x)`), keyword arguments (`(f :if 1)`, class
52
+ keywords), parameters (`(def f [:if 1] ...)`), type parameters, import
53
+ names and aliases, match captures and class pattern attributes. CPython
54
+ compiled such names from the AST, but no Python source spells them, so
55
+ `l2py` printed code that does not parse. Operators that mangle to a name
56
+ (`-` is `_`, `is-not` is `is_not`) are names as before.
57
+ - An attribute slot takes one name: `(. a b.c)` and `(.a.b x)` are compile
58
+ errors; write `(. a b c)` and `(.b x.a)`.
59
+ - `(. a)`, an attribute form with no attribute, is a compile error in every
60
+ context (value, assignment, `del`, annotated and augmented targets); it
61
+ used to compile to the plain name `a`. Write `a`, or `(. a b)` for an
62
+ attribute.
63
+ - A hard keyword called in an expression (`(f (else 1))`) is reported as
64
+ `else is a Python keyword and cannot be used as a name`, at the keyword,
65
+ instead of `else is a Python keyword and cannot be called` at the form.
66
+ - Forms that compiled to something other than what they say are compile
67
+ errors: `(ife a b c d)` (dropped `d`), `(yield a b)`, a comprehension clause
68
+ with a stray or dangling operand (`[x for y in z if]`, `(x for y in z if a
69
+ b)`), and a parameter list CPython refuses: a misplaced
70
+ or repeated `/`, `*` or `**kw` (`[a / /]` dropped `a`, `[*a *b]` made `b`
71
+ keyword-only), a bare `*` with no keyword-only parameter, and `[a :b 1
72
+ c]`, which compiled to `def f(a, b, c=1)`. An annotated lambda parameter (`(lambda [a ^int] 1)`, which `l2py`
73
+ printed as `lambda a: int: 1`) is refused too.
74
+ - Class bases are read as call arguments: `(class A [**kw])` compiles to
75
+ `class A(**kw)`.
76
+ - A statement form (`do`, `=`, `if`, `return`, ...) in expression position is
77
+ now a compile error instead of emitting an invalid call such as `do(...)`.
78
+ A Python function named `do`, `deco`, `async_def`, etc. can no longer be
79
+ called with lispython call syntax in expression position; they were already
80
+ unreachable as statements.
81
+ - A Python hard keyword in call position (e.g. `(not a b)`, which used to
82
+ compile to `not (a, b)`, silently false, or `(else ...)`) is now a compile
83
+ error.
84
+ - Statement forms that compiled while dropping part of what was written are
85
+ now compile errors: `(deco d (f))`, whose definition is not a `def`,
86
+ `async-def`, `class` or `deco` (the decorator was silently dropped);
87
+ `(assert a b c)` (the third operand was ignored); a form other than
88
+ `(case ...)` inside `match` (it was read as a case); and def arguments or
89
+ class bases written as anything but a `[...]` bracket (`(def f (x) ...)`
90
+ compiled). A `case` with `as` after its guard (`(case 1 if g as y ...)`,
91
+ which compiled `as y` into the body) is refused: `as` comes before `if`.
92
+ - A `;` ends a symbol or number, so a comment needs no space before it:
93
+ `a;c` was the symbol `a;c`, now `a` followed by a comment.
94
+ - A Python hard keyword as a symbol name, or as a part of a dotted symbol,
95
+ is a compile error: `(raise from)`, `(= o.if 1)` (which compiled to an
96
+ attribute named `if`), and a keyword as a def, class, global, nonlocal,
97
+ `case ... as` or `except [... as]` name (`(def if [] 1)`,
98
+ `(case 1 as as ...)`).
99
+ - `~` directly before a closing bracket, a comma, `;` or the end of input
100
+ (`(f ~)`, `(f ~,a)`, `~;c`) is the symbol `~`, as before a space, instead
101
+ of an unquote that took the next form or gave "unmatched ')'".
102
+ - A dotted name in a case pattern (`(case Color.RED ...)`) is a value
103
+ pattern, compared with `==` as in Python. It compiled to a capture pattern
104
+ that matched every subject, so code that relied on that now falls through
105
+ to the next case.
106
+ - A `()` in a case pattern whose head is an expression form (`(sub x)`,
107
+ `(ife a b c)`, `(is-not x)`, `(not-in x)`) is a compile error ("pattern
108
+ expected") instead of a class pattern on a class of that name.
109
+ - A sign before anything but a number in a case pattern (`(- a)`, `{-a 1}`)
110
+ reports "pattern can sign only a number literal, got (- a)" with the
111
+ pattern usage, instead of "only a number literal can be signed in a match
112
+ pattern".
113
+ - An f/t-string format spec reads braces as CPython does: `}` always ends
114
+ the spec and `{` opens a nested field, so `{{` and `}}` are no escapes
115
+ there, except that on 3.12+ `{{` is a literal `{` once a nested field has
116
+ opened in the spec. `f"{x:{{}}}"`, which formatted `x` with the literal
117
+ spec `{}`, is now `x` with a spec field holding `{}`, and `f"{x:{y}{{}}}"`,
118
+ whose spec was `{y}` then `{}`, has the spec `{y}` then `{` followed by a
119
+ literal `}` on 3.12+ (a spec field `{}` on 3.11).
120
+ - The operand after `^`, `*`, `**` or `:` keeps its own token type, so `*1`,
121
+ `**1`, `:1` and `^1` read a number `1` instead of a symbol named `1`. A
122
+ keyword with a number or other non-name after it (`(f :1 x)`, `(def g [:1
123
+ 2] ...)`) is a compile error at it, a bare `:a` or `**x` used as a value
124
+ (`(= x :a)`, `[**1]`) is a compile error instead of silently the name `a`
125
+ (or a raw `AttributeError`), and a trailing `:a` with no value in a call,
126
+ def or class argument list is a compile error instead of a raw `IndexError`.
127
+ - Threading a literal into a call (`(-> 1 (f))`, `(->> 1 (f 2))`,
128
+ `(-> "a" (f))`, `(-> ... (f))`) compiles to `f(1)`, `f(2, 1)`, `f('a')`
129
+ and `f(...)` instead of failing with an `AttributeError`, and
130
+ `(-> None (f))`, which compiled to `f()`, now gives `f(None)`. A macro may
131
+ put Python values (`None`, booleans, numbers, `...`, str, bytes, and
132
+ lists and tuples of them) anywhere in the forms it returns, including
133
+ inside `*`, `**`, `^` and quote forms (`` `(list *~y) ``); each compiles
134
+ to the same value (including `inf`, `nan`, `-0.0`, complex numbers with a
135
+ signed zero, `inf` or `nan` part and ints of any size) at the macro
136
+ call's position, even where `float` or `complex` is rebound or inside a
137
+ match pattern. Converting nested lists does not recurse, so deep nesting
138
+ does not hit `RecursionError` there. Any other value (a dict, a set, an
139
+ object), or a form or list that contains itself (also one unquoted with
140
+ `~`), is a compile error at the call naming the macro.
141
+ - A str a macro returns keeps its backslashes literally: `"a\\b"` was
142
+ compiled as `"a\b"` (backspace), and a quote, line ending or control
143
+ character is kept as written. The same holds for quoted strings and
144
+ quoted symbols (`'a\b` was `Symbol('a\x08')`); bytes are supported.
145
+ - An assignment, augmented assignment, `del`, `for` or `with ... as` target
146
+ that Python cannot bind (`(= 1 2)`, `(= (f) 1)`, `(+= [] 1)`, `(del *a)`)
147
+ is a compile error at the target with CPython's message ("cannot assign to
148
+ literal"), and so is an annotation anywhere but right after a single
149
+ target (`(= ^int 1)`, `(= a b ^int 1)`) or a def/class name that is not a
150
+ name (`(def [] [] 1)`, `(class 1 [])`).
151
+ - A quote, quasiquote or unquote-splice with no form right after it
152
+ (`(f ' a)`, `(f *' a)`, `(f ~@ a)`, `(f ')`, `',a`) is a compile error at
153
+ the quote instead of a symbol `'`, `f(*', a)`, an unquote of `@` or
154
+ "unmatched ')'". `~` alone is still the invert operator.
155
+ - A malformed `match` case pattern is a compile error at the part at fault,
156
+ quoting the usage of its kind (class, mapping or sequence pattern, `(| ...)`),
157
+ instead of an `IndexError`, `ValueError` or `AttributeError` or an AST
158
+ that means something else: a dangling `:attr` (`(P :x)`), a positional
159
+ pattern after a keyword one (`(P :x 1 2)`, which compiled to `P(2, x=1)`),
160
+ a mapping key without a pattern (`{"a"}`), `**rest` not last (`{**r 1}`),
161
+ `**_` (the rest must be a capture name), a key that is no literal or
162
+ dotted name (`{a 1}`), a second `*rest` (`[*a *b]`), `*rest` outside a
163
+ sequence (`(case *a ...)`), `(| 1)`, a class pattern head that is no name
164
+ (`(1)`, `((f) 1)`), a `()` that is no call (`(+ 1 2)`, `(, a b)`,
165
+ `f"x"`), `...` and a `:x` keyword token as a pattern.
166
+ - A name after `*`, `**` or `:` in a case pattern that is no Python
167
+ identifier (`[*1a]`, `{**1a}`, `(P :1a 2)`) is a compile error instead of
168
+ an AST naming `1a`.
169
+ - A malformed dotted name in a case pattern (`.x`, `a..b`, `x.`), as a value
170
+ pattern, a mapping key or a class pattern head, is a compile error instead
171
+ of an attribute with an empty name.
172
+ - A string opened with a bare quote inside an f/t-string replacement field
173
+ reads as Python reads it: `\"` in it is an escaped quote and its
174
+ backslashes are kept. `f"{(g "a\" b \"c")}"` compiles to
175
+ `f"{g('a" b "c')}"`; it used to turn every `\"` into `"` before parsing
176
+ the field, which split the string into `g('a', b, 'c')` or refused it
177
+ (`f"{(g "say \"hi\"")}"`: "invalid string prefix").
178
+
179
+ ### Deprecated
180
+
181
+ - Writing a string in an f/t-string replacement field with `\"`
182
+ (`f"{(.get d \"k\")}"`) is deprecated: each such string emits a
183
+ `SyntaxWarning` at its opening `\"`, shown by default (as CPython
184
+ 3.12+'s invalid escape sequences are). Write plain quotes
185
+ (`f"{(.get d "k")}"`), which works on every supported Python. A
186
+ warnings filter that turns it into an error
187
+ (`PYTHONWARNINGS=error::SyntaxWarning`) makes it a compile error at the
188
+ opener.
189
+
190
+ ### Fixed
191
+
192
+ - A replacement field whose expression starts with a bare `=` (`f"{=}"`,
193
+ `f"{=x}"`, `f"{=!r}"`, a source ending at `f"{=`) is refused with CPython
194
+ 3.12+'s "valid expression required before '='" at the `=`, as compile() does
195
+ there. On 3.11 lispython reports that same 3.12+ message for a closed
196
+ literal, as it does for `f"{}"` (CPython 3.11's own wording differs); a
197
+ source ending inside the literal gets 3.11's whole-literal error. Before, a
198
+ closed literal was refused only by the compile step ("= is not a valid
199
+ Python name") or as a missing conversion (`f"{=!}"`), and on 3.12+ `f"{=`
200
+ gave "'{' was never closed" and `f"{=}` "unterminated f-string literal".
201
+ It is now refused when the source is read, so also inside quoted code.
202
+ - On 3.12+ an error after a line break in a single-quoted f/t-string's text
203
+ (`f"a\n}"`, `f"a\n{x!z}"`, `f"a\n{)`) reports CPython's "unterminated
204
+ f-string literal (detected at line N)" for that literal, as `compile()`
205
+ does, whether or not the literal later closes: CPython's tokenizer stops
206
+ at the break. A multi-line literal with no error still compiles.
207
+ - A comment ends at a lone CR as at LF and CRLF, as in CPython. A CR-only
208
+ source (`; c\r(f 1)`) used to lose every form after a comment.
209
+ - A source ending inside a string, an f/t-string, a replacement field's
210
+ expression, a format spec or a conversion reports the error `compile()`
211
+ raises for that text on the running Python: "unterminated ... literal
212
+ (detected at line N)" at the literal's prefix, 3.12+'s "'(' was never
213
+ closed" (or "'{' was never closed") at the innermost bracket or field
214
+ brace still open,
215
+ "expecting '}'" for a string whose quote closes the
216
+ literal it is in, 3.13+'s "newlines are not allowed in format specifiers",
217
+ 3.15's "expecting '}' to close '{' on line N", and 3.11's whole-literal
218
+ error. A name glued to a quote after `!` reads as a string prefix (`!r"`),
219
+ and an invalid conversion name is reported at the name, after any error in
220
+ its field's format spec where CPython checks it only once the field closes.
221
+ - An f/t-string's closing quote inside a format spec (`f"{x:{y}"`) reports
222
+ CPython 3.12+'s "expecting '}', or format specs" at the quote.
223
+ - An empty replacement field ("valid expression required before '}'") is
224
+ reported where CPython 3.12+ report it: at the `}`, `!`, `:` or the `=` of
225
+ `{ = }`, not at the field's `{`. A missing conversion (`f"{x! }"`) is
226
+ reported at the token after the `!`, not at the `!`.
227
+ - An error in an f/t-string's field (an empty expression, more than one
228
+ expression, a missing or invalid conversion, a token out of place after
229
+ `!`) gives way, as in CPython, to a later tokenizer error outside any
230
+ literal, such as a string that never closes: `f"{x!z}" "b` and
231
+ `f"{a b}" "c` report "unterminated string literal" at the last string. On
232
+ 3.11 a later f-string that never closes does too. On 3.12+ a closing
233
+ bracket out of place in a field after the error keeps it (`f"{x!r y)}" "b`),
234
+ and one right after a conversion is CPython's "f-string: unmatched ')'".
235
+ - Error positions count a lone CR as a line break, as CPython does (CRLF is
236
+ one break), so a source with CR line endings reports the same line as
237
+ `compile()`.
238
+ - Parsing no longer spends Python frames per nesting level, so these parse
239
+ at any depth and from a deep caller stack instead of raising
240
+ `RecursionError`: f/t-strings nested in each other's fields, format specs
241
+ nested in format specs, brackets nested deep inside one replacement
242
+ field, and long prefix chains on one token
243
+ (`^^^x`, `:::x`, `***x`, `^*^*x`), also glued to a bracket (`^^^a[int]`).
244
+ - Quasiquoting f-strings nested in each other's fields checks each field for
245
+ an unquote once, linear instead of quadratic in the nesting (4,000 levels
246
+ took 15 s), and a deep nest of `(do ...)` statements compiles in linear
247
+ time.
248
+ - Deeply nested forms compile in time linear in their nesting: a nested list
249
+ (`[[[...]]]`, also inside a macro call such as `when`) was exponential (20
250
+ levels took seconds) and a form in head position (`((((f))))`) quadratic.
251
+ Rendering a form and expanding macros no longer recurse, so a form nested
252
+ past Python's recursion limit renders and expands.
253
+ - A long run of digits that is not a number lexes in linear time instead of
254
+ quadratic.
255
+ - Forms nested deeper than about 245–330 levels (case patterns about 490)
256
+ are no longer refused with "form nested too deeply to compile": the
257
+ compiler and quote expansion no longer spend Python frames per level, so
258
+ nesting compiles as deep as CPython's compile() takes (about 10,000
259
+ Python AST levels on 3.12/3.13, more on 3.14+; forms that cost two AST
260
+ levels per lispython level, such as keyword arguments, starred arguments
261
+ and method calls, stop near half that). On 3.11 compile() itself still
262
+ stops near the recursion limit, with the positioned error below. `l2py`
263
+ still stops at a few hundred levels, because `ast.unparse` recurses: that
264
+ is the positioned error "form nested too deeply to translate".
265
+ - Nesting past compile()'s own limit is the positioned "form nested too
266
+ deeply to compile" error from `lpy`, imports, the REPL, nREPL, `defmacro`
267
+ and `require`, instead of a `RecursionError` traceback, at the bottom of the
268
+ deepest path in what compile() was given: for `lpy` files and imports the
269
+ first statement that fails to compile alone (the whole module when each
270
+ fits alone), the whole form for `defmacro` and `require`, and the statement
271
+ compile() fails on for `lpy -c`, the REPL and nREPL. Its message names
272
+ Python's recursion limit only where that is the limit hit (3.11, and
273
+ `l2py`), since raising it does not move compile()'s limit on 3.12+. A
274
+ macro's own runaway recursion still raises `RecursionError`.
275
+ - Data a macro puts in its expansion, spliced with `~x` or placed in a
276
+ form's list, that holds the same Python list or tuple in many places is
277
+ converted to nodes in time linear in the distinct data, instead of
278
+ exponential in the depth of the sharing. Compiling such data still emits
279
+ each shared value where it appears.
280
+ - A malformed expression form is a positioned compile error quoting its
281
+ usage instead of an IndexError, ValueError or AssertionError, or an AST
282
+ CPython rejects: `(:=)`, `(:= a ^int 1)`, `(.)`, `(.foo)`, `(ife a)`,
283
+ `(sub)`, `(lambda [a])`, `(await)`, `(yield-from)`, `(and)`, `(< a)`,
284
+ `[:]`, `{1 2 3}` and `(x for y)`. Hover and signature help for these forms
285
+ show the same usage line.
286
+ - `(= (sub a) ^int 1)` records `a` in `__annotations__`, as `(= a ^int 1)`
287
+ does.
288
+ - `~x` or `~@x` outside a quasiquote is a compile error at it (`~ (unquote)
289
+ is only allowed inside a quasiquote`) instead of a raw `ValueError`.
290
+ - A comprehension target that cannot be assigned (`[x for 1 in xs]`) and a
291
+ walrus target that is not a name (`(:= a.b 1)`, `(:= 1 2)`) are compile
292
+ errors with CPython's messages (`cannot assign to literal`, `cannot use
293
+ assignment expressions with attribute`) at the target, instead of a raw
294
+ `ValueError` or `TypeError`.
295
+ - `(case pattern as _ ...)` is a compile error at the `_` (`cannot use '_'
296
+ as a target`, as in CPython) instead of a raw `ValueError`.
297
+ - A dotted `defmacro` name (`(defmacro a.b ...)`) is refused as
298
+ `(defmacro ...) expects an undotted name, got a.b`, at the name, instead
299
+ of an error about the internal `___defmacro_temp___a.b` def.
300
+ - LSP hover and signature help for a statement form show the same usage
301
+ line as the compiler's error messages (`(def name [args] body...)`,
302
+ `(for target in iterable body...)`), and now cover the `except`,
303
+ `except*`, `else`, `finally` and `case` clauses, augmented assignment
304
+ (`+=` and friends), and `|` as an or-pattern. For `|`, hover and signature
305
+ help show both the bitwise-OR operator and the or-pattern usage.
306
+ Completion now also lists the augmented assignment operators and the
307
+ `except`, `else` and `finally` clauses as special forms (`except*` and
308
+ `case` were already offered as keywords).
309
+ Hover text for `defmacro`, `lambda` and `fn` says `[args]`, matching `def`.
310
+ - A complex number a macro returns shows in macroexpand output as `1+2j`
311
+ instead of `(1+2j)`, which read back as a call, not a number. A negative
312
+ nan (alone or as a complex part) shows as `(- (float "nan"))` instead of
313
+ losing its sign.
314
+ - A macro whose expansion puts Python data in a form's list compiles when
315
+ another attribute also holds that list, and when the form's list is a
316
+ tuple or deque, instead of failing with `AttributeError: 'int' object has
317
+ no attribute 'name'`. A form's list of any other type (a dict, a set) is a
318
+ compile error at the macro call naming the macro.
319
+ - Code a macro builds (from a quasiquote template) takes the position of the
320
+ macro call, so a traceback or compile error in it points at that call
321
+ instead of line 0; arguments written by the user keep their own positions.
322
+ - nREPL eval tracebacks no longer include the server's own `nrepl-eval`
323
+ frame.
324
+ - The nREPL server buffers bytes and decodes each complete line, so a
325
+ multi-byte UTF-8 character split across two socket reads no longer drops
326
+ the connection. An unknown op, a malformed request or a failing handler
327
+ (such as `eval` without `code`) now gets an `error` reply echoing the
328
+ request id, and the connection keeps serving; a line that is not valid
329
+ UTF-8 or JSON (or JSON that is not an object, or is nested too deeply to
330
+ parse) gets an id-less `error` reply. A result that cannot be
331
+ serialised is an `error` reply too, `(exit)` in an eval is reported as a
332
+ `SystemExit` eval error, and any socket error ends the connection quietly.
333
+ A request line over 4 MiB gets one id-less `error` reply and is dropped up
334
+ to its next newline, so a client that never sends a newline cannot grow the
335
+ server's memory; the connection keeps serving.
336
+ - `as->` expands to an expression (a tuple of `:=` assignments indexed with
337
+ `-1`), so it works in expression position such as a `return` value or a
338
+ call argument instead of emitting an invalid `do(...)` call.
339
+ - `as->` renames only references to the threading name: a keyword-argument
340
+ name (`(f :x x)` was `f(__x_0=__x_0)`, now `f(x=__x_0)`) and the params
341
+ and body of a `fn`/`lambda` that binds the same name are left alone (its
342
+ defaults are still renamed), and `~x` inside a quasiquote now gets the
343
+ threaded value.
344
+ - `as->` also renames a dotted reference (`x.real` becomes `__x_0.real`) and
345
+ a reference inside an f-string format spec (`f"{1:{x}}"`).
346
+ - A `for` or `while` with no target, iterable or test, or with no body
347
+ before its `(else ...)` (`(for x in)`, `(for [x [1]] (print x))`,
348
+ `(while x)`), is a compile error at the loop instead of an `IndexError`
349
+ or a loop with an empty body that Python rejects.
350
+ - `^` followed by stars before a bracket or a quote (`^*(`, `^**[`, `^*'`,
351
+ `^**~`, also with a space after the quote) or alone (`^*`, `^**`) is a
352
+ compile error at the prefix instead of a `KeyError`, an `AttributeError`
353
+ or a call such as `f(*', a)`; `^*Ts` and `^**P` are unchanged.
354
+ - Every malformed statement form is a compile error naming the problem and
355
+ the form's usage, at the form, clause or operand at fault, instead of a
356
+ `ValueError`, `IndexError`, `AssertionError`, `TypeError` or an AST that
357
+ Python rejects: `if`, `def`, `class`, `with`, `try` and its clauses,
358
+ `match` and `case`, `deco`, `raise`, `assert`, `=`, augmented assignment,
359
+ `del`, `global` and `nonlocal` with missing parts (`(if x)`, `(def f)`,
360
+ `(with [a])`, `(try (f))`, `(match x (case 1))`, `(= x)`, `(global)`, ...),
361
+ extra parts (`(raise a b)`, `(= x ^int 1 2)`) or a dangling `as`
362
+ (`(with [a as] ...)`, `(except [E as] ...)`). A `try` clause out of order
363
+ (except after else or finally, except mixed with except*, a form after
364
+ the clauses) is reported at that clause instead of as "except is a Python
365
+ keyword".
366
+ - An empty `()` is a compile error at it ("use (,) for an empty tuple")
367
+ instead of an `IndexError`, wherever it appears.
368
+ - Installing lispython no longer puts a `test` command (pytest's entry point)
369
+ on `PATH`.
370
+
371
+ ## [0.5.0]
372
+
373
+ ### Added
374
+
375
+ - Python 3.11 through 3.15 support, with each newer AST form gated on the
376
+ running interpreter and a clear compile error on older ones. A `nox` matrix
377
+ (`uvx nox -s tests`) runs the suite on every supported version.
378
+ - Type aliases: `(type NAME value)` compiles to a `type NAME = value`
379
+ statement (3.12+). The three-operand `(type name bases ns)` call is unchanged.
380
+ - Generic definitions: `def`, `async def` and `class` names accept glued type
381
+ parameters such as `f[T]`, including `ParamSpec`, `TypeVarTuple` and
382
+ defaults (parameters 3.12+, defaults 3.13+).
383
+ - t-strings compile to `TemplateStr` (3.14+).
384
+ - `lazy-import` and `lazy-from` (3.15+).
385
+ - Comprehension unpacking (3.15+), including `{**d for d dicts}`, which was
386
+ mistranslated as a dict display; unary plus is gated to 3.15+.
387
+ - Errors the CPython compiler raises on `.lpy` modules, and lispython's own
388
+ compile errors, are reported with the file, line and caret instead of a
389
+ traceback by `lpy` (file, `-m`, `-c`), `l2py`, the REPL and nREPL eval.
390
+ - LSP: `textDocument/signatureHelp` and `workspace/symbol`; `(type NAME ...)`
391
+ aliases are indexed as definitions.
392
+ - nREPL: completion responses carry type kinds, and the server writes
393
+ `.nrepl-port` in the working directory for editor auto-discovery.
394
+
395
+ ### Changed
396
+
397
+ - **The REPL and nREPL compile every statement of an input before running any
398
+ of it**, and `lpy -c` compiles its whole input first. A compile error,
399
+ lispython's or CPython's, now stops the input before any statement runs
400
+ (macro expansion still happens first); a `SyntaxError` the program raises
401
+ at run time keeps its own traceback.
402
+ - **Stricter parsing** rejects input that used to be accepted silently: an
403
+ unclosed final form or mismatched closing bracket, an unterminated string,
404
+ an f-string field holding more than one form, a lone `}` in an f-string,
405
+ non-symbol macro names (`(defmacro m[T] ...)`), `(import "x")`,
406
+ `(lazy-import "numpy")`, `(return a b)`, and `name[...]` glued under a
407
+ prefix such as `^list[int]` in a parameter list. Malformed literals such as
408
+ `09` or `\x4` are reported at their token.
409
+ - **Literal values**: `-1+2j` is now `(-1+2j)` rather than `-(1+2j)`; `\r\n`
410
+ and lone `\r` become `\n` in f-strings too; a multi-line raw string no
411
+ longer gains a literal `\n`; backslash-newline continues a string; a lone
412
+ `"` inside `"""..."""` no longer ends the token.
413
+ - **F-strings follow Python's rules.** An uppercase `F` prefix (and `rf`,
414
+ `fR`, ...) is a real f-string rather than being evaluated at compile time,
415
+ `{x=}` echoes the expression text and defaults to `!r`, and format specs
416
+ may nest replacement fields (`{x:>{w}.{p}}`). Unquotes expand inside
417
+ f-string and t-string fields.
418
+ - **`(require mod [])` registers no macros**, like an empty import list; only
419
+ an absent list (`(require mod)`) registers all of them. The LSP follows the
420
+ same rule in go-to-definition and hover.
421
+ - `lpy -m` puts the current directory on `sys.path` like `python -m`,
422
+ honouring `PYTHONSAFEPATH`.
423
+ - Requires `pygls` 2.0 or later.
424
+
425
+ ### Fixed
426
+
427
+ - `(return [])` compiled to a bare `return` (yielding `None`); it now returns
428
+ the empty list.
429
+ - `(case {})` accepts an empty mapping pattern.
430
+ - Signed literal match patterns: `(case -1 ...)` compiled to `case _(1):`.
431
+ - `(from . [a])` compiles; it raised `IndexError`.
432
+ - Quasiquoted f/t-string fields keep their conversion, spec and `=` text
433
+ (`f"{x!r:>6}"` used to come back as `{x}`), and `f"{~x}"` with a literal
434
+ macro argument no longer crashes.
435
+ - `l2py` emits valid Python for t-string interpolations.
436
+ - A lone starred subscript slice compiles to a one-element `Tuple`, like
437
+ Python's `t[*s]`.
438
+ - LSP: workspace symbols no longer include installed packages or function
439
+ locals, and report their real kinds; a buffer that is mid-edit keeps
440
+ working; signature help is not offered inside a quoted form.
441
+ - nREPL: completion kinds treat scope bindings as shadowing builtins, never
442
+ run user code to classify attributes, and report callables as functions;
443
+ an unwritable `.nrepl-port` no longer stops the server.
444
+
445
+ ## [0.4.7]
446
+
447
+ See the `v0.4.7` tag.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lispython
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Lisp-like Syntax for Python with Lisp-like Macros
5
5
  Project-URL: Homepage, https://jetack.github.io/lispython
6
6
  Project-URL: Repository, https://github.com/jetack/lispython
@@ -61,6 +61,8 @@ uv sync # install dependencies
61
61
  uv pip install -e . # for development
62
62
  ```
63
63
 
64
+ Contributors using git worktrees: run `git config core.hooksPath .githooks` once in the main checkout; see [`.githooks/README.md`](.githooks/README.md). This replaces `.git/hooks`, so hooks installed there stop running.
65
+
64
66
  ## How to Run LisPython code
65
67
  ### Run from source
66
68
  ```bash
@@ -31,6 +31,8 @@ uv sync # install dependencies
31
31
  uv pip install -e . # for development
32
32
  ```
33
33
 
34
+ Contributors using git worktrees: run `git config core.hooksPath .githooks` once in the main checkout; see [`.githooks/README.md`](.githooks/README.md). This replaces `.git/hooks`, so hooks installed there stop running.
35
+
34
36
  ## How to Run LisPython code
35
37
  ### Run from source
36
38
  ```bash
@@ -65,14 +65,15 @@ Unlike `->` (thread first) and `->>` (thread last), `as->` lets you name the thr
65
65
  (str x)) ;; x = "20"
66
66
  ```
67
67
 
68
- This expands to:
68
+ This expands to a single expression, a tuple of walrus assignments indexed with `-1`:
69
69
  ```python
70
- x = 0
71
- x = x + 10
72
- x = 2 * x
73
- x = str(x)
70
+ (__x_0 := 0, __x_0 := __x_0 + 10, __x_0 := 2 * __x_0, str(__x_0))[-1]
74
71
  ```
75
72
 
73
+ Because it is an expression, `as->` works anywhere: as a return value, a call argument, a lambda body, or nested. CPython refuses `:=` in some positions (e.g. a comprehension's iterable, annotations, type-alias values); there it is a loud compile error, so bind the value first.
74
+
75
+ The name is replaced only where it is read as a variable, not in keyword-argument names, quoted data, or the body of a `fn`/`lambda` that rebinds it.
76
+
76
77
  Useful when the threaded value doesn't always go in the first or last position.
77
78
 
78
79
  ## gensym
@@ -57,6 +57,9 @@ Python version
57
57
  (name := value)
58
58
  ```
59
59
 
60
+ ## Statements are not expressions
61
+ Statement forms (`do`, `=`, `+=`, `if`, `return`, `def`, ...) cannot appear inside an expression; the compiler reports an error. Use `(:= name value)` instead of `=`, `(ife test then else)` instead of `if`, and a tuple `(sub (, a b c) -1)` to sequence expressions.
62
+
60
63
  ## `lambda`
61
64
  LisPy version
62
65
  ```python
@@ -221,6 +224,12 @@ f"Hello, {2 * name}!"
221
224
  ```
222
225
  Replacement fields follow Python's rules: `{{`/`}}` are literal braces,
223
226
  `!r`/`!s`/`!a` conversions, format specs with nested fields, and `{x=}`.
227
+ A field is ordinary code, so a string in it is written with plain quotes as in
228
+ Python 3.12+ (`f"{(.get d "k")}"`), on every supported Python. The old
229
+ spelling with `\"` (`f"{(.get d \"k\")}"`) is deprecated: it still compiles,
230
+ with a `SyntaxWarning` (shown by default, as for an invalid escape sequence
231
+ in Python 3.12+), and a string opened with `\"` must be closed by
232
+ `\"`.
224
233
  LisPy version
225
234
  ```python
226
235
  f"{{literal}} {x!r:>{width}} {x=}"
@@ -365,11 +365,13 @@ match x:
365
365
  ## `raise`
366
366
  LisPy version
367
367
  ```python
368
+ (raise)
368
369
  (raise Exception)
369
370
  (raise Exception from ValueError)
370
371
  ```
371
372
  Python version
372
373
  ```python
374
+ raise
373
375
  raise Exception
374
376
  raise Exception from ValueError
375
377
  ```
@@ -1,5 +1,6 @@
1
1
  import tomllib
2
2
 
3
+
3
4
  def define_env(env):
4
5
  try:
5
6
  with open("pyproject.toml", "rb") as f:
@@ -8,4 +9,4 @@ def define_env(env):
8
9
  except Exception:
9
10
  version = "dev"
10
11
 
11
- env.variables["version"] = version
12
+ env.variables["version"] = version
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lispython"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Lisp-like Syntax for Python with Lisp-like Macros"
5
5
  authors = [{ name = "Jetack", email = "jetack23@gmail.com" }]
6
6
  license = { text = "MIT" }
@@ -28,7 +28,6 @@ Repository = "https://github.com/jetack/lispython"
28
28
  lpy = "lispy:run"
29
29
  l2py = "lispy:l2py"
30
30
  lpy-lsp = "lispy.lsp:main"
31
- test = "pytest:main"
32
31
 
33
32
  [project.optional-dependencies]
34
33
  dev = [
@@ -50,13 +49,34 @@ build-backend = "hatchling.build"
50
49
  [tool.hatch.build.targets.wheel]
51
50
  packages = ["src/lispy"]
52
51
 
52
+ # The sdist ships an explicit list, never "everything .gitignore allows":
53
+ # hatchling drops every .gitignore exclusion when the project root itself
54
+ # matches one, as any checkout under the ignored .claude/worktrees/ does, and
55
+ # the sdist then carried agent symlinks and .claude/settings.local.json.
56
+ [tool.hatch.build.targets.sdist]
57
+ include = [
58
+ "/src",
59
+ "/tests",
60
+ "/docs",
61
+ "/mkdocs.yml",
62
+ "/noxfile.py",
63
+ "/uv.lock",
64
+ "/README.md",
65
+ "/LICENSE.md",
66
+ "/CHANGELOG.md",
67
+ ]
68
+ exclude = ["__pycache__", "*.py[cod]"]
69
+
53
70
  [tool.pytest.ini_options]
54
71
  testpaths = ["tests"]
55
72
  python_files = ["test_*.py"]
56
73
  # AST constructor misuse (unknown keywords, missing fields) warns on 3.13/3.14
57
- # and is a TypeError from 3.15: fail the suite on the warning.
74
+ # and is a TypeError from 3.15: fail the suite on the warning. A SyntaxWarning
75
+ # (the deprecated \" in an f-string field, as CPython's invalid escape
76
+ # sequences) fails it too unless the test expects it.
58
77
  filterwarnings = [
59
78
  "error::DeprecationWarning",
79
+ "error::SyntaxWarning",
60
80
  # pygls (third-party) calls asyncio.iscoroutinefunction, deprecated in 3.14.
61
81
  "ignore:'asyncio.iscoroutinefunction' is deprecated:DeprecationWarning",
62
82
  ]
@@ -68,5 +88,22 @@ src = ["src"]
68
88
  [tool.ruff.lint]
69
89
  select = ["E", "F", "I", "UP"]
70
90
 
91
+ [tool.ruff.lint.per-file-ignores]
92
+ # Generated from core_meta_functions.lpy by l2py (tests/test_meta_functions.py checks
93
+ # it byte for byte), so its shape is the transpiler's, not ruff's.
94
+ "src/lispy/core/meta_functions.py" = ["E", "F", "I", "UP"]
95
+ # The compiler modules share one namespace on purpose (`from ... import *`).
96
+ "src/lispy/core/compiler/*.py" = ["F403", "F405"]
97
+ "src/lispy/core/macro.py" = ["F403", "F405"]
98
+ "src/lispy/core/parser.py" = ["F403", "F405"]
99
+ # Package __init__ imports run for their side effects or re-export.
100
+ "src/lispy/__init__.py" = ["F401"]
101
+ "src/lispy/lsp/__init__.py" = ["F401"]
102
+ "src/lispy/macros/__init__.py" = ["F401", "F403"]
103
+
104
+ [tool.ruff.format]
105
+ # Generated file (see above); the docs' fenced "python" blocks hold LisPython.
106
+ exclude = ["src/lispy/core/meta_functions.py", "docs/**/*.md"]
107
+
71
108
  [tool.ruff.lint.isort]
72
109
  known-first-party = ["lispy"]
@@ -1,4 +1,4 @@
1
1
  import sys
2
2
 
3
3
  import lispy.core.importer
4
- from lispy.tools import run, l2py
4
+ from lispy.tools import l2py, run