libexpr: coerce integers under the XP feature coerce-integers
This introduces a new (demanded?) feature for coercing integers in interpolation arguments under the experimental feature `coerce-integers`. This feature is being introduced behind an *experimental feature flag* due to the cautious approach we're taking. The codebase has a track record of revealing unexpected behaviors, often in subtle ways, so we want to give this sufficient time and exposure before making it stable. To remove the experimental flag, we want to see **at least two releases or six months of real-world usage -- whichever is longer** -- that demonstrate strong confidence the feature doesn't introduce regressions or unintended side effects. If that level of confidence is reached, we'll proceed to stabilize it. Change-Id: I825904719eeba8f0e2a93cd6b93cfe6cebd7d827 Signed-off-by: Raito Bezarius <raito@lix.systems>
This commit is contained in:
@@ -66,6 +66,9 @@ delan:
|
||||
forgejo: delan
|
||||
github: delan
|
||||
|
||||
delroth:
|
||||
github: delroth
|
||||
|
||||
detroyejr:
|
||||
display_name: Jonathan De Troye
|
||||
github: detroyejr
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
synopsis: Experimental integer coercion in interpolated strings
|
||||
issues: []
|
||||
cls: [3198]
|
||||
category: "Features"
|
||||
credits: [raito, delroth, horrors, winter]
|
||||
---
|
||||
|
||||
Ever tried interpolating a port number in Lix and ended up with something like this?
|
||||
|
||||
```nix
|
||||
"http://${config.network.host}:${builtins.toString config.network.port}/"
|
||||
```
|
||||
|
||||
You're not alone. Thousands of Lix users suffer every day from excessive `builtins.toString` syndrome. It’s 2025, and we still have to cast integers to use them in strings.
|
||||
|
||||
To address this, Lix introduces the **`coerce-integers`** experimental feature. When enabled, interpolated integers within `"${...}"` are automatically coerced to strings. This allows writing:
|
||||
|
||||
```nix
|
||||
"http://${config.network.host}:${config.network.port}/"
|
||||
```
|
||||
|
||||
without additional conversion.
|
||||
|
||||
To enable the feature, you need to add `coerce-integers` to your set of experimental features.
|
||||
|
||||
### Stabilization criteria
|
||||
|
||||
The `coerce-integers` feature is experimental and limited strictly to string interpolation (`"${...}"`). Before stabilization, the following must hold:
|
||||
|
||||
1. **Interpolation-only**
|
||||
Coercion must not occur outside interpolation. Expressions like `"" + 42` must continue to fail.
|
||||
|
||||
2. **Expectation that no explicit cast are being observed**
|
||||
Cases observing explicit coercion (e.g., via `tryEval` gadget or similar) are expected not to be load-bearing in actual production code.
|
||||
|
||||
### Timeline for stabilization
|
||||
|
||||
If the feature proves safe and is widely adopted across typical usage (e.g., actual configurations in the wild turning on the flag, non-trivial out-of-tree projects using it), the experimental flag will be removed **after six months of active use or two Lix releases**, whichever is longer.
|
||||
|
||||
This avoids locking the feature in experimental status indefinitely, as happened with Flakes, while allowing time for validation and ecosystem integration.
|
||||
|
||||
### What about coercing floats or more?
|
||||
|
||||
Coercion beyond integers -- such as for floats or other types -- is **not planned**, even under an experimental flag. Questions like "what is the canonical string representation of a float?" involve subtle and context-dependent trade-offs. Without a robust and principled mechanism to define and audit such behavior, introducing broader coercion risks setting unintended and hard-to-reverse precedents. The scope of `coerce-integers` is intentionally narrow and will remain so.
|
||||
|
||||
In terms of outlook, a proposal like https://git.lix.systems/lix-project/lix/issues/835 could pave the way for a better solution.
|
||||
+12
-2
@@ -2028,12 +2028,17 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
|
||||
state.ctx.errors.make<EvalError>("cannot add %1% to a float", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
|
||||
} else {
|
||||
if (s.empty()) s.reserve(es.size());
|
||||
|
||||
/* If we are coercing inside of an interpolation, we may allow slightly more comfort by coercing things like integers. */
|
||||
auto coercionMode = isInterpolation && featureSettings.isEnabled(Xp::CoerceIntegers)
|
||||
? StringCoercionMode::Interpolation : StringCoercionMode::Strict;
|
||||
|
||||
/* skip canonization of first path, which would only be not
|
||||
canonized in the first place if it's coming from a ./${foo} type
|
||||
path */
|
||||
auto part = state.coerceToString(i_pos, vTmp, context,
|
||||
"while evaluating a path segment",
|
||||
StringCoercionMode::Strict, firstType == nString, !first);
|
||||
coercionMode, firstType == nString, !first);
|
||||
sSize += part->size();
|
||||
s.emplace_back(std::move(part));
|
||||
}
|
||||
@@ -2340,12 +2345,17 @@ BackedStringView EvalState::coerceToString(
|
||||
}
|
||||
}
|
||||
|
||||
/* Raito: Any addition to this mode is subject to extra scrutiny
|
||||
* until we have better formatting tools. */
|
||||
if (mode >= StringCoercionMode::Interpolation) {
|
||||
if (v.type() == nInt) return std::to_string(v.integer.value);
|
||||
}
|
||||
|
||||
if (mode >= StringCoercionMode::ToString) {
|
||||
/* Note that `false' is represented as an empty string for
|
||||
shell scripting convenience, just like `null'. */
|
||||
if (v.type() == nBool && v.boolean) return "1";
|
||||
if (v.type() == nBool && !v.boolean) return "";
|
||||
if (v.type() == nInt) return std::to_string(v.integer.value);
|
||||
if (v.type() == nFloat) return std::to_string(v.fpoint);
|
||||
if (v.type() == nNull) return "";
|
||||
|
||||
|
||||
@@ -69,11 +69,14 @@ typedef enum {
|
||||
*
|
||||
* - Strict: Only allow coercion of values that are already strings,
|
||||
* paths, or derivations.
|
||||
* - Interpolation: Additionally allow coercion of unambiguously printable values in a string, for
|
||||
* now: integers. This mode is meant as a stopgap measure until we get better formatting tools.
|
||||
* - ToString: Additionally allow coercion of integers, booleans, null,
|
||||
* and lists to strings.
|
||||
*/
|
||||
enum class StringCoercionMode {
|
||||
Strict,
|
||||
Interpolation,
|
||||
ToString,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: coerce-integers
|
||||
internalName: CoerceIntegers
|
||||
---
|
||||
Automatically coerces integer values used in string interpolation to strings. This feature allows constructs like:
|
||||
|
||||
```nix
|
||||
let
|
||||
version = 3;
|
||||
in
|
||||
"v${version}"
|
||||
```
|
||||
|
||||
to evaluate to:
|
||||
|
||||
```nix
|
||||
"v3"
|
||||
```
|
||||
|
||||
instead of producing a type error due to mismatched types in interpolation and hence requiring a call to `builtins.toString`.
|
||||
@@ -144,6 +144,7 @@ experimental_feature_definitions = files(
|
||||
'experimental-features/auto-allocate-uids.md',
|
||||
'experimental-features/ca-derivations.md',
|
||||
'experimental-features/cgroups.md',
|
||||
'experimental-features/coerce-integers.md',
|
||||
'experimental-features/daemon-trust-override.md',
|
||||
'experimental-features/fetch-closure.md',
|
||||
'experimental-features/flakes.md',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
error:
|
||||
… while evaluating x
|
||||
at /pwd/lang/eval-fail-accidental-coerce-integer-attempts.nix:1:21:
|
||||
1| let x = "" + 42; in x == "42"
|
||||
| ^
|
||||
2|
|
||||
|
||||
… while evaluating a path segment
|
||||
at /pwd/lang/eval-fail-accidental-coerce-integer-attempts.nix:1:14:
|
||||
1| let x = "" + 42; in x == "42"
|
||||
| ^
|
||||
2|
|
||||
|
||||
error: cannot coerce an integer to a string: 42
|
||||
@@ -0,0 +1 @@
|
||||
--extra-experimental-features coerce-integers
|
||||
@@ -0,0 +1 @@
|
||||
let x = "" + 42; in x == "42"
|
||||
@@ -0,0 +1,8 @@
|
||||
error:
|
||||
… while evaluating a path segment
|
||||
at /pwd/lang/eval-fail-coerce-integers-without-xp.nix:1:2:
|
||||
1| "${42}" == 42
|
||||
| ^
|
||||
2|
|
||||
|
||||
error: cannot coerce an integer to a string: 42
|
||||
@@ -0,0 +1 @@
|
||||
"${42}" == 42
|
||||
@@ -0,0 +1,10 @@
|
||||
error:
|
||||
… while calling the 'substring' builtin
|
||||
at /pwd/lang/eval-fail-over-coercive-builtins-with-xp-feature.nix:1:1:
|
||||
1| builtins.substring 0 1 42 == "4"
|
||||
| ^
|
||||
2|
|
||||
|
||||
… while evaluating the third argument (the string) passed to builtins.substring
|
||||
|
||||
error: cannot coerce an integer to a string: 42
|
||||
@@ -0,0 +1 @@
|
||||
--extra-experimental-features coerce-integers
|
||||
@@ -0,0 +1 @@
|
||||
builtins.substring 0 1 42 == "4"
|
||||
@@ -0,0 +1 @@
|
||||
true
|
||||
@@ -0,0 +1 @@
|
||||
--extra-experimental-features coerce-integers
|
||||
@@ -0,0 +1 @@
|
||||
"${42}" == "42"
|
||||
Reference in New Issue
Block a user