joyfl 0.4__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.
- joyfl-0.4/PKG-INFO +81 -0
- joyfl-0.4/README.rst +70 -0
- joyfl-0.4/pyproject.toml +22 -0
- joyfl-0.4/src/joyfl/__init__.py +3 -0
- joyfl-0.4/src/joyfl/__main__.py +124 -0
- joyfl-0.4/src/joyfl/api.py +10 -0
- joyfl-0.4/src/joyfl/builtins.py +44 -0
- joyfl-0.4/src/joyfl/combinators.py +58 -0
- joyfl-0.4/src/joyfl/errors.py +40 -0
- joyfl-0.4/src/joyfl/formatting.py +62 -0
- joyfl-0.4/src/joyfl/interpreter.py +123 -0
- joyfl-0.4/src/joyfl/library.py +84 -0
- joyfl-0.4/src/joyfl/libs/stdlib.joy +270 -0
- joyfl-0.4/src/joyfl/linker.py +56 -0
- joyfl-0.4/src/joyfl/loader.py +85 -0
- joyfl-0.4/src/joyfl/operators.py +85 -0
- joyfl-0.4/src/joyfl/parser.py +144 -0
- joyfl-0.4/src/joyfl/runtime.py +101 -0
- joyfl-0.4/src/joyfl/types.py +61 -0
joyfl-0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: joyfl
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: A minimal but elegant dialect of Joy, a functional / concatenative stack language.
|
|
5
|
+
Requires-Dist: click
|
|
6
|
+
Requires-Dist: lark
|
|
7
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Description-Content-Type: text/x-rst
|
|
11
|
+
|
|
12
|
+
``joyfl`` (pronounced *ˈjȯi-fəl*) is a dialect of the programming language Joy.
|
|
13
|
+
|
|
14
|
+
Joy is a stack-based programming environment that's both functional and concatenative, which results in highly expressive but short programs. ``joyfl`` is an implementation of Joy in Python in its early stages; it's not entirely backwards compatible by design.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EXAMPLES
|
|
18
|
+
========
|
|
19
|
+
|
|
20
|
+
You can run a simple REPL (read-eval-print loop) by typing: ``python3 joyfl.py repl``. From there, try typing these statements:
|
|
21
|
+
|
|
22
|
+
.. code-block:: bash
|
|
23
|
+
|
|
24
|
+
# Take the number one, the number two, add them. Then the number three and add it to the
|
|
25
|
+
# previous result. Is six equal to that?
|
|
26
|
+
1 2 + 3 +
|
|
27
|
+
6 equal? .
|
|
28
|
+
>>> true
|
|
29
|
+
|
|
30
|
+
# Take the list of numbers seven eight nine. Take a program that subtracts one. Map the
|
|
31
|
+
# program onto the list, then reverse it.
|
|
32
|
+
[7 8 9] [1 -] map reverse .
|
|
33
|
+
>>> [8 7 6]
|
|
34
|
+
|
|
35
|
+
# Take a list of symbols 'a 'b 'c. Take the symbol 'd. Swap the symbol with the list. Get
|
|
36
|
+
# the "rest" of the list omitting the first item. Construct a new list with "cons" that
|
|
37
|
+
# uses 'd as the new head.
|
|
38
|
+
['a 'b 'c] 'd swap rest cons .
|
|
39
|
+
>>> ['d 'b 'c]
|
|
40
|
+
|
|
41
|
+
Also look at the ``#/examples/`` folder and run them with ``python3 joyfl.py <filename>``.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
MOTIVATION
|
|
45
|
+
==========
|
|
46
|
+
|
|
47
|
+
While it's fun to implement languages, this project has particular #ML / #LLM research questions in mind...
|
|
48
|
+
|
|
49
|
+
**“ What if there was a language for a 'micro-controller' able to process 99% of tokens? ”**
|
|
50
|
+
|
|
51
|
+
📥 TRAINING: To produce training data, what's the middle ground between a web-template (gasp!) and a synthetic theorem generator (whew!)? The answer looks more like another language than a hacked-together Python script.
|
|
52
|
+
|
|
53
|
+
📤 INFERENCE: For output tokens, how can we make sure prompts are followed, any arithmetic is correct, no items are missed, and formatting is right? The solution isn't more Python but special tokens that can be interpreted as instructions...
|
|
54
|
+
|
|
55
|
+
The research goal of this project is to find out where and how Joy can shine in these cases!
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
DIFFERENCES
|
|
59
|
+
===========
|
|
60
|
+
|
|
61
|
+
While some of the tests from the original Joy pass, many also do not. Here are the design decisions at play:
|
|
62
|
+
|
|
63
|
+
1. **Symbols vs. Characters** — Individual byte characters are not supported, so it's not possible to extract or combine them with strings. Instead, the single quote denotes symbols (e.g. ``'alpha``) that can only be compared and added to containers.
|
|
64
|
+
|
|
65
|
+
2. **Data-Structures** — Sets are not implemented yet, but will be. When they are, the sets will have the functionality of the underlying Python sets. Lists too behave like Python lists. Dictionaries will likely follow too at some point.
|
|
66
|
+
|
|
67
|
+
3. **Conditionals** — Functions that return booleans are encouraged to use the ``?`` suffix, for example ``equal?`` or ``list?``. This change is inspired by Factor, and makes the code more readable so you know when to expect a boolean.
|
|
68
|
+
|
|
69
|
+
4. **Stackless** - The interpreter does not use the Python callstack: state is stored entirely in data-structures. There's a stack (for data created in the past) and a queue (for code to be executed in the future). Certain advanced combinators may feel a bit different to write because of this!
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
REFERENCES
|
|
73
|
+
==========
|
|
74
|
+
|
|
75
|
+
* The `official documentation <https://hypercubed.github.io/joy/joy.html>`__ for Joy by Manfred van Thun.
|
|
76
|
+
|
|
77
|
+
* The `various C implementations <https://github.com/Wodan58>`__ (joy0, joy1) by Ruurd Wiersma.
|
|
78
|
+
|
|
79
|
+
* Python implementations, specifically `Joypy <https://github.com/ghosthamlet/Joypy>`__ by @ghosthamlet.
|
|
80
|
+
|
|
81
|
+
* An entire `book chapter <https://github.com/nickelsworth/sympas/blob/master/text/18-minijoy.org>`_ implementing Joy in Pascal.
|
joyfl-0.4/README.rst
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
``joyfl`` (pronounced *ˈjȯi-fəl*) is a dialect of the programming language Joy.
|
|
2
|
+
|
|
3
|
+
Joy is a stack-based programming environment that's both functional and concatenative, which results in highly expressive but short programs. ``joyfl`` is an implementation of Joy in Python in its early stages; it's not entirely backwards compatible by design.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
EXAMPLES
|
|
7
|
+
========
|
|
8
|
+
|
|
9
|
+
You can run a simple REPL (read-eval-print loop) by typing: ``python3 joyfl.py repl``. From there, try typing these statements:
|
|
10
|
+
|
|
11
|
+
.. code-block:: bash
|
|
12
|
+
|
|
13
|
+
# Take the number one, the number two, add them. Then the number three and add it to the
|
|
14
|
+
# previous result. Is six equal to that?
|
|
15
|
+
1 2 + 3 +
|
|
16
|
+
6 equal? .
|
|
17
|
+
>>> true
|
|
18
|
+
|
|
19
|
+
# Take the list of numbers seven eight nine. Take a program that subtracts one. Map the
|
|
20
|
+
# program onto the list, then reverse it.
|
|
21
|
+
[7 8 9] [1 -] map reverse .
|
|
22
|
+
>>> [8 7 6]
|
|
23
|
+
|
|
24
|
+
# Take a list of symbols 'a 'b 'c. Take the symbol 'd. Swap the symbol with the list. Get
|
|
25
|
+
# the "rest" of the list omitting the first item. Construct a new list with "cons" that
|
|
26
|
+
# uses 'd as the new head.
|
|
27
|
+
['a 'b 'c] 'd swap rest cons .
|
|
28
|
+
>>> ['d 'b 'c]
|
|
29
|
+
|
|
30
|
+
Also look at the ``#/examples/`` folder and run them with ``python3 joyfl.py <filename>``.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
MOTIVATION
|
|
34
|
+
==========
|
|
35
|
+
|
|
36
|
+
While it's fun to implement languages, this project has particular #ML / #LLM research questions in mind...
|
|
37
|
+
|
|
38
|
+
**“ What if there was a language for a 'micro-controller' able to process 99% of tokens? ”**
|
|
39
|
+
|
|
40
|
+
📥 TRAINING: To produce training data, what's the middle ground between a web-template (gasp!) and a synthetic theorem generator (whew!)? The answer looks more like another language than a hacked-together Python script.
|
|
41
|
+
|
|
42
|
+
📤 INFERENCE: For output tokens, how can we make sure prompts are followed, any arithmetic is correct, no items are missed, and formatting is right? The solution isn't more Python but special tokens that can be interpreted as instructions...
|
|
43
|
+
|
|
44
|
+
The research goal of this project is to find out where and how Joy can shine in these cases!
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
DIFFERENCES
|
|
48
|
+
===========
|
|
49
|
+
|
|
50
|
+
While some of the tests from the original Joy pass, many also do not. Here are the design decisions at play:
|
|
51
|
+
|
|
52
|
+
1. **Symbols vs. Characters** — Individual byte characters are not supported, so it's not possible to extract or combine them with strings. Instead, the single quote denotes symbols (e.g. ``'alpha``) that can only be compared and added to containers.
|
|
53
|
+
|
|
54
|
+
2. **Data-Structures** — Sets are not implemented yet, but will be. When they are, the sets will have the functionality of the underlying Python sets. Lists too behave like Python lists. Dictionaries will likely follow too at some point.
|
|
55
|
+
|
|
56
|
+
3. **Conditionals** — Functions that return booleans are encouraged to use the ``?`` suffix, for example ``equal?`` or ``list?``. This change is inspired by Factor, and makes the code more readable so you know when to expect a boolean.
|
|
57
|
+
|
|
58
|
+
4. **Stackless** - The interpreter does not use the Python callstack: state is stored entirely in data-structures. There's a stack (for data created in the past) and a queue (for code to be executed in the future). Certain advanced combinators may feel a bit different to write because of this!
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
REFERENCES
|
|
62
|
+
==========
|
|
63
|
+
|
|
64
|
+
* The `official documentation <https://hypercubed.github.io/joy/joy.html>`__ for Joy by Manfred van Thun.
|
|
65
|
+
|
|
66
|
+
* The `various C implementations <https://github.com/Wodan58>`__ (joy0, joy1) by Ruurd Wiersma.
|
|
67
|
+
|
|
68
|
+
* Python implementations, specifically `Joypy <https://github.com/ghosthamlet/Joypy>`__ by @ghosthamlet.
|
|
69
|
+
|
|
70
|
+
* An entire `book chapter <https://github.com/nickelsworth/sympas/blob/master/text/18-minijoy.org>`_ implementing Joy in Pascal.
|
joyfl-0.4/pyproject.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires=["uv_build"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "joyfl"
|
|
7
|
+
version = "0.4"
|
|
8
|
+
description = "A minimal but elegant dialect of Joy, a functional / concatenative stack language."
|
|
9
|
+
readme = { file = "README.rst", content-type = "text/x-rst" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"click",
|
|
13
|
+
"lark",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
test = [
|
|
18
|
+
"pytest",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
joyfl = "joyfl.__main__:main"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
#
|
|
3
|
+
# joyfl — A minimal but elegant dialect of Joy, functional / concatenative stack language.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from .types import nil
|
|
14
|
+
from .errors import JoyError, JoyParseError, JoyNameError, JoyIncompleteParse, JoyAssertionError, JoyImportError
|
|
15
|
+
from .parser import format_parse_error_context, print_source_lines, format_source_lines
|
|
16
|
+
from .formatting import write_without_ansi, format_item, show_stack
|
|
17
|
+
|
|
18
|
+
from . import api as J
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.command()
|
|
22
|
+
@click.argument('files', nargs=-1, type=click.File('r'))
|
|
23
|
+
@click.option('--command', '-c', 'commands', multiple=True, type=str, help='Execute Joy code from command line.')
|
|
24
|
+
@click.option('--repl', is_flag=True, help='Start REPL after executing commands and files.')
|
|
25
|
+
@click.option('--verbose', '-v', default=0, count=True, help='Enable verbose interpreter execution.')
|
|
26
|
+
@click.option('--validate', is_flag=True, help='Enable type and stack validation before each operation.')
|
|
27
|
+
@click.option('--ignore', '-i', is_flag=True, help='Ignore errors and continue executing.')
|
|
28
|
+
@click.option('--stats', is_flag=True, help='Display execution statistics (e.g., number of steps).')
|
|
29
|
+
@click.option('--plain', '-p', is_flag=True, help='Strip ANSI color codes and redirect stderr to stdout.')
|
|
30
|
+
def main(files: tuple, commands: tuple, repl: bool, verbose: int, validate: bool, ignore: bool, stats: bool, plain: bool):
|
|
31
|
+
|
|
32
|
+
if plain is True:
|
|
33
|
+
writer = write_without_ansi(sys.stdout.write)
|
|
34
|
+
sys.stdout.write, sys.stderr.write = writer, writer
|
|
35
|
+
failure = False
|
|
36
|
+
|
|
37
|
+
def _maybe_fatal_error(message: str, detail: str, exc_type: str = None, context: str = '', is_repl=False):
|
|
38
|
+
header = detail if not exc_type else f"{detail} (Exception: \033[33m{exc_type}\033[0m)"
|
|
39
|
+
print(f'\033[30;43m {message} \033[0m {header}\n{context}', file=sys.stderr)
|
|
40
|
+
if not is_repl and not ignore: sys.exit(1)
|
|
41
|
+
|
|
42
|
+
def _handle_exception(exc, filename, source, is_repl=False):
|
|
43
|
+
if isinstance(exc, JoyParseError):
|
|
44
|
+
if is_repl and isinstance(exc, JoyIncompleteParse): return True
|
|
45
|
+
context = format_parse_error_context(filename, exc.line, exc.column, exc.token, source=source)
|
|
46
|
+
context += f"\n\033[90m{str(exc).replace(chr(10), ' ').replace(chr(9), ' ')}\033[0m\n"
|
|
47
|
+
_maybe_fatal_error("SYNTAX ERROR.", f"Parsing `\033[97m{filename}\033[0m` caused a problem!", type(exc).__name__, context, is_repl)
|
|
48
|
+
elif isinstance(exc, JoyNameError):
|
|
49
|
+
detail = f"Term `\033[1;97m{exc.joy_op}\033[0m` from `\033[97m{filename}\033[0m` was not found in library!"
|
|
50
|
+
context = '\n' + format_source_lines(exc.joy_meta, exc.joy_op)
|
|
51
|
+
_maybe_fatal_error("LINKER ERROR.", detail, type(exc).__name__, context, is_repl)
|
|
52
|
+
elif isinstance(exc, JoyAssertionError):
|
|
53
|
+
print(f'\033[30;43m ASSERTION FAILED. \033[0m Function \033[1;97m`{exc.joy_op}`\033[0m raised an error.\n', file=sys.stderr)
|
|
54
|
+
print_source_lines(exc.joy_op, J.library.quotations, file=sys.stderr)
|
|
55
|
+
print(f'\033[1;33m Stack content is\033[0;33m\n ', end='', file=sys.stderr)
|
|
56
|
+
show_stack(exc.joy_stack, width=None, file=sys.stderr); print('\033[0m', file=sys.stderr)
|
|
57
|
+
if not is_repl and not ignore: sys.exit(1)
|
|
58
|
+
elif isinstance(exc, JoyImportError):
|
|
59
|
+
detail = f"Importing library module failed while resolving `{exc.joy_op}`: \033[97m{exc.filename}\033[0m"
|
|
60
|
+
context = '\n' + format_source_lines(exc.joy_meta, exc.joy_op)
|
|
61
|
+
_maybe_fatal_error("IMPORT ERROR.", detail, type(exc).__name__, context, is_repl)
|
|
62
|
+
elif isinstance(exc, Exception):
|
|
63
|
+
print(f'\033[30;43m RUNTIME ERROR. \033[0m Function \033[1;97m`{exc.joy_op}`\033[0m caused an error in interpret! (Exception: \033[33m{type(exc).__name__}\033[0m)\n', file=sys.stderr)
|
|
64
|
+
tb_lines = traceback.format_exc().split('\n')
|
|
65
|
+
print(*[line for line in tb_lines if 'lambda' in line], sep='\n', end='\n', file=sys.stderr)
|
|
66
|
+
print_source_lines(exc.joy_op, J.library.quotations, file=sys.stderr)
|
|
67
|
+
traceback.print_exc()
|
|
68
|
+
if not is_repl and not ignore: sys.exit(1)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
base = Path(__file__).resolve().parent
|
|
72
|
+
candidates = (d / 'libs' / 'stdlib.joy' for d in (base, *base.parents[:2]))
|
|
73
|
+
stdlib_path = next((p for p in candidates if p.exists()), Path('libs/stdlib.joy'))
|
|
74
|
+
J.load(stdlib_path.read_text(encoding='utf-8'), filename='libs/stdlib.joy', validate=validate)
|
|
75
|
+
|
|
76
|
+
# Build execution list: files first, then commands.
|
|
77
|
+
items = [(f.read(), f.name) for f in files]
|
|
78
|
+
items += [(cmd, f'<INPUT_{i+1}>') for i, cmd in enumerate(commands)]
|
|
79
|
+
|
|
80
|
+
total_stats = {'steps': 0, 'start': time.time()} if stats else None
|
|
81
|
+
for source, filename in items:
|
|
82
|
+
try:
|
|
83
|
+
r = J.run(source, filename=filename, verbosity=verbose, validate=validate, stats=total_stats)
|
|
84
|
+
(r is None and ((failure := True) or (not ignore and sys.exit(1))))
|
|
85
|
+
except (JoyError, Exception) as exc:
|
|
86
|
+
_handle_exception(exc, filename, source, is_repl=False)
|
|
87
|
+
|
|
88
|
+
if total_stats and len(items) > 0:
|
|
89
|
+
elapsed_time = time.time() - total_stats['start']
|
|
90
|
+
print(f"\n\033[97m\033[48;5;30m STATISTICS. \033[0m")
|
|
91
|
+
print(f"step\t\033[97m{total_stats['steps']:,}\033[0m")
|
|
92
|
+
print(f"time\t\033[97m{elapsed_time:.3f}s\033[0m")
|
|
93
|
+
|
|
94
|
+
# Start REPL if no items were provided or --repl flag was set
|
|
95
|
+
if len(items) == 0 or repl:
|
|
96
|
+
if sys.platform != "win32": import readline
|
|
97
|
+
|
|
98
|
+
print('joyfl - Functional stack language REPL; type Ctrl+C to exit.')
|
|
99
|
+
source = ""
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
try:
|
|
103
|
+
prompt = "\033[36m<<< \033[0m" if not source.strip() else "\033[36m... \033[0m"
|
|
104
|
+
line = input(prompt)
|
|
105
|
+
if len(line.strip()) == 0: continue
|
|
106
|
+
if line.strip() in ('quit', 'exit'): break
|
|
107
|
+
source += line + " "
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
stack = J.run(source, filename='<REPL>', verbosity=verbose, validate=validate)
|
|
111
|
+
if stack is not nil: print("\033[90m>>>\033[0m", format_item(stack[-1]))
|
|
112
|
+
source = ""
|
|
113
|
+
except (JoyError, Exception) as exc:
|
|
114
|
+
if not _handle_exception(exc, '<REPL>', source, is_repl=True):
|
|
115
|
+
source = ""
|
|
116
|
+
|
|
117
|
+
except (KeyboardInterrupt, EOFError):
|
|
118
|
+
print(""); break
|
|
119
|
+
|
|
120
|
+
sys.exit(failure)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
main()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## joyfl — Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
|
|
3
|
+
from .types import Operation, Stack, nil
|
|
4
|
+
from .errors import *
|
|
5
|
+
from .runtime import Runtime
|
|
6
|
+
|
|
7
|
+
_RUNTIME = Runtime()
|
|
8
|
+
|
|
9
|
+
def __getattr__(name):
|
|
10
|
+
return getattr(_RUNTIME, name)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
|
|
3
|
+
from .library import Library
|
|
4
|
+
from . import operators
|
|
5
|
+
from .combinators import comb_i, comb_dip, comb_step, comb_cont
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _joy_name_from_python(py_name: str) -> str:
|
|
9
|
+
# py_name like 'op_equal_q' -> 'equal?'; 'op_put_b' -> 'put!'; underscores -> dashes
|
|
10
|
+
base = py_name[3:]
|
|
11
|
+
base = base.replace('_q', '?').replace('_b', '!')
|
|
12
|
+
return base.replace('_', '-')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_builtins_library():
|
|
16
|
+
# Combinators
|
|
17
|
+
combinators = {
|
|
18
|
+
'i': comb_i,
|
|
19
|
+
'dip': comb_dip,
|
|
20
|
+
'step': comb_step,
|
|
21
|
+
',,,': comb_cont,
|
|
22
|
+
}
|
|
23
|
+
quotations = {}
|
|
24
|
+
constants = {'true': True, 'false': False}
|
|
25
|
+
factories = {}
|
|
26
|
+
aliases = {
|
|
27
|
+
'+': 'add', '-': 'sub', '*': 'mul', '/': 'div', '%': 'rem',
|
|
28
|
+
'>': 'gt', '>=': 'gte', '<': 'lt', '<=': 'lte',
|
|
29
|
+
'=': 'equal?', '!=': 'differ?', 'size': 'length',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
lib = Library(functions={}, combinators=combinators, quotations=quotations, constants=constants, factories=factories, aliases=aliases)
|
|
33
|
+
|
|
34
|
+
# Functions (wrapped via Library helper)
|
|
35
|
+
for k in dir(operators):
|
|
36
|
+
if not k.startswith('op_'):
|
|
37
|
+
continue
|
|
38
|
+
joy = _joy_name_from_python(k)
|
|
39
|
+
lib.add_function(joy, getattr(operators, k))
|
|
40
|
+
|
|
41
|
+
lib.ensure_consistent()
|
|
42
|
+
return lib
|
|
43
|
+
|
|
44
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
#
|
|
3
|
+
# joyfl — A minimal but elegant dialect of Joy, functional / concatenative stack language.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
from .types import Operation
|
|
7
|
+
from .parser import parse
|
|
8
|
+
from .formatting import show_stack
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def comb_i(_, queue, tail, head, lib):
|
|
12
|
+
"""Takes a program as quotation on the top of the stack, and puts it into the queue for execution."""
|
|
13
|
+
assert isinstance(head, (list, tuple))
|
|
14
|
+
queue.extendleft(reversed(head))
|
|
15
|
+
return tail
|
|
16
|
+
|
|
17
|
+
def comb_dip(_, queue, *stack, lib):
|
|
18
|
+
"""Schedules a program for execution like `i`, but removes the second top-most item from the stack too
|
|
19
|
+
and then restores it after the program is done. This is like running `i` one level lower in the stack.
|
|
20
|
+
"""
|
|
21
|
+
((tail, item), head) = stack
|
|
22
|
+
assert isinstance(head, list)
|
|
23
|
+
queue.appendleft(item)
|
|
24
|
+
queue.extendleft(reversed(head))
|
|
25
|
+
|
|
26
|
+
return tail
|
|
27
|
+
|
|
28
|
+
def comb_step(this: Operation, queue, *stack, lib):
|
|
29
|
+
"""Applies a program to every item in a list in a recursive fashion. `step` expands into another
|
|
30
|
+
quotation that includes itself to run on the rest of the list, after the program was applied to the
|
|
31
|
+
head of the list.
|
|
32
|
+
"""
|
|
33
|
+
(tail, values), program = stack
|
|
34
|
+
assert isinstance(program, list) and isinstance(values, list)
|
|
35
|
+
if len(values) == 0: return tail
|
|
36
|
+
queue.extendleft(reversed([values[0]] + program + [values[1:], program, this]))
|
|
37
|
+
return tail
|
|
38
|
+
|
|
39
|
+
def comb_cont(this: Operation, queue, *stack, lib):
|
|
40
|
+
from .linker import link_body
|
|
41
|
+
|
|
42
|
+
print(f"\033[97m ~ :\033[0m ", end=''); show_stack(stack, width=72, end='')
|
|
43
|
+
try:
|
|
44
|
+
program = []
|
|
45
|
+
value = input("\033[4 q\033[36m ... \033[0m")
|
|
46
|
+
if value.strip():
|
|
47
|
+
for typ, data in parse(value, start='term'):
|
|
48
|
+
program, _ = link_body(data, meta={'filename': '<REPL>', 'lines': (1, 1)}, globals_=lib)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print('EXCEPTION: comb_cont could not parse or compile the text.', e)
|
|
51
|
+
import traceback; traceback.print_exc(limit=2)
|
|
52
|
+
finally:
|
|
53
|
+
print("\033[0 q", end='')
|
|
54
|
+
|
|
55
|
+
if program:
|
|
56
|
+
queue.extendleft(reversed(program + [this]))
|
|
57
|
+
return stack
|
|
58
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
|
|
3
|
+
import lark
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JoyError(Exception):
|
|
7
|
+
def __init__(self, message: str = "", *, joy_op=None, joy_meta=None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.joy_op = joy_op
|
|
10
|
+
self.joy_meta = joy_meta
|
|
11
|
+
|
|
12
|
+
class JoyParseError(JoyError):
|
|
13
|
+
def __init__(self, message, *, filename=None, line=None, column=None, token=None):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.filename = filename
|
|
16
|
+
self.line = line
|
|
17
|
+
self.column = column
|
|
18
|
+
self.token = token
|
|
19
|
+
|
|
20
|
+
class JoyIncompleteParse(JoyParseError, lark.exceptions.ParseError):
|
|
21
|
+
def __init__(self, message, *, filename=None, line=None, column=None, token=None):
|
|
22
|
+
super().__init__(message, filename=filename, line=line, column=column, token=token)
|
|
23
|
+
|
|
24
|
+
class JoyNameError(JoyError, NameError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class JoyRuntimeError(JoyError, RuntimeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
class JoyAssertionError(JoyError, AssertionError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JoyImportError(JoyError, ImportError):
|
|
35
|
+
def __init__(self, message, *, joy_op=None, filename=None, joy_meta=None):
|
|
36
|
+
super().__init__(message, joy_op=joy_op, joy_meta=joy_meta)
|
|
37
|
+
self.filename = filename
|
|
38
|
+
|
|
39
|
+
class JoyModuleError(JoyImportError):
|
|
40
|
+
pass
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
#
|
|
3
|
+
# joyfl — A minimal but elegant dialect of Joy, functional / concatenative stack language.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .types import stack_list, Stack, nil
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def stack_to_list(stk: Stack) -> stack_list:
|
|
13
|
+
result = []
|
|
14
|
+
while stk is not nil:
|
|
15
|
+
stk, head = stk
|
|
16
|
+
result.append(head)
|
|
17
|
+
return stack_list(result)
|
|
18
|
+
|
|
19
|
+
def list_to_stack(values: list, base=None) -> Stack:
|
|
20
|
+
stack = nil if base is None else base
|
|
21
|
+
for value in reversed(values):
|
|
22
|
+
stack = Stack(stack, value)
|
|
23
|
+
return stack
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_without_ansi(write_fn):
|
|
27
|
+
"""Wrapper function that strips ANSI codes before calling the original writer."""
|
|
28
|
+
ansi_re = re.compile(r'\033\[[0-9;]*m')
|
|
29
|
+
return lambda text: write_fn(ansi_re.sub('', text))
|
|
30
|
+
|
|
31
|
+
def format_item(it, width=None, indent=0):
|
|
32
|
+
if (is_stack := isinstance(it, stack_list)) or isinstance(it, list):
|
|
33
|
+
items = reversed(it) if is_stack else it
|
|
34
|
+
lhs, rhs = ('<', '>') if is_stack else ('[', ']')
|
|
35
|
+
formatted_items = [format_item(i, width, indent + 4) for i in items]
|
|
36
|
+
single_line = lhs + ' '.join(formatted_items) + rhs
|
|
37
|
+
# If it fits on one line, use single line format.
|
|
38
|
+
if width is None or len(single_line) + indent <= width: return single_line
|
|
39
|
+
# Otherwise use multi-line format...
|
|
40
|
+
result = lhs + ' '
|
|
41
|
+
for i, item in enumerate(formatted_items):
|
|
42
|
+
if i > 0: result += '\n' + (' ' * (indent + 4))
|
|
43
|
+
result += item
|
|
44
|
+
result += '\n' + (' ' * indent) + rhs
|
|
45
|
+
return result
|
|
46
|
+
if isinstance(it, str) and indent > 0: return f'"{it.replace(chr(34), chr(92)+chr(34))}"'
|
|
47
|
+
if isinstance(it, bool): return str(it).lower()
|
|
48
|
+
if isinstance(it, bytes): return str(it)[1:-1]
|
|
49
|
+
return str(it)
|
|
50
|
+
|
|
51
|
+
def show_stack(stack, width=72, end='\n', file=None):
|
|
52
|
+
stack_str = ' '.join(format_item(s) for s in reversed(stack_to_list(stack))) if stack is not nil else '∅'
|
|
53
|
+
if len(stack_str) > (width or sys.maxsize):
|
|
54
|
+
stack_str = '… ' + stack_str[-width+2:]
|
|
55
|
+
print(f"{stack_str:>{width}}" if width else stack_str, end=end, file=file)
|
|
56
|
+
|
|
57
|
+
def show_program_and_stack(program, stack, width=72):
|
|
58
|
+
prog_str = ' '.join(format_item(p) for p in program) if program else '∅'
|
|
59
|
+
if len(prog_str) > width:
|
|
60
|
+
prog_str = prog_str[:+width-2] + ' …'
|
|
61
|
+
show_stack(stack, end='')
|
|
62
|
+
print(f" \033[36m <=> \033[0m {prog_str:<{width}}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
#
|
|
3
|
+
# joyfl — A minimal but elegant dialect of Joy, functional / concatenative stack language.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
import collections
|
|
9
|
+
|
|
10
|
+
from typing import Any, TypeVar
|
|
11
|
+
|
|
12
|
+
from .types import Operation, Stack, nil
|
|
13
|
+
from .parser import print_source_lines
|
|
14
|
+
from .library import Library
|
|
15
|
+
from .formatting import show_stack, show_program_and_stack, stack_to_list
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def can_execute(op: Operation, stack: Stack) -> tuple[bool, str]:
|
|
19
|
+
"""Check if operation can execute on stack using inferred stack effects."""
|
|
20
|
+
# Special cases for combinators and runtime hazards that don't come from signature
|
|
21
|
+
if op.type == Operation.COMBINATOR and op.name in ("i", "dip"):
|
|
22
|
+
if stack is nil:
|
|
23
|
+
return False, f"`{op.name}` needs at least 1 item on the stack, but stack is empty."
|
|
24
|
+
_, head = stack
|
|
25
|
+
if not isinstance(head, (list, tuple)):
|
|
26
|
+
return False, f"`{op.name}` requires a quotation as list as top item on the stack."
|
|
27
|
+
return True, ""
|
|
28
|
+
|
|
29
|
+
# Division by zero guard for division, as binary int/float op.
|
|
30
|
+
if op.name in ('div', '/') and stack is not nil:
|
|
31
|
+
_, head = stack
|
|
32
|
+
if head == 0:
|
|
33
|
+
return False, f"`{op.name}` would divide by zero and cause a runtime exception."
|
|
34
|
+
|
|
35
|
+
if op.type != Operation.FUNCTION: return True, ""
|
|
36
|
+
|
|
37
|
+
eff = getattr(op.ptr, '__joy_meta__')
|
|
38
|
+
inputs = eff['inputs']
|
|
39
|
+
items = stack_to_list(stack)
|
|
40
|
+
depth = len(items)
|
|
41
|
+
if depth < len(inputs):
|
|
42
|
+
need = len(inputs)
|
|
43
|
+
return False, f"`{op.name}` needs at least {need} item(s) on the stack, but {depth} available."
|
|
44
|
+
|
|
45
|
+
# Type checks from top downward
|
|
46
|
+
for i, expected_type in enumerate(inputs):
|
|
47
|
+
if isinstance(expected_type, TypeVar): expected_type = expected_type.__bound__
|
|
48
|
+
if expected_type in (Any, None): continue
|
|
49
|
+
actual = items[i]
|
|
50
|
+
if not isinstance(actual, expected_type):
|
|
51
|
+
type_name = expected_type.__name__ if hasattr(expected_type, '__name__') else str(expected_type)
|
|
52
|
+
return False, f"`{op.name}` expects {type_name} at position {i+1} from top, got {type(actual).__name__}."
|
|
53
|
+
|
|
54
|
+
# Extra semantic guard for 'index' bounds when types look correct
|
|
55
|
+
if op.name == 'index' and len(items) >= 2 and isinstance(items[0], (list, str)) and isinstance(items[1], int):
|
|
56
|
+
idx, seq = items[1], items[0]
|
|
57
|
+
if not (0 <= int(idx) < len(seq)):
|
|
58
|
+
return False, f"`{op.name}` would index a list out ouf bounds."
|
|
59
|
+
|
|
60
|
+
return True, ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def interpret_step(program, stack, lib: Library):
|
|
64
|
+
op = program.popleft()
|
|
65
|
+
if isinstance(op, bytes) and op in (b'ABORT', b'BREAK'):
|
|
66
|
+
print(f"\033[97m ~ :\033[0m ", end=''); show_program_and_stack(program, stack)
|
|
67
|
+
if op == b'ABORT': sys.exit(-1)
|
|
68
|
+
if op == b'BREAK': input()
|
|
69
|
+
|
|
70
|
+
if not isinstance(op, Operation):
|
|
71
|
+
stack = Stack(stack, op)
|
|
72
|
+
return stack, program
|
|
73
|
+
|
|
74
|
+
match op.type:
|
|
75
|
+
case Operation.FUNCTION:
|
|
76
|
+
stack = op.ptr(stack)
|
|
77
|
+
case Operation.COMBINATOR:
|
|
78
|
+
stack = op.ptr(op, program, *stack, lib=lib)
|
|
79
|
+
case Operation.EXECUTE:
|
|
80
|
+
program.extendleft(reversed(op.ptr))
|
|
81
|
+
|
|
82
|
+
return stack, program
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def interpret(program: list, stack=None, lib: Library = None, verbosity=0, validate=False, stats=None):
|
|
86
|
+
stack = nil if stack is None else stack
|
|
87
|
+
program = collections.deque(program)
|
|
88
|
+
|
|
89
|
+
def is_notable(op):
|
|
90
|
+
if not isinstance(op, Operation): return False
|
|
91
|
+
return isinstance(op.ptr, list) or op.type == Operation.COMBINATOR
|
|
92
|
+
|
|
93
|
+
step = 0
|
|
94
|
+
while program:
|
|
95
|
+
if validate and isinstance(program[0], Operation):
|
|
96
|
+
if (check := can_execute(program[0], stack)) and not check[0]:
|
|
97
|
+
print(f'\033[30;43m TYPE ERROR. \033[0m {check[1]}\n', file=sys.stderr)
|
|
98
|
+
print(f'\033[1;33m Stack content is\033[0;33m\n ', end='', file=sys.stderr)
|
|
99
|
+
show_stack(stack, width=None, file=sys.stderr)
|
|
100
|
+
print('\033[0m', file=sys.stderr)
|
|
101
|
+
print_source_lines(program[0], lib.quotations, file=sys.stderr)
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if verbosity == 2 or (verbosity == 1 and (is_notable(program[0]) or step == 0)):
|
|
105
|
+
print(f"\033[90m{step:>3} :\033[0m ", end='')
|
|
106
|
+
show_program_and_stack(program, stack)
|
|
107
|
+
|
|
108
|
+
step += 1
|
|
109
|
+
try:
|
|
110
|
+
op = program[0]
|
|
111
|
+
stack, program = interpret_step(program, stack, lib)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
exc.joy_op = op
|
|
114
|
+
exc.joy_stack = stack
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
if verbosity > 0:
|
|
118
|
+
print(f"\033[90m{step:>3} :\033[0m ", end='')
|
|
119
|
+
show_program_and_stack(program, stack)
|
|
120
|
+
if stats is not None:
|
|
121
|
+
stats['steps'] = stats.get('steps', 0) + step
|
|
122
|
+
|
|
123
|
+
return stack
|