[parenscript-devel] A simpler way to do multiple value return? (Was: PS test failures in 7be9b45)

Vladimir Sedach vsedach at gmail.com
Sat Sep 1 21:48:48 UTC 2012


> But could the compiled PS instead keep a global
> RETURN_VALUES stack? i.e. a list of MV arrays that callers would
> push/pop as appropriate? I haven't thought about how this might work
> and it feels like it probably wouldn't, but I'd like to know why.

You're right that it would need to be a stack, hence the prev_mv
variable in the code I posted previously. But you'd still need to
associate the values with the function that returned them, and if you
can do that by setting a property on the function object, why bother
with global variables and tables that hold stacks? The former approach
is less code.

> I like the array-passing idea and agree that it probably needs to be
> passed out of band - but can we state explicitly why? For example, why
> can't we make it a hidden first argument (it couldn't very well go
> anywhere else because of things like &REST) and make Parenscript smart
> enough to add in the correct value for that hidden argument every
> place that function is called (i.e. pass null if the extra return
> values aren't to be bound, and an array to hold them if they are)?
>
> One obvious drawback is that non-PS functions wouldn't be able to call
> such a function normally; they'd have to know about the extra arg.
> What other drawbacks are there?

You wouldn't be able to have calls to these functions before they're
defined in your code.

> I like this idea, because everyone in the JS world is so adamant that
> one shouldn't mess with arguments.metablah (though arguments.callee
> has got to be better than arguments.callee.caller). But how would it
> work for lambdas?

I tried it out in FF and CL-JS and arguments.callee always seems to be
the function object, even for lambdas.

> Right. I don't follow your example here, though, so I wonder if you
> can spell it out a bit further.

foo is a function that returns either one value or multiple values. We
call foo expecting to receive multiple values. Now foo.mv is set to
the multiple value array. In the course of computation, foo calls bar,
which does not care about multiple values. In this second call to foo,
foo.mv is still the same multiple value array. The second call to foo
returns multiple values, so the multiple value array gets filled up.
However, the first call to foo returns only one value. But because the
multiple value array has been filled up, we get bogus multiple values.

Vladimir

> Daniel
>
> On Thu, Aug 30, 2012 at 7:12 PM, Vladimir Sedach <vsedach at gmail.com> wrote:
>>
>> I played around with several approaches to multiple values, and Red is
>> correct that they all need to be stack aware. However, it turns out
>> you don't need callee.caller for that, just first-class functions and
>> closures. If you don't care about changing the calling convention,
>> you don't even need that.
>>
>> The basic idea is that a form expecting multiple values creates a
>> mutable array to store those values, and passes it down the stack
>> (this is a common pattern in C code). If you don't care about function
>> calling convention, you can just pass around a mutable array for
>> multiple values as an implicit part of the argument list.
>>
>> Parenscript cares, so we need to pass that data out-of-band. We can do
>> this by associating the function object about to be called with the
>> multiple value array:
>>
>> (multiple-value-bind (x y) (foo)
>>   ...body...)
>>
>> prev_mv = foo.mv; // don't clobber things up the stack
>> var values = [];
>> foo.mv = values;
>> x = foo();
>> if (values.length > 0) {
>>   y = values[0];
>> }
>> foo.mv = prev_mv;
>> ...body...
>>
>> Note that we just need some way to associate foo with the array. You
>> can do that with a global table instead of setting a property on the
>> function object.
>>
>> Functions that return multiple values look like:
>>
>> (defun foo (x y z)
>>   (values x y z))
>>
>> function foo (x, y, z) {
>>   var values = arguments.callee.mv;
>>   if (values) {
>>     values[0] = y;
>>     values[1] = z;
>>   }
>>   return x;
>> }
>>
>> Note that you don't need arguments.callee for a function to have a
>> reference to itself:
>>
>> var foo = (function () {
>>               var self = function foo () {
>>                            self.blah = whatever;
>>                             ...body...
>>                           };
>>               return self;
>>               })();
>>
>> That is very ugly though.
>>
>> multiple-value "pass-through" only happens in the case when there is
>> an expression like "(return (some-multi-valued-function))" in the
>> code. Since Parenscript now instruments all returns (this was not the
>> case when the original multiple value mechanism was worked out), we
>> can pass multiple values along like so:
>>
>> (defun bar ()
>>   (foo)
>>   (foo))
>>
>> function bar () {
>>   foo(); // first invocation, don't care about multiple values
>>   foo.mv = arguments.callee.mv;
>>   var result = foo();
>>   delete foo.mv;
>>   return result;
>> }
>>
>> As you can see we only give the array to functions in instances where
>> they can potentially return multiple values.
>>
>> A problem arises for recursive functions (and any function objects
>> that can appear multiple times in the stack):
>>
>> (defun foo (x)
>>   (if (= x 1)
>>       (values 1 2)
>>       (1+ (foo (1- x)))))
>>
>> foo(2) will now return multiple values, even though it shouldn't.
>>
>> In general, this can happen if foo calls any function x calls... a
>> function that eventually calls foo again.
>>
>> This wouldn't happen if the values array was passed as an argument.
>>
>> One way I see to solve this problem is to add some code to any
>> function that can potentially return multiple values:
>>
>> function foo (x) {
>>   var values = arguments.callee.mv;
>>   delete arguments.callee.mv;
>>
>>   if (x === 1) {
>>     if (values) values[0] = 2;
>>     return 1;
>>   } else {
>>     return 1 + foo(x - 1); // not expecting values
>>   }
>> }
>>
>> Obviously the above code will need things like unwind-protect and
>> gensyms, etc., but does anyone see anything that's wrong with the
>> above proposal?
>>
>> Vladimir
>>
>>
>> On Wed, Aug 29, 2012 at 1:33 PM, Daniel Gackle <danielgackle at gmail.com>
>> wrote:
>> > Ah yes clearer. This is similar to Vladimir's case from upthread:
>> >
>> >   (defun foo ()
>> >     (blah)
>> >     (some-random-js-function))
>> >
>> > ... except that my suggestion that the compiler figure out when BLAH
>> > isn't in a return position and do something like:
>> >
>> >   function foo() {
>> >       blah();
>> >       RETURN_VALUES = null;
>> >       return someRandomJsFunction();
>> >   };
>> >
>> > ... won't work in your case, i.e. when FOO is not a PS function.
>> >
>> > But I wonder whether perfect interop from JS back into PS isn't an
>> > overly ambitious a thing to promise. When language A and language B
>> > have different calling conventions and you call B from A, it's normal
>> > to have to follow some protocol to manually bridge the gap between
>> > them. In this case the protocol might be: you must either return the
>> > call to BLAH or clear RETURN_VALUES. Yeah this would be a pain and
>> > easy to forget, but it is arguably a reasonable card for PS to have to
>> > play here.
>> >
>> > That being said, it's not surprising that a simple global variable
>> > wouldn't do the trick in every case. I hope we can come up with
>> > something that does.
>> >
>> > But let's not forget that the current implementation is even more
>> > broken. It doesn't do the right thing even if BLAH is in a return
>> > position - so *none* of the cases we're talking about actually work
>> > right now.
>> >
>> > Daniel
>> >
>> >
>> > On Wed, Aug 29, 2012 at 10:16 AM, Red Daly <reddaly at gmail.com> wrote:
>> >>
>> >> Hi Daniel,
>> >>
>> >> I'm glad to be part of the discussion :)
>> >>
>> >> On Aug 28, 2012 8:53 PM, "Daniel Gackle" <danielgackle at gmail.com>
>> >> wrote:
>> >>>
>> >>> Hi Red,
>> >>>
>> >>> I was hoping you'd chime in. I'll see your scenario 3 and raise you a
>> >>> 3a and a 3b:
>> >>>
>> >>> Scenario 3a: A non-Parenscript function that calls a mv-returning
>> >>>              Parenscript function but only needs its first return
>> >>> value;
>> >>>
>> >>> Scenario 3b: A non-Parenscript function that calls a mv-returning
>> >>>              Parenscript function and needs all its return values.
>> >>>
>> >>> 3a works fine as long as the MV implementation is careful to use a
>> >>> normal JS return to pass the first return value back to the caller.
>> >>> That's true both of what PS does today and of the global-var proposal.
>> >>>
>> >>> As for 3b (the scenario, not the hacker!), seems to me this can't work
>> >>> at all and there's no need to support it. If you're a non-PS function
>> >>> then by definition you can't use PS's MULTIPLE-VALUE-BIND to access
>> >>> the additional return values because the MV construct only exists in
>> >>> PS. I suppose if you really wanted to you could manually write JS to
>> >>> do whatever PS does to supply those values to the caller, but then
>> >>> you're not really a non-PS function anymore, so much as a
>> >>> manually-compiled PS function.
>> >>>
>> >>> Daniel
>> >>
>> >>
>> >> I wasn't clear in my original post.  I'm not concerned with the
>> >> non-Parenscript function's ability to receive multiple values.  I'm
>> >> concerned that the non-Parenscript function will interfere with the
>> >> multiple
>> >> value return.
>> >>
>> >> If a non-Parenscript function calls a Parenscript function that messes
>> >> with the global variables, the non-Parenscript function could end up
>> >> returning stuff unintentionally.  This is because the non-Parenscript
>> >> function will not manipulate the global variables to clean up the
>> >> unused
>> >> multiple values that are returned.
>> >>
>> >> Here's some code to illustrait the point:
>> >>
>> >> (defun ps-foo ()
>> >>   (multiple-value-bind (a b) (bar)
>> >>      (+ a b)))
>> >>
>> >> (defun ps-fn-returns-mv ()
>> >>   (values 1 2))
>> >>
>> >> function barWorks() {
>> >>   return psFnReturnsMv();
>> >> }
>> >>
>> >> function barBreaks() {
>> >>   psFnReturnsMv(); // global variables for MV returning get set up and
>> >> linger
>> >>   // return a single value: 42
>> >>   return 42;
>> >> }
>> >>
>> >> Let's assume the two Parenscript functions translate into something
>> >> like
>> >> this:
>> >>
>> >> var RETURN_VALUES = null;
>> >> var MV_CALL = false;
>> >> ... other global variables related to multiple values
>> >>
>> >> function psFoo() {
>> >>   // global variable stuff
>> >>   MV_CALL = true;
>> >>   RETURN_VALUES = null;
>> >>
>> >>   var a = bar();
>> >>   MV_CALL = false;
>> >>   var b = RETURN_VALUES ? RETURN_VALUES[0] : undefined;
>> >>   return a + b;
>> >> }
>> >>
>> >> function psFnReturnsMv() {
>> >>   if (MV_CALL)
>> >>     RETURN_VALUES = [ 2 ];
>> >>   return 1;
>> >> }
>> >>
>> >>
>> >> When bar = barWorks, foo will return 1 + 2, as intended.  When bar =
>> >> barBreaks, foo will incorrectly return 42 + 2 because the global
>> >> variables
>> >> were not properly cleaned up.
>> >>
>> >> I hope this makes sense.
>> >>
>> >> - Red
>> >>
>> >
>> >
>> > _______________________________________________
>> > parenscript-devel mailing list
>> > parenscript-devel at common-lisp.net
>> > http://lists.common-lisp.net/cgi-bin/mailman/listinfo/parenscript-devel
>> >
>>
>> _______________________________________________
>> parenscript-devel mailing list
>> parenscript-devel at common-lisp.net
>> http://lists.common-lisp.net/cgi-bin/mailman/listinfo/parenscript-devel
>
>
>
> _______________________________________________
> parenscript-devel mailing list
> parenscript-devel at common-lisp.net
> http://lists.common-lisp.net/cgi-bin/mailman/listinfo/parenscript-devel
>




More information about the parenscript-devel mailing list