Skip to content
Merged
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
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,59 @@ git commit -m "Add type declaration file"

Although `tycl-types.d.lisp` is auto-generated during transpilation, it serves as the type declaration file for consumers of your library (similar to `.d.ts` in TypeScript) and should be checked into version control.

## Declaration Files for External Libraries

TyCL can type-check calls to external libraries that are not written in TyCL by using `.d.tycl` declaration files — similar to TypeScript's `.d.ts` files.

### Writing Declaration Files

Create a `tycl-declarations/` directory in your project root and place `.d.tycl` files in it. Each file declares type signatures using standard TyCL syntax, without function bodies:

```lisp
;;; tycl-declarations/cl-functions.d.tycl

(in-package #:common-lisp)

(defun [1+ :number] ([n :number]))
(defun [1- :number] ([n :number]))
(defun [zerop :boolean] ([n :number]))
(defun [length :integer] ([sequence :t]))
(defun [cons :cons] ([car :t] [cdr :t]))
(defun [first :t] ([list :t]))
(defun [reverse :list] ([sequence :list]))
(defun [string-upcase :string] ([string :string]))
```

You can also declare classes:

```lisp
;;; tycl-declarations/my-orm.d.tycl

(in-package #:my-orm)

(defclass db-connection ()
(([host :string] :initarg :host)
([port :integer] :initarg :port)))

(defun [connect db-connection] ([host :string] [port :integer]))
(defun [query :list] ([conn db-connection] [sql :string]))
```

### Discovery and Loading

Declaration files are automatically loaded during transpilation, type checking, and LSP server initialization. TyCL searches the following locations:

1. **Project-local**: `tycl-declarations/` directory relative to the source file or project root
2. **User-global**: `~/.config/tycl/declarations/` for declarations shared across projects

### Notes

- Use **canonical package names** in `in-package` (e.g., `#:common-lisp` instead of `#:cl`), because TyCL stores package names as written and the type checker uses canonical names for lookup.
- For CL functions with `&rest`, `&optional`, or `&key` parameters, only declare the required parameters — TyCL validates argument count against the declared parameter list.
- Symbols starting with `<` or `>` cannot be used in `[...]` annotations because those characters are reserved for type parameter syntax `<T>`.

See the [sample project](sample/tycl-declarations/) for a working example.

## Custom Macro Support

TyCL supports custom macros through a hook mechanism. This allows extracting type information from project-specific macro definitions.
Expand Down Expand Up @@ -469,7 +522,8 @@ When the LSP server starts, it performs the following initialization:

1. **`.asd` file discovery**: Scans the workspace root for `.asd` files
2. **Full transpilation**: If `.asd` files with `tycl-system` definitions are found, all `.tycl` files in those systems are transpiled to generate `tycl-types.d.lisp`. This runs unconditionally regardless of whether `tycl-types.d.lisp` already exists, ensuring type information is always up-to-date.
3. **Type information loading**: Loads `tycl-types.d.lisp` files from the workspace to populate the type cache
3. **Declaration file loading**: Loads `.d.tycl` files from `tycl-declarations/` directories for external library type information
4. **Type information loading**: Loads `tycl-types.d.lisp` files from the workspace to populate the type cache

This ensures that LSP features (hover, completion, diagnostics) have complete type information available from the first interaction.

Expand Down Expand Up @@ -529,6 +583,7 @@ tycl/
│ ├── emacs/ # Emacs tycl-mode
│ └── vscode/ # VS Code extension
├── sample/ # Sample project using ASDF integration
│ └── tycl-declarations/ # Declaration files for external libraries
└── docs/ # Documentation
├── design.md # Design specification
├── asdf.md # ASDF extension design
Expand Down
6 changes: 3 additions & 3 deletions sample/src/math.tycl
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@

(defun [add num] ([x num] [y num])
"Add two integers"
(+ x y))
[(+ x y) num])

(defun [multiply num] ([x num] [y num])
"Multiply two integers"
(* x y))
[(* x y) num])

(defun [factorial num] ([n num])
"Calculate factorial recursively"
(if (<= n 1)
1
(* n (factorial (1- n)))))
[(* n (factorial (1- n))) num]))

(defun [safe-divide nullable-num] ([x num] [y num])
"Divide x by y, returning nil on division by zero"
Expand Down
62 changes: 62 additions & 0 deletions sample/tycl-declarations/cl-functions.d.tycl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
;;; -*- mode: lisp -*-
;;;; Type declarations for Common Lisp standard library functions
;;;; Used by sample-project to enable type checking on CL function calls
;;;;
;;;; This is a .d.tycl file - it declares types for external functions
;;;; that are not written in TyCL, similar to TypeScript's .d.ts files.
;;;;
;;;; Note: Symbols starting with < or > cannot be used in [...] annotations
;;;; because those characters are reserved for type parameter syntax <T>.
;;;;
;;;; Note: CL functions with &rest/&optional/&key parameters should only
;;;; declare the required parameters, since TyCL's type checker validates
;;;; argument count against the declared parameter list.

(in-package #:common-lisp)

;;; ============================================================
;;; Arithmetic
;;; ============================================================

(defun [+ :number] ([a :number] [b :number]))
(defun [- :number] ([a :number] [b :number]))
(defun [* :number] ([a :number] [b :number]))
(defun [/ :number] ([a :number] [b :number]))
(defun [1+ :number] ([n :number]))
(defun [1- :number] ([n :number]))

;;; ============================================================
;;; Predicates
;;; ============================================================

(defun [zerop :boolean] ([n :number]))
(defun [null :boolean] ([obj :t]))
(defun [not :boolean] ([obj :t]))
(defun [numberp :boolean] ([obj :t]))
(defun [stringp :boolean] ([obj :t]))
(defun [listp :boolean] ([obj :t]))

;;; ============================================================
;;; List Operations
;;; ============================================================

(defun [cons :cons] ([car :t] [cdr :t]))
(defun [car :t] ([list :t]))
(defun [cdr :t] ([list :t]))
(defun [first :t] ([list :t]))
(defun [second :t] ([list :list]))
(defun [third :t] ([list :list]))
(defun [rest :list] ([list :list]))
(defun [last :list] ([list :list]))
(defun [nth :t] ([n :integer] [list :list]))
(defun [length :integer] ([sequence :t]))
(defun [reverse :list] ([sequence :list]))

;;; ============================================================
;;; String Operations
;;; ============================================================

(defun [string-upcase :string] ([string :string]))
(defun [string-downcase :string] ([string :string]))

;;; vim: set filetype=lisp :
6 changes: 5 additions & 1 deletion src/asdf.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@
(extract-types (tycl-extract-types-p system))
(save-types (tycl-save-types-p system)))
(when extract-types
(load-dependency-types system))
(load-dependency-types system)
;; Load declaration files for external libraries
(tycl:find-and-load-declarations
(asdf:system-source-directory system)
:output *error-output*))
(ensure-directories-exist output-file)
(tycl:transpile-file input-file output-file
:extract-types extract-types
Expand Down
88 changes: 88 additions & 0 deletions src/declarations.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
;;;; TyCL Declaration Files
;;;; Load type declarations for external libraries (.d.tycl files)
;;;; Similar to TypeScript's .d.ts files

(in-package #:tycl)

;;; Configuration

(defvar *declaration-search-paths*
(list (merge-pathnames
(make-pathname :directory '(:relative ".config" "tycl" "declarations"))
(user-homedir-pathname)))
"List of directories to search for .d.tycl declaration files.
Project-local paths are added dynamically during transpilation.")

(defvar *loaded-declaration-files* nil
"List of declaration files that have been loaded (to avoid duplicate loading)")

;;; Core Loading

(defun load-declaration-file (file &key (output *error-output*))
"Load type declarations from a .d.tycl file.
Reads the file using TyCL reader and extracts type information only.
No transpilation or code generation occurs.
Returns T if loaded successfully, NIL otherwise."
(let ((path (probe-file file)))
(cond
((not path)
(when output
(warn "Declaration file not found: ~A" file))
nil)
((member path *loaded-declaration-files* :test #'equal)
;; Already loaded
t)
(t
(handler-case
(let ((*readtable* tycl/reader:*tycl-readtable*)
(*package* *package*)
(*current-file* (namestring path))
(*current-package* "COMMON-LISP-USER"))
(with-open-file (in path :direction :input)
(loop for form = (read in nil :eof)
until (eq form :eof)
do ;; Process in-package/defpackage for correct symbol resolution
(tycl/transpiler:process-reader-package-form form)
;; Extract type information only
(extract-type-from-form form)))
(push path *loaded-declaration-files*)
(when output
(format output "~&; Loaded declarations from ~A~%" path))
t)
(error (e)
(warn "Failed to load declaration file ~A: ~A" path e)
nil))))))

(defun load-declarations-directory (directory &key (output *error-output*))
"Load all .d.tycl files from a directory.
Returns the number of files successfully loaded."
(let ((dir (uiop:ensure-directory-pathname directory))
(count 0))
(when (uiop:directory-exists-p dir)
(let ((files (directory (merge-pathnames "*.d.tycl" dir))))
(dolist (file files)
(when (load-declaration-file file :output output)
(incf count)))))
count))

(defun find-and-load-declarations (directory &key (output *error-output*))
"Find and load declaration files from standard locations.
Searches:
1. <directory>/tycl-declarations/ (project-local)
2. Each path in *declaration-search-paths* (user-global)
Returns the total number of files loaded."
(let ((total 0)
(project-dir (merge-pathnames
(make-pathname :directory '(:relative "tycl-declarations"))
(uiop:ensure-directory-pathname directory))))
;; 1. Project-local declarations
(incf total (load-declarations-directory project-dir :output output))
;; 2. User-global declarations
(dolist (search-path *declaration-search-paths*)
(incf total (load-declarations-directory search-path :output output)))
total))

(defun clear-loaded-declarations ()
"Clear the list of loaded declaration files.
Useful for testing or reloading declarations."
(setf *loaded-declaration-files* nil))
7 changes: 7 additions & 0 deletions src/lsp/handlers.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
(when *debug-mode*
(format *error-output* "~%Error transpiling ~A: ~A~%" asd-file e))))))

;; Load declaration files for external libraries
(handler-case
(tycl:find-and-load-declarations root-path :output *error-output*)
(error (e)
(when *debug-mode*
(format *error-output* "~%Error loading declarations: ~A~%" e))))

;; Load type information from workspace
(load-workspace-types root-path)

Expand Down
6 changes: 6 additions & 0 deletions src/packages.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@
#:clear-hook-configuration
;; Hooks loading
#:load-tycl-hooks
;; Declaration files
#:load-declaration-file
#:load-declarations-directory
#:find-and-load-declarations
#:clear-loaded-declarations
#:*declaration-search-paths*
;; LSP integration
#:serialize-type-database-json
#:save-type-database-json
Expand Down
6 changes: 6 additions & 0 deletions src/transpiler.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
(when extract-types
(tycl:find-and-load-hooks (uiop:pathname-directory-pathname input-file)))

;; Load declaration files for external libraries
(when extract-types
(tycl:find-and-load-declarations
(uiop:pathname-directory-pathname input-file)
:output *error-output*))

;; Transpile
(let* ((tycl-source (uiop:read-file-string input-file))
(cl-source (transpile-string tycl-source :extract-types extract-types)))
Expand Down
4 changes: 4 additions & 0 deletions src/type-checker.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,10 @@
Prints errors to *standard-output*."
;; Load hooks (custom type extractors)
(tycl:find-and-load-hooks (uiop:pathname-directory-pathname input-file))
;; Load declaration files for external libraries
(tycl:find-and-load-declarations
(uiop:pathname-directory-pathname input-file)
:output *error-output*)
;; Load project type database (search file dir and parent)
(load-project-type-context (uiop:pathname-directory-pathname input-file))
(let ((tycl-source (uiop:read-file-string input-file))
Expand Down
Loading
Loading