Skip to content

Frozen proposal #1493

Open
Open
@zapashcanon

Description

@zapashcanon

Hi,

@chambart and myself have a proposal for introducing freezable GC/global values. As asked by @dtig, we present the idea here and hopefully there will be some time available at one of the CG meeting to discuss it further.

Building recursive values

Let's say you have a recursive type $t, currently you can build a value of type $t like this:

(module
  (rec
    (type $t (struct (field $f (mut (ref null $t))))))

  (func $loop (result (ref $t))
    (local $l (ref $t))
    (local.set $l (struct.new $t (ref.null $t)))
    (struct.set $t $f (local.get $l) (local.get $l))
    (local.get $l)
  )
)

Building immutable recursive values ?

Now, let us define a type $t' which is the same as $t but with an immutable field $f:

(module
  (rec
    (type $t  (struct (field $f (mut (ref null $t))))))

  (rec
    (type $t' (struct (field $f      (ref      $t')))))

  (func $loop (result (ref $t)) ... )
)

Currently, there's no way to build a value of type $t'.

Freezing

Our proposal allows to build immutable recursive values in the following way:

(module
  (rec
    (type $t         freezable  (struct (field $f (mut (ref null $t))))))

  (rec
    (type $t_freeze (freeze $t) (struct (field $f      (ref      $t_freeze)))))

  (func $loop_tmp (result (ref $t)) ... )

  (func $loop (result (ref $t_freeze))
    (ref.freeze $t_freeze $t (call $loop_tmp))
  )
)

The point ?

The same as immutable values in general:

  • cleaner API / preserve code invariants ;
  • avoid read barriers ;
  • being more explicit about the use.

Could also avoid null checks and allow creating immutable arrays.

The idea

The freezability check can be similar to the sub-typing rules:

  • there should be the same (or less ?) fields ;
  • can remove mut annotations ;
  • can remove null annotations ;
  • fields should be (frozen versions of) subtypes (maybe also upcasts ?).

After the freeze, the freezable values should not be accessed:

  • dynamic checks on freezable types accesses ;
  • the freeze operation is expected to walk the value and flip a frozen bit ;
  • trap if fields are still null at freeze time.

Heuristic: unfrozen values are seldom accessed, frozen ones can be accessed a lot

Freezing is not 'fixed number of hardware instruction', but the combined time of building then freezing is kind of an amortized version of it.

Globals

Similarly, a global needs to be nullable in some cases:

(module
  (rec (type $t (freeze $t) (struct (field $f (ref $t))))))
  (global $g (mut (ref null $t)) (ref.null $t))

  (func $f (result (ref $t))
    (ref.as_non_null (global.get $g))
  )
  (func $loop (result (ref $t)) ...)

  (func $st
    (global.set $g (call $loop))
    (drop (call $f))
  )

  (start $st))

Freezing globals

Here is what our proposal would allow:

(module
  (global $g (mut (ref null $t)) (ref.null $t))
  (global $g_frozen (ref $t) (freeze $g))
  (func $f (result (ref $t))
    (global.get $g_frozen) ;; <-- no test
  )

  (func $st
    (global.set $g (call $loop))
    (drop (call $f))
  )
  (start $st))

But when are the global actually frozen ? Well...

Phases

We introduce phases:

(module
  (global $g (mut (ref null $t)) freezable (ref.null $t))
  (global $g_frozen (ref $t) (freeze $g) (phase 1))

  (func $f (phase 1) (result (ref $t))
    (global.get $g_frozen)
  )
  (func $loop (phase 0) (result (ref $t)) ...)

  (func $st_0 (phase 0) (global.set $g (call $loop)))
  (func $st_1 (phase 1) (drop (call $f)))
  (start $st_0 (phase 0))
  (start $st_1 (phase 1))
)

The idea:

  • invariant: cannot call a function of phase $n$ before the end of the start of phase $n-1$ ;
  • a function can only refer to values of phase less or equal than its own phase (ref and calls) ;
  • each phase can have one start ;
  • starts are run in phase order ;
  • global are frozen at the end of the previous phase ;
  • failure to freeze is a panic ;
  • accessing an unfrozen global from a previous phase is a panic ;
  • cannot export freezable global.

Also:

  • if multiple start functions seems distasteful, we could have a call_and_freeze instruction moving to the next phase ;
  • default phase is 0 ;
  • global freeze can change the type of its contents ;
  • encoding: not really thought, could be compact.

Who might be interested ?

It would be useful in our OCaml compiler Wasocaml. Maybe @vouillon would also use it in its other OCaml compiler.

Maybe @wingo would use it in its Guile compiler ? At least that what we thought after the presentation he gave at the in-person CG meeting.

Questions

Would you see other use cases for this ?

Should there be a phase 1 proposal to explore that kind of needs ?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions