Ix Programming Language

Ix (Inferable Expression) is a high-level programming language for developing web applications and games.

Key goals of this project are:

It primarily focuses on merging some great points of Rust, Zig, and JavaScript's language designs:

This manual is a standalone HTML document. Code samples are compiled and tested as a part of the release cycle.

Hello World

example.ix
let print = @extern("console").log;
fn main {
  print("Hello, world!");
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

The print function is implemented by getting an external reference to the console JavaScript global, retrieving the log property from it, and then calling it when the program starts.

In the future, common APIs will be available in the standard library, such as std.log.info(). For now, no such library exists.

Module Scope and Declarations

The top-level

example.ix
let x = 1;

Primitive Data Types

name ts equivalent description
bool boolean either true or false
float number a 64-bit floating point number
int number an integer number in signed 32-bit range
bigint bigint an integer number with no range limit
string string a sequence of text characters
void undefined / null / void detectable lack of a value
opaque unknown / any could be any value
noreturn never control flow prevents this value from existing
Note: Unlike JavaScript, the values undefined and null are merged into one concept: void

Built-in Constants

name description
true boolean true (result of 1 == 1)
false boolean false (result of 1 == 2)
std handle to the standard library module

These constants act as reserved names, and it is an error to re-declare them. Note that primitive data types are also considered built-in constants.

example.ix
fn main {
  let opaque = -5.2;
  let true = 1 == 0;
}
$ ix build example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

Numbers

Numbers have the following rules:

Strings

Numbers

Multi-line string literals

example.ix
let bwaa =
  \\hello
  \\  world
  \\end
;

Void

Void is the lack a value. For example, a function that does nothing returns void.

example.ix
pub fn hello() {
  // do nothing
}

!-- TODO: change this section to "Optionals" -->

Variables

There are two types of variables, let and var.

let bindings cannot be overridden, and should be thought of as aliases of the expression within. Referring to these identifiers is equivalent to inlining it where it was used, aside from side effect duplication.

example.ix
let print = @extern("console").log;

fn main {
  let hello: int = 1;
  let world = hello + 2;
  let hello = hello * world;

  print(hello);
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

var bindings create mutable memory locations at runtime. This means they can be re-assigned to:

example.ix
fn main {
  var message = "first message";

  if @extern("Math").random() > 0.5 {
    message = "second message";
  }

  @extern("console").log(message);
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

Using let is preferred whenever possible, as it will lead to more readable code, and the compiler is able to optimize it better. The above can be rewritten as:

example.ix
fn main {
  let message = if @extern("Math").random() > 0.5 {
    "second message"
  } else {
    "first message"
  };

  @extern("console").log(message);
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

It should be noted that numeric literals are a comptime-only data type, meaning it cannot be assigned to a var without an explicit type annotation. Since int is a strict subset of valid floats, the coercion will implicitly be a float, but a compiler warning will be emitted.

example.ix
var number = 0;        // `number` is `float`, but not explicit
var number: int = 0;   // `number` is `int`
var number: float = 0; // `number` is `float`

Functions

Functions are declared with the fn keyword. Parameter types must be annotated. The function body is a block, which may use implicit return to return.

example.ix
let print = @extern("console").log;

fn add(a: int, b: int) {
  a + b
}

fn main {
  print("Sum of 2 and 7:", add(2, 7));
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

The return expression can be used within functions to return early.

example.ix
let print = @extern("console").log;

fn something(a: int) -> bool {
  if a == 0 {
    return false;
  }

  a > 100
}

fn main {
  print(something(0xFF), something(0));
}

Number Literals

TODO

String Literals

TODO

JavaScript Interop

A global variable handle can be retrieved with @extern. We've seen this in the hello world. The type of this value is a special type called opaque, which disables type-checking.

Note: A future update will improve on the type-safety of opaque

Many JS-exclusive actions are exposed via built-in functions, such as using @jsNew for calling constructors.

example.ix
let print = @extern("console").log;

fn currentYear() -> int {
  @jsNew(@extern("Date")).getFullYear()
}

fn main {
  print(currentYear());
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

A field access on an external handle is allowed to be performed at compile time, where such access will only crash at runtime when the value is used.

Structs

To group data together, a struct can be used. A struct contains zero or one fields of runtime-compatible types.

example.ix
let print = @extern("console").log;

struct Position {
  x: int,
  y: int,
}

fn main {
  let center = Position{ x: 0, y: -2 };
  print(center.x, center.y);

  // To allow less verbose code, the struct type can be inferred with `.`
  let added = addPositions(
    .{ x: 1, y: -4 },
    .{ x: 5, y: 8 },
  );
  print("Add result:", added.x, added.y);
}

fn addPositions(a: Position, b: Position) -> Position {
  .{ x: a.x + b.x, y: a.y + b.y }
}
$ ix run example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]

To allow optimizations, structs by default do not have a defined representation in JavaScript. This means passing the struct to an opaque function is not allowed.

example.ix
let print = @extern("console").log;

struct Person {
  name: string,
  age: int,
}

fn main {
  print(Person{ name: "hello", age: 42 });
}
$ ix build example.ix
✘ error: missing top-level function "main"
 ┬ /dev/stdin:1:1
1│ 
 ╰─
[Process exited with 1]