Introduction

Hop is a statically typed, compiled programming language. Like Forth, Hop is stack-based. Instead of assigning values to variables, values are pushed and popped from an implicit stack. When called, functions pop their arguments off the stack and push their return values back onto it. Let's look at an example.

Hello World

Here is a hello world program in Hop:

// hello_world.hop

fn main {
    "Hello, World!" putlns
}

When this program is run, the string literal "Hello, World!" is pushed onto the stack. The putlns function then pops the string from the stack and prints it.

Running Hop Code

The Hop compiler is called chop. chop transpiles Hop code to C code. It is invoked like this:

$ chop hello_world.hop

chop produces a file called out.c. The C file can be compiled by any C compiler and run.

$ clang -o out out.c
$ ./out
Hello, World!

Functions

Built-in Types

Hop has 4 built-in types:

  1. int: a 64 bit, signed integer
  2. float: a double-precision floating point number
  3. bool: a boolean
  4. byte: an 8 bit, unsigned integer

Hop also has pointer types like *int and **byte. Hop doesn't have a character type. Instead, character literal produce integers. Like C, Hop represents strings as null-terminated sequences of bytes. A string literal produces a *byte:

    2      // int
    1.5    // float
    'A'    // int
    true   // bool
    b'z'   // byte
    "str"  // *byte

Function Signatures

Functions are defined with the fn keyword followed by a name. Then comes the parameter types, an arrow, and the return types. The arrow can be skipped if the function doesn't return anything.

fn increment int -> int { ... }   // takes an int and returns an int
fn print int { ... }              // takes an int and returns nothing
fn print int -> { ... }           // same as above, but with the optional arrow
fn constants -> byte byte { ... } // takes nothing and returns two bytes

Function Bodies

At the start of the function body, the stack contains the arguments that the function was called with. At the end of the function body, the stack should contain the return values. Here is a function that takes three ints and adds them together:

fn add_ints int int int -> int {
    + +
}

The + function adds 2 ints. It's signature is int int -> int, so each time it is called, it pops two ints off the stack and pushes their sum onto the stack. Here is an annotated version with what the stack looks like at each point when add_ints is called with 1, 2, and 3 as arguments:

fn add_ints int int int -> int { // start:   [1, 2, 3]
    +                            // after +: [1, 5]
    +                            // after +: [6]
}

Notice that at the end of the function, the stack contains a single int which matches the function's declared return type.

Function Overloading

Hop supports function overloading. This means multiple functions can have the same name as long as their parameters don't overlap. Each time an overloaded function is called, the compiler looks at the stack and determines which version of the function should be called. Here is an example:

fn overloaded int {
    ~
    "int on top of the stack!" putlns
}

fn overloaded float {
    ~
    "float on top of the stack!" putlns
}

fn main {
    1   overloaded // int version
    1.5 overloaded // float version
}

When run, this program produces the following output:

int on top of the stack!
float on top of the stack!

Two overloaded functions overlap if there exists a stack where calling them would be ambiguous. For example, consider these two overloaded functions:

fn overloaded int byte { ... }
fn overloaded byte { ... }

fn main {
    1 b'a'     // stack is [int, byte]
    overloaded // ambiguous! Which version should be called?
}

If we try to compile this code, chop gives us this error message:

$ chop bad_overload.hop
type error: signature conflicts with a previous definition
 --> bad_overload.hop:2:4
  |
2 | fn overloaded byte { ... }
  |    ^^^^^^^^^^

note: previous definition is here
 --> bad_overload.hop:1:4
  |
1 | fn overloaded int byte { ... }
  |    ^^^^^^^^^^

note: 'overloaded' has signature [int, byte] -> []

Checking for overlap with primitive types is simple. The compiler just checks if one parameter list is a suffix of another. But things get more complicated when checking generic functions.

The main Function

Every Hop program must have exactly one main function with one of the following signatures:

  • fn main
  • fn main -> int
  • fn main int **byte
  • fn main int **byte -> int

If main returns an int, that value is the exit code of the process. If main does not return an int, the exit code is 0. If main takes an int and **byte, those values correspond to argc and argv in a C program's main function.

Control Flow

Hop has 3 control flow statements: if, while, and for.

If/Else Statements

An if statement requires a bool on the top of the stack. If the value on the top of the stack is true, the if block is executed. Otherwise, the optional else block is executed. Here is an example:

fn print_age int {
    if 21 < {
        "under 21" putlns
    } else {
        "21 or over" putlns
    }
}

This function takes an int and compares it with 21. If it's less than 21, the if block is executed and prints "under 21." Otherwise, the else block is executed and prints "21 or over." We can rewrite the function to do nothing if the input is 21 or over:

fn print_age int {
    if 21 < {
        "under 21" putlns
    }
}

Now consider this code:

// bad_if.hop
fn return_1_if_true bool -> int {
    if {
        1
    } // error here
}

If we try to compile this, we get the following error message:

$ chop bad_if.hop
type error: stack does not match the stack before if block
 --> bad_if.hop:4:5
  |
4 |     } // error here
  |     ^

note: before if block, stack is [] (bad_if.hop:2:8)
note: after if block, stack is [int] (bad_if.hop:4:5)

We cannot compile this code because the stack depends on which path the program takes. At the start of the function, the stack contains the bool that the function was called with. The if statement pops the bool off the top of the stack. If it is true, the if block executes and pushes 1 onto the stack. So after the if block, the stack is [int]. But if the input is false, the if block does not execute and the stack stays empty (stack is []). This is not allowed in Hop. The types on the stack cannot depend on the path that the program takes. These two examples are okay:

fn okay_if bool {
    if {
        "Yes!" putlns // *byte is pushed and then popped
    } // so stack is empty here
}

fn okay_if_else bool {
    if {
        "Yes!"
    } else {
        "No."
    } // *byte is pushed in both cases
    putlns
}

While Statements

A while statement also pops a bool from the top of the stack. The block continues to execute as long as the bool is true. Here is an example that prints the numbers from 1 to 5:

// count.hop
fn main {
    1               // push 1 onto the stack
    while . 5 <= {  // check if the int is still less than 5
        . putln     // print the int
        1 +         // add 1 to the int
    }
    ~
}

Ignore the . and ~ functions for now. They are explained in the built-in functions chapter. When run, this is the program's output:

$ chop count.hop
$ clang -o out out.c
$ ./out
1
2
3
4
5

Again, the stack after the while block must match the stack before the while block. So this program is invalid:

// bad_while.hop
fn main {
    while true {
        1
    } // error here
}

When compiled, chop outputs this error message:

$ chop bad_while.hop
type error: stack does not match the stack before while block
 --> bad_while.hop:5:5
  |
5 |     } // error here
  |     ^

note: before while block, stack is [] (bad_while.hop:2:16)
note: after while block, stack is [int] (bad_while.hop:5:5)

For Statements

For statements iterate from a lower bound to an upper bound. At the start of each iteration, the int is pushed into the stack. Here is an alternate Implementation of our counting program that uses a for loop:

// count.hop
fn main {
    for 1 to 6 {  // int pushed onto the stack
        putln     // int printed. stack is empty
    }
}

Notice that the upper bounds is 6 because the iteration does not include the upper bound. When run, this program outputs the following:

$ chop count.hop
$ clang -o out out.c
$ ./out
1
2
3
4
5

Built-in Functions

Here is a list of Hop's built-in functions. Because Hop functions can be overloaded, some of them have multiple signatures. Functions with brackets after their names are generic. Generic functions are described in the generics chapter.

  • . - duplicate a value on the stack

    • fn .[T] T -> T T
  • ~ - pop a value from the stack

    • fn ~[T] T
  • @ - get a pointer to stack's top value

    • fn @[T] T -> T *T
  • put - print a primitive value

    • fn put int
    • fn put float
    • fn put byte
    • fn put bool
  • putln - print a primitive value with a newline

    • fn putln int
    • fn putln float
    • fn putln byte
    • fn putln bool
  • puts - print a string

    • fn puts *byte
  • putlns - print a string with a newline

    • fn putlns *byte
  • putc - print a character

    • fn putc int
  • putlnc - print a character with a newline

    • fn putlnc int
  • putp - print a pointer

    • fn putp[T] *T
  • putlnp - print a pointer with a newline

    • fn putlnp[T] *T
  • ln - print a newline

    • fn ln
  • read - read a pointer

    • fn read[T] *T -> T
  • write - write to a pointer

    • fn write[T] *T T
  • exit - exit the program with the given exit code

    • fn exit int
  • realloc - reallocate an allocated pointer

    • fn realloc[T] *T int -> *T
  • free - free an allocated pointer

    • fn free[T] *T
  • copy - copy values from one pointer to another

    • fn copy[T] let dst: *T src: *T n: int
  • pow - raise one float to the power of another

    • fn pow float float -> float
  • random - generate a random float in [0, 1)

    • fn random -> float
  • time - get the current time in seconds

    • fn time -> float
  • stdin - read a line from stdin into a heap-allocated string (or return null on failure)

    • fn -> *byte
  • to_byte - convert a number to a byte

    • fn to_byte int -> byte
    • fn to_byte float -> byte
  • to_int - convert a number to an int

    • fn to_int byte -> int
    • fn to_int float -> int
    • fn to_int[T] *T -> int
  • to_float - convert a number to a float

    • fn to_float byte -> float
    • fn to_float int -> float
  • + - add two numbers

    • fn + byte byte -> byte
    • fn + byte int -> int
    • fn + byte float -> float
    • fn + int byte -> int
    • fn + int int -> int
    • fn + int float -> float
    • fn + float byte -> float
    • fn + float int -> float
    • fn + float float -> float
    • fn +[T] *T int -> *T
    • fn +[T] int *T -> *T
  • - - subtract two numbers

    • fn - byte byte -> byte
    • fn - byte int -> int
    • fn - byte float -> float
    • fn - int byte -> int
    • fn - int int -> int
    • fn - int float -> float
    • fn - float byte -> float
    • fn - float int -> float
    • fn - float float -> float
    • fn -[T] *T int -> *T
    • fn -[T] *T *T -> int
  • * - multiply two numbers

    • fn * byte byte -> byte
    • fn * byte int -> int
    • fn * byte float -> float
    • fn * int byte -> int
    • fn * int int -> int
    • fn * int float -> float
    • fn * float byte -> float
    • fn * float int -> float
    • fn * float float -> float
  • / - divide two numbers

    • fn / byte byte -> byte
    • fn / byte int -> int
    • fn / byte float -> float
    • fn / int byte -> int
    • fn / int int -> int
    • fn / int float -> float
    • fn / float byte -> float
    • fn / float int -> float
    • fn / float float -> float
  • % - calculate the modulus of two numbers

    • fn % byte byte -> byte
    • fn % byte int -> int
    • fn % byte float -> float
    • fn % int byte -> int
    • fn % int int -> int
    • fn % int float -> float
    • fn % float byte -> float
    • fn % float int -> float
    • fn % float float -> float
  • & - logical and

    • fn & byte byte -> byte
    • fn & int int -> int
    • fn & bool bool -> bool
  • ^ - logical xor

    • fn ^ byte byte -> byte
    • fn ^ int int -> int
    • fn ^ bool bool -> bool
  • | - logical or

    • fn | byte byte -> byte
    • fn | int int -> int
    • fn | bool bool -> bool
  • << - bit shift left

    • fn << byte byte -> byte
    • fn << byte int -> byte
    • fn << int byte -> int
    • fn << int int -> int
  • >> - bit shift right

    • fn >> byte byte -> byte
    • fn >> byte int -> byte
    • fn >> int byte -> int
    • fn >> int int -> int
  • == - check if two numbers are equal

    • fn == byte byte -> bool
    • fn == byte int -> bool
    • fn == byte float -> bool
    • fn == int byte -> bool
    • fn == int int -> bool
    • fn == int float -> bool
    • fn == float byte -> bool
    • fn == float int -> bool
    • fn == float float -> bool
    • fn ==[T] *T *T -> bool
  • != - check if two numbers are not equal

    • fn != byte byte -> bool
    • fn != byte int -> bool
    • fn != byte float -> bool
    • fn != int byte -> bool
    • fn != int int -> bool
    • fn != int float -> bool
    • fn != float byte -> bool
    • fn != float int -> bool
    • fn != float float -> bool
    • fn !=[T] *T *T -> bool
  • < - compare two numbers

    • fn < byte byte -> bool
    • fn < byte int -> bool
    • fn < byte float -> bool
    • fn < int byte -> bool
    • fn < int int -> bool
    • fn < int float -> bool
    • fn < float byte -> bool
    • fn < float int -> bool
    • fn < float float -> bool
    • fn <[T] *T *T -> bool
  • <= - compare two numbers

    • fn <= byte byte -> bool
    • fn <= byte int -> bool
    • fn <= byte float -> bool
    • fn <= int byte -> bool
    • fn <= int int -> bool
    • fn <= int float -> bool
    • fn <= float byte -> bool
    • fn <= float int -> bool
    • fn <= float float -> bool
    • fn <=[T] *T *T -> bool
  • > - compare two numbers

    • fn > byte byte -> bool
    • fn > byte int -> bool
    • fn > byte float -> bool
    • fn > int byte -> bool
    • fn > int int -> bool
    • fn > int float -> bool
    • fn > float byte -> bool
    • fn > float int -> bool
    • fn > float float -> bool
    • fn >[T] *T *T -> bool
  • >= - compare two numbers

    • fn >= byte byte -> bool
    • fn >= byte int -> bool
    • fn >= byte float -> bool
    • fn >= int byte -> bool
    • fn >= int int -> bool
    • fn >= int float -> bool
    • fn >= float byte -> bool
    • fn >= float int -> bool
    • fn >= float float -> bool
    • fn >=[T] *T *T -> bool
  • ! - logical not

    • fn ! bool -> bool
    • fn ! byte -> byte
    • fn ! int -> int
  • neg - negate a number

    • fn neg int -> int
    • fn neg float -> float
  • strcmp - compare 2 strings

    • fn strcmp *byte *byte -> int
  • streq - check if 2 strings are equal

    • fn streq *byte *byte -> bool
  • strcpy - copy a string from one allocation to another

    • fn strcpy let dst: *byte src: *byte
  • strlen - find a string's length

    • fn strlen *byte -> int
  • read_file - read a file into a heap-allocated string (or return null on failure)

    • fn read_file *byte -> *byte
  • write_to_file - write a string to a file (and return true on success)

    • fn write_to_file let text: *byte file_path: *byte -> bool
  • append_to_file - append a string to a file (and return true on success)

    • fn append_to_file let text: *byte file_path: *byte -> bool

Structs

Structs can be defined with the struct keyword. Here is how you might define an array of floats:

struct FloatArray {
    ptr: *float
    len: int
}

When the Hop compiler encounters this struct definition, it autogenerates some functions. First, it creates a constructor. The constructor has the same name as the struct. It takes in each of the struct's members as parameters and returns an instance of the struct:

fn FloatArray *float int -> FloatArray { ... }

The Hop compiler also generates 4 member accessors for each member for the ptr member in our FloatArray, the compiler generates these four functions:

fn .ptr FloatArray -> *float { ... }
fn ..ptr FloatArray -> FloatArray *float { ... }
fn .ptr *FloatArray -> **float { ... }
fn ..ptr *FloatArray -> *FloatArray **float { ... }

The single dot functions consume an instance of a FloatArray and return the ptr member. The double dot functions return the ptr member but do not consume the FloatArray from the stack. The compiler also provides overloaded functions that take a pointer to the FloatArray and return a pointer to the ptr member. The compiler generates the same 4 functions for the len member:

fn .len FloatArray -> int { ... }
fn ..len FloatArray -> FloatArray int { ... }
fn .len *FloatArray -> *int { ... }
fn ..len *FloatArray -> *FloatArray *int { ... }

Here is a small program that uses the FloatArray struct:

struct FloatArray {
    ptr: *float
    len: int
}

fn print_last_float FloatArray {
    ..len          // get the length
    let len {
       .ptr        // get a pointer to the first element
       len + 1 -   // get a pointer to the last element
       read putln  // print the value
    }
}

fn main {
    5 zalloc_arr[float] // allocate room for 5 floats (stack is [*float])
    5                   // push the len               (stack is [*float, int])
    FloatArray          // construct the FloatArray   (Stack is [FloatArray])
    print_last_float    // print the last float
}

The let keyword is described here and the zalloc_arr keyword is described here.

Globals

Globals can be defined with the global keyword. Globals must have an explicit type and can optionally have an initializer in curly braces. Here are some examples:

global count: int
global flag: bool { true }
global epsilon: float { 1.0 10000.0 / }

Each global declaration autogenerates 3 functions. For the count global above with type int, these three functions are generated:

fn count -> int { ... }
fn write_count int { ... }
fn count_ptr -> *int { ... }

The count function returns the value of the global. The write_count function takes an int and updates the global. The count_ptr function returns a pointer to the global.

Example

Here is an example:

global count: int { 1 2 + } // count initialized to 3

fn increment_by_value {
    count       // get the old value
    1 +         // add 1
    write_count // write the new value
}

fn increment_by_ptr {
    count_ptr   // get a pointer to count
    . read 1 +  // increment the value (stack is [*int, int])
    write       // write the new value to the pointer
}

fn main {
    increment_by_value // count is now 4
    increment_by_ptr   // count is now 5

    count putln        // prints 5
}

Imports

Functions and structs can be imported from other files with the import keyword. Consider this directory layout:

main.hop
hello.hop
helpers
 ├ goodbye.hop
 └ pairs
    └ pair_defs.hop

hello.hop defines the say_hello and hello_again functions, goodbye.hop defines the say_goodbye function and pair_defs.hop defines the IntPair and BytePair structs:

struct IntPair {
    a: int
    b: int
}

struct BytePair {
    a: byte
    b: byte
}

Importing Functions

Let's say we want to call the say_hello, hello_again and say_goodbye functions in main.hop. We can bring them into scope like this:

import hello::{say_hello hello_again}
import helpers::goodbye::{say_goodbye}

fn main {
    say_hello
    hello_again
    say_goodbye
}

When chop encounters an import statement, it goes and parses the appropriate file. For the first import, chop looks for a file called hello.hop in the same directory as the current file (main.hop). It then parses it and bring the say_hello and hello_again functions into scope. When chop sees the second import, it looks for the file helpers/goodbye.hop and brings the say_goodbye function into scope.

Importing Overloaded Functions

You can import multiple functions with the same name as long as their parameters do not overlap. If two functions with the same name have overlaping signatures, chop will emit an error message. Here is an example of overlaping imports:

import a::{overloaded}
import b::{overloaded}

chop emits this error message:

$ chop main.hop
import error: imported function 'overloaded' overlaps with a previous import
 --> main.hop:2:12
  |
2 | import b::{overloaded}
  |            ^^^^^^^^^^

note: 'overloaded' is imported here
 --> main.hop:1:12
  |
1 | import a::{overloaded}
  |            ^^^^^^^^^^

note: previously imported signature is [int] -> [int]
note: new signature is [int] -> [int]

Imported functions can also conflict with functions defined in the current file. For example, this code contains an overlapping import:

fn overloaded int -> int {}
import a::{overloaded}

chop emits the following message:

$ chop main.hop
import error: imported function conflicts with a previously defined function
 --> main.hop:2:12
  |
2 | import b::{overloaded}
  |            ^^^^^^^^^^

note: 'overloaded' is defined here
 --> main.hop:1:4
  |
1 | fn overloaded int -> int {}
  |    ^^^^^^^^^^

note: definition of 'overloaded' has signature [int] -> [int]
note: imported function 'overloaded' has signature [int] -> [int]

Importing Structs

Structs can be imported with the import and struct keywords. If we want to import the IntPair and BytePair structs, we can do this:

import struct helpers::pairs::pair_defs::{IntPair BytePair}

fn consume_int_pair IntPair { ~ }

fn consume_byte_pair BytePair { ~ }

Note that the import struct statement only imports the struct name (for use in function signatures). The struct constructor and member accessors must be imported separately like any other function:

import struct helpers::pairs::pair_defs::{IntPair}
import helper::pairs::pair_defs::{IntPair}

fn consume_pair IntPair { ~ }

fn main {
    1 2 IntPair
    consume_pair
}

Importing Modules

Sometimes it's easier to import a whole module. You can do this by not including curly braces in the import statement. Then you can refer to imported functions and types with the :: qualified syntax:

import helpers::pairs::pair_defs

fn consume_pair pair_defs::IntPair { ~ }

fn main {
    1 2 pair_defs::IntPair
    consume_pair
}

Unlike other languages, you cannot qualify a name more than once. For example, you cannot do this because the compiler does not understand the doubly qualified name pairs::pair_defs::IntPair:

import helpers::pairs

fn consume_pair pairs::pair_defs::IntPair { ~ }

Standard Library Imports

Imports that begin with std:: get looked up in the Hop standard library. More about the standard library can be found in the standard library chapter.

Generics

Like most modern languages, Hop supports generic functions and structs. Generic parameters are specified in square brackets.

Generic Functions

As an example, consider the generic function swap from std::prelude. swap has the following signature:

fn swap[T U] T U -> U T { ... }

For some type T and some type U, it takes a value of type T and a value of type U and returns them in the opposite order. Here are some example uses:

fn main {
    true 1       // stack is [bool, int]
    swap         // T is bool and U is int (stack is now [int, bool])
    putln putln  // prints true and then 1

    "s" 1.0      // stack is [*byte, float]
    swap         // T is *byte and U is float (stack is now [float, *byte])
    putlns putln // prints s and then 1.0
}

Notice that a single function can be called with different types. Each time swap is called, the compiler looks at the stack and figures out the type of T and U. Consider this other example:

fn swap[T] T T -> T T {
    let a b { b a }
}

Ignore the let for now, it is explained in the let statement chapter. Our new swap function looks like the old one but it only has one generic parameter (T). This new function can only swap values of the same type:

fn main {
    1 2 swap      // okay, T is int
    putln putln   // prints 2 and then 1

    true 1 swap   // not okay. Should T be int or bool?
}

When we try to compile this code we get this error message:

$ chop generic_swap.hop
type error: no variant of 'swap' matches the stack
 --> generic_swap.hop:9:12
  |
9 |     true 1 swap   // not okay. Should T be int or bool?
  |            ^^^^

note: stack is [bool, int]
note: 'swap' has signature [T0, T0] -> [T0, T0]

Note that chop uses T0 in the signatures instead of T. In error messages, the compiler uses T0, T1, T2, ... where the index is the position of the generic parameter. So this function

fn generic[T U V] T V U V { ... }

really has signature [T0 T2 T1 T2].

Generic Structs

Generic structs work just like generic functions. Here is an example of how you might define a dynamic array (or vector) using a generic struct:

struct Vector[T] {
    ptr: *T
    length: int
    capacity: int
}

Now any function that takes or returns a Vector must specify the generic parameter:

// generic push
fn push[T] *Vector[T] T { ... }

// push an int to a Vector[int] (not generic)
fn push_int *Vector[int] int { ... }

Generic structs can contain any number of generic parameters:

struct Tuple[A B C] {
    a: A
    b: B
    c: C
}

fn make_int_tuple -> Tuple[int int int] {
    1 2 3 Tuple
}

Special Functions

Normally, generic functions contain all their generic parameters in their parameter list. This allows the compiler to infer the generic types based on the stack. Here are some examples:

fn foo[T] T -> T T { ... }
fn bar[K V] HashMap[K V] { ... }
fn baz[T U] T *T U *U { ... }

Functions that do not contain all their generic parameters in their parameter lists are called 'special'. Special functions can only be called through function pointers, where their generic types are written explicitly. More about function pointers can be found in the function pointers chapter. Here are some special signatures:

fn nothing[T] { ... }
fn new_ptr[T] -> *T { ... } // T only appears in return types
fn new_vector[T] -> Vector[T] { ... }

Let Statements

When dealing with more than 2 or 3 values, manipulating them on the stack becomes hard. In these cases, let statements can be helpful. The let keyword allows you to bind stack variables to names. For example, let's say we want to define a function that returns the positive root of a quadratic from its coefficients. It should take \(a\), \(b\) and \(c\) and return

\[\frac{-b + \sqrt{b^2 - 4ac}}{2a}\]

We might do something like this:

import std::prelude::{swap under fourth}

fn sqrt float -> float { 0.5 pow }

fn quad_form float float float -> float {
    under . *
    swap fourth * 4 * - sqrt
    swap neg +
    swap 2 * /
}

But notice that many of the functions are just manipulating the stack (swap, under, . and fourth). If we instead use a let statement, we can bind the function's arguments to a, b and c variables which we can refer to in our expression:

fn quad_form float float float -> float {
    let a b c {
        // 3 stack values are bound. stack is now empty
        // in this block, we can use a, b and c

        b neg
        b b * 4 a c * * -
        sqrt +
        2 a * /
    }
}

This looks much nicer.

fn let Syntax

It's quite common to immediately bind a function's arguments with a let statement. In these cases, you can use the fn let syntax. Here is the quad_form function using fn let:

fn quad_form let a: float b: float c: float -> float {
    b neg
    b b * 4 a c * * -
    sqrt +
    2 a * /
}

let for Syntax

It's also common to bind a for loop's iterator variable with a let statement. In these cases, you can use the let for syntax. Here is a countdown function using let for:

fn main {
    let i for 0 to 10 {
        10 i - putln
    }
}

Expressions

Writing everything in reverse Polish notation is kind of a pain. When doing complicated math, you can use expressions. Expressions are surrounded by parenthesis. Inside expressions, all the normal precedence rules apply and functions can be called like C functions. Our quad_form function now looks like this:

fn sqrt float -> float { 0.5 pow }

fn quad_form let a: float b: float c: float -> float {
    ((-b + sqrt(b*b - 4*a*c))/(2*a))
}

The compiler basically parses the expression, checks that every function called returns a single value and then flattens the expression. So

    (3*f(1, 3 + 2))

becomes

    3 1 3 2 + f *

Inside expressions, you can return to normal Hop syntax with square brackets. This is usually not that useful.

    (3*f(1, [3 2 +]))

Allocations

TODO

assert and abort

TODO

Function Pointers

TODO

The Standard Library

TODO

Examples

Here are some small sample programs in Hop:

  1. Guessing Game
  2. Sorting
  3. Strings

Guessing Game

Sorting

Here is a Hop program that pushes 20 random floats onto a vector, sorts the vector and prints the sorted floats:

import struct std::vector::{Vector}
import std::vector::{new_vector free push pop len set get}
import std::prelude::{swap rot}

fn sort_vector let vector: *Vector[float] {
    vector len
    let len {
        for 0 to len {
            let i for 0 to len swap - 1 - {
                if (get(vector, i) > get(vector, i + 1)) {
                    vector i i 1 + swap_values
                }
            }
        }
    }
}

fn swap_values let vector: *Vector[float] i: int j: int {
    vector i get
    vector i vector j get set
    vector j rot set
}

fn main {
    new_vector[float] call
    let _ for 0 to 20 {
        @ random push
    }
    @ sort_vector
    let _ for 0 to 20 {
        @ pop putln
    }
    free
}

When I ran this code, I got the following output:

$ chop sorting.hop
$ clang -o out out.c
$ ./out
0.984995500650257
0.975773698184639
0.922811361029744
0.865170631557703
0.828553025610745
0.819387137424201
0.779078703373671
0.690552048850805
0.686372652184218
0.490707924123853
0.439625101163983
0.418656431138515
0.358641421422362
0.328084589913487
0.29020162159577
0.273296257480979
0.272030772641301
0.117705243639648
0.0211979113519192
0.0145168676972389

Strings

Here is some sample code that demonstrates the standard library's Unicode-capable String struct:

import std::string::{push free new_string putln pointer}

global EXPECTED: *byte { "abcങ𢄯Γx𢐎yz" }

fn main {
    new_string
    @ 'a' push
    @ 'b' push
    @ 'c' push
    @ 'ങ' push
    @ '𢄯' push
    @ 'Γ' push
    @ 'x' push
    @ '𢐎' push
    @ 'y' push
    @ 'z' push
    @ putln                         // print the string
    @ pointer EXPECTED streq assert // check that it's equal to the expected string
    free
}

A Note on Transpilation

Unlike Porth, the stack exists only at compile time. When the compiler emits C code, all values are stored in variables. (Ironically, these values still live on a stack. But the C code uses a call stack.) To see how this works, consider the following program:

fn func {
    true    // stack is [bool]
    1       // stack is [bool, int]
    false   // stack is [bool, int, bool]
    putln   // stack is [bool, int]
    put     // stack is [bool]
    true    // stack is [bool, bool]
    ~ ~     // stack is []
}

The compiler keeps track of the maximum number of values of each type on the stack at any time. In this case, we have a maximum of 2 bools and 1 int. So the C code looks like this:

void func() {
    bool bool1, bool2;
    int int1;

    bool1 = true;
    int1 = 1;
    bool2 = false;
    putln(bool2);
    put(int2);
    bool2 = true;
}

As you can see, the compiler allocates enough variables to hold all the stack values. Each time we push a value onto the stack, the compiler stores it in one of the variables. We we push true in the first like of the function func, it gets stored in the variable bool1. When we push 1, it gets stored in the variable int1. When we push false, the variable bool1 is already in use, so the values get stored in the variable bool2.

When generating C code, the compiler keeps track of a variable stack instead of a type stack. The variable stack looks like this:

fn func {
    true    // stack is [bool1]
    1       // stack is [bool1, int1]
    false   // stack is [bool1, int1, bool2]
    putln   // stack is [bool1, int1]
    put     // stack is [bool1]
    true    // stack is [bool1, bool2]
    ~ ~     // stack is []
}

When a function is called (let's use putln as an example), the compiler looks at the stack and determines which variable to pass into the function. Before the putln call, the variable stack is [bool1, int1, bool2]. The compiler looks at the top of the stack and passes the variable bool2 into the function putln.