Skip to content

Commit 5349ff3

Browse files
committed
feat(analysis): add annotations for let and eieio slots
1 parent 5610e0e commit 5349ff3

File tree

6 files changed

+157
-51
lines changed

6 files changed

+157
-51
lines changed

elsa-analyser.el

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,25 @@ The BINDING should have one of the following forms:
116116
- place ; initial is nil
117117
- (place) ; initial is nil
118118
- (place initial-value)"
119-
(cond
120-
((or (listp binding)
121-
(elsa-form-list-p binding))
122-
(-let [(var source) (elsa-form-sequence binding)]
123-
(when source
124-
(elsa--analyse-form source scope state))
125-
(when (elsa-form-symbol-p var)
126-
(if (not source)
119+
(let* ((annotation (oref binding annotation))
120+
(annotation-type (and annotation
121+
(elsa--make-type (nth 2 annotation)))))
122+
(cond
123+
((or (listp binding)
124+
(elsa-form-list-p binding))
125+
(-let [(var source) (elsa-form-sequence binding)]
126+
(when source
127+
(elsa--analyse-form source scope state))
128+
(when (elsa-form-symbol-p var)
129+
(if (not source)
130+
(elsa-variable
131+
:name (oref var name) :type (elsa-type-nil))
127132
(elsa-variable
128-
:name (oref var name) :type (elsa-type-nil))
129-
(elsa-variable
130-
:name (oref var name) :type (oref source type))))))
131-
((elsa-form-symbol-p binding)
132-
(elsa-variable :name (oref binding name) :type (elsa-make-type nil)))
133-
(t nil)))
133+
:name (oref var name) :type (or annotation-type
134+
(oref source type)))))))
135+
((elsa-form-symbol-p binding)
136+
(elsa-variable :name (oref binding name) :type (elsa-type-nil)))
137+
(t nil))))
134138

135139
(defun elsa--analyse:let (form scope state)
136140
(let* ((new-vars nil)
@@ -1153,13 +1157,6 @@ SCOPE and STATE are the scope and state objects."
11531157
11541158
FORM is a result of `elsa-read-form'."
11551159
(oset form reachable (elsa-state-get-reachability state))
1156-
(when-let ((annotation (oref form annotation)))
1157-
(cond
1158-
((eq (car annotation) 'var)
1159-
(when-let ((var (elsa-scope-get-var scope (cadr annotation))))
1160-
;; update the type in the current scope
1161-
(oset var type (eval `(elsa-make-type ,@(nthcdr 3 annotation))))))))
1162-
11631160
(cond
11641161
((elsa-form-float-p form) (elsa--analyse-float form scope state))
11651162
((elsa-form-integer-p form) (elsa--analyse-integer form scope state))

elsa-extension-eieio.el

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,16 @@
140140
(mapcar
141141
(lambda (slot)
142142
(let* ((slot-name (elsa-get-name slot))
143+
(slot-annotation (oref slot annotation))
143144
(type-form (map-elt (elsa-cdr slot) :type))
144145
(type-lisp (elsa-form-to-lisp type-form))
145-
(elsa-type (or (and type-lisp
146-
(or (elsa--cl-type-to-elsa-type type-lisp)
147-
(elsa-type-mixed)))
148-
(elsa-type-mixed)))
146+
(elsa-type (or
147+
(and slot-annotation
148+
(elsa--make-type (nth 2 slot-annotation)))
149+
(and type-lisp
150+
(or (elsa--cl-type-to-elsa-type type-lisp)
151+
(elsa-type-mixed)))
152+
(elsa-type-mixed)))
149153
(accessor (map-elt (elsa-cdr slot) :accessor))
150154
(initarg (map-elt (elsa-cdr slot) :initarg)))
151155

elsa-form.el

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,34 @@ This only makes sense for the sequence forms:
7373
(cl-defgeneric elsa-form-sequence-p (_this)
7474
nil)
7575

76+
(defun elsa-form-find-parent (form pred)
77+
"Find first parent of FORM satisfying predicate PRED."
78+
(declare (indent 1))
79+
(let ((parent form))
80+
(while (and parent (not (funcall pred parent)))
81+
(setq parent (and (slot-boundp parent 'parent)
82+
(oref parent parent))))
83+
parent))
84+
85+
(defun elsa-form-find-child (form pred)
86+
"Find first child of FORM satisfying predicate PRED."
87+
(declare (indent 1))
88+
(catch 'found
89+
(elsa-form-visit form
90+
(lambda (child)
91+
(when (funcall pred child)
92+
(throw 'found child))))))
93+
94+
(defun elsa-locate-dominating-form (form name)
95+
"Starting at FORM, look up parent forms for form with NAME.
96+
97+
NAME can be a symbol or list of symbols, in which case matching
98+
any symbol from the list will stop the search."
99+
(elsa-form-find-parent
100+
form
101+
(lambda (parent)
102+
(if (listp name)
103+
(memq (elsa-get-name parent) name)
104+
(eq (elsa-get-name parent) name)))))
105+
76106
(provide 'elsa-form)

elsa-reader.el

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,15 @@ STATE is Elsa local state."
607607
(elsa-state-add-defvar state
608608
(elsa-defvar :name (elsa-get-name (cadr (oref reader-form sequence)))
609609
:type (eval `(elsa-make-type ,@(cddr comment-form))))))
610+
;; slot annotation in defclass
611+
((and (elsa-locate-dominating-form reader-form 'defclass)
612+
(elsa-form-function-call-p reader-form (car comment-form)))
613+
(oset reader-form annotation comment-form))
614+
;; annotations for let bindings
615+
((and (elsa-locate-dominating-form
616+
reader-form '(let let* when-let when-let* if-let if-let*))
617+
(elsa-form-function-call-p reader-form (car comment-form)))
618+
(oset reader-form annotation comment-form))
610619
((eq annotation-name 'var)
611620
(oset reader-form annotation comment-form))
612621
(t
@@ -681,37 +690,42 @@ for the analysis."
681690
((functionp form) (elsa--read-function form state))
682691
(t (error "Invalid form")))))
683692
(elsa--set-line-and-column reader-form)
684-
;; check if there is a comment atached to this form
685-
;; TODO: this is really inefficient because it checks the same
686-
;; line multiple times. We should only do this parsing for the
687-
;; first form on a line.
688-
(save-excursion
689-
(goto-char (oref reader-form start))
690-
(forward-line -1)
691-
(skip-chars-forward " \t\n\r")
692-
(let ((line-end (line-end-position)))
693-
(when (and (re-search-forward
694-
(rx "(" (+ (+? (or (syntax word) (syntax symbol))) (+ space)) ":: ")
695-
line-end t)
696-
(nth 4 (syntax-ppss)))
697-
;; we are inside a comment and inside a form starting with
698-
;; (elsa
699-
(search-backward "(")
700-
(let ((comment-form (read (current-buffer))))
701-
;; we must end on the same line, that way we can be sure
702-
;; the entire read form was inside a comment.
703-
(when (<= (point) line-end)
704-
;; handle defun type declaration
705-
(elsa--process-annotation reader-form comment-form state))))))
706693
reader-form))
707694

708695
(defun elsa-read-form (state)
709696
"Read form at point."
710697
(while (forward-comment 1))
711698
(unless (eobp)
712699
(let* ((form (save-excursion
713-
(read (current-buffer)))))
714-
(while (forward-comment 1))
715-
(elsa--read-form form state))))
700+
(read (current-buffer))))
701+
(elsa-form (progn
702+
(while (forward-comment 1))
703+
(elsa--read-form form state))))
704+
705+
706+
;; Process annotations attached to subforms in this top-level
707+
;; form. We have to do it here after the entire form is read to
708+
;; be able to make use of parent/previous relations.
709+
(elsa-form-visit elsa-form
710+
(lambda (ef)
711+
(save-excursion
712+
(goto-char (oref ef start))
713+
(forward-line -1)
714+
(skip-chars-forward " \t\n\r")
715+
(let ((line-end (line-end-position)))
716+
(when (and (re-search-forward
717+
(rx "(" (+ (+? (or (syntax word) (syntax symbol))) (+ space)) ":: ")
718+
line-end t)
719+
(nth 4 (syntax-ppss)))
720+
;; we are inside a comment and inside a form starting with
721+
;; (elsa
722+
(search-backward "(")
723+
(let ((comment-form (read (current-buffer))))
724+
;; we must end on the same line, that way we can be sure
725+
;; the entire read form was inside a comment.
726+
(when (<= (point) line-end)
727+
;; handle defun type declaration
728+
(elsa--process-annotation ef comment-form state))))))))
729+
elsa-form)))
716730

717731
(provide 'elsa-reader)

tests/test-analysis-form-let.el

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
;; -*- lexical-binding: t -*-
2+
(require 'buttercup)
3+
4+
(require 'elsa-test-helpers)
5+
(require 'elsa-analyser)
6+
7+
(describe "Form analysis"
8+
9+
(describe "let"
10+
11+
(xdescribe "return type analysis")
12+
13+
(describe "introducing variables to scope"
14+
15+
(it "should update local scope with let-bound variables"
16+
(let ((state (elsa-state)))
17+
(spy-on 'elsa-scope-add-var :and-call-through)
18+
(elsa-test-with-analysed-form "(let ((slot 1)))" form
19+
:state state
20+
(expect 'elsa-scope-add-var
21+
:to-have-been-called-with
22+
(spy-arg-matcher #'elsa-scope-p)
23+
(spy-arg-matcher
24+
(lambda (x)
25+
(and (elsa-variable-p x)
26+
(eq (oref x name) 'slot)
27+
(elsa-type-accept (elsa-type-int) (oref x type))))))))))
28+
29+
(describe "annotations"
30+
31+
(it "should set type of binding to type from annotation"
32+
(elsa-test-with-analysed-form ";; (slot :: number)\n(let ((slot (mixed))) slot)"
33+
form
34+
(expect (elsa-nth 2 form)
35+
:to-accept-type (elsa-type-number)))))))

tests/test-elsa-extension-eieio.el

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,30 @@
108108
(it "should signal error if the slot type does not accept value"
109109
(elsa-test-with-analysed-form "|(progn (defclass a () ((name :type string))) (oset (a) name 2))" form
110110
:errors-var errors
111-
(expect (car errors) :message-to-match "Property `name' can not accept type `(const 2)', has type `string'")))))))
111+
(expect (car errors) :message-to-match "Property `name' can not accept type `(const 2)', has type `string'")))))
112+
113+
(describe "annotations"
114+
115+
(it "can annotate first slot if aligned with the surrounding list"
116+
(elsa-test-with-analysed-form "(defclass foo ()\n ;; (slot :: number)\n ((slot)))" form
117+
:state-var state
118+
(expect (elsa-get-type (elsa-get-slot (elsa-state-get-defclass state 'foo) 'slot))
119+
:to-accept-type (elsa-type-number))))
120+
121+
(it "can annotate first slot if annotated under the surrounding list"
122+
(elsa-test-with-analysed-form "(defclass foo ()\n (\n ;; (slot :: number)\n (slot)))" form
123+
:state-var state
124+
(expect (elsa-get-type (elsa-get-slot (elsa-state-get-defclass state 'foo) 'slot))
125+
:to-accept-type (elsa-type-number))))
126+
127+
(it "can annotate second slot"
128+
(elsa-test-with-analysed-form "(defclass foo ()\n ((slot)\n ;; (slot2 :: number)\n (slot2)))" form
129+
:state-var state
130+
(expect (elsa-get-type (elsa-get-slot (elsa-state-get-defclass state 'foo) 'slot2))
131+
:to-accept-type (elsa-type-number))))
132+
133+
(it "overrides native slot type if provided"
134+
(elsa-test-with-analysed-form "(defclass foo ()\n ;; (slot :: number)\n ((slot :type string)))" form
135+
:state-var state
136+
(expect (elsa-get-type (elsa-get-slot (elsa-state-get-defclass state 'foo) 'slot))
137+
:to-accept-type (elsa-type-number)))))))

0 commit comments

Comments
 (0)