Teramithic

From Esolang
Jump to navigation Jump to search

Teramithic is an esoteric programming language by User:Digital Hunter. The name is intended to evoke a terrible, almost mythical arithmetic system, one that seems minimalist and leads to expressions of gargantuan size. And to add to the list of User:Digital Hunter's languages with names ending in "-ic".

Teramithic was meant to be purely functional, with most side effects being "forgotten" outside of any given expression. It in its early stages may also have been influenced somewhat by Scheme, which carries over somewhat in certain programming techniques.

Language overview

Programs in Teramithic are made up of a series of blocks and expressions. Blocks contain, in sequence, expressions or other blocks. Expressions enclose logical statements comprised of variable names and various evaluation and comparison operators. Blocks that contain a single expression can be rewritten as just that expression (otherwise, it'd just be blocks all the way down). Blocks can be "empty", but expressions cannot. The entirety of the program is a single block, so it can be rewritten as just that block's constituents for convenience and clarity. A block will evaluate to the value of the final block (or expression) inside of it. Teramithic supports complex numbers as its primary type, and a special "false" form; anything other than false is a truthy.

If a program terminates, then its "output" is just what its value would have been had it been treated as a block.

Reserved characters

The characters ( ) < > [ ] / \ - ^ = { } are reserved, and cannot be used as a part of a variable name. Variable names may contain any of the other ASCII characters from the 33 to 126 block, but cannot be # or @. Whitespace is completely ignored.

Blocks and flow control

Within a block, the characters [ ] enclose blocks, and ( ) enclose expressions. Within a block, blocks and expressions are evaluated from left to right.

Expressions, and therefore blocks, can have either a truthy or a false value. If a block is evaluated to have a truthy value, the block after the next block is stricken from the order of execution. If a block is evaluated to false, the next block is stricken from the order of execution.

Anywhere within the code, {, }, and anything enclosed within matching { } are treated equivalently to whitespace.

Expressions, variables, and functions

The simplest object that can appear within an expression is a variable. Excepting the two special variables # and @ (to be discussed later), a variable name when referenced for the first time must be defined. How exactly this works will also be discussed later, but less "later".

The next tier up are statements created by combining variables with the characters -, /, and ^. These statements may also be enclosed within ( ), and can be used in the exact same places as variable names inside of other statements. The three evaluation operators have their usual meanings of subtraction, division, and exponentiation, respectively, and are interpreted in the order exponentiation, division, subtraction, and from left to right. Parenthesised statements are evaluated only as soon as doing so is necessary. - and / may appear at the beginning of statements, and behave as if preceded by an additive identity (zero) or multiplicative identity (one), respectively.

The arithmetic-expression variety of statements can be combined with comparison operators = and < to form what are typically called equations and inequalities. These comparison operators have their usual meanings of "is equal to" and "is less than", respectively, and evaluate to truthy if their condition is met (false otherwise). Because the complex numbers cannot be ordered, < compares absolute magnitudes. Multiple arithmetic-expression variety statements can be "chained together" with comparison operators, and in this case an entire chain is truthy if and only if every individual pairing is truthy (each comparison operator contributes a comparison of its two adjacent arithmetic-expression variety statements).

The highest "level" within a statement is constructed with any of the above and \s. These \ are boolean NANDs, which operate by the following truth table.

(value A) (value B) (value A)\(value B)
truthy truthy false
truthy false truthy
false truthy truthy
false false truthy

\s group from right to left in the absence of parentheses (but things are still evaluated from left to right).

Parentheses ( ), as described, will act to "group" items when within expressions, serving a different purpose than they do inside of blocks. Similarly, [ ] take on a new meaning inside of expressions, evaluating to the complex conjugate of whatever value is enclosed. In this light, ( ) can be seen as an identity operator instead of a "grouper", but these are functionally the same.

Variables

A variable is created when a variable name is referenced for the first time. Immediately following the variable name must be an expression. A value for that variable is then selected that will not make the expression false. Note that this does make the language nondeterministic. For example, if the variable 1 is already defined over this expression (how to do this will be described soon) as 1, the expression

(x(x = 1) - 1)

evaluates to 0, as the variable x was given the only value that would make the expression x = 1 valid. However, the expression

(x(x = 1 / x) - 1)

could equal either 0 or -2. Note as well that a "clarifying" expression need not have the potential to be false; (x(1)) is a perfectly valid way of generating a "completely random" complex number (though in practice this is simply not possible). As a mnemonic, the expression that defines a variable may be read as a "such that ..." clause.

Once a variable is defined, its value remains tied to that name for the rest of the expression it is contained within. Variables cannot be reassigned (as following an already-defined variable name with parentheses will cause an error), but are limited in scope to only the expression they are in.

The only two variables that need not be defined in this way are # and @, also known as "wildcard" and "input", respectively. When a Teramithic program is run, it is fed a single special complex number, as well as an optional sequence of complex numbers. The individual complex number becomes the wildcard, and every # is treated as pre-defined with this value. Each time a @ is met, it behaves as the next not-yet-used member of the input sequence. If there are more inputs than executed @s, nothing detrimental happens. When @ runs out of input numbers to be, @ will instead act as the "false" special form.

If ever a truthy value from comparison chains or NANDs is expected to have a numerical value, it takes that of the wildcard (is treated equivalently to #).

Functions

Earlier it was assumed that a variable name must be followed by an expression enclosed within ( ) when used for the first time. However, a variable name may be followed by a special form constructed with [ ] and >s to define a function. This special form has syntax of, enclosed within [ ], a sequence of variable names separated with > and followed with a true block (which can, as usual, be a single expression). Each of the listed variables will be defined over the entirety of the final block. For example,

a function with one "parameter" [ x> (x)]
a function with two parameters [ a> b> [ (a < b) (b - a) (a - b)]]
an alternative way to get a constant is to use a function with no parameters [ (# - #)]

. Note that the "variable name" of the topmost function is afunctionwithone"parameter".

When a variable name that is defined with a function is encountered in an expression, it will also expect to be followed by exactly the number of expressions (technically blocks, but in practice this is unwieldy) necessary to "fill" the placeholder variables at the start of its definition. This applies as well to the first time a function is named.

Using functions, one may effectively define variables with scope across an entire block. For example, using a snippet from earlier, the following is a program that gives either 0 or -2 (assuming nonzero wildcard).

(f[ 1> (x(x = 1 / x) - 1)] (# / #))

Of course, this is a fairly simple example for which other methods (such as defining 1 as a variable) would be better. In general, however, it is a good idea to get familiar with techniques such as this.

Examples

Generally, the two "best" wildcards to use are 1 and Euler's number e. 1 is relatively straightforward for use, and allows for the construction of any algebraic number. Euler's number is a number of great importance, and many transcendental functions can be written using it. Additionally, π (pi), another commonly-used transcendental number, can be defined using e (as can 1 and therefore any other algebraic number). Examples will take wildcard to be Euler's number as this is more generally applicable, unless stated otherwise.

Examples that are functions will be provided in valid-program form, unless stated otherwise.

Truth-machine

Returns 0 if input is zero. Loops forever if input is one. Returns "false" for any other input.

(f[n>[
  (n=-n)
  (n)
  [
    (n=n/n)
    (f(n))
    (n\n)]]
  ](@))

Note that this program does not use the wildcard at all.

Ackermann function

Uses wildcard value 1. Takes two inputs for m and n, computes A(m,n).

(a[m>n>[
  (m=-m)
  (n-(-#))
  [
    (n=-n)
    (a(m-#)(#))
    (a(m-#)(a(m)(n-#)))]]
  ](@)(@))

Check if a positive real number is an integer

(?[n>[
  (n=-n)
  (n)
  [
    (n<#/#)
    (#\#)
    (?(n-#/#))]]
  ](@))

Fibonacci numbers

Finds the nth member of the Fibonacci sequence, given F(0) = 0, F(1) = 1, F(n) = F(n - 1) + F(n - 2). Takes an input to be n. Implements the recursive definition, so expects natural number inputs for expected results.

(f[n>[
  (n<(-#-#)/#)
  (n)
  (f(n-#/#)-(-f(n-#/#-#/#)))]
  ](@))

A slightly more verbose program may use Binet's formula.

(f[1>
  (-(p(#\p-1/p=1\1<p)^n(n=@)-(-1/p)^n)/(1-p-p))
  ](#/#))

Factorial

Takes an input and determines its factorial.

(f[n>1>[
  (n<1)
  (1)
  (f(n-1)/(/n))]
  ](@)(#/#))

Something using a technique resembling tail recursion can be written as well.

(f[n>1>
  (r[t>c>[
    (c=1)
    (/t)
    (r(t/c)(c-1)(1))]
    ](1)(n)(1))
  ](@)(#/#))

"Get" and "put" functions

Implements "arrays" as reciprocals of numbers of the form S(0)^p(0) * S(1)^p(1) * ... * S(n)^p(n) * ... for members of Sylvester's sequence S(n) and arbitrary whole number powers p(n) (which are the entries of the simulated array). As Sylvester's sequence grows incredibly rapidly, these functions exist purely to demonstrate the possibility of more complex datatypes than complex numbers alone.

get[ array> index>                     {accepts an "array" and an index from which to extract
                                        the "return value"}
  (vars[ syl> 1>                       {establishes predefined variables}
    (divide loop[ big> count> [        {main loop to find the exponent by counting how many times
                                        "big" can be divided by the appropriate factor}
      (check int[ x> [                 {a function that determines whether its argument is a whole number}
        (x=-x)                         {if the number is zero,}
        (#)                            {then it is.}
        [                              {otherwise,}
          (x<1)                        {if it is less than one,}
          (#\#)                        {it is not.}
          (checkint(x-1))]]            {if x is greater than one, it is an integer iff x-1 is an integer}
        ](big))                        {this check is applied to "big"}
      (divideloop(big/syl)(count-1))   {if "big" is still whole, it can be divided again (and the count goes up)}
      (-count)]                        {otherwise, it is time to escape.
                                        "count" starts out at one and is decremented with each iteration,
                                        and "big" was divided one too many times.  The negative of "count"'s
                                        present value is the number of powers of that factor that were in "big"
                                        originally.  This is the final value of the "get" function}
      ](/array)(1))                    {the loop is run with the reciprocal of "array" as "big",
                                        and "count" is given initial value one as mentioned above}
    ](syl[ n> 1> [                     {the "vars" helper function establishes the "index"th Sylvester number}
      (n=-n)                           {if the index is zero,}
      (-(-1-1))                        {the desired factor is two.}
      ((s(s=syl(n-1)(1))-1)/(/s)-(-1))]{otherwise, it will be one more than the product of the previous
                                        Sylvester number and its decrement}
      ](index)(#/#))                   {this Sylvester's sequence generator takes the index and one to work}
    (#/#))                             {the "vars" helper also takes a one to make calculations easier}
  ]                                    {the "get" function when used will expect two following values}
put[ array> index> thing>              {accepts an "array", an index, and an item to be inserted
                                        returns the altered "array"}
  (vars[ syl> 1>                       {establishes predefined variables}
    (divide loop[ big> [               {main loop to "remove" the value presently at the index}
      (check int[ x> [                 {a function to check for when the appropriate Sylvester number
                                        has been divided enough from "big"}
        (x=-x)                         {if a number is zero, }
        (#)                            {it is an integer.}
        [                              {if it is not zero, }
          (x<1)                        {but less than one, }
          (#\#)                        {it is not an integer.}
          (checkint(x-1))]]            {if x is greater than one, x is an integer iff x-1 is an integer}
        ](big))                        {this function is applied to "big"}
      (divideloop(big/syl))            {if "big" is still an integer, it can be divided again}
      (/big/syl)]                      {otherwise, it is time to escape.
                                        "big" has to be reciprocaled, as "array" was originally reciprocaled
                                        to enter the loop.  An additional division is done because the loop
                                        divided an extra time, leading to the reciprocal having an extra factor}
      ](/array) / syl^thing)           {the loop is done using the reciprocal of the "array"
                                        and "thing" is inserted as a negative power of the appropriate
                                        Sylvester number.  This is the final value of the function}
    ](syl[ n> 1> [                     {the "vars" helper function establishes the "index"th Sylvester number}
      (n=-n)                           {if the index is zero,}
      (-(-1-1))                        {the desired factor is two.}
      ((s(s=syl(n-1)(1))-1)/(/s)-(-1))]{otherwise, it will be one more than the product of the previous
                                        Sylvester number and its decrement}
      ](index)(#/#))                   {this Sylvester's sequence generator takes the index and one to work}
    (#/#))                             {the "vars" helper also takes a one to make calculations easier}
  ]                                    {the "put" function when used will expect three following values}

External resources