Skip to content

HOP: HVM Mini Language - Part 1

Posted on:March 16, 2023 at 10:51 AM

In my previous post, I talked about why I want to use HVM to make a game engine. I decided that, for now, I was going to try to get along writing a thin language on top of raw HVM code, to make it a little bit easier to write and maintain.

For now, I’m calling that language HOP for High Order Plus.

This turned more into a basic HVM tutorial than I initially thought so I figure I’ll make this a 2 part post, with the first one going through HVM basics such as printing and loading files, and the next post going over the compiler designing aspect.

So let’s get started!

HVM Programming Basics

If you want, you can follow along yourself. ( Open an issue if you run into problems following along. )

You just have to install HVM ( assuming you’ve already installed Rust ):

cargo +nightly install --git https://github.com/HigherOrderCO/HVM.git

ℹ️ Note: I had to submit a couple fixes to HVM to get through this, and at the time of writing they haven’t been merged yet, so you can install from my fork if the above command doesn’t work.

cargo +nightly install --git https://github.com/zicklag/HVM.git

Hello World

Let’s start with the “Hello World!” of HVM. I create a file called hop.hvm and write:

Main = "Hello World!";

Now to run it we do:

$ hvm run -f hop.hvm '(Main)'
"Hello world!"

Simple enough, but what happened exactly?

  1. The -f hop.hvm flag told HVM to load our hop.hvm file.
  2. And the '(Main)' argument told HVM to normalize ( evaluate ) the expression (Main) and return the result.
  3. HVM found that Main is equal to "Hello World!" and made a substitution to give us the result.

So simple!

But can we actually write a program like this? Let’s find out.

Arguments

First, we’ll make our main function take a couple of arguments. Since we’re writing a compiler, we want to take an infile and an outfile argument:

(Main infile outfile) = (Files infile outfile)

Now we run it again:

$ hvm run -f hop.hvm '(Main "hello.hop" "hello.hvm")'
(Files "hello.hop" "hello.hvm")

Now what happened?

  1. This time we tell it to evaluate, not just (Main) but (Main "hello.hop" "hello.hvm"). "hello.hop" and "hello.hvm" correspond to our infile and outfile arguments.
  2. HVM evaluates the expression and finds that saying (Main "hello.hop" "hello.hvm") is equal to (Files "hello.hop" "hello.hvm").
  3. HVM makes the substitution and returns the result.

That’s kind of interesting, but it doesn’t do much for us. It’s just substituting main for Files. And even though it changed it to Files that doesn’t really mean anything.

Testing this out isn’t super useful but it helps us build intuition about what HVM is going to do and how we pass arguments through the HVM CLI.

Printing Output

Let’s get HVM printing some output instead. We can do that with the builtin function HVM.log or with HVM.print. They are both similar, but HVM.print will only print strings, and HVM.log will print any expression. So log is great for debugging, and print is great for user-facing output.

The HVM.log and HVM.print functions have the same structure:

(HVM.print something_to_print expression_to_return_when_done_printing)
(HVM.log   something_to_print expression_to_return_when_done_printing)

Let’s see what that looks like:

(Main infile outfile) = (HVM.print infile Done)
//                                 |        |
// Expression to print ------------^        |
// Expression to return when done printing -^
$ hvm run -f hop.hvm '(Main "hello.hop" "hello.hvm")'
hello.hop
(Done)

We printed something! But what’s up with the (Done)?

We told the print function to return Done when it was done, so when HVM goes and makes it’s substitutions, it finds that Main finally substitutes to Done.

Done, similar to Files from earlier, is completely arbitrary, and could just as well be Finished or Complete or ThisDoesNotMatter.

HVM is just going through and making substitutions until there are no more substitutions to be made.

Now, what if we want to print the input file, and then print the output file?

(Main infile outfile) =
    (HVM.print                   // First print call.
        infile                   // Expression to print.
        (HVM.print outfile Done) // Expression to return when done
                                 // ( which is another print call! )
    )
$ vm run -f hop.hvm '(Main "hello.hop" "hello.hvm")'
hello.hop
hello.hvm
(Done)

Ah, interesting! Note how we are able to use whitespace and newlines pretty much however we want to improve readability. And it shows how we are able to get sequential behavior with HVM’s print function by passing another print function in as it’s second argument.

Creating Our Input File

Now that we can print, let’s load a file! First, make sure you create the file we’ll be loading: hello.hop:

Main = "Hello from HOP!"

Our goal is to have HVM print out the contents of that file. Note that, for now, our HOP file is just normal HVM code. We’ll add extra stuff to it later.

Loading Our Input File

Loading files can be done with the HVM.load function. It takes arguments like so:

(HVM.load file_path_to_load lambda_to_run_with_file_contents_after_loading)

OK, so what’s a lambda?

Lambdas are functions that:

Lambdas in HVM look like this, with argument being any name you want to give the variable:

@argument lambda_body_expression

So we can load a file and print it out like this:

(Main infile outfile) =
    (HVM.load                               // Load a file
        infile                              // With this path
        @contents (HVM.print contents Done) // Then run this lambda
    )
$ hvm run -f hop.hvm '(Main "hello.hop" "hello.hvm")'
Main = "Hello from HOP!"

(Done)

It worked! It loaded our hello.hop file and printed it out.

Writing a File

As the last step for the introduction, let’s write to our output file.

We do this with the HVM.store function:

(HVM.store filepath file_contents expression_to_return_after_file_is_written)

Let’s do it!

(Main infile outfile) =
    (HVM.load
        infile
        @contents (HVM.store outfile contents Done)
    )
$ hvm run -f hop.hvm '(Main "hello.hop" "hello.hvm")'
(Done)

Voilà! You can now check and see that hello.hvm contains the same code as hello.hop now! We successfully read a file and wrote it to another file.

Now all we have to do is make it actually do some compiling by changing the file as it is written.

Coming right up!