x7

From Esolang
Jump to navigation Jump to search
x7
Paradigm(s) imperative
Designed by User:LyricLy
Appeared in 2022
Memory system Stack-based
Dimensions one-dimensional
Computational class Turing complete
Major implementations Reference implementation
File extension(s) .x7

x7 (pronounced "except") is a stack-based pseudo-golfing language devised by User:LyricLy in 2020 and first implemented in 2022. While ostensibly a language meant to be used for golfing, x7's design does not optimize for byte count per se; instead, it is a proof of concept for an original paradigm of golf programming. As such, it makes some design decisions that are counter to the direct goal of golfing in order to support its novel concepts. x7's main draw is that its control flow is based on exceptions, called 'raises' in the language's documentation, and the ability for the program to fully rewind its state to certain points to retry blocks of code without affecting the program state. A focus on exceptions allows the programmer to more easily control edge cases and potentially save bytes by eliding explicit checks.

Syntax

As x7 is stack-based, simple expressions are written as you'd expect in RPN:

1 2 3*+
> 7

Each instruction is one character long. Integer literals are written in decimal; spaces are needed to separate them.

But things get more complex: certain operators take other code as arguments, and they do this by parsing some number of blocks. Let's start things off simple with an operator that takes 1 block: T. T pops a number from the stack and runs its block that many times. Take a look:

1 10T2*`
> 1024

Here, we give T the code 2* as an argument, and it pops 10 from the stack, so it will double the 1 at the bottom of the stack 10 times, ultimately pushing 210.

Note that we used a backtick to end the block. Backticks work like closing brackets for the implicit opening brackets after each instruction that takes a block. Nesting calls to T is simple:

1 2T2T2*``
> 16

This nested loop doubles the 1 2×2 times, yielding 24.

There's an alternative to using backticks if you want to close many blocks at once. For example, consider this (somewhat contrived) scenario:

0 10T10T10T1+```
> 1000

I want to add 1 a total of 10*10*10 times by nesting T blocks, but I don't want to write all of those backticks to close the blocks. Luckily, there's a solution. Blocks can be demarcated with braces, which implicitly close all blocks at the end. So I can just write this:

0{10T10T10T1+}
> 1000

Wonderful! There's one extra trick here: if you're at top level (not nested inside any other blocks), you don't need to include opening braces or closing braces at the end of the file. x}y}z will be parsed the same as {x}{y}{z}.

Some instructions take multiple blocks as arguments, which changes the rules in one simple way. Let's imagine an example with e, an operator that works similarly to the try/except feature in many practical languages. It takes two blocks, runs the first one, then runs the second if and only if the first block raises an exception. Here's an example of using it without any shenanigans:

e1 0D`2`
> 2

Two backticks to close two blocks. The first block divides by 0, which raises, so the second block is executed instead. Now, if we use the rule from before about how braces work, then this code wouldn't make much sense:

e1}2
> 1

Where's the opening bracket? x7 has a rule in place to make } more useful with multi-block commands. If there is more than one block to be parsed, then instead of closing all blocks (which would make any subsequent blocks be empty) a closing brace will act as a separator between blocks. However, it still has the effect of immediately closing all other blocks, like so:

0e10T10T10T1+}2
> 1000

Variables

Values can be stored in and retrieved from variables using the special operators : and ; respectively by writing the name of a variable after it:

42:x 22:y ;x;y;x
> 42 22 42

Variable names can be any single character. They won't collide with the names of instructions.

Functions

The ; operator has another use: it can call user-defined functions. Every line in an x7 program is its own function. By default, only the last line is executed, but you can refer to lines by number with ; to execute them:

3 4
1 2;1
> 1 2 3 4

Data types

Rationals

Rationals are the only number type supported by the language. They are represented as fractions; a pair of arbitrary-size integers. When a function says it expects an integer, that isn't a separate type; it's just a whole rational. Rationals can be constructed with a combination of integer literals, the division instruction D, and the negation instructionN:

1N2D
> -1/2

Lists

Lists are what you think they are; big ordered collections of anything you can imagine. They can be constructed with the empty list instruction [, the concatenation instruction ., or the collection instruction ]. Here's an example:

1 2.3.
> [1, 2, 3]

Unlike in most dynamically-typed languages, lists are homogeneous, so you can't make a list that consists of incompatible types. The types of two values are said to be compatible if one of the following holds:

  • They are both rationals
  • They are both boxes
  • They are both pairs, and their respective elements are of compatible type
  • They are both lists, and at least one of them is empty
  • They are both lists, and the elements inside them are of compatible type

Boxes

Boxes are a way to hide your values away. The only way to get one is to put a value in a box, and the only thing you can do with the box is unbox it again to get the value back:

0B
> &0
b
> 0

Boxes are an opaque structure. They make the value inside inaccessible without unboxing, and every box has a compatible type as far as lists are concerned.

Pairs

The ideal fixed-size heterogeneous collection, pairs are a simple combination of two values. They are constructed with ,:

1 2,
> (1, 2)

Unlike boxes, the type of pairs is not opaque, so you can't store pairs containing different types in a list (unless you box them first)

Rewinding

Normally, if you write a try/except block in a language like Python, it might act something like this:

x = 0
try:
    x += 1
    x /= 0
except:
    # x will be 1

In x7, not so: when a raise is caught in a block (with the exception of !), everything the block did is rewound, so any effect it had on the program before raising is completely forgotten.

Masking

One instruction in the language, `m`, can mask raises to prevent them being caught. Here's an example using the e instruction introduced earlier:

em1 0D``2`

Unlike the first example with e, this code will raise instead of pushing anything. However...

eem1 0D``2``3`

This code will push 3. This is due to the mask, which prevents raises from being caught for one layer. You can think of it like this. Call normal raises Raise, and say there is some constructor Mask(e) for masked raises. Now when m catches some raise e, it wraps it in a mask and raises Mask(e) instead. When another operator tries to catch a Raise, and gets a Mask(e) instead, it unwraps the mask and raises e. This allows you to nest levels of matching to do things like this:

eeemm1 0D```2``3``4`

This code creates a doubly-masked raise and puts it through 3 layers of catching. The third layer will successfully catch it, pushing 4. Careful use of masking can allow to create much more terse programs by controlling which raises are caught by what.

Unless otherwise specified, when an instruction says it "catches" raises, it will only catch unmasked raises. Masked raises will have one layer of masking removed and be propagated.

Temporary groups

Usually, stack manipulation commands operate on single values. However, sometimes it is convenient to treat multiple stack values as a consecutive group. Values on the stack can be grouped together temporarily with the & command, which will pop two values (or existing groups) from the stack and connect them together. Here's an example:

0 1&d
> 0 1 0 1

Usually the d command would only duplicate the top value, but the & causes it to consider the 0 and 1 together and duplicate them at once.

Only certain commands respect grouping, mostly those related to stack shuffling. (The documentation for them will refer to "groups" instead of "values".) If a different command, like +, tries to pop a value in a group, its entire group (even if it contains more than 2 values) will be broken apart, and only a single value from it will be taken.

Lenses

Perhaps the most intricate feature of the language, lenses are how pairs and lists are accessed. Focusing into a position or set of positions with lenses allows you to then query, update or remove the focused values. Here's an example:

1 2.3.0n
> [<1>, 2, 3]
@
> 1

The indexing lens n focuses on a particular index of a list. Here we selected the first element, then used the @ instruction to access the value. We can also do something else with the selected value, like replacing it with $:

1 2.3.0n2137$
> [2137, 2, 3]

Operators like @ and $ are referred to as enders, because they are used to finish off after selecting values with lenses.

You can also focus on the first and second values of pairs with h and t:

1 0,h
> (<1>, 0)
0$
> (0, 0)

But focusing isn't limited to single values. By default, most instructions will implicitly focus on all the values in a list, so we can use them like this:

1 2.3.
> [1, 2, 3]
0$
> [0, 0, 0]

The j operator will nest further:

1 2.]3 4.].
> [[1, 2], [3, 4]]
0$
> [0, 0]
p1 2.]3 4.].
> [[1, 2], [3, 4]]
j
> [[<1>, <2>], [<3>, <4>]]
0$
> [[0, 0], [0, 0]]

@ can't handle more than one value, so it'll raise if we try to use it on multiple values. Instead, we can use ] to get all the selected values:

1 2.]3 4.].j]
> [1, 2, 3, 4]

Lo! The list is flattened. The nesting of lenses goes further than this, because we can use multiple at once:

1 2.]3 4.]].5 6.]7 8.].].
> [[[1, 2]], [[3, 4]], [[5, 6], [7, 8]]]
j
> [[<[1, 2]>], [<[3, 4]>], [<[5, 6]>, <[7, 8]>]]
j
> [[[<1>, <2>]], [[<3>, <4>]], [[<5>, <6>], [<7>, <8>]]]

See what it did there? Lenses work off the focusing that is already present, allowing you to chain them to nest deeper into a structure. This lets us do things like flattening a nested list.

We can do similar with a list of pairs, using h to take the first element of each one.

1 2,3 4,.h
> [(<1>, 2), (<3>, 4)]

This is just the tip of the iceberg and there are many more lenses and enders to make use of. You can see what they all do in the table of instructions below.

Drilling

In the section above, we saw that instructions will often work implicitly on lists, like how h works on lists of pairs as well as single pairs. This is because many lenses and enders will automatically apply the j operator for you before running, a process known as drilling. Instructions can have one of 5 approaches to drilling into lists:

  • Never drilling at all.
  • Drilling only when the value has not already been focused ("from top").
  • Drilling only when the value has not already been focused by something other than h or t ("from pair").
  • Drilling only when the value has not already been focused by something other than h, t or n ("from single").
  • Drilling as much as possible ("to atom").

All lenses and enders will raise if they are given an unfocused value that is not a list, with the exception of h, t and ]. The strategy used by each instruction is documented in the section below.

Instructions

This list is subject to change. x7 is still new and under active development.

Where instructions specify the type of their operands, assume that they will raise if the operands are of the wrong type. Operands are popped from the top of the stack; popping an empty stack raises.

Arithmetic

+
add
Add two rationals.
-
subtract
Subtract two rationals.
*
multiply
Multiply two rationals.
Q
quotient
Perform Euclidean division on two integers. Raises on division by zero.
R
remainder
Perform Euclidean modulo on two integers.
D
divide
Divide two rationals. Raises on division by zero.
N
negate
Negate a rational.
J
floor
Floor a rational.
K
ceil
Ceil a rational.

Comparison

<
less than
Take two values and raise if the bottommost is not less than the other. Collections are compared lexicographically.
G
not less than
Take two values and raise if the bottommost is not greater than or equal to the other.
=
equals
Take two values and raise if they are not equal.
/
not equals
Take two values and raise if they are equal.
>
greater than
Take two values and raise if the bottommost is not greater than the other.
L
not greater than
Take two values and raise if the bottommost is not less than or equal to the other.

Collections

b
unbox
Take a box's contents.
B
box
Form a new box.
,
pair
Form a pair out of two values.
[
list
Push an empty list.
.
concat
Given two lists of compatible type, concatenate them. Given a list and a value of compatible type with its contents, append it. Given a value and a list with elements of compatible type, prepend it. Given two elements of compatible type, form a double-element list with them.
i
iota
Push a half-open range from 0 to a given nonnegative integer.

Lenses

These instructions all take focused values as the bottommost argument, which they may drill into as specified in the Drilling section.

h
head
drills: to atom
Focus on the first item of a pair.
t
tail
drills: to atom
Focus on the second item of a pair.
j
join
drills: from top
Focus on the elements inside a list.
n
nth
drills: no
Take an nonnegative integer and focus on the nth value of a list. Raises if the index is out of bounds.
s
select
drills: no (arg 1)
drills: from single (arg 2)
Like n, but for multiple indices. Take a view of integers and focus on the nth values of a list. Raises if any index is out of bounds.
w
where
drills: from single
Take a block and run it on each focused value. Unfocus values for which the block raises.

Enders

These instructions all take focused values as the bottommost argument, which they may drill into as specified in the Drilling section, and return the updated value after making changes.

@
only
drills: from top
Push the only focused value. Raise if there is less than or more than one focused value.
]
enlist
drills: no
Push all focused values as a list. If given an unfocused value, wrap it in a list.
c
count
drills: from single
Push the number of values focused.
$
set
drills: from top
Take a value and set all focused values to it, pushing the updated value.
X
86
drills: from pair
Remove the focused values.
P
paste
drills: both from pair
Take a view and replace the focused values from the first view with ones from the second, removing any missing values from the end.
u
u-turn
drills: from single
Reverse the order of the focused values in place.
M
map
drills: from top
Takes a block. Run the block on each focused value and replace it with the result. If the block raises, the respective value will be removed. Raises if the type invariant of lists is broken.
Z
zip
drills: both from top
Takes a second view. As M, but iterate over both views at once. The result has the shape of the first value. If the second view is too short, values from the end of the first view will be removed.

Loops

W
while
Takes a block. Run the block repeatedly until it raises.
F
for
Takes a block. Take a list. For each element in the list, push that element and run the block.
M
map
Takes a block. Take a list. For each element in the list, push that element and run the block. If the block raises, go to the next element; otherwise, pop a value. At the end, collect all values popped this way into a list and push it.
T
times
Takes a block. Take a nonnegative integer. Run the block that many times.
P
pick
Takes a block. Take a list. For each element in the list, push that element and run the block. If the block raises, go to the next element; otherwise, stop.

Stack utilities

d
dup
Duplicate the group on top of the stack.
p
pop
Pop a value from the stack.
f
flip
Swap the places of the top two groups on the stack.
^
yank
Push the second group from the top.
_
under
Takes a block. Pop a group, run the block, then push the group again.
l
lift
Takes two blocks. Run the second block, pop a group, rewind the state to before the block ran, run the first block, then push the popped group again.
~
permute
Takes a block. For each possible permutation of values (not groups) on the stack, permute the stack that way, then run the block. If it raises, continue; otherwise, stop.

Raise manipulation

r
raise
Raises.
e
except
Takes two blocks. Run the first block. If it raises, run the second block.
q
quiet
Takes a block. Run the block. If it raises, ignore it.
!
invert
Takes a block. Run the block. If it raises, catch it without rewinding. If it finishes without raising, raise.
m
mask
Takes a block. Run the block. If it raises, mask and propagate it.

Debugging

v
trace
Display the current state of the stack. Not subject to rewinding.
V
monitor
Takes a block. Run the block and display a message if it raises, without catching. Not subject to rewinding.