Hailstone
- This is still a work in progress. It may be changed in the future.
| Paradigm(s) | functional, object-oriented (prototype-based) |
|---|---|
| Designed by | User:RaiseAfloppaFan3925 |
| Appeared in | 2026 |
| Memory system | variable-based, stack-based |
| Computational class | Unknown |
| Reference implementation | Unimplemented |
| File extension(s) | .hail |
Hailstone is an esolang made by User:RaiseAfloppaFan3925. It was made
Memory
Hailstone uses both variables and a stack. The stack is the main memory region used for computations, while variables are used to hold values outside the stack and bring them back in when needed.
Data types
Hailstone has a few core data types.
"Nullable" tri-states
Hailstone does not have traditional booleans and null, instead "nullable" tri-states. They have three valid states and one "null" state, @_@ (the null tri-state), which represents no valid choice.
The tri-states are ^v^, -_-, and =_=.
Integers
Hailstone has arbitrary-precision integers. Numbers can be written with either decimal or base-62. Base-62 literals must start with a leading zero.
0123456789 00123456789 1ABCDEFGHIJ 2KLMNOPQRST 3UVWXYZabcd 4efghijklmn 5opqrstuvwx 6yz
Examples of numeric literals
12 /* 12 */ 0a /* 10 */ 010 /* 62 */ 06T /* 401 */ 0verycool /* 203053492849283 */ 0isntthisamazing /* 556640392080993526278449680 */ 0KASANETETO /* 273024326477340654 */
Floats
Floating-point values are written exclusively in base-62 starting with a & with a & for the decimal point. The decimal part is written like a normal base-62 number.
&1 /* 1 / 62 = 0.01612903... */ &D /* 31 / 62 = 0.5 */ &z /* 61 / 62 = 0.983870967... */ &1& /* 1 */ &3&tnbCOm /* approx. 3.9 (3.8999999999876761...) */ &3&8mHUcx /* approx. pi (3.141592653841159...) */
Strings
Strings are just strings. However, arithmetic and bitwise operations can be performed on them. Arithmetic and bitwise operations on strings operate on them as if they were giant hexadecimal numbers made of their UTF-32 codepoints. For example, the string "ABC" would be 0x000000410000004200000043.
Below is an example of abusing bitwise operations on strings.
str "ABC" <
${str 64 ^ . | 言え} /* "A" */
${str 32 ^ | 言え} /* "B" */
${str 04gfFC3 . | 言え} /* "C" */
${'\n' | 言え}
Indexing a string uses the \ operator. Negative indices wrap around like in Python.
"abc" 0 \ /* "a" */ "abc" -1 \ /* "c" */
Getting the length of a string uses the ,; operator. It returns the length in UTF-32 codepoints, not UTF-8 bytes or Unicode graphemes.
"abc123" ,; /* 6 */
Functions
- See #Functions for a full description.
Functions are first-class values. The only operation that can be performed on a function is calling it.
factorial [
n 2 <=> :?
^v^ [n n 1 - # *]<>
=_= | -_- | @_@ [1]<>
?:
]<n> <
Array
Arrays can store elements of any type and can be dynamically resized.
Appending
Appending an element to an array uses the addition (+) operator. However, this creates a new array instead of modifying the array in place.
array (123) <
${array | 言え} /* (123) */
xyz <- array + 'xyz'
${array | 言え} /* (123) */
${xyz | 言え} /* (123, 'xyz') */
array array 456 + <
${array | 言え} /* (123, 456) */
Popping
Popping the last element from an array uses the bitwise NOT operator (~). It pushes to the stack the new array without the last element, and the popped last element.
array 123, ('an', 'esolang'), 'xyz') <
${array | 言え} /* (123, ('an', 'esolang'), 'xyz') */
array xyz array ~ < <
${array | 言え} /* (123, ('an', 'esolang')) */
${xyz | 言え} /* 'xyz' */
Indexing
Getting an element at an index from an array uses the bitwise XOR operator (`). It only returns a reference to the element at the specified index.
array (123, ('abc', 'xyz'), '456') <
${array | 言え} /* (123, ('abc', 'xyz'), '456') */
${array 0 ` | 言え} /* 123 */
${array 1 ` | 言え} /* ('abc', 'xyz') */
${array 2 ` | 言え} /* '456' */
Operators
Operators operate on the stack.
Relational operator
The relational operator or two-way arrow, is the comparison operator. It takes in two values and returns ^v^ if the first value is greater than the second, -_- if the first value is less than the second, =_= if both values are equal, and @_@ if both values cannot be compared.
a b <=>
Arithmetic operators
The arithmetic operators are + for addition, - for subtraction, * for multiplication, / for division, % for modulo, and ; for unary negation. If the operation cannot be performed on the two values, then @_@ is returned.
x ; a b + a b - a b * a b / a b %
Bitwise operators
The bitwise operators are ~ for bitwise NOT, . for bitwise AND, : for bitwise OR, ; for bitwise XOR, ` for left shift, and ^ for right shift. Just like the arithmetic operators, they return @_@ if the operation cannot be performed.
x ~ a b . a b : a b ; a b ` a b ^
Logical operators
The logical operators are ,. for logical AND and ,: for logical OR. Logical NOT doesn't exist because the "boolean" type is the nullable tri-state.
a b ,. a b ,:
Assignment
The assignment operator (<) takes a name and a value. It pops both off of the stack and assigns the value to the variable of the same name.
x 2 <
Variables
Defining and assigning to a variable both use the same syntax.
variable value <
Some operations such as array ~ (pop from array) return more than one value. In this case, more <s can be used as it is just a normal stack operator.
The example below defines two variables val and new_array and sets them to the popped value from the array and the new array respectively.
new_array val array ~ < <
Functions
Functions are first-class values that can be created with this syntax. The number of parameters is unlimited.
[body]<p0|p1|p2|p3|p4|p5>
To call a function, the "pipeline shell substitution" syntax should be used.
${arg1 arg2 arg3 | f}
Pipeline shell substitutions can be chained by simply adding more pipes. For example, h(g(f(x))) is written as:
${x | f | g | h}
A function can call itself using the # symbol. If # is used at the top level (outside of any function), it restarts the script but with the environment parameters replaced with whatever arguments were passed into it, if any.
This program is an infinite loop, as # calls the script it is in and never terminates.
${#}
A function can call an outer function using the $ symbol.
x ${尋ねろ} <
x '0' <=> :?
=_= | [${0 | 言え}]<>
^v^ | -_- [
x '1' <=> :?
=_= | [${1 | 言え} #]<>
^v^ | -_- [${'1/0 only' | 言え} $]<>
?:
]<>
?:
Recursion
Since functions and recursion are the only way to implement loops in Hailstone, implementations must be able to support deep recursion without crashing. Practically, this would be impossible. However, optimizations can be made to eliminate calls, such as tail-call optimization and optimizing self-calls to GOTOs.
Structures
Structures are object-like values that can store named fields. They are first-class values and do not have to be bound to names like in C.
Point @{ x | y } < /* named struct */
@{ line | column } /* anonymous struct */
Instantiating a structure can be done by pushing the field values to the stack, then the structure, then the bitwise NOT (~) operator.
Cursor @{ line | column } <
start 1 1 Cursor ~ <
${start | 言え} /* @{ line: 1 | column: 1 } */
Accessing a member of a structure can be done via the bitwise XOR operator `, similar to array indexing.
struct @{ a | b | c | hello | world | xyz } <
my_struct 'abc' 123 'xyz' 'hello ' 'world' struct struct ~ <
${my_struct @a ` | 言え} /* abc */
${'struct' mystruct ` | 言え} /* @{ a | b | c | hello | world | xyz } */
Structures are passed by reference, so assigning a structure to a field of another structure gives a reference to that structure.
A @{ B } <
B @{ A } <
a @_@ A ~ <
b a B ~ <
a @B B <
/* a and b now reference each other */
${a | 言え} /* recurses infinitely */
I/O
Hailstone has two functions for I/O, 尋ねろ and 言え.
言え is used for printing and accepts an unlimited number of arguments. 言え is the only function that can accept an unlimited number of arguments. However, it does NOT print a newline.
x "x" <
y ["y"]<> <
z [["z"]<>]<> <
${123 "abc" 'strings can use single quotes too' x ${y} ${${z}} '\n' | 言え}
尋ねろ is used for receiving user input. It can take one argument which will be used as the prompt, or none.
x ${尋ねろ} <
y ${'what is 1 + 1' | 尋ねろ} <
Pattern matching
Pattern matching is done with a :? ?: block. It takes the current stack top and matches it against a set of patterns. If a pattern is matched, then the function after it is called. ? matches any value that does not match any of the previous patterns.
x ${尋ねろ} <
x ${x | int} <
x :?
3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 |
19 | 21 | 23 | 25 | 27 | 29 | 31
[${'odd' | 言え}]<>
2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 |
18 | 20 | 22 | 24 | 26 | 28 | 30
[${'even' | 言え}]<>
? [${'what is ' x | 言え}]<>
?:
Libraries
Despite looking like symbol vomit, Hailstone supports proper library imports and exports.
Importing
To import a library (whether it's a part of the standard library or a file), the 輸入 function is used. This returns a structure with the fields being the exports of the library.
math ${"std.math" | 輸入} <
lua ${"lua" | 輸入} <
token ${"../lexer/token.hail" | 輸入} <
ast ${"ast.hail" | 輸入} <
Exporting
The 輸出 function can be used to export values under a certain name.
sin <- [
/* implement sin(x) somehow */
]<x>
cos <- [
/* implement cos(x) somehow */
]
${"pi" &3&8mHUcx | 輸出}
${"sin" sin | 輸出}
${"cos" cos | 輸出}
Foreign function interface
Hailstone has an FFI (foreign function interface) located in std.ffi. This provides functions for interoperation with native code.
Examples
Truth machine
val ${尋ねろ} <
val '0' <=> :?
^v^ | -_- [${1 | 言え} #]<>
=_= | [${0 | 言え}]<>
?:
XKCD random number
${4|言え}
Cat program
${尋ねろ|言え}
Reversed cat program
/* tac program? */
input ${尋ねろ} <
i input ,; 1 - <
${[
i 0 <=> :?
^v^ | =_= [${input i \ | 言え} i 1 - ${$}]<>
-_- []<>
?:
]<>}
Golfed
i${尋ねろ}<j i,;1-<${[i 0<=>:?^v^|=_=[${i j\|言え}i 1-${$}]<>-_-[]<>?:]<>}
Factorial (recursive)
fac [
n 2 <=> :?
^v^ [n ${n 1 - | fac} *]<>
=_= | -_- [1]<>
?:
]<n> <
Simpler factorial
[
n 2 <=> :?
^v^ | =_= [n ${n 1 - | $} *]<>
-_- [1]<>
?:
]<n>
Golfed factorial (44 bytes)
[n 2<=>:?^v^[n${n 1-|#}]<>=_=|-_-[1]<>?:]<n>
Factorial (tail-recursive)
Hailstone does not have tail-call optimization.
fac [
tail_call [
n 2 <=> :?
^v^ | =_= [${acc n * n | $}]<>
-_- | [1]<>
?:
]<acc|n> <
${n n 1 - | tail_call}
]<n> <
Golfed (56 bytes)
[${n[n 2<=>:?^v^|=_=[${a n*n|$}]<>-_-|[1]<>?:]<a|n>}]<n>
Implementation
Implementing Hailstone is tricky because loops are made with recursive functions like in functional programming.
Take the following program, which is an infinite loop.
${#}
It calls the current function (the script) which can either overflow the call frames (if an implementation has a fixed number of call frames) or overflow the stack. However, any conforming implementation must allow things like these to execute without stack overflows or frame overflows.
This would be pretty easy to detect, if only it weren't for local variables.
${[
x 7 <
${x | 言え}
${#}
]<>}
And also complex function calls like this.
fn2 [${(1) f | map}]<f> <
fn1 [${f | fn2}]<f> <
${1 [
${# | fn1}
]<n>}
Here, the anonymous function calls fn1 passing itself as the parameter, which then calls fn2 with the anonymous function, which then calls a native function map (this function doesn't exist in the standard library, but it's just map in functional programming) which maps the anonymous function on an array with the number 1.
The compiler does not know what map does and it must be able to support arbitrary native functions, so a stack overflow is guaranteed.
If the function only called itself at the end of its body (a tail call), then TCO could be used to flatten the call stack or convert the recursive function into a loop.
For example, the infinite loop program ${#} is a tail call, so it could be optimized to this code:
0 GOTO 0
The reference implementation does this, some functions that call themselves are optimized into jumps.