joyfl 0.4__py3-none-any.whl
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/__init__.py +3 -0
- joyfl/__main__.py +124 -0
- joyfl/api.py +10 -0
- joyfl/builtins.py +44 -0
- joyfl/combinators.py +58 -0
- joyfl/errors.py +40 -0
- joyfl/formatting.py +62 -0
- joyfl/interpreter.py +123 -0
- joyfl/library.py +84 -0
- joyfl/libs/stdlib.joy +270 -0
- joyfl/linker.py +56 -0
- joyfl/loader.py +85 -0
- joyfl/operators.py +85 -0
- joyfl/parser.py +144 -0
- joyfl/runtime.py +101 -0
- joyfl/types.py +61 -0
- joyfl-0.4.dist-info/METADATA +81 -0
- joyfl-0.4.dist-info/RECORD +20 -0
- joyfl-0.4.dist-info/WHEEL +4 -0
- joyfl-0.4.dist-info/entry_points.txt +3 -0
joyfl/__init__.py
ADDED
joyfl/__main__.py
ADDED
|
@@ -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()
|
joyfl/api.py
ADDED
|
@@ -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)
|
joyfl/builtins.py
ADDED
|
@@ -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
|
+
|
joyfl/combinators.py
ADDED
|
@@ -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
|
+
|
joyfl/errors.py
ADDED
|
@@ -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
|
joyfl/formatting.py
ADDED
|
@@ -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}}")
|
joyfl/interpreter.py
ADDED
|
@@ -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
|
joyfl/library.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
## Copyright © 2025, Alex J. Champandard. Licensed under AGPLv3; see LICENSE! ⚘
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from .types import Stack
|
|
7
|
+
from .errors import JoyNameError
|
|
8
|
+
from .loader import get_stack_effects, resolve_module_op
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Library:
|
|
13
|
+
functions: dict[str, Callable[..., Any]]
|
|
14
|
+
combinators: dict[str, Callable[..., Any]]
|
|
15
|
+
quotations: dict[str, tuple[list, dict]] # name -> (program, meta)
|
|
16
|
+
constants: dict[str, Any]
|
|
17
|
+
factories: dict[str, Callable[[], Any]]
|
|
18
|
+
aliases: dict[str, str] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
# Registration helpers
|
|
21
|
+
def add_function(self, name: str, fn: Callable[..., Any]) -> None:
|
|
22
|
+
fn, meta = _make_wrapper(fn, name)
|
|
23
|
+
fn.__joy_meta__ = meta
|
|
24
|
+
self.functions[name] = fn
|
|
25
|
+
|
|
26
|
+
def add_quotation(self, name: str, program: list, meta: dict) -> None:
|
|
27
|
+
self.quotations[name] = (program, meta)
|
|
28
|
+
|
|
29
|
+
def ensure_consistent(self) -> None:
|
|
30
|
+
for _, fn in list(self.functions.items()):
|
|
31
|
+
assert hasattr(fn, '__joy_meta__')
|
|
32
|
+
|
|
33
|
+
def get_function(self, name: str, *, meta: dict | None = None) -> Callable[..., Any]:
|
|
34
|
+
resolved_name = self.aliases.get(name, name)
|
|
35
|
+
if (fn := self.functions.get(resolved_name)) is not None:
|
|
36
|
+
return fn
|
|
37
|
+
if '.' in resolved_name:
|
|
38
|
+
ns, op = resolved_name.split('.', 1)
|
|
39
|
+
py_fn = resolve_module_op(ns, op, meta=meta)
|
|
40
|
+
fn, meta = _make_wrapper(py_fn, resolved_name)
|
|
41
|
+
fn.__joy_meta__ = meta
|
|
42
|
+
self.functions[resolved_name] = fn
|
|
43
|
+
return fn
|
|
44
|
+
raise JoyNameError(f"Operation `{name}` not found in library.", joy_op=name, joy_meta=meta)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _make_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
|
|
48
|
+
meta = get_stack_effects(fn=fn, name=name)
|
|
49
|
+
|
|
50
|
+
match meta['valency']:
|
|
51
|
+
case -1:
|
|
52
|
+
def push(_, res): return res
|
|
53
|
+
case 0:
|
|
54
|
+
def push(base, _): return base
|
|
55
|
+
case 1:
|
|
56
|
+
def push(base, res): return Stack(base, res)
|
|
57
|
+
case _:
|
|
58
|
+
def push(base, res):
|
|
59
|
+
for v in res: base = Stack(base, v)
|
|
60
|
+
return base
|
|
61
|
+
|
|
62
|
+
match meta['arity']:
|
|
63
|
+
case -1:
|
|
64
|
+
def w_n(stk: Stack):
|
|
65
|
+
return push(stk, fn(*stk))
|
|
66
|
+
return w_n, meta
|
|
67
|
+
case 1:
|
|
68
|
+
def w_1(stk: Stack):
|
|
69
|
+
base, a = stk
|
|
70
|
+
return push(base, fn(a))
|
|
71
|
+
return w_1, meta
|
|
72
|
+
case 2:
|
|
73
|
+
def w_2(stk: Stack):
|
|
74
|
+
(base, b), a = stk
|
|
75
|
+
return push(base, fn(b, a))
|
|
76
|
+
return w_2, meta
|
|
77
|
+
case _:
|
|
78
|
+
def w_x(stk: Stack):
|
|
79
|
+
args, base = (), stk
|
|
80
|
+
for _ in range(meta['arity']):
|
|
81
|
+
base, h = base
|
|
82
|
+
args = (h,) + args
|
|
83
|
+
return push(base, fn(*args))
|
|
84
|
+
return w_x, meta
|