marple-lang 0.3.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.
- marple_lang-0.3.0/LICENSE +21 -0
- marple_lang-0.3.0/PKG-INFO +171 -0
- marple_lang-0.3.0/README.md +143 -0
- marple_lang-0.3.0/pyproject.toml +53 -0
- marple_lang-0.3.0/setup.cfg +10 -0
- marple_lang-0.3.0/src/marple/__init__.py +3 -0
- marple_lang-0.3.0/src/marple/arraymodel.py +33 -0
- marple_lang-0.3.0/src/marple/backend.py +49 -0
- marple_lang-0.3.0/src/marple/cells.py +110 -0
- marple_lang-0.3.0/src/marple/errors.py +66 -0
- marple_lang-0.3.0/src/marple/functions.py +213 -0
- marple_lang-0.3.0/src/marple/glyphs.py +85 -0
- marple_lang-0.3.0/src/marple/interpreter.py +1092 -0
- marple_lang-0.3.0/src/marple/namespace.py +66 -0
- marple_lang-0.3.0/src/marple/parser.py +502 -0
- marple_lang-0.3.0/src/marple/repl.py +219 -0
- marple_lang-0.3.0/src/marple/script.py +41 -0
- marple_lang-0.3.0/src/marple/stdlib/__init__.py +0 -0
- marple_lang-0.3.0/src/marple/stdlib/io/nread.apl +1 -0
- marple_lang-0.3.0/src/marple/stdlib/io/nwrite.apl +1 -0
- marple_lang-0.3.0/src/marple/stdlib/io_impl.py +23 -0
- marple_lang-0.3.0/src/marple/stdlib/str/lower.apl +1 -0
- marple_lang-0.3.0/src/marple/stdlib/str/trim.apl +1 -0
- marple_lang-0.3.0/src/marple/stdlib/str/upper.apl +1 -0
- marple_lang-0.3.0/src/marple/stdlib/str_impl.py +19 -0
- marple_lang-0.3.0/src/marple/structural.py +303 -0
- marple_lang-0.3.0/src/marple/terminal.py +102 -0
- marple_lang-0.3.0/src/marple/tokenizer.py +176 -0
- marple_lang-0.3.0/src/marple/web/__init__.py +0 -0
- marple_lang-0.3.0/src/marple/web/server.py +186 -0
- marple_lang-0.3.0/src/marple/web/static/desktop.html +107 -0
- marple_lang-0.3.0/src/marple/web/static/index.html +103 -0
- marple_lang-0.3.0/src/marple/workspace.py +151 -0
- marple_lang-0.3.0/src/marple_lang.egg-info/PKG-INFO +171 -0
- marple_lang-0.3.0/src/marple_lang.egg-info/SOURCES.txt +72 -0
- marple_lang-0.3.0/src/marple_lang.egg-info/dependency_links.txt +1 -0
- marple_lang-0.3.0/src/marple_lang.egg-info/entry_points.txt +2 -0
- marple_lang-0.3.0/src/marple_lang.egg-info/requires.txt +6 -0
- marple_lang-0.3.0/src/marple_lang.egg-info/top_level.txt +1 -0
- marple_lang-0.3.0/tests/test_arraymodel.py +52 -0
- marple_lang-0.3.0/tests/test_assignment.py +31 -0
- marple_lang-0.3.0/tests/test_backend.py +54 -0
- marple_lang-0.3.0/tests/test_cells.py +112 -0
- marple_lang-0.3.0/tests/test_ct.py +73 -0
- marple_lang-0.3.0/tests/test_dfns.py +84 -0
- marple_lang-0.3.0/tests/test_display.py +54 -0
- marple_lang-0.3.0/tests/test_ea.py +53 -0
- marple_lang-0.3.0/tests/test_extended_functions.py +116 -0
- marple_lang-0.3.0/tests/test_from.py +58 -0
- marple_lang-0.3.0/tests/test_functions.py +63 -0
- marple_lang-0.3.0/tests/test_glyphs.py +33 -0
- marple_lang-0.3.0/tests/test_ibeam.py +59 -0
- marple_lang-0.3.0/tests/test_indexing.py +75 -0
- marple_lang-0.3.0/tests/test_integration.py +42 -0
- marple_lang-0.3.0/tests/test_interpreter.py +92 -0
- marple_lang-0.3.0/tests/test_match.py +51 -0
- marple_lang-0.3.0/tests/test_matrices.py +71 -0
- marple_lang-0.3.0/tests/test_matrix_ops.py +35 -0
- marple_lang-0.3.0/tests/test_name_table.py +182 -0
- marple_lang-0.3.0/tests/test_namespaces.py +56 -0
- marple_lang-0.3.0/tests/test_operators.py +53 -0
- marple_lang-0.3.0/tests/test_parser.py +60 -0
- marple_lang-0.3.0/tests/test_phase4_remaining.py +57 -0
- marple_lang-0.3.0/tests/test_products.py +47 -0
- marple_lang-0.3.0/tests/test_random.py +100 -0
- marple_lang-0.3.0/tests/test_rank.py +93 -0
- marple_lang-0.3.0/tests/test_script.py +84 -0
- marple_lang-0.3.0/tests/test_statement_separator.py +13 -0
- marple_lang-0.3.0/tests/test_structural.py +97 -0
- marple_lang-0.3.0/tests/test_sysvar.py +115 -0
- marple_lang-0.3.0/tests/test_tokenizer.py +124 -0
- marple_lang-0.3.0/tests/test_workspace.py +99 -0
- marple_lang-0.3.0/tests/test_workspace_chars.py +28 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Romilly Cocking
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marple-lang
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Mini APL in Python Language Experiment. Uses APL arrays as the internal data model.
|
|
5
|
+
Author-email: Romilly Cocking <romilly.cocking@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/romilly/marple
|
|
8
|
+
Project-URL: Repository, https://github.com/romilly/marple.git
|
|
9
|
+
Project-URL: Issues, https://github.com/romilly/marple/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
25
|
+
Requires-Dist: pyright; extra == "test"
|
|
26
|
+
Requires-Dist: PyHamcrest; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# marple
|
|
30
|
+
|
|
31
|
+
Mini APL in Python Language Experiment. A first-generation APL interpreter with the rank operator, namespaces, and Python FFI. Uses APL arrays (shape + flat data) as the internal data model. Inspired by Rodrigo Girão Serrão's [RGSPL](https://github.com/rodrigogiraoserrano/RGSPL) and Iverson's [Dictionary of APL](https://www.jsoftware.com/papers/APLDictionary.htm).
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
More extensive documentation is available [here](https://romilly.github.io/marple/)
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **40+ primitive functions** — arithmetic, comparison, boolean, structural, circular/trig, match/tally, membership
|
|
40
|
+
- **Operators** — reduce (`/`), scan (`\`), inner product (`f.g`), outer product (`∘.f`), **rank** (`⍤`)
|
|
41
|
+
- **Rank operator** — `(f⍤k)` applies any function along any axis: `(⌽⍤1) M` reverses rows, `(+/⍤1) M` sums rows
|
|
42
|
+
- **From function** (`⌷`) — leading-axis selection that composes with rank
|
|
43
|
+
- **Direct functions (dfns)** — `{⍵}` syntax with guards, recursion via `∇`, default `⍺`
|
|
44
|
+
- **Symbol table-aware parser** — named functions work without parens: `double ⍳5`
|
|
45
|
+
- **Namespaces** — `$::str::upper 'hello'`, `#import` directives, `::` separator
|
|
46
|
+
- **I-beam operator** (`⌶`) — Python FFI for extending MARPLE with Python code
|
|
47
|
+
- **Error handling** — `⎕EA` (execute alternate), `⎕EN` (error number), `⎕DM` (diagnostic message), `⎕SIGNAL`
|
|
48
|
+
- **System variables** — `⎕IO`, `⎕CT`, `⎕PP`, `⎕RL`, `⎕A`, `⎕D`, `⎕TS`, `⎕WSID`, `⎕UCS`, `⎕NC`, `⎕EX`
|
|
49
|
+
- **Matrices** — reshape, transpose, bracket indexing (`M[r;c]` any rank), matrix inverse (`⌹`)
|
|
50
|
+
- **Numpy backend** — automatic vectorization (73x faster for element-wise, 380x for outer product), with pure-Python fallback
|
|
51
|
+
- **Web REPL** — browser-based REPL at `http://localhost:8888/`, Pico W-ready
|
|
52
|
+
- **Terminal REPL** — live backtick→glyph input, workspace save/load, APL-style formatting
|
|
53
|
+
- **Script runner** — `marple script.marple` with session transcript output
|
|
54
|
+
- **426 tests**, pyright strict, no external runtime dependencies
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install -e .
|
|
60
|
+
marple
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
MARPLE v0.2.11 - Mini APL in Python
|
|
65
|
+
CLEAR WS
|
|
66
|
+
|
|
67
|
+
⍳5
|
|
68
|
+
1 2 3 4 5
|
|
69
|
+
+/⍳100
|
|
70
|
+
5050
|
|
71
|
+
fact←{⍵≤1:1⋄⍵×∇ ⍵-1}
|
|
72
|
+
fact 10
|
|
73
|
+
3628800
|
|
74
|
+
double←{⍵+⍵}
|
|
75
|
+
double ⍳5
|
|
76
|
+
2 4 6 8 10
|
|
77
|
+
M←3 4⍴⍳12
|
|
78
|
+
(⌽⍤1) M
|
|
79
|
+
4 3 2 1
|
|
80
|
+
8 7 6 5
|
|
81
|
+
12 11 10 9
|
|
82
|
+
$::str::upper 'hello'
|
|
83
|
+
HELLO
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Running scripts
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
marple examples/01_primitives.marple # run and display
|
|
90
|
+
marple examples/01_primitives.marple > out.txt # capture session transcript
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Four demo scripts are included in `examples/`:
|
|
94
|
+
- `01_primitives.marple` — arithmetic, vectors, matrices, reduce, products
|
|
95
|
+
- `02_dfns.marple` — user functions, guards, recursion, rank operator
|
|
96
|
+
- `03_namespaces.marple` — system library, imports, file I/O, i-beams
|
|
97
|
+
- `04_errors.marple` — ea/en error handling, error codes
|
|
98
|
+
|
|
99
|
+
### APL character input
|
|
100
|
+
|
|
101
|
+
If you have a Dyalog APL keyboard layout installed (e.g. via `setxkbmap` with `grp:win_switch`), you can use the Win key to type APL glyphs directly.
|
|
102
|
+
|
|
103
|
+
Alternatively, type APL glyphs using backtick prefixes — they appear immediately as you type:
|
|
104
|
+
|
|
105
|
+
| Key | Glyph | Key | Glyph | Key | Glyph | Key | Glyph |
|
|
106
|
+
|-----|-------|-----|-------|-----|-------|-----|-------|
|
|
107
|
+
| `` `r `` | ⍴ | `` `i `` | ⍳ | `` `l `` | ← | `` `w `` | ⍵ |
|
|
108
|
+
| `` `a `` | ⍺ | `` `V `` | ∇ | `` `x `` | ⋄ | `` `c `` | ⍝ |
|
|
109
|
+
| `` `- `` | × | `` `= `` | ÷ | `` `< `` | ≤ | `` `> `` | ≥ |
|
|
110
|
+
| `` `/ `` | ≠ | `` `o `` | ○ | `` `* `` | ⍟ | `` `2 `` | ¯ |
|
|
111
|
+
| `` `q `` | ⌽ | `` `Q `` | ⍉ | `` `g `` | ⍋ | `` `G `` | ⍒ |
|
|
112
|
+
| `` `t `` | ↑ | `` `y `` | ↓ | `` `n `` | ⊤ | `` `N `` | ⊥ |
|
|
113
|
+
| `` `J `` | ⍤ | `` `I `` | ⌷ | `` `j `` | ∘ | `` `D `` | ⌹ |
|
|
114
|
+
| `` `B `` | ⌶ | | | | | | |
|
|
115
|
+
|
|
116
|
+
### System commands
|
|
117
|
+
|
|
118
|
+
| Command | Action |
|
|
119
|
+
|---------|--------|
|
|
120
|
+
| `)off` | Exit |
|
|
121
|
+
| `)clear` | Clear workspace |
|
|
122
|
+
| `)wsid [name]` | Show or set workspace ID |
|
|
123
|
+
| `)save [name]` | Save workspace (sets WSID if name given) |
|
|
124
|
+
| `)load name` | Load workspace |
|
|
125
|
+
| `)lib` | List saved workspaces |
|
|
126
|
+
| `)fns [ns]` | List defined functions (optionally in namespace) |
|
|
127
|
+
| `)vars` | List defined variables |
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install -e .[test]
|
|
133
|
+
pytest
|
|
134
|
+
pyright src/
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
To run without numpy (pure-Python mode):
|
|
138
|
+
```bash
|
|
139
|
+
MARPLE_BACKEND=none pytest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Architecture
|
|
143
|
+
|
|
144
|
+
| Module | Purpose |
|
|
145
|
+
|--------|---------|
|
|
146
|
+
| `arraymodel.py` | `APLArray(shape, data)` — the core data structure |
|
|
147
|
+
| `backend.py` | Numpy/CuPy/ulab detection with pure-Python fallback |
|
|
148
|
+
| `tokenizer.py` | Lexer for APL glyphs, numbers, strings, qualified names |
|
|
149
|
+
| `parser.py` | Right-to-left recursive descent with symbol table |
|
|
150
|
+
| `interpreter.py` | Tree-walking evaluator with dfn closures |
|
|
151
|
+
| `functions.py` | Scalar functions with pervasion (numpy-accelerated) |
|
|
152
|
+
| `structural.py` | Shape-manipulating and indexing functions |
|
|
153
|
+
| `cells.py` | Cell decomposition and reassembly for the rank operator |
|
|
154
|
+
| `namespace.py` | Hierarchical namespace resolution and system workspace |
|
|
155
|
+
| `errors.py` | APL error classes with numeric codes |
|
|
156
|
+
| `repl.py` | Interactive read-eval-print loop |
|
|
157
|
+
| `script.py` | Script runner with session transcript output |
|
|
158
|
+
| `terminal.py` | Raw terminal input with live glyph translation |
|
|
159
|
+
| `glyphs.py` | Backtick → APL character mapping |
|
|
160
|
+
| `workspace.py` | Directory-based workspace persistence |
|
|
161
|
+
| `stdlib/` | Standard library: string, I/O, and error handling |
|
|
162
|
+
|
|
163
|
+
## References
|
|
164
|
+
|
|
165
|
+
- [RGSPL](https://github.com/rodrigogiraoserrano/RGSPL) — Rodrigo Girão Serrão's Python APL interpreter (design reference)
|
|
166
|
+
- [RGSPL blog series](https://mathspp.com/blog/lsbasi-apl-part1) — step-by-step interpreter build
|
|
167
|
+
- [Iverson's Dictionary of APL](https://www.jsoftware.com/papers/APLDictionary.htm) — the rank operator and leading-axis theory
|
|
168
|
+
- [Language spec](docs/MARPLE_Language_Reference.md) — full first-generation APL reference and roadmap
|
|
169
|
+
- [Rank operator spec](docs/MARPLE_Rank_Operator.md) — detailed rank operator design
|
|
170
|
+
- [Indexing spec](docs/MARPLE_Indexing.md) — From function and indexing approach
|
|
171
|
+
- [Namespaces spec](docs/MARPLE_Namespaces_And_IBeams.md) — namespaces, i-beams, and standard library
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# marple
|
|
2
|
+
|
|
3
|
+
Mini APL in Python Language Experiment. A first-generation APL interpreter with the rank operator, namespaces, and Python FFI. Uses APL arrays (shape + flat data) as the internal data model. Inspired by Rodrigo Girão Serrão's [RGSPL](https://github.com/rodrigogiraoserrano/RGSPL) and Iverson's [Dictionary of APL](https://www.jsoftware.com/papers/APLDictionary.htm).
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
More extensive documentation is available [here](https://romilly.github.io/marple/)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **40+ primitive functions** — arithmetic, comparison, boolean, structural, circular/trig, match/tally, membership
|
|
12
|
+
- **Operators** — reduce (`/`), scan (`\`), inner product (`f.g`), outer product (`∘.f`), **rank** (`⍤`)
|
|
13
|
+
- **Rank operator** — `(f⍤k)` applies any function along any axis: `(⌽⍤1) M` reverses rows, `(+/⍤1) M` sums rows
|
|
14
|
+
- **From function** (`⌷`) — leading-axis selection that composes with rank
|
|
15
|
+
- **Direct functions (dfns)** — `{⍵}` syntax with guards, recursion via `∇`, default `⍺`
|
|
16
|
+
- **Symbol table-aware parser** — named functions work without parens: `double ⍳5`
|
|
17
|
+
- **Namespaces** — `$::str::upper 'hello'`, `#import` directives, `::` separator
|
|
18
|
+
- **I-beam operator** (`⌶`) — Python FFI for extending MARPLE with Python code
|
|
19
|
+
- **Error handling** — `⎕EA` (execute alternate), `⎕EN` (error number), `⎕DM` (diagnostic message), `⎕SIGNAL`
|
|
20
|
+
- **System variables** — `⎕IO`, `⎕CT`, `⎕PP`, `⎕RL`, `⎕A`, `⎕D`, `⎕TS`, `⎕WSID`, `⎕UCS`, `⎕NC`, `⎕EX`
|
|
21
|
+
- **Matrices** — reshape, transpose, bracket indexing (`M[r;c]` any rank), matrix inverse (`⌹`)
|
|
22
|
+
- **Numpy backend** — automatic vectorization (73x faster for element-wise, 380x for outer product), with pure-Python fallback
|
|
23
|
+
- **Web REPL** — browser-based REPL at `http://localhost:8888/`, Pico W-ready
|
|
24
|
+
- **Terminal REPL** — live backtick→glyph input, workspace save/load, APL-style formatting
|
|
25
|
+
- **Script runner** — `marple script.marple` with session transcript output
|
|
26
|
+
- **426 tests**, pyright strict, no external runtime dependencies
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e .
|
|
32
|
+
marple
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
MARPLE v0.2.11 - Mini APL in Python
|
|
37
|
+
CLEAR WS
|
|
38
|
+
|
|
39
|
+
⍳5
|
|
40
|
+
1 2 3 4 5
|
|
41
|
+
+/⍳100
|
|
42
|
+
5050
|
|
43
|
+
fact←{⍵≤1:1⋄⍵×∇ ⍵-1}
|
|
44
|
+
fact 10
|
|
45
|
+
3628800
|
|
46
|
+
double←{⍵+⍵}
|
|
47
|
+
double ⍳5
|
|
48
|
+
2 4 6 8 10
|
|
49
|
+
M←3 4⍴⍳12
|
|
50
|
+
(⌽⍤1) M
|
|
51
|
+
4 3 2 1
|
|
52
|
+
8 7 6 5
|
|
53
|
+
12 11 10 9
|
|
54
|
+
$::str::upper 'hello'
|
|
55
|
+
HELLO
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Running scripts
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
marple examples/01_primitives.marple # run and display
|
|
62
|
+
marple examples/01_primitives.marple > out.txt # capture session transcript
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Four demo scripts are included in `examples/`:
|
|
66
|
+
- `01_primitives.marple` — arithmetic, vectors, matrices, reduce, products
|
|
67
|
+
- `02_dfns.marple` — user functions, guards, recursion, rank operator
|
|
68
|
+
- `03_namespaces.marple` — system library, imports, file I/O, i-beams
|
|
69
|
+
- `04_errors.marple` — ea/en error handling, error codes
|
|
70
|
+
|
|
71
|
+
### APL character input
|
|
72
|
+
|
|
73
|
+
If you have a Dyalog APL keyboard layout installed (e.g. via `setxkbmap` with `grp:win_switch`), you can use the Win key to type APL glyphs directly.
|
|
74
|
+
|
|
75
|
+
Alternatively, type APL glyphs using backtick prefixes — they appear immediately as you type:
|
|
76
|
+
|
|
77
|
+
| Key | Glyph | Key | Glyph | Key | Glyph | Key | Glyph |
|
|
78
|
+
|-----|-------|-----|-------|-----|-------|-----|-------|
|
|
79
|
+
| `` `r `` | ⍴ | `` `i `` | ⍳ | `` `l `` | ← | `` `w `` | ⍵ |
|
|
80
|
+
| `` `a `` | ⍺ | `` `V `` | ∇ | `` `x `` | ⋄ | `` `c `` | ⍝ |
|
|
81
|
+
| `` `- `` | × | `` `= `` | ÷ | `` `< `` | ≤ | `` `> `` | ≥ |
|
|
82
|
+
| `` `/ `` | ≠ | `` `o `` | ○ | `` `* `` | ⍟ | `` `2 `` | ¯ |
|
|
83
|
+
| `` `q `` | ⌽ | `` `Q `` | ⍉ | `` `g `` | ⍋ | `` `G `` | ⍒ |
|
|
84
|
+
| `` `t `` | ↑ | `` `y `` | ↓ | `` `n `` | ⊤ | `` `N `` | ⊥ |
|
|
85
|
+
| `` `J `` | ⍤ | `` `I `` | ⌷ | `` `j `` | ∘ | `` `D `` | ⌹ |
|
|
86
|
+
| `` `B `` | ⌶ | | | | | | |
|
|
87
|
+
|
|
88
|
+
### System commands
|
|
89
|
+
|
|
90
|
+
| Command | Action |
|
|
91
|
+
|---------|--------|
|
|
92
|
+
| `)off` | Exit |
|
|
93
|
+
| `)clear` | Clear workspace |
|
|
94
|
+
| `)wsid [name]` | Show or set workspace ID |
|
|
95
|
+
| `)save [name]` | Save workspace (sets WSID if name given) |
|
|
96
|
+
| `)load name` | Load workspace |
|
|
97
|
+
| `)lib` | List saved workspaces |
|
|
98
|
+
| `)fns [ns]` | List defined functions (optionally in namespace) |
|
|
99
|
+
| `)vars` | List defined variables |
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install -e .[test]
|
|
105
|
+
pytest
|
|
106
|
+
pyright src/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
To run without numpy (pure-Python mode):
|
|
110
|
+
```bash
|
|
111
|
+
MARPLE_BACKEND=none pytest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Architecture
|
|
115
|
+
|
|
116
|
+
| Module | Purpose |
|
|
117
|
+
|--------|---------|
|
|
118
|
+
| `arraymodel.py` | `APLArray(shape, data)` — the core data structure |
|
|
119
|
+
| `backend.py` | Numpy/CuPy/ulab detection with pure-Python fallback |
|
|
120
|
+
| `tokenizer.py` | Lexer for APL glyphs, numbers, strings, qualified names |
|
|
121
|
+
| `parser.py` | Right-to-left recursive descent with symbol table |
|
|
122
|
+
| `interpreter.py` | Tree-walking evaluator with dfn closures |
|
|
123
|
+
| `functions.py` | Scalar functions with pervasion (numpy-accelerated) |
|
|
124
|
+
| `structural.py` | Shape-manipulating and indexing functions |
|
|
125
|
+
| `cells.py` | Cell decomposition and reassembly for the rank operator |
|
|
126
|
+
| `namespace.py` | Hierarchical namespace resolution and system workspace |
|
|
127
|
+
| `errors.py` | APL error classes with numeric codes |
|
|
128
|
+
| `repl.py` | Interactive read-eval-print loop |
|
|
129
|
+
| `script.py` | Script runner with session transcript output |
|
|
130
|
+
| `terminal.py` | Raw terminal input with live glyph translation |
|
|
131
|
+
| `glyphs.py` | Backtick → APL character mapping |
|
|
132
|
+
| `workspace.py` | Directory-based workspace persistence |
|
|
133
|
+
| `stdlib/` | Standard library: string, I/O, and error handling |
|
|
134
|
+
|
|
135
|
+
## References
|
|
136
|
+
|
|
137
|
+
- [RGSPL](https://github.com/rodrigogiraoserrano/RGSPL) — Rodrigo Girão Serrão's Python APL interpreter (design reference)
|
|
138
|
+
- [RGSPL blog series](https://mathspp.com/blog/lsbasi-apl-part1) — step-by-step interpreter build
|
|
139
|
+
- [Iverson's Dictionary of APL](https://www.jsoftware.com/papers/APLDictionary.htm) — the rank operator and leading-axis theory
|
|
140
|
+
- [Language spec](docs/MARPLE_Language_Reference.md) — full first-generation APL reference and roadmap
|
|
141
|
+
- [Rank operator spec](docs/MARPLE_Rank_Operator.md) — detailed rank operator design
|
|
142
|
+
- [Indexing spec](docs/MARPLE_Indexing.md) — From function and indexing approach
|
|
143
|
+
- [Namespaces spec](docs/MARPLE_Namespaces_And_IBeams.md) — namespaces, i-beams, and standard library
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "marple-lang"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Mini APL in Python Language Experiment. Uses APL arrays as the internal data model."
|
|
9
|
+
authors = [{name = "Romilly Cocking", email = "romilly.cocking@gmail.com"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
marple = "marple.repl:main"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = [
|
|
31
|
+
"pytest>=7.0.0",
|
|
32
|
+
"pytest-cov",
|
|
33
|
+
"pyright",
|
|
34
|
+
"PyHamcrest",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/romilly/marple"
|
|
39
|
+
Repository = "https://github.com/romilly/marple.git"
|
|
40
|
+
Issues = "https://github.com/romilly/marple/issues"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
marple = ["web/static/*.html", "stdlib/**/*.apl"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
python_files = ["test_*.py"]
|
|
51
|
+
python_classes = ["Test*"]
|
|
52
|
+
python_functions = ["test_*"]
|
|
53
|
+
addopts = "-v --tb=short"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from marple.backend import is_numeric_array, np, to_array, to_list
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class APLArray:
|
|
9
|
+
def __init__(self, shape: list[int], data: Any) -> None:
|
|
10
|
+
self.shape = shape
|
|
11
|
+
self.data = to_array(data) if isinstance(data, list) else data
|
|
12
|
+
|
|
13
|
+
def is_scalar(self) -> bool:
|
|
14
|
+
return self.shape == []
|
|
15
|
+
|
|
16
|
+
def __eq__(self, other: object) -> bool:
|
|
17
|
+
if not isinstance(other, APLArray):
|
|
18
|
+
return NotImplemented
|
|
19
|
+
if self.shape != other.shape:
|
|
20
|
+
return False
|
|
21
|
+
if is_numeric_array(self.data) and is_numeric_array(other.data):
|
|
22
|
+
return bool(np.array_equal(self.data, other.data))
|
|
23
|
+
return to_list(self.data) == to_list(other.data)
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
if self.is_scalar():
|
|
27
|
+
return f"S({self.data[0]})"
|
|
28
|
+
data_list = to_list(self.data) if is_numeric_array(self.data) else self.data
|
|
29
|
+
return f"APLArray({self.shape}, {data_list})"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def S(value: Any) -> APLArray:
|
|
33
|
+
return APLArray([], [value])
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# Backend selection: environment variable overrides auto-detection
|
|
7
|
+
_backend_name = os.environ.get("MARPLE_BACKEND", "auto")
|
|
8
|
+
|
|
9
|
+
np: Any = None
|
|
10
|
+
|
|
11
|
+
if _backend_name != "none":
|
|
12
|
+
try:
|
|
13
|
+
import cupy as np # type: ignore[no-redef]
|
|
14
|
+
except ImportError:
|
|
15
|
+
try:
|
|
16
|
+
import numpy as np # type: ignore[no-redef]
|
|
17
|
+
except ImportError:
|
|
18
|
+
try:
|
|
19
|
+
import ulab.numpy as np # type: ignore[no-redef,import-not-found]
|
|
20
|
+
except ImportError:
|
|
21
|
+
try:
|
|
22
|
+
from ulab import numpy as np # type: ignore[no-redef,import-not-found]
|
|
23
|
+
except ImportError:
|
|
24
|
+
np = None
|
|
25
|
+
|
|
26
|
+
HAS_BACKEND: bool = np is not None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def to_array(data: list[Any]) -> Any:
|
|
30
|
+
"""Convert a Python list to an ndarray if numeric and backend is available."""
|
|
31
|
+
if not HAS_BACKEND or len(data) == 0:
|
|
32
|
+
return data
|
|
33
|
+
if not all(isinstance(x, (int, float)) for x in data):
|
|
34
|
+
return data
|
|
35
|
+
return np.array(data)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def to_list(data: Any) -> list[Any]:
|
|
39
|
+
"""Convert an ndarray to a Python list. Pass lists through unchanged."""
|
|
40
|
+
if isinstance(data, list):
|
|
41
|
+
return data
|
|
42
|
+
return data.tolist() # type: ignore[union-attr]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_numeric_array(data: Any) -> bool:
|
|
46
|
+
"""Check if data is an ndarray from the active backend."""
|
|
47
|
+
if not HAS_BACKEND:
|
|
48
|
+
return False
|
|
49
|
+
return hasattr(data, "dtype")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from marple.arraymodel import APLArray, S
|
|
4
|
+
from marple.backend import to_list
|
|
5
|
+
from marple.errors import LengthError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_rank_spec(spec: APLArray) -> tuple[int, int, int]:
|
|
9
|
+
"""Resolve rank spec to (monadic, left_dyadic, right_dyadic).
|
|
10
|
+
|
|
11
|
+
Scalar c → (c, c, c)
|
|
12
|
+
2-vector b c → (c, b, c)
|
|
13
|
+
3-vector a b c → (a, b, c)
|
|
14
|
+
"""
|
|
15
|
+
data = to_list(spec.data)
|
|
16
|
+
if spec.is_scalar():
|
|
17
|
+
c = int(data[0])
|
|
18
|
+
return (c, c, c)
|
|
19
|
+
if len(data) == 2:
|
|
20
|
+
b, c = int(data[0]), int(data[1])
|
|
21
|
+
return (c, b, c)
|
|
22
|
+
if len(data) == 3:
|
|
23
|
+
a, b, c = int(data[0]), int(data[1]), int(data[2])
|
|
24
|
+
return (a, b, c)
|
|
25
|
+
raise LengthError(f"Rank spec must be 1, 2, or 3 elements, got {len(data)}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def clamp_rank(k: int, r: int) -> int:
|
|
29
|
+
"""Clamp cell rank k to [0, r]. Handle negative (complementary) rank."""
|
|
30
|
+
if k < 0:
|
|
31
|
+
k = r + k
|
|
32
|
+
return max(0, min(k, r))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decompose(array: APLArray, cell_rank: int) -> tuple[list[int], list[APLArray]]:
|
|
36
|
+
"""Decompose array into cells of the given rank.
|
|
37
|
+
|
|
38
|
+
Returns (frame_shape, cells) where frame_shape is the leading axes
|
|
39
|
+
and cells is a list of APLArray objects in row-major frame order.
|
|
40
|
+
"""
|
|
41
|
+
r = len(array.shape)
|
|
42
|
+
k = clamp_rank(cell_rank, r)
|
|
43
|
+
frame_shape = array.shape[:r - k] if k < r else []
|
|
44
|
+
cell_shape = array.shape[r - k:] if k > 0 else []
|
|
45
|
+
cell_size = 1
|
|
46
|
+
for s in cell_shape:
|
|
47
|
+
cell_size *= s
|
|
48
|
+
if cell_size == 0:
|
|
49
|
+
cell_size = 1
|
|
50
|
+
data = to_list(array.data)
|
|
51
|
+
n_cells = 1
|
|
52
|
+
for s in frame_shape:
|
|
53
|
+
n_cells *= s
|
|
54
|
+
if n_cells == 0:
|
|
55
|
+
n_cells = 1
|
|
56
|
+
cells: list[APLArray] = []
|
|
57
|
+
for i in range(n_cells):
|
|
58
|
+
cell_data = data[i * cell_size : (i + 1) * cell_size]
|
|
59
|
+
cells.append(APLArray(list(cell_shape), cell_data))
|
|
60
|
+
return (list(frame_shape), cells)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reassemble(frame_shape: list[int], cells: list[APLArray]) -> APLArray:
|
|
64
|
+
"""Reassemble cells into a single array.
|
|
65
|
+
|
|
66
|
+
If all cells have the same shape, result shape is frame_shape + cell_shape.
|
|
67
|
+
If shapes differ, pad with fill elements to max shape.
|
|
68
|
+
"""
|
|
69
|
+
if len(cells) == 0:
|
|
70
|
+
return APLArray(frame_shape + [0], [])
|
|
71
|
+
if len(cells) == 1 and frame_shape == []:
|
|
72
|
+
return cells[0]
|
|
73
|
+
# Determine max cell shape
|
|
74
|
+
max_rank = max(len(c.shape) for c in cells)
|
|
75
|
+
max_shape: list[int] = [0] * max_rank
|
|
76
|
+
for c in cells:
|
|
77
|
+
# Pad cell shape on the left with 1s to match max_rank
|
|
78
|
+
padded = [1] * (max_rank - len(c.shape)) + c.shape
|
|
79
|
+
for j in range(max_rank):
|
|
80
|
+
max_shape[j] = max(max_shape[j], padded[j])
|
|
81
|
+
# Check if all cells match max_shape (no padding needed)
|
|
82
|
+
all_uniform = all(c.shape == max_shape for c in cells)
|
|
83
|
+
if all_uniform:
|
|
84
|
+
all_data: list[object] = []
|
|
85
|
+
for c in cells:
|
|
86
|
+
all_data.extend(to_list(c.data))
|
|
87
|
+
return APLArray(frame_shape + max_shape, all_data)
|
|
88
|
+
# Padding needed
|
|
89
|
+
max_size = 1
|
|
90
|
+
for s in max_shape:
|
|
91
|
+
max_size *= s
|
|
92
|
+
# Determine fill value
|
|
93
|
+
is_char = any(
|
|
94
|
+
len(c.data) > 0 and isinstance(to_list(c.data)[0], str)
|
|
95
|
+
for c in cells
|
|
96
|
+
)
|
|
97
|
+
fill = " " if is_char else 0
|
|
98
|
+
all_data = []
|
|
99
|
+
for c in cells:
|
|
100
|
+
cell_data = to_list(c.data)
|
|
101
|
+
if c.shape == max_shape:
|
|
102
|
+
all_data.extend(cell_data)
|
|
103
|
+
else:
|
|
104
|
+
# Pad: embed cell data in a max_shape-sized block
|
|
105
|
+
padded_cell: list[object] = [fill] * max_size
|
|
106
|
+
# Simple case: 1D padding
|
|
107
|
+
for j, val in enumerate(cell_data):
|
|
108
|
+
padded_cell[j] = val
|
|
109
|
+
all_data.extend(padded_cell)
|
|
110
|
+
return APLArray(frame_shape + max_shape, all_data)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class APLError(Exception):
|
|
5
|
+
"""Base class for APL errors."""
|
|
6
|
+
code: int = 0
|
|
7
|
+
name: str = "ERROR"
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str = "") -> None:
|
|
10
|
+
self.message = message
|
|
11
|
+
super().__init__(f"{self.name}: {message}" if message else self.name)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SyntaxError_(APLError):
|
|
15
|
+
code = 1
|
|
16
|
+
name = "SYNTAX ERROR"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ValueError_(APLError):
|
|
20
|
+
code = 2
|
|
21
|
+
name = "VALUE ERROR"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DomainError(APLError):
|
|
25
|
+
code = 3
|
|
26
|
+
name = "DOMAIN ERROR"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LengthError(APLError):
|
|
30
|
+
code = 4
|
|
31
|
+
name = "LENGTH ERROR"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RankError(APLError):
|
|
35
|
+
code = 5
|
|
36
|
+
name = "RANK ERROR"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class IndexError_(APLError):
|
|
40
|
+
code = 6
|
|
41
|
+
name = "INDEX ERROR"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LimitError(APLError):
|
|
45
|
+
code = 7
|
|
46
|
+
name = "LIMIT ERROR"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WSFullError(APLError):
|
|
50
|
+
code = 8
|
|
51
|
+
name = "WS FULL"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SecurityError(APLError):
|
|
55
|
+
code = 9
|
|
56
|
+
name = "SECURITY ERROR"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DependencyError(APLError):
|
|
60
|
+
code = 10
|
|
61
|
+
name = "DEPENDENCY ERROR"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ClassError(APLError):
|
|
65
|
+
code = 11
|
|
66
|
+
name = "CLASS ERROR"
|