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");endA 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" endendBoolean 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);endArithmetic 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.
Wrapping arithmetic
Section titled “Wrapping arithmetic”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));endThe four results are, in order:
-21474836482147483647-1794967296-2147483648Wrapping 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.
Division and remainder
Section titled “Division and remainder”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);endThese 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);endAll 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);endConversions 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);endAddition, 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
Section titled “Double”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 * radiusendInt 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());endRounding 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);endA 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
Section titled “String”String is immutable Unicode text and uses double quotes. ++ concatenates
two strings:
func greeting(name : String) -> String do "Hello, " ++ name ++ "!"endRaw multiline strings
Section titled “Raw multiline strings”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 #|endThe #| 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.
Type equality and explicit conversions
Section titled “Type equality and explicit conversions”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());endThe available conversion method depends on the source type’s associated standard-library extensions. Conversion is kept visible at the call site.
Types introduced later
Section titled “Types introduced later”These basic types are enough to understand bindings and function boundaries. Other important types are introduced where their behavior becomes useful:
- Records and tuples are structural language types;
- Datatypes define nominal alternatives;
- Arrays are standard-library collection views over the language’s array representation;
- Option and Result model absence and success-or-error outcomes;
- Async code covers promises, scopes, and cancellation.
Next: values and bindings.