Can
Paradigm(s) | Object-oriented, Functional |
---|---|
Designed by | Nxe |
Appeared in | 2021 |
Type system | static |
Memory system | variable-based |
Computational class | Turing complete |
Reference implementation | Unimplemented |
File extension(s) | .can , .can.txt |
- Note that can is typically lowercased except, often, at the start of a sentence.
Can is a statically types esolang created by Nxe in December 2021. Can is a binary based language with all commands based on boolean algebra, with a few added extras. The name is an "alternative" to the shortened bin (from binary). This esolang was created after using the same notation for notes in a computer systems class to display boolean algebra in code, and as such is fairly simple to read.
Syntax
- Note that this section uses regex syntax within definitions, with descriptors placed inside angled brackets (
<
,>
)
The syntax for can is fairly simple
Command | Description |
---|---|
<< |
Bit shift left |
>> |
Bit shift right |
& |
Bit-wise and |
| |
Bit-wise or |
◊ |
Bit-wise xor^{1} |
~ |
Bit-wise not |
:= |
Variable assignment |
-> |
Return statement |
=> |
Output given value |
<= |
Input value |
^ |
Run if value is zero |
- ^{1}This is character code 0x25ca
You can also use rounded brackets ((
and )
) as you would expect, to enclose expressions that would otherwise be evaluated in a different way
Comments are sections that begin with a /
and end in a newline. This does not have to be the first character, but any characters afterwards are treated as comments.
Variables
Because can is designed around binary arithmetic, all variables have a set size in bits ranging from 1 to 64 bits. The syntax for defining a variable is as follows
<size of variable in bits> <variable name> := <value>
The variable name must match the regex string [\w\d_]+
and the size of the variable cannot be larger than 64 bits. The value can be written in any base using the following prefixes
Prefix | Base |
---|---|
None | Base 10 |
0d |
Base 10 |
0b |
Binary |
0x |
Hexidecimal |
0o |
Octal |
All variables in can are unsigned integers. It is up to the end user if they want to extend this to use signed integers (whatever system of signing you want to use) and/or floating point numbers.
For example, a 12 bit variable example
with the initial value of 0x20d
would be
12 example := 0o1015
Local variable names
Variables are all local, with no way to access variables from outside of their scope. This means that variables with the same name do not necessarily always refer to the same variable.
Functions
Functions are defined in a similar way to variables with the addition of arguments and one small alteration to the value type
<function return format> <function name>((<arg bit size> <argument identifier>(, <arg bit size> <argument identifier>)?)?) := { <function suite> }
Argument identifiers must match the same regex string as for variable identifiers.
The function return type can be a single value, in which case the bit size of the value is required. However, you can also return several values from a function. This comes from the inspiration for the language. Binary gates can have several outputs, and thus so can functions in can. If multiple values are returned, the bit length has to be specified in order. Returning several values is done on multiple lines, with each line representing one return value. For example, a simple half adder would return a sum bit and a carry bit. In our case we'll return them in that order. The sum bit is the xor of the two inputs, and the carry is the two inputs anded together. This means we can write a full adder as follows
(1, 1) half_adder(1 A, 1 B) := { -> A ◊ B -> A & B }
If we changed the return type to be (2, 1)
then the sum bit would become a sum two-bits. A value like this can't be stored as a single variable, but must be unpacked into separate variables. These values also can't be passed into an output function. A good example of this is the 4-bit adder in the examples section
IO
IO in can is dealt with in a very binary way. All inputs are read in UTF-8 and only the 8-bit character code is made available to the program. Output is dealt with slightly differently. To output something the syntax is as follows
=> <output type> <value>
The output type here indicates if the value being output is a UTF-8 character c
or a numerical value v
. If the output type is a character the only the first 8 bits are used. If the value is longer than 8 bits the value is "truncated" (Or anded with 0xff). Some useful UTF-8 codes are
Hex code(s) | Character(s) |
---|---|
0x0041 -0x005a |
A-Z |
0x0061 -0x007a |
a-z |
0x002b |
+ |
0x002d |
- |
Examples
Hello World!
=> c 0x48 => c 0x65 => c 0x6c => c 0x6c => c 0x6f => c 0x20 => c 0x57 => c 0x6f => c 0x72 => c 0x6c => c 0x64 => c 0x21
Adding
- This is a 4 bit adder. I will not write out any larger adder by hand
(1, 1) half_add(1 A, 1 B) := { -> A ◊ B / Sum bit -> A & B / Carry bit } (1, 1) full_add(1 A, 1 B, 1 C) := { 1 S1, 1 C1 := half_add(A, B) / Sum A and B 1 S, 1 C2 := half_add(S1, C) / Sum the sum of A and B with the carry bit 1 C := C1 | C2 / Set the final carry -> S / Return the sum -> C / Return the carry } 4 add(4 a, 4 b) := { 4 s, 1 c := half_add(a & 1, b & 1) / Initialise the output variable to the first sum bit and save the carry bit 1 s_, c := full_add(a & 2, b & 2, c) / Get sum and carry of the second bit s := s | (s_ << 1) / Add the second sum bit to the total sum s_, c := full_add(a & 4, b & 4, c) / Continue the pattern for the 3rd and 4th bits s := s | (s_ << 2) s_, c := full_add(a & 8, b & 8, c) s := s | (s_ << 2) -> s }
Interpreter
A can interpreter is currently being written in Rust. Bear with me on that though