We are currently working on new rules for what content should and shouldn't be allowed on this website, and are looking for feedback! See Esolang:2026 topicality proposal to view and give feedback on the current draft.

Simp

From Esolang
Jump to navigation Jump to search
Simp
Paradigm(s) Functional
Designed by User:Hakerh400
Appeared in 2026
Computational class Turing complete
Major implementations Implemented
File extension(s) .txt

Simp is an esolang invented by User:Hakerh400 in 2026.

Overview

A term is either a variable or a function call. Variables are represented as single-letter identifiers, or identifiers starting with underscore. Function call is represented as identifier followed by a list of terms separated by commas inside parentheses. For example func(x, 5) - the function name is func and arguments are variable x and constant 5 (constants are nullary functions, 5() is the same as 5).

A rewriting rule consists of two terms: LHS and RHS, separated by ===. For example: x * 0 === 0 - it means whenever x * 0 occurs as a subterm anywhere, it will be replaced with 0. Here, x is a variable, so it will match any term. Every variable on the RHS must also appear on LHS, and every variable on the LHS cannot appear multiple times. Rules such as eq(x, x) === true are invalid, because x appears two times on the LHS.

There are no declarations or definitions. The set of identifiers and their arities is directly inferred from the rewriting rules. For example, if we define a rule add(x, 0) === x, then we will automatically have global function add with arity 2. Note also that arity is part of the function name in some sense, so we can have two functions with different arities, but the same name such as reverse(x) === reverse(nil, x) - in this example we define rule for evaluating reverse/1 by delegating the computation to function reverse/2 that uses an accumulator.

Note that if a function has non-zero arguments, even if its name is a single-letter identifier, it is a function and not a variable. For example, f(x, y) means calling function f with variables x and y. But f is not a variable.

Syntax

New operators can be defined using directives such as prefix, postfix, infixl, infixr. For example, we can do infixl "+" and then n + m will be parsed as +(n, m) - that is, a function named + called with arguments n and m. Note that +(n, m) is not a valid syntax, so there is no way to have a function named + without using operators.

Multi-digit natural numbers can be parsed using the syntax_nat directive. For example, syntax_nat natCons will enable parsing numbers such as 375 as natCons(natCons(3, 7), 5). It folds multi-digit number from the left, and individual digits are just global constants (nullary functions).

Directive syntax_list nil cons allows parsing lists. For example, [x, y, z] will be parsed as cons(x, cons(y, cons(z, nil))). Directive syntax_char char will enable parsing characters ('a' will be parsed as char(97), which in turn parses to char(natCons(9, 7)) if the syntax_nat directive is enabled). Directive syntax_str str is for strings (for example "abc" is parsed as str(['a', 'b', 'c'])).

I/O format

Input and output are strings. They are parsed according to the syntax_str directive. The main expression is then constructed as main(input), where input is the input string. The main expression is then reduced according to the reduction rules.

The order of applying rules is not specified. If multiple rules can be applied at the same time to the same subterm, it is undefined behavior. The order of reduction is from innermost subterms, to the outwards. For example, if we have rules

g(x) === 5
f(g(x)) === 7

and we want to reduce expression f(g(3)), we first do the innermost expression, which is 3. But there are no rules for reducing it, so it is already in the normal form. Then we go one step up, and reduce g(3) to 5. Then we try to reduce f(5), but there are no rules for it, so we leave it as-is.

Note the difference between the evaluation strategy (which subterm to reduce next) and the rule selection strategy (which rule to apply if multiple match). Subterms are always reduced from innermost to outermost, but the order of applying rules to each particular subterm is undefined.

Example

Brainfuck interpreter:

infixr "->"
infixr "||"
infixr "&&"
prefix "!"
infixl "==" "/="
infixl "+" "-"
infixl "*" "/" "%"
infixr "^"
infixl "++"
infixr "::"
syntax_nat natCons
syntax_list [] ::
syntax_char char
syntax_str str

-- Ordered pair

fst(pair(a, b)) === a
snd(pair(a, b)) === b

-- Natural number

nat(zero) === zero
nat(nb(a, x)) === nb(a, x)
nb(zero, b0) === zero
succ(a) === a + n1
pred(a) === a - n1
n1 === nb(zero, b1)
n10 === nb(nb(nb(nb(zero, b1), b0), b1), b0)
n48 === nb(nb(nb(nb(nb(nb(zero, b1), b1), b0), b0), b0), b0)
a /= b === !(a == b)

-- Digits

n(a) === ofDigits(a)
ofDigits(0) === zero
ofDigits(1) === nb(zero, b1)
ofDigits(2) === nb(nb(zero, b1), b0)
ofDigits(3) === nb(nb(zero, b1), b1)
ofDigits(4) === nb(nb(nb(zero, b1), b0), b0)
ofDigits(5) === nb(nb(nb(zero, b1), b0), b1)
ofDigits(6) === nb(nb(nb(zero, b1), b1), b0)
ofDigits(7) === nb(nb(nb(zero, b1), b1), b1)
ofDigits(8) === nb(nb(nb(nb(zero, b1), b0), b0), b0)
ofDigits(9) === nb(nb(nb(nb(zero, b1), b0), b0), b1)
ofDigits(natCons(a, b)) === ofDigits(a) * n10 + ofDigits(b)

toDigit(zero) === 0
toDigit(nb(zero, b1)) === 1
toDigit(nb(nb(zero, b1), b0)) === 2
toDigit(nb(nb(zero, b1), b1)) === 3
toDigit(nb(nb(nb(zero, b1), b0), b0)) === 4
toDigit(nb(nb(nb(zero, b1), b0), b1)) === 5
toDigit(nb(nb(nb(zero, b1), b1), b0)) === 6
toDigit(nb(nb(nb(zero, b1), b1), b1)) === 7
toDigit(nb(nb(nb(nb(zero, b1), b0), b0), b0)) === 8
toDigit(nb(nb(nb(nb(zero, b1), b0), b0), b1)) === 9
toDigits(zero) === 0
toDigits(nb(a, x)) === toDigitsStep(divMod(nb(a, x), n10))
toDigitsStep(pair(q, r)) === toDigitsCheck(q == zero, q, r)
toDigitsCheck(true, q, r) === toDigit(r)
toDigitsCheck(false, q, r) === natCons(toDigits(q), toDigit(r))

-- Show & parse

show(zero) === showNat(zero)
show(nb(a, b)) === showNat(nb(a, b))
showNat(a) === showNat1(a, a == zero)
showNat1(a, true) === [c('0')]
showNat1(a, false) === showNat2([], divMod(a, n10), false)
showNat2(a, b, true) === a
showNat2(a, pair(b, c), false) === showNat2(char(n48 + c) :: a, divMod(b, n10), b == zero)

parseNat(a) === parseNat1(zero, a)
parseNat1(a, []) === a
parseNat1(a, char(b) :: c) === parseNat1(a * n10 + (b - n48), c)
call(parseNat, a) === parseNat(a)

-- Addition

zero + a === nat(a)
nb(a, x) + zero === nb(a, x)
nb(a, x) + nb(b, b0) === nb(a + b, x)
nb(a, b0) + nb(b, b1) === nb(a + b, b1)
nb(a, b1) + nb(b, b1) === nb(a + b + nb(zero, b1), b0)
call(add, a, b) === a + b

-- Subtraction

a - zero === nat(a)
nb(a, x) - nb(b, b0) === nb(a - b, x)
nb(a, b1) - nb(b, b1) === nb(a - b, b0)
nb(a, b0) - nb(b, b1) === nb(a - b - nb(zero, b1), b1)

-- Multiplication

zero * a === zero
nb(a, x) * zero === zero
nb(a, b0) * nb(b, b0) === nb(nb(a * b, b0), b0)
nb(a, b0) * nb(b, b1) === nb(nb(a * b, b0) + a, b0)
nb(a, b1) * nb(b, b0) === nb(nb(a * b, b0) + b, b0)
nb(a, b1) * nb(b, b1) === nb(nb(a * b, b0) + a + b, b0) + nb(zero, b1)

-- Division and modulo

divMod(zero, b) === pair(zero, zero)
divMod(nb(a, x), b) === divModStep(divMod(a, b), x, b)
divModStep(pair(q, r), x, b) === divModCheck(q, nb(r, x), b, le(b, nb(r, x)))
divModCheck(q, r, b, true) === pair(nb(q, b1), r - b)
divModCheck(q, r, b, false) === pair(nb(q, b0), r)
a / b === fst(divMod(a, b))
a % b === snd(divMod(a, b))

-- Exponentiation

square(a) === a * a
a ^ zero === n1
a ^ nb(b, b0) === square(a ^ b)
a ^ nb(b, b1) === square(a ^ b) * a

-- Equality

zero == zero === true
zero == nb(a, b0) === zero == nat(a)
zero == nb(a, b1) === false
nb(a, b0) == zero === nat(a) == zero
nb(a, b1) == zero === false
nb(a, b0) == nb(b, b0) === nat(a) == nat(b)
nb(a, b0) == nb(b, b1) === false
nb(a, b1) == nb(b, b0) === false
nb(a, b1) == nb(b, b1) === nat(a) == nat(b)
char(a) == char(b) === nat(a) == nat(b)

-- Comparison

le(zero, zero) === true
le(zero, nb(b, b0)) === le(zero, nat(b))
le(zero, nb(b, b1)) === true
le(nb(a, b0), zero) === le(nat(a), zero)
le(nb(a, b1), zero) === false
le(nb(a, b0), nb(b, b0)) === le(nat(a), nat(b))
le(nb(a, b1), nb(b, b1)) === le(nat(a), nat(b))
le(nb(a, b0), nb(b, b1)) === le(nat(a), nat(b))
le(nb(a, b1), nb(b, b0)) === lt(nat(a), nat(b))

lt(zero, zero) === false
lt(zero, nb(b, b0)) === lt(zero, nat(b))
lt(zero, nb(b, b1)) === true
lt(nb(a, b0), zero) === lt(nat(a), zero)
lt(nb(a, b1), zero) === false
lt(nb(a, b0), nb(b, b0)) === lt(nat(a), nat(b))
lt(nb(a, b1), nb(b, b1)) === lt(nat(a), nat(b))
lt(nb(a, b0), nb(b, b1)) === le(nat(a), nat(b))
lt(nb(a, b1), nb(b, b0)) === lt(nat(a), nat(b))

-- Logic

ite(true, a, b) === a
ite(false, a, b) === b
!true === false
!false === true
true -> true === true
true -> false === false
false -> a === true
true || a === true
false || true === true
false || false === false
false && a === false
true && false === false
true && true === true
false == false === true
false == true === false
true == false === false
true == true === true

show(true) === s("true")
show(false) === s("false")

-- List

list([]) === []
list(a :: b) === a :: b

[] == [] === true
a :: b == [] === false
[] == a :: b === false
a :: b == c :: d === listEq1(a == c, b, d)
listEq1(false, a, b) === false
listEq1(true, a, b) === list(a) == list(b)

reverse(a, []) === list(a)
reverse(a, b :: c) === reverse(b :: a, c)
reverse(a) === reverse([], a)

length(a) === length(zero, a)
length(n, []) === nat(n)
length(n, a :: b) === length(succ(n), b)

[] ++ a === list(a)
(a :: b) ++ c === a :: (b ++ c)

isPrefix([], a) === true
isPrefix(a :: b, []) === false
isPrefix(a :: b, c :: d) === isPrefix1(a == c, b, d)
isPrefix1(false, a, b) === false
isPrefix1(true, a, b) === isPrefix(a, b)

take(n, a) === take1(n == zero, n, a)
take1(true, n, a) === []
take1(false, n, []) === []
take1(false, n, a :: b) === a :: take(pred(n), b)

drop(n, a) === drop1(n == zero, n, a)
drop1(true, n, a) === list(a)
drop1(false, n, []) === []
drop1(false, n, a :: b) === drop(pred(n), b)

flatten([]) === []
flatten(a :: b) === a ++ flatten(b)

split([], a) === split0(a)
split(a :: b, c) === split1(a :: b, c)
split0([]) === []
split0(a :: b) === [a] :: split0(b)
split1(a, b) === split2(a, [], b, isPrefix(a, b))
split2(a, b, [], x) === [reverse(b)]
split2(a, b, c :: d, true) === reverse(b) :: split1(a, drop(length(a), c :: d))
split2(a, b, c :: d, false) === split2(a, c :: b, d, isPrefix(a, d))

join(a, []) === []
join(a, [b]) === b
join(a, b :: c :: d) === b ++ a ++ join(a, c :: d)

map(f, []) === []
map(f, a :: b) === call(f, a) :: map(f, b)

foldl(f, z, []) === z
foldl(f, z, a :: b) === foldl(f, call(f, z, a), b)

sum(a) === foldl(add, zero, a)

listToPair([a, b]) === pair(a, b)

-- String and I/O

s(a) === f1(a)
c(char(a)) === char(n(a))
f1(str(a)) === f1'(a)
f1'([]) === []
f1'(a :: b) === c(a) :: f1'(b)
f2(a) === str(f2'(a))
f2'([]) === []
f2'(char(a) :: b) === char(toDigits(a)) :: f2'(b)
main(a) === f2(main1(f1(a)))

-- Main

main1(a) === main2(listToPair(split(s(";"), a)))
main2(pair(s, i)) === main3(parse(s), i)
main3(p, i) === runProg(prog([], zero, [], i, s("")), p)

p(a, b) === p(a == c(b))
p(true) === 1
p(false) === 0

parse([]) === []
parse(a :: b) === parse1(
 p(a, '<'), p(a, '>'), p(a, '+'), p(a, '-'),
 p(a, ','), p(a, '.'), p(a, '['), p(a, ']'), b)
parse1(1, 0, 0, 0, 0, 0, 0, 0, a) === left :: parse(a)
parse1(0, 1, 0, 0, 0, 0, 0, 0, a) === right :: parse(a)
parse1(0, 0, 1, 0, 0, 0, 0, 0, a) === inc :: parse(a)
parse1(0, 0, 0, 1, 0, 0, 0, 0, a) === dec :: parse(a)
parse1(0, 0, 0, 0, 1, 0, 0, 0, a) === inp :: parse(a)
parse1(0, 0, 0, 0, 0, 1, 0, 0, a) === out :: parse(a)
parse1(0, 0, 0, 0, 0, 0, 1, 0, a) === parseLoop([], parse(a))
parse1(0, 0, 0, 0, 0, 0, 0, 1, a) === blockEnd(a)
parse1(0, 0, 0, 0, 0, 0, 0, 0, a) === parse(a)
parseLoop(a, b :: c) === parseLoop(b :: a, c)
parseLoop(a, blockEnd(b)) === loop(reverse(a)) :: parse(b)

showProg([]) === s("")
showProg(a :: b) === showInst(a) ++ showProg(b)
showInst(left) === s("<")
showInst(right) === s(">")
showInst(inc) === s("+")
showInst(dec) === s("-")
showInst(inp) === s(",")
showInst(out) === s(".")
showInst(loop(a)) === s("[") ++ showProg(a) ++ s("]")

getVal(prog(L, C, R, I, O)) === C
getOutput(prog(L, C, R, I, O)) === reverse(O)

runProg(p, a) === getOutput(runInsts(p, a))
runInsts(p, []) === p
runInsts(p, a :: b) === runInsts(runInst(p, a), b)

runInst(prog([], C, R, I, O), left) === prog([], zero, C :: R, I, O)
runInst(prog(n :: L, C, R, I, O), left) === prog(L, n, C :: R, I, O)
runInst(prog(L, C, [], I, O), right) === prog(C :: L, zero, [], I, O)
runInst(prog(L, C, n :: R, I, O), right) === prog(C :: L, n, R, I, O)
runInst(prog(L, C, R, I, O), inc) === prog(L, (C + n(1)) % n(256), R, I, O)
runInst(prog(L, C, R, I, O), dec) === prog(L, (C + n(255)) % n(256), R, I, O)
runInst(prog(L, C, R, [], O), inp) === prog(L, zero, R, [], O)
runInst(prog(L, C, R, char(n) :: I, O), inp) === prog(L, n % n(256), R, I, O)
runInst(prog(L, C, R, I, O), out) === prog(L, C, R, I, char(C) :: O)
runInst(p, loop(a)) === runLoop(getVal(p) /= zero, p, a)
runLoop(false, p, a) === p
runLoop(true, p, a) === runInst(runInsts(p, a), loop(a))

Input consists of brainfuck source code and brainfuck input, separated by semicolon. Example usage:

  • Input: ,[+.,]+[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+.;01234---
  • Output: 12345...Hello, World!

External resources