WEP: Lenient String Parsing (LenientFromStr)
Context
Human-supplied strings become typed values throughout a program: CLI arguments (core:cli::args()), environment variables, config entries, query strings, compile-time parameters (Compile-Time Parameters, v2). Being human-typed, they carry surface variation a strict parser rejects — casing, alternate spellings (1 / 0 for a boolean), radix prefixes, digit separators.
core:prelude already has FromStr, but it is strict by design: it backs hot, machine-facing paths (router path segments, the JSON deserializer) where "TRUE" or "0x2A" must be rejected. Loosening it would break those.
So a parallel, forgiving conversion is missing. core:cli::args() returns a raw List<String>; every caller parses by hand.
Decision
Add LenientFromStr to core:prelude (auto-imported), the forgiving sibling of FromStr.
pub trait LenientFromStr {
type Err: Error;
fn from_str_lenient<S: AsStrSlice>(s: S) -> Result<Self, Self::Err>;
}
- Returns
Result: leniency widens the accepted spellings, it does not salvage invalid input. An undenotable string still yieldsErr. Err: Error, asFromStrbounds its own: a caller reaching the failure through the bound can report the reason rather than only the expected type.- One method, as
FromStr::from_stris:AsStrSlicealready admits an ownedString, a reference to one, and a view. from_str_lenientsits besidefrom_strin method completion.
A Separate Trait, Not FromStr or TryFrom<&String>
Strictness and leniency are independent capabilities — a wire-format type wants only strict FromStr, a "human duration" only lenient parsing — so they are separate traits. Against the general TryFrom<&String> (Conversion Traits), LenientFromStr is the named contract: the name promises forgiving, human-friendly spellings.
Leniency Contract
An impl accepts the common human-equivalent spellings for its type (see built-ins) and never panics — an undenotable value is Err. Leniency forgives spelling, not meaning: "0x2A" → 42, "forty-two" → Err.
It does not touch whitespace. from_str_lenient does not trim, so surrounding whitespace yields Err; trimming is the caller's concern (s.trim(), or a consumer-level policy — e.g. compile-time params trims each override before converting). Keeping the conversion faithful avoids silently discarding input and resolves whitespace-significant types (char, String) uniformly.
Built-In Implementations
| Type | Accepted |
|---|---|
String |
Any string (identity) |
char |
Exactly one Unicode scalar |
i8..i128 |
Decimal, or 0x / 0o / 0b (case-insensitive); optional leading sign |
u8..u128 |
As integers, but unsigned (a leading - is Err) |
f32, f64 |
Decimal with optional e exponent; nan / inf / infinity / ±inf (case-insensitive) |
bool |
true / false / 1 / 0 (case-insensitive) |
- Integers and floats ignore
_anywhere in the digit body (1_000,0xFF_FF).,is not a separator — it collides with the locale decimal comma and with future list/tuple delimiters;_matches Wado's own numeric literals. - A leading zero is not octal:
010is10, only0o12is octal. - Numeric impls preprocess (strip
_, split off sign / radix prefix) then delegate tofrom_str_radix/from_str(the floatFromStralready acceptsnan/inf/infinity).chardelegates to itsFromStrunchanged — one Unicode scalar has one spelling, so there is nothing for leniency to add. Onlyboolparses directly, for the spellingsFromStrrefuses. No impl alters whitespace.
Future WEPs may extend the set (e.g. core:temporal multi-format dates).
No Blanket Impl (Yet)
A blanket impl<T: FromStr> LenientFromStr for T would drop the per-type boilerplate but overlaps the bool impl. Wado grants concrete-over-general priority only for variadic impls (Variadic Type Parameters); general specialization is unadopted (soundness unsettled — research-from-into-framework.md). So built-in impls stay explicit; revisit if specialization lands.
User Types
A user type implements LenientFromStr directly, independently of FromStr (either, both, or neither). Newtypes inherit it from their base type — type Port = u16 parses like u16 with no extra impl.
Consumers
- Runtime CLI arguments / environment variables:
core:cligains a typed parse path, e.g.i32::from_str_lenient(&args[1].trim()). The ergonomic surface (a helper that trims and parses) is acore:clifollow-up. - Compile-time parameters v2: v1 converts built-in types natively with accepted forms matching these impls, so v2 — evaluating
LenientFromStrfor arbitrary types via wasm-CTFE — preserves built-in behavior.
Implementation Strategy
- Define the trait in
core:prelude(lib/core/prelude/traits.wado), auto-imported. - Add the built-in impls and a single shared
Errtype,LenientParseError(a short reason string). One unified error keeps the associated-type story simple and gives the infallibleStringimpl a never-returnedErr; numeric impls mapParseIntError/ParseFloatErrorinto it. - Document in
docs/stdlib-core-prelude.mdand the cheatsheet.
Consequences
- One discoverable contract for forgiving external input, shared by CLI args, env, config, and params.
FromStrstays strict; hot paths untouched.- The conversion is faithful: it never trims or otherwise discards input, so whitespace-significant values (
char, a space;String, leading indent) survive. Callers trim when they want to. - Built-in impls mostly wrap
FromStr, so little parsing logic is duplicated. - A type wanting both parses implements two traits — acceptable, since the contracts differ.
Future Extensions
- Blanket
impl<T: FromStr> LenientFromStr for T, once specialization is settled. - A derive for enums (case-insensitive discriminant names).
- Lenient date/time parsing in
core:temporal. - A
core:clityped-argument API on the trait.
