Serenity

From Esolang
Jump to navigation Jump to search
Serenity
Paradigm(s) Object-oriented
Designed by User:Hakerh400
Appeared in 2020
Computational class Turing complete
Major implementations Interpreter
File extension(s) .txt

Serenity is an object-oriented esoteric programming language. In this language everything is an object, including boolean values, numbers, object keys, symbols, arrays, strings, functions, etc. There are no constraints regarding what you can do with each type of object, so you can multiply strings like numbers, call integers like functions, push a value into a literal identifier like it is an array, modify function body like it is an ordinary object, and so on. You can even modify values of literal constants: for example you can change constant to be and from that point on, all operations and functions that would normally return as the result will return instead. The are no runtime errors (all syntactically valid programs do not produce any errors).

This language has a lot of features, so documenting every single detail would require a lot of effort and will probably just introduce unnecessary complexity to this article, so we tend to explain the most important features, while the interpreter provided at the end of this article can be considered the formal specification (except if there are bugs the author is not aware of).

Object

An object is a value that lies somewhere in the memory. All objects are unique. We access objects via references.

Each object contains a prototype (which is also an object) and can contain zero or more key-value pairs. All object must have a prototype. Any object can contain any number of key-value pairs.

Null is an object, like any other object. Its prototype is null itself. Initially it has no key-value pairs, but they can be added to it.

Prototype chain of an object is the sequence of objects that we obtain by following the prototypes recursively. Each prototype chain must end with null. If we explicitly change a prototype of an object and it creates a cyclic prototype chain (that does not end with null), the prototype of the last object in the prototype chain that did not yet create the cycle will implicitly be set to null. Null is not considered a part of a prototype chain when getting and setting key values recursively.

Keys are objects (not only strings). Each key maps to a single value. Duplicate keys cannot co-exist (the second value will overwrite the first one). Keys can be added (together with values) and they can be deleted. Accessing a key will search accross the prototype chain if the key does not exist in the current object. This applies both to getting and setting a key. It is also possible to ignore the prototype chain and search only in the current object. Each object keeps track of two arrays of keys (keys1 and keys2). The first array contains all keys sorted from the least recently added to the most recently added key. The second array contains all keys sorted from the least recently updated to the most recently updated key. The term "property" is used interchangeably with the term "key" in this article.

Hidden properties are some properties that are implicitly defined for each object, but which are not accessible or examinable directly. For example, each object has an integer value, which is for all objects except integers (and of course integer also has integer value ).

Types of objects

Null

We already explained what null is. All null values reference the same null object (the null object is unique). Null can be accessed explicitly by executing instruction null, which pushes null to the stack (we explain all instructions in details in some of the next paragraphs), or it can be obtained implicitly by trying to access a non-existing key of an object.

Root

Root is the global object. It contains all other accessible objects. There are special cases when we can store some objects that cannot be reached by recursively following key-value pairs of the root: for example null is not by default anywhere in the root (recursively) and integers (there are infinitely many of them) are also not stored anywhere (except if they literally appear in the source code), but they can be obtained by performing math operations and since all integers are also objects, we can store key-value pairs in them.

Symbol

A symbol is a type of object which is represented by a uniqueue identifier in the source code. For example, abc is a unique symbol. All identifiers abc that appear in the source code will represent references to the same symbol. Symbols by default do not contain any keys. Some of the symbols represent instructions. When they appear in a function body, they can be executed. The list of all instructions is posted below.

Integer

An integer is a type of object which contains integer value as a hidden property. All integers with the same integer value are also the same object. Also, by defult, they have no keys. An integers can be either positive or negative, or it can be zero. They are represented as integral values in the source code.

Character

It is very similar to an integer, but it is limited to the range and it has different prototype. In most cases, characters are interchangeable with integers.

Array

An array is an object which contains symbol length as a key and its value is an integer representing the length of the array. It also contains keys that are integers in range and their values are elements of the array. Note that this is how arrays are supposed to be used. It is possible to set any value as the length and even to delete the length. Adding/deleting keys does not implicitly change the length, nor does modifying the length adds/removes other keys. In general, it is impossible to distinguish arrays from non-array objects.

String

Strings are arrays of characters.

Function

A function is an object that contains symbol insts as a key and its value is an array of instructions that represent the function body. Functions can be called (but not only functions - any object can be called). A function can also contain other useful keys, such as symbol scope, which points to the current scope. A function can also contain symbol prototype, which can be used to instantiate new objects. Functions can also contain any number of other keys.

Scope

A scope is an object that is usually bound to a function. It contains variables and/or arguments. Scopes can be linked in a prototype chain, so that we can achieve the effect of closures and shadowing variables. A scope can also contain key this, which can be used in functions that are bound to a specific object (methods).

Syntax

Objects

Integers are literal integral values in decimal or hexadecimal. Examples: 35, -0x123, 0

Symbols are literal identifiers. They can contain alphanumeric characters and hyphens (underscores are not allowed). Example: someIdent

Objects are represented as key-value pairs (no commas) surrounded by a pair of braces. Example:

{
  someKey: someValue
  someOtherKey: 123
}

Arrays are denoted by elements surrounded by a pair of brackets (no commas). Example: [a b c].

Arrays can also contain labels. A label definition is an identifier followed by a colon, while label reference is a colon followed by an identifier. Label definitions are removed from arrays, while label references are replaced with corresponding index (in the array) of the label definition with the same name. Example: [x lab: y z :lab r] (it is equivalent to [x y z 1 r]).

Characters are surrounded by '. Character \ can be used to escape any other character. Examples: 'a', '\\', '\''

Strings are denoted by characters surrounded by a pair of ". Character \ can be used to escape any other character. Example: "abc\\de\"fgh"

Program

Program consists of a single object, which is interpreted as a function and implicitly called.

Structure of the root object

Root

The root object initially contains a single property mainStack, which contains the main stack.

Main stack

The main stack is an array that contains stack frames for all functions that are currently called.

Stack frame

Each stack frame contains the following properties:

  • func - The current function
  • inst - Instruction pointer (integer)
  • scope - The current scope
  • stack - Local stack for that function

Local stack

A local stack is contained in each stack frame. Everything that the function does operates on the local stack, except calling another function or returning from the current function, which operates on the main stack (calling a function creates a new stack frame, while returning from a function deletes the last stack frame).

Instructions

Here is the table of all instructions. Everything that appears in the function body and is not an instruction will be pushed to the stack. All instructions implicitly increment the inst (instruction pointer) of the current stack frame, unless stated otherwise. All instructions must be symbols.

Operands are taken (popped) from the top of the stack. If an instruction takes two operands x and y, the last element on the stack is y and the second last element is x. Same applies to more arguments. Arithmetical operations implicitly retrieve the integer value of the arguments.

The last element of the stack is also called the top of the stack. When we say -th element of the stack, it mans from the top of the stack (-th element if the last element of the stack, -th is the second last element, and so on).

Instruction Operands Description
nop No effects besides incrementing the instruction pointer
plus Convert to an integer (get its integer value) and push it to the stack
minus Push
not If is nonzero push , otherwise push
neg Push
inc Push
dec Push
and Push binary AND of and
or Push binary OR of and
xor Push binary XOR of and
shl Push
shr Push
add Push
sub Push
mul Push
div If push , otherwise push null
mod If push , otherwise push null
exp If or push , otherwise push null
lt If push , otherwise push
gt If push , otherwise push
le If push , otherwise push
ge If push , otherwise push
push Push the next element from the function body to the stack and increment the instruction pointer once again
pop Delete the -th element from of the stack (the -th element is the top element after popping )
disc Simply discard . It is effectively the same as pop with operand
move Move the -th element from of the stack to the top of the stack (the size of the stack does not change, except from implicitly popping )
copy Push the -th element from of the stack (but also leave it in the original position)
dupe Duplicate the last element of the stack. It is effectively the same as copy with argument
swap Swap -th and -th element from the stack
eq If both and are references to the same object then push , otherwise push
neq If both and are not references to the same object then push , otherwise push
has If has as a key (including prototype chain search), then push , otherwise push
get If has as a key (including prototype chain search), then push , otherwise push null
set Perform . If does not contain , then find the first object in the prototype chain that contains as a key and set its value to . However, if no objects in the prototype chain contain , then set it into .
setk Similar to set, but keeps on the stack (does not pop it). It is useful if you want to set multiple properties in a row.
delete Delete key from object (include prototype chain search)
deletek Similar to delete, but keeps on the stack
hasl Similar to has, but performs local operations in the object (does not include prototype chain search)
getl Similar to get, but local
setl Similar to set, but local
setlk Similar to setk, but local
deletel Similar to delete, but local
deletelk Similar to deletek, but local
getProto Push the prototype of
setProto Set as the prototype of
keys1 Push keys1 of
keys2 Push keys2 of
prod Push dictionary product of and , as explained in User:Hakerh400/Code_golf_challenges#Product_of_two_dictionaries
prod* Replace each object that was ever created in the program with the dictionary product of that object with (this also applies to constants like integers and chars, and it also applies to all other objects like root, null, etc). Note that the asterisk * is literally a part of the symbol name and it is the only identifier that is allowed to contain * in its name.
raw Push a new object that has prototype and no keys
obj Push a new object that has the same prototype as all other objects that appear in the source code as literal objects
int Alias for plus
char Push converted to a character
arr Push an array containing elements
str Similar to arr, but converts each element to a character and pushes the string to the stack
clone Push a new object that has the same prototype, key-value pairs, keys1 and keys2 as . Hidden properties, such as integer value, are not copied (the new object has integer value ).
pusha Interpret as an array and push into it (to the end).
pushk Similar to pusha, put keeps on the stack
popa Pop the last element of array and push it to the stack
null Push null
root Push the root
mainStack Push the main stack
frame Push the current stack frame
func Push the current function
scope Push the current scope
this Push this from the current scope
getv Get the value of the key from the current scope (effectively the variable from the current function)
setv Set the value of the key from the current scope to
setvk Similar to setv, but keeps on the stack
getvl Similar to getv, but does not consider the scopes prototype chain of the scope
setvl Similar to setv, but does not consider the scopes prototype chain of the scope
setvlk Similar to setvl, but keeps on the stack
enter Update the current stack frame scope to be a new object whose prototype is the old scope
leave Update the current stack frame scope to be the prototype of the current scope
bind Set the key scope of x to to the scope of the current stack frame. Keeps on the stack.
arg Push a new object whose prototype is the value of the key scope of . Keeps on the stack.
args Push an array containing elements and whose prototype is the value of the scope property of . Keeps on the stack.
crg Alias for args call
cbs Alias for clone bind setv
jz If set the instruction pointer of the current stack frame to
jnz If set the instruction pointer of the current stack frame to
jmp Set the instruction pointer of the current stack frame to
alt If set the instruction pointer of the current stack frame to , otherwise set the instruction pointer of the current stack frame to
call Call function with scope . It creates a new stack frame (and pushes it to the main stack) whose func is and scope is . Instruction pointer is and the stack of that stack frame is a new array.
method Set property this of to and then call with scope
new Set property this of to a new object whose prototype is the value of the prototype property of and then call with scope
ret Pop the last stack frame and push to the stack of the new last stack frame (effectively returns to the previous function)
retv / Pop the last stack frame (effectively returns void). This instruction is performed implicitly if the instruction pointer becomes greater or equal to the length of the insts array of the current function.
in / Push the input string (reads the entire input at once). Pushes the same string each time it is called.
out Output string and halt the program. Note that this is the only way to halt the program. Reaching the end of the main function does not implicitly halt the program (and what happens if the end of the main function is reached can be deduced from the rest of this article).

Examples

Hello, World!

{insts: [
  "Hello, World!" out
]}

Cat program

{insts: [
  in out
]}

Print digits from 0 to 9

{insts: [
  s 0 str setv
  i 0 setv

  loop:
    i getv 10 eq :end jnz
    s getv i getv 0x30 or char pusha
    i i getv inc setv
    :loop jmp

  end:
    s getv out
]}

Reverse the input string

{insts: [
  a in setv
  b 0 str setv

  loop:
    a getv length get :end jz
    b getv a getv popa pusha
    :loop jmp

  end:
    b getv out
]}

Add two integers

Input contains two non-negative integers separated by a space character.

{insts: [
  rev {insts: [
    a 0 getv clone setv
    b 0 str setv

    loop:
      a getv length get :end jz
      b getv a getv popa pusha
      :loop jmp

    end:
      b getv ret
  ]} cbs

  split {insts: [
    str1 0 getv clone setv
    str2 0 str setv

    loop:
      str1 getv dupe length get dec get ' ' eq :end jnz
      str2 getv str1 getv popa pusha
      :loop jmp

    end:
      str1 getv dupe popa disc
      rev getv str2 getv 1 crg
      2 arr ret
  ]} cbs

  map {insts: [
    array 0 getv setv
    fn 1 getv setv

    arrayNew 0 arr setv
    len array getv length get setv
    index 0 setv

    loop:
      index getv len getv eq :end jnz
      enter
        elem array getv index getv get setv
        arrayNew getv fn getv elem getv index getv 2 crg pusha
      leave
      index dupe getv inc setv
      :loop jmp

    end:
      arrayNew getv ret
  ]} cbs

  str2int {insts: [
    s 0 getv clone setv
    num 0 setv
    mask 1 setv

    loop:
      s getv length get :end jz
      num dupe getv s getv popa 0x30 xor mask getv mul add setv
      mask dupe getv 10 mul setv
      :loop jmp

    end:
      num getv ret
  ]} cbs

  int2str {insts: [
    num 0 getv setv
    s 0 str setv

    loop:
      num getv :end jz
      s getv num getv 10 mod 0x30 or char pusha
      num dupe getv 10 div setv
      :loop jmp

    end:
      s dupe getv rev getv 1 move 1 crg setv
      s getv dupe length get :ret jnz
      '0' pushk
      ret: ret
  ]} cbs

  nums {insts: [
    f {insts: [
      str2int getv 0 getv 1 crg ret
    ]} cbs

    map getv split getv 0 getv 1 crg f getv 2 crg ret
  ]} cbs

  x y
  nums getv in 1 crg dupe
  1 2 swap popa setv
  popa setv

  int2str getv
  x getv y getv add
  1 crg out
]}

Test

This example is used to test the correctness of the interpreter. It should output string PQcdefgRQ8

{insts: [rev {insts: [output 0x50 char dupe inc char 2 str setv i 1 2 3 j k 0 5 swap 1 move disc j
input getv length get 1 pop setv i i getv dec setv i getv 0 lt 75 jnz c input getv i getv get setv
output getv 5 output getv 1 pop length get c getv setk length output getv length get inc set output
getv dupe popa inc char pusha 29 jmp push this null setv null root mainStack frame func scope this
obj null output getv setk prod* this clone ret]} bind setv rev getv arg input rev getv arg input
"abcde" setk call setk call 2 3 exp 0x30 or char pushk out]}

Replace a constant

{insts: [
  obj 12345 12347 setk prod*
  0x30 12345 10 mod or 1 str out
]}

This program outputs 7. It first replaces constant with , then pushes to the stack (which is now actually ), takes the last digit and converts it to a string. We could also replace, say, with and then push (which should push instead) and output it, but it would cause problems, because it would interfere with the value of the instruction pointer and may cause very unpredictable and weird side effects. For example, this program:

{insts: [
  obj 5 7 setk prod*
  0x30 5 or 1 str out
]}

outputs the zero byte \x00 instead of 7, because when the instruction pointer becomes , it will actually be and it will execute or on the empty stack (because 0x30 5 are skipped), which will push onto the stack, and then converts it to a string of character and outputs it. Now consider this example:

{insts: [
  obj 5 11 setk prod*
  0x30 5 or 1 str out
]}

It is an infinite loop. It does not halt, because the instruction out is never executed.

Rename an instruction

It is also possible to rename any instruction. For example, we can rename (actually keeping the old name too) instruction out to print, as can be seen in the following example:

{insts: [
  obj print push out setk prod*
  "ok" print
]}

It prints ok, and we achieved it by executing the instruction print, which we defined to be an alias for out.

Interpreters

Interpreter