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
}