spl

From Esolang
Jump to navigation Jump to search
For the brainfuck derivative, see SPL. For the Shakespearean language, see Shakespeare.

spl (Set Processing Language) by Edward Cree is a dynamically typed, interpreted, functional programming language. Unlike many other esoteric languages, which try to achieve difficulty by having as few different instructions as possible, spl has a very wide variety of operators; the difficulty comes from the fact that none of them ever seem to do what you want.

Language overview

The original idea of spl is to have 'set' as a data type, with various specialised operators for working with sets.

Functions

In spl, there are no named variables; the only declarations you can make are functions, and all functions are unary. Each function is effectively formed in Polish Notation; this design was chosen so that there would be no such thing as precedence of operators, thus making the implementation easier (after all, there are a lot of operators). Also, there are no brackets. The interpreter will always start by calling the function "#main." with no arguments. Functions can call other functions, but this is a two-part process: the function label acts as a unary operator and binds to its argument, producing an "unforced function pointer with attached argument". This is not evaluated straight away; it is a variable that can be stuffed into a set, pushed onto the stack, etc., but the function is not yet evaluated. The evaluation of the function occurs when the 'force' operator is applied to it. This, in conjunction with short-circuiting boolean operators and various stack-manipulation operators, allows rudimentary control structures, such as loops and conditionals, to be formed.

Types

All data types and casting thereof are implicit. The data types that may occur in spl programs are as follows:

  • unsigned char
  • boolean
  • set
  • ordered set (list)
  • unforced function pointer with attached argument (either hashed or unhashed)

The last of these may not be cast at all; others will sometimes cast and sometimes not, depending on the operator in a manner which is currently implementation-defined. If a cast fails, the null value for that type is used instead (0, false, empty set, ordered empty set, undefined). The reason that no null value is defined for an unforced function is that the only operator which acts on an unforced function is the forcing operator, which acts as an identity operator on any other type. Casting rules are not fully specified yet; the current implementation tends not to attempt to cast at all, except when a boolean is required, in which case any type (apart from an unforced function, which cannot be cast) casts to true unless it equals the null value for its type. Note, however, that you can forcibly convert a hashed pointer to an unhashed, or vice-versa, with code such as the following:

force:f
#force.%label.(arg) // returns #label.(arg)
%force.#label.(arg) // returns %label.(arg)

where (arg) is some argument.

Operators

With the exception of preprocessor directives (spl programs are run through cpp before execution), function labels and character constants, all tokens in spl are single characters.

The operators supported by the current implementation are: (Binary)

  • = (any)(any) equivalence, returns a boolean (the two need not be of the same type; there are casting rules, but they haven't been properly specified yet)
  • > (char)(char) greater than, returns a boolean
  • < (char)(char) less than, returns a boolean
  • + (char)(char) sum, returns a numeric
  • - (char)(char) difference, returns a numeric (second op - first op)
  • * (char)(char) product, returns a numeric
  • / (char)(char) quotient, returns a numeric (floor(second op / first op))
  • u (set)(set) union of sets, returns a set - OR: catenation if both sets are ordered, returns an ordered set
  • c (set)(set) complement of first set in second set, returns a set
  • & (bool)(any) if the boolean evaluates to false, discard the second argument (it will be evaluated but not passed on to possible later forcing of functions) and return the first; otherwise, return the second argument
  • | (bool)(any) if the boolean evaluates to true, discard the second argument (it will be evaluated but not passed on to possible later forcing of functions) and return the first; otherwise, return the second argument
  • k (any)(any) discard the first argument and return the second

(Unary)

  • { (any) return an ordered set containing only the argument
  • O (set) return the order of the argument (i.e. number of elements)
  • ; (list) recursive-print: for each element in the argument, if it is a list recursive-print it, if it is a char print it (, operator), otherwise ignore it. return the number of characters actually printed, as a char
  • D (set) dereference: return the only element if O(set)=1, the first element if set is ordered, otherwise it's a casting failure
  • , (char) print: sends the argument to standard output, and returns 1 (because that is the number of characters printed)
  • p (char) return the n-th argument from the stack, where n is the first argument, to which p1 points
  • ! (bool) boolean not, returns a boolean
  • % (any) function call; full form %label.(any), returns an unforced function pointer with attached argument
  • # (any) function call; full form #label.(any), returns a hashed unforced function pointer with attached argument
  • f (any) force, if argument is an unforced function, then execute that function and return the result; if result is a hashed unforced function, force it as well; if unhashed, don't (so only the top-level function gets forced). For other types, return argument

(Nullary)

  • ? read a char from standard input (will block execution until it gets one). EOF is represented as 255.

(Double return)

  • d (any) duplicates its argument, returning two copies of it
  • ~ (any) (any) "swizzle": swaps the order of its arguments in the stack

Constants

The constants available in spl are:

  • the integers 0 to 9
  • a backtick (`) followed by another character gives that character's ASCII code; `\n gives the code for a UN*X newline, which is 10.

Any constants which cannot be inserted in this way must be produced by arithmetic operations on those which can.

Error handling

Currently, stack underflow is handled by adding a copy of the empty set to the top of the stack. An unrecognised operator will cause the interpreter to exit with a return code of 255. Casting failures can cause almost any behaviour; sometimes a null value is returned, sometimes an ERR object (which is supposed to unwind the stack and return 255, but often doesn't); sometimes you get pathological behaviour such as an infinite loop because the error has interacted strangely with recursion in your code...

Examples

Hello world

// spl "Hello World"
// prints "Hello world\n" on stdout
// return value: 1 iff the wrong number of chars were printed; 0 otherwise
main: !=O~;df%hellostring.
hellostring: u{`Hu{`euud{`lu{`ou{` u{`wu{`ou{`ru{`lu{`d{`\n

cat

This program is only 16 bytes long (or 17 if you count the trailing newline). In previous versions, it ate up all the stack, but now we have the '#' operator, we can optimise tail-recursion into a loop. Also, because main now auto-forces, we've saved another character!

main:&+,d?#main.

spl Standard Library

Note that there is no function "main" in this code: this is a library which can be incorporated into other spl source files with the #include directive (since spl source files are passed through cpp before being executed)

// spl
#pragma once
// subroutine library "stdspl.s"
// local namespace _SS
// provides if, sum, mod, printnum, printdig
if: &f~k~Dc{p4d~Dd
// "%if.u{%cond.{%code." will return %code. (not f%code., so you probably need "ff", one f for the %if. and the other for the %code.) if f%cond. is true, otherwise {}
sum: +ff%if.u{~{%sum.~Odc{~p4dDd
// "%sum.(set)" will return the sum of the elements in (set), like the S operator is supposed to but doesn't because it doesn't exist yet.
mod: -*p4/~p3D~Dcp3p4{dDd
// "%mod. u{(char1){(char2)" will return char2 modulo char1 (= c2 - (((int)(c2/c1))*c1)
printnum: k~0kf%printdig.f%mod.u{+d8{kf%printdig./+d8d~f%_SSprintox.0
// "%printnum. (char)" will print char in hex, and return 0.
printdig: ff%if.u{~{%_SSprintdec.~!kff%if.u~{%_SSprinthex.p4{d!>9d
_SSprintdec: ,+`0
_SSprinthex: ,+`W
_SSprintox: kf%_SSprintdec.0,`x

Specification status

The full specification for spl is incomplete and probably inconsistent, but that's alright because the interpreter doesn't fully implement it anyway.

Computational class

The computational class of spl is unknown. It is probably Turing-complete, but a proof has yet to be produced.

One way to prove it would be to produce a Minsky machine; since sets are of unbounded size, you could use an ordered set for each register, and write a 'pop' function which took a set {a, b, c, ...} and returned the set {{a}, {b, c, ...}}, and a 'push' function which did the reverse. Then the order of the set would be the value of the register. Calling pop on the empty set {} would have to return {{}, {}} (which would be difficult since D{} is an error, so pop would have to O the set first and test for zero).

(Note that although it is useful to refer to the empty set as {}, and other sets as eg. {a, b}, and this would be the obvious way to add them to the language, } is not a valid spl operator (because one of the rules of spl is there are no brackets, in order to make the parser brutally simple). One way to get the empty set in spl is to use the code 'cd{0' since complementing (c) a set with itself returns the empty set. To construct sets with more than one element, you generally have to use the union operator (u).)

There is a complication which would make this task harder than it seems, however: Building the set {b, c, ...} from the set {a, b, c, ...} is not so simple as complementing (c) it with {a}; this only works if there are no more instances of a in the set (this is arguably the Wrong Thing for c to do on ordered sets, but it's not clear what the Right Thing is). This can be got around by always using O(stack) as the value to push, so that each stack always consists of numbers in decreasing order, and the last element is 1. If, instead, O(stack)-1 is used, and each stack is initialised to {0}, then the value popped from the stack can be taken to be the value of that register, thus simplifying the calling code. Another solution is to instead use the select operator (z), by constructing a set {0, 1, 1, ...} with 1s unioned on until it is the same order as the stack, but this would have much longer running time.

History

spl was invented by Edward Cree in 2009, so it hasn't had much chance to get any history yet. The spl manual is written as a close parody of the INTERCAL manual.

External resources

spl download -- note that this server is not always online.