jscl-project / jscl

A Lisp-to-JavaScript compiler bootstrapped from Common Lisp
https://jscl-project.github.io
GNU General Public License v3.0
885 stars 109 forks source link

Freeze literal values #350

Open davazp opened 5 years ago

davazp commented 5 years ago

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 https://github.com/jscl-project/jscl/issues/345#issuecomment-450793913

mseddon commented 5 years ago

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)).

vlad-km commented 5 years ago

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

vlad-km commented 5 years ago

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.

mseddon commented 5 years ago

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.

vlad-km commented 5 years ago

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>)
vlad-km commented 5 years ago

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)
vlad-km commented 5 years ago

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

() cannot be equal (vector)

mseddon commented 5 years ago

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