Statements

Table of Contents

import

import imports names from other files. It should look like the following:

import {
  Qux,
  ZZ,
  sample.buz,
  this.foo,
  this.item.bar {some-func, other-func},
}

import can only be at the top of a file.

Regular Entry

A regular entry in import is something like the following:

  • this.foo
  • this.item.bar {some-func, other-func}
  • sample.buz

A regular entry starts from the alias of the module (this, sample). The alias of the module is specified in dependency in module.ens. If the file we want to import is inside the current module, we'll write this.

The remaining part of the regular entry is the relative path from the source directory. For example, if we want to import (source-dir)/item/bar, we'll have to write item.bar after the alias of the module.

A regular entry can be constructed by concatenating the alias and the path with .. In the case of this.item.bar, the alias part is this, and the path part is item.bar.

You can specify names in {}. The names specified here can be used without qualifiers:

import {
  this.item.bar {some-func},
}

define yo(): unit {
  some-func(arg-1, arg-2)
}

Unlisted names must be qualified:

import {
  this.item.bar,
}

define yo(): unit {
  this.item.bar.some-func(arg-1, arg-2)
}

You can also list static files in import:

import {
  static {some-file, other-file}
}

For more on static files, please see the section in Modules.

Prefix Entry

A prefix entry in import is something like Qux or ZZ. That is, a capitalized name that doesn't contain any ..

A prefix entry in import must be defined in the prefix of the current module's module.ens. Suppose that module.ens contains the following:

{
  // ..
  prefix {
    Qux "this.item.bar",
  },
  // ..
}

Then, the code

import {
  this.item.bar,
}

define use-some-func(): unit {
  this.item.bar.some-func()
}

can be rewritten into:

import {
  Qux,
}

define use-some-func(): unit {
  Qux.some-func()
}

You may also want to see the explanation of prefix in Modules.

define

define defines a function. It should look like the following:

define foo(x: int, y: int): int {
  add-int(x, y)
}

define identity-1(a: type, x: a): a {
  x
}

// a function with an implicit argument
define identity-2<a>(x: a): a {
  x
}

Defined functions can then be used:

define use-foo(): int {
  foo(1, 2)
}

define can optionally have implicit arguments, as in identity-2 in the above example. The compiler inserts these implicit arguments at compile time, so you don't have to write them explicitly:

define use-func-with-implicit-arg(): int {
  let x = 10 in
  let y = identity-1(int, x) in // ← explicit version
  let z = identity-2(x) in      // ← implicit version
  z
}

A function with the same name can't be defined in the same file.

All the tail-recursions in Neut are optimized into loops (thanks to geniuses in the LLVM team).

Note that statements are order-sensitive as in F#. Thus, the following code results in an error:

define bar(): int {
  foo() // `foo` is undefined here
}

define foo(): int {
  10
}

You have to use the statement nominal explicitly for forward references.

inline

inline defines an inline function. It should look like the following:

inline foo(x: int, y: int): int {
  print("foo");
  add-int(x, y)
}

inline is the same as define except that the definition is always expanded at compile-time. For example, if you write

define use-inline-foo(): int {
  let val =
    foo(10, 20)
  in
  val
}

The compiler will translate the above code into the following:

define use-inline-foo(): int {
  let val =
    let tmp1 = 10 in
    let tmp2 = 20 in
    print("foo");
    add-int(tmp1, tmp2)
  in
  val
}

constant

constant defines a constant. It should look like the following:

constant some-number: int {
  123
}

The compiler tries to reduce the body of a constant at compile time. The compiler reports an error if it can't reduce the body into a value. For example, the following should raise an error:

constant some-number: int {
  print("hello");
  123
}

The compiler can't reduce print("hello"); 123 into a value, so it raises an error.

You can use constants just like ordinary variables:

// define a constant
constant some-number: int {
  123
}

define use-constant(): int {
  // ... and use it
  print-int(some-number);
  456
}

data

data defines an algebraic data type (ADT). It should look like the following:

data nat {
| Zero
| Succ(nat)
}

data list(a) {
| Nil
| Cons(a, list(a))
}

data config {
| Config(
    count: int,
    foo-path: &text,
    colorize: bool,
  )
}

You can use the content of an ADT value by using match or case:

define length<a>(xs: list(a)): int {
  // destruct ADT values using `match`
  match xs {
  | Nil =>
    0
  | Cons(_, ys) =>
    add-int(1, length(ys))
  }
}

define length-noetic<a>(xs: &list(a)): int {
  // read noetic ADT values using `case`
  case xs {
  | Nil =>
    0
  | Cons(_, ys) =>
    add-int(1, length-noetic(ys))
  }
}

define use-config(c: config) {
  // pattern-matching in `let` is also possible
  let Config of {count, some-path} = c in
  print(count)
}

resource

resource defines a new type by specifying how to discard/copy the values of the type. It should look like the following:

resource my-new-type {
  function (value: int) {
    // .. discard the value ..
  },
  function (value: int) {
    // .. create a new clone of the value and return it as int ..
  },
}

resource takes two terms. The first term ("discarder") receives a value of the type and discards the value. The second term ("copier") receives a value of the type and returns the clone of the value (keeping the original value intact).

The type of a discarder is (int) -> int. The type of the argument is int, so you'll have to cast it if necessary. After discarding the argument, you should return 0 from this function. You might want to call functions like free in this term.

The type of a copier is (int) -> int. The type of the argument is int, so you'll have to cast it as necessary. The return value in this term is the new clone of the argument, cast to int. You might want to call functions like malloc in this term.

For example, the following is a definition of a "boxed" integer type with some noisy messages:

resource boxed-int {
  // discarder
  function (v: int) {
    print("discarded!\n");
    free(v);
    0
  },
  // copier
  function (v: int) {
    let orig-value = load-int(v) in
    let new-ptr = malloc(1) in
    magic store(int, orig-value, new-ptr);
    new-ptr
  },
}

// provide a way to introduce new boxed integer
define create-new-boxed-int(x: int): boxed-int {
  let new-ptr = malloc(8) in
  store-int(x, new-ptr);
  magic cast(int, boxed-int, new-ptr)
}

A value of type boxed-int prints "discarded!\n" when the value is discarded.

resource can be used to define low-level types like arrays.

You can find an example usage of resource in the int8-array.nt in the core library.

nominal

nominal declares functions for forward references. It should look like the following:

nominal {
  is-odd(x: int): int,
}

An entry of nominal is the same form as found in define. Nominal definitions can be used to achieve mutual recursions:

nominal {
  is-odd(x: int): int, // nominal definition of `is-odd`
}

// given a non-negative integer `x`, returns true if `x` is even.
define is-even(x: int): bool {
  if eq-int(x, 0) {
    True
  } else {
    is-odd(sub-int(x, 1)) // ← using nominal definition
  }
}

// given a non-negative integer `x`, returns true if `x` is odd.
// ("real" definition of `is-odd`)
define is-odd(x: int): bool {
  if eq-int(x, 0) {
    False
  } else {
    is-even(sub-int(x, 1))
  }
}

If a nominal definition isn't followed by a real definition, the compiler reports an error.

foreign

foreign declares functions that are defined in linked objects. It should look like the following:

foreign {
  neut_myapp_v1_add_const(int): int,
}

Foreign functions declared here can be called by using magic external(..).

Suppose that you have a C source file with the following definition:

// add_const.c

int neut_myapp_v1_add_const(int value) {
  return value + 100;
}

You can add the field foreign to your module.ens to compile and link this C source file, as written here. Under this setting, the following code can utilize neut_myapp_v1_add_const:

foreign {
  neut_myapp_v1_add_const(int): int,
}

define main(): unit {
  let x: int = 10 in
  print-int(magic external neut_myapp_v1_add_const(x)); // ← `magic external` is used here
  print("\n")
}

An example project that uses foreign can be found here.

You can also use LLVM intrinsics. For example, the LLVM langref states that llvm.sin.* intrinsic is available:

declare float     @llvm.sin.f32(float  %Val)
declare double    @llvm.sin.f64(double %Val)
declare x86_fp80  @llvm.sin.f80(x86_fp80  %Val)
declare fp128     @llvm.sin.f128(fp128 %Val)
declare ppc_fp128 @llvm.sin.ppcf128(ppc_fp128  %Val)

Thus, the next is a valid use of foreign:

foreign {
  llvm.sin.f64(float): float,
}

define sin(x: float): float {
  magic external llvm.sin.f64(x)
}

Syscall wrapper functions and library functions are also available:

foreign {
  write(int, pointer, int): int, // write(2)
  exit(int): void, // exit(3)
  pthread_exit(pointer): void, // pthread_exit(3)
}

In foreign entries, you can use int, int1, ..., int64 float, float16, float32, float64, void, and pointer as types.

When declaring a variadic function, declare only the non-variadic part:

foreign {
  printf(pointer): void,
}

Then, specify the types of variadic arguments when using magic external:

define print(t: &text): unit {
  // ..
  magic external printf(fmt)(len: int, val: pointer)
  //                         ^^^^^^^^^^^^^^^^^^^^^^
  //                         passing variadic arguments with types
}