Fool

From Esolang
Jump to navigation Jump to search

Fool is an esoteric programming language created by User:DigitalDetective47. Its name comes from the common abbreviations func and bool, which are the only concepts that the language defines. It also references the fact that anyone who would attempt use this language for anything productive is a fool.

Memory

Data is stored on a tape of bits that extends infinitely in both directions. The tape is initially filled with 0s.

Program structure

A program is read as a list of function definitions separated by newlines. Programs must not include a trailing newline. The syntax for a function declaration is:

<function name>:<function code>

Any function name that does not contain any of &().:| or newlines is valid. This means that the empty string is a valid function name. A function name may not be declared multiple times within a single program. Every function takes a bit as input and returns a bit as output. There are three built‐in functions in Fool. First is <, which moves the tape head one cell to the left. It returns its input as its output. Second is >, which move the tape head one cell to the right, copying its input to its output. Third is *. If its input is 1, it flips the bit pointed to by the tape head. It then returns the value of the bit pointed to (after flipping), regardless of its input. There are three special operators that combine two functions together. All operators of the same precedence bind right to left. Most binding is ., which is a function composition. It takes the input to its expression and passes it to the expression bound on the right. It then passes the output from the expression bound on the right and passes it as the input to the expression bound on the left. It then returns the output of the expression bound on the left. (h:g.f defines as .) Less binding are & and |, which act as a binary AND and OR, respectively. Both sides are given the expression's input, and they both use lazy evaluation right‐to‐left. For example, define h:g&f. The process for evaluating is:

  1. Evaluate and store the result as .
  2. If , return 0. Otherwise, evaluate and return .

Parentheses can be used to change the order of binding. As an example of the default operator binding, a.b|c.d&e.f|g.h is parsed as (a.b)|((c.d)&((e.f)|(g.h))). To get ((a.b)|(c.d))&((e.f)|(g.h)), the simplest form would be (a.b|c.d)&e.f|g.h. To get (a.b)|((c.d)&(e.f)|(g.h), the simplest form would be a.b|(c.d&e.f)|g.h. When the program is run, the main function is called with an input of 1, and its output is ignored. A program without a function called main is invalid. All implementations must reject invalid programs.

Examples

Hello, world!

Writes the ASCII representation of the string onto the tape.

H:>.>.>.>.*.>.>.>.*.>
e:>.*.>.>.*.>.>.>.*.>.*.>
l:>.>.>.*.>.*.>.>.*.>.*.>
o:>.*.>.*.>.*.>.*.>.>.*.>.*.>
,:>.>.>.*.>.*.>.>.*.>.>
 :>.>.>.>.>.>.*.>.>
w:>.*.>.*.>.*.>.>.*.>.*.>.*.>
r:>.>.*.>.>.>.*.>.*.>.*.>
d:>.>.>.*.>.>.>.*.>.*.>
!noshift:*.>.>.>.>.>.*.>.>
main:!noshift.d.l.r.o.w. .,.o.l.l.e.H

Truth-machine

To pass a 0 as input, run the program as‐is. To pass a 1 as input, append .* to the last line to set cell 0 to 1.

stepLeft:*.<.*.>|stepLeft&*.<
glideLeft:glideRight&stepLeft
stepRight:*.>.*.<|stepRight&*.>
glideRight:glideLeft&stepRight
main:*.(glideRight.<.*.*.>.*|*)

Infinite loop

Trivial recursive version

main:main

Golfed version

:
main:

(7 characters)

Computational class

Fool can be proven to be Turing-complete as BitChanger can be compiled into it, with the omission of I/O, which is not required for Turing-completeness. First, reverse the program, and replace each square bracket with its opposite ([ → ], ] → [). The non‐looping functions are easy to define: < → <.<, } → >|*.*.>.*. Each bracketed block can be separated into its own function: [<code>] → <some unique name>:<.>|<some unique name>.<code>&*.<.*.*.>.

Implementations

Official interpreter

(The output form used is not part of the specification and is not required for any implementations.)

from __future__ import annotations

from sys import argv, stderr
from typing import Callable, Final
from collections.abc import Callable as CallableABC


class FoolFunc(CallableABC):
    def __init__(self, func: Callable[[int], int], /) -> None:
        self.func = func

    def __add__(self, other: FoolFunc, /) -> FoolFunc:
        return FoolFunc(lambda x: self.func(x) or other.func(x))

    def __call__(self, value: int, /) -> int:
        return self.func(value)

    def __mul__(self, other: FoolFunc, /) -> FoolFunc:
        return FoolFunc(lambda x: other.func(self.func(x)))

    def __sub__(self, other: FoolFunc) -> FoolFunc:
        return FoolFunc(lambda x: self.func(x) and other.func(x))


tape: Final[list[int]] = [0]
pointer_location: int = 0


def shift_left(value: int, /) -> int:
    global pointer_location
    if pointer_location == 0:
        tape.insert(0, 0)
    else:
        pointer_location -= 1
    return value


def shift_right(value: int, /) -> int:
    global pointer_location
    pointer_location += 1
    if pointer_location == len(tape):
        tape.append(0)
    return value


def star(value: int, /) -> int:
    result = value ^ tape[pointer_location]
    tape[pointer_location] = result
    return result


function_table: Final[dict[str, FoolFunc]] = {
    "*": FoolFunc(star),
    "<": FoolFunc(shift_left),
    ">": FoolFunc(shift_right),
}

if len(argv) < 2:
    print(
        "Pass the location of the script to run as a command-line argument.",
        file=stderr,
    )
elif len(argv) > 2:
    print("Unknown command-line arguments passed.", file=stderr)
else:

    try:
        with open(argv[1]) as source:
            statements = source.readlines()
        for line_number, line_contents in tuple(enumerate(statements))[:-1]:
            statements[line_number] = line_contents[:-1]

        current_function_name: str
        function_code: str
        function_names: Final[list[str]] = []
        num_colons: int
        unclosed_parens: int
        for line_number, statement in enumerate(statements, 1):
            num_colons = statement.count(":")
            if num_colons > 1:
                raise SyntaxError(f"Multiple colons found on line {line_number}.")
            elif num_colons == 0:
                raise SyntaxError(f"Missing colon on line {line_number}.")
            current_function_name, function_code = statement.split(":", 1)
            if not frozenset(current_function_name).isdisjoint(frozenset("&().|")):
                raise SyntaxError(f"Invalid function name on line {line_number}")
            elif current_function_name in frozenset("*<>"):
                raise SyntaxError(
                    f"Attempted to overwrite default function {current_function_name} on line {line_number}."
                )
            try:
                raise SyntaxError(
                    f"Found multiple definitions for function {current_function_name}. (First 2 occurrences on lines {function_names.index(current_function_name)} & {line_number}.)"
                )
            except ValueError:
                # Means function name not in list, this should be raised each time.
                unclosed_parens = 0
                for char in statement:
                    if char == "(":
                        unclosed_parens += 1
                    elif char == ")":
                        unclosed_parens -= 1
                    if unclosed_parens < 0:
                        raise SyntaxError(
                            f"Unmatched parenthesis on line {line_number}."
                        )
                if unclosed_parens != 0:
                    raise SyntaxError(f"Unmatched parenthesis on line {line_number}.")
                function_names.append(current_function_name)
                function_table[current_function_name] = FoolFunc(lambda: None)
        if "main" not in function_names:
            raise SyntaxError("main function not found.")

        function_definition_expression: str
        is_function_name: bool
        for line_number, statement in enumerate(statements, 1):
            current_function_name, function_code = statement.split(":", 1)
            function_definition_expression = ""
            is_function_name = True
            for token in reversed(
                function_code.replace("&", ": - :")
                .replace("(", "(:")
                .replace(")", ":)")
                .replace(".", ": * :")
                .replace("|", ": + :")
                .split(":")
            ):
                if token == "(":
                    function_definition_expression += ")"
                elif token == ")":
                    function_definition_expression += "("
                else:
                    if is_function_name:
                        if token in function_table:
                            function_definition_expression += (
                                f"function_table[{token!r}]"
                            )
                        else:
                            raise SyntaxError(
                                f"Unrecognized function {token} on line {line_number}."
                            )
                    elif token in frozenset({" * ", " + ", " - "}):
                        function_definition_expression += token
                    else:
                        raise SyntaxError(f"Invalid syntax on line {line_number}")
                    is_function_name = not is_function_name
            function_table[current_function_name].func = eval(
                function_definition_expression
            )
    except OSError:
        print("There was an issue accessing the file you requested.", file=stderr)
    except SyntaxError as e:
        print(e, file=stderr)
    else:
        main_return: Final[int] = function_table["main"](1)
        print(f"...{''.join(map(str, tape))}... [{main_return}]")