esolangs 0.1.0__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.
Files changed (129) hide show
  1. esolangs/__init__.py +188 -0
  2. esolangs/__main__.py +6 -0
  3. esolangs/cli.py +145 -0
  4. esolangs/compilers/__init__.py +1 -0
  5. esolangs/compilers/_riscv_common.py +143 -0
  6. esolangs/compilers/addsubjump.py +178 -0
  7. esolangs/compilers/bf_pda.py +84 -0
  8. esolangs/compilers/bfstack.py +146 -0
  9. esolangs/compilers/collatz_multiverse.py +330 -0
  10. esolangs/compilers/decleq.py +125 -0
  11. esolangs/compilers/forth.py +554 -0
  12. esolangs/compilers/home_row.py +174 -0
  13. esolangs/compilers/jaune.py +314 -0
  14. esolangs/compilers/ram0.py +229 -0
  15. esolangs/compilers/sbleq.py +145 -0
  16. esolangs/compilers/suffolk.py +153 -0
  17. esolangs/compilers/unsquare.py +188 -0
  18. esolangs/debug.py +152 -0
  19. esolangs/exceptions.py +32 -0
  20. esolangs/interpreters/__init__.py +1 -0
  21. esolangs/interpreters/_template.py +92 -0
  22. esolangs/interpreters/brackets.py +24 -0
  23. esolangs/interpreters/grid_based/__init__.py +1 -0
  24. esolangs/interpreters/grid_based/a_painter_ant.py +150 -0
  25. esolangs/interpreters/grid_based/arrowqueue.py +100 -0
  26. esolangs/interpreters/grid_based/circuit_diagram.py +801 -0
  27. esolangs/interpreters/grid_based/clockwise.py +136 -0
  28. esolangs/interpreters/grid_based/cod.py +250 -0
  29. esolangs/interpreters/grid_based/dig.py +172 -0
  30. esolangs/interpreters/grid_based/flowchart.py +594 -0
  31. esolangs/interpreters/grid_based/laserfuck.py +187 -0
  32. esolangs/interpreters/grid_based/streetcode.py +1117 -0
  33. esolangs/interpreters/grid_based/wii2d.py +163 -0
  34. esolangs/interpreters/io.py +157 -0
  35. esolangs/interpreters/memory.py +22 -0
  36. esolangs/interpreters/oisc_cli.py +47 -0
  37. esolangs/interpreters/other/__init__.py +1 -0
  38. esolangs/interpreters/other/container.py +140 -0
  39. esolangs/interpreters/other/forbin.py +782 -0
  40. esolangs/interpreters/other/lamfunc.py +522 -0
  41. esolangs/interpreters/other/suptiftam.py +729 -0
  42. esolangs/interpreters/other/ztoalc_l.py +230 -0
  43. esolangs/interpreters/queue_based/__init__.py +1 -0
  44. esolangs/interpreters/queue_based/bitdeque.py +105 -0
  45. esolangs/interpreters/queue_based/taglate.py +176 -0
  46. esolangs/interpreters/register_based/__init__.py +1 -0
  47. esolangs/interpreters/register_based/addsubjump.py +150 -0
  48. esolangs/interpreters/register_based/between.py +308 -0
  49. esolangs/interpreters/register_based/bio.py +153 -0
  50. esolangs/interpreters/register_based/collatz_multiverse.py +132 -0
  51. esolangs/interpreters/register_based/decleq.py +95 -0
  52. esolangs/interpreters/register_based/minsky_swap.py +135 -0
  53. esolangs/interpreters/register_based/myscript.py +449 -0
  54. esolangs/interpreters/register_based/nevermind.py +162 -0
  55. esolangs/interpreters/register_based/pct_squared_minus_one.py +87 -0
  56. esolangs/interpreters/register_based/point_break.py +336 -0
  57. esolangs/interpreters/register_based/polynomial.py +298 -0
  58. esolangs/interpreters/register_based/qoibl.py +121 -0
  59. esolangs/interpreters/register_based/ram0.py +106 -0
  60. esolangs/interpreters/register_based/sophie.py +194 -0
  61. esolangs/interpreters/stack_based/__init__.py +1 -0
  62. esolangs/interpreters/stack_based/bf_pda.py +138 -0
  63. esolangs/interpreters/stack_based/bfstack.py +108 -0
  64. esolangs/interpreters/stack_based/eval.py +124 -0
  65. esolangs/interpreters/stack_based/forth.py +242 -0
  66. esolangs/interpreters/stack_based/grapheme.py +362 -0
  67. esolangs/interpreters/stack_based/modulous.py +231 -0
  68. esolangs/interpreters/stack_based/three_x.py +155 -0
  69. esolangs/interpreters/stack_based/unsquare.py +139 -0
  70. esolangs/interpreters/tape_based/__init__.py +1 -0
  71. esolangs/interpreters/tape_based/back.py +109 -0
  72. esolangs/interpreters/tape_based/basicfuck.py +447 -0
  73. esolangs/interpreters/tape_based/bit_tilde.py +126 -0
  74. esolangs/interpreters/tape_based/brainfuck.py +81 -0
  75. esolangs/interpreters/tape_based/brainif.py +95 -0
  76. esolangs/interpreters/tape_based/circlefuck.py +152 -0
  77. esolangs/interpreters/tape_based/dimensional.py +274 -0
  78. esolangs/interpreters/tape_based/factor.py +84 -0
  79. esolangs/interpreters/tape_based/home_row.py +128 -0
  80. esolangs/interpreters/tape_based/jaune.py +229 -0
  81. esolangs/interpreters/tape_based/minifuck.py +83 -0
  82. esolangs/interpreters/tape_based/nocomment.py +122 -0
  83. esolangs/interpreters/tape_based/one_two_three.py +131 -0
  84. esolangs/interpreters/tape_based/painfuck.py +246 -0
  85. esolangs/interpreters/tape_based/rotfuck.py +204 -0
  86. esolangs/interpreters/tape_based/sbleq.py +134 -0
  87. esolangs/interpreters/tape_based/six_five.py +133 -0
  88. esolangs/interpreters/tape_based/slow_acv_mammalian.py +161 -0
  89. esolangs/interpreters/tape_based/suffolk.py +96 -0
  90. esolangs/interpreters/tape_based/three_d_brainfuck.py +145 -0
  91. esolangs/py.typed +0 -0
  92. esolangs/registry.py +478 -0
  93. esolangs/tools/__init__.py +1 -0
  94. esolangs/tools/_polynomial.py +42 -0
  95. esolangs/tools/boolean/__init__.py +207 -0
  96. esolangs/tools/boolean/a_painter_ant.py +263 -0
  97. esolangs/tools/boolean/circuit_diagram.py +420 -0
  98. esolangs/tools/boolean/cod.py +232 -0
  99. esolangs/tools/boolean/dimensional.py +184 -0
  100. esolangs/tools/boolean/examples.py +507 -0
  101. esolangs/tools/boolean/helpers.py +173 -0
  102. esolangs/tools/boolean/other.py +1328 -0
  103. esolangs/tools/boolean/parameterized.py +899 -0
  104. esolangs/tools/boolean/register.py +542 -0
  105. esolangs/tools/boolean/rotfuck.py +219 -0
  106. esolangs/tools/boolean/six_five.py +248 -0
  107. esolangs/tools/boolean/stack.py +255 -0
  108. esolangs/tools/boolean/streetcode.py +169 -0
  109. esolangs/tools/boolean/tape.py +841 -0
  110. esolangs/tools/boolean/wii2d.py +537 -0
  111. esolangs/tools/boolean/ztoalc_l.py +182 -0
  112. esolangs/tools/laserfuck_layout.py +180 -0
  113. esolangs/tools/text/__init__.py +125 -0
  114. esolangs/tools/text/__main__.py +9 -0
  115. esolangs/tools/text/helpers.py +156 -0
  116. esolangs/tools/text/other.py +1650 -0
  117. esolangs/tools/text/register.py +351 -0
  118. esolangs/tools/text/stack.py +28 -0
  119. esolangs/tools/text/tape.py +482 -0
  120. esolangs/tools/transpilers.py +1074 -0
  121. esolangs/tools/wrap.py +605 -0
  122. esolangs/tools/ztoalc_starts.py +35 -0
  123. esolangs/vm.py +1966 -0
  124. esolangs-0.1.0.dist-info/METADATA +455 -0
  125. esolangs-0.1.0.dist-info/RECORD +129 -0
  126. esolangs-0.1.0.dist-info/WHEEL +5 -0
  127. esolangs-0.1.0.dist-info/entry_points.txt +2 -0
  128. esolangs-0.1.0.dist-info/licenses/LICENSE +674 -0
  129. esolangs-0.1.0.dist-info/top_level.txt +1 -0
esolangs/__init__.py ADDED
@@ -0,0 +1,188 @@
1
+ """Public API for the esolangs package.
2
+
3
+ Provides ``generate`` (produce a program that prints a text), ``run``
4
+ (execute a program through an interpreter), ``make_vm`` (a step-and-inspect
5
+ wrapper around the step-capable interpreters), ``make_debugger`` (a
6
+ breakpoint/watch layer over the VM), ``transpile`` (rewrite a
7
+ program between languages), ``describe`` (a structured language summary),
8
+ and ``list_languages``.
9
+ """
10
+
11
+ import importlib
12
+ import pathlib
13
+ import signal
14
+ import threading
15
+ from collections.abc import Callable
16
+ from typing import Any
17
+
18
+ from esolangs.debug import Debugger, make_debugger
19
+ from esolangs.exceptions import (
20
+ HaltError,
21
+ UnknownLanguageError,
22
+ UnsupportedTranspilationError,
23
+ )
24
+ from esolangs.interpreters.io import ScriptedIO
25
+ from esolangs.registry import GENERATORS, LANGUAGES, RUNNERS
26
+ from esolangs.tools.boolean import BOOLEAN
27
+ from esolangs.tools.transpilers import TRANSPILERS
28
+ from esolangs.tools.wrap import takes_width, wrap_program
29
+ from esolangs.vm import VM, make_vm
30
+
31
+ _EXAMPLES = pathlib.Path(__file__).resolve().parents[2] / "examples"
32
+
33
+ # Interpreter module family -> state model name.
34
+ _STATE_MODELS = {
35
+ "register_based": "register",
36
+ "tape_based": "tape",
37
+ "stack_based": "stack",
38
+ "grid_based": "grid",
39
+ "queue_based": "queue",
40
+ "other": "other",
41
+ }
42
+
43
+
44
+ def generate(language: str, text: str, width: int | None = None) -> str:
45
+ """Return a program in ``language`` that prints ``text``.
46
+
47
+ ``width`` bounds the program to that many columns for readability;
48
+ :data:`esolangs.tools.wrap.DEFAULT_WIDTH` is the conventional choice.
49
+ The default of ``None`` asks for no bound, so a caller that does not
50
+ want one gets exactly what the generator has always produced.
51
+
52
+ Most languages honour it by *wrapping* the finished program, breaking
53
+ only between whole tokens so it still means the same thing. A few build
54
+ a shape rather than a line -- Clockwise lays its code around a
55
+ rectangle's perimeter -- and cannot be reflowed after the fact; those
56
+ generators take the width themselves and lay the program out to fit.
57
+
58
+ A language whose newlines are semantic (the 2D grid languages) or that
59
+ rejects them outright (NoComment) ignores ``width`` rather than raising,
60
+ so one width can be passed across every language.
61
+ """
62
+ try:
63
+ fn = GENERATORS[language]
64
+ except KeyError:
65
+ raise UnknownLanguageError(language) from None
66
+ if width is not None and takes_width(fn):
67
+ return str(fn(text, width))
68
+ return wrap_program(str(fn(text)), LANGUAGES[language].id, width)
69
+
70
+
71
+ def run(
72
+ language: str,
73
+ program: str,
74
+ stdin: str = "",
75
+ timeout: float | None = None,
76
+ ) -> str:
77
+ """Execute ``program`` and return its output.
78
+
79
+ Input is fed to the program line by line from ``stdin``. ``timeout``
80
+ bounds execution wall-clock: after ``timeout`` seconds the run raises
81
+ :class:`HaltError`. The guard uses ``SIGALRM``, so it requires a Unix
82
+ main thread; elsewhere a ``timeout`` raises :class:`ValueError`.
83
+ """
84
+ if timeout is not None and timeout <= 0:
85
+ raise ValueError(f"timeout must be positive, got {timeout}")
86
+ try:
87
+ module, split, kwargs = RUNNERS[language]
88
+ except KeyError:
89
+ raise UnknownLanguageError(language) from None
90
+ run_fn = importlib.import_module("esolangs.interpreters." + module).run
91
+ io_obj = ScriptedIO(stdin)
92
+ program_args: str | list[str] = program.splitlines() if split else program
93
+ _run(run_fn, program_args, io_obj, timeout, dict(kwargs))
94
+ return io_obj.getvalue()
95
+
96
+
97
+ def _run(
98
+ run_fn: Callable[..., Any],
99
+ program: str | list[str],
100
+ io_obj: ScriptedIO,
101
+ timeout: float | None,
102
+ kwargs: dict[str, int],
103
+ ) -> None:
104
+ """Run ``run_fn``, applying the wall-clock ``timeout`` guard when set."""
105
+ if timeout is None:
106
+ run_fn(program, io_obj, **kwargs)
107
+ elif threading.current_thread() is threading.main_thread() and hasattr(
108
+ signal, "SIGALRM"
109
+ ):
110
+ _run_timed_signal(run_fn, program, io_obj, timeout, kwargs)
111
+ else:
112
+ raise ValueError("the timeout guard uses SIGALRM and needs a Unix main thread")
113
+
114
+
115
+ def _run_timed_signal(
116
+ run_fn: Callable[..., Any],
117
+ program: str | list[str],
118
+ io_obj: ScriptedIO,
119
+ timeout: float,
120
+ kwargs: dict[str, int],
121
+ ) -> None:
122
+ """Run ``run_fn`` under a ``SIGALRM`` wall-clock guard (main thread only)."""
123
+
124
+ def _timeout_handler(_signum: int, _frame: object) -> None:
125
+ # coverage cannot trace a raise inside a signal handler
126
+ raise HaltError(
127
+ f"execution exceeded the {timeout}-second timeout"
128
+ ) # pragma: no cover
129
+
130
+ old = signal.signal(signal.SIGALRM, _timeout_handler)
131
+ signal.setitimer(signal.ITIMER_REAL, timeout)
132
+ try:
133
+ run_fn(program, io_obj, **kwargs)
134
+ finally:
135
+ signal.setitimer(signal.ITIMER_REAL, 0)
136
+ signal.signal(signal.SIGALRM, old)
137
+
138
+
139
+ def describe(language: str) -> dict[str, object]:
140
+ """Return a structured description of ``language``.
141
+
142
+ The summary carries the state model (derived from the interpreter's
143
+ module family), whether the language has text and boolean generators,
144
+ its transpilers, its example programs, and its esolangs.org page.
145
+ """
146
+ try:
147
+ lang = LANGUAGES[language]
148
+ except KeyError:
149
+ raise UnknownLanguageError(language) from None
150
+ module = RUNNERS.get(language)
151
+ family = module[0].split(".")[0] if module else None
152
+ examples = sorted(
153
+ str(p.relative_to(_EXAMPLES.parent)) for p in _EXAMPLES.glob(f"*/{lang.id}.txt")
154
+ )
155
+ transpilers = sorted(
156
+ (source, target)
157
+ for (source, target) in TRANSPILERS
158
+ if source == language or target == language
159
+ )
160
+ return {
161
+ "name": language,
162
+ "id": lang.id,
163
+ "state_model": _STATE_MODELS.get(family) if family else None,
164
+ "interpreter": lang.interpreter,
165
+ "text_generator": lang.generator is not None,
166
+ "boolean_generator": language in BOOLEAN,
167
+ "transpilers": transpilers,
168
+ "examples": examples,
169
+ "wiki_url": f"https://esolangs.org/wiki/{language.replace(' ', '_')}",
170
+ }
171
+
172
+
173
+ def transpile(source: str, target: str, program: str, **kwargs: int) -> str:
174
+ """Rewrite a ``program`` in ``source`` into an equivalent one in ``target``.
175
+
176
+ Transpilers with extra options accept them as keyword arguments (for
177
+ example ``size`` when targeting Circlefuck).
178
+ """
179
+ try:
180
+ fn = TRANSPILERS[(source, target)]
181
+ except KeyError:
182
+ raise UnsupportedTranspilationError(source, target) from None
183
+ return fn(program, **kwargs)
184
+
185
+
186
+ def list_languages() -> list[str]:
187
+ """Return the supported language names, sorted."""
188
+ return sorted(LANGUAGES)
esolangs/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Run the esolangs command-line interface."""
2
+
3
+ from esolangs.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
esolangs/cli.py ADDED
@@ -0,0 +1,145 @@
1
+ """Command-line interface for the esolangs package.
2
+
3
+ Subcommands:
4
+ esolangs list list the supported languages
5
+ esolangs generate <language> <text> print a program that outputs text
6
+ (``--width N`` wraps it to N columns)
7
+ esolangs run <language> <file> run a program through its interpreter
8
+ esolangs transpile <from> <to> <file> rewrite a program into another language
9
+
10
+ For anything else (compilers, tools), invoke the module directly with
11
+ ``python -m``, e.g. ``python -m esolangs.compilers.unsquare``.
12
+ """
13
+
14
+ import sys
15
+
16
+ from esolangs import generate, list_languages, run, transpile
17
+ from esolangs.tools.wrap import DEFAULT_WIDTH
18
+
19
+ USAGE = """usage: esolangs <command> [...]
20
+
21
+ commands:
22
+ list list the supported languages
23
+ generate [--width N] <language> <text>
24
+ print a program that outputs text
25
+ (--width wraps it for readability)
26
+ run <language> <file> run a program through its interpreter
27
+ transpile <from> <to> <file> rewrite a program into another language
28
+
29
+ examples:
30
+ esolangs list
31
+ esolangs generate Circlefuck "Hello, World!"
32
+ esolangs generate --width Polynomial "Hello, World!"
33
+ esolangs run Circlefuck hello.txt
34
+ esolangs transpile brainfuck Circlefuck hello.bf
35
+ """
36
+
37
+
38
+ def _fail(message: str) -> None:
39
+ sys.stderr.write(message + "\n")
40
+ sys.exit(2)
41
+
42
+
43
+ def _is_int(value: str) -> bool:
44
+ """Whether ``value`` parses as an integer."""
45
+ try:
46
+ int(value)
47
+ except ValueError:
48
+ return False
49
+ return True
50
+
51
+
52
+ def _pop_width(rest: list[str]) -> tuple[list[str], int | None]:
53
+ """Split a ``--width N`` (or ``--width=N``) option out of ``rest``.
54
+
55
+ Returns the remaining positional arguments and the width, or ``None``
56
+ when the option is absent -- which leaves the program on one line, the
57
+ output ``generate`` has always produced. A bare ``--width`` takes the
58
+ conventional :data:`DEFAULT_WIDTH`, so the common case needs no number;
59
+ an explicit value must be an integer, since silently reading the
60
+ language name as a width would generate the wrong thing.
61
+ """
62
+ args: list[str] = []
63
+ width: int | None = None
64
+ i = 0
65
+ while i < len(rest):
66
+ arg = rest[i]
67
+ if arg == "--width":
68
+ following = rest[i + 1] if i + 1 < len(rest) else None
69
+ if following is None or not _is_int(following):
70
+ width = DEFAULT_WIDTH
71
+ i += 1
72
+ continue
73
+ value = following
74
+ i += 2
75
+ elif arg.startswith("--width="):
76
+ value = arg.split("=", 1)[1]
77
+ i += 1
78
+ else:
79
+ args.append(arg)
80
+ i += 1
81
+ continue
82
+ try:
83
+ width = int(value)
84
+ except ValueError:
85
+ _fail(f"--width must be an integer, got {value!r}")
86
+ if width is not None and width <= 0:
87
+ _fail(f"--width must be positive, got {width}")
88
+ return args, width
89
+
90
+
91
+ def main() -> None:
92
+ """Dispatch the ``esolangs`` subcommands."""
93
+ argv = sys.argv[1:]
94
+ if not argv:
95
+ sys.stderr.write(USAGE)
96
+ sys.exit(2)
97
+
98
+ cmd, rest = argv[0], argv[1:]
99
+ if cmd == "list":
100
+ for name in list_languages():
101
+ print(name)
102
+ elif cmd == "generate":
103
+ rest, width = _pop_width(rest)
104
+ if len(rest) < 2:
105
+ _fail("usage: esolangs generate [--width N] <language> <text>")
106
+ try:
107
+ program = generate(rest[0], rest[1], width)
108
+ except ValueError as exc:
109
+ _fail(str(exc))
110
+ print(program)
111
+ elif cmd == "run":
112
+ if len(rest) < 2:
113
+ _fail("usage: esolangs run <language> <program-file>")
114
+ language, path = rest[0], rest[1]
115
+ try:
116
+ with open(path) as f:
117
+ program = f.read()
118
+ except OSError as exc:
119
+ _fail(f"cannot read {path}: {exc}")
120
+ stdin = "" if sys.stdin.isatty() else sys.stdin.read()
121
+ try:
122
+ output = run(language, program, stdin)
123
+ except ValueError as exc:
124
+ _fail(str(exc))
125
+ sys.stdout.write(output)
126
+ elif cmd == "transpile":
127
+ if len(rest) < 3:
128
+ _fail("usage: esolangs transpile <from> <to> <program-file>")
129
+ source, target, path = rest[0], rest[1], rest[2]
130
+ try:
131
+ with open(path) as f:
132
+ program = f.read()
133
+ except OSError as exc:
134
+ _fail(f"cannot read {path}: {exc}")
135
+ try:
136
+ output = transpile(source, target, program)
137
+ except ValueError as exc:
138
+ _fail(str(exc))
139
+ sys.stdout.write(output)
140
+ else:
141
+ _fail(f"unknown command: {cmd}\n\n{USAGE}")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1 @@
1
+ """Compilers that turn esolang programs into RISC-V assembly."""
@@ -0,0 +1,143 @@
1
+ """Shared RISC-V assembly fragments for the OISC-style compilers.
2
+
3
+ decleq, S*bleq, and AddSubJump all compile to a fetch-decode-execute loop
4
+ over a ``.data`` array of 8-byte cells (their jump targets are computed at
5
+ runtime, so unlike the token-unrolling compilers they can't emit static
6
+ per-token blocks). These three routines -- byte I/O and the final
7
+ dword-per-row memory dump -- are copy-pasted identically between them;
8
+ keeping one copy here is what the ``duplicate-code`` check in
9
+ ``scripts/verify.py`` enforces.
10
+ """
11
+
12
+ GETBYTE = (
13
+ "# getbyte() -> a0; reads one byte of stdin, halts the program at EOF\n"
14
+ "# (matching the interpreter's EOFError, which unwinds the whole run)\n"
15
+ "getbyte:\n"
16
+ " addi sp, sp, -16\n"
17
+ " sd ra, 8(sp)\n"
18
+ " li a7, 63\n"
19
+ " li a0, 0\n"
20
+ " mv a1, sp\n"
21
+ " li a2, 1\n"
22
+ " ecall\n"
23
+ " blez a0, .halt\n"
24
+ " lbu a0, 0(sp)\n"
25
+ " ld ra, 8(sp)\n"
26
+ " addi sp, sp, 16\n"
27
+ " ret\n"
28
+ )
29
+
30
+ READ_BYTE_OR_EOF = (
31
+ " addi sp, sp, -16\n"
32
+ " sd ra, 8(sp)\n"
33
+ " li a7, 63\n"
34
+ " li a0, 0\n"
35
+ " mv a1, sp\n"
36
+ " li a2, 1\n"
37
+ " ecall\n"
38
+ " blez a0, .eof\n"
39
+ " lbu a0, 0(sp)\n"
40
+ " ld ra, 8(sp)\n"
41
+ " addi sp, sp, 16\n"
42
+ " ret\n"
43
+ )
44
+
45
+ PUTBYTE = (
46
+ "# putbyte(value: a0) -- write the low byte to stdout\n"
47
+ "putbyte:\n"
48
+ " addi sp, sp, -16\n"
49
+ " sd ra, 8(sp)\n"
50
+ " andi t0, a0, 0xff\n"
51
+ " sb t0, 0(sp)\n"
52
+ " li a7, 64\n"
53
+ " li a0, 1\n"
54
+ " mv a1, sp\n"
55
+ " li a2, 1\n"
56
+ " ecall\n"
57
+ " ld ra, 8(sp)\n"
58
+ " addi sp, sp, 16\n"
59
+ " ret\n"
60
+ )
61
+
62
+
63
+ MUL32 = (
64
+ "# a0 *= a1 (unsigned 32-bit), result in a0\n"
65
+ "mul32:\n"
66
+ "\tmv t4, a0\n"
67
+ "\tli a0, 0\n"
68
+ ".mul_loop:\n"
69
+ "\tandi t5, a1, 1\n"
70
+ "\tbeqz t5, .mul_skip\n"
71
+ "\tadd a0, a0, t4\n"
72
+ ".mul_skip:\n"
73
+ "\tslli t4, t4, 1\n"
74
+ "\tsrli a1, a1, 1\n"
75
+ "\tbnez a1, .mul_loop\n"
76
+ "\tret"
77
+ )
78
+
79
+
80
+ def start_preamble(num_cells_used: int) -> str:
81
+ """``.text``/``_start``: point ``s1`` at ``mem``, ``s2``/``s3`` at ip/length.
82
+
83
+ ``num_cells_used`` is the compiled program's cell count (``n``), not
84
+ the fixed buffer size -- ``s3`` bounds the fetch-decode-execute loop
85
+ to the program itself, with the rest of the buffer as zeroed scratch.
86
+ """
87
+ return (
88
+ " .text\n"
89
+ " .global _start\n"
90
+ "_start:\n"
91
+ " la s1, mem\n"
92
+ " li s2, 0\n"
93
+ f" li s3, {num_cells_used}\n"
94
+ )
95
+
96
+
97
+ def cell_load_or_zero(num_cells: int) -> str:
98
+ """Range-check ``a0`` against the cell buffer, load it or return 0.
99
+
100
+ Expects ``s1`` = memory base. Falls through from a caller that has
101
+ already ruled out its own special addresses, so this only needs the
102
+ generic bounds check every ``read_cell`` ends with.
103
+ """
104
+ return (
105
+ " bltz a0, .zero_ret\n"
106
+ f" li t0, {num_cells}\n"
107
+ " bge a0, t0, .zero_ret\n"
108
+ " slli t0, a0, 3\n"
109
+ " add t0, s1, t0\n"
110
+ " ld a0, 0(t0)\n"
111
+ " ret\n"
112
+ ".zero_ret:\n"
113
+ " li a0, 0\n"
114
+ " ret\n"
115
+ )
116
+
117
+
118
+ def cell_store_or_drop(num_cells: int) -> str:
119
+ """Range-check ``a0`` against the cell buffer and store ``a1``, or drop.
120
+
121
+ Expects ``s1`` = memory base. Like :func:`cell_load_or_zero`, this is
122
+ the generic bounds check a ``write_cell`` falls through to after its
123
+ own special addresses are ruled out.
124
+ """
125
+ return (
126
+ " bltz a0, .no_write\n"
127
+ f" li t0, {num_cells}\n"
128
+ " bge a0, t0, .no_write\n"
129
+ " slli t0, a0, 3\n"
130
+ " add t0, s1, t0\n"
131
+ " sd a1, 0(t0)\n"
132
+ ".no_write:\n"
133
+ " ret\n"
134
+ )
135
+
136
+
137
+ def dump_cells(cells: list[int], num_cells: int) -> str:
138
+ """Render ``.data``/``.align``/dword rows for a fixed-size cell buffer."""
139
+ res = " .data\n .align 3\nmem:\n"
140
+ for i in range(0, num_cells, 8):
141
+ row = cells[i : i + 8]
142
+ res += " .dword " + ", ".join(str(v) for v in row) + "\n"
143
+ return res
@@ -0,0 +1,178 @@
1
+ """Compiler that turns AddSubJump programs into RISC-V Linux assembly."""
2
+
3
+ import sys
4
+
5
+ from esolangs.compilers._riscv_common import (
6
+ READ_BYTE_OR_EOF,
7
+ cell_load_or_zero,
8
+ cell_store_or_drop,
9
+ dump_cells,
10
+ start_preamble,
11
+ )
12
+ from esolangs.interpreters.memory import parse_int_memory as _parse
13
+
14
+ # Special addresses, matching esolangs.interpreters.register_based.addsubjump.
15
+ _IO, _CF, _ZF, _NF, _OF, _ONE, _ZERO, _NEG, _FUM = -1, -2, -3, -4, -5, -6, -7, -8, -9
16
+
17
+ # Fixed-size cell buffer: the interpreter's memory grows on out-of-range
18
+ # writes, but a compiled program gets a generous preallocated word array
19
+ # instead (the same tradeoff RAM0's compiler makes for its 256-cell RAM).
20
+ _CELLS = 65536
21
+
22
+
23
+ def comp(code: str) -> str:
24
+ """Compile an AddSubJump program to RISC-V assembly.
25
+
26
+ AddSubJump's memory is self-modifying and its jump target (``*c``) is
27
+ computed at runtime, so unlike the other compilers this cannot unroll
28
+ into static per-token blocks: it emits a real fetch-decode-execute loop
29
+ over a ``.data`` array of 8-byte cells. Registers: ``s1`` = memory base,
30
+ ``s2`` = ip (in cells), ``s3`` = memory length (in cells), ``s4`` = flag
31
+ update mode, ``s5``/``s6`` = zero/negative flags. ``read_cell`` and
32
+ ``write_cell`` centralize the special-address handling (``-1`` I/O,
33
+ ``-2``..``-5`` flags, ``-6``/``-7``/``-8`` constants, ``-9`` flag mode)
34
+ that both operand fetch and the write-back share.
35
+ """
36
+ cells = _parse(code)
37
+ n = len(cells)
38
+ if n < _CELLS:
39
+ cells = cells + [0] * (_CELLS - n)
40
+
41
+ res = (
42
+ start_preamble(n) + " li s4, 0\n"
43
+ " li s5, 0\n"
44
+ " li s6, 0\n"
45
+ ".loop:\n"
46
+ " bltz s2, .halt\n"
47
+ " bge s2, s3, .halt\n"
48
+ " slli t0, s2, 3\n"
49
+ " add t0, s1, t0\n"
50
+ " ld a0, 0(t0)\n" # a
51
+ " ld a1, 8(t0)\n" # b
52
+ " ld a2, 16(t0)\n" # c
53
+ " ld a3, 24(t0)\n" # d
54
+ " mv t1, a0\n" # save a (write target) across calls
55
+ " mv t2, a2\n" # save c (jump target)
56
+ " mv a0, a3\n"
57
+ " call read_cell\n" # vd = read(d)
58
+ " mv t3, a0\n"
59
+ " mv a0, a1\n"
60
+ " call read_cell\n" # vb = read(b)
61
+ " mv t4, a0\n" # vb
62
+ " li t0, -1\n"
63
+ " bne t1, t0, .not_io\n"
64
+ " mv a0, t4\n" # a == -1: write vb straight to I/O
65
+ " j .have_new\n"
66
+ ".not_io:\n"
67
+ " mv a0, t1\n"
68
+ " call read_cell\n" # read(a)
69
+ " blez t3, .add\n"
70
+ " sub a0, a0, t4\n"
71
+ " j .have_new\n"
72
+ ".add:\n"
73
+ " add a0, a0, t4\n"
74
+ ".have_new:\n"
75
+ " mv t5, a0\n" # new
76
+ " mv a1, t5\n"
77
+ " mv a0, t1\n"
78
+ " call write_cell\n"
79
+ " beqz s4, .no_flags\n"
80
+ " seqz s5, t5\n"
81
+ " sltz s6, t5\n"
82
+ ".no_flags:\n"
83
+ " mv a0, t2\n"
84
+ " call read_cell\n" # ip = read(c)
85
+ " mv s2, a0\n"
86
+ " j .loop\n"
87
+ ".halt:\n"
88
+ " li a0, 0\n"
89
+ " li a7, 93\n"
90
+ " ecall\n"
91
+ "\n"
92
+ "# read_cell(addr: a0) -> a0\n"
93
+ "read_cell:\n"
94
+ f" li t0, {_IO}\n"
95
+ " bne a0, t0, 1f\n" + READ_BYTE_OR_EOF + "1:\n"
96
+ f" li t0, {_CF}\n"
97
+ " bne a0, t0, 2f\n"
98
+ " li a0, 0\n"
99
+ " ret\n"
100
+ "2:\n"
101
+ f" li t0, {_ZF}\n"
102
+ " bne a0, t0, 3f\n"
103
+ " mv a0, s5\n"
104
+ " ret\n"
105
+ "3:\n"
106
+ f" li t0, {_NF}\n"
107
+ " bne a0, t0, 4f\n"
108
+ " mv a0, s6\n"
109
+ " ret\n"
110
+ "4:\n"
111
+ f" li t0, {_OF}\n"
112
+ " bne a0, t0, 5f\n"
113
+ " li a0, 0\n"
114
+ " ret\n"
115
+ "5:\n"
116
+ f" li t0, {_ONE}\n"
117
+ " bne a0, t0, 6f\n"
118
+ " li a0, 1\n"
119
+ " ret\n"
120
+ "6:\n"
121
+ f" li t0, {_ZERO}\n"
122
+ " bne a0, t0, 7f\n"
123
+ " li a0, 0\n"
124
+ " ret\n"
125
+ "7:\n"
126
+ f" li t0, {_NEG}\n"
127
+ " bne a0, t0, 8f\n"
128
+ " li a0, -1\n"
129
+ " ret\n"
130
+ "8:\n"
131
+ f" li t0, {_FUM}\n"
132
+ " bne a0, t0, 9f\n"
133
+ " mv a0, s4\n"
134
+ " ret\n"
135
+ "9:\n" + cell_load_or_zero(_CELLS) + ".eof:\n"
136
+ " li a0, 1\n"
137
+ " li a7, 93\n"
138
+ " ecall\n"
139
+ "\n"
140
+ "# write_cell(addr: a0, value: a1)\n"
141
+ "write_cell:\n"
142
+ f" li t0, {_IO}\n"
143
+ " bne a0, t0, 1f\n"
144
+ " addi sp, sp, -16\n"
145
+ " sd ra, 8(sp)\n"
146
+ " andi t0, a1, 0xff\n"
147
+ " sb t0, 0(sp)\n"
148
+ " li a7, 64\n"
149
+ " li a0, 1\n"
150
+ " mv a1, sp\n"
151
+ " li a2, 1\n"
152
+ " ecall\n"
153
+ " ld ra, 8(sp)\n"
154
+ " addi sp, sp, 16\n"
155
+ " ret\n"
156
+ "1:\n"
157
+ f" li t0, {_FUM}\n"
158
+ " bne a0, t0, 2f\n"
159
+ " mv s4, a1\n"
160
+ " ret\n"
161
+ "2:\n"
162
+ f" li t0, {_CF}\n" # -8..-2 (NEG..CF): other specials are read-only
163
+ " bgt a0, t0, 3f\n"
164
+ f" li t1, {_NEG}\n"
165
+ " blt a0, t1, 3f\n"
166
+ " j .no_write\n"
167
+ "3:\n" + cell_store_or_drop(_CELLS)
168
+ )
169
+ return res + dump_cells(cells, _CELLS)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ if len(sys.argv) > 1:
174
+ with open(sys.argv[1]) as f:
175
+ data = f.read()
176
+
177
+ with open("output.asm", "w") as f:
178
+ f.write(comp(data))