Repository navigation
Conversation
A map literal with any computed value is not folded into a constant, and
every entry, constant or not, was evaluated each time the literal was.
After optimizing a map literal's entries, the optimizer now moves the
constant values into the literal's template, so only the computed ones
are evaluated. A function pointer stays an entry, as it takes the
environment it is evaluated in, and so does a shared value, which is
flattened as it is evaluated. The promotion of `#{..}.a` and `#{..}["a"]`
looks in the template for a moved value.
The walker and the VM start a map literal's running data size from its
template, so the constant values still count towards the limits.
The parser checks for a duplicated property in the template rather than
scanning the entries.
At OptimizationLevel::None nothing moves, so constant entries still
count as operations there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Putting it inside the optimizer instead of the parser would make the parser create large numbers of constant AST nodes for the first version of the AST, only to throw those away after folding into th template. For thousands of properties this would be a drag. Would it be better to just do it in the parser itself? The optimizer then simply becomes taking the template as the value if there are no non-constant props left. Subtle differences wrt operations counting etc. can be tolerated, I think, for such an optimization. The test results can be updated to reflect the new behavior. |
|
I have created #1207 which implements it in the parser (in which case you don't create and immediately throw away large numbers of constant properties). Some subtleties wrt data size counting flushed out existing issues (aka bugs...) in Rhai. These were actually preventing AI from doing a more thorough job to keep with the incorrect behavior... which is why I mentioned that there are subtle complications. Take that PR as a reference. @luispedro I would be interested to see how my PR benchmarks vs yours (doing it as an optimizer step). |
|
I'm going to merge #1127 as a preferred implementation. Closing this now. |
The second item from #1196: the constant values of a map literal go into its template, so only the computed values are evaluated each time the literal is. This is separate from #1202 (the constant pool index), as suggested there.
Changes
Expr::lift_map_constants). If that leaves no entries, the next pass folds the map into a constant, as before.imports when called after the script has run #1193). A shared value also stays, because it is flattened as it is evaluated.#{..}.aand#{..}["a"]looks in the template for a moved value; otherwise it would give().()for each computed value, so every entry is counted once, as before.DebugforExpr::Mapshows the moved constants.Why the optimizer and not the parser
Moving the constants in the parser breaks
test_max_operations_literal, which requires each constant entry of a map literal to count as an operation underOptimizationLevel::None. Doing it in the optimizer matches how it already folds a map literal that is all constant. UnderOptimizationLevel::None, and inno_optimizebuilds, nothing changes.Behavior changes (when optimizing)
These are the same as for an all-constant map literal today:
internals, the entries of an optimizedExpr::Maphold only the computed values.Benchmarks
Release build, pinned to one core, median of 11 (synthetic) or 21 (luish) runs; each build run twice.
Evaluating a map literal (per evaluation)
let s = 0; for i in 0..N { let m = #{ ... }; s += m.len(); } s, where the computed values readiand the rest are"none".Real scripts
The luish completion scripts from #1196 (luish-extra
completion/), with a stubshmodule:ffmpeg_optsvalues()slurm_optsspec()htssamtools()These gain less than the synthetic case because their computed values are calls such as
k("..."), which this does not touch.On its own, Grain compile time is still dominated by the quadratic constant pool that #1202 fixes. With both PRs together,
ffmpeg_optscompiles in 0.17 ms (57 ms on main), and a 1000-key literal with one computed value in 0.04 ms (86 ms on main).The two PRs conflict only in the comment above
Expr::Mapin the Grain compiler.Tests
New tests, each confirmed to fail without the part of the change it covers:
tests/maps.rs: promoting a property of a map literal whose value was moved into the template.tests/modules.rs: a closure in a map literal keeps the imports of its script.tests/data_size.rs: string and map size limits count the constant values of a map literal.tests/grain/limits.rs: the VM refuses the same literals as the walker, with the same error and position, and agrees on the value under the limits.Checks
cargo test --testspasses with default features,internals,grain,bin-features,no_optimize,unchecked,grain,internals,no_closure,grain,internals,sync,grain,internals, and the two CI feature sets that failed on Grain: index the constant pool by hash #1202.cargo test --lib --features internals,grainpasses.cargo fmt --all -- --checkpasses. Clippy reports nothing on the changed lines.cargo check --libis warning-free across a feature matrix includingno_optimize,no_object,no_index,uncheckedandgraincombinations. Theno_ast,grainbuild has the same two warnings as main.🤖 Generated with Claude Code