Pronunciation: /ˈtɪkəl/ (sounds like "tickle")
TyCL (Typed Common Lisp) is a type system extension that brings gradual typing and modern development experience to Common Lisp.
- Enhanced Developer Experience: Provide code completion, static analysis, and documentation through LSP (Language Server Protocol) based on type information
- Full Compatibility with Existing CL: Code with type annotations can be executed directly in standard Common Lisp implementations
- Optional Typing: Works with or without types, allowing gradual type adoption
# Install with roswell
ros install tamurashingo/tyclAfter installation, the tycl command is available directly in your PATH.
Transpile a .tycl file to .lisp, stripping type annotations and generating standard Common Lisp code.
tycl transpile <input.tycl> [<output.lisp>]If the output path is omitted, it uses the same name with a .lisp extension.
# Transpile a .tycl file to .lisp
tycl transpile src/example.tycl
# Transpile with custom output path
tycl transpile src/example.tycl build/example.lispTranspile all .tycl files defined in a .asd file. This scans all tycl-file components across all tycl-system definitions in the given .asd file, transpiles each one, and saves project-level type information to tycl-types.d.lisp next to the .asd file.
tycl transpile-all <file.asd>Type check a single .tycl file without transpiling. Returns exit code 0 if all types are valid, exit code 1 if errors are found.
tycl check <input.tycl>Type check all .tycl files defined in a .asd file. Pre-loads tycl-types.d.lisp before checking so that dependency types are available.
tycl check-all <file.asd>Start the Language Server Protocol server. See the LSP section for details.
tycl lspShow usage information for all commands.
tycl helpTyCL uses [] (brackets) for type annotations:
;; Function definition
(defun [add :integer] ([x :integer] [y :integer])
(+ x y))
;; Variable binding
(let (([name :string] (get-name))
(age 30)) ; Type inference
(format t "~A is ~A years old" name age))
;; Local functions
(flet (([square :integer] ([n :integer])
(* n n)))
(square 5))- Numbers:
:integer,:float,:double-float,:rational,:number, etc. - Strings:
:string,:character,:simple-string - Sequences:
:list,:vector,:array,:cons - Logic:
:boolean,:symbol,:keyword - Control:
:void(no return value),:null,:t(any type) - Others:
:function,:hash-table,:stream,:pathname
Accept multiple types:
(defun [process :void] ([value (:integer :string)])
(typecase value
(integer (handle-number value))
(string (handle-string value))))Reusable type definitions with deftype-tycl:
;; Simple alias
(deftype-tycl userid :integer)
(deftype-tycl nullable-num (:integer :null))
;; Parametric type aliases
(deftype-tycl (result T) (:list (T)))
(deftype-tycl (pair A B) (:list (A B)))
;; Usage
(defun [get-user :string] ([id userid])
(fetch-user-from-db id))
(defun [get-range (result :integer)] ([start :integer] [end :integer])
(loop for i from start to end collect i))
(defun [make-pair (pair :integer :string)] ([n :integer] [label :string])
(list n label))Type aliases are resolved during transpilation and do not appear in the generated .lisp output.
Data structures with type parameters:
;; Specify element type for lists (Java: List<Integer>)
(defun [sum-list :integer] ([nums (:list (:integer))])
(reduce #'+ nums :initial-value 0))
;; Hash tables (Java: Map<String, String>)
(defun [lookup (:string :null)]
([table (:hash-table (:string) (:string))]
[key :string])
(gethash key table))
;; Nested generics (Java: List<List<String>>)
(defun [matrix (:list (:list (:string)))] ()
...)Define generic functions with type variables using <E> notation:
;; Single type variable
(defun [identity <E> E] ([x E])
x)
;; => (defun identity (x) x)
;; Compound return type using type variable
(defun [wrap <E> (:list (E))] ([x E])
(list x))
;; => (defun wrap (x) (list x))
;; Multiple type variables
(defun [swap-pair <A B> (:cons B A)] ([p (:cons A B)])
(cons (cdr p) (car p)))
;; => (defun swap-pair (p) (cons (cdr p) (car p)))
;; Type variable in parameters
(defun [first-or-default <E> E] ([lst (:list E)] [default E])
(if lst (first lst) default))
;; => (defun first-or-default (lst default) (if lst (first lst) default))The <...> notation is only active inside [...] brackets. Outside brackets, < and > remain normal symbols, so (< a b) works as expected.
TyCL supports subtype polymorphism through class inheritance. When a class inherits from another, the subclass is accepted wherever the parent type is expected:
(defclass animal () ((name :type :string)))
(defclass dog (animal) ((breed :type :string)))
;; dog is accepted where animal is expected
(defun [greet :string] ([a animal])
(slot-value a 'name))
(defun [greet-dog :string] ([d dog])
(greet d)) ; OK — dog is a subtype of animalMulti-level inheritance is also supported:
(defclass a () ((x :type :integer)))
(defclass b (a) ((y :type :string)))
(defclass c (b) ((z :type :float)))
(defun [process :t] ([obj a]) obj)
(defun [use-c :t] ([obj c]) (process obj)) ; OK — c inherits from b, which inherits from aSubtype checking is directional: a parent class cannot be used where a child type is expected, and unrelated classes are not compatible with each other.
The bracket notation [expr type] can also be used on arbitrary expressions to assert a type. This works like TypeScript's as operator — it tells the type checker to treat the expression as the specified type without affecting the generated code.
(defun [foo :integer] () 3)
(defun [hello :void] ([msg :string])
(format t "msg: ~A~%" msg))
;; (hello (foo)) — type error: :integer is not compatible with :string
;; Use a type cast to override:
(hello [(foo) :string])
;; => (hello (foo))Any expression can be cast:
;; Cast a function call result
(process [(get-value) :string])
;; Cast an arithmetic expression
(display [(+ 1 2) :string])Note: Type casts are unchecked — they override the type checker without runtime validation. Use them when you know the types are compatible at runtime but the type checker cannot infer this.
TyCL provides an ASDF extension that allows .tycl files to be used directly in defsystem definitions. asdf:load-system handles the full transpile → compile → load pipeline automatically.
(defsystem my-app
:class tycl/asdf:tycl-system
:defsystem-depends-on (#:tycl)
:tycl-output-dir "build/"
:components
((:module "src"
:serial t
:components
((:file "config") ; plain .lisp — copied to output dir
(:tycl-file "math") ; .tycl — transpiled to .lisp
(:tycl-file "main")))))| Option | Default | Description |
|---|---|---|
:tycl-output-dir |
nil |
Output directory for transpiled/copied files. Relative to system root. When nil, files are generated alongside sources. |
:tycl-extract-types |
t |
Extract type information during transpilation |
:tycl-save-types |
t |
Save type information to tycl-types.d.lisp |
When ASDF reads a .asd file, the Lisp reader must resolve tycl/asdf:tycl-system before :defsystem-depends-on loads TyCL. Add this stub before your defsystem form:
(unless (find-package :tycl/asdf)
(defpackage #:tycl/asdf
(:export #:tycl-system #:tycl-file)))See docs/asdf.md for the full design document and a sample project for a working example.
When a project depends on another TyCL project, type information from the dependency is automatically available.
During transpilation (asdf:load-system or tycl transpile-all), TyCL automatically loads tycl-types.d.lisp from all tycl-system dependencies before processing the current project. This means types defined in the dependency (functions, classes, variables) are available for type checking without any additional configuration.
To use this, specify the dependency in :depends-on:
(defsystem my-app
:class tycl/asdf:tycl-system
:defsystem-depends-on (#:tycl)
:depends-on ("my-library") ; another tycl-system project
:components (...))When running tycl transpile-all or tycl check-all, dependency types are loaded from each dependency's tycl-types.d.lisp file. The type information file is located next to the dependency's .asd file and contains S-expressions describing all exported types (one entry per package). The file supports merge-on-write to accumulate type information across transpilations.
When publishing a library written in TyCL, include tycl-types.d.lisp in your repository. Without this file, projects that depend on your library cannot type-check calls to your functions or classes.
# Generate tycl-types.d.lisp
tycl transpile-all my-library.asd
# Commit to your repository
git add tycl-types.d.lisp
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.
TyCL supports custom macros through a hook mechanism. This allows extracting type information from project-specific macro definitions.
The :type-extractor function receives the entire form and returns a list of plists, each describing one type definition. Each plist must contain :kind (one of :value, :function, :class, :method), :symbol, and the relevant type fields.
;; Register a type extractor for a custom API macro
;; (define-api get-user :params ((id :integer)) :return :string)
(tycl:register-type-extractor 'define-api
:type-extractor
(lambda (form)
(let ((name (second form))
(body (cddr form)))
(list
`(:kind :function
:symbol ,name
:params ,(mapcar (lambda (p)
(list :name (symbol-name (first p))
:type (second p)))
(getf body :params))
:return ,(getf body :return))))))A hook can also return multiple type definitions at once (e.g., a class, its constructor, and a predicate):
;; Register a type extractor for a model macro
;; (defmodel person :slots ((name :string) (age :integer)))
(tycl:register-type-extractor 'defmodel
:type-extractor
(lambda (form)
(let ((name (second form))
(slots (getf (cddr form) :slots)))
(list
`(:kind :class
:symbol ,name
:slots ,(mapcar (lambda (s)
(list :name (symbol-name (first s))
:type (second s)))
slots))
`(:kind :function
:symbol ,(intern (format nil "MAKE-~A" name))
:params ,(mapcar (lambda (s)
(list :name (symbol-name (first s))
:type (second s)))
slots)
:return ,name)))))Hooks can be loaded automatically from a tycl-hooks.lisp file placed in your project root. The file is loaded when load-tycl or transpile-file is called:
;;;; tycl-hooks.lisp
(in-package #:tycl)
(register-type-extractor 'my-framework:define-entity
:type-extractor
(lambda (form)
(list `(:kind :class
:symbol ,(second form)
:slots ,(extract-entity-slots form)))))TyCL provides a Language Server Protocol implementation for modern editor integration.
tycl lspA full-featured VS Code extension is available in clients/vscode/:
cd clients/vscode
npm install
npm run compile
npm run package
code --install-extension tycl-0.1.0.vsixDevelopment configuration example (.vscode/settings.json):
{
"tycl.lsp.serverPath": "/path/to/tycl-project-root"
}See clients/vscode/README.md for details.
Install tycl-mode from clients/emacs/:
(add-to-list 'load-path "/path/to/tycl/clients/emacs")
(require 'tycl-mode)
;; With lsp-mode
(use-package lsp-mode
:hook (tycl-mode . lsp-deferred))
;; Optional: for development, specify TyCL project root
(setq tycl-lsp-server-root-path "/path/to/tycl-project-root")See clients/emacs/README.md for details.
Configure with coc.nvim or other LSP clients:
{
"languageserver": {
"tycl": {
"command": "tycl",
"args": ["lsp"],
"filetypes": ["tycl", "lisp"],
"rootPatterns": ["tycl.asd", ".git"]
}
}
}- Hover: Show type information for symbols
- Completion: Context-aware code completion
- Diagnostics: Real-time type checking and error detection
- Go to Definition: Navigate to symbol definitions
- Find References: Locate all uses of a symbol
- Document Symbols: Outline view of file structure
When the LSP server starts, it performs the following initialization:
.asdfile discovery: Scans the workspace root for.asdfiles- Full transpilation: If
.asdfiles withtycl-systemdefinitions are found, all.tyclfiles in those systems are transpiled to generatetycl-types.d.lisp. This runs unconditionally regardless of whethertycl-types.d.lispalready exists, ensuring type information is always up-to-date. - Type information loading: Loads
tycl-types.d.lispfiles 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.
By default, diagnostics are debounced with a 500ms delay to avoid unnecessary CPU load during continuous typing. The debounce delay can be configured via the editor client:
- VS Code:
tycl.diagnostics.debounceMssetting (0-5000ms, default: 500) - Other clients: Send
diagnosticDebounceMsininitializationOptions
Setting the value to 0 disables debouncing and computes diagnostics immediately on every change. File save always triggers diagnostics immediately regardless of the debounce setting.
See docs/lsp-server.md for implementation details.
;; Load and transpile a .tycl file
(tycl:load-tycl "src/example.tycl")
;; With options
(tycl:load-tycl "src/example.tycl"
:output-dir "build" ; Output directory
:if-exists :overwrite ; Overwrite existing files
:compile t) ; Compile before loading
;; Or use shorthand
(tycl:compile-and-load-tycl "src/example.tycl" :if-exists :overwrite)
;; Transpile a single file
(tycl:transpile-file "src/example.tycl" "src/example.lisp")
;; Transpile a string
(tycl:transpile-string
"(defun [add :integer] ([x :integer] [y :integer]) (+ x y))")
;; Check types in a file
(tycl:check-file "src/example.tycl")
;; => T (no errors) or NIL (errors found)
;; Check types in a string
(tycl:check-string "(defun [add :integer] ([x :integer]) x)")
;; => Ttycl/
├── src/ # Core transpiler and type checker
│ └── asdf.lisp # ASDF extension (tycl-system, tycl-file)
├── test/ # Test suite
├── roswell/ # CLI tools
├── clients/ # Editor clients
│ ├── emacs/ # Emacs tycl-mode
│ └── vscode/ # VS Code extension
├── sample/ # Sample project using ASDF integration
└── docs/ # Documentation
├── design.md # Design specification
├── asdf.md # ASDF extension design
└── lsp-server.md # LSP server design
# Run all tests
make test
# Unit tests only
make test.unit
# CLI integration tests
make test.cliMIT