Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spread operator not applying to arguments object #111

Closed
Patrick-ring-motive opened this issue Jan 29, 2025 · 14 comments
Closed

Spread operator not applying to arguments object #111

Patrick-ring-motive opened this issue Jan 29, 2025 · 14 comments
Assignees
Labels
bug Something isn't working enhancement New feature or request

Comments

@Patrick-ring-motive
Copy link
Contributor

Patrick-ring-motive commented Jan 29, 2025

The spread operator is not being properly applied to the arguments object. I use this often to chain function arguments.

console.log('Sval Version: ' + Sval.version); //"Sval Version: 0.5.8"

const interpreter = new Sval();

interpreter.run(`(${()=>{
  function log(){
    console.log(...arguments);
    /* [object Arguments] {
          0: 1,
          1: 2,
          2: 3
       }  */
    console.log(...Array.from(arguments));
    /* 1
       2
       3 */
  }
  log(1,2,3);
}})()`);

Expected console.log(...Array.from(arguments)); and console.log(...arguments); to have the same output.

Edit: for better highlighting

@Patrick-ring-motive
Copy link
Contributor Author

Patrick-ring-motive commented Feb 1, 2025

I'm not sure how deep this bug goes so I've refactored my code using the following utilities in order to avoid the spread operator.

//spread into a method call
const apply = ($this, fn, args) => $this[fn].apply($this, args);
// apply(String,'fromCharCode',args) ≈ String.fromCharCode(...args)

//spread into a function call
  const enact = (fn, args) => fn.apply(undefined, args);
// enact(fetch,args) ≈ fetch(...args)

//spread into an array
  const arr = (x) => Array.from(x);
// arr(x) ≈ [...x]

//spread into a constructor
  const anew = (fn,args) => Reflect.construct(fn,args);
// anew(Request,args) ≈ new Request(...args)

@Siubaak
Copy link
Owner

Siubaak commented Feb 2, 2025

yeh, there was a bug of spread element. just fixed, and could you try if 0.5.9 works for you?

@Patrick-ring-motive
Copy link
Contributor Author

@Siubaak
It seems to work with the arguments object but now I get an error on the array.


"Sval Version: 0.6.0"
1
2
3
"error"
"vr@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:156134
CallExpression@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:161887
CallExpression@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:162332
Br@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:166647
Dr@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:166840
e@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:176831
CallExpression@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:162494
Br@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:166647
Dr@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:166840
e@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:176831
CallExpression@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:162494
Br@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:166647
Program@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:166477
@https://unpkg.com/sval@0.6.0/dist/sval.min.js:1:185712
global code@https://null.jsbin.com/runner:9:16
write@[native code]
@https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:13929
@https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:10867"


@Patrick-ring-motive
Copy link
Contributor Author

I don't think I'll have time today but I'll try and set up a PR tomorrow. I'm thinking that using Array.from(x) would work better than [...x]. The objects can either be iterable and/or have a length property. The same issues may be present in for...of loops since the same logic applies. For example HTMLCollection does not have a Symbol.iterator but it can spread to an array.

@Siubaak
Copy link
Owner

Siubaak commented Feb 3, 2025

well. i think literally only iterable objects can be spread in an array according to the spec:

Image

and HTMLCollection does have a Symbol.iterator in its prototype, so that why it can be spread:

Image

i can't reproduce your error of spreading arguments object. could you show me the code?


edit:

oh, i saw your code above and reproduced it. let me see what happened later on.

@Patrick-ring-motive
Copy link
Contributor Author

Sorry I'm not trying to be annoying. Just following where my code goes.

@Siubaak
Copy link
Owner

Siubaak commented Feb 3, 2025

that's alright. no problem. sorry if i came across as too direct. all thoughts are welcome. :D

i digged deep into it and found that's the problem of babel, instead of sval. when you use the code below:

interpreter.run(`(${()=>{
  function log(){ console.log(...Array.from(arguments)) }
  log(1,2,3);
}})()`);

it will be compiled to be the following result in the playground (by babel provided by jsbin):

interpreter.run(`(${()=>{
  function log(){ console.log(_toConsumableArray(Array.from(arguments))) }
  log(1,2,3);
}})()`);

then sval eventually runs the code like:

interpreter.run(`
  function log(){ console.log(_toConsumableArray(Array.from(arguments))) }
  log(1,2,3);
`);

and _toConsumableArray is not defined in the context of sval, throwing error. so to make it right, the code should be directly like:

interpreter.run(`(()=>{
  function log(){ console.log(...Array.from(arguments)) }
  log(1,2,3);
})()`);

edit:

to be more specific, you could try the code below, and it will print the exact code in the jsbin console:

const code = `(${()=>{
  function log(){
    console.log(...arguments);
    /* [object Arguments] {
          0: 1,
          1: 2,
          2: 3
       }  */
    console.log(...Array.from(arguments));
    /* 1
       2
       3 */
  }
  log(1,2,3);
}})()`

console.log(code);

then you will see the code we run ends up being like:

"(function () {
  function log() {
    window.runnerWindow.proxyConsole.log.apply(console, arguments);
    /* [object Arguments] {
          0: 1,
          1: 2,
          2: 3
       }  */
    window.runnerWindow.proxyConsole.log.apply(console, _toConsumableArray(Array.from(arguments)));
    /* 1
       2
       3 */
  }
  log(1, 2, 3);
})()"

@Patrick-ring-motive
Copy link
Contributor Author

Patrick-ring-motive commented Feb 3, 2025

Ah I see. Things like this is why I append global scope manually to sval global scope like this.

import Sval from './sval.js'


const interpreters = {
  module : new Sval({
    ecmaVer: 'latest',
    sourceType: 'module',
    sandBox: false,
  }),
  script : new Sval({
    ecmaVer: 'latest',
    sourceType: 'script',
    sandBox: false,
  }),
};


const svalGlobal = interpreters['module'].scope.context.globalThis.value;

const keys = [];
for(const key in globalThis){keys.push(key);}

const props = new Set(Object.getOwnPropertyNames(globalThis).concat(keys).concat(Object.getOwnPropertySymbols(globalThis)));

for(const prop of props){
  svalGlobal[prop] = globalThis[prop];
}

This could be a feature flag like how jsdom has runScripts: "dangerously".

Not that it would have helped here.

@Siubaak
Copy link
Owner

Siubaak commented Feb 3, 2025

if i'm not wrong, you don't have to do that, since sval has already import all variables from global context. see here. right now only window and global get checked, though. i think i should check globalThis as well. i'll add it in the future.

the issue right here is that, _toConsumableArray is a util function of babel injected into the compiled code. it's not a global function or something global. the best way to avoid unexpected compilation is to directly input a code string into sval. it's not a good idea to explicitly or implicitly call the function.toString() to generate a code string.

for example, the code below

interpreter.run(`${()=>console.log(...Array.from(arguments))}`);

will be compiled to be (if using babel for compiling)

function _toConsumableArray() { /* babel polyfill stuffs */ }
interpreter.run(`${()=>console.log(_toConsumableArray(Array.from(arguments)))}`);

then during runtime, function.toString() will be called implicitly on the arrow function, so it ends up being like

function _toConsumableArray() { /* babel polyfill stuffs */ }
interpreter.run(`()=>console.log(_toConsumableArray(Array.from(arguments)))`);

this is the reason why _toConsumableArray is not defined inside sval. so instead, it would be better if directly inputing a code string

interpreter.run(`()=>console.log(...Array.from(arguments))`);

@Patrick-ring-motive
Copy link
Contributor Author

Patrick-ring-motive commented Feb 3, 2025

The reason I wrote the code using function.toString was so that the code would get better syntax highlighting and make it easier to read. I wouldn't normally do that.

I've seen such outputs of babel that generate those utility functions. I know appending global scope objects wouldn't have helped here but I'm glad I mentioned it. My main use case is in cloudflare workers. Using sval is so far the only way to dynamically import remote scripts into cloudflare workers as all methods like eval or Function are disabled. That is one reason I appreciate this project so much. Cloudflare workers do not have global or window only globalThis so that explains why it wasn't working how I expected. I would request checking globalThis and self as self is the default global object reference for clientside workers.

@Siubaak
Copy link
Owner

Siubaak commented Feb 3, 2025

Ah, i got your points. my bad for just mistaking ”not that” for “note that”.

i will try to add globalThis and self tomorrow.

@Siubaak
Copy link
Owner

Siubaak commented Feb 4, 2025

I've just added support for globalThis and self. Could you try if 0.6.1 helps?

Image

@Patrick-ring-motive
Copy link
Contributor Author

Yay it works. I initially got an error Cannot assign to read only property 'Infinity' of object but that turned out to be from my previous code to do this manually.

for(const prop of props){
  svalGlobal[prop] = globalThis[prop];
}

I removed that and it worked perfectly.

I have one tiny suggestion that you might consider. In utils.ts starting at line 89 you have this.

try { globalObj.Object = Object                         } catch (err) { /* empty */ }

Rather than assign with = you can assign with

const setProp = (obj,prop,value) => Object.defineProperty(obj,prop,{
  value:value,
  enumerable:true,
  configurable:true,
  writeable:true
});

try { setProp(globalObj,'Object',Object); } catch (err) { /* empty */ }

It's essentially the same but bypasses any strange custom getter/setter logic.

@Siubaak
Copy link
Owner

Siubaak commented Feb 4, 2025

That's true. You're right. Some other logic should be handled like what you said as well. For example, using Object.assign for implementing object spreading in Sval will also implicitly call setters, but the native object spreading won't call setters at all.

I think I'll review the whole codebase to fix all the similar situations in the future.

@Siubaak Siubaak self-assigned this Feb 4, 2025
@Siubaak Siubaak added bug Something isn't working enhancement New feature or request labels Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants