-
Notifications
You must be signed in to change notification settings - Fork 10
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
Refactor nbJs #148
Refactor nbJs #148
Conversation
I have tested this new slim nbJs on our documentation and our NimConf slides now and it seems to work. Tomorrow I'll go through your nblog posts which uses nbJs. Specifically there was one which needed |
I checked nblog now and everything seems to work if we just move the imports to a global block 🥳 |
Documentation and tests have been updated now. It's actually quite impressive how short I actually think this PR might be ready for review :o @pietroppeter |
Have you tried running the three_js example in nblog? |
Yes, moving the imports to a global block gives me a rotating purple cube :D |
I am thinking that we might want to have also a block for when you plan to have a single (nim derived) script in the entire page. |
I don't quite understand what you mean. We have nbJsFromCodeOwnFile which karaxCodeBlock uses internally which compiles a code block the old way into its own file. Is that what you mean? |
Without Gensym so that a case like threes_js works without a separate global section |
Nothing uses gensym anymore. So for this case you could either use |
ah ok, not even karax block use gensym? so two identical karax blocks will not run into issues of sharing variables? and I need still to actually review code and understand for real this time how all this js stuff works. 🙄 thanks for all the gentle explanations! ❤️ |
No, the
The code has never been as comprehensible as it is now, so now is a good time to dig in. Keep shooting questions at me, I'm not merging this until you understand it ;)
Most of the complexity is gone now thanks to the removal of all things gensym. So this is basically what we are doing now. Except that we are baking all the scripts into a single file (for ordinary Capturing variables is probably the most complex part that we have left, but it is "only" ordinary typed macro stuff to generate the correct code to serialize and deserialize the variable on the different sides. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added some comments (mostly questions) from what I understand on the changes. Given that I did not understand the code you are replacing I will also add more questions in general reviewing the complete files in your actual branch
Ok, I have read the rest and I can go on with comments and questions, it seems I understand now most of the code except the stuff in nimToJsString. Correct me on the statements below if I am wrong. In
On capturing variables:
Finally I think, having understood a bit more, we probably could improve the interactivity document:
|
Ok, with the in-code-review and last comment, I think I have exhausted my review on current code. Hey I am taking this literally 🤣:
|
Correct observations! 👍 It shouldn't be used unless you have no other choice, basically. The implementation is so simple though that it doesn't hurt to keep it around.
Correct 👍
Correct 👍
Correct observations 👍
It entirely depends on what we decide on in the discussion of where to place the global blocks. If they are placed at the top, these names are misleading in my opinion. If they are mixed between each other, these names make more sense but are a bit too verbose imo. Better to remove the "add" then and go with
It is common to put your javascript at the bottom of your body after all, so why not put it at the bottom of our page as well. I don't see any reason anyone would want to insert this anywhere else (there might still be reasons of course but
I don't know what showing the original code for the entire global script would give us tbh. There would be multiple variables named the same for example, so it wouldn't be compilable Nim code. And the processed global script file isn't perhaps the prettiest of code, but could be good to have for debugging purposes. Here we could just add a new command which prints out the current code in template nbJsFromCodeShow([args](args: varargs[untyped])) =
nbCodeDontRun: # show the body of the code
args[0]
nbJsFromCode(args) This way we don't have to save the original code. |
Sure, will do 👍
The complexity is mainly due to the capturing of variables, but there still is logic in there that we need all the time. We still need the
The fact that we have two stages is that we want the body to be untyped. This is because typed code has been transformed and isn't the code that we wrote originally. Here's an example: # untyped
let a = 1
let b = 2
echo a + b
# typed
let a = 1
let b = 2
echo [a + b] As you can see, if we compiled the typed code in a file, it would echo out an array instead of an int. So having the body be untyped is very important. But we also want to get the type of the variables we want to capture, so that we can unserialize them to the correct type on the javascript side. So we need a typed macro to get those types. The question that is, how do we pass the untyped body to a typed macro without making it typed? The answer is putting it in a cache table! In the typed macro (second stage), we can then get the untyped AST of the body from the cache table. The key to the element in the table, So this should hopefully bring some light on why we need the two stages, the cache table and degensym. |
Now for the actual capturing of variables! let captureVars = toSeq(captureVars) Here we save the symbols to the captured variables in a seq. Next we get the type of each symbol: let captureTypes = collect:
for cap in captureVars:
cap.getTypeInst Here we fetch the untyped body from the cache: var body: NimNode
if key in bodyCache:
body = bodyCache[key] Next up we generate the assignments and deserialization of the captured variables: # 1. Generate the captured variable assignments and return placeholders
let (capAssignments, placeholders) = genCapturedAssignment(captureVars, captureTypes) If we for example have this code: var x: int = 123
var y: string = "hello world"
nbJsFromCode(x, y):
echo x, y What let x = parseJson(placeholder`gensym1).to(int)
let y = parseJson(placeholder`gensym2).to(string) and the list of placeholder-symbols it has generated. Now what's up with Next up we combine these new assignments and the body, degensym it, put it in a block (if specified) and turn the code into a string: # 2. Stringify code
var code = newStmtList(capAssignments, body).copyNimTree()
code.degensymAst()
if putCodeInBlock:
code = newBlockStmt(code)
var codeText = code.toStrLit So We will basically have this string in the end: let codeText = """
let x = parseJson(placeholder`gensym1).to(int)
let y = parseJson(placeholder`gensym2).to(string)
echo x, y
""" Now we have come to the most complex part of it all: generating the replacements of the placeholders with the serialized json. We start off with assigning the codeString to an actual variable: let codeTextIdent = genSym(NimSymKind.nskVar, ident="codeText")
result = newStmtList()
result.add newVarStmt(codeTextIdent, codeText) So this will generate the following code: var codeText = """
let x = parseJson(placeholder`gensym1).to(int)
let y = parseJson(placeholder`gensym2).to(string)
echo x, y
""" Then we loop over each of the capture variables: for i in 0 .. captureVars.high: and for each one we do the following: # Get string literal of the placeholder (e.g. "placeholder`gensym1")
let placeholder = placeholders[i].repr.newLit
# Get the variable symbol
let varIdent = captureVars[i]
# Generate the call to serialize the variable
let serializedValue = quote do:
$(toJson(`varIdent`)) So the serialized value is basically obtained like this: $(toJson(x)) The last thing we do in the loop is to do the actual string replacement of the placeholder with the json: result.add quote do:
`codeTextIdent` = `codeTextIdent`.replace(`placeholder`, "\"\"\"" & `serializedValue` & "\"\"\"") This basically generates this code: codeText = codeText.replace("placeholder`gensym1", &"{$(toJson(x))}") And that is basically it. This final code on the C-side is: var codeText = """
let x = parseJson(placeholder`gensym1).to(int)
let y = parseJson(placeholder`gensym2).to(string)
echo x, y
"""
codeText = codeText.replace("placeholder`gensym1", &"{$(toJson(x))}")
codeText = codeText.replace("placeholder`gensym2", &"{$(toJson(y))}") |
Good point 👍
WIll add it to the
Do you have any concrete examples in mind? (I don't 🤣 )
Haha yes, it has many use-cases :) I haven't tried it out with any complex datatypes yet tbh, might be a good excuse to test its limits and do something cool with it as you say 😎 In your planned Machine learning tutorial on SciNim for example, if we trained some simple machine learning model and provided an interactive way of testing it using nbJs somehow. Basically training it in the C-side and making predictions on the JS side with user inputs. |
Haha that's good! 😄 Didn't expect to sit nearly 2h answering 🤣 But that time will be well spent if it means I can save more hours in the future now that you know how this all works and can cover for me ;) |
Thanks a lot for the answer and the great write up! I have been reading and thinking about this stuff, and I have had some interesting thoughts (some outside of scope of this PR). Before doing that, I will like first to list stuff that is undecided, to do or needs more discussion: from in-code review:
rest of conversation.
first off, did I miss something? I intentionally did not quote here, but I tried to go with order of appearance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good. just add a few suggestion to align block names to new naming convention.
I may have lost some nbJsToCode
along the way (especially if they do not appear in the diff..).
Will now look again at the preview docs but I guess we are basically done! :)
Had a look at the preview docs and had a few more remarks:
I think with this I finally completed the review. Sorry for the delay and again great work on this! |
Co-authored-by: Pietro Peterlongo <[email protected]>
Co-authored-by: Pietro Peterlongo <[email protected]>
Co-authored-by: Pietro Peterlongo <[email protected]>
Not sure I understand what you mean :/
Will fix! |
Now I found it. Changed
🥳 |
well, there is plenty of open issues... 😆 |
ready to merge, right? |
🤣 very true, 51 open issues should have something yummy in them. I can bump the version and changelog when I get home 👍 I think this is my first time making a new release on my own? 😯 |
then go ahead and enjoy the release! 🚀 |
Versions are bumped, hope I didn't forget anything now... |
ah, maybe add a line about the new |
Good point :+1 Looked through the docs before pushing the last commit and everything seems to work fine. So I'll merge this after dinner unless we find something else until then. |
looks good to me. the more I think about it the more stuff it pops to mind. we probably should link the NimConf 2022 presentation (and slides) in the readme/index? not sure in the minimal way we did for 2021 or showing the video thumbnail (would look more attractive). to be honest this could all be separate PRs but since we are here... |
It would be nice with a thumbnail, but we'd just have to get a better thumbnail where we both are in it. I have uploaded one such example. But I think it is better if we merge this first so we can get a static link to it in the main branch instead before linking to it. So I'll merge this and create a follow-up PR for that. |
As has been discussed in #118, the gensym-logic of nbJs is too complicated for the basic use-cases. Karax needs this advanced logic though, as each karax component must be compiled in its own file.
So the goal for this PR is to make the normal nbJsFromCode only use blocks instead gensym-logic. I have already started and I am getting nice behavior for simple cases. I need to check many more examples, though.
This PR will introduce several breaking changes:
nbJsFromCode
will put the code inside ablock
soimport
s will not be allowed in them. Instead, you must usenbJsFromCodeGlobal
which will be put at the top of the final script.nbJsFromCodeInit
,addCodeToJs
will be deprecated as we are limiting ourselves to isolated blocks. If you want interop between multiplenbJs
blocks, define a global variable usingnbJsFromCodeGlobal
.Now I'm going to try and fix the docs.
TODO:
from in-code review:
rest of conversation.
compileNimToJs
fromrenders.nim
tojsutils.nim
internal working
sections on sources of complexity