Skip to content

Track unimplemented builtins #9596

Description

@lukewilliamboswell

Goal

Track the remaining builtin work for the Zig compiler using modern Roc API shapes.

The old Rust compiler builtin files were used as a capabilities checklist only. This issue should not be read as a request to port old ability-era APIs directly.

Current source of truth:

  • src/build/roc/Builtin.roc
  • src/builtins/main.zig, src/builtins/num.zig, src/builtins/dec.zig, src/builtins/list.zig, src/builtins/str.zig
  • src/base/LowLevel.zig
  • src/canonicalize/BuiltinLowLevel.zig
  • src/backend/dev/LirCodeGen.zig
  • src/backend/wasm/WasmCodeGen.zig
  • src/eval/interpreter.zig

Modern syntax guidelines for this checklist:

  • Use List(U8), Try(ok, err), and open error rows like Try(a, [OutOfRange, ..]).
  • Use => for effectful function variants.
  • Use structural constraints like where [a.is_eq : a, a -> Bool], not old implements constraints.
  • Prefer module-specific numeric functions such as F64.sqrt or I32.count_one_bits over old generic Num.* shapes unless a generic modern mechanism is deliberately designed.

A row is done when the builtin is declared in Builtin.roc, implemented or lowered, supported by the relevant backend/interpreter path, and covered by focused tests.

Recent Zulip discussion added a few guiding points for this tracker:

  • Do not automatically re-add every old helper; demand, ergonomics, and performance still matter.
  • Effectful collection variants are not an immediate priority. Track pure and Try-returning helpers first; put effectful collection variants in the deferred section.
  • Iter is now part of the builtin direction, so some old List-centric helpers should be expressed as Iter/from_iter/collect APIs instead.
  • Dict should remain an insertion-ordered hash map; key hashing/equality should mostly be auto-derived, with a modern public hash contract still needing design.

Existing Compiler Wiring Gaps

These are not new public API proposals; they are gaps in wiring for builtin machinery that is already present or partially present.

  • T.from_numeral : Num.Numeral -> Try(T, [InvalidNumeral(Str), ..]) for every numeric T
    • Already declared in Builtin.roc and mapped to .num_from_numeral.
    • The interpreter has support.
    • The dev backend currently panics for .num_from_numeral.
    • The wasm backend expects this to be resolved before backend codegen.
  • Finish dev backend support or earlier lowering for .num_pow, .num_sqrt, .num_log, .num_round, .num_floor, .num_ceiling.
    • These low-level ops exist in LowLevel.zig.
    • The interpreter has handlers.
    • The dev backend currently panics for them.
    • Wasm supports sqrt, floor, ceiling, and round; pow and log are expected to be resolved earlier.
  • Finish dev backend support for .compare.
    • Exercism migration ran into missing Num.compare-style functionality.
    • The low-level .compare op exists and the interpreter/wasm paths have support, but the dev backend currently panics for it.

Implementation note: backend builtin wrappers should not invent their own reference-counting behavior. They should follow explicit ownership/LIR information produced earlier in the pipeline.

Numeric Builtins

Shorthand used below:

  • IntT: U8, I8, U16, I16, U32, I32, U64, I64, U128, I128
  • SignedIntT: I8, I16, I32, I64, I128
  • FracT: F32, F64, Dec
  • FloatT: F32, F64

Rows written as T.foo : ... mean the same-shaped function should be added to each concrete module in the listed type group, for example U8.foo, I8.foo, and so on.

Numeric Comparison

  • T.compare : T, T -> [LT, EQ, GT] for T in IntT
  • Dec.compare : Dec, Dec -> [LT, EQ, GT]
  • Decide whether F32.compare and F64.compare should exist, and if so define the NaN ordering semantics before exposing them.

Integer Predicates And Bit Counts

Runtime helpers already exist for is_multiple_of, leading zero count, trailing zero count, and one bit count.

Integer Arithmetic Variants

T.plus_saturated already exists for integer types and Dec. Runtime helpers also exist for subtract and multiply saturation.

  • T.minus_saturated : T, T -> T for T in IntT
  • T.times_saturated : T, T -> T for T in IntT
  • Dec.minus_saturated : Dec, Dec -> Dec
  • Dec.times_saturated : Dec, Dec -> Dec
  • T.times_try : T, T -> Try(T, [Overflow, ..]) for T in FracT - modern module-specific spelling of old Num.mul_checked; integer variants already exist.
  • T.div_ceil_by : T, T -> T for T in IntT
  • T.div_ceil_checked : T, T -> Try(T, [DivByZero, Overflow, ..]) for T in SignedIntT
  • T.div_ceil_checked : T, T -> Try(T, [DivByZero, ..]) for unsigned T in IntT

Power

Runtime helpers exist for integer and fractional power, but checked integer power is tracked separately.

Fractional Constants And Classification

Use module-specific constants and predicates rather than old top-level Num.nan_f32 / Num.infinity_f64 style names.

  • F32.nan : F32
  • F64.nan : F64
  • F32.infinity : F32
  • F64.infinity : F64
  • T.e : T for T in FracT
  • T.pi : T for T in FracT
  • T.tau : T for T in FracT
  • T.is_nan : T -> Bool for T in FloatT
  • T.is_infinite : T -> Bool for T in FloatT
  • T.is_finite : T -> Bool for T in FloatT

Fractional Math

Dec.sqrt has its own issue because the implementation needs decimal-specific care.

  • T.sqrt : T -> T for T in FloatT
  • Dec.sqrt : Dec -> Dec - see Add sqrt to Dec #5831
  • T.sqrt_checked : T -> Try(T, [SqrtOfNegative, ..]) for T in FracT
  • Decide modern log API and implement it for FracT - see Update Num.log builtin to support more implementations #5107
    • Candidate module-specific shape:
      • T.ln : T -> T
      • T.log2 : T -> T
      • T.log10 : T -> T
      • T.log_base : T, T -> T
      • T.ln_checked : T -> Try(T, [LogNeedsPositive, ..])
  • T.sin : T -> T for T in FracT
  • T.cos : T -> T for T in FracT
  • T.tan : T -> T for T in FracT
  • T.asin : T -> T for T in FracT
  • T.acos : T -> T for T in FracT
  • T.atan : T -> T for T in FracT
  • Decide modern approximate equality API - see New builtin function: Num.fracApproxEquals #5475
    • Candidate module-specific shape:
      • F32.approx_eq : F32, F32, F32 -> Bool
      • F64.approx_eq : F64, F64, F64 -> Bool
      • Dec.approx_eq : Dec, Dec, Dec -> Bool

Fractional Rounding To Integers

Runtime helpers already export round, floor, and ceiling for F32, F64, and Dec to every integer type. The public API should probably name the destination type explicitly, matching the existing to_i64_try / to_u64_wrap pattern.

For each source S in FracT and destination D in IntT:

  • S.round_to_<D> : S -> D
  • S.floor_to_<D> : S -> D
  • S.ceiling_to_<D> : S -> D

Examples:

F32.round_to_i64 : F32 -> I64
F64.floor_to_u32 : F64 -> U32
Dec.ceiling_to_i128 : Dec -> I128

Raw Bit Conversions

Runtime helpers already exist for F32 and F64 bit conversion. Dec needs a final API decision because the old names were decimal-specific (with_decimal_point / without_decimal_point) and the new API should be explicit about whether this is raw representation or scaled decimal coefficient.

  • F32.to_bits : F32 -> U32
  • F32.from_bits : U32 -> F32
  • F64.to_bits : F64 -> U64
  • F64.from_bits : U64 -> F64
  • Decide and implement raw Dec representation helpers:
    • Candidate raw-bit shape:
      • Dec.to_bits : Dec -> U128
      • Dec.from_bits : U128 -> Dec
    • Candidate scaled-coefficient shape:
      • Dec.without_decimal_point : Dec -> I128
      • Dec.with_decimal_point : I128 -> Dec

Number Parsing

Whole-string parsing via T.from_str : Str -> Try(T, [BadNumStr, ..]) already exists for all numeric types. Prefix parsing and byte parsing are tracked by #7010 and should be modernized away from the old generic Num.parse spelling.

Candidate modern shape:

Str Builtins

Current Str already has UTF-8 conversion, splitting, trimming, ASCII case conversion, prefix/suffix helpers, repeat, capacity reservation, and inspect support.

  • Str.capacity : Str -> U64
  • Str.from_utf16 : List(U16) -> Try(Str, [BadUtf16({ problem : Str.Utf16Problem, index : U64 }), ..])
  • Str.from_utf16_lossy : List(U16) -> Str
  • Str.from_utf32 : List(U32) -> Try(Str, [BadUtf32({ problem : Str.Utf32Problem, index : U64 }), ..])
  • Str.from_utf32_lossy : List(U32) -> Str
  • Str.replace_each : Str, Str, Str -> Str
  • Str.replace_first : Str, Str, Str -> Str
  • Str.replace_last : Str, Str, Str -> Str
  • Str.split_first : Str, Str -> Try({ before : Str, after : Str }, [NotFound, ..])
  • Str.split_last : Str, Str -> Try({ before : Str, after : Str }, [NotFound, ..])
  • Str.iter_utf8 : Str -> Iter(U8)
  • Str.fold_utf8 : Str, state, (state, U8 -> state) -> state
  • Str.fold_utf8_with_index : Str, state, (state, U8, U64 -> state) -> state
  • Str.drop_first_bytes : Str, U64 -> Try(Str, [BadUtf8, ..]) - see Implement builtin number parsing #7010
  • Str.drop_last_bytes : Str, U64 -> Try(Str, [BadUtf8, ..]) - see Implement builtin number parsing #7010

Not tracking old Str.to_u64 / Str.to_i64 / Str.to_f64 style functions here. The modern API already has T.from_str on numeric modules, plus the parsing work in #7010.

Iter Builtins

Current Iter has custom, iter, next, map, keep_if, drop_if, fold, take_first, drop_first, take_last, and drop_last. Recent discussion says Iter should be the shared direction for allocation-free traversal, and collection types should expose iter / from_iter where appropriate.

  • Iter.len_if_known : Iter(_item) -> [Known(U64), Unknown]
  • Iter.collect : Iter(item) -> output where [output.from_iter : Iter(item) -> output]

Collection-specific from_iter / iter functions are listed in their module sections below.

Effectful iterators should probably be a separate Stream design, not part of this builtin parity checklist yet.

List Builtins

Current List already has length, capacity reservation, sort_with, equality, append/prepend, safe access/update/swap, reverse, map/map2, filters, folds, find, split, slicing, repeat, sum/min/max, and encode/decode support.

Storage And UTF-8 Helpers

  • List.capacity : List(_item) -> U64
  • List.concat_utf8 : List(U8), Str -> List(U8)
  • List.from_iter : Iter(item) -> List(item)

Combination And Construction Helpers

  • List.join : List(List(a)) -> List(a)
  • List.join_map : List(a), (a -> List(b)) -> List(b)
  • List.intersperse : List(a), a -> List(a)
  • List.chunks_of : List(a), U64 -> List(List(a))
  • List.product : List(item) -> item where [item.times : item, item -> item, item.default : item]
  • List.append_if_ok : List(a), Try(a, _err) -> List(a)
  • List.prepend_if_ok : List(a), Try(a, _err) -> List(a)
  • List.keep_oks : List(before), (before -> Try(after, _err)) -> List(after)
  • List.keep_errs : List(before), (before -> Try(_ok, after)) -> List(after)
  • List.insert : List(a), U64, a -> Try(List(a), [OutOfBounds, ..])
  • List.drop_at_unordered : List(a), U64 -> List(a)

Multi-List Mapping

  • List.map3 : List(a), List(b), List(c), (a, b, c -> d) -> List(d)
  • List.map4 : List(a), List(b), List(c), List(d), (a, b, c, d -> e) -> List(e)

Try-Returning Variants

These fill in the higher-order variants that old builtins represented with result types and that modern Roc should spell with Try.

  • List.map_try : List(a), (a -> Try(b, err)) -> Try(List(b), err)
  • List.keep_if_try : List(a), (a -> Try(Bool, err)) -> Try(List(a), err)
  • List.fold_try : List(a), state, (state, a -> Try(state, err)) -> Try(state, err)

Sorting And Indexing

Dict Builtins

Current Dict is list-backed in Builtin.roc with a TODO to use hashing. Recent discussion says Dict should remain an insertion-ordered hash map. The old Hash ability/module should not be ported directly; the new compiler needs a modern hash/equality contract, with most key types deriving hashing/equality automatically.

Public API candidates once the representation exists:

  • Dict.with_capacity : U64 -> Dict(_k, _v)
  • Dict.capacity : Dict(_k, _v) -> U64
  • Dict.reserve : Dict(k, v), U64 -> Dict(k, v)
  • Dict.release_excess_capacity : Dict(k, v) -> Dict(k, v)
  • Dict.clear : Dict(k, v) -> Dict(k, v)
  • Dict.subscript : Dict(k, v), k -> Try(v, [KeyNotFound, ..])
  • Dict.iter : Dict(k, v) -> Iter((k, v))
  • Dict.from_iter : Iter((k, v)) -> Dict(k, v)
  • Dict.fold_until : Dict(k, v), state, (state, k, v -> [Continue(state), Break(state)]) -> state

Exact hash constraints for Dict functions are intentionally not spelled out here until the modern hash contract lands.

Set Builtins

Current Set is also list-backed in Builtin.roc; storage-capacity functions should land with the hash-backed representation work.

  • Set.with_capacity : U64 -> Set(_item)
  • Set.capacity : Set(_item) -> U64
  • Set.reserve : Set(item), U64 -> Set(item)
  • Set.release_excess_capacity : Set(item) -> Set(item)
  • Set.clear : Set(item) -> Set(item)
  • Set.subscript : Set(item), item -> Bool
  • Set.iter : Set(item) -> Iter(item)
  • Set.from_iter : Iter(item) -> Set(item)
  • Set.fold : Set(item), state, (state, item -> state) -> state
  • Set.fold_until : Set(item), state, (state, item -> [Continue(state), Break(state)]) -> state
  • Set.join_map : Set(a), (a -> Set(b)) -> Set(b)

Try Builtins

Current Try already has is_ok, is_err, ok_or, err_or, map_ok, map_ok!, map_err, map_err!, and equality.

  • Try.map_both : Try(ok1, err1), (ok1 -> ok2), (err1 -> err2) -> Try(ok2, err2)
  • Try.map_both! : Try(ok1, err1), (ok1 => ok2), (err1 => err2) => Try(ok2, err2)
  • Try.map2 : Try(a, err), Try(b, err), (a, b -> c) -> Try(c, err)
  • Try.map2! : Try(a, err), Try(b, err), (a, b => c) => Try(c, err)
  • Try.on_err : Try(ok, err), (err -> Try(ok, other_err)) -> Try(ok, other_err)
  • Try.on_err! : Try(ok, err), (err => Try(ok, other_err)) => Try(ok, other_err)
  • Try.catch : Try(ok, err), (err -> a), (ok -> a) -> a - modern spelling of Add function catch : Result ok err, (err -> a), (ok -> a) -> a to standard library.  #6759
  • Try.catch! : Try(ok, err), (err => a), (ok => a) => a
  • Try.collapse : Try(a, a) -> a - modern spelling of add Result.collapse #3439

Deferred / Needs Design

These came up during the research pass, but should not be treated as immediate implementation checklist items.

  • Effectful collection variants. Recent List builtin discussion explicitly suggested not doing these yet. Candidate shapes, if/when we revisit them:
    • List.map! : List(a), (a => b) => List(b)
    • List.map_with_index! : List(a), (a, U64 => b) => List(b)
    • List.map3! : List(a), List(b), List(c), (a, b, c => d) => List(d)
    • List.map4! : List(a), List(b), List(c), List(d), (a, b, c, d => e) => List(e)
    • List.keep_if! : List(a), (a => Bool) => List(a)
    • List.drop_if! : List(a), (a => Bool) => List(a)
    • List.count_if! : List(a), (a => Bool) => U64
    • List.fold! : List(a), state, (state, a => state) => state
    • List.fold_with_index! : List(a), state, (state, a, U64 => state) => state
    • List.fold_until! : List(a), state, (state, a => [Continue(state), Break(state)]) => state
    • List.fold_with_index_until! : List(a), state, (state, a, U64 => [Continue(state), Break(state)]) => state
    • List.map_try! : List(a), (a => Try(b, err)) => Try(List(b), err)
    • List.keep_if_try! : List(a), (a => Try(Bool, err)) => Try(List(a), err)
    • List.fold_try! : List(a), state, (state, a => Try(state, err)) => Try(state, err)
    • List.for_each_try! : List(a), (a => Try({}, err)) => Try({}, err)
    • Dict.fold! : Dict(k, v), state, (state, k, v => state) => state
    • Dict.fold_until! : Dict(k, v), state, (state, k, v => [Continue(state), Break(state)]) => state
    • Dict.map! : Dict(k, a), (k, a => b) => Dict(k, b)
    • Dict.keep_if! : Dict(k, v), ((k, v) => Bool) => Dict(k, v)
    • Dict.drop_if! : Dict(k, v), ((k, v) => Bool) => Dict(k, v)
    • Set.fold! : Set(item), state, (state, item => state) => state
    • Set.fold_until! : Set(item), state, (state, item => [Continue(state), Break(state)]) => state
    • Set.map! : Set(a), (a => b) => Set(b)
    • Set.keep_if! : Set(a), (a => Bool) => Set(a)
    • Set.drop_if! : Set(a), (a => Bool) => Set(a)
  • Effectful iterators / streams. Recent iterator discussion suggested these should probably be a separate Stream design and are not high priority.
  • String literal dispatch via a future from_quote-style static dispatch hook. This is a language/API design topic, not part of current builtin parity.
  • Builtin import behavior such as import roc.Str. Recent discussion did not settle on changing default builtin imports.
  • Interpreter behavior for erroneous builtin calls. That is a compiler error-staging/correctness issue, not a missing public builtin API.

Intentionally Not Tracked From The Old Compiler

These existed in the old compiler builtins but should not be added to this checklist without a fresh design:

  • Old Inspect.roc module. Current direction is automatic string conversion/inspection via to_str and Str.inspect, not a public ability-era inspector module.
  • Old Hash.roc ability and Hasher API. Dict/Set still need hash-backed implementation work, but the public hash contract should be designed for modern structural dispatch.
  • Old Str.to_u64 / Str.to_i64 / Str.to_f64 parsing functions. Numeric modules already provide T.from_str; prefix parsing belongs with Implement builtin number parsing #7010.
  • Old List.walk* names where current List.fold* or Iter APIs already cover the same behavior.
  • Str.subscript or string slicing in core builtins. Recent subscript discussion called out string indexing as a Unicode footgun; use dedicated text/unicode APIs instead.

Related Issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    builtinsRelates to roc builtins like Bool, List, Str ...good first issueGood for newcomers

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions