Waves

A general-purpose programming language.

It's about as fast as C++. Run your code straight away on the interpreter, or compile it to a native program through LLVM.

func main()
{
    println("Hello from Waves!");
}
$ wave dev hello
Compiled hello.waves -> hello.wbc
Hello from Waves!

It looks like this.

Every program on this page was compiled and run with the current build of Waves, and the output next to it is what it actually printed.

sea.waves Open in the playground
func main()
{
    for (int y = 0; y < 10; y++)
    {
        string line = "";

        for (int x = 0; x < 60; x++)
        {
            double swell = sin(x * 0.26) * 2.6;
            swell += sin(x * 0.05 + 1.0);
            int surface = (int)(4.5 + swell);

            if (y < surface)       { line += " "; }
            elif (y == surface) { line += "~"; }
            else                   { line += "#"; }
        }

        println(line);
    }
}

It prints the sea.

The type goes first, strings have +=, and sin is already there without an import.

                                                            
                                          ~~                
                 ~~~                   ~~~##~~~             
               ~~###~~~               ~########~            
             ~~########~             ~##########~~         ~
~           ~###########~~         ~~#############~~     ~~#
#~~       ~~##############~~     ~~#################~~~~~###
###~~~ ~~~##################~~~~~###########################
######~#####################################################
############################################################
save.waves Open in the playground
Class File
{
    string name;

    File(string name)
    {
        that.name = name;
        println(f"open  {name}");
    }

    func close()
    {
        println(f"close {that.name}");
    }
}

func main()
{
    File log = new File("log.txt");
    defer log.close();

    File data = new File("data.bin");
    defer data.close();

    println("writing...");
}

Clean up with defer.

A defer runs when its block ends, last one first. An early return, a break or a throw can't skip it. Also, it's that instead of this.

open  log.txt
open  data.bin
writing...
close data.bin
close log.txt
shapes.waves Open in the playground
import Generics;

Class Shape
{
    func double area() { return 0.0; }
}

Class Circle extends Shape
{
    double radius;
    Circle(double radius) { that.radius = radius; }
    func double area() { return 3.14159 * radius * radius; }
}

func main()
{
    var shapes = new List<Shape>();
    shapes.add(new Circle(2.0));
    shapes.add(new Shape());

    foreach (Shape s : shapes)
    {
        switch (s)
        {
            case Circle c: println(f"circle, area {c.area()}");
            default:       println("some other shape");
        }
    }
}

Classes, generics, and a switch that behaves.

A switch can match on a type, and it never falls through, so there's no break to forget.

circle, area 12.56636
some other shape
sum.waves Open in the playground
import Thread;

func sum(int from, int to)
{
    long total = 0;
    for (int i = from; i < to; i++) { total += i; }
    return total;
}

func main()
{
    var a = Thread.run(() -> sum(0, 500000));
    var b = Thread.run(() -> sum(500000, 1000000));

    println(a.result() + b.result());
}

Real threads.

Thread.run takes a closure and gives you back a handle. .result() waits for the answer.

499999500000
cube.waves Open in the playground
func int cube(int n)
{
    int result;

    asm volatile {
        use n;
        use result;
        mov rax, n
        imul rax, n
        imul rax, n
        mov [rel __waves_var_result], rax
        modifies { rax, memory }
    };

    return result;
}

func main()
{
    println(cube(7));
}

Assembly, right in the function.

Your variables are in scope, and you say what you touched. This works on the interpreter too, not only in native builds.

343
oops.waves Open in the playground
func main()
{
    int count = 3;
    println(count)
}

And when you get it wrong?

The compiler points at the spot and tells you what to do about it.

error: Expected ';' after statement
  --> oops.waves:4:18
  |
4 | println(count)
  |              ^
  | hint: Add a semicolon ';' at the end of the statement.
aborting due to 1 error

It has two compilers.

The first turns your source into Waves' own bytecode. From there you can run it straight away while you're working on it, or put it through the second one, which uses LLVM, when you want full speed.

hello.waves

50 bytes

What you write. This compiles it and runs it in one go, in about 20 ms:

wave dev hello

hello.wbc

102 bytes

Bytecode. Make it, run it on the interpreter, or pack it with the VM into a 371 KB program you can hand to someone:

wave compile hello
wave run hello
wave pack hello

hello

72 KB

Native code through LLVM. Add -Target win-x86_64 and you get a Windows .exe, built on Linux:

wave build hello -o hello -Release

3 ms behind C++.

These bars move in real time, so that's really how long each one takes to get through the same ten benchmarks. LuaJIT isn't frozen. It's just still going.

3332 ms
  1. Rust 317 ms 0.96× Waves
  2. C++ 326 ms 0.99× Waves
  3. Waves 330 ms
  4. C# AOT 590 ms 1.79× Waves
  5. C# JIT 637 ms 1.93× Waves
  6. LuaJIT 3332 ms 10.1× Waves

Total time, lower is better. The median of 11 runs on an i5-12400F, with every program producing the same checksums. Waves was built with -Release -Native, C++ with clang -O3 -march=native, and the others are the fastest builds that were available.

All ten benchmarks, in ms
BenchmarkRustC++WavesC# AOTC# JITLuaJIT
loops29.028.428.6128.8129.3122.3
fib22.722.525.240.744.041.7
collatz132.1136.2135.4242.7261.52685.1
sieve84.187.698.681.586.054.1
mandelbrot21.121.021.121.522.222.4
matmul3.83.04.06.17.08.9
bubble sort3.03.13.14.64.411.4
objects19.619.216.615.032.7317.1
alloc0.00.00.07.18.029.3
strings0.05.90.040.242.017.6
Total317.0326.4329.7590.3637.33332.1

Things made with it.

Everything here was captured from the real programs, all of them written in Waves.

Epidemic sim

8,000 agents, six of them sick. Red is infected, green has recovered. The infection spreads out in rings, so yes, it makes actual waves. 187 lines, drawn with raylib, and sped up here.

Cube Collector

3D, WASD, eight coins. The whole game is 97 lines.

The Waves installer's options page, with an install location field and two checkboxes. The Waves installer's install page, with a progress bar part of the way along.

The installer

A wizard for installing Waves, written in Waves with its GUI libraries, Vouncy and Xelquo. 296 lines. The wizard works. The part that actually installs things is still to do.

It gets tested.

461

checks, and none of them are failing. 134 programs get run three ways and compared against their expected output, and 59 more have to refuse to compile.

Get it.

There's a build for 64-bit Linux so far. Windows and macOS builds aren't up yet. Native builds also need clang or gcc installed.

See the releases

Install

curl -LO https://github.com/Merfilek/Waves-language/releases/download/0.0.1/linux-x64.tar.gz
tar -xzf linux-x64.tar.gz
sudo mkdir -p /usr/share/Wave
sudo cp linux-x64/* /usr/share/Wave/
sudo ln -s /usr/share/Wave/wave /usr/local/bin/wave
sudo /usr/share/Wave/wave restore
wave version

Start a project

wave project new mygame
cd mygame
wave project build

It comes with Basics, Color, File, Generics, Math, Random, Terminal, Time and Vouncy, and wave lib install <name> gets you the rest. Browse the libraries.