Search code examples
javascriptpointersreferencesimulationdereference

How to simulate pointers in JavaScript?


I am creating a language which is compilable to Swift, Rust, and JavaScript (or at least trying). Rust and Swift both use pointers/references/dereferencing/etc., while JavaScript does not. So in a Rust-like language, you might do something like this:

fn update(x) {
  *x++
}

fn main() {
  let i = 0
  update(&i)
  log(i) #=> 1
}

In a JavaScript-like language, if you did this then it would fail:

function update(x) {
  x++
}

function main() {
  let i = 0
  update(i)
  log(i) #=> 0
}

Because the value is cloned as it is passed in (as we obviously know).

So what I am thinking about is doing this at first:

function update(scopeWithI) {
  scopeWithI.i++
}

function main() {
  let i = 0
  let scopeWithI = { i }
  update(scopeWithI)
  i = scopeWithI.i
  log(i) #=> 1
}

But that is a lot of extra processing going on, and kind of unnecessary it seems. Instead I might try compiling to this:

function update(scopeWithI) {
  scopeWithI.i++
}

function main() {
  let scope = {}
  scope.i = 0
  update(scope)
  log(scope.i) #=> 1
}

This would mean every nested scope you create, you would have to start manually creating/managing the scope chain. And actually that wouldn't work because update is hardcoded to i. So you might have to pass in what the variable name is you want.

function update(scope, ...names) {
  scope[names[0]]++
}

But then it's like:

function update(scope, ...names) {
  scope[names[0]]++
}

function main() {
  let scope = {}
  scope.i = 0
  if random() > 0.5
    let childScope = { scope }
    childScope.x = 0
    update(childScope, ['i'])
    update(childScope, ['x'])
    update(childScope, ['x'])
    log(childScope.x) #=> 2
  else
    update(childScope, ['i'])

  log(scope.i) #=> 1
}

So that seems like it might get us somewhere.

So then it's like, the generic solution is to have scope be the first parameter to a function.

function add(scope, name1, name2) {
  return scope[name1] + scope[name2]
}

Dereferencing means reading a value directly from the scope, while passing a reference (like &name in Rust or C), would mean passing the scope and the name.

Will something like this work? Or better put, what needs to be changed or added? Does it need to get any more complicated than this?

I would like to try and find a way to transform the pointer-oriented code into JavaScript (transpilation), without at first trying to figure out the seemingly much more complicated approach of not being so direct, and avoiding pointer simulation in JavaScript by redefining a lot of the methods. It seems that avoiding any pointer use in JavaScript would be way harder to figure out, so I am trying to see if a pointer sort of system would be possible to simulate in JavaScript.

To avoid pointer simulation, you would have to redefine methods.

update(x) {
  *x++
}

Would have to change the outer usage of the function everywhere. So this:

main() {
  let i = 0
  update(&i)
}

Would become:

main() {
  let i = 0
  i++ // inline the thing
}

For this simple case it's fine, but for a more complicated function it starts to seem like macros and might get complicated.

So instead of changing the outer usage, we make it so you have to pass the scope.

Another approach might be to have every variable be an object with a value, so it's more like:

update(x) {
  x.value++
}

main() {
  let i = { value: 0 }
  update(i)
}

So then I'm thinking to myself, how to handle references to references then?

update2(x) {
  update(&x)
}

update(x) {
  *x++
}

main() {
  let i = 0
  update2(&i)
}

In the system i described, that would be like:

update2(x) {
  // then what?
  let y = { value: x }
  update(y)
}

update(x) {
  // like this?
  x.value.value++
}

main() {
  let i = { value: 0 }
  update2(i)
}

So it seems this wouldn't really work.


Solution

  • Needless to say, but JavaScript does not have a general mechanism to pass arguments by reference.

    There can be some confusion around the term "reference", as in JavaScript one can pass objects to functions -- which are references -- but this is a call-by-value mechanism. Call-by-reference really means that the parameter variable is an alias for the caller's variable, such that assigning to that alias is equivalent to assigning to the caller's variable. Except for some very particular situations (like the arguments exotic object in non-strict mode, or the export mechanism, or the var link with the window object, none of which helps you in your case in a best practice way), there is no such variable-aliasing mechanism in JavaScript.

    Here is an example on how to "exploit" the effect of var in a browser context, in non-strict mode:

    function modify(ref) {
        // Using the fact that global `var` variables are aliases
        //  for properties on the global object
        // (This is not considered good practice)
        globalThis[ref] = globalThis[ref] + 1;
    }
    
    var a = 1;
    modify("a"); // We pass a reference to `a`
    console.log(a);

    In short, apart from some bad design in the old JavaScript language (in sloppy mode), it just is not possible in JavaScript in general. All your attempts that work, perform a mutation on a given object, by setting one of its properties. You cannot hope to have a function that assigns to a parameter variable, thereby modifying the caller's variable. It just is not possible -- by design. Note the important distinction between assignment and mutation.

    If the caller's variable (not a property) needs to be assigned a new value (not just mutation, but really assignment), then that assignment must happen to that variable -- something a function cannot do for the caller.

    So the way to perform such an assignment in JavaScript, is that you make the function return whatever the caller needs to reassign, and it remains the responsibility of the caller to perform that assignment:

    function modify(value) {
        return 3;
    }
    
    let value = 1;
    value = modify(value);
    console.log(value);

    When you have more than one variable that is involved, let the function return a "packed" object, which the caller can destructure back into its own variables:

    function modify(a, b) {
        return [a + 1, b * 2];
    }
    
    let a = 1, b = 2;
    [a, b] = modify(a, b);
    console.log(a, b);