Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: type annotations docs #96

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 2 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,59 +232,8 @@ code. While at many places the types can be inferred there are
places, especially in user-defined functions, where we can not guess
the correct type (we can only infer what we see during runtime).

Users can annotate their `defun` definitions like this:

``` emacs-lisp
;; (elsa-pluralize :: String -> Int -> String)
(defun elsa-pluralize (word n)
"Return singular or plural of WORD based on N."
(if (= n 1)
word
(concat word "s")))
```

The `(elsa-pluralise :: ...)` inside a comment form provides
additional information to the Elsa analysis. Here we say that the
function following such a comment takes two arguments, string and int,
and returns a string.

The syntax of the type annotation is somewhat modeled after Haskell
but there are some special constructs available to Elsa

Here are general guidelines on how the types are constructed.

- For built-in types with test predicates, drop the `p` or `-p` suffix and PascalCase to get the type:
- `stringp` → `String`
- `integerp` → `Integer` (`Int` is also accepted)
- `markerp` → `Marker`
- `hash-table-p` → `HashTable`
- A type for everything is called `Mixed`. It accepts anything and is
always nullable. This is the default type for when we lack type
information.
- Sum types can be specified with `|` syntax, so `String | Integer` is
a type accepting both strings or integers.
- Cons types are specified by prefixing wrapping the `car` and `cdr`
types with a `Cons` constructor, so `Cons Int Int` is a type where
the `car` is an int and `cdr` is also an int, for example `(1 . 3)`.
- List types are specified by wrapping a type in a vector `[]`
constructor, so `[Int]` is a list of integers and `[String | Int]`
is a list of items where each item is either a string or an integer.
A type constructor `List` is also supported.
- Function types are created by separating argument types and the
return type with `->` token.
- To make variadic types (for the `&rest` keyword) add three dots
`...` after the type, so `String... -> String` is a function taking
any number of strings and returning a string, such as `concat`.
Note: a variadic type is internally just a list of the same base
type but it has a flag that allows the function be of variable
arity. A `Variadic` type constructor is also available to construct
complex types.
- To mark type as nullable you can attach `?` to the end of it, so
that `Int?` accepts any integer and also a `nil`. A `Maybe` type
constructor is also available to construct complex types.

Some type constructors have optional arguments, for example writing
just `Cons` will assume the `car` and `cdr` are of type `Mixed`.
Read the type annotations [documentation](./docs/type-annotations.org)
for more information on how to write your own types.

# How can I contribute to this project

Expand Down
238 changes: 238 additions & 0 deletions docs/type-annotations.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
* Type annotations

In Elisp users are not required to provide type annotations to their
code. While at many places the types can be inferred there are
places, especially in user-defined functions, where we can not infer
the correct type.

Therefore Elsa provides users with the option to annotate their
function definitions. The annotations are placed in the body of the
function inside a =declare= form:

#+BEGIN_SRC elisp
(defun add-one (x)
(declare (elsa (int) int))
(1+ x))
#+END_SRC

The =declare= form starts with =elsa= followed by one or two values. If
two are provided, first must be a list which is the list of input
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to drop the parens around arguments and just have a sequence of types where the last one is automatically return type.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that would be awkward in case you want a void function or rather, ignore the return value. Requiring parentheses around arguments would model this naturally (nothing would follow them) whereas without them you'd always need to specify the last thing. Also, what if you have an empty argument list, but a meaningful return value (like, emacs-version)? How would you tell both apart without parentheses around the arguments? If you need a better separator of arguments and return value, I like : in the Wirth family, an arrow would be another option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can always inspect the argument list, so incase of (point) you know the int is the return value since there is no argument.

I have never considered void functions, such a thing does not really exist in Elisp does it? At the very least you always return nil or some such.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course there are void functions, everything you call for its side effect, like save-buffer (returns nil) or revert-buffer (returns t). What I'm pointing out is that we could avoid ambiguity with sexp-syntax.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A void function returns nothing. Every function in elisp returns something.

Doing (string-to-number (save-buffer)) would error. We don't like errors, therefore we must always consider the return type... the above code is silly but might be a result of a failed refactoring or a mistake and we want to emit a warning there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing (string-to-number (save-buffer)) would error.

I agree and my conclusion is that we should consider the void return type. The result of a function returning void should not be passed as argument to another function nor stored into a variable. When calling a void function is the last statement of a function, then that function itself returns void.

I agree with @Fuco1 that in ELisp, every function returns something. But not all functions are advertised as returning something: those functions should have the void return type.

Consider save-buffer for example and consider that no void return type exists. A static analysis could detect that this function returns a bool for example. Then, Elsa should accept code like this (if (save-buffer) ...). My opinion is that this is wrong because the implementation of save-buffer can change tomorrow to return an int instead. This will be an implementation detail until save-buffer docstring describes what the return value is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Void is nasty because it doesn't play well with the type algebra.

What would this function return?

(defun what-is-return-type ()
  (if (we-should-save-buffer) ;; bool
      (save-buffer) ;; void
    (return-time-since-last-save-in-seconds) ;; int
    ))

A type like void | int makes no sense because void basically poisons everything. If you would want to use such a function you would have to test the return type anyway (did I get an int back?). So then the type of save-buffer doesn't really matter.

It's a bit silly example, but the problem is that void is not a type in a sense every other type is a type (i.e. a set). Void is not even an empty set type, it's something outside the hierarchy.

This function

(defun forever-and-ever ()
  (while t t))

also returns void? Or is that something else.

I have this issue #68 to check for unused return values so I would rather go with a "this function should be only used for side-effect" warning but not on the type level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are much more knowledgeable than me in this area. Do what you think is best!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A type like void | int makes no sense because void basically poisons everything.

Some languages (Rust, OCaml) have unit types for this purpose (). In this example, (or void int) tells you that you may get a useful value (int) or you may get a useless one (void). You have to test the return type in order to use the int, because you cannot do anything with void. But that's the point of the type analysis: to make sure your code is correct in all situations.

If you would want to use such a function you would have to test the return type anyway (did I get an int back?). So then the type of save-buffer doesn't really matter.

But it does matter. Consider the alternative: (or bool int), in which case you may freely use the bool return value even though it's undocumented, and may change in the future as argued by @DamienCassou. If it changes to int, then if you previously made use of the bool value your code won't typecheck anymore, but there is value in an annotation to indicate "this value is not relevant and subject to change".

This function
(defun forever-and-ever ()
(while t t))
also returns void? Or is that something else.

Something else: it's the empty type (or bottom type). It indicates that this function never returns, which is different than void as argued above, which means returns something you cannot use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To complete: the empty type may be more tricky to deal with, but in practice may not be that useful. The void unit type however, though by not mean indispensable. is useful.

types and the second is the return type.

If only one is provided it is taken to be the return type and the
function must have no input arguments.

Here are general guidelines on how the types are constructed.

** Built-in types

For built-in types with test predicates, drop the =p= or =-p= suffix to
get the type:

- =stringp= → =string=
- =integerp= → =integer= (=int= is also accepted)
- =markerp= → =marker=
- =hash-table-p= → =hash-table=

Some additional special types:

- =t= stands for =t= and is always true
- =nil= stands for =nil= and is always false
- =bool= is a combination of explicitly =t= or =nil=

** Nullable types

By default types do not accept =nil= values. This helps preventing
errors where you would pass =nil= to a function expecting a value. To
mark a type nullable you can combine it with =nil= type using the [[id:5a21a68a-4df1-4d44-a854-1d9700858a1a][or]]
combinator: =(or string nil)= is a type which takes any string but also
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the rationale for dropping the ? suffix? I liked the fact that it was both expressive and short.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I like it and I would not oppose having it remain. It is a lot nicer to read int? instead of (or int nil).

I don't want to overdo it with the suffixes though. There will also be the generic a* syntax (or some other variant) and then it might matter on how we parse the suffixes (i.e. is a*? the same as a?* or does it even make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can combine * and ? in the same type because what is before * is an abstract name that conveys no meaning by itself. My opinion is that a*? and a?* should trigger an error.

Copy link
Member Author

@Fuco1 Fuco1 Nov 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you know Haskell I can think of this as an analogy of Maybe a, that would be in our syntax a*? (since a* is the generic type constructor). So basically ? is short for (or nil <arg>), similar as one can write (list a*)

a?* probably makes no sense other than a? would be the placeholder and that shouldn't be allowed indeed.

We can also put some things to the front, (e)lisp has a tradition with the & prefix.

Or we can go entirely different way and for example use strings or quoted symbols for generic names, so that it would be "foo" or (list "foo") instead of foo*.

=nil=.

** Most general type

A type for everything is called =mixed=. It accepts anything and is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that "accepts anything" is a bit wrong here: apparently, mixed does not accept [mixed] (see #112).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should so that's a bug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue in #112 is the other way around though. We want an array [Mixed] and we are getting Mixed instead which might not be an array. So this is a mismatch.

always nullable (that means it accepts =nil=). This is the default type
for when we lack type information.

Because a =mixed= type can be anything it itself is only accepted by
=mixed=, so that the following would not type-check:

#+BEGIN_SRC elisp
(defun a-goes-in (a) ;; a is mixed
(declare (elsa (mixed) int))
;; a *might* be an int, but we don't know for sure
(1+ a))
#+END_SRC

** Composite (higher order) types

Composite or higher-order types are types which take other types as
arguments.

Composite types usually correspond to data constructors such as =cons=,
=list=, =vector=...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you specify a plist where odd items are symbols and even items are strings? E.g.,:

`(:background "#583333" :foreground "#29457")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For plists I'm planning to have a special type (plist key value), same for alist.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it will be a bit more involved since you might also want to express what keys exactly are allowed. For that I'm pondering a set type, such that

(set :foo :bar :baz)

Allows any of :foo, :bar, :baz zero or once and nothing else.

Then

(plist (set :foo :bar :baz) string)

Is a type of a plist with string values and keys :foo, :bar, :baz.

Now this still does not allow us to specify what value type we want for what key type exactly. In javascript (flow/typescript) they do it simply by enumerating:

{
  name: string,
  age: int
}

so maybe we can also have such a syntax:

(plist :foo string :bar int)

I'm not decided yet here what is the best way. Ideally somehow combine it all for maximal flexibility.


- =(cons a b)= where =a= is the type of =car= and =b= is the type of =cdr=. If
the =car= and =cdr= can be anything write =(cons mixed mixed)= or simply
=cons= for short.
- =(list a)= where =a= is the type of items in the list. If the list can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this syntax, you can't distinguish between:

  1. a list with unknown length and whose elements are all of the same type, and
  2. a list with length 1

I suggest (list a) to represent case 2 and (list a...) to represent case 1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I never actually had in mind to have lists of finite/known length with the type constructor list. I was thinking of using tuple as short for (cons a (cons b (cons c nil))) which is a list of three things of types a, b, and c.

Soy idea was (tuple a b c) and list would be specifically for a list (of unknown length) with a repeated value.

I think I did not write it down anywhere, so I should. Does this make sense to you?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using tuple probably makes more sense than to reuse list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had another thought at tuple and I'm not sure about it anymore. The tuple solution won't let you express that the first items are of some known type where the rest is unknown. Some use cases:

  • the parameters to format ((list string mixed...))
  • tagged lists where the first element is a symbol indicating how to interpret what follows ((list symbol mixed...))
  • poor-man structures (e.g., a person could be described as a name, a birthdate and children with (string date person...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's also a bit related to structures like plists or alists, where alists are lists of lists where the car is something. These are good points.

In the future I also want to support "constant" types, so you can explicitly require specific values, for example, using your notation

(list (or 'regex 'syntax) string...)

could mean "this is a list of strings which should be interpreted as regular expressions if the car is a constant 'regex, or as syntax descriptors if 'syntax.

The constants will probably have a constructor const similar to how defcustom does it. Maybe we can use that syntax? The defcustom type (repeat string) is a list of strings ("a" "b" "c" "d" ...) where (list string integer symbol) is a list of three things ("foo" 1 'bar).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wanted to reuse the defcustom stuff so I dived into it and I realized that I really like the constructor-as-type principle.

There's also this cool pattern-matching library which uses a similar concept: https://github.com/VincentToups/shadchen-el

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So actually maybe we can lift the list-rest idea from there. Or allow a &rest keyword inside list such that (list &rest int) is a list of integers of any length and (list string bool &rest int) requires a string, a bool and then a bunch of ints.

The same can work in parallel with vector such that (vector string bool &rest int) is... you get the idea.

This might be it! :D

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this


Lists are made by chaining cons cells together and leaving =nil= in the
last =cdr= of the last =cons=. Therefore =(cons a (cons b (cons c nil)))=
can be understood as a list of exactly three items of type =a=, =b=, and
=c=.

Elisp provides a data constructor =list= to make creating such lists
easier. In analogy to =cons= we provide a =list= type which corresponds
to this, such that =(list "foo" 1 :bar)= has type =(list string int
keyword)=.

In general =(list a b c ...)= is a list type of known length where =a=, =b=,
=c=... are types of the items on the 0th, 1st, 2nd... places. It's a
proper list so the last =cons='s =cdr= is a =nil=.

Sometimes we need to express lists of unknown length which contain
items of the same thing repeatedly. For example =(3 2 5 1 5 6 7 5)= is
a list of numbers. Because we don't know its length beforehand we
provide a special syntax to express that a type repeats forever inside
the list.

The type =(list a b c . rest-type)= is a type similar to =list= except the
last =rest-type= repeats forever. Therefore, we expect a known number
of items of types =a=, =b=, =c= and then any number of items of =rest-type=.
If we only speficy one type it is automatically the rest type, so that
=(list . rest-type)= holds any number of items of =rest-type=.

If the list can hold any number of anything, write =(list . mixed)= or
simply =list= for short.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it very much, good job. I'm not so sure about this phrasing though:

If we only speficy one type it is automatically the rest type, so that (list . rest-type) holds any number of items of rest-type.

If the reader doesn't pay attention, she might understand that you are talking about (list rest-type) and not (list . rest-type). May I suggest this phrasing instead?

If we specify no type before the dot . as in (list . rest-type), this means the list holds any number of items of rest-type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, I wasn't so sure myself but couldn't phrase it better.

I'm also thinking about maybe introducing some other shorter notation without the dot as that is confusing and we maybe might want to use it for dotted lists (albeit those are really rare so I wouldn't care about those very much).

Something like (list* a) where the star denotes a repetition like in formal grammars and stuff. I'd probably move the generic * marker before the type then.

hold anything, write =(list mixed)= or simply =list= for short.
- =(vector a)= where =a= is the type of items in the vector. If the
vector can hold anything, write =(vector mixed)= or simply =vector= for
short.
- =(hash-table k v)= where =k= is the key type and =v= is the value type.
If the hash table can hold anything, write =(hash-table mixed mixed)=
or simply =hash-table= for short.

** Constant types

A constant type always holds a specific value. Functions often take
flags which can be symbols such as ='append= or ='prepend= or constant
strings.

To specify a constant type wrap the value in a =(const)= constructor, so
that:

- =(const a)= is the symbol =a= (when used in a lisp program you would
pass it around as ='a=),
- =(const 1)= is the integer =1=,
- =(const "foo")= is the string ="foo"=.

** Function types

Function types are types of functions. They have input argument types
and a return type.

The function =add-one= from the introduction has a function type =(function
(int) int)= which means it takes in one integer and returns an integer.

A =lambda= form =(lambda (x) (number-to-string x))= has function type
=(function (number) string)=, it takes in a number and returns a string.

A function can have a function type as one of its input types. An
Fuco1 marked this conversation as resolved.
Show resolved Hide resolved
example of such a function is =mapcar= which takes a function and a list
and applies the function to every item of the list.

#+BEGIN_SRC elisp
(defun app (fn)
"Apply FN to the list (1 2 3 4)"
(declare (elsa ((function (number) number)) (list number)))
(mapcar fn (list 1 2 3 4)))

(app (lambda (x) (* x x)))
#+END_SRC

The =app= function requires that we pass in a function which processes a
Fuco1 marked this conversation as resolved.
Show resolved Hide resolved
number into a number and returns a list of numbers.

** TODO Generic types

Generic types are types where some of the type arguments are variable.
Both basic and composite types can be turned into generic types.

*** Motivation

An example of a generic function is =identity=. This function takes
anything in and anything out. We could therefore give it a type
annotation =(elsa (mixed) mixed)=.

However, we can do better! We know that whatever was passed in will
be returned and so the type actually must be the same. The =(elsa
(mixed) mixed)= signature allows us to pass in an =int= and it can return
back a =string= no problem and so it would not catch a huge number of
possible errors.

What we want to express here is "X comes in, X comes out".
Fuco1 marked this conversation as resolved.
Show resolved Hide resolved

*** Syntax

The syntax for generic types is "generic type name" + =*= suffix. Any
string can be used for the generic type name, but customarily
single-letter names are used.

For the above mentioned identity function we therefore write the type
as =(elsa (a*) a*)= where =a*= stands for a generic type =a=.

A function such as =car= can be typed as follows:

#+BEGIN_SRC elisp
(elsa ((cons a* b*)) a*)
#+END_SRC

It takes a cons with =a= in the =car= and =b= in the =cdr= and return the =car=
which is of type =a= , whatever that happens to be.

** Optional types

If a function can take optional arguments we need to convert them into
a nullable type =(or type nil)=.

#+BEGIN_SRC elisp
(defun drop-items (list &optional n)
"Drop first item of LIST or N items if N is provided."
(declare (elsa ((list a*) (or int nil)) (list a*)))
(setq n (or n 1))
(dotimes (_ n list)
(setq list (cdr list))))
#+END_SRC

** Variadic types

If a function can take arbitrary number of arguments we preceed the
last variadic argument with =&rest= marker just as we do in the argument
list.

#+BEGIN_SRC elisp
(defun join (separator &rest strings)
"Join STRINGS with SEPARATOR."
(declare (elsa (string &rest string) string))
(mapconcat 'identity strings separator))
#+END_SRC

** Type combinators
*** Sum types
:PROPERTIES:
:ID: 5a21a68a-4df1-4d44-a854-1d9700858a1a
:END:

Sum types can be specified as a list form starting with =or=, so =(or
string int)= is a type accepting strings or integers.

A sum type is useful if the function internally checks the passed
value and decides what processing to do:

#+BEGIN_SRC elisp
(defun to-number (x)
(declare (elsa ((or int string)) int))
(cond
((numberp x) x)
((stringp x) (string-to-number x))))
#+END_SRC

*** Intersection types

Intersection types can be specified as list form starting with =and=, so
=(and string float)= is a type which is at the same time string and
float (such a type has empty domain, nothing can be string and float
at the same time). Intersection types are used to track impossible
assignments.

#+BEGIN_SRC elisp
;; Such a condition can never evaluate to true
(if (and (stringp x) (integerp x))
"X is both string and int which is impossible, this branch never executes"
"This branch always executes")
#+END_SRC

*** Difference types

Difference types can be specified as list form starting with =diff= so =(diff
mixed string)= is a type which can be anything except a string.

Difference types are useful in narrowing the possible values of variables after conditional checks.

#+BEGIN_SRC elisp
(if (stringp x)
"X is definitely string here"
"X is anything but string here")
#+END_SRC
26 changes: 13 additions & 13 deletions elsa-analyser.el
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

(require 'elsa-typed-builtin)

;; (elsa--arglist-to-arity :: List Symbol | T | String -> Cons Int (Int | Symbol))
;; (elsa--arglist-to-arity :: (function ((or (list symbol) t string)) (cons int (or int symbol))))
(defun elsa--arglist-to-arity (arglist)
"Return minimal and maximal number of arguments ARGLIST supports.

Expand Down Expand Up @@ -39,7 +39,7 @@ number by symbol 'many."
(setq max 'many))
(cons min max)))))

;; (elsa-fn-arity :: Symbol -> Cons Int (Int | Symbol))
;; (elsa-fn-arity :: (function (symbol) (cons int (or int symbol))))
(defun elsa-fn-arity (fn)
(elsa--arglist-to-arity (help-function-arglist fn)))

Expand All @@ -55,12 +55,12 @@ number by symbol 'many."
(defun elsa--analyse-symbol (form scope _state)
(let* ((name (oref form name))
(type (cond
((eq name t) (elsa-make-type T))
((eq name nil) (elsa-make-type Nil))
((eq name t) (elsa-make-type t))
((eq name nil) (elsa-make-type nil))
((-when-let (var (elsa-scope-get-var scope form))
(clone (oref var type))))
((get name 'elsa-type-var))
(t (elsa-make-type Unbound)))))
(t (elsa-make-type unbound)))))
(oset form type type)
(unless (memq name '(t nil))
(oset form narrow-types
Expand Down Expand Up @@ -395,9 +395,9 @@ collects all the arguments, turns &optional arguments into
nullables and the &rest argument into a variadic."
(-let (((min . max) (elsa--arglist-to-arity args)))
(if (eq max 'many)
(-snoc (-repeat min (elsa-make-type Mixed))
(elsa-make-type Variadic Mixed))
(-repeat max (elsa-make-type Mixed)))))
(-snoc (-repeat min (elsa-make-type mixed))
(elsa-make-type &rest mixed))
(-repeat max (elsa-make-type mixed)))))

(defun elsa--analyse-defun-like-form (name args body form scope state)
(let* (;; TODO: there should be an api for `(get name
Expand Down Expand Up @@ -462,7 +462,7 @@ make it explicit and precise."
(progn
(elsa--analyse-form value scope state)
(put var-name 'elsa-type-var (oref value type)))
(put var-name 'elsa-type-var (elsa-make-type Unbound))))))
(put var-name 'elsa-type-var (elsa-make-type unbound))))))

(defun elsa--analyse:defcustom (form scope state)
"Analyze `defcustom'.
Expand All @@ -481,7 +481,7 @@ automatically deriving the type."
;; TODO: check the `:type' form here and also compare if we
;; are doing a valid assignment.
(put var-name 'elsa-type-var (oref value type)))
(put var-name 'elsa-type-var (elsa-make-type Unbound))))))
(put var-name 'elsa-type-var (elsa-make-type unbound))))))

(defun elsa--analyse:defconst (form scope state)
"Analyze `defconst'.
Expand All @@ -503,7 +503,7 @@ If no type annotation is provided, find the value type through
(body (nthcdr 2 sequence))
;; TODO: this should use `elsa--get-default-function-types'
(arg-types (-repeat (length (elsa-form-sequence args))
(elsa-make-type Mixed)))
(elsa-make-type mixed)))
(vars))
(when (elsa-form-list-p args)
(-each-indexed (elsa-form-sequence args)
Expand Down Expand Up @@ -580,7 +580,7 @@ If no type annotation is provided, find the value type through
(not (oref arg quote-type))
(elsa-get-name arg)))

;; (elsa--analyse-normalize-spec :: Bool | List Bool -> Mixed -> List Bool)
;; (elsa--analyse-normalize-spec :: (function ((or bool (list bool)) mixed) (list bool)))
(defun elsa--analyse-normalize-spec (spec form)
"Normalize evaluation SPEC for FORM."
(cond
Expand All @@ -595,7 +595,7 @@ If no type annotation is provided, find the value type through
t)))
(t spec)))

;; (elsa--analyse-macro :: Mixed -> Bool | List Bool -> Mixed -> Mixed -> Mixed)
;; (elsa--analyse-macro :: (function (mixed (or bool (or list bool)) mixed mixed) mixed))
(defun elsa--analyse-macro (form spec scope state)
(setq spec (elsa--analyse-normalize-spec spec form))
(let* ((head (elsa-car form))
Expand Down
2 changes: 1 addition & 1 deletion elsa-check.el
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
;; (elsa-checks :: [Mixed])
;; (elsa-checks :: (list mixed))
(defvar elsa-checks nil)

(defun elsa-check-add (check)
Expand Down
Loading