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