svaarala / duktape

Duktape - embeddable Javascript engine with a focus on portability and compact footprint
MIT License
5.94k stars 514 forks source link

What is needed to implement optional parameters? #1344

Open fatcerberus opened 7 years ago

fatcerberus commented 7 years ago

Both for parsing as well as compilation. Parsing should in theory be straightforward (just check for an equals sign followed by an expression), but I'm not sure what kind of compiler/executor changes would be necessary. One way would be to emit extra instructions in the function's prologue to check for undefined arguments and then replace them with their default values if so. But it could also maybe be done in call handling (this would require the compiler to know the function's complete signature though).

svaarala commented 7 years ago

The former sounds easier just quickly thinking.

svaarala commented 7 years ago

To avoid a lot if IF and branch instructions a LDIFUNDEF load instruction would be nice.

svaarala commented 7 years ago

Side effects apparently need to happen in order, and only if the corresponding argument is omitted:

> function foo(a=123, b=(console.log('A:', a), Math.sqrt(9)), c=console.log('A+B:', a +b)) {}
undefined
> foo()
A: 123
A+B: 126
undefined
> foo(100)
A: 100
A+B: 103
undefined
> foo(100, 200)
A+B: 300
undefined
> foo(100, 200, 400)
undefined

The arguments may also reference both previous and following arguments. So, a LDIFUNDEF doesn't really work except for side effect free cases. Those cases just might be worth special casing for (say simple strings and other primitive values) to avoid emitting actual IF branches around each argument. But a general solution requires an "if undefined ..." check so that side effects come out right.

There are also some corner cases involved in scope handling and IIRC all engines didn't handle these correctly yet. For example, you can create a closure in the default argument value and reuse it after the function has exited:

> myFn = null;
null
> function foo(a = 123, b = (myFn = function (str) { return eval(str); }), c = a * 5) { var d = 'foobar'; console.log(a, b, c) }
undefined
> foo()
123 [Function: myFn] 615
undefined
> myFn('a')
123
> myFn('b')
[Function: myFn]
> myFn('c')
615
> myFn('d')
ReferenceError: d is not defined
    at eval (eval at myFn (repl:1:74), <anonymous>:1:1)
    at myFn (repl:1:69)
    at repl:1:1
    at sigintHandlersWrap (vm.js:22:35)
    at sigintHandlersWrap (vm.js:96:12)
    at ContextifyScript.Script.runInThisContext (vm.js:21:12)
    at REPLServer.defaultEval (repl.js:313:29)
    at bound (domain.js:280:14)
    at REPLServer.runBound [as eval] (domain.js:293:12)
    at REPLServer.<anonymous> (repl.js:513:10)

The interesting thing here is that:

I'm not sure if these are the correct semantics but at least V8 behaves this way. I'd need to read ES2015 with a large cup of coffee.

fatcerberus commented 7 years ago

Huh, I assumed it would only allow constant expressions to be used as default values. I knew that arguments could reference previous arguments, but allowing arbitrary expressions in that context seems like overkill from a language design POV.

svaarala commented 7 years ago

Requiring a constant expression would on the other hand be very limiting. For example, you couldn't use undefined because it's not a literal (not that you'd want to), and couldn't use Math.PI, etc.

fatcerberus commented 7 years ago

So considering the rules for side effects, etc. here, it seems that an optional parameter is essentially the same thing as putting:

if (arg === undefined) arg = defValueExpr;

at the top of a function. That's nice, as it closely mimics idioms already used in JS before optional arguments were added to the language. In particular, each argument is tested independently (rather than basing the decision on the number of arguments at the call site) which is similar to what happens for arg || defValue (another common idiom).

Overall, I have to commend the ES committee for being able to introduce all-new language features like this while largely maintaining the semantics of existing idioms.