15
\$\begingroup\$

This is the cops' challenge. To post a robber, go here.

In this challenge, cops will invent a (likely simple) programming language, and write an interpreter, transpiler, or compiler that allows you to run it. Robbers will write a program in this language that manages to inject arbitrary code into the interpreter, transpiler, or compiler.

Cops

Cops should design a language. This language should be "pure"; its programs can take input and/or produce output, but cannot affect files, make network requests, behave non-deterministically (based on something like randomness or the system time), or otherwise impact or be impacted by the outside world (within reason).

Cops must then implement this language, with one of:

  • An interpreter: This will take the code and input as arguments/input, and produce the programs's output as output.
  • A transpiler: This will take the code and input, and produce code in some other language which does the same task. When run using that language's interpreter/compiler with input, the correct output is produced.
  • A compiler: Similar to a transpiler, but with machine code instead of another language.

There must be an intended "crack", or way to inject arbitrary code.

Robbers

Robbers should find a way to do one of the following:

  • Write a program that, when interpreted, transpiled, or compiled, can run arbitrary code as the interpreter/transpiler/compiler (e.g., if written in Python, you could run arbitrary Python, manipulate files, and so on)
  • Write a program that, when transpiled or compiled, injects arbitrary code into the resulting program/machine code (e.g., if your transpiler converts the language to Node.js, you could inject arbitrary JS into the transpiler output)

Cop Posts

Cops should include the interpreter/transpiler/compiler code, and some documentation for the language. For example:

Print1Lang

function transpile(code) {
    return code.replace(/^.+/g, "print(1)");
}

Print1Lang takes code as input, and returns a Python program that prints 1.

Cops should be designed with some exploit in mind that allows injecting any code, in some language or situation that allows performing arbitrary computations and interacting with the computer's files, running shell commands, system calls, non-deterministic code, or anything like that. Simply printing a nondeterministic value does not count, as it does not give you unrestricted access to the environment the code or transpilation/compilation output is being run or transpiled/compiled in.

For an interpreter, this is likely going to be running code in the language the interpreter is written in. For a transpiler or compiler, the vulnerability can occur at either compile time or run time, and for the latter, the vulnerability would most likely consist of being able to inject arbitrary code or machine code into the compiled code.

Other

All the boring stuff. Nothing malicious, no crypto stuff.

Winner for cops is the user with the most safe cops, winner for robbers is the user who cracks the most cops.

Note:

This challenge is intended for languages to be designed for the challenge specifically, but using existing languages is allowed. In this case, you can define a subset of the language such that it follows the rules about non-deterministic commands.

\$\endgroup\$
8
  • \$\begingroup\$ So cat converting to a Brainfuck program is valid but boring? \$\endgroup\$
    – l4m2
    Jan 29, 2022 at 2:40
  • \$\begingroup\$ So if we make a transpiled language, does the transpilation target language have to be able to interact with files and run shell commands? Or is it enough if it just allows for non-deterministic code? \$\endgroup\$
    – DLosc
    Jan 29, 2022 at 4:12
  • \$\begingroup\$ @DLosc This is a good question. I think any non-deterministic behavior should be fine. \$\endgroup\$ Jan 29, 2022 at 4:22
  • \$\begingroup\$ Does the language have to be turing complete? \$\endgroup\$ Feb 12, 2022 at 7:33
  • \$\begingroup\$ Can my crack (and robbers' solutions) choose the input? \$\endgroup\$ Feb 12, 2022 at 10:42

9 Answers 9

8
\$\begingroup\$

Exceptionally (cracked by emanresu A)

Exceptionally is a toy language I invented for this challenge. It is inspired by, implemented in, and transpiled to Whython (pxeger's modified version of Python 3 with an added exception-handling operator).

The language

A program in Exceptionally contains of a series of lines, each consisting of a command (or multiple commands connected with the Rescue operator; see below under Exceptions). The lines are executed one by one. When the instruction pointer reaches the end of the program, it wraps around to the beginning. The program thus forms an infinite loop; the only way to break out of the loop is by causing an error. Every Exceptionally program either does not terminate or terminates with an exception.

Each command consists of a symbol, optionally followed by an argument. The argument can be an integer literal, a string literal, or a variable name.

Most commands modify the value of a register, initially set to 0. A command without an explicit argument uses the value of the register as its argument. For example, the command *3 multiplies the register by 3, while the command * multiplies the register by itself.

Commands

Here is the full list of commands:

  • {: Load (copy value into register)
  • }: Store (copy register into variable)
  • +: Add (value to register)
  • -: Subtract (value from register)
  • *: Multiply (register by value)
  • /: Divide (register by value--floating point division)
  • %: Mod (register by value)
  • ^: Pow (take register to the power of value--result may be floating point)
  • :: Item (at given index in register)
  • [: Slice From (given index to end of register)
  • ]: Slice To (given index from beginning of register)
  • @: Find (value's index in register)
  • #: Count (occurrences of value in register)
  • |: Split (register by value)
  • $: Join (register on value)
  • <: Print (value)
  • >: Input (line of stdin into variable)
  • =: Equal (assert that register is equal to value)
  • !: Skip (skip execution of the given number of lines)
  • \: Func (apply the given function to the register)

The Skip command ! provides simple control flow. Think of it as a goto, relative to the current line.

The Func command \ takes a string as its argument and does one of several things:

  • \"int": Cast the register to an integer
  • \"str": Cast the register to a string
  • \"ord": Convert a single-character register value to its character code
  • \"chr": Convert an integer register value to the corresponding character
  • \"elems": Convert a string or list register value to a list of its elements
  • \"len": Get the length of the register
  • \"sum": Sum/concatenate the register
  • \"range": Get the range from 0 to the register's value (exclusive)
  • \"wrap": Wrap the register's value in a singleton list
  • \"inv": Reverse the register's value, or negate it if it's a number

Exceptions

Most commands are capable of triggering an exception in some way. For example, dividing by 0 will cause an exception, as will an out-of-bounds index, as will trying to add a string and a number. These exceptions are the only way to end the program, but they don't have to end the program. They can be caught using the Rescue operator ?, borrowed from Whython.

A command may be followed by ? and an additional command. If the first command succeeds, execution continues to the next line. If the first command causes an exception, the second command is executed instead. A line can contain any number of commands chained with ?. If the last command is reached and it also causes an exception, then the program halts.

For example, consider the line /x ? <"Division by zero". The command /x attempts to divide the register by the value of x. If x is zero, this operation will trigger an exception, in which case the second command <"Division by zero" is executed, printing an error message (and leaving the value of the register unchanged).

The ? operator is the only conditional construct in Exceptionally. Different commands can be used to trigger exceptions under specific circumstances, and ! can be used to jump to different points in the program depending on the results. For example, in =5 ? !4, the = command raises an exception if the register does not equal 5; in this case the ! command is executed, skipping the next four lines. Or again, the following two lines:

-5
/ ? !4

will skip four lines if the register equals 5: -5 subtracts 5 from the register, and / divides it by itself, resulting in 1 if it is nonzero or an exception if it is zero.

Miscellaneous

Exceptionally has comments that start with ' and go until the next newline.

Whitespace is generally unimportant in Exceptionally. This program to square an input number and halt:

>
\"int"
*
<
/0

could also be written as >\"int"*</0. Newlines that end comments, and whitespace in strings, are the only significant whitespace.

The transpiler

Here is the Exceptionally transpiler, written in Whython:

import re
import sys

COMMANDS = {
    "{": "reg := %s",               # Load
    "}": "%s := reg",               # Store
    "+": "reg := reg + %s",         # Add
    "-": "reg := reg - %s",         # Sub
    "*": "reg := reg * %s",         # Mul
    "/": "reg := reg / %s",         # Div
    "%": "reg := reg %% %s",        # Mod
    "^": "reg := reg ** %s",        # Pow
    ":": "reg := reg[%s]",          # Item
    "[": "reg := reg[%s:]",         # SliceFrom
    "]": "reg := reg[:%s]",         # SliceTo
    "@": "reg := reg.index(%s)",    # Find
    "#": "reg := reg.count(%s)",    # Count
    "|": "reg := reg.split(%s)",    # Split
    "$": "reg := %s.join(reg)",     # Join
    "<": "print(%s)",               # Print
    ">": "%s := input()",           # Input
    "=": "1 / (reg == %s)",         # Equal
    "!": "ip := ip + %s",           # Skip
    "\\": "reg := FUNCS[%s](reg)",  # Func
    }

PROGRAM_TEMPLATE = """program = %s
FUNCS = {
"int": int,
"str": str,
"chr": chr,
"ord": ord,
"elems": list,
"len": len,
"sum": lambda x: sum(x) ? sum(x, []) ? "".join(x),
"range": lambda x: list(range(x)),
"wrap": lambda x: [x],
"inv": lambda x: x[::-1] ? -x,
}
ip = 0
reg = 0
while True:
    eval(program[ip])
    ip = (ip + 1) %% len(program)"""

def transpile(code):
    transpiled_lines = []
    code = code.lstrip()
    if code[0] == "?":
        raise SyntaxError(f"Program cannot begin with {code[0]}")
    while code:
        if m := re.match("'.*", code):
            code = code[m.end():].lstrip()
            continue
        if code[0] == "?":
            # Continuing a previous line
            operator = code[0]
            transpiled_lines[-1] += f" {operator} "
            code = code[1:].lstrip()
            if not code:
                raise SyntaxError(f"Program cannot end with {operator}")
        else:
            # Start of a new line
            transpiled_lines.append("")
        if code[0] in COMMANDS:
            command = code[0]
            code = code[1:].lstrip()
            # Parse the command's argument
            if m := re.match(r"\w+", code):
                # Name or integer literal
                argument = m.group()
                argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
                code = code[m.end():].lstrip()
            elif m := re.match(r'"[^"]*"', code):
                # String literal
                argument = repr(m.group())
                code = code[m.end():].lstrip()
            else:
                # No argument, defaults to register
                argument = "'reg'"
            argument = eval(argument) ? f"eval({argument})"
            translation = "(" + COMMANDS[command] % argument + ")"
            transpiled_lines[-1] += translation
        else:
            raise SyntaxError(f"Expected command, found: {code[0]}")
    return PROGRAM_TEMPLATE % transpiled_lines

Attempt This Online!

You can run the transpiler for yourself at Attempt This Online. Put your Exceptionally code in the Input box and click Execute; the program will output the transpiled Whython code.

Here is a version that immediately executes the transpiled code: Attempt This Online.

Example program

Here is a commented program that outputs the orbit of an input number under the Collatz function. (This is the program used in the second ATO link above.)

' Read initial number from stdin
>
' Convert to an integer
\"int"
' Store a copy in n
}n
' Output
<
' Minus 1
-1
' Divide by itself; if n is 1, (n-1)/(n-1) is division by zero and the program halts
/
' Otherwise, keep going; load n back into the register
{n
' Mod 2
%2
' Is this equal to 0? If not, skip the next three lines
=0 ? !3
' Load n
{n
' Divide by 2
/2
' Skip the next three lines
!3
' Load n
{n
' Multiply by 3
*3
' Add 1
+1
' Skip the next line (that is, skip the first line when the program loops)
!1
\$\endgroup\$
2
  • 1
    \$\begingroup\$ +1 for first new lang made for this challenge and also cause it looks fun \$\endgroup\$ Jan 31, 2022 at 14:26
  • \$\begingroup\$ Cracked! \$\endgroup\$
    – emanresu A
    Jan 31, 2022 at 18:57
8
\$\begingroup\$

Vyxal 2.7 (safe)

Yes, I found another another one.

No, I'm not JoKing.

As the rules call for deterministic languages, commands involving randomness are not allowed. Also, the EĖ† commands are not allowed, since those are designed to execute arbitrary code.

Code and documentation can be found at the repo.

Solution:

If you look at the recent releases and commits in the GitHub repo, you may notice that we don't like SymPy anymore. There's a reason for that. It turns out that it doesn't play nicely with strings. More specifically, a lot of the time, when you pass a string to it, you can make it do unintended stuff. For example, if you do sympy.nsimplify("f'{print(69)}'"), it will evaluate the f-string contained within the string, resulting in it printing 69. The ACE was originally found using øḋ, but there were actually a lot of commands that used it. This has now been fixed.

\$\endgroup\$
2
  • 6
    \$\begingroup\$ This is basically free labour and I like it. \$\endgroup\$
    – lyxal
    Jan 29, 2022 at 4:35
  • 5
    \$\begingroup\$ Plot twist: I didn’t actually find another ACE, I’m just hoping somebody else will find it for me. \$\endgroup\$ Jan 29, 2022 at 6:56
5
+250
\$\begingroup\$

Exceptionally v0.3 (safe)

Exceptionally is a toy language I invented for this challenge. It is inspired by, implemented in, and transpiled to Whython (pxeger's modified version of Python 3 with an added exception-handling operator).

This is the third (and hopefully final) version of the language, but the only difference is in the implementation; the language spec is the same as before. See the original post for a full description.

New transpiler

Here is the Exceptionally v0.3 transpiler, written in Whython:

import re
import sys

COMMANDS = {
    "{": "reg := %s",               # Load
    "}": "%s := reg",               # Store
    "+": "reg := reg + %s",         # Add
    "-": "reg := reg - %s",         # Sub
    "*": "reg := reg * %s",         # Mul
    "/": "reg := reg / %s",         # Div
    "%": "reg := reg %% %s",        # Mod
    "^": "reg := reg ** %s",        # Pow
    ":": "reg := reg[%s]",          # Item
    "[": "reg := reg[%s:]",         # SliceFrom
    "]": "reg := reg[:%s]",         # SliceTo
    "@": "reg := reg.index(%s)",    # Find
    "#": "reg := reg.count(%s)",    # Count
    "|": "reg := reg.split(%s)",    # Split
    "$": "reg := %s.join(reg)",     # Join
    "<": "print(%s)",               # Print
    ">": "%s := input()",           # Input
    "=": "1 / (reg == %s)",         # Equal
    "!": "ip := ip + %s",           # Skip
    "\\": "reg := program.FUNCS[%s](reg)",  # Func
    }

PROGRAM_TEMPLATE = """class Program: pass
program = Program()
program.LINES = %s
program.FUNCS = {
"int": int,
"str": str,
"chr": chr,
"ord": ord,
"elems": list,
"len": len,
"sum": lambda x: sum(x) ? sum(x, []) ? "".join(x),
"range": lambda x: list(range(x)),
"wrap": lambda x: [x],
"inv": lambda x: x[::-1] ? -x,
}
ip = 0
reg = 0
while True:
    eval(program.LINES[ip])
    ip = (ip + 1) %% len(program.LINES)"""

BUILTIN_NAMES = dir(__builtins__)

def transpile(code):
    transpiled_lines = []
    code = code.lstrip()
    if code[0] == "?":
        raise SyntaxError(f"Program cannot begin with {code[0]}")
    while code:
        if m := re.match("'.*", code):
            code = code[m.end():].lstrip()
            continue
        if code[0] == "?":
            # Continuing a previous line
            operator = code[0]
            transpiled_lines[-1] += f" {operator} "
            code = code[1:].lstrip()
            if not code:
                raise SyntaxError(f"Program cannot end with {operator}")
        else:
            # Start of a new line
            transpiled_lines.append("")
        if code[0] in COMMANDS:
            command = code[0]
            code = code[1:].lstrip()
            # Parse the command's argument
            if m := re.match(r"\w+", code):
                # Name or integer literal
                argument = m.group()
                if argument in BUILTIN_NAMES and command == "}":
                    raise SyntaxError(f"Cannot overwrite built-in name {argument}")
                argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
                code = code[m.end():].lstrip()
            elif m := re.match(r'"[^"\\]*"', code):
                # String literal
                argument = repr(m.group())
                code = code[m.end():].lstrip()
            else:
                # No argument, defaults to register
                argument = "'reg'"
            argument = eval(argument) ? f"eval({argument})"
            translation = "(" + COMMANDS[command] % argument + ")"
            transpiled_lines[-1] += translation
        else:
            raise SyntaxError(f"Expected command, found: {code[0]}")
    return PROGRAM_TEMPLATE % transpiled_lines

Changelog

The second version was cracked thanks to unescaped backslashes in string literals. To close this loophole, backslashes are no longer allowed in string literals. A string can contain any character that is not \ or ".

I've also closed another unintended loophole: previously, you could overwrite range with exec and then do \"range" to execute an arbitrary string. Now you can no longer use a Whython builtin as the argument for the } command.

Attempt This Online!

You can run the transpiler for yourself at Attempt This Online. Put your Exceptionally code in the Input box and click Execute; the program will output the transpiled Whython code.

Here is a version that immediately executes the transpiled code: Attempt This Online.

Solution

There are two suspiciously convoluted lines in the transpiler. First,

argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"

which does this:

Try to cast argument to an integer. If that works and the integer is nonzero, remove any leading 0s from argument; if the integer is zero, use "0"; if argument cannot be converted to an integer, wrap it in single quotes. (Note also that literal strings are repr'd, and the register as an argument is 'reg' rather than just reg.)

Then,

argument = eval(argument) ? f"eval({argument})"

If the argument was a name, it should be wrapped in quotes at this point, so eval gives just the name as a string. If the argument was a number, eval gives the number as a Python int. If the argument was a string, eval reverses the repr we did earlier. If eval fails (???), then we substitute argument into a string that has the effect of eval'ing it at post-transpilation runtime (???).

What kind of input will trigger this case?

We need an argument matching the regex \w+ that does not error when you pass it to int, but does error when you strip leading zeros and pass it to eval. Solution: Python 3 (and therefore Whython) allows underscores as digit-group separators in numeric literals. Thus, 0_1 is a legal int literal (with a value of 1); but when we strip the leading zero, _1 is no longer a legal int literal, but rather a name. When we try to eval it, it errors because _1 is not defined. So instead of 1 or _1 being placed in the transpiled code, we get eval(_1).

So the exploit is:

If we set the variable _1 to some string value, any reference to 0_1 will cause it to be eval'd. Here's a simple example:

{"exec('import os; print(os.getcwd()); exit(0)')"
}_1
<0_1

Transpiled, this becomes (in essence):

reg := "exec('import os; print(os.getcwd()); exit(0)')"
_1 := reg
print(eval(_1))
Attempt This Online!

\$\endgroup\$
4
\$\begingroup\$

Javastack, Cracked by tsh

Not much of a language but it should be enough. Crappy programming for the win!

Code + docs is in the repo.

Since this must be deterministic, random builtins are banned.

As tsh pointed out, there's a small chance loop variables may coincide, but that chance is so small that you may simply consider it to not happen.

\$\endgroup\$
3
  • \$\begingroup\$ But won't this language be non-deterministically as there is a probability when secret === secret2? \$\endgroup\$
    – tsh
    Jan 29, 2022 at 1:58
  • \$\begingroup\$ @tsh True. It's incredibly unlikely though. \$\endgroup\$
    – emanresu A
    Jan 29, 2022 at 2:02
  • \$\begingroup\$ codegolf.stackexchange.com/a/241959/44718 \$\endgroup\$
    – tsh
    Jan 29, 2022 at 2:28
4
\$\begingroup\$

Vyxal, since v2.6 (safe)

Why does Vyxal have so many ACEs?

Commands with randomness aren’t allowed, and neither are EĖ†. Info is in the repo.

\$\endgroup\$
3
\$\begingroup\$

Jyxal, Cracked by redwolf

Jyxal is a half-baked JS implementation of Vyxal. While it has most core features (loops, functions, lambdas etc), it has almost no elements, but it's still possible to ACE it.

Vyxal docs should apply for most of it, but for some parts you may have to read the code.

Intended solution:

@=1,console.log("ACE"),process.exit(),'+',d|;

Quite similar to Redwolf's, this gets compiled into

var arity = '=1,console.log("ACE"),process.exit(),d';FUNC_=1,console.log("ACE"),process.exit(),d = <meaningless junk>

Just like this Vyxal ACE, this exploits how arbitrary code can be inserted into functions.

\$\endgroup\$
1
3
\$\begingroup\$

Exceptionally v0.2 (cracked by emanresu A)

Exceptionally is a toy language I invented for this challenge. It is inspired by, implemented in, and transpiled to Whython (pxeger's modified version of Python 3 with an added exception-handling operator).

This is the second version of the language, but the only difference is in the implementation; the language spec is the same as before. See the original post for a full description.

New transpiler

Here is the Exceptionally v0.2 transpiler, written in Whython:

import re
import sys

COMMANDS = {
    "{": "reg := %s",               # Load
    "}": "%s := reg",               # Store
    "+": "reg := reg + %s",         # Add
    "-": "reg := reg - %s",         # Sub
    "*": "reg := reg * %s",         # Mul
    "/": "reg := reg / %s",         # Div
    "%": "reg := reg %% %s",        # Mod
    "^": "reg := reg ** %s",        # Pow
    ":": "reg := reg[%s]",          # Item
    "[": "reg := reg[%s:]",         # SliceFrom
    "]": "reg := reg[:%s]",         # SliceTo
    "@": "reg := reg.index(%s)",    # Find
    "#": "reg := reg.count(%s)",    # Count
    "|": "reg := reg.split(%s)",    # Split
    "$": "reg := %s.join(reg)",     # Join
    "<": "print(%s)",               # Print
    ">": "%s := input()",           # Input
    "=": "1 / (reg == %s)",         # Equal
    "!": "ip := ip + %s",           # Skip
    "\\": "reg := program.FUNCS[%s](reg)",  # Func
    }

PROGRAM_TEMPLATE = """class Program: pass
program = Program()
program.LINES = %s
program.FUNCS = {
"int": int,
"str": str,
"chr": chr,
"ord": ord,
"elems": list,
"len": len,
"sum": lambda x: sum(x) ? sum(x, []) ? "".join(x),
"range": lambda x: list(range(x)),
"wrap": lambda x: [x],
"inv": lambda x: x[::-1] ? -x,
}
ip = 0
reg = 0
while True:
    eval(program.LINES[ip])
    ip = (ip + 1) %% len(program.LINES)"""

def transpile(code):
    transpiled_lines = []
    code = code.lstrip()
    if code[0] == "?":
        raise SyntaxError(f"Program cannot begin with {code[0]}")
    while code:
        if m := re.match("'.*", code):
            code = code[m.end():].lstrip()
            continue
        if code[0] == "?":
            # Continuing a previous line
            operator = code[0]
            transpiled_lines[-1] += f" {operator} "
            code = code[1:].lstrip()
            if not code:
                raise SyntaxError(f"Program cannot end with {operator}")
        else:
            # Start of a new line
            transpiled_lines.append("")
        if code[0] in COMMANDS:
            command = code[0]
            code = code[1:].lstrip()
            # Parse the command's argument
            if m := re.match(r"\w+", code):
                # Name or integer literal
                argument = m.group()
                argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
                code = code[m.end():].lstrip()
            elif m := re.match(r'"[^"]*"', code):
                # String literal
                argument = repr(m.group())
                code = code[m.end():].lstrip()
            else:
                # No argument, defaults to register
                argument = "'reg'"
            argument = eval(argument) ? f"eval({argument})"
            translation = "(" + COMMANDS[command] % argument + ")"
            transpiled_lines[-1] += translation
        else:
            raise SyntaxError(f"Expected command, found: {code[0]}")
    return PROGRAM_TEMPLATE % transpiled_lines

Changelog

The first version was cracked because too many parts of the interpreter were modifiable from within the code. To close this loophole, I've changed the program variable to program.LINES and the FUNCS variable to program.FUNCS. Since variable names in Exceptionally cannot contain dots, these changes should make the program contents hack-proof.

Attempt This Online!

You can run the transpiler for yourself at Attempt This Online. Put your Exceptionally code in the Input box and click Execute; the program will output the transpiled Whython code.

Here is a version that immediately executes the transpiled code: Attempt This Online.

\$\endgroup\$
1
3
\$\begingroup\$

Vyxal 2.10, cracked

Yep, another another another one.

Again, commands with randomness aren’t allowed, and neither are EĖ†. Info is in the repo.

\$\endgroup\$
2
  • 3
    \$\begingroup\$ Terse. Elegant. Readable. And best of all, it can run anything Python can :p \$\endgroup\$ Apr 18, 2022 at 23:07
  • \$\begingroup\$ Cracked \$\endgroup\$
    – Seggan
    Apr 19, 2022 at 1:05
2
\$\begingroup\$

Vyxal 2.7.2 to 2.11.1 (safe, patched in v2.11.2)

Yet another ACE in Vyxal.

Commands with randomness aren’t allowed, and neither are EĖ†. Info is in the repo.

Explanation:

The vulnerability is in ∆K (Stationary Points) and ∆¢ (Local Maxima). These both use sympy directly on a string, meaning you can just inject an f-string, like this:

f"{open('look, an ACE!', 'w').write('!')}" ∆K

or:

f"{open('look, an ACE!', 'w').write('!')}" ∆¢

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.