I have a Scheme interpreter written in JavaScript called LIPS, and I have a define-class macro which is using the ES5 prototype system. But I have a problem in creating super().
I'm trying to create a PoC of super inside a class. So I can create Web Components:
This is my code:
<script src="https://cdn.jsdelivr.net/npm/lips@beta/dist/lips.js"></script>
<script type="text/x-scheme" bootstrap>
;; MODIFICATION OF define-class MACRO FROM STANDARD LIBRARY
(define (%class-constructor expr parent)
"(%class-constructor expr)
Version of %class-lambda that is used as constructor."
(let ((args (gensym 'args))
(ret (gensym 'ret)))
`(lambda ,args
(let ((,ret (apply ,(cadr expr) (unbind this) ,args)))
(if (and (object? ,parent) (instanceof ,parent ,ret))
,ret
;; we return this to solve issue when constructor
;; return a promise
;; ref: https://stackoverflow.com/a/50885340/387194
this)))))
(define-macro (define-class name parent . body)
"(define-class name parent . body)
Defines a class - JavaScript function constructor with prototype.
parent needs to be class, constructor function, or #null
usage:
(define-class Person Object
(constructor (lambda (self name)
(set-obj! self '_name name)))
(hi (lambda (self)
(display (string-append self._name \" says hi\"))
(newline))))
(define jack (new Person \"Jack\"))
(jack.hi) ; prints \"Jack says hi\""
(let ((g:parent (gensym)))
(let iter ((functions '()) (constructor '()) (lst body))
(if (null? lst)
`(begin
(define ,name ,(if (null? constructor)
`(lambda ())
(%class-constructor constructor parent)))
(set-obj! ,name (Symbol.for "__class__") true)
(let ((,g:parent ,parent ))
(if (not (null? ,g:parent))
(begin
(set-obj! ,name 'prototype (Object.create (. ,g:parent 'prototype)))
(set-obj! (. ,name 'prototype) 'constructor ,name))))
(set-obj! ,name '__name__ ',name)
,@(map (lambda (fn)
`(set-obj! (. ,name 'prototype)
,(%class-method-name (car fn))
,(%class-lambda fn)))
functions))
(let ((item (car lst)))
(if (eq? (car item) 'constructor)
(iter functions item (cdr lst))
(iter (cons item functions) constructor (cdr lst))))))))
;; MAIN CODE
(define-class CurrentDate HTMLElement
(constructor (lambda (self)
(Reflect.construct HTMLElement #() CurrentDate)))
(connectedCallback (lambda (self)
(let ((now (new Date)))
(set! self.textContent (now.toLocaleDateString))))))
(customElements.define "current-date" CurrentDate)
</script>
<current-date></current-date>
The Scheme code just uses ES5 prototypes to build a class. It's just a 1-1 mapping of JavaScript code in Scheme.
I keep getting the error:
Uncaught TypeError: Failed to construct 'HTMLElement': Illegal constructor
(The stack snippet doesn't show the correct error; you need to open DevTools)
I was testing the code from: What is the ES5 way of writing web component classes?
The code throws at this expression:
(apply ,(cadr expr) this ,args)
And (cadr expr) is:
(lambda (self) (Reflect.construct HTMLElement #() CurrentDate))
The lambda macro executes a bit of code, but Reflect.construct doesn't need to be the first expression like with super. And the only thing that the custom component needs to do is to return a DOM node from calling the HTMLElement constructor.
This executes without any errors:
function HelloElement() {
console.log('x');
const ret = make();
console.log(ret);
console.log(ret instanceof HTMLElement);
return ret;
}
function make() {
return Reflect.construct(HTMLElement, [], HelloElement);
}
customElements.define('hello-element', HelloElement);
<hello-element id="example"></hello-element>
I also tried to call JavaScript from Scheme but got the same error:
(define js-eval self.eval)
(define-macro (super class)
`((js-eval "(unbind, constructor) => Reflect.construct(HTMLElement, [], unbind(constructor));")
,unbind ,class))
(define-class CurrentDate HTMLElement
(constructor (lambda (self)
(super CurrentDate))))
In LIPS, functions have what I call soft binding (this context), which you can unbind (get the original function).
I was able to trigger the same error in JavaScript by adding:
return Promise.resolve(ret);
LIPS sometimes may return async code that I wanted to get rid of, but after doing a quick test, the define-class doesn't return a promise.
I can trigger the same error when I call (new CurrentDate) in Scheme and new HelloElement() in the JavaScript example. But I don't call new in my Scheme? The code is triggered by HTML code.
What may be other possible issues with my code? And the possible root cause of the error?