Project

General

Profile

Actions

Feature #17397

closed

`shareable_constant_value: literal` should check at runtime, not at parse time

Added by marcandre (Marc-Andre Lafortune) 12 months ago. Updated 12 months ago.

Status:
Closed
Priority:
Normal
Target version:
-
[ruby-core:101472]

Description

I think shareable_constant_value: literal is too strict because it has too crude checks at parse time.

I wish the following code would parse and run:

# shareable_constant_value: literal

class Foo < RuntimeError
end

# Similar code, but does not parse:
Bar = Class.new(RuntimeError) # => unshareable expression

Baz = Ractor.make_shareable(anything_here) # => unshareable expression

Qux = Set[1, 2, 3].freeze # => unshareable expression

Could we instead raise some sort of RuntimeError when an assignment is made that is not shareable?


Related issues

Related to Ruby master - Feature #17273: shareable_constant_value pragmaClosedActions

Updated by marcandre (Marc-Andre Lafortune) 12 months ago

Ideally, the following would also be accepted:

# shareable_constant_value: literal

Map = {
  int: Integer,
  str: String,
  # ...
}
Map.frozen # => true

Literals that are arguments to method calls should not be frozen:

# shareable_constant_value: literal

def check(arg)
  p arg.frozen?
  arg.freeze
end

X = check([1,2,3]) # => prints false
Y = [1] + [2] # => error
Actions #2

Updated by marcandre (Marc-Andre Lafortune) 12 months ago

  • Subject changed from shareable_literal_constant should check at runtime, not at parse time to `shareable_constant_value: literal` should check at runtime, not at parse time

Updated by marcandre (Marc-Andre Lafortune) 12 months ago

After discussion with ko1, the following options came up:

(1) raises an error for non-literals on parse time (current)
(2) raises an error for unshareable non-literals on runtime (my proposal)
(3) just ignore non-literals
(4) = (3) + (1) with literal-strict
(5) = (3) + (2) with literal-strict

I like all options that do not include (1).

Solution 2 is as I presented. Solution 3 is more permissive. Potential downsides include:

# shareable_constant_value: literal

FOO = [1, 2, 3]
BAR = [4, 5, 6, 7, 8, 9, 10]
BAZ = FOO + BAR  # => not frozen / shareable
QUX = Set[11, 12, 13] # => not frozen / shareable

It would be easy to call ractor.send(BAZ) # or QUX and assume that no copy is being made.

My opinion remains that many rubyists will want all their constants to be immutable, so option (2) would be a good way to do this and enforce it at the same time. I expect that global registry / cache are better implemented as attributes of singleton class.

Actions #4

Updated by Eregon (Benoit Daloze) 12 months ago

Updated by nobu (Nobuyoshi Nakada) 12 months ago

Your proposal seems shareable_constant_value: everything (it's prefixed with experimental_ now though).

Updated by marcandre (Marc-Andre Lafortune) 12 months ago

nobu (Nobuyoshi Nakada) wrote in #note-5:

Your proposal seems shareable_constant_value: everything (it's prefixed with experimental_ now though).

No, that value deep-freezes implicitly all constants. Non-literals are not frozen in my proposal.

Here's a code summary of the differences (scenarios 2, 3 or 5 == 2+3):

# shareable_constant_value: experimental_everything (current, ok)

X = [1, 2, 3]
p X.frozen? # => true
Y = Set[4, 5, 6]
p Y.frozen? # => true

# shareable_constant_value: literal (2) or literal_strict (5)

X = [1, 2, 3]
p X.frozen? # => true
(Y = Set[4, 5, 6]) rescue :error # => :error (at runtime, currently at parse time)
Z = Set[8, 9].freeze # => no error (currently parse error)

# shareable_constant_value: literal (3 or 5)

X = [1, 2, 3]
p X.frozen? # => true
Y = Set[4, 5, 6] # => no error (currently parse error)
p Y.frozen? # => false

Updated by shyouhei (Shyouhei Urabe) 12 months ago

marcandre (Marc-Andre Lafortune) wrote:

I think shareable_constant_value: literal is too strict because it has too crude checks at parse time.

This is true. Current restriction is very conservative not to break things (See #17273 for the background). If we can gain safety and usability at once, I think it's OK to relax. But experimental_everything is too lax (-1 for nobu's comment). There must be a better safety-usability trade-off than that. This pragma must not be an all-or-nothing flag I believe.

Updated by ko1 (Koichi Sasada) 12 months ago

By discussion with Matz and Nobu, we choose "(2) raises an error for unshareable non-literals on runtime (my proposal)".

* on compile time: for `FOO = expr`
    * case `FOO = literal_only`
        * compile to => `FOO = RubyVM::FrozenCore.make_shareable(literal_only)`
    * otherwise
        * compile to => `FOO = RubyVM::FrozenCore.check_shareable(expr)`

RubyVM::FrozenCore.check_shareable(obj) is obj.tap{|o| raise Ractor::Error unless Ractor.shareable?(o)}.

One concern was "literal" doesn't mean anything about checking unshareable feature ("otherwise" behavior).
We challenged to find out good naming, but we determined to choose "literal" as is.

The logic is here:

  • shareable_constant_value: none means there is no checking. Other options force constants are sharable (or an error)
  • shareable_constant_value: literal means we do unshareable checking and make all literals sharable.

We can list the following possible options:

  • (default) none: do nothing
  • (not contained) check: FOO = RubyVM::FrozenCore.check_shareable(expr)
  • literal
    • if expr is literal expr, FOO = RubyVM::FrozenCore.make_shareable(expr)
    • else, FOO = RubyVM::FrozenCore.check_shareable(expr)
  • (not contained) copy: FOO = RubyVM::FrozenCore.make_shareable(expr, copy: true)
  • experimental_everything: FOO = RubyVM::FrozenCore.make_shareable(expr)

I think "copy" here can be introduced as experimental.

Updated by ko1 (Koichi Sasada) 12 months ago

By seeing the list, about experimental_everything, experimental_shareable seems better naming...

Updated by marcandre (Marc-Andre Lafortune) 12 months ago

Great, thank you for the update.

One last concern with experimental_everything/experimental_shareable is for Ruby 3.1... If no longer experimental, we then accept everything. But if someone want to write code also compatible with Ruby 3.0, they still have to use experimental_everything (assuming Ruby 3.1 also allow it). And in Ruby 3.2 same thing.

Only in ~3 years can people start writing everything (assuming they want to be compatible with non-EOL rubies).

Could be use everything (or my favorite: all) and either warn (once) that it is experimental, or document it?

Actions #12

Updated by nobu (Nobuyoshi Nakada) 12 months ago

  • Status changed from Open to Closed

Applied in changeset git|7a094146e6ef38453a7e475450d90a9c83ea2277.


Changed shareable literal semantics [Feature #17397]

When literal, check if the literal about to be assigned to a
constant is ractor-shareable, otherwise raise Ractor::Error at
runtime instead of SyntaxError.

Actions

Also available in: Atom PDF