jscl-project / jscl

A Lisp-to-JavaScript compiler bootstrapped from Common Lisp

Home Page:https://jscl-project.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Freeze literal values

davazp opened this issue · comments

Currently, literal values are mutable. For example, in

(defun f ()
  (let ((v #(1)))
    (incf (aref v 0))
    v))

f will modify the literal vector, which is always the same object. Making this function stateful. Some implementations mark literal values as read-only to avoid that, as SBCL for example, which will trigger a warning like

; in: DEFUN F
;     (INCF (AREF X 0))
; --> LET* FUNCALL SB-C::%FUNCALL 
; ==>
;   ((SETF AREF) #:NEW1 #:X0 0)
; 
; caught WARNING:
;   Destructive function (SETF AREF) called on constant data: #(1)
;   See also:
;     The ANSI Standard, Special Operator QUOTE
;     The ANSI Standard, Section 3.2.2.3
; 
; compilation unit finished
;   caught 1 WARNING condition
WARNING: redefining COMMON-LISP-USER::F in DEFUN

and will ignore the mutation completely. We could achieve something similar with Object.freeze.

See #345 (comment)

Object.freeze will of course not signal when an attempt is made to mutate a property.

However, if you were to target ECMA6, Proxy Objects would allow you to intercept this (although they wrap a single reference only, so you would have to deep-copy the whole list this way).

Proxy does have a noticeable performance hit, so perhaps if you did go this route it perhaps should not happen when you (declare (optimize safety 1)).

commented

Object.freeze will of course not signal when an attempt is made to mutate a property.

This is not exactly the case.
as an example:
image

commented

Another question is whether it is necessary to change the property # ()?

In the example I described earlier, it is enough to use a local type declaration:

(let ((v (vector))
...)

or

(let ((v (make-array 0))
...)

Such a property is a tricky feature.

Which can be used in unexpected ways. This is a plus. And about the minuses we must clearly say. Who uses it, knows what he is doing and what the result may be.

Note that CCL when compiling the function, no message is display , like SBCL.

Resume:
Tend to think of it as a useful tricky feature for conscious use.

Good catch @vlad-km, I think the difference I was noticing was that I was not executing that freeze and mutation in strict mode (which jscl uses by default iirc). This is of course great news in terms of performance and code complexity.

Bikeshedding your example, it is acceptable to me for #() to be an immutable singleton, returned by the relevant constructors. I would assume (eq #() (vector)) as an implementation detail here.

commented

Resume:
Tend to think of it as a useful tricky feature for conscious use.

For example

(defmacro genmakv (name catch &rest initials)
  (let ((g!name (gensym (symbol-name name)))
        (c!name (intern (jscl::concat "MAK-" (symbol-name name))))
        (a!name (intern (jscl::concat "PUT-" (symbol-name name))))
        (h!code)
        (ini!code))
    (if catch
        (setq h!code `(handler-case 
                       (progn ((jscl::oget ,g!name "push") value) value)
                       (error (msg)
                              (values))))
      (setq h!code `((jscl::oget ,g!name "push") value)))
    (if initials
        (setq ini!code
              `(progn
                 (map 'nil (lambda (x) ((jscl::oget ,g!name "push") x)) ',initials))))

    `(let ((,g!name #()))
       ,ini!code
       (defun ,c!name (&key freeze)
         (if freeze (#j:Object:freeze ,g!name))
         ,g!name) 
       (defun ,a!name (value)
         ,h!code
         value))))

Example

CL-USER> (genmakv v0 t 1 2 3)
PUT-V0
CL-USER> (genmakv v1 nil a b c)
PUT-V1
CL-USER> (mak-v0)
#(1 2 3)
CL-USER> (mak-v1)
#(A B C)
CL-USER> (eq (mak-v0)(mak-v1))
NIL
CL-USER> (put-v1 (lambda (x) x))
#<FUNCTION>
CL-USER> (put-v0 (list 'a 'b 'c))
(A B C)
CL-USER> (mak-v0)
#(1 2 3 (A B C))
CL-USER> (mak-v1)
#(A B C #<FUNCTION>)
CL-USER> (mak-v1 :freeze t)
#(A B C #<FUNCTION>)
CL-USER> (put-v1 (lambda (x) x))
ERROR: Cannot add property 4, object is not extensible
CL-USER> (mak-v0 :freeze t)
#(1 2 3 (A B C))
CL-USER> (put-v0 (list 'a 'b 'c))
(A B C)
CL-USER> (mak-v0)
#(1 2 3 (A B C))
CL-USER> (mak-v1)
#(A B C #<FUNCTION>)
commented

Do you want to singleton ?

Singleton

(let ((guard nil))
  (defmacro mak-singl (name catch &rest init)
    (let ((code))
      (if guard (error "There must be only one :~a" guard))
      (setq guard `,name)
      (setq code 
            `(progn
               (genmakv ,name ,catch ,@init)))
      `,code)))
CL-USER> (mak-singl mclaud t 1 2 3)
PUT-MCLAUD
CL-USER> (mak-singl victor t 1 2 3)
ERROR: There must be only one :MCLAUD
CL-USER> (mak-mclaud)
#(1 2 3)
commented

I would assume (eq #() (vector)) as an implementation detail here.

#() cannot be equal (vector)

Ah, #() is not a string nor a bit-vector, I understand. I'd forgotten that oddity with equal vs equalp.