- Variables And Data
- Embedded Syntaxes
- Operators
- Vectorized Operations
- Arrays
- Range Literals
- Conditionals
- Loops
- Switch Expressions
- Blocks
- Classes
- Type Classes
- Type Definitions
- Worker Type Definitions
- Functions
- Bombs Errors
- Inline
- Immutability
- Generics
- Declare Invariants
- Interop
- Comments
- Grammar
- Standard Library
- Standard Library Extensions
- Project Organization
- Test System
- Build System
- Telemetry System
- Object Files Specification
- References
Decimal Types
1. Overview
Typical’s decimal types provide fixed-point decimal arithmetic with explicit control over precision and scale. Designed for serious domains like finance, scientific computing, and safety-critical systems, decimals in Typical prioritize precision and correctness, while still delivering respectable performance through SIMD optimizations and efficient backing implementations.
The decimal type family offers a spectrum of options, from ergonomic 128-bit-backed fixed decimals (decimalN) to highly customizable fixed-size variants (d32xN, d64xN) and an infinitely precise variable-width decimal type (decimal) for edge cases and maximum flexibility.
Key design goals of Typical decimals include:
Predictability: No surprises from implicit casts, silent precision loss, or runtime errors like NaN or infinity.
Performance: SIMD-friendly fixed-size decimals and Rust-backed implementations provide efficient arithmetic without sacrificing correctness.
Ergonomics: Syntax and semantics closely follow TypeScript conventions, minimizing learning friction.
LLM Friendliness: Compiler-enforced explicitness combined with simple, universal operators like the ! cast enable reliable AI code generation.
Safety: Static typing and no dynamic dispatch ensure all decimal operations are analyzable and safe at compile time.
Overall, Typical decimals are a first-class numeric family, designed to “just work” for use cases where precision and correctness are non-negotiable, without the usual ergonomic or tooling trade-offs seen in other languages.
2. Decimal Categories
2.1 Ergonomic Decimals (decimalN)
Ergonomic decimals are the flagship fixed-point decimal types in Typical, backed by a 128-bit integer representation. They are named decimal1, decimal2, ..., up to decimal128, where the number suffix indicates the number of decimal places of precision. These types use a fixed-point approach, meaning the decimal point is implicitly fixed based on the type.
Backing: All decimalN variants are backed by a 128-bit integer (scaled integer)
Precision: Fixed number of decimal places (N)
Integer Range: The integer portion automatically adjusts based on the decimal places (e.g., more decimal places reduce the integer range)
Performance: SIMD-optimized arithmetic leveraging Rust decimal libraries under the hood
Use cases: General purpose decimals where ergonomics, reasonable performance, and correctness matter most
Syntax: Simple and concise, e.g., const price: decimal4 = 19.99;
2.2 Performance Decimals (d32xN, d64xN)
Performance decimals provide finer control over the backing size and decimal precision, useful when both speed and precision are critical. They come in two variants:
d32xN: 32-bit integer backing,Ndecimal placesd64xN: 64-bit integer backing,Ndecimal places
Here, the developer specifies both the bit-width and the decimal precision explicitly, trading off ergonomics for maximal control and efficiency.
- Backing: 32-bit or 64-bit scaled integer
- Precision: User-defined decimal places (
N), limited by backing bit size - Performance: Faster than 128-bit decimals due to smaller backing integer sizes; ideal for tight loops and performance-critical code
- Use cases: HPC, embedded, or real-time systems where perf and precision are both non-negotiable
- Syntax: Slightly more verbose, e.g.,
const latency: d64x8 = 0.00012345;
2.3 Dynamic Decimal (decimal)
The dynamic decimal type offers variable precision and variable integer width, suitable for use cases where extreme precision or very large numbers are required and performance is not a core concern.
- Backing: Combination of fixed 128-bit decimal part and arbitrary-sized integer part (variable width)
- Precision: Unbounded decimal precision (limited only by implementation and memory)
- Performance: Relatively slow compared to fixed-size decimals; uses BigDecimal-like implementations on both Rust and TypeScript backends
- Use cases: Financial audits, scientific calculations requiring arbitrary precision, or developer convenience when precision/size requirements are unknown
- Syntax: Ergonomic, e.g.,
const amount: decimal = 123456789.987654321;
3.1 Explicit and Implicit Numeric Coercion Rules
Typical enforces clear, consistent rules for numeric type coercion to preserve precision and avoid accidental lossy behavior. There are two modes of coercion: implicit (safe) and explicit (forceful via !).
Implicit Coercion (Safe)
Implicit coercion is only allowed when the following conditions are met:
-
Same category:
- Coercion is only allowed within the same numeric category (e.g., int → int, decimal → decimal, float → float).
- No implicit coercion between distinct categories (e.g.,
int→decimal,decimal→float,float→int, etc.).
-
Width-safe:
-
A value of a narrower numeric type may be implicitly coerced into a wider one if the wider type can fully represent the range of the narrower type without loss.
- Example:
u8 → u16is allowed. - Example:
decimal2 → decimal8is allowed.
- Example:
-
Widening coercions across fixed decimal types are permitted when:
- The destination has equal or greater decimal precision and
- The integer portion can still be represented without overflow.
-
-
No precision loss:
- Decimal precision cannot be dropped implicitly. For example,
decimal8 → decimal2is disallowed. - Integer → float or float → decimal coercions are always explicit.
- Decimal precision cannot be dropped implicitly. For example,
-
Const folding exemption:
-
The compiler may allow implicit coercion for constant literals where the value is statically known to fit.
- Example:
const x: u8 = 1;is valid even if1was typed as adecimal128constant literal.
- Example:
-
Explicit Coercion (! operator)
All other numeric conversions must use the ! operator to indicate forceful coercion.
Rules:
- Always placed on the assignment site, not the source expression.
- Indicates that the developer is aware of potential truncation, rounding, or overflow.
- Cannot be used to coerce non-numeric types (e.g.,
string!is invalid). - May be used between any numeric categories (e.g.,
decimal → u8,i64 → decimal32,f64 → decimal). - Applies uniformly to all numeric types (ints, uints, floats, decimals).
- Cast behavior is truncate by default (rounding not yet implemented or spec’d).
Example:
const x: decimal128 = 123.456;
const y: u8 = x!; // y == 123
Semantics:
"Force this value into the target type format, even if lossy""Do whatever is necessary to make this fit"— overflow, underflow, truncation all permitted.- Useful in sum types, serialization boundaries, and performance-critical zones.
Example with sum types:
handle(n: u8 | decimal128)
{
const byte: u8 = n!; // Always legal. Will yield 0 if `n` is decimal128 with no integer part.
}
This model avoids surprises, ensures precision is never lost silently, and eliminates the refactor tax of source-side casts, which is especially beneficial for LLM-based generation and long-term maintainability.
3.2 Semantics of the ! Operator
The ! operator in Typical is a forceful numeric coercion directive. It is used to convert between numeric types when implicit conversion is disallowed due to potential precision loss, overflow, or cross-category conversion.
Purpose
- Provides a universal escape hatch for numeric type coercion.
- Signals the programmer's intent to coerce a value regardless of the risks.
- Minimizes refactor tax by colocating coercion logic at the destination site.
- Ensures ergonomics even in complex type scenarios (e.g., sum types, inferred return types).
Semantics
- Destination-directed: The
!operator is always applied at the point of assignment or usage — not at the origin of the value.
const result: u32 = someDecimal!; // NOT: const coerced = decimal!; const result: u32 = coerced;
-
Coercion is one-way:
!cannot be reversed or un-applied. -
Always explicit: The presence of
!disables the compiler's precision or overflow safety checks for that conversion.
Coercion Behavior
By default, ! performs truncate-down coercion (toward zero):
| Source → Target | Coercion Behavior |
|---|---|
| decimal → int | Truncate fractional part (e.g., 12.9 → 12) |
| int → decimal | Embed as whole number (e.g., 7 → 7.000000) |
| float → decimal | Truncate fractional part; no rounding |
| decimal → float | Convert decimal exactly (if possible) or truncate |
| decimalA → decimalB | Truncate if precision loss occurs |
| int → int (narrowing) | Wrap/truncate as needed (e.g., 300 → u8 becomes 44) |
| Any → Any | No runtime check; coercion always succeeds |
Note: Rounding modes (e.g.,
round-half-up,floor,ceil) are not currently part of the spec.
Applicability
-
Can be used on any numeric type, including:
int,uintfloatdecimalN,d32xN,d64xN,decimal
-
Cannot be applied to non-numeric types.
Examples
const a: decimal32 = 12.999;
const b: u8 = a!; // b == 12
const f: f64 = 3.14;
const d: decimal64 = f!; // d == 3.000000...
const mixed: u8 | decimal128 = getInput();
const out: u8 = mixed!; // Succeeds regardless of variant; decimals truncated to 0
Rationale
- Ergonomic: No need to wrap or rewrite source expressions when refactoring.
- Resilient to sum types: Works naturally even when the actual type is not known at compile time.
- LLM-friendly: One syntactic pattern to learn and apply everywhere (
x: T = y!always compiles). - Predictable: Always truncates (until additional rounding modes are introduced).
3.3 Destination-Side Coercion with !
The ! operator in Typical is strictly destination-side. This design choice emphasizes assignment-site control and minimizes refactor friction, while aligning with Typical’s philosophy of ergonomic correctness without silent footguns.
Rule Summary
!must appear at the assignment or usage site — never at the value definition.- Coercion is contextualized by the destination, not the source.
- The
!operator forces coercion from source into the destination type, regardless of narrowing, truncation, or precision loss. - Eliminates the need for source-side cast rewrites when refactoring destination types.
Syntax Form
const destination: TargetType = sourceExpression!;
This form is universal for numeric coercion across the language. Examples:
const a: decimal64 = 123.456;
const b: u8 = a!; // decimal64 → u8, truncates to 123
const value: f64 | decimal128 = getNumeric();
const asFloat: f64 = value!; // Either type is coerced into f64
const weird: u32 = 999999999999n!;
Motivation
✅ Refactor-Resilient
Typical intentionally places the coercion at the point of type enforcement, rather than the origin of the value, so that changing the destination type doesn’t require updating all upstream code. This avoids the “refactor shrapnel” common in source-side casting models:
// BAD: Source-cast is brittle
const a = (x as decimal64);
...
const y: f64 = a; // Needs manual re-cast if `a` changes
// GOOD: Destination-side cast
const y: f64 = x!; // Only this line depends on the target type
✅ Sum-Type Friendly
In union types, the actual runtime type may not be known. The destination-directed model allows coercion even when the input type is ambiguous at the call site:
parse(n: u8 | decimal128)
{
const byte: u8 = n!; // Always legal; decimals truncate to 0
}
✅ Inference-Friendly
This model cooperates with type inference and avoids needing complex overloads or nested conditionals when numeric type widths vary across a function or struct.
✅ LLM-Optimized
LLMs (and programmers) benefit from having a single, consistent coercion site. Since x = y! is always valid (as long as x is a number), it’s easier to generate and refactor code with minimal ambiguity.
Invalid Usage Patterns
The following are illegal:
// ❌ Source-side coercion
const coerced = someDecimal!; // Error: no known target type for coercion
// ❌ Ambiguous coercion site
const result = (a! + b!); // Error: `!` must occur in a coercible assignment context
Note: Arithmetic operations between differing numeric types must be resolved by coercion to a common type, but coercion via
!can only be applied at an explicit assignment or parameter site, not mid-expression.
4. Operator Support and Semantics
This section defines the behavior of operators for Typical’s decimal types across the three categories:
decimalN— ergonomic fixed-scale decimals with 128-bit backingdMxN— performance-oriented decimals withM-bit backing (d32xN,d64xN)decimal— dynamically wide integer, with 128-bit fixed precision
Operator support is only included where semantics are well-defined and non-surprising. If an operator is omitted or restricted, it is due to ambiguity, low utility, or safety constraints.
4.1 Arithmetic Operators
| Operator | decimalN |
dMxN |
decimal |
Notes |
|---|---|---|---|---|
+ |
✅ | ✅ | ✅ | Standard decimal addition. Overflow behavior defined per backend. |
- |
✅ | ✅ | ✅ | Subtraction. Negative values permitted. |
* |
✅ | ✅ | ✅ | Multiplication. Fixed decimal scale preserved. |
/ |
✅ | ✅ | ✅ | NaN-unsafe: division by 0 triggers runtime error. |
/? |
✅ | ✅ | ✅ | NaN-safe: division by 0 yields null. |
\ |
✅ | ✅ | ✅ | Integer division. Truncates toward zero. |
\? |
✅ | ✅ | ✅ | Integer division with null on divide-by-zero. |
% |
✅ | ✅ | ✅ | Modulo. NaN-unsafe. |
%? |
✅ | ✅ | ✅ | Modulo with null on divide-by-zero. |
** |
✅ (see below) | ✅ (see below) | ✅ (see below) | Base and exponent must be well-typed. |
** Notes:
- Semantics are only defined when exponent is an integer.
- If exponent is decimal, operation is disallowed (subject to change).
- Overflow checks must be implemented per backend.
4.2 Increment / Decrement Operators
| Operator | decimalN |
dMxN |
decimal |
Notes |
|---|---|---|---|---|
++ |
✅ (value += 1) | ✅ (value += 1) | ✅ (value += 1) | Always means +1, never “unit-scale”. |
-- |
✅ (value -= 1) | ✅ (value -= 1) | ✅ (value -= 1) | Disallowed only if result overflows backing type. |
✅ Decimals increment and decrement by the literal value
1, not the smallest unit of the type. This preserves consistency across numeric families and prevents sum-type ambiguity.
4.3 Assignment Operators
All assignment operators that correspond to supported arithmetic or logical operations are available.
| Operator | decimalN |
dMxN |
decimal |
Notes |
|---|---|---|---|---|
= |
✅ | ✅ | ✅ | Standard assignment. No implicit coercion. |
+= |
✅ | ✅ | ✅ | Adds and reassigns. |
-= |
✅ | ✅ | ✅ | Subtracts and reassigns. |
*= |
✅ | ✅ | ✅ | Multiplies and reassigns. |
/= |
✅ | ✅ | ✅ | Division (NaN-unsafe). |
%= |
✅ | ✅ | ✅ | Modulo (NaN-unsafe). |
**= |
✅ | ✅ | ✅ | Exponentiation. Same constraints as **. |
ISSUE: Rewrite this so that it says that all math operators are available for all decimal types.
Shift (
<<=,>>=,>>>=) and bitwise assignment operators (&=,|=,^=) are not defined on any decimal type. These are restricted to integer types only. This technically breaks strict behavioral equivalence with JavaScript (and TypeScript), because JavaScript allows bitwise operations on float64 values, although there is a silent conversion to 32-bit integers when this is used. This decision is subject to change before the specification has finalized.
4.4 Comparison Operators
| Operator | decimalN |
dMxN |
decimal |
Notes |
|---|---|---|---|---|
=== |
✅ | ✅ | ✅ | Identity comparison (same value + type). |
!== |
✅ | ✅ | ✅ | Identity inequality. |
== |
✅ | ✅ | ✅ | Loose value equality. Cross-type comparison coerces to widest type. |
!= |
✅ | ✅ | ✅ | Loose inequality. |
< / <= / > / >= |
✅ | ✅ | ✅ | Cross-type coercion uses widest precision between operands. |
⚠️ All cross-type comparisons coerce the narrower operand into the format of the wider one before evaluating.
4.5 Logical Operators
| Operator | decimalN |
dMxN |
decimal |
Notes |
|---|---|---|---|---|
&& |
✅ | ✅ | ✅ | Boolean coercion: 0 is falsy, non-zero is truthy. |
| ` | ` | ✅ | ✅ | |
! |
✅ | ✅ | ✅ | Boolean negation. !0 → true, !nonzero → false. |
4.6 Bitwise Operators
Not defined on any decimal types.
&,|,^,~,<<,>>,>>>: disallowed- Justification:
- Poor semantic fit for scaled fixed-point values
- No real-world use cases
- Avoids LLM and user confusion
- Not viable even with sum types (e.g.
u8 | decimal128), since the semantics don’t align
4.7 NaN-Safety Notes
Typical does not implement NaN. Instead, all dangerous operations are bifurcated into:
- Unsafe variant: produces a bomb on error (e.g.
/) - Safe variant: returns
nullon error (e.g./?)
This applies to:
- Division
- Integer division
- Modulo
Use
/when failure is unrecoverable,/?when you want a nullable fallback.
5. Type Relationships
This section defines the coercion rules, equivalence, and subtype relations between decimal types (decimalN, dMxN, decimal) and other numeric types (uN, iN, fN). It also outlines rules for implicit widening, explicit narrowing, and sum-type interactions.
5.1 Implicit Widening Rules
Typical allows safe, precision-preserving implicit conversions from narrower numeric types to wider ones.
Allowed implicit widenings:
| From | To |
|---|---|
u8 → decimalN |
✅ |
u32 → d32xN |
✅ |
u64 → d64xN |
✅ |
uN → decimal |
✅ |
d32xN → decimalN |
✅ |
dMxN → decimal |
✅ |
decimalN → decimal |
✅ |
⚠️ Implicit conversions must be lossless in both value and precision.
No implicit narrowing is permitted. All narrowing is explicit and must use !.
The Typical LSP will generate a meaningful error here to suggest that the coercion operator
!be used to avoid the error. It will also offer a quick-fix to perform the insertion automatically.
5.2 Explicit Narrowing Rules (!)
When converting from a wider to a narrower type, the ! operator must be used. This applies to:
decimal128 → u8decimal64 → d32xNdecimal → decimalNdecimalN → d32xNf64 → u32- etc.
const a: decimal64 = 42.75;
const b: u8 = a!; // becomes 42
Narrowing behavior:
- All
!coercions perform truncation, not rounding. - Overflow is not a runtime error—it yields the truncated value as-is.
- Result is implementation-defined if source value cannot fit destination’s range (e.g., huge
decimal128→u8).
5.3 Decimal-to-Decimal Conversions
All conversions between decimal types are explicit unless they meet implicit widening criteria.
| From | To | Allowed Implicitly |
|---|---|---|
d32x2 |
d64x2 |
✅ |
d32x2 |
decimal2 |
✅ |
decimal2 |
decimal |
✅ |
d64x4 |
decimal4 |
✅ |
decimal2 |
d32x2 |
❌ (requires !) |
decimal |
decimal2 |
❌ (requires !) |
decimal |
d64xN |
❌ (requires !) |
Key rule: A smaller backing size (bit width) or lower scale never implicitly accepts a larger one.
5.4 Mixed-Type Comparison Coercion
In binary comparisons between differing numeric types, the wider or more expressive type dominates:
const a: decimal64 = 42.5;
const b: u8 = 42;
a == b // b is promoted to decimal64
Promotion follows this hierarchy:
u8 < u32 < u64 < u128 < d32xN < d64xN < decimalN < decimal
If two types are equal in width but differ in precision (e.g.
d64x2vsd64x6), the one with greater scale is considered wider for comparison purposes.
5.5 Sum Types
All decimal types can participate in sum types alongside other decimals or numeric types. Coercion rules within branches follow standard ! behavior.
foo(val: u8 | decimal64)
{
const x: u8 = val!; // force-coerces regardless of which type val is
}
This approach ensures:
- Uniform coercion logic (
!always works) - Predictable semantics at destructure sites
- No special-casing for sum types
5.6 Equivalence and Identity
===checks for value and type identity.==allows for value comparison with cross-type coercion.d64x4 === decimal4→ false (different types)d64x4 == decimal4→ true if values align
6. Runtime Semantics
This section defines how decimal types behave at runtime: how they are represented in memory, how they interact with the runtime type system, and what guarantees they provide in typical execution environments. It also includes specifics on overflow, arithmetic, and type introspection.
6.1 Backed Representations
All decimal types are implemented using fixed-width integers, with a compile-time known scale (number of digits after the decimal point).
6.1.1 decimalN
- Backed by a 128-bit signed integer.
- Scale is
Ndecimal digits. - Always fits in a single register or SIMD lane.
- Supports full arithmetic and comparisons at 128-bit precision.
- Example:
const n: decimal4 = 12.3456;is stored as123456with an implicit scale of 4.
6.1.2 d32xN, d64xN
- Backed by 32-bit or 64-bit signed integers.
- Scale is
N, must be ≤ 32 ford32xN, ≤ 64 ford64xN. - No dynamic metadata at runtime.
- Arithmetic is typically done inline, suitable for embedded or SIMD use.
6.1.3 decimal
- Backed by:
- A 128-bit signed integer for the decimal part (fractional digits).
- A variable-width signed integer for the integer part.
- Uses a tagged union representation with runtime branching to select the appropriate fixed implementation, resulting in slower performance than fixed-size decimals.
- Precision is implementation-defined but expected to cover extreme use cases.
- Used when ergonomics matter more than speed.
6.2 Overflow Behavior
Decimal types do not trap on overflow. All arithmetic follows wraparound semantics, identical to integers.
const n: decimal2 = 9999.99 + 0.01→ wraps around to0.00if overflow occurs.- This behavior ensures consistency with integer types and SIMD-friendly execution.
For dynamic decimals (decimal), wraparound may degrade to a panic in debug mode, but overflow is allowed in release unless explicitly trapped.
6.3 NaN and Divide-by-Zero
Decimal types are NaN-safe.
- No
NaNvalues exist in decimal types. - Instead, explicit "safe" operators return
null:/and%→ produces a bomb (because of possible division by zero)./?and%?→ returnnullon divide/mod-by-zero.
This design avoids silent propagation of invalid numbers and aligns with explicit nullability elsewhere in the type system.
6.4 Boolean Coercion
All decimal types coerce to boolean in the same way integers do:
0.0000→false- Any nonzero value →
true
This applies to all decimal categories (decimalN, dMxN, decimal), enabling logical expressions like:
const x: decimal64 = 0.0;
if (!x) {
// treated as falsy
}
6.5 Increment and Decrement
++ and --
- Always increment or decrement by 1, not by the decimal scale.
- Applies to all decimal types, including
decimalNanddMxN.
let x: decimal4 = 2.3400;
x++; // becomes 3.3400
This preserves consistency with user intuition and makes
++predictable in sum types.
This creates a knowingly awkward situation where the ++ and -- operations on a decimal128 are effectively a no-op. This is because the decimal128 can only store numbers between 0 and 1. However, this needs to be allowed because the decimal128 could be part of a sum type. If however the ++ or -- operators are used on a standalone decimal128, the compiler could generate a warning that the operation is useless, with an explaination as to why.
6.6 Type Introspection
Decimal types expose their properties to the type system:
type decimal4 = Decimal<4, 128>;
type d32x2 = Decimal<2, 32>;
This enables:
- Static analysis
- Pattern matching on precision and width
- Type-generic math logic
6.7 Arithmetic Semantics
All arithmetic is integer-backed, scale-preserving, and does not degrade to float logic.
+,-,*operate directly on scaled integers/,%scale-adjust internally (e.g. multiplying numerator before dividing)**(exponentiation) is supported, but:- Exponent must be an integer.
- Result is truncated to scale of base type.
Example:
const x: decimal3 = 2.500;
const y = x ** 3; // = 15.625
6.8 Runtime Cost Model
| Type | Relative Perf | Notes |
|---|---|---|
d32xN |
🟢 Fastest | Fits in 32-bit register, SIMD-friendly |
d64xN |
🟢 Fast | Scalar operations on common CPUs |
decimalN |
🟡 Medium | 128-bit ops, slightly heavier |
decimal |
🔴 Slow | Allocations, dynamic size, GC pressure |
Users are encouraged to use
dMxNordecimalNwhere performance matters, anddecimalonly when flexibility is required.
7. Operator Semantics
This section defines the operator behavior for all decimal categories in Typical: decimalN, d32xN, d64xN, and decimal. Operators behave predictably and safely, with a strict avoidance of silent failure modes (such as NaN). When operations result in undefined or invalid behavior (e.g., divide-by-zero), a bomb is produced rather than a runtime error or a silent NaN.
7.1 General Rules
- All operators preserve the scale of the operands.
- Decimal types never produce NaN or exceptions.
- Bombs are produced in any context where a NaN or arithmetic error would traditionally occur.
- All decimal operators are value-preserving, no hidden rounding unless explicitly invoked via coercion.
- Operator overloads for decimals follow standard precedence and associativity rules.
7.2 Supported Operators and Behavior Matrix
| Operator | Meaning | decimalN |
d32xN/d64xN |
decimal |
Notes |
|---|---|---|---|---|---|
+ |
Addition | ✅ | ✅ | ✅ | Scale-preserving |
- |
Subtraction | ✅ | ✅ | ✅ | Scale-preserving |
* |
Multiplication | ✅ | ✅ | ✅ | Scale is preserved in result |
/ |
Division | 💣 | 💣 | 💣 | Bomb on divide-by-zero |
/? |
Safe division | ✅ | ✅ | ✅ | Returns null on divide-by-zero |
\ |
Integer divide | 💣 | 💣 | 💣 | Same as /, but truncates result |
\? |
Safe integer divide | ✅ | ✅ | ✅ | Returns null on divide-by-zero |
% |
Modulo | 💣 | 💣 | 💣 | Bomb on mod-zero |
%? |
Safe modulo | ✅ | ✅ | ✅ | Returns null on mod-zero |
** |
Exponentiation | ✅ | ✅ | ✅ | Exponent must be an integer |
++ |
Increment | ✅ | ✅ | ✅ | Adds 1 (not 0.1) |
-- |
Decrement | ✅ | ✅ | ✅ | Subtracts 1 |
Note: Division and modulo always bomb on zero unless using their safe (
?) variants. The silencing operator!can be used to silence the bomb.
7.3 Assignment Operators
All compound assignment operators are supported where applicable.
| Operator | Meaning | Supported | Notes |
|---|---|---|---|
+= |
Add & assign | ✅ | In-place addition |
-= |
Subtract & assign | ✅ | — |
*= |
Multiply & assign | ✅ | — |
/= |
Divide & assign | 💣 | Bomb on divide-by-zero |
%= |
Modulo & assign | 💣 | Bomb on mod-zero |
**= |
Power & assign | ✅ | Integer exponents only |
7.4 Comparison Operators
All comparisons between decimals and/or other number types are allowed, with the following rules:
- Cross-type comparison coerces the narrower operand into the representation of the wider operand, then compares bitwise values.
===/!==compares both value and type (type includes scale and backing width).==/!=compares value only, scale is ignored.
| Operator | Semantics |
|---|---|
=== |
Identity comparison (value + type + scale) |
!== |
Identity negation |
== |
Value comparison (type coercion allowed) |
!= |
Value inequality |
<, <=, >, >= |
Standard numeric comparison after coercion |
Example:
1.0000 == 1.00istrue, but the following is false:const a: decimal4 = 1.0000; const b: decimal2 = 1.0; console.log(a === b); // Prints "false"
7.5 Logical Operators
Logical operations follow boolean coercion semantics:
| Operator | Meaning |
|---|---|
&& |
Logical AND (coerces to bool) |
| ` | |
! |
Logical NOT (coerces to bool) |
Rules:
0.0,0.0000, etc →false- Any nonzero decimal →
true
7.6 Bitwise Operators
Bitwise operations are not supported on decimal types.
Bitwise operators are omitted intentionally. There is no meaningful use case for applying them to decimal types, and doing so introduces semantic ambiguity, especially in mixed-type contexts.
7.7 Result of Bomb-Producing Operations
Bombs are values which represent unresolvable errors in expression semantics (e.g. divide-by-zero), without immediately halting execution.
- Bombs are typed, so a
decimal64bomb is still of typedecimal64. - Any operation that consumes a bomb will also produce a bomb.
- A bomb is not
null, and cannot be pattern-matched as a regular value. - Runtime systems may halt, log, or recover based on presence of bombs, but this behavior is defined outside the type system.
7.8 Examples
const x: decimal2 = 10.00;
const y: decimal2 = 0.00;
const z1 = x / y; // 💣 bomb
const z2 = x /? y; // null
const a = decimal3(1.000);
const b = decimal2(1.00);
const same = a == b; // true
const notSame = a === b; // false
if (decimal64(0.0)) {
// will not enter
}
8. Constants, Literals, and Formatting
This section defines the syntax, resolution behavior, and formatting rules for numeric constants and literals, with special attention to decimal type interactions. The goal is to support ergonomic literal syntax for common use cases, while maintaining full control over scale, precision, and numeric category when needed.
8.1 Default Literal Resolution
Literals without a suffix or annotation resolve to the most general numeric types:
| Literal | Default Type |
|---|---|
1 |
int |
1.0 |
f64 |
1.0f |
f32 |
1.0d |
decimal |
Literals with decimals but no suffix are not decimals by default. They are floating-point values unless explicitly marked or coerced.
const x = 1.0; // x: f64
const y = 1.0f; // y: f32
const z = 1.0d; // z: decimal
8.2 Decimal Literal Resolution with as
To bind a literal directly to a precision-specific decimal type, use the as keyword.
const a = 1.25 as decimal4; // a: decimal4
const b = 2.00 as d64x2; // b: d64x2
const c = 3.14159 as d32x5; // c: d32x5
Rules:
- The literal must contain a decimal point (or be an integer with
.0). - The scale of the literal must not exceed the target type’s scale.
- e.g.
1.234 as d64x2→ compile-time error (overflowing decimal precision)
- e.g.
- Integers like
1must be written as1.0to clarify decimal-ness.
8.3 Compiler Behavior on Overprecision
If the literal's precision exceeds the declared type's scale, this is a compile-time error:
const a = 1.2345 as decimal2; // ❌ Error: value has 4 decimal digits, but type only allows 2
This avoids any implicit rounding or truncation during literal binding.
8.4 Formatting Rules (REPL, Logs, Debug)
All decimals are formatted with exact scale fidelity—trailing zeros are preserved to reflect declared precision.
| Declaration | Printed Format |
|---|---|
const x = 1.2 as decimal4 |
1.2000 |
const y = 3.0 as d64x1 |
3.0 |
const z = 2.100 as d32x3 |
2.100 |
Decimal formatting guarantees round-trippability from source to print and back to source again.
In REPL/debug output:
let price = 19.9900 as decimal4;
price: decimal4 = 19.9900
8.5 No Unit-Suffix Literals for Specific Decimals
The following does not compile:
const x = 1.234d64x3; // ❌ Invalid
To use d64x3, use as:
const x = 1.234 as d64x3;
This avoids suffix explosion and keeps decimal type control centralized to as-based explicit binding.
8.6 Design Philosophy
- Decimal literals should never surprise—no rounding, no implicit truncation.
- Suffixes (
f,d) are ergonomic entry points, not precision specifiers. - Precision-aware types must be explicitly opted into via
as, not accident. - Formatting always reflects declared intent, not runtime interpretation.