Design Trade-offs

Interpolation for String Building

+ is arithmetic only. Strings are built with interpolation ("{p.first} {p.last}"), which reads naturally and keeps multi-fragment messages punctuation-light. For a list of strings, use string.join.

Homogeneous Maps

All map values must be the same type. For heterogeneous data, use records. Heterogeneous maps would defeat the purpose of static typing.

No Nested Named Functions

Named functions are top-level only. let f = { x -> ... } for local helpers. Keeps scoping simple — no hoisting, no forward-reference confusion.

Pipe First-Argument Insertion

Matches Elixir convention. Simpler than auto-currying. Trade-off: no partial application through pipes.

? Precedence

? is a tight postfix operator: it binds like a call, so int.parse(a)? + int.parse(b)? adds two unwrapped values and -x? negates the unwrapped x. One rule covers pipelines: a ? that ends a pipeline applies to the whole pipeline, so x |> f |> g? parses as (x |> f |> g)? — the common shape pipeline? needs no parentheses.

fold_until Same-Type Constraint

Stop(value) and Continue(value) carry the same accumulator type. For search where the result type differs from state, use loop.

Integer Overflow

Silt uses 64-bit signed integers. Arithmetic that overflows (e.g. 9223372036854775807 + 1) is a runtime error, not silent wrapping. This matches the “explicit over implicit” philosophy — silent wrong answers are worse than crashes. int.abs also errors on the single unrepresentable value: int.abs(-9223372036854775808) raises integer overflow: abs(-9223372036854775808).

Float Safety

Silt has one float type, Float, and a Float is always finite: never NaN, never infinite. An operation whose result would be NaN or infinite is a runtime error, the same rule as integer overflow:

println(1.0 / 0.0) -- error: float division by zero
println(float.max_value * 2.0) -- error: float overflow
println(math.sqrt(-4.0)) -- error: math.sqrt of a negative number: -4.0
println(math.log(0.0)) -- error: math.log of a number that is not positive: 0.0

Where an input can be out of range, guard it explicitly:

let ratio = match total {
  0.0 -> 0.0
  _ -> part / total
}

Because every Float is finite, ==, ordering, hashing, sets and maps all treat floats as ordinary totally ordered values, and -0.0 is the same value as 0.0 (0.0 * -1.0 prints 0.0).

No Negative Indexing

list.get(xs, -1) is None, not “last element”: a negative index is no position of the list, like one past the end. Indices are positions from the start, period. Use list.last(xs) for the last element, or list.get(xs, list.length(xs) - 1) for explicit end-relative access. This keeps the mental model simple and avoids hidden “if negative, wrap” logic.

Immutability Cost

DP and graph algorithms must thread state through loop or fold. More verbose, but enables concurrency safety and reasoning guarantees.

Newline Sensitivity

Postfix operators (function call, ?, trailing closure) do not cross newlines. Infix operators (|>, ., ==, *, etc.) do. + and - do not cross newlines — - is ambiguous with unary negation, and + is treated symmetrically even though silt has no unary plus — place them at the end of the line to continue:

let x = 10 +
  20            -- OK: + at end of line

let y = 10
  + 20          -- NOT a continuation -- parse error (no unary plus)

(Bracket indexing xs[i] is reserved syntax but is not a real postfix operator — silt’s parser rejects it. Use list.get(xs, i), map.get(m, k), or string.slice(s, i, i + 1) instead.)

Trailing closures must start on the same line as the function call:

let ys = xs |> list.map { x -> x + 1 } -- OK
let zs = xs |> list.map { x ->        -- OK: { on same line
  x + 1
}