-
-
Notifications
You must be signed in to change notification settings - Fork 754
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
Increases promise alignment with spec #2454
Conversation
Let me know your thoughts on this, once you have some free time. I did spend quite a lot of effort arranging this, happy to have it on the sidelines in case needed though. |
Thanks - sorry for the delay, I was away last week and I'm still catching up a bit - I wasn't sure if you thought this was quite ready for review. At first glances it seems hard to narrow down exactly what's changed because it looks like everything has :) I need to take some time and build this and test it out. Does this run through the existing local tests ( |
Just looked into this and ran And as above, it'd be really nice to have tests added for the issues that you fixed to ensure they don't end up broken in the future. |
When you ran them in node.js , what was the output? For me, I get Unhandled promise rejection, which is correct and due to the line: p.then(function(value) { // 1
}
); calling p.then() again on the initial promise, sets up a new chain, and because the second chain doesn't have a catch handler, it has to error. Even though the first chain does have a catch handler. test_promise4.js has the same idea in it, D1 is correct, but since it errors, the 'result' variable is not set, so the test fails. Those two tests should be fixed for that condition. |
Thanks! I'd previously run them in Node by copy/pasting them into the REPL so I could check the value of However now I see fails on:
So it seems this still might not fix the original reported 2 issues fully? |
Yes it must be because of the merge issue because I tested them here, and they pass (whilst using my fork 'promises-rework') |
Well I have no idea what happened but I've been unable to merge this without introducing errors interpreting promises - so I just took the file directly from this PR and overwrote what's there before. But this is now merged, and yes, the tests do all appear to pass. Thanks for the PR - but it would have been nice if it included the tests that it was attempting to fix. |
Yes, sorry about that, I didn't have the time and when I did, I had forgot most of the details of the implementation. I want to just say that the crux of the issue with the previous implementation was that, the 'chain' pointer (linked list) was used for resolve/reject pass-through when there is no 'callback' defined. Yet as pointed out in #2227 , this will not work because one promise can have multiple 'children' (when calling .then on it). This means one promise can only ever have one chain. You tried to get around it by removing the chain from the promise after its resolved, but that still means one chain for unresolved promises. As for #2450 , its similar issue, the chain needs to to be an array chain, instead of linear chain. But making the constructor(executor) be able to return a promise is similar to your code that enables .then callback to also return a promise, which isn't so difficult. I implemented it like : So now there can be multiple callbacks and chain for each Promise. I hope that helps understand the changes. I suppose your initial premise about promises was lacking, thus why the code was how it was, but for approximating to how you thought promise's work I really liked how you arranged it originally. |
Thank you so much for fixing this! |
Reopening because of #2495 - I've reverted this for now as at least it's just a single file change, and I don't want to have literally every use of Promises in the Espruino APIs broken |
sad thing is, I can't remember why I am using a container object, I need time to remember. Its possible we don't need it? |
I have remembered the reasoning for using the container object. Its because: When resolving a promise, it needs a unique instance of "resolved" , that is why resolved sits inside the prombox next to the promise. So think of it as spawning a new instance of 'resolved', which is needed for the recursion, yet re-using the promise. The // 'this' == prombox.
if (resolve) jsvObjectSetChild(resolve, JSPARSE_FUNCTION_THIS_NAME, promBox);
if (reject) jsvObjectSetChild(reject, JSPARSE_FUNCTION_THIS_NAME, promBox); So when javascript land calls resolve or reject via constructor executor, the prombox is passed via first 'this' argument, that is how the container doesn't need its reference stored. This is making me wonder if the only time that the prombox is required, is if you need to resolve it in C. If the C function is returning a promise, it is planning to resolve it internally, thus internally the prombox pointer would be needed. If the user creates the promise with Instead of too many lines in all api instances that return a promise, we could create another promise function that extracts the promise from the prombox to keep line count shorter? But the entire reasoning behind returning a promise in C, is to resolve it in C too, which requires the promBox pointer and calls the If the promise is created in javascriptland the scripter has control of the resolve. |
// otherwise the returned promise will be fulfilled with the value.
JsVar *promBox = jspromise_create();
if (!promBox) return 0;
promise = jsvObjectGetChildIfExists(promBox, JS_PROMISE_PROM_NAME);
if (promise) {
jspromise_resolve(promBox, data);
}
jsvUnLock(promBox);
return promise; In the JsVar *jspromise_extract(JsVar * promBox) {
JsVar * promise = jsvObjectGetChildIfExists(promBox, JS_PROMISE_PROM_NAME);
return promise;
} Up to you, mb the function |
Why can't you use the promise directly? Why is the wrapping box needed? |
Ok, it's to handle the scenario where you resolve a promise with another promise: new Promise((resolveOuter) => {
resolveOuter(
new Promise((resolveInner) => {
setTimeout(resolveInner, 1000);
}),
);
}); I'm this case the outer promise will be resolved but not yet fulfilled (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#description). |
If there's any way around it I'd really rather not have two separate types of promise object kicking around external to jswrap_promise.c (if it's used inside, fine) - it feels like it's going to cause all kinds of headaches in the future where in code someone creates the wrong promise object inside the Espruino codebase, or thinks that they can call promise_resolve on something passed from JS land, or pass the promise from promise_create into JS land. At the very least I guess we should rename I'm not quite sure I understood from the diagram you drew, but looking at the code the Box stores a single value for the resolved state, and a link to the Promise? But the link to the promise is never updated once set, so every box is linked to just one promise... and you have multiple boxes, one for each time a promise is resolved? But it feels like the next item in the promise 'tree' can't even be executed until the first one has resolved, so presumably only one 'box' is active at a time... And when you do Is there a reason we can't have the 'current' Box linked from the promise itself? So then we only pass the promise around, but when we need to set resolved/etc we can just follow the link to Box and set that? I'm still not sure I get it - Do you think you could give me an example of when it wouldn't work for some code in Espruino to use promise_create and promise_resolve on the actual promise rather than the box? I know there wasn't much documentation before (although there were a few in-code comments), but it would be really nice to have some kind of code documentation at the end of this, even if it's just what you said above at the top of the file explaining what's going on with promBox. Right now there are even less comments than there were before. I've spent the last hour trying to dig through this and get my head around it, and I'm still not there - and when the original author says "I had forgot most of the details of the implementation" it's worries me about how maintainable this is going to be in the future. |
The code is heavily inspired from : https://github.com/humanwhocodes/pledge also: Its not like i am completely reinventing the wheel here, understanding the above helps understand my code too. The reason for lack of comments is because I was under the impression you preferred less comments. I could adjust it so that it doesn't use a box, and has more comments, it seems possible. |
I'm just looking at some of the promise test cases and how they work, and I'm wondering whether everything is easier than I expected... In the original code I was trying to have an array as a chain of promises, because I hadn't realised that So...
Does that sound right, or am I missing something obvious? Is there a need for the boxes in here? |
For my code to not use box, I would make the |
Ok, looks like I posted this just as you posted your response! Thanks - those links look really helpful. I'm not entirely sure I understand what you mean about making them fields of the function objects, but if you think it could work that would be perfect. If it matches what happens in the spec more then that's great.
Oh no, I like comments! Not:
But actually explaining what's going on is very helpful - especially here where the jswrap_promise has been a bit of a minefield in the past :) |
So given a promise It can have many
When You do not have to iterate the chain, that will happen automatically when each resolve like a domino effect. If a callback function returns another promise, then the outer promise will not resolve until the inner promise resolves, and that can be recursive endlessly too. Your points and descriptions above sound interesting and solid, I never thought about the idea of moving the children, could work. If you wanna try implement yourself a neat solution, i'm down. But I must say that the promise concept is a lot to digest, that is why its so difficult to implement. If we manage to keep our heads afloat we should be alright.
The current technique is to call Your idea is |
Okay, so I have remembered why I used a promBox instead of doing as the spec does by making the JsVar *jsResolve = jsvNewNativeFunction((void (*)(void))_jswrap_promise_resolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
JsVar *jsReject = jsvNewNativeFunction((void (*)(void))_jswrap_promise_reject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS)); So I didn't want to create two native functions just for the sake of the c land manual resolve cases, and I wouldn't know where to store them. So because I lacked imagination on memory management in c land, that is when I came up with the idea of the prom box. |
This part in particular is what i'm referring to. So I guess I do need a function that creates the resolving functions and returns them as JsVars, then you could resolve a promise in c land by grabbing the resolve functions. |
https://esdiscuss.org/topic/es6-promises-alreadyresolved-needed This seems interesting, so the part we don't fully understand is why it needs a separate isResolved, but mb this sheds some light? I think we don't support this 'locking in' feature anyway, so mb its not needed. https://github.com/domenic/promises-unwrapping/blob/master/README.md One guys suggests: So I'm guessing its ye to prevent multiple resolve, for the case where the promise resolves another promise? Hm. Ah so I think it means this : |
Wow, thanks for digging into this - that's great! So in the 'outer promise' case, you can call My initial thought is that having a 4th state on the promise So I guess we're stuck with the box, or what I'd mentioned:
But I feel like what you've done being in line with the spec makes a lot more sense. So do you think it's possible to make promise_create just return a promise, and jspromise_resolve/reject to take a normal promise as before? It's possible they can just use a fake All the rest of the your code can be basically the same |
I don't think this is that unique from what is already done. You still would have to call resolve eventually on the outer promise, which already was resolved. EDIT: Ah mb you right that because you not resolving the outer promise, but its children instead then you are not duplicating/resolving twice. I gotcha, I see now. But ye its state would still need to be updated. because of below regarding Resolved field is to prevent firing of resolve function twice ( prevent triggering execution of the children callbacks multiple times ). Imagine in a constructor new Promise((resolve,reject)=>{
resolve();
resolve();
return;
}) State field is used primarily in the As for your other question, I am still thinking on it, but highly probable.
I see what you mean. The wrapper functions are only called from within espruino, correct. This sounds do-able. |
Minor changes, what do you think? Its now mb mergable since I updated to the master again. Why does the puckjs not compile? |
Thanks! If those changes work, that looks like it'd solve the problems? Looks great. Looking at where jspromise_create_prombox is used I guess a minor optimisation might be to give it a |
Thats a great idea! |
XD, pushing slightly too often, I need to be more patient. But ye compile errors fixed. Locks currently not good, gotta find the lock problem. Hm. |
I don't know how to find which variable/line of code is caused the LOCK assert, any ideas? In the past I think I spammed trace and consoleprint lines everywhere. My assert issue is coming from : Somehow there is a value returned from the evaluation of a script? What would that value be? How is it detecting the return value of my jswrap_promise_then() function, very confusing, one call to jsvUnLock() yet it targets my return value of jswrap_promise_then(). Actually its just at the end of each line I guess? How is the jspEvaluate calling jsvUnLock line by line? It doesnt' seem that way in the code, it seems as if its one large buffer. Kinda confused. Is the rule about having Lock Count of >0 when returning variables true even for functions which are jswrap functions called from javascript land? I would assume not, I would assume they would have a lock count of 0 and stay alive via refs only? and if they had lockcount above 0, how would their lock count ever get to 0? Nevermind, I have finally narrowed it down to this unlock: NO_INLINE JsVar *jspParse() {
JsVar *v = 0;
while (!JSP_SHOULDNT_PARSE && lex->tk != LEX_EOF) {
jsvUnLock(v); I put : if (jsvGetLocks(var)==0) raise(SIGTRAP); in So this answers my question above, every line that is parsed is unlocked, so everything returned to javascript does need to be locked atleast once because its unlocked as its parsed. |
Gotta fix a lock or two, then should be ready. |
tests/manual/bangle2_fill.js Only tests that fail, but I guess unrelated? Wait, I left debug print lines in there, gotta remove them, one sec. |
Looks great, thanks!
Yes, that's fine - I tagged the ones we expect to fail with |
Argh, what a pain - it seems that this increases the code size by 800 bytes, so now the Puck and Pixl builds fail :( I've had a bit of a look through and I've tried to reduce some duplication but I only managed to save 250 bytes, so now at least the Pixl build is working but the Puck one fails. If we could save another 300 bytes it'd work, but I think I'll probably have to pull some features out to make this build again now |
we could remove the support for thenables and less strict error checking in certain places. Dunno if that would be enough. The code is already quite compact, i'm surprised. if (isThenable) {
JsVar *args[2] = {jsResolve,jsReject};
JsExecFlags oldExecute = execInfo.execute;
jsvUnLock(jspeFunctionCall(then, 0, data, false, 2, args));
execInfo.execute = oldExecute;
JsVar *exception = jspGetException();
if (exception) {
_jswrap_promise_reject(prombox, exception);
jsvUnLock(exception);
}
} else {
jsvUnLock(jswrap_promise_then(data,jsResolve,jsReject));
} That is the thenable code lines above. Some of the strings can be reduced in length mb : this child property renamed shorter? |
Thanks - yes, I was a bit surprised too! 800b is quite a lot all things considered. I had a bit of a dive through and I found some other places I could save a bit of space, so everything is now compiling again. Good point about the names though - I'll lower them - I believe if we can get under 4 chars in total then key+value get stored in a single JsVar usually so they same RAM too. Removing thenables is a good idea - although I'm sure someone will come along and complain at some point, but I'll comment it out for now and see. |
Actually we do have a test in there for thenables, so I guess we'll keep them for now. With the other changes I made (reducing string sizes in the Puck.js lib and other stuff) we're back down to normal flash usage now |
#2450
#2227
This code aims to make promise behave closer to ES6 specs.