Implement new FFI system as in the proposal#583
Conversation
57313f3 to
9a361ae
Compare
function (values, ...args) {
}
Now CL functions take a general JS signature
fucntion (...args) {
...
}
but can write multiple values to a internals._mv variable. The
compiler has been adjusted to carefully set and reset this side
channel variable in multiple places.
mv contain all values, not just the secondaries
That way we can differentiate between (values) and (values nil)
9a361ae to
63c060f
Compare
The _mv side channel is cleared too early in compile-funcall. When a function call is compiled with *multiple-value-p* = t, the compiler generates: (internals._mv = null, func.fvalue(arg1, arg2, ...)) The _mv = null clearing happens before argument evaluation (due to JS comma operator semantics - the left side executes first, then the arguments are evaluated as part of the function call expression on the right). But if any argument contains a sub-expression that calls a function returning multiple values (like intern, which returns (values symbol was-present-p)), that sub-expression will set _mv to a non-null value after the clearing but before the outer function call executes. When the outer multiple-value-call then checks _mv, it finds the stale values from the inner intern call instead of the actual multiple values from the outer call. This causes it to use the wrong values. In the defstruct case: 1. (setf (dsd-constructors dd) (or constructors (list (make-dsd-constructor :name (%symbolize "MAKE-" (dsd-name dd)) :boa :default)))) 2. The setf is wrapped in a multiple-value-call by the compiler 3. Inside the argument evaluation, %symbolize calls intern which sets _mv = [MAKE-ES, NIL] 4. The outer multiple-value-call picks up this stale _mv value 5. Instead of passing the correct list (#S(dsd-constructor ...)) to the setf function, it passes MAKE-ES 6. So (dsd-constructors dd) becomes the symbol MAKE-ES instead of a list of constructor structs 7. Later, mapcar over this "list" tries caar on MAKE-ES, producing the TYPE-ERROR The fix would need to ensure _mv is cleared after all arguments are evaluated but before the function body begins executing, or the code generation needs to evaluate arguments into temporaries first and clear _mv after.
c8bbaba to
24669ec
Compare
|
Deploy preview ready! |
Initialize the symbols earlier as uninterned symbols, and intern them into the CL package later.
|
I don't think |
50eec1e to
6b52388
Compare
It would be very convenient if any js object is printed in a way that would be readable too. |
|
The ergonomics are not great yet: ["foo", "bar"].map(x => x.toUpperCase())can be done like (let ((arr (vector #j"foo" #j"bar")))
((oget arr "map")
(lambda (x) ((oget x "toUpperCase")))))A syntax for calling methods would be nice (let ((arr (vector #j"foo" #j"bar")))
(jsmethod arr "map"
(lambda (x) (jsmethod x "toUpperCase"))))would be even nicer if #j allowed for dynamic root more easily, then (let ((arr (vector #j"foo" #j"bar")))
(#j:(arr):map (lambda (x) (#j:(x):toUpperCase)))) |
|
I think the current |
A common solution found in the wild is |
Use special UNBOUND marker instead of undefined.
kchanqvq
left a comment
There was a problem hiding this comment.
Kudos for this ambitious undertaking!
Besides the inline review/discussion, two high-level things:
- I think a lot of complexity is added to emulate JS values in the host because we use
#j:...literal during bootstrap.
- How strongly do you want to use
DEFSTRUCTs for the emulation? We could use some magic symbol markers, which will remove the need forMAKE-LOAD-FORM. But you could say this is one step backward. - The option I like better is to not use
#j:...literal in the host at all. I think this is possible: just write(jsstring "cl-string"), and our compiler is smart enough the elide the conversion. If this works I think this can massively simplify. We're currently only emulating these literals as opaque datum anyway and I think it's unlikely we really need them.
- I think representing
NILwithundefinedwill simplify lots of things and is more ergonomic. But this probably goes into another PR. Let's discuss!
src/compiler/codegen.lisp
Outdated
| (output-arg (cadr args)) | ||
| (dolist (operand (cddr args)) | ||
| (js-format ",") | ||
| (output-arg operand)))) |
There was a problem hiding this comment.
I think this loop can be written cleaner with a first-arg-p flag. Something like
(let ((first-arg-p t)) (dolist ... (when first-arg-p ...output-comma... (setq first-arg-p nil)) ...)). And there would be no need for an flet (and cddr etc to peek into later part of the list).
I had this before. But the thing is now the reader does not return We could make the reader return logic conditional on being the host or not. But even if now they are only tokens, it'd be useful to dispatch based on js types in the host. So I think we'd end up replicating defstructs on top of s-expressions. We might use defstruct lists to remove the make-load-form to this also? we have to be careful with consp when implementing the compatibility |
yeah. But undefined can't do the nice jscl_car trick we did to make I think we should still experiment with it, but I'd leave it for another branch. |
IIUC my proposal is different. My proposal is never use I speculate this is possible, and wherever we use |
We could also use |
I see. That could work. So to make sure I understand:
So we do not dump structs. I'll still have to keep the structs. And make |
Use jsstring and jsbool in key bootstrap places to make `make-load-form` unnecessary.
|
I'm happy with the state of the PR now. So if not objections, I'd leave extra improvements for future PRs. |
7215fba to
bd30e21
Compare
I think we can get rid of any emulation on host stage, but I'll have to experiment, only way to know! Otherwise LGTM! There're some cosmetic simplification on the compiler code that I wanted (like the loop for generating |
FFI Proposal
Done
Not included: