Draft of a proposal for an Explicit Ownership Syntax

JS implicit copy or reference variable depend on thair type in function call. This new syntax essentially uniformize argument call behaviour. Moreover, it add an explicit syntax to change scope variable (eg.: for globals variables used by function of if blocks)
It:

  • Prevent hidden behaviour
  • Simplify syntax for varaible copy (structuredClone)
  • Better variable lifetime trace (possible optimisation for implementers)

Syntax

This syntax introduce 2 new keywords

  • move (move permanently a variable from a parent scope to a new child scope)
  • copy (copy a variable from a parent scope to a new child scope)

Referenced variable can't be moved to prevent access to released memory space

If function use a key word in the declaration it is mandatory at call

Notation

Basic usage

{
    //scope A
    const a = /* any */
    const b = /* any */
    
    foo(move a, copy b) {
        //scope B
        a //can be accessed
        b //can be accessed
    }
    
    a //reference error, a is not referenced
    b //can be accessed

    if (move b !== undefined) {
        foo(move b)
        b //reference error, b is not referenced
    }

    b //reference error, b is not referenced

}

Function declaration

function foo(move arg0, copy arg1, arg2) {
    //
}

const a, b, c = [1, 2, 3]

Keyword are mandatory if declared in function arguments

Respecting keywords

foo(move a, copy b, c) //ok

Copy to move "cast"

foo(copy a, copy b, c) //ok, copy encapsulate move for call

Move to copy "cast"

foo(move a, move b, c) //ok, move equivalent to copy for call

To keywordless declaration "cast"

foo(move a, copy b, copy c) //ok
foo(move a, copy b, move c) //ok

No keyword to move "cast"

foo(a, copy b, c) //throws, foo require ownership for arg0

No keyword to copy "cast"

foo(move a, b, c) //throws, foo require a copy for arg1 (ensure non mutation of b)

Rules

copy a is equivalent to structuredClone(a)
move a de-reference a to all the parents scopes

Examples

Copy data

const source = /* any */
const sourceCopy = copy source

If block

let value

if (move value = foo()) {
    //do stuff
}

value //out of scope

Legacy muting methods

const a = [1, 2, 3]

const b = (copy a).reverse()

//a is unchanged

Ressource access prevent

const ressource = /* */

function release(move ressource) {
    ressource.release()
}

release(move ressource)

//ressource not accessible anymore

Proposal: https://github.com/JOTSR/proposal_explicit_ownership_syntax`

I'm not sure what you mean - JS is always strictly pass by value for every type of value, see https://web.archive.org/web/20161005155047/http://www.jon-carlos.com/2013/is-javascript-call-by-value-or-call-by-reference/ for an explanation.

Yes, primitive are passed bt value and object by reference, but since JS is not strictly typed you can pass an object ass argument and the execution will not throws on reference/value choice but on possibly type error. Here is a soluction to enforce a reference passing call (that also works for primitive). Moreover, it suggest a syntax to give non mutable reference, since third party function can mutate object and cause difficult to see behaviours.

No, objects are also only passed by value. "pass by reference", as the article explains, refers to direct assignment (and not property mutation).

1 Like

This sounds fairly Rust inspired. While it would be awesome if we could bring some of these behaviors over to JavaScript, I don't think they translate well, and I don't think there's a practical way to make them translate. There's a few reasons for this:

  1. StructuredClone isn't a great general-purpose cloning algorithm. Indeed, I don't believe it's possible to create a good, safe, general-purpose cloning algorithm in JavaScript. For example, if I create a custom Map class, which I'll call MyMap, and I pass an instance of it into structuredClone, it will lose all of its methods and its private state will be inaccessible (I presume that's just thrown away as well). Any objects with functions can't be cloned either.
  2. Move semantics work great in Rust, because they're part of a whole system that can provide guarantees that, once you move ownership into a function, no one else can touch that resource. Every piece of code that has been written in Rust has been written in this framework where memory must be managed this way. This isn't the case in JavaScript. In JavScript, I could trivially bypass the guarantees of the move semantics by taking my resource stored in a local variable and putting it on, lets say, globalThis. Later, I "move" my local variable in a function, and lose my ability to use that local variable, but I still have access to the resource via globalThis, and I can still mutate it all I want. I used globalThis as an example, but it could be any object that's being stored in an outer scope, like at the module-level.

You could prevent issue 2 by layering on new rules, like, I must prove that I have full control of the resource, and there aren't references to it hanging out in other places. But, its really too late to do that sort of thing. My code might create a local variable, pass it into one third party library, then pass it into your newer library that has the "move" semantics (e.g. "const resource = ...; thirsPartyLib(resource); yourLib(move resource);`). These "move" semantics require me to prove that there isn't another mutable reference to this variable hanging around somewhere, but I have no way to prove this, because the first library could have very well captured the variable and stored it, and I can't do anything to prove that it hasn't done this.

I guess you can have the same behaviour currently with objects and proxies / getter, setters.

Setting the value of a property to null after the initial 'get'.

The structuredClone as alias was an example of the behaviour, but yes this algorithm is not suited for that. Can't a low level memory copy be used in runtime inmplementations ? Some Object seems to be hard to copy, maybe a call to new static Object method or Object.prototype named for example "toCopy" that be called by the keyword and that to be surgarged by specific structure and thorws else (the same with move and a "toMove"). Maybe a lazy copy that check ownership at property call or a proxy ?

For the move keyword, object that reference a non anonymous variable can be disallow (eg.: Type error, "var" as no ownership to "reference")

In this proposed deep-clone algorithm, how would it handle things like:

  • prototypes? If I have, for example, an instance of a userland MyUser class, and I deep-clone my instance, will it still use MyUser.prototype as its prototype? What if I deep clone MyUser itself, how would that behave?
  • Closures? if I clone a function that has captured variables from the outer scopes, will it also deep-clone those captured variables?
  • How would you handle scenarios where, for example, a userland class keeps track of all instances made of it in some static "instances" property. There's no reasonable way to make the deep clone understand that it needs to auto-add the instance to this static "instances" property.

In first, copying only refers to the "intrinsic" value, not a reference. So, a simple approach can be to:

  1. Primitives: copy the value
  2. Object:
    a. copy own properties and refers the prototype
    b. Throws if no ownership on a value (eg: parent scope value that have not be moved with move or copyied with copy to avoid performance leak, memory race and long recursion copy.
    c. Recurse from (1.)

Another implementation can be to, for example in V8, duplicate JS Object and recurse on all properties and elements and for JS Function copy all JS Object and JS Function in Context and recurse until global context (that can be make lazy). But it relieves on runtime implementation and not TC39 normalization I think.

As mentioned before, we can also, for Object, implicitly call a copy function (static of Object or in prototype) that can be surcharged.

The goal is to have a unique syntax to move or copy primitives or objects and declare mandatory explicit behavior for callers. In addition to normalize syntax of such manipulations it can also, in a second time and independently from TC39, allow optimization in engine and flow safety for js supersets (e.g. typescript)

I think it's worth giving a link to the original idea?

As for everything else:

Prevent hidden behavior

JS is pretty explicit. All plain types such as Number or Boolean passed by value (as in all languages). Everything else is passed by reference;

Simplify syntax for varaible copy (structuredClone).

structuredClone is a very specific thing, used very rarely. For example, for passing objects to WebWorker.

Better variable lifetime trace (possible optimisation for implementers)

JavaScript is GC-based Moreover, many optimizations like escape analysis don't need explicit lifetime marking at all.

All this would only add verbosity and increase the entry threshold, with no obvious benefits