In Ruby 4.0, what is a chilled string literal, and why does mutating one usually run with no visible warning?
answer
- file without the magic comment
- frozen? still returns false
- deprecated category, off by default
- "literal string will be frozen in the future"
- added in 3.4, kept in 4.0
basics
~20 sA chilled string is a literal from a file with no frozen_string_literal comment. It reports frozen? as false and mutates normally; the first mutation emits a deprecation warning, which is invisible unless deprecation warnings are enabled.
solid answer
~50 sSince Ruby 3.4, and still in 4.0, a string literal in a file **without** any `frozen_string_literal` comment is **chilled**: marked as "will be frozen in a future version". It is not frozen: `frozen?` returns `false`, `<<` and `upcase!` work, and identical chilled literals are still separate objects. The **first** mutation of each chilled string emits `warning: literal string will be frozen in the future`, but in the `deprecated` warning category, which is **disabled by default**; you see it only with `-W:deprecated`, `-w` or `Warning[:deprecated] = true`. After that first mutation the string is an ordinary mutable string. Ruby 4.0 did **not** freeze literals by default, so chilled strings are a migration signal: turn deprecation warnings on in the test suite, then fix each reported site with `+"..."` or opt the file in with the magic comment.
code
ruby · 10 lines# no frozen_string_literal comment in this file
Warning[:deprecated] = true
body = "ALERT"
body.frozen? # => false (chilled, not frozen)
body << "!" # warning: literal string will be frozen in the future
body << "!" # silent: now an ordinary string
safe = +"ALERT" # a mutable copy of the chilled literal
safe << "!" # no warninggo deeper
Recall that literals in a file with no magic comment still mutate normally in Ruby 4.0, and that +"text" gives a safe mutable copy.
Explain the three states a literal can be in and why the chilled warning is silent until the deprecated warning category is enabled.
Lay out the migration: enable deprecation warnings in the suite, fix each reported mutation with +"" or the magic comment, and keep CI reporting new ones.
Decide how far ahead of the language default to move: freezing every file now versus fixing warnings as they appear, weighed against library compatibility.
## Three states of a literal In Ruby 4.0 a string literal ends up in one of three states, decided by the file's `frozen_string_literal` comment: | File header | State of `"text"` | `frozen?` | Mutation | |---|---|---|---| | `# frozen_string_literal: true` | frozen, deduplicated | `true` | raises `FrozenError` | | `# frozen_string_literal: false` | ordinary mutable string | `false` | silent | | no comment | **chilled** | `false` | works; first mutation may warn | **Chilled** is the transition state introduced in Ruby 3.4 (Feature #20205). Ruby's source describes it as a literal "allocated as a literal in a file without an explicit `frozen_string_literal` comment" that "emits a deprecation warning when mutated for the first time". Ruby 4.0's release notes list no change here: literals were **not** frozen by default in 4.0. ## What a chilled string does - `frozen?` returns `false`. Code that checks `frozen?` before mutating sees a mutable string. - Mutation succeeds. `"body" << "!"` returns `"body!"`. - The **first** mutation of each chilled object calls the warning path, then the object becomes an ordinary string, so later mutations of the same object stay silent. - Identical chilled literals are **separate objects**, as before: `"a".equal?("a")` is `false`. Chilling changes no identity or allocation behaviour. - The string returned by `Symbol#to_s` is chilled the same way; mutating it warns with `string returned by :name.to_s will be frozen in the future`. ## Why you usually see nothing The warning belongs to Ruby's `deprecated` **warning category**, and that category is disabled by default. So on a default run the mutation is completely silent. The warning appears only when deprecation warnings are switched on, for example: 1. the `-W:deprecated` command-line switch, or `-w`, which enables the default categories including deprecation; 2. `Warning[:deprecated] = true` early in the program, such as in a test helper. Once enabled, the output looks like: ``` app/sms.rb:12: warning: literal string will be frozen in the future ``` ## How chilled strings meet other methods A few operations change or copy the chilled state, as the Ruby 4.0 test suite pins down: - `freeze` on a chilled string clears the chilled mark and freezes it for real; later mutation raises `FrozenError`. - `dup` and `clone` of a chilled string return strings whose `frozen?` is `false`. - `+str` (`String#+@`) returns a `dup` of a chilled string, so the literal itself is never touched. - `-str` (`String#-@`) returns a frozen deduplicated copy, not the chilled object. - A substring taken with `str[0..-1]` is independent: replacing the original's contents afterwards does not change it. These rules matter when a check such as `str.frozen? ? str.dup : str` is used as a guard: for a chilled string it passes the literal through unchanged, and the later mutation warns. ## Using it as a migration tool Chilled strings exist so a codebase can find its literal mutations **before** a future Ruby freezes literals. A practical plan: 1. Enable deprecation warnings in the test run and in a staging process. 2. Collect the reported file and line numbers. 3. Fix each site: - a buffer: start from `+""` instead of `""`; - a literal passed to a method that mutates it: pass `+"text"` or change the method to a non-bang form; - a whole file that is clean: add `# frozen_string_literal: true` so it is frozen now. 4. Keep warnings on in CI so new mutations are reported as they are written. Unary plus is the purpose-built fix: `String#+@` returns the receiver when it is mutable without warning, and a copy (`dup`) when it is frozen **or chilled**, so `+"body"` never warns. ## Common misreadings - "Ruby 4.0 freezes literals." It does not; it keeps them chilled. - "Chilled strings raise on mutation." They do not; only frozen ones raise `FrozenError`. - "`frozen?` tells you a literal is chilled." It returns `false` for chilled strings, exactly as for mutable ones. - "`# frozen_string_literal: false` still gives warnings." An explicit `false` produces ordinary strings, which never warn.
- How does +"body" avoid the chilled-string warning?`String#+@` returns the receiver only when it is not frozen and can be mutated without a warning. For a chilled or frozen string it returns `dup`, an ordinary mutable copy, so later mutation never touches the chilled literal.
- Does # frozen_string_literal: false produce chilled strings in Ruby 4.0?No. Chilling applies only to literals in files with no `frozen_string_literal` comment at all. An explicit `false` produces ordinary mutable literals that never warn.
- Why would a team turn deprecation warnings on in CI but not in production?CI runs the whole suite and can fail or report on new warnings cheaply, which finds literal mutations early. In production the warnings add log noise and cost for sites already known, so they are usually left off there.
saying these in an interview costs you the question
- Ruby 4.0 freezes string literals by default
- Mutating a chilled string raises FrozenError
- A chilled string reports frozen? as true
- The chilled warning prints on every mutation, even with default warning settings
- An explicit frozen_string_literal: false comment still produces chilled literals