Skip to content

Commit

Permalink
Implement chilled strings
Browse files Browse the repository at this point in the history
[Feature #20205]

As a path toward enabling frozen string literals by default in the future,
this commit introduce "chilled strings". From a user perspective chilled
strings pretend to be frozen, but on the first attempt to mutate them,
they lose their frozen status and emit a warning rather than to raise a
`FrozenError`.

Implementation wise, `rb_compile_option_struct.frozen_string_literal` is
no longer a boolean but a tri-state of `enabled/disabled/unset`.

When code is compiled with frozen string literals neither explictly enabled
or disabled, string literals are compiled with a new `putchilledstring`
instruction. This instruction is identical to `putstring` except it marks
the String with the `STR_CHILLED (FL_USER3)` and `FL_FREEZE` flags.

Chilled strings have the `FL_FREEZE` flag as to minimize the need to check
for chilled strings across the codebase, and to improve compatibility with
C extensions.

Notes:
  - `String#freeze`: clears the chilled flag.
  - `String#-@`: acts as if the string was mutable.
  - `String#+@`: acts as if the string was mutable.
  - `String#clone`: copies the chilled flag.

Co-authored-by: Jean Boussier <byroot@ruby-lang.org>
  • Loading branch information
etiennebarrie and byroot committed Mar 19, 2024
1 parent 86b1531 commit 12be40a
Show file tree
Hide file tree
Showing 36 changed files with 714 additions and 282 deletions.
6 changes: 6 additions & 0 deletions NEWS.md
Expand Up @@ -7,6 +7,12 @@ Note that each entry is kept to a minimum, see links for details.

## Language changes

* String literals in files without a `frozen_string_literal` comment now behave
as if they were frozen. If they are mutated a deprecation warning is emited.
These warnings can be enabled with `-W:deprecated` or by setting `Warning[:deprecated] = true`.
To disable this change you can run Ruby with the `--disable-frozen-string-literal` command line
argument. [Feature #20205]

* `it` is added to reference a block parameter. [[Feature #18980]]

* Keyword splatting `nil` when calling methods is now supported.
Expand Down
11 changes: 11 additions & 0 deletions bootstraptest/test_ractor.rb
Expand Up @@ -1792,3 +1792,14 @@ class C8; def self.foo = 17; end
}

end # if !ENV['GITHUB_WORKFLOW']

# Chilled strings are not shareable
assert_equal 'false', %q{
Ractor.shareable?("chilled")
}

# Chilled strings can be made shareable
assert_equal 'true', %q{
shareable = Ractor.make_shareable("chilled")
shareable == "chilled" && Ractor.shareable?(shareable)
}
31 changes: 31 additions & 0 deletions bootstraptest/test_yjit.rb
Expand Up @@ -4679,6 +4679,37 @@ def test(klass, args)
test(KwInit, [Hash.ruby2_keywords_hash({1 => 1})])
}

# Chilled string setivar trigger warning
assert_equal 'literal string will be frozen in the future', %q{
Warning[:deprecated] = true
$VERBOSE = true
$warning = "no-warning"
module ::Warning
def self.warn(message)
$warning = message.split("warning: ").last.strip
end
end
class String
def setivar!
@ivar = 42
end
end
def setivar!(str)
str.setivar!
end
10.times { setivar!("mutable".dup) }
10.times do
setivar!("frozen".freeze)
rescue FrozenError
end
setivar!("chilled") # Emit warning
$warning
}

# arity=-2 cfuncs
assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{
def test_cases(file, chain)
Expand Down
5 changes: 4 additions & 1 deletion class.c
Expand Up @@ -2244,7 +2244,10 @@ singleton_class_of(VALUE obj)
return klass;

case T_STRING:
if (FL_TEST_RAW(obj, RSTRING_FSTR)) {
if (CHILLED_STRING_P(obj)) {
CHILLED_STRING_MUTATED(obj);
}
else if (FL_TEST_RAW(obj, RSTRING_FSTR)) {
rb_raise(rb_eTypeError, "can't define singleton");
}
}
Expand Down
17 changes: 13 additions & 4 deletions compile.c
Expand Up @@ -4723,7 +4723,7 @@ frozen_string_literal_p(const rb_iseq_t *iseq)
return ISEQ_COMPILE_DATA(iseq)->option->frozen_string_literal > 0;
}

static inline int
static inline bool
static_literal_node_p(const NODE *node, const rb_iseq_t *iseq, bool hash_key)
{
switch (nd_type(node)) {
Expand Down Expand Up @@ -10365,12 +10365,18 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
debugp_param("nd_lit", get_string_value(node));
if (!popped) {
VALUE lit = get_string_value(node);
if (!frozen_string_literal_p(iseq)) {
switch (ISEQ_COMPILE_DATA(iseq)->option->frozen_string_literal) {
case ISEQ_FROZEN_STRING_LITERAL_UNSET:
lit = rb_fstring(lit);
ADD_INSN1(ret, node, putchilledstring, lit);
RB_OBJ_WRITTEN(iseq, Qundef, lit);
break;
case ISEQ_FROZEN_STRING_LITERAL_DISABLED:
lit = rb_fstring(lit);
ADD_INSN1(ret, node, putstring, lit);
RB_OBJ_WRITTEN(iseq, Qundef, lit);
}
else {
break;
case ISEQ_FROZEN_STRING_LITERAL_ENABLED:
if (ISEQ_COMPILE_DATA(iseq)->option->debug_frozen_string_literal || RTEST(ruby_debug)) {
VALUE debug_info = rb_ary_new_from_args(2, rb_iseq_path(iseq), INT2FIX(line));
lit = rb_str_dup(lit);
Expand All @@ -10382,6 +10388,9 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
}
ADD_INSN1(ret, node, putobject, lit);
RB_OBJ_WRITTEN(iseq, Qundef, lit);
break;
default:
rb_bug("invalid frozen_string_literal");
}
}
break;
Expand Down
6 changes: 6 additions & 0 deletions error.c
Expand Up @@ -3860,6 +3860,12 @@ void
rb_error_frozen_object(VALUE frozen_obj)
{
rb_yjit_lazy_push_frame(GET_EC()->cfp->pc);

if (CHILLED_STRING_P(frozen_obj)) {
CHILLED_STRING_MUTATED(frozen_obj);
return;
}

VALUE debug_info;
const ID created_info = id_debug_created_info;
VALUE mesg = rb_sprintf("can't modify frozen %"PRIsVALUE": ",
Expand Down
2 changes: 2 additions & 0 deletions ext/objspace/objspace_dump.c
Expand Up @@ -476,6 +476,8 @@ dump_object(VALUE obj, struct dump_config *dc)
dump_append(dc, ", \"embedded\":true");
if (FL_TEST(obj, RSTRING_FSTR))
dump_append(dc, ", \"fstring\":true");
if (CHILLED_STRING_P(obj))
dump_append(dc, ", \"chilled\":true");
if (STR_SHARED_P(obj))
dump_append(dc, ", \"shared\":true");
else
Expand Down
12 changes: 8 additions & 4 deletions gems/bundled_gems
Expand Up @@ -5,8 +5,12 @@
# - repository-url: URL from where clone for test
# - revision: revision in repository-url to test
# if `revision` is not given, "v"+`version` or `version` will be used.
minitest 5.22.3 https://github.com/minitest/minitest
power_assert 2.0.3 https://github.com/ruby/power_assert

# Waiting for https://github.com/minitest/minitest/pull/991
minitest 5.22.3 https://github.com/Shopify/minitest b5f5202575894796e00109a8f8a5041b778991ee

# Waiting for https://github.com/ruby/power_assert/pull/48
power_assert 2.0.3 https://github.com/ruby/power_assert 78dd2ab3ccd93796d83c0b35b978c39bfabb938c
rake 13.1.0 https://github.com/ruby/rake
test-unit 3.6.2 https://github.com/test-unit/test-unit
rexml 3.2.6 https://github.com/ruby/rexml
Expand All @@ -17,8 +21,8 @@ net-pop 0.1.2 https://github.com/ruby/net-pop
net-smtp 0.4.0.1 https://github.com/ruby/net-smtp
matrix 0.4.2 https://github.com/ruby/matrix
prime 0.1.2 https://github.com/ruby/prime
rbs 3.4.4 https://github.com/ruby/rbs 61b412bc7ba00519e7d6d08450bd384990d94ea2
typeprof 0.21.11 https://github.com/ruby/typeprof
rbs 3.4.4 https://github.com/ruby/rbs ba7872795d5de04adb8ff500c0e6afdc81a041dd
typeprof 0.21.11 https://github.com/ruby/typeprof b19a6416da3a05d57fadd6ffdadb382b6d236ca5
debug 1.9.1 https://github.com/ruby/debug 2d602636d99114d55a32fedd652c9c704446a749
racc 1.7.3 https://github.com/ruby/racc
mutex_m 0.2.0 https://github.com/ruby/mutex_m
Expand Down
3 changes: 3 additions & 0 deletions include/ruby/internal/fl_type.h
Expand Up @@ -916,6 +916,9 @@ static inline void
RB_OBJ_FREEZE_RAW(VALUE obj)
{
RB_FL_SET_RAW(obj, RUBY_FL_FREEZE);
if (TYPE(obj) == T_STRING) {
RB_FL_UNSET_RAW(obj, FL_USER3); // STR_CHILLED
}
}

RUBY_SYMBOL_EXPORT_BEGIN
Expand Down
1 change: 0 additions & 1 deletion include/ruby/internal/intern/error.h
Expand Up @@ -190,7 +190,6 @@ RBIMPL_ATTR_NONNULL(())
*/
void rb_error_frozen(const char *what);

RBIMPL_ATTR_NORETURN()
/**
* Identical to rb_error_frozen(), except it takes arbitrary Ruby object
* instead of C's string.
Expand Down
12 changes: 11 additions & 1 deletion insns.def
Expand Up @@ -375,7 +375,17 @@ putstring
()
(VALUE val)
{
val = rb_ec_str_resurrect(ec, str);
val = rb_ec_str_resurrect(ec, str, false);
}

/* put chilled string val. string will be copied but frozen in the future. */
DEFINE_INSN
putchilledstring
(VALUE str)
()
(VALUE val)
{
val = rb_ec_str_resurrect(ec, str, true);
}

/* put concatenate strings */
Expand Down
23 changes: 22 additions & 1 deletion internal/string.h
Expand Up @@ -17,6 +17,7 @@

#define STR_NOEMBED FL_USER1
#define STR_SHARED FL_USER2 /* = ELTS_SHARED */
#define STR_CHILLED FL_USER3

#ifdef rb_fstring_cstr
# undef rb_fstring_cstr
Expand Down Expand Up @@ -77,7 +78,7 @@ VALUE rb_id_quote_unprintable(ID);
VALUE rb_sym_proc_call(ID mid, int argc, const VALUE *argv, int kw_splat, VALUE passed_proc);

struct rb_execution_context_struct;
VALUE rb_ec_str_resurrect(struct rb_execution_context_struct *ec, VALUE str);
VALUE rb_ec_str_resurrect(struct rb_execution_context_struct *ec, VALUE str, bool chilled);

#define rb_fstring_lit(str) rb_fstring_new((str), rb_strlen_lit(str))
#define rb_fstring_literal(str) rb_fstring_lit(str)
Expand Down Expand Up @@ -108,6 +109,26 @@ STR_SHARED_P(VALUE str)
return FL_ALL_RAW(str, STR_NOEMBED | STR_SHARED);
}

static inline bool
CHILLED_STRING_P(VALUE obj)
{
return RB_TYPE_P(obj, T_STRING) && FL_TEST_RAW(obj, STR_CHILLED);
}

static inline void
CHILLED_STRING_MUTATED(VALUE str)
{
rb_category_warn(RB_WARN_CATEGORY_DEPRECATED, "literal string will be frozen in the future");
FL_UNSET_RAW(str, STR_CHILLED | FL_FREEZE);
}

static inline void
STR_CHILL_RAW(VALUE str)
{
// Chilled strings are always also frozen
FL_SET_RAW(str, STR_CHILLED | RUBY_FL_FREEZE);
}

static inline bool
is_ascii_string(VALUE str)
{
Expand Down
31 changes: 18 additions & 13 deletions iseq.c
Expand Up @@ -720,18 +720,20 @@ finish_iseq_build(rb_iseq_t *iseq)
}

static rb_compile_option_t COMPILE_OPTION_DEFAULT = {
OPT_INLINE_CONST_CACHE, /* int inline_const_cache; */
OPT_PEEPHOLE_OPTIMIZATION, /* int peephole_optimization; */
OPT_TAILCALL_OPTIMIZATION, /* int tailcall_optimization */
OPT_SPECIALISED_INSTRUCTION, /* int specialized_instruction; */
OPT_OPERANDS_UNIFICATION, /* int operands_unification; */
OPT_INSTRUCTIONS_UNIFICATION, /* int instructions_unification; */
OPT_FROZEN_STRING_LITERAL,
OPT_DEBUG_FROZEN_STRING_LITERAL,
TRUE, /* coverage_enabled */
.inline_const_cache = OPT_INLINE_CONST_CACHE,
.peephole_optimization = OPT_PEEPHOLE_OPTIMIZATION,
.tailcall_optimization = OPT_TAILCALL_OPTIMIZATION,
.specialized_instruction = OPT_SPECIALISED_INSTRUCTION,
.operands_unification = OPT_OPERANDS_UNIFICATION,
.instructions_unification = OPT_INSTRUCTIONS_UNIFICATION,
.frozen_string_literal = OPT_FROZEN_STRING_LITERAL,
.debug_frozen_string_literal = OPT_DEBUG_FROZEN_STRING_LITERAL,
.coverage_enabled = TRUE,
};

static const rb_compile_option_t COMPILE_OPTION_FALSE = {0};
static const rb_compile_option_t COMPILE_OPTION_FALSE = {
.frozen_string_literal = -1, // unspecified
};

int
rb_iseq_opt_frozen_string_literal(void)
Expand Down Expand Up @@ -770,9 +772,11 @@ set_compile_option_from_ast(rb_compile_option_t *option, const rb_ast_body_t *as
{
#define SET_COMPILE_OPTION(o, a, mem) \
((a)->mem < 0 ? 0 : ((o)->mem = (a)->mem > 0))
SET_COMPILE_OPTION(option, ast, frozen_string_literal);
SET_COMPILE_OPTION(option, ast, coverage_enabled);
#undef SET_COMPILE_OPTION
if (ast->frozen_string_literal >= 0) {
option->frozen_string_literal = ast->frozen_string_literal;
}
return option;
}

Expand Down Expand Up @@ -814,13 +818,14 @@ make_compile_option_value(rb_compile_option_t *option)
SET_COMPILE_OPTION(option, opt, specialized_instruction);
SET_COMPILE_OPTION(option, opt, operands_unification);
SET_COMPILE_OPTION(option, opt, instructions_unification);
SET_COMPILE_OPTION(option, opt, frozen_string_literal);
SET_COMPILE_OPTION(option, opt, debug_frozen_string_literal);
SET_COMPILE_OPTION(option, opt, coverage_enabled);
SET_COMPILE_OPTION_NUM(option, opt, debug_level);
}
#undef SET_COMPILE_OPTION
#undef SET_COMPILE_OPTION_NUM
VALUE frozen_string_literal = option->frozen_string_literal == -1 ? Qnil : RBOOL(option->frozen_string_literal);
rb_hash_aset(opt, ID2SYM(rb_intern("frozen_string_literal")), frozen_string_literal);
return opt;
}

Expand Down Expand Up @@ -1248,7 +1253,7 @@ pm_iseq_compile_with_option(VALUE src, VALUE file, VALUE realpath, VALUE line, V
pm_parse_result_t result = { 0 };
pm_options_line_set(&result.options, NUM2INT(line));

pm_options_frozen_string_literal_set(&result.options, option.frozen_string_literal);
pm_options_frozen_string_literal_init(&result, option.frozen_string_literal);

VALUE error;
if (RB_TYPE_P(src, T_FILE)) {
Expand Down
6 changes: 5 additions & 1 deletion iseq.h
Expand Up @@ -47,6 +47,10 @@ extern const ID rb_iseq_shared_exc_local_tbl[];

#define ISEQ_FLIP_CNT(iseq) ISEQ_BODY(iseq)->variable.flip_count

#define ISEQ_FROZEN_STRING_LITERAL_ENABLED 1
#define ISEQ_FROZEN_STRING_LITERAL_DISABLED 0
#define ISEQ_FROZEN_STRING_LITERAL_UNSET -1

static inline rb_snum_t
ISEQ_FLIP_CNT_INCREMENT(const rb_iseq_t *iseq)
{
Expand Down Expand Up @@ -227,7 +231,7 @@ struct rb_compile_option_struct {
unsigned int specialized_instruction: 1;
unsigned int operands_unification: 1;
unsigned int instructions_unification: 1;
unsigned int frozen_string_literal: 1;
signed int frozen_string_literal: 2; /* -1: not specified, 0: false, 1: true */
unsigned int debug_frozen_string_literal: 1;
unsigned int coverage_enabled: 1;
int debug_level;
Expand Down
22 changes: 22 additions & 0 deletions lib/ruby_vm/rjit/insn_compiler.rb
Expand Up @@ -57,6 +57,7 @@ def compile(jit, ctx, asm, insn)
when :putobject then putobject(jit, ctx, asm)
when :putspecialobject then putspecialobject(jit, ctx, asm)
when :putstring then putstring(jit, ctx, asm)
when :putchilledstring then putchilledstring(jit, ctx, asm)
when :concatstrings then concatstrings(jit, ctx, asm)
when :anytostring then anytostring(jit, ctx, asm)
when :toregexp then toregexp(jit, ctx, asm)
Expand Down Expand Up @@ -776,6 +777,27 @@ def putstring(jit, ctx, asm)

asm.mov(C_ARGS[0], EC)
asm.mov(C_ARGS[1], to_value(put_val))
asm.mov(C_ARGS[2], 0)
asm.call(C.rb_ec_str_resurrect)

stack_top = ctx.stack_push(Type::TString)
asm.mov(stack_top, C_RET)

KeepCompiling
end

# @param jit [RubyVM::RJIT::JITState]
# @param ctx [RubyVM::RJIT::Context]
# @param asm [RubyVM::RJIT::Assembler]
def putchilledstring(jit, ctx, asm)
put_val = jit.operand(0, ruby: true)

# Save the PC and SP because the callee will allocate
jit_prepare_routine_call(jit, ctx, asm)

asm.mov(C_ARGS[0], EC)
asm.mov(C_ARGS[1], to_value(put_val))
asm.mov(C_ARGS[2], 1)
asm.call(C.rb_ec_str_resurrect)

stack_top = ctx.stack_push(Type::TString)
Expand Down
20 changes: 10 additions & 10 deletions mini_builtin.c
Expand Up @@ -28,16 +28,16 @@ builtin_iseq_load(const char *feature_name, const struct rb_builtin_function *ta
}
vm->builtin_function_table = table;
static const rb_compile_option_t optimization = {
TRUE, /* unsigned int inline_const_cache; */
TRUE, /* unsigned int peephole_optimization; */
FALSE,/* unsigned int tailcall_optimization; */
TRUE, /* unsigned int specialized_instruction; */
TRUE, /* unsigned int operands_unification; */
TRUE, /* unsigned int instructions_unification; */
TRUE, /* unsigned int frozen_string_literal; */
FALSE, /* unsigned int debug_frozen_string_literal; */
FALSE, /* unsigned int coverage_enabled; */
0, /* int debug_level; */
.inline_const_cache = TRUE,
.peephole_optimization = TRUE,
.tailcall_optimization = FALSE,
.specialized_instruction = TRUE,
.operands_unification = TRUE,
.instructions_unification = TRUE,
.frozen_string_literal = TRUE,
.debug_frozen_string_literal = FALSE,
.coverage_enabled = FALSE,
.debug_level = 0,
};
const rb_iseq_t *iseq = rb_iseq_new_with_opt(&ast->body, name_str, name_str, Qnil, 0, NULL, 0, ISEQ_TYPE_TOP, &optimization);
GET_VM()->builtin_function_table = NULL;
Expand Down

0 comments on commit 12be40a

Please sign in to comment.