Caps

From Esolang
Jump to navigation Jump to search

Overview

Caps is a Turing-complete language with minimal features, array/block-based memory, and IO access. Its conditional and looping logic sucks to use, so making conditional branching or looping programs is really annoying - significantly worse than Brainfuck's [] conditionals.

File Format

Caps files are plain-text files ending with the extension ".cps" that contain statements separated by the keyword STOP. Statements perform programming operations, and begin with a statement name, in all caps (using lowercase in statement names throws an error), then any flags, references, or data arguments, and then STOP. Line breaks between statements, or anywhere in the file (including comments), are not allowed.

Caps is meant to be challenging to write in. Nearly everything is done indirectly, with no variable names and little visibility into program state. Caps is interpreted, not compiled, with an implementation available in Python. Caps is Turing complete but has a very limited feature set, especially in the area of conditionals, making it a Turing tarpit. In addition to Turing completeness, Caps includes file and device I/O capabilities, allowing it to interact with the system. Caps relies on operating system resources to accept input, print output, and possibly throw custom errors at runtime (for output, files used are CON on Windows or /dev/stdout on most Unix systems and macOS).

Syntax & Language Operation

Statements can have one or multiple arguments. Some statements require references, others require data, one requires a choice between static options, and one has an optional flag. Everything but data, file paths, and comments should be in all Caps.

References to specific data location IDs (indicies) are denoted as & in this guide, and take in an index that refers to a position in memory. Arbitrary user-defined data, which often must conform to the rules of a statement and the current block, is written as [D] in this guide. An optional flag is written as {+A}, where A is the name of the argument (coincidentally, this only appears once, and the argument is actually +A). In the language, arguments are delimited by spaces (except for instruction literals, which count as only one argument). All statements are terminated with the all-caps keyword STOP. Statements and arguments must be separated by a space.

Data in Caps is stored in blocks. Blocks are zero-indexed arrays of data that are usually of the same type. Programs are initialized in a block of type 0, which is a block of type number. Numbers in Caps are positive arbitrary-precision floating-point values (with no rounding errors), or negative floats if a negation modifier is used, described below. A Caps program can only edit and access data in the current block. Data is written to the current block in order, such that the following code: BUILD BLOCK 1 STOP ENTRY C STOP ENTRY a STOP ENTRY p STOP ENTRY s STOP Would write the characters 'C', 'a', 'p', 's' to a new block of type 1 (char) in order. Since this is a new block, the total contents of the current block would be: ['C', 'a', 'p', 's'] Since this code built a new block, any data in the previous block is essentially lost, unless it is loaded:

BUILD BLOCK 0 STOP NOTE Building a new type 0 (number) block and adding data. STOP ENTRY 1 STOP ENTRY 23 STOP ENTRY 5.71 STOP ENTRY 8.004 STOP BUILD BLOCK 0 STOP NOTE This is a new block, even though it has the same type! STOP ENTRY 5 STOP ENTRY 0 STOP LOAD &3 NOTE That LOAD statement tries to load the fourth element of the current block, but there is no fourth element in this new block. After building a new block, the previous elements cannot be referenced! STOP

Normally, writing data of the wrong type to the current block would result in an error. The one exception to this is an UNLOAD statement, which can carry over data of a previous block (of a possibly different type) into the current block. This is the only mechanic that allows you to store different data types at once. This is an example of code that results in an error, from writing a string directly to a Boolean block (type 2):

BUILD BLOCK 2 STOP ENTRY @T STOP NOTE That previous entry works, because it is writing the True value (written as @T in Caps) to a Boolean block... STOP ENTRY 4 STOP NOTE ...but this entry fails with an error, because writing numbers to a block that isn't type number (type 0) is not allowed. STOP

Using LOAD, UNLOAD, and BUILD multiple times, this is an example of code that stores a number, character, and boolean at once.

BUILD BLOCK 0 STOP NOTE This builds a new number block, where numbers can be written. STOP ENTRY 20000 STOP NOTE Adds 20,000 to the block. STOP LOAD &0 STOP NOTE This loads the first element of the current block into memory, which persists even after new blocks are built. STOP BUILD BLOCK 1 STOP NOTE Building a new character block. STOP ENTRY A STOP NOTE Adds 'A' to the block. Note that 20,000 is still in load memory from the previous block. Now we can load in the character, making the memory store both. STOP LOAD &0 STOP NOTE Now, the memory has both a string and a number. STOP BUILD BLOCK 2 STOP NOTE Finally, building a boolean block! STOP ENTRY @F STOP NOTE Adding False to the block. STOP UNLOAD +A STOP NOTE This now unloads both the string and the character from memory. The +A argument is needed so that ALL memory elements are unloaded, not just the first. STOP

After this code, the contents of the current block is: [@F, 20000, 'A']

As previously shown, @T represents True, and @F represents False. These are examples of Reserved Keys. Another reserved key, @N, is the Negation Modifier, or negator. Normally, number-type blocks don't allow negative numbers to be written. However, placing a negator in the position after a number in a block and then loading both the number and the negator into memory in order (number first), possibly followed by other elements in memory, creates a negative number in that space. This works because the negator converts the element before it in memory to a negative number upon being loaded, if possible, and that number will remain negative when unloaded to any block. For example, if 4 was loaded into memory, then @N, those two elements would immediately merge to create -4 in memory. Combining a negator with a non-number (or nothing at all) causes it to self-delete and take no effect, and negating a negative number makes it positive. @T and @F are reserved keys of type boolean, but @N is of type modifier, so it can only be written in a type 3 (modifier) block. Using the build-load-build-unload trick to put two different data types into the same block, this example will create -10 and write it to the current block.

BLOCK 0 STOP ENTRY 10 STOP LOAD &0 STOP BUILD BLOCK 3 STOP ENTRY @N STOP LOAD &0 STOP BUILD NOTE Memory is now -10, because a negator is combined with a number. STOP UNLOAD STOP NOTE Current block now stores -10 in the second element (ID 1). STOP

The other modifier-type reserved key, which can also only be written to a type 3 block, is @J (Join Modifier/joiner). Modifiers disappear after taking effect, and only have effects in memory, not stored on the block. If multiple modifiers are entered into memory sequentially, later modifiers count as data (which could cause an error). @J joins the two elements immediately before it it in memory into one, if: - Both are positive numbers or zero, - The first number is negative and the second is positive or zero, - Both are characters, in which case a multi-character element (string) is created that is still technically of type character, - Or both are instructions. This creates a compound instruction, which executes its constituent instructions in sequential order. If none these conditions are met, or there aren't two elements before the modifier in memory, modifiers self-delete and takes no effect. Note that joining integers does not add or multiply them, but rather concatenates them. Joining 5 and 78.3 would yield 578.3, and joining -1 and 53 would yield -153. If both numbers have decimals, the decimals are joined separately: -53.1 and 88.917 would yield -5388.1917. If only one has a decimal point, the numbers are concatenated normally (-3 and 56.31 would yield -356.31, -3.5 and 56 would yield -3.556). Joining the number -0 followed by any positive integer is equivalent to negating it, and joining 0 followed by any positive integer has no effect.

In addition to basic data types, Caps allows instructions to be stored in a type 4 block, which store a single Caps command (such as "BUILD IO STOP") with normal syntax. These are written in string form as an instruction literal, but must be started with the reserved key @I, which denotes an instruction. This key does not have to be joined in memory, and is not a modifier. Instructions can be executed at a later time with RUN. Unlike standard functions in other languages, instructions cannot take arguments, but can reference data on the current block. Instructions also cannot return, but can write data to the block for later usage. NOTE This example enters the "X" character into the block. STOP BUILD BLOCK 4 STOP ENTRY @I"LOAD &0 STOP" STOP RUN &0 STOP This may seem like a roundabout, inefficient method of loading the first element on the block, but instructions can come in handy when you're repeating blocks of code multiple times, or combining multiple pieces of code using joiners.

Note that Caps provides file I/O capabilities, described below. If an I/O operation fails, it will fail silently.

Documentation legend:

  1. Brackets [] denote the text within as a placeholder that must be filled in by a user.
  2. Braces {} denote a placeholder that is optional.
  3. The placeholder "D" denotes user-defined data.
  4. The placeholder "+?" where "?" can be any single uppercase ASCII letter denotes a flag. Subsequent uppercase letters after the initial are treated as separate flags.
  5. Pieces of data separated by slashes (CHOICE1/CHOICE2/CHOICE3 etc) denotes multiple static options, of which one is required.
  6. The placeholder "&" represents a reference to another piece of data in a certain position on the block.

Statements List

Statement Synopsis Description
BUILD BUILD [BLOCK/IO] {0/1/2/3/4/5} BUILD BLOCK creates a new block of the specified type. BUILD IO builds an empty input/output stream that can be hooked to a file, file descriptor, or network object later, writing it to the block if it is type 5. BUILD BLOCK requires a type from zero to five, while BUILD IO does not.

0: Type number 1: Type character 2: Type boolean 3: Type modifier 4: Type instruction 5: Type IO

ENTRY ENTRY [D] Writes the specified single piece of data to the end of the current block, if the data is of the same type as the block. Does not support negative numbers, strings, or multiple pieces of data. If the specified data is "%n", a newline character is written. If the specified data is "%s", a space character is written (normal spaces are not accepted). Written characters must be ASCII.
LOAD LOAD [&] Loads the provided referenced data into memory, appending after existing memory.
UNLOAD UNLOAD {+A} Unloads the last element into the block, irrespective of type. Unloaded elements are deleted from memory. The flag +A unloads all elements at once, not just the last one, also clearing the current memory fully.
RUN RUN [&] Executes the provided referenced data if it is an instruction, otherwise fails.
NOTE NOTE [D] Denotes a single-line comment that can contain any Unicode characters supported by the host system and ends at the STOP keyword.
CONNECT CONNECT [&] [D] Connects the specified IO object to a specified file path. If the referenced file does not exist, it is created. Spaces in file names should be replaced with "%s".
IO_PUSH IO_PUSH [&] [&] Overwrites the contents of the file or file descriptor connected to the referenced IO object (the second reference) with the contents of the referenced piece of data (the first reference).
IO_PULL IO_PULL [&] Reads the contents of the file or file descriptor connected to the referenced IO object, attaching it to that object (overwriting previously attached data).
IO_READ IO_READ [&] Gets any data attached by IO_PULL to the specified IO object, and writes it to the current block if it is of type 1 (character), with one entry per character (for example, reading a 10-character file would write 10 different elements to the block in sequential order). Detaches the data from the object after. Fails if block is not of type character.
LOOPDEF LOOPDEF [&] Executes the supplied instruction reference while the elements with ID 0 and ID 1 are equivalent (&0 equals &1). No other conditional comparisons can be done in Caps other than checking the strict equality of the first two elements as a loop. If one or both elements do not exist, the condition is automatically false.
INCREMENT INCREMENT [&] Increments the value referenced by one. If it is a number, increments normally. If it is a single character, it is converted to its ASCII ordinal, incremented by one modulo 127, and converted back to a character. If it is a boolean, it is converted to its binary form, incremented by one modulo 2, and converted back to a boolean. Otherwise, does nothing.

]

Programs

Hello World

NOTE Hello world in Caps STOP BUILD BLOCK 1 STOP ENTRY H STOP ENTRY e STOP ENTRY l STOP ENTRY l STOP ENTRY o STOP ENTRY %s STOP ENTRY w STOP ENTRY o STOP ENTRY r STOP ENTRY l STOP ENTRY d STOP ENTRY ! STOP ENTRY %n STOP LOAD &0 STOP LOAD &1 STOP LOAD &2 STOP LOAD &3 STOP LOAD &4 STOP LOAD &5 STOP LOAD &6 STOP LOAD &7 STOP LOAD &8 STOP LOAD &9 STOP LOAD &10 STOP LOAD &11 STOP LOAD &12 STOP BUILD BLOCK 5 STOP BUILD IO STOP CONNECT &0 /dev/stdout STOP UNLOAD +A STOP IO_PUSH &1 &0 STOP IO_PUSH &2 &0 STOP IO_PUSH &3 &0 STOP IO_PUSH &4 &0 STOP IO_PUSH &5 &0 STOP IO_PUSH &6 &0 STOP IO_PUSH &7 &0 STOP IO_PUSH &8 &0 STOP IO_PUSH &9 &0 STOP IO_PUSH &10 &0 STOP IO_PUSH &11 &0 STOP IO_PUSH &12 &0 STOP IO_PUSH &13 &0 STOP

Repeat N Times

Changing the fourth ENTRY statement changes the number of times to loop. In this program it is 3. NOTE Loop 3 times in Caps, printing 'x' each time. 105 statements long not counting the three comments. STOP NOTE Initial memory layout: [control (0), control (1), counter (2), counter (3), main instruction (4), auxiliary instruction (5), printing character 'x' (6), stdout IO object (7)]. Memory past index &3 is not changed, while indicies &0-&3 inclusive are moved during execution to support looping. STOP ENTRY 0 STOP ENTRY 0 STOP ENTRY 0 STOP ENTRY 3 STOP LOAD &0 STOP LOAD &1 STOP LOAD &2 STOP LOAD &3 STOP BUILD BLOCK 4 STOP ENTRY @I"IO_PUSH &6 &7 STOP" STOP ENTRY @I"INCREMENT &2 STOP" STOP ENTRY @I"LOAD &2 STOP" STOP ENTRY @I"LOAD &3 STOP" STOP ENTRY @I"LOAD &0 STOP" STOP ENTRY @I"LOAD &1 STOP" STOP ENTRY @I"LOAD &4 STOP" STOP ENTRY @I"LOAD &5 STOP" STOP ENTRY @I"LOAD &6 STOP" STOP ENTRY @I"LOAD &7 STOP" STOP ENTRY @I"BUILD BLOCK 0 STOP" STOP ENTRY @I"UNLOAD +A STOP" STOP ENTRY @I"LOOPDEF &5 STOP" STOP ENTRY @I"LOAD &2 STOP" STOP ENTRY @I"LOAD &3 STOP" STOP ENTRY @I"LOAD &0 STOP" STOP ENTRY @I"LOAD &1 STOP" STOP ENTRY @I"LOAD &4 STOP" STOP ENTRY @I"LOAD &5 STOP" STOP ENTRY @I"LOAD &6 STOP" STOP ENTRY @I"LOAD &7 STOP" STOP ENTRY @I"BUILD BLOCK 0 STOP" STOP ENTRY @I"UNLOAD +A STOP" STOP LOAD &0 STOP LOAD &1 STOP LOAD &2 STOP LOAD &3 STOP LOAD &4 STOP LOAD &5 STOP LOAD &6 STOP LOAD &7 STOP LOAD &8 STOP LOAD &9 STOP LOAD &10 STOP LOAD &11 STOP LOAD &12 STOP LOAD &13 STOP LOAD &14 STOP LOAD &15 STOP LOAD &16 STOP LOAD &17 STOP LOAD &18 STOP LOAD &19 STOP LOAD &20 STOP LOAD &21 STOP LOAD &22 STOP BUILD BLOCK 3 STOP ENTRY @J STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP LOAD &0 STOP BUILD BLOCK 4 STOP ENTRY @I"ENTRY 1 STOP" STOP ENTRY @I"LOAD &8 STOP" STOP ENTRY @I"INCREMENT &0 STOP" STOP LOAD &0 STOP LOAD &1 STOP LOAD &2 STOP BUILD BLOCK 3 STOP ENTRY @J STOP LOAD &0 STOP LOAD &0 STOP BUILD BLOCK 1 STOP ENTRY x STOP LOAD &0 STOP BUILD BLOCK 5 STOP BUILD IO STOP CONNECT &0 /dev/stdout STOP LOAD &0 STOP BUILD BLOCK 0 STOP UNLOAD +A STOP LOOPDEF &4 STOP NOTE Looping is complete. Now, print a newline to finish the job. Load &8 because every element was offset, and we want the IO object to write to again. STOP LOAD &8 STOP BUILD BLOCK 1 STOP UNLOAD STOP ENTRY %n STOP IO_PUSH &1 &0 STOP

I will write a quine at some point