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 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.
@@ -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,3 @@
1
+ ## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
2
+
3
+ __version__ = "0.4"
@@ -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