From 0331bb20a76507bd0ee3e128f6196d812e003993 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Mon, 16 Mar 2026 12:26:06 +0900 Subject: [PATCH 1/5] Add .d.tycl declaration files for external library type definitions Introduce a mechanism similar to TypeScript's .d.ts files, allowing users to declare types for external libraries not written in TyCL. Declaration files are automatically discovered from tycl-declarations/ directories (project-local) and ~/.config/tycl/declarations/ (global). --- sample/tycl-declarations/cl-functions.d.tycl | 90 ++++++++++ src/asdf.lisp | 6 +- src/declarations.lisp | 88 ++++++++++ src/packages.lisp | 6 + src/transpiler.lisp | 6 + test/declarations-test.lisp | 159 ++++++++++++++++++ .../tycl-declarations/alexandria.d.tycl | 13 ++ .../tycl-declarations/cl-ppcre.d.tycl | 11 ++ tycl.asd | 4 +- 9 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 sample/tycl-declarations/cl-functions.d.tycl create mode 100644 src/declarations.lisp create mode 100644 test/declarations-test.lisp create mode 100644 test/declarations/tycl-declarations/alexandria.d.tycl create mode 100644 test/declarations/tycl-declarations/cl-ppcre.d.tycl diff --git a/sample/tycl-declarations/cl-functions.d.tycl b/sample/tycl-declarations/cl-functions.d.tycl new file mode 100644 index 0000000..7fe366d --- /dev/null +++ b/sample/tycl-declarations/cl-functions.d.tycl @@ -0,0 +1,90 @@ +;;; -*- 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 . + +(in-package #:cl) + +;;; ============================================================ +;;; 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])) +(defun [floor :integer] ([number :number] [divisor :number])) +(defun [mod :number] ([number :number] [divisor :number])) +(defun [abs :number] ([n :number])) + +;;; ============================================================ +;;; Predicates +;;; ============================================================ + +(defun [= :boolean] ([a :number] [b :number])) +(defun [zerop :boolean] ([n :number])) +(defun [null :boolean] ([obj :t])) +(defun [not :boolean] ([obj :t])) +(defun [equal :boolean] ([a :t] [b :t])) +(defun [equalp :boolean] ([a :t] [b :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] ([cons :cons])) +(defun [cdr :t] ([cons :cons])) +(defun [first :t] ([list :list])) +(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 [append :list] ([list1 :list] [list2 :list])) +(defun [reverse :list] ([sequence :list])) +(defun [list :list] ([item :t])) +(defun [push :t] ([item :t] [place :list])) +(defun [member :list] ([item :t] [list :list])) + +;;; ============================================================ +;;; Higher-Order Functions +;;; ============================================================ + +(defun [mapcar :list] ([function :function] [list :list])) +(defun [reduce :t] ([function :function] [sequence :list])) +(defun [remove-if :list] ([predicate :function] [sequence :list])) +(defun [remove-if-not :list] ([predicate :function] [sequence :list])) +(defun [find :t] ([item :t] [sequence :list])) +(defun [funcall :t] ([function :function] [arg :t])) +(defun [apply :t] ([function :function] [args :list])) + +;;; ============================================================ +;;; String Operations +;;; ============================================================ + +(defun [concatenate :string] ([result-type :symbol] [seq1 :string] [seq2 :string])) +(defun [subseq :string] ([sequence :string] [start :integer] [end :integer])) +(defun [string-upcase :string] ([string :string])) +(defun [string-downcase :string] ([string :string])) +(defun [string= :boolean] ([string1 :string] [string2 :string])) +(defun [format (:string :null)] ([destination :t] [control-string :string])) + +;;; ============================================================ +;;; CLOS +;;; ============================================================ + +(defun [make-instance :t] ([class :symbol])) + +;;; vim: set filetype=lisp : diff --git a/src/asdf.lisp b/src/asdf.lisp index aa9bfd6..d781b8e 100644 --- a/src/asdf.lisp +++ b/src/asdf.lisp @@ -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 diff --git a/src/declarations.lisp b/src/declarations.lisp new file mode 100644 index 0000000..5a940e9 --- /dev/null +++ b/src/declarations.lisp @@ -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. /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)) diff --git a/src/packages.lisp b/src/packages.lisp index fd16105..bc2523f 100644 --- a/src/packages.lisp +++ b/src/packages.lisp @@ -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 diff --git a/src/transpiler.lisp b/src/transpiler.lisp index f5ef85f..653722c 100644 --- a/src/transpiler.lisp +++ b/src/transpiler.lisp @@ -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))) diff --git a/test/declarations-test.lisp b/test/declarations-test.lisp new file mode 100644 index 0000000..5dcfff6 --- /dev/null +++ b/test/declarations-test.lisp @@ -0,0 +1,159 @@ +;;;; Tests for TyCL Declaration Files (.d.tycl) + +(defpackage #:tycl/test/declarations + (:use #:cl #:rove)) + +(in-package #:tycl/test/declarations) + +(defun test-declarations-dir () + "Return the path to the test declarations directory" + (merge-pathnames + "test/declarations/" + (asdf:system-source-directory :tycl))) + +(deftest test-load-declaration-file + (testing "Loading a single .d.tycl file" + (tycl:clear-type-database) + (tycl:clear-loaded-declarations) + + (let ((decl-file (merge-pathnames + "tycl-declarations/alexandria.d.tycl" + (test-declarations-dir)))) + ;; Load the declaration file + (ok (tycl:load-declaration-file decl-file :output nil)) + + ;; Verify types were registered + (let ((flatten-info (tycl:get-type-info "ALEXANDRIA" "FLATTEN"))) + (ok flatten-info "flatten should be registered") + (ok (typep flatten-info 'tycl:function-type-info)) + (ok (equal (tycl:function-return-type flatten-info) :list))) + + (let ((starts-info (tycl:get-type-info "ALEXANDRIA" "STARTS-WITH-SUBSEQ"))) + (ok starts-info "starts-with-subseq should be registered") + (ok (equal (tycl:function-return-type starts-info) :boolean))) + + (let ((iota-info (tycl:get-type-info "ALEXANDRIA" "IOTA"))) + (ok iota-info "iota should be registered") + (ok (equal (length (tycl:function-params iota-info)) 3)))) + + (tycl:clear-type-database) + (tycl:clear-loaded-declarations))) + +(deftest test-load-declaration-file-idempotent + (testing "Loading the same file twice does not duplicate" + (tycl:clear-type-database) + (tycl:clear-loaded-declarations) + + (let ((decl-file (merge-pathnames + "tycl-declarations/alexandria.d.tycl" + (test-declarations-dir)))) + (ok (tycl:load-declaration-file decl-file :output nil)) + ;; Second load should succeed (already loaded) + (ok (tycl:load-declaration-file decl-file :output nil)) + + ;; Types should still be there + (ok (tycl:get-type-info "ALEXANDRIA" "FLATTEN"))) + + (tycl:clear-type-database) + (tycl:clear-loaded-declarations))) + +(deftest test-load-declaration-file-not-found + (testing "Loading a non-existent file returns NIL" + (ok (not (tycl:load-declaration-file "/nonexistent/file.d.tycl" :output nil))))) + +(deftest test-load-declarations-directory + (testing "Loading all .d.tycl files from a directory" + (tycl:clear-type-database) + (tycl:clear-loaded-declarations) + + (let ((decl-dir (merge-pathnames + "tycl-declarations/" + (test-declarations-dir)))) + (let ((count (tycl:load-declarations-directory decl-dir :output nil))) + (ok (>= count 2) "Should load at least 2 declaration files"))) + + ;; Verify Alexandria types + (ok (tycl:get-type-info "ALEXANDRIA" "FLATTEN")) + (ok (tycl:get-type-info "ALEXANDRIA" "ENSURE-LIST")) + + ;; Verify CL-PPCRE types + (ok (tycl:get-type-info "CL-PPCRE" "SCAN")) + (ok (tycl:get-type-info "CL-PPCRE" "SPLIT")) + + (tycl:clear-type-database) + (tycl:clear-loaded-declarations))) + +(deftest test-load-declarations-directory-nonexistent + (testing "Loading from non-existent directory returns 0" + (ok (= 0 (tycl:load-declarations-directory "/nonexistent/dir/" :output nil))))) + +(deftest test-find-and-load-declarations + (testing "Auto-discovery of declaration files" + (tycl:clear-type-database) + (tycl:clear-loaded-declarations) + + ;; Use the test directory which has tycl-declarations/ subdirectory + (let ((tycl:*declaration-search-paths* nil)) ; disable global paths + (let ((count (tycl:find-and-load-declarations + (test-declarations-dir) :output nil))) + (ok (>= count 2) "Should find and load declarations"))) + + ;; Verify types from both files were loaded + (ok (tycl:get-type-info "ALEXANDRIA" "FLATTEN")) + (ok (tycl:get-type-info "CL-PPCRE" "SCAN")) + + (tycl:clear-type-database) + (tycl:clear-loaded-declarations))) + +(deftest test-declaration-with-defclass + (testing "Declaration files can declare classes" + (tycl:clear-type-database) + (tycl:clear-loaded-declarations) + + ;; Create a temporary declaration file with a class + (uiop:with-temporary-file (:pathname p :type "d.tycl" :keep t) + (with-open-file (out p :direction :output :if-exists :supersede) + (write-string + "(defpackage #:test-ext (:use #:cl)) +(in-package #:test-ext) +(defclass test-widget () + (([name :string] :initarg :name) + ([width :integer] :initarg :width) + ([height :integer] :initarg :height))) +(defun [make-widget test-widget] ([name :string] [width :integer] [height :integer]))" + out)) + (unwind-protect + (progn + (ok (tycl:load-declaration-file p :output nil)) + + ;; Verify class + (let ((class-info (tycl:get-type-info "TEST-EXT" "TEST-WIDGET"))) + (ok class-info "class should be registered") + (ok (typep class-info 'tycl:class-type-info)) + (ok (= 3 (length (tycl:class-slots class-info))))) + + ;; Verify constructor function + (let ((func-info (tycl:get-type-info "TEST-EXT" "MAKE-WIDGET"))) + (ok func-info "constructor should be registered") + (ok (typep func-info 'tycl:function-type-info)))) + (when (probe-file p) + (delete-file p)))) + + (tycl:clear-type-database) + (tycl:clear-loaded-declarations))) + +(deftest test-clear-loaded-declarations + (testing "Clearing loaded declarations allows reloading" + (tycl:clear-type-database) + (tycl:clear-loaded-declarations) + + (let ((decl-file (merge-pathnames + "tycl-declarations/alexandria.d.tycl" + (test-declarations-dir)))) + (ok (tycl:load-declaration-file decl-file :output nil)) + (tycl:clear-loaded-declarations) + ;; Should be able to load again after clearing + (ok (tycl:load-declaration-file decl-file :output nil))) + + (tycl:clear-type-database) + (tycl:clear-loaded-declarations))) diff --git a/test/declarations/tycl-declarations/alexandria.d.tycl b/test/declarations/tycl-declarations/alexandria.d.tycl new file mode 100644 index 0000000..b3e23c7 --- /dev/null +++ b/test/declarations/tycl-declarations/alexandria.d.tycl @@ -0,0 +1,13 @@ +;;; Type declarations for Alexandria library +;;; This is a sample .d.tycl file for testing + +(defpackage #:alexandria + (:use #:cl)) + +(in-package #:alexandria) + +(defun [flatten :list] ([list :list])) +(defun [ensure-list :list] ([thing :t])) +(defun [starts-with-subseq :boolean] ([prefix :string] [sequence :string])) +(defun [ends-with-subseq :boolean] ([suffix :string] [sequence :string])) +(defun [iota :list] ([n :integer] [start :integer] [step :integer])) diff --git a/test/declarations/tycl-declarations/cl-ppcre.d.tycl b/test/declarations/tycl-declarations/cl-ppcre.d.tycl new file mode 100644 index 0000000..a672964 --- /dev/null +++ b/test/declarations/tycl-declarations/cl-ppcre.d.tycl @@ -0,0 +1,11 @@ +;;; Type declarations for CL-PPCRE library + +(defpackage #:cl-ppcre + (:use #:cl)) + +(in-package #:cl-ppcre) + +(defun [scan (:integer :integer :integer :integer)] ([regex :string] [target :string])) +(defun [regex-replace :string] ([regex :string] [target :string] [replacement :string])) +(defun [regex-replace-all :string] ([regex :string] [target :string] [replacement :string])) +(defun [split :list] ([regex :string] [target :string])) diff --git a/tycl.asd b/tycl.asd index 6d75bbe..83590a2 100644 --- a/tycl.asd +++ b/tycl.asd @@ -24,6 +24,7 @@ (:file "type-info") (:file "type-serializer") (:file "type-extractor") + (:file "declarations") (:file "type-checker") (:file "transpiler") (:file "lsp-integration") @@ -61,7 +62,8 @@ (:file "asdf-test") (:file "type-checker-test") (:file "deftype-tycl-test") - (:file "type-vars-test"))) + (:file "type-vars-test") + (:file "declarations-test"))) (:module "test/lsp" :components ((:file "did-change-test") From db14900a2576c9ee0544fdd8f68a3a18d2538bf0 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Mon, 16 Mar 2026 14:31:07 +0900 Subject: [PATCH 2/5] Load declaration files in check-file for type checking check-file was missing the find-and-load-declarations call, so .d.tycl type declarations were not available during standalone type checking (e.g. tycl check, check-all). --- src/type-checker.lisp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/type-checker.lisp b/src/type-checker.lisp index 5b1b9ef..146631d 100644 --- a/src/type-checker.lisp +++ b/src/type-checker.lisp @@ -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)) From 1dd6d03a84889a5dfebe76fb9721de3e4f39e141 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Mon, 16 Mar 2026 17:38:31 +0900 Subject: [PATCH 3/5] Fix sample declarations and math.tycl for correct type checking - Use canonical package name (common-lisp) instead of nickname (cl) in declaration file to match type checker's package resolution - Remove CL functions with &rest/&optional/&key parameters that cause false positive argument count errors - Add type annotations to math.tycl arithmetic results to resolve :number -> num (integer) downcast warnings --- sample/src/math.tycl | 6 +-- sample/tycl-declarations/cl-functions.d.tycl | 44 ++++---------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/sample/src/math.tycl b/sample/src/math.tycl index 845c7f7..7b55e01 100644 --- a/sample/src/math.tycl +++ b/sample/src/math.tycl @@ -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" diff --git a/sample/tycl-declarations/cl-functions.d.tycl b/sample/tycl-declarations/cl-functions.d.tycl index 7fe366d..cbb3b71 100644 --- a/sample/tycl-declarations/cl-functions.d.tycl +++ b/sample/tycl-declarations/cl-functions.d.tycl @@ -7,8 +7,12 @@ ;;;; ;;;; Note: Symbols starting with < or > cannot be used in [...] annotations ;;;; because those characters are reserved for type parameter syntax . +;;;; +;;;; 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 #:cl) +(in-package #:common-lisp) ;;; ============================================================ ;;; Arithmetic @@ -20,20 +24,14 @@ (defun [/ :number] ([a :number] [b :number])) (defun [1+ :number] ([n :number])) (defun [1- :number] ([n :number])) -(defun [floor :integer] ([number :number] [divisor :number])) -(defun [mod :number] ([number :number] [divisor :number])) -(defun [abs :number] ([n :number])) ;;; ============================================================ ;;; Predicates ;;; ============================================================ -(defun [= :boolean] ([a :number] [b :number])) (defun [zerop :boolean] ([n :number])) (defun [null :boolean] ([obj :t])) (defun [not :boolean] ([obj :t])) -(defun [equal :boolean] ([a :t] [b :t])) -(defun [equalp :boolean] ([a :t] [b :t])) (defun [numberp :boolean] ([obj :t])) (defun [stringp :boolean] ([obj :t])) (defun [listp :boolean] ([obj :t])) @@ -43,48 +41,22 @@ ;;; ============================================================ (defun [cons :cons] ([car :t] [cdr :t])) -(defun [car :t] ([cons :cons])) -(defun [cdr :t] ([cons :cons])) -(defun [first :t] ([list :list])) +(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 [append :list] ([list1 :list] [list2 :list])) (defun [reverse :list] ([sequence :list])) -(defun [list :list] ([item :t])) -(defun [push :t] ([item :t] [place :list])) -(defun [member :list] ([item :t] [list :list])) - -;;; ============================================================ -;;; Higher-Order Functions -;;; ============================================================ - -(defun [mapcar :list] ([function :function] [list :list])) -(defun [reduce :t] ([function :function] [sequence :list])) -(defun [remove-if :list] ([predicate :function] [sequence :list])) -(defun [remove-if-not :list] ([predicate :function] [sequence :list])) -(defun [find :t] ([item :t] [sequence :list])) -(defun [funcall :t] ([function :function] [arg :t])) -(defun [apply :t] ([function :function] [args :list])) ;;; ============================================================ ;;; String Operations ;;; ============================================================ -(defun [concatenate :string] ([result-type :symbol] [seq1 :string] [seq2 :string])) -(defun [subseq :string] ([sequence :string] [start :integer] [end :integer])) (defun [string-upcase :string] ([string :string])) (defun [string-downcase :string] ([string :string])) -(defun [string= :boolean] ([string1 :string] [string2 :string])) -(defun [format (:string :null)] ([destination :t] [control-string :string])) - -;;; ============================================================ -;;; CLOS -;;; ============================================================ - -(defun [make-instance :t] ([class :symbol])) ;;; vim: set filetype=lisp : From effd8eff53c1aaa37d2b076193f560b4c6aaa49e Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Mon, 16 Mar 2026 17:53:43 +0900 Subject: [PATCH 4/5] Load declaration files in LSP server initialization Add find-and-load-declarations call to handle-initialize so that .d.tycl type declarations are available for diagnostics in the editor. --- src/lsp/handlers.lisp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lsp/handlers.lisp b/src/lsp/handlers.lisp index 14f26b0..2570a4b 100644 --- a/src/lsp/handlers.lisp +++ b/src/lsp/handlers.lisp @@ -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) From e9a807e13f0ed471ff60d7cf0677158dd612d140 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Mon, 16 Mar 2026 17:58:10 +0900 Subject: [PATCH 5/5] update README --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1614c56..f4a18bb 100644 --- a/README.md +++ b/README.md @@ -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 ``. + +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. @@ -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. @@ -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