Skip to content
Noodle
InstallLearnPlayground
GitHub

Basic types

Noodle’s basic value types cover completion, truth values, integers, floating-point numbers, Unicode code points, and text. They are distinct static types: Noodle never silently converts one into another.

The default standard library’s Prelude makes their conventional source names available. Prelude and modules explains that relationship in detail.

Unit has one value, written (). It represents a computation that completes without returning meaningful data:

func announce() -> Unit do
Console.println("ready");
end

A block with no tail expression also produces Unit, so effect-only functions normally end with a semicolon-terminated statement rather than an explicit ().

Bool has the literals true and false. Conditions require an actual Bool; numbers, strings, and other values are not treated as truthy:

func access_label(allowed : Bool) -> String do
if allowed then "allowed" else "denied" end
end

Boolean operators are ! for negation, && for conjunction, and || for disjunction. && and || short-circuit, so the right side is evaluated only when needed.

Int is a signed 32-bit integer. Decimal, hexadecimal, and binary literals all produce Int values:

func main() -> Unit do
decimal = 42;
hexadecimal = 0x2a;
binary = 0b101010;
Debug.trace(decimal == hexadecimal && hexadecimal == binary);
end

Arithmetic operators include +, -, *, /, and %. Integer division truncates toward zero. Noodle defines the result of overflow and division by zero; these operations do not raise an arithmetic runtime error.

Int values range from -2147483648 through 2147483647. Addition, subtraction, multiplication, and unary negation use wrapping arithmetic. The mathematical result is reduced modulo 2^32, then the resulting 32 bits are interpreted as a signed two’s-complement value.

func main() -> Unit do
Debug.trace(2147483647 + 1);
Debug.trace(-2147483648 - 1);
Debug.trace(50000 * 50000);
Debug.trace(-(-2147483648));
end

The four results are, in order:

-2147483648
2147483647
-1794967296
-2147483648

Wrapping applies to a value produced by an operation. It does not make every integer literal valid: 2147483648 is rejected at compile time. Its magnitude is accepted only as the direct operand of -, which allows the minimum value -2147483648 to be written.

For a nonzero divisor, / performs signed integer division and discards the fractional part toward zero. % returns the corresponding remainder. A nonzero remainder has the sign of the dividend, the left operand:

func main() -> Unit do
Debug.trace(7 / 3);
Debug.trace(-7 / 3);
Debug.trace(-7 % 3);
Debug.trace(7 % -3);
end

These expressions produce 2, -2, -1, and 1. For every nonzero right, division and remainder satisfy:

left == (left / right) * right + (left % right)

When the divisor is zero, both operations evaluate to 0:

func main() -> Unit do
Debug.trace(42 / 0);
Debug.trace(42 % 0);
Debug.trace(0 / 0);
end

All three results are 0. No error is thrown, and the result does not retain enough information to distinguish division by zero from an ordinary zero result. Check the divisor before the operation when zero is invalid for the application, and represent that case explicitly with a type such as Option or Result.

There is one overflowing division boundary: -2147483648 / -1 wraps to -2147483648; the corresponding remainder is 0.

Comparisons use ==, !=, <, <=, >, and >= and produce Bool.

Int.parse(text) parses decimal text with an optional sign, answering with an Option: it yields None for any other shape and for magnitudes outside the Int range.

Int64 is a signed 64-bit integer with values from -9223372036854775808 through 9223372036854775807. It follows the same rules as Int at 64-bit width: addition, subtraction, multiplication, and unary negation wrap modulo 2^64; division truncates toward zero and division or remainder by zero evaluates to 0; -9223372036854775808 / -1 wraps to -9223372036854775808.

Unlike Int, every bitwise and shift operator is built in for Int64: ~~~, &&&, ^^^, |||, <<<, >>, and >>> all produce Int64, and shift amounts use the low six bits of the right operand.

Int64 has no literal syntax of its own: when an Int64 is expected, an integer literal in range becomes an Int64 and a literal outside the range is a compile-time error. The minimum value is only writable as the direct operand of -. Elsewhere, widen an Int explicitly with Int64.from_int, which succeeds for every input:

func main() -> Unit do
big : Int64 = 9223372036854775807;
small = Int64.from_int(42);
Debug.trace(big);
Debug.trace(small);
end

Conversions to_int(), to_double(), and to_string() are explicit like every other numeric conversion. Narrowing to Int keeps the low 32 bits; widening to Double may lose precision past 2^53.

Int64.parse(text) parses the same decimal shape, answering with an Option that is empty outside the 64-bit range.

Byte is an unsigned 8-bit integer with values from 0 through 255. Bytes have no literal syntax of their own: when a Byte is expected, an integer literal in range becomes a Byte and a literal outside the range is a compile-time error. Elsewhere, construct a Byte explicitly with Byte.from_int, which answers with an Option:

func main() -> Unit do
small : Byte = 65;
maybe = Byte.from_int(300);
Debug.trace(maybe);
end

Addition, subtraction, and multiplication are built in and use wrapping arithmetic: the mathematical result is reduced modulo 2^8, so 200 + 100 is 44 and 0 - 1 is 255. Division, remainder, and unary negation are not built in for Byte because no wrapping result exists for every operand; an extension may provide them for a narrower contract. Conversions to_int() and to_string() are explicit like every other numeric conversion.

Double uses binary64 floating-point values. A decimal point or exponent distinguishes a Double literal:

func circle_area(radius : Double) -> Double do
3.141592653589793 * radius * radius
end

Int and Double are different types. Write an explicit conversion such as value.to_double() when one is required. Lossy operations such as floor_to_int() and truncate_to_int() state their rounding behavior in the method name. Double.parse(text) parses decimal text with an optional fraction and exponent, answering with an Option.

Trigonometry works in radians: sin(), cos(), tan() and their inverses asin(), acos(), atan(), plus the two-argument atan2(), alongside exp(), ln(), and the PI, TAU, and E constants:

func main() -> Unit do
Debug.trace((Double.PI / 2.0).sin());
Debug.trace(Double.E.ln());
end

Rounding stays in Double with floor(), ceil(), round(), and truncate(); abs() and sign() mirror their Int counterparts, and min()/max() select extremes. Further helpers cover cube roots (cbrt()), the hypotenuse (hypot()), base-10 and base-2 logarithms (log10(), log2()), expm1()/log1p(), and the hyperbolic functions with their inverses (sinh(), cosh(), tanh(), asinh(), acosh(), atanh()).

The special values are Double.INFINITY, Double.NEG_INFINITY, and Double.nan(). The infinities behave as their arithmetic implies (INFINITY + 1.0 is still infinity); nan() is never equal to itself, so test for it with != rather than ==. Double.parse() rejects all three spellings; infinities arise from overflow and nan() from invalid operations such as 0.0 / 0.0.

More numeric helpers are listed in Strings and characters, alongside the standard conversion methods.

Char represents one Unicode code point and uses single quotes:

func main() -> Unit do
latin = 'N';
emoji = '😀';
Debug.trace(latin);
Debug.trace(emoji);
end

A character is not an integer or a one-character string. The standard library provides explicit to_int() and to_string() conversions, plus Char.from_codepoint. See Strings and characters.

String is immutable Unicode text and uses double quotes. ++ concatenates two strings:

func greeting(name : String) -> String do
"Hello, " ++ name ++ "!"
end

Use a raw string when text contains quotes, backslashes, comment markers, or several physical lines that should be copied without escape processing:

func message() -> String do
#|first line
#|second line with "quotes" and \\slashes
#|
end

The #| prefix is structural and is removed from each line. Consecutive prefixed lines are joined with \n; the final empty #| line requests a trailing newline. Raw strings can appear anywhere a string literal is accepted, including test names and attribute arguments.

Strings do not expose integer indexing or a code-point length() operation. The standard library uses Unicode-aware cursors and provides joining, iteration, slicing, and conversion APIs in Strings and characters.

An annotation checks a type; it does not request conversion:

func main() -> Unit do
count : Int = 42;
ratio : Double = count.to_double();
Console.println(ratio.to_string());
end

The available conversion method depends on the source type’s associated standard-library extensions. Conversion is kept visible at the call site.

These basic types are enough to understand bindings and function boundaries. Other important types are introduced where their behavior becomes useful:

Next: values and bindings.