When traversing the case for the reverse arrow, if there are
no changes, the case-expr are not re-executed, which meant
variables would not be redefined.
Closes#15402.
Closes#15403.
This was done by moving the rewriting which change evaluation order to the Erlang pass,
otherwise would be hard to rewrite back in the type checker reports.
Closes#15310.
Add the two important optimization paths for negative-map projection: value-side projection for map fetch/get and shape-side projection for map put/update.
Keep regression coverage for projected negative maps.
The previous implementation noted the trade-off:
```elixir
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
```
It computed `m * power_of_5(count)` where `count` could grow to ~104, producing 250-bit bignums for a single division. The cost grew with both float magnitude and target precision.
### Benchmark results
11 different float workloads, measured with Benchee on Apple M2. Reported times are the median μs of one iteration over all 11 workloads.
precision | current | cox (this PR) | native_double | cox vs current | native_double vs current | native_double vs cox |
|---|---|---|---|---|---|---|
| 0 | 0.25 μs | 0.25 μs | 0.25 μs | 1.1× | 1.1× | ~tied |
| 1 | 2.42 μs | 0.33 μs | 0.33 μs | **7.1×** | 7.0× | ~tied |
| 2 | 2.67 μs | 0.63 μs | 0.33 μs | **3.8×** | 7.7× | 2.0× |
| 3 | 4.29 μs | 0.63 μs | 0.33 μs | **5.8×** | 11.9× | 2.1× |
| 5 | 5.50 μs | 0.67 μs | 0.33 μs | **6.8×** | 15.6× | 2.3× |
| 8 | 7.21 μs | 0.96 μs | 0.38 μs | **7.5×** | 19.5× | 2.6× |
| 12 | 7.75 μs | 0.96 μs | 0.38 μs | **8.2×** | 20.3× | 2.5× |
| 15 | 8.58 μs | 1.58 μs | 0.38 μs | **5.3×** | 22.8× | 4.3× |
Median μs per iteration over the 11-float mixed workload (per-call median ≈ value / 11).
- **`current`** is the bignum-based implementation we're replacing
- **`cox`** is this PR
- **`native_double`** is included as a "what does this look like in pure native float arithmetic" reference (`:erlang.round(f * pow) / pow`) - it's 2-4× faster than Cox but incorrect on tie inputs because `5.5675 * 1000` rounds to `5567.5` in IEEE due to double-rounding (multiplication round + integer round), then bumps to `5568`. It's why we can't simply use the obvious approach. Cox preserves exact semantics by working on the float's true binary representation.
#### Floats benchmarked
```
floats = [
{"small_pi", 3.141592653589793}, # typical small value
{"common_money", 5.5675}, # tie-boundary case
{"large_e10", 1.2345e10}, # large magnitude
{"tiny_e_neg8", 1.2345e-8}, # very small magnitude
{"near_zero", 0.00001}, # close to zero
{"negative", -123.456789}, # negative sign
{"integer_valued", 12.0}, # exactly representable integer
{"tie_half", 12.5}, # half-tie
{"tie_5675", 5.5675}, # documented tie example
{"large_e30", 1.234e30}, # very large
{"max_finite_ish", 1.7e308} # near max double
]
```
Previously `empty_list()` and `empty_map` were returned for `length(x) != n` and `map_size(x) != n` when n > 0. Now the types are widened to `list()` and `open_map()`
Previously if path parameter was passed in as chardata it was returned as is on `:file.get_cwd` failure
All `relative_to` and `join` code paths normalize to binary