cwd-k2/typist
GitHub: cwd-k2/typist
为 Perl 5.40+ 提供静态类型注解、编译期类型检查和 LSP 工具链支持的纯 Perl 类型系统。
Stars: 0 | Forks: 0
# Typist
一个适用于 Perl 5.40+ 的纯 Perl 类型系统。
Typist 通过标准属性语法将静态类型注解引入 Perl。错误会在编译期(CHECK 阶段)以及通过 LSP 被捕获 —— 无需源码过滤器,无需外部工具,默认也无运行时开销。
```
`:sig(...)` attribute
|
+----------+------+------+----------+
| | | |
CHECK phase CLI Check LSP Server Runtime (opt-in)
(compile) (terminal) (editor) (-runtime flag)
| | | |
warn STDERR exit code Diagnostics die on mismatch
```
## 文档
完整文档托管在 [docs/](docs/index.md),采用 MkDocs Material 结构以适配 GitHub Pages。
| 章节 | 受众 | 内容 |
|---------|----------|---------|
| [入门指南](docs/getting-started/index.md) | 用户 | 安装、第一个程序、编辑器设置 |
| [指南](docs/guide/index.md) | 用户 | 类型注解、类型层级、泛型、效应 (effects) 及更多 |
| [进阶](docs/advanced/index.md) | 用户 | HKT、rank-2、类型窄化、子类型规则 |
| [Cookbook](docs/cookbook/index.md) | 用户 | 领域建模、错误处理、多文件项目 |
| [工具链](docs/tooling/index.md) | 用户 | typist-check、LSP、Perl::Critic、调试工具 |
| [参考](docs/reference/index.md) | 用户 | 类型语法文法、prelude、诊断信息 |
| [内部实现](docs/internal/index.md) | 贡献者 | 架构、静态分析、约定、LSP 覆盖范围 |
## 概要
```
use Typist;
# 类型别名与记录(结构化)
BEGIN {
typedef Name => 'Str';
typedef Config => '{ host => Str, port => Int }';
}
# 名义结构类型(blessed, immutable)
BEGIN {
struct Person => (name => 'Str', age => 'Int', optional(email => 'Str'));
}
my $p = Person(name => "Alice", age => 30);
$p->name; # getter
$p->with(age => 31); # immutable update
# 类型化变量
my $count :sig(Int) = 0;
my $label :sig(Maybe[Str]) = undef;
# 类型化子程序 —— 统一的 :sig() 注解
sub add :sig((Int, Int) -> Int) ($a, $b) {
$a + $b;
}
# 带边界量化的泛型
sub max_of :sig((T, T) -> T) ($a, $b) {
$a > $b ? $a : $b;
}
# 带行多态的代数效果
BEGIN {
effect Console => +{
readLine => '() -> Str',
writeLine => '(Str) -> Void',
};
}
sub greet :sig((Str) -> Str ![Console]) ($name) {
"Hello, $name!";
}
# 行多态 —— "至少包含 Log,加上 r 增加的任何内容"
sub with_log :sig((Str) -> Str ![Log, r]) ($msg) {
$msg;
}
```
## 特性
完整的类型系统参考请参见 [指南](docs/guide/index.md)。
### 类型系统
| 特性 | 语法 | 示例 |
|---------|--------|---------|
| 原始类型 | `Int`, `Str`, `Double`, `Num`, `Bool`, `Any`, `Void`, `Never`, `Undef` | `my $x :sig(Int) = 42` |
| 参数化类型 | `Name[T, ...]` | `ArrayRef[Int]` (`Array[Int]`), `HashRef[Str, Int]` (`Hash[Str, Int]`) |
| 联合 / 交集 | `A \| B`, `A & B` | `Int \| Str`, `Readable & Writable` |
| 函数类型 | `(A, B) -> R` | `(Int, Int) -> Int` |
| Struct (名义类型) | `struct Name => (fields)` | 带访问器的受祝福不可变对象 |
| Record (结构类型) | `{ k => T, k? => T }` | `{ name => Str, age? => Int }` |
| Maybe | `Maybe[T]` | `Maybe[Str]` = `Str \| Undef` |
| 字面量类型 | `42`, `"hello"` | 特定值的单例类型 |
| 类型别名 | `typedef` | `typedef Price => 'Int'` |
| 名义类型 | `newtype` / `Name::coerce` | `newtype UserId => 'Int'` |
| ADT / GADT | `datatype` | 标签联合,按构造器返回类型 |
| 枚举 | `enum` | `enum Color => qw(Red Green Blue)` |
| 泛型 | ``, `` | `(T, T) -> T` |
| Rank-2 多态 | `forall` | `forall A. (A) -> A` |
| 可变参数函数 | `...Type` | `(Int, ...Str) -> Void` |
| Type classes / HKT | `typeclass` / `instance` | 特设多态,`F: * -> *` |
| 代数效应 | `effect` / `![...]` | `![Console, Log]` |
| 效应协议 | `protocol('sig', 'From -> To')` | `![DB<* -> Authed>]` |
| 行多态 | `` / `[E, r]` | 效应行扩展 |
| 效应处理器 | `Effect::op(...)` / `handle` | 直接分发 + 作用域处理 |
### 分析
| 特性 | 描述 |
|---------|-------------|
| CHECK 阶段分析 | 通过 `warn` 在编译期报告类型/效应错误 |
| CLI 检查器 | 带彩色输出的终端静态分析 |
| LSP 服务器 | 悬停、补全、诊断、转到定义、引用、重命名、代码操作、语义 token 等 |
| 跨文件检查 | 模块间的工作区级类型解析(aliases, newtypes, datatypes, structs, effects, typeclasses, instances) |
| 渐进式类型 | 注解密度决定检查严格程度 |
| 类型推导 | 双向推导,可变绑定的字面量宽化,控制流窄化(`defined`、真值判断、`isa`、`ref()`、提前返回) |
| 内置 prelude | 80 个带类型注解的内置函数,以及三个标准效应标签(`IO`, `Exn`, `Decl`) |
### 模式
| 模式 | 代价 | 行为 |
|------|------|----------|
| 纯静态(默认) | 零运行时开销 | CHECK 阶段 `warn` 诊断 |
| 运行时 (`-runtime`) | 通过 `tie` + sub 包装进行每次调用的类型检查 | 类型违规时 `die` |
| Newtype 边界 | 始终激活 | 无论何种模式都进行构造器验证 |
## 安装
### 环境要求
- Perl 5.40+
- [PPI](https://metacpan.org/pod/PPI)(由下列任一方法自动解析依赖)
### 从 GitHub 安装
```
cpanm https://github.com/cwd-k2/typist.git
```
### 从 GitHub 安装
添加到你的 `cpanfile`:
```
requires 'Typist', git => 'https://github.com/cwd-k2/typist.git';
```
然后执行:
```
carton install
```
### 从源码安装
```
git clone https://github.com/cwd-k2/typist.git
cd typist
perl Makefile.PL
make
make test
make install # Installs typist-check and typist-lsp
```
## CLI 工具
### typist-check
终端静态类型检查器。使用与 LSP 服务器相同的分析引擎。
```
typist-check # Scan lib/ for .pm files
typist-check lib/Shop/Order.pm # Check specific file(s)
typist-check --root src/ # Custom workspace root
typist-check --no-color # Disable colored output
typist-check --verbose # Show clean files too
```
输出示例:
```
lib/Shop/Order.pm
42:5 error expected Int, got Str in argument 1 [TypeMismatch]
58:1 error wrong number of arguments [ArityMismatch]
lib/Shop/Payment.pm
17:1 warning undeclared type variable 'T' [UndeclaredTypeVar]
2 error(s), 1 warning(s) in 2 file(s) (4 files checked)
```
退出码:`0` = 干净,`1` = 有错误,`2` = 仅有警告。
当 stdout 不是 TTY、传入 `--no-color` 或设置了 `NO_COLOR` 时,颜色会自动禁用。
## 注解语法
Typist 对所有类型注解使用统一的 `:sig(...)` 属性。
### 变量
```
my $x :sig(Int) = 42;
my $y :sig(Str | Undef) = undef;
my $z :sig({ name => Str, age => Int }) = { name => "Alice", age => 30 };
```
### 函数
```
# 参数与返回类型
sub add :sig((Int, Int) -> Int) ($a, $b) { $a + $b }
# 带效果
sub greet :sig((Str) -> Str ![Console]) ($name) { "Hello, $name!" }
# 带泛型
sub first :sig((ArrayRef[T]) -> T) ($arr) { $arr->[0] }
# 带边界量化
sub max_of :sig((T, T) -> T) ($a, $b) { $a > $b ? $a : $b }
# 可变参数
sub log_all :sig((Str, ...Any) -> Void ![Console]) ($fmt, @args) { }
```
### Struct 类型(名义类型)
```
BEGIN {
struct Person => (
name => Str,
age => Int,
email => optional(Str), # omittable field
);
}
my $p = Person(name => "Alice", age => 30);
$p->name; # "Alice"
Person::derive($p, age => 31); # immutable derive
```
Struct 类型是**名义类型**:`Struct <: Record`(结构兼容),但 `Record 'Int';
newtype Email => 'Str';
}
my $uid = UserId(42); # Constructor validates inner type
my $raw = UserId::coerce($uid); # Extracts inner value: 42
```
### 代数数据类型
```
BEGIN {
datatype Shape =>
Circle => '(Int)',
Rectangle => '(Int, Int)',
Point => '';
enum Color => qw(Red Green Blue); # Nullary-only ADT
}
my $c = Circle(5); # Auto-generated constructor
```
GADT 构造器指定按构造器的返回类型:
```
BEGIN {
datatype 'Expr[A]' =>
IntLit => '(Int) -> Expr[Int]',
BoolLit => '(Bool) -> Expr[Bool]',
Add => '(Expr[Int], Expr[Int]) -> Expr[Int]';
}
```
### 效应与处理器
```
BEGIN {
effect Console => +{
readLine => '() -> Str',
writeLine => '(Str) -> Void',
};
}
# 函数声明其效果
sub io_greet :sig((Str) -> Void ![Console]) ($name) { say "Hi, $name" }
# 效果操作作为限定子程序调用
Console::writeLine("hello");
# handle 安装作用域处理程序并执行主体
my $result = handle {
Console::writeLine("start");
42;
} Console => +{
writeLine => sub ($msg) { say $msg },
};
```
### 效应协议(有状态效应)
效应可以携带协议状态机,强制执行操作顺序:
```
BEGIN {
# * is the ground state (protocol inactive); explicit states are active states only
effect Database => qw/Connected Authed/ => +{
connect => protocol('(Str) -> Void', '* -> Connected'),
auth => protocol('(Str, Str) -> Void', 'Connected -> Authed'),
query => protocol('(Str) -> Str', 'Authed -> Authed'),
disconnect => protocol('() -> Void', 'Authed -> *'),
};
}
# 状态转换在类型注解中声明
sub setup :sig(() -> Void ![Database<* -> Authed>]) () {
Database::connect("localhost"); # * → Connected
Database::auth("user", "pass"); # Connected → Authed
}
# 不变状态:在同一状态开始和结束
sub run_query :sig((Str) -> Str ![Database]) ($sql) {
Database::query($sql); # Authed → Authed
}
# ![Database] 默认为 * -> *(完整会话周期)
sub session :sig(() -> Str ![Database]) () {
setup();
my $r = run_query("SELECT 1");
Database::disconnect();
$r;
}
```
静态分析器跟踪操作序列,并验证最终状态与声明的结束状态匹配。从状态 `*` 调用 `DB::query` 会产生 `ProtocolMismatch` 诊断。
### Type Classes
```
BEGIN {
typeclass Show => T, +{
show => '(T) -> Str',
};
instance Show => Int, +{
show => sub ($x) { "$x" },
};
}
say Show::show(42); # "42"
```
Instances 可以在与 typeclass 不同的文件中定义。LSP 工作区和 `typist-check` 会跨模块解析 instances:
```
# lib/MyApp/Classes.pm
typeclass Eq => T, +{ eq => '(T, T) -> Bool' };
# lib/MyApp/Instances.pm —— 不同文件
instance Eq => Int, +{ eq => sub ($a, $b) { $a == $b } };
instance Eq => Str, +{ eq => sub ($a, $b) { $a eq $b } };
```
### 声明与抑制
```
# 为外部函数添加类型/效果检查注解
declare say => '(Str) -> Void ![Console]';
sub handler :sig((Str) -> Str ![Console]) ($s) {
# @typist-ignore
some_unannotated_function($s); # Diagnostic suppressed
}
```
## 渐进式类型
实现细节请参见[静态分析内部机制](docs/internal/static-analysis.md#gradual-typing-semantics)。
Typist 根据注解密度按比例执行检查:
| 注解级别 | 类型检查 | 效应检查 |
|------------------|-------------|---------------|
| 完全注解 | 所有参数、返回值、调用点 | 完整效应包含检查 |
| 部分注解(无返回值) | 仅参数,返回类型未知 | 按声明检查 |
| 部分注解(无 `:Eff`) | 按声明检查 | 视为纯函数 |
| 完全未注解 | 跳过(`Any -> Any`) | 视为纯函数(无约束) |
## 编辑器集成
### LSP 服务器
独立的 LSP 服务器提供全面的编辑体验:
| 能力 | 描述 |
|------------|-------------|
| 诊断 | 类型不匹配、元数不匹配、效应不匹配、别名循环 |
| 悬停 | 函数、变量、构造器、typedef 的类型签名 |
| 补全 | 类型注解补全和类型感知的代码补全 |
| 转到定义 | 同文件和跨文件定义查找 |
| 查找引用 | 跨打开文档和工作区的词边界搜索 |
| 重命名 | 跨所有工作区文件的符号重命名 |
| 签名帮助 | 带活动参数追踪的函数参数提示 |
| 文档符号 | 函数、变量、typedef、newtype、datatype、effect、typeclass 的大纲 |
| Inlay Hints | 未注解变量的推导类型,未注解函数的推导效应 |
| 代码操作 | 针对效应不匹配和类型错误的快速修复建议 |
| 语义 Token | Typist 关键字、类型名称、构造器和类型参数的语法高亮 |
启动服务器:
```
typist-lsp # After make install
carton exec -- perl bin/typist-lsp # Development (carton)
```
#### Neovim (nvim-lspconfig)
```
local configs = require('lspconfig.configs')
configs.typist = {
default_config = {
cmd = { 'typist-lsp' },
filetypes = { 'perl' },
root_dir = function(fname)
return vim.fs.dirname(
vim.fs.find({ 'lib', '.git' }, { upward = true, path = fname })[1]
)
end,
},
}
require('lspconfig').typist.setup {}
```
#### VS Code
专用的扩展位于 `editors/vscode/`。
构建并安装:
```
cd editors/vscode
npm install
npm run build
npx vsce package
code --install-extension typist-0.0.1.vsix
```
该扩展会在工作区根目录查找 `local/bin/typist-lsp`(例如通过 `cpanm -L local Typist` 安装),然后回退到 `$PATH` 中的 `typist-lsp`。若要覆盖,请在 VS Code 设置中设置 `typist.server.path`。
### Perl::Critic 策略
四个用于代码质量强制的策略:
| 策略 | 描述 | 默认严重度 |
|--------|-------------|-----------------|
| `Typist::TypeCheck` | 通过 Typist 分析器进行静态类型检查 | 2 |
| `Typist::AnnotationStyle` | 要求公开 subs 具有 `:sig()` | 2 |
| `Typist::EffectCompleteness` | 要求有效应的函数声明效应 | 3 |
| `Typist::ExhaustivenessCheck` | 对非穷尽 `match` 表达式发出警告 | 2 |
```
# .perlcriticrc
[Typist::TypeCheck]
severity = 2
[Typist::AnnotationStyle]
severity = 2
[Typist::EffectCompleteness]
severity = 3
[Typist::ExhaustivenessCheck]
severity = 2
```
## 环境变量
| 变量 | 效果 |
|----------|--------|
| `TYPIST_RUNTIME` | 启用运行时强制(`1` = 开启) |
| `TYPIST_CHECK_QUIET` | 抑制 CHECK 阶段诊断(`1` = 静默) |
| `TYPIST_LSP_LOG` | LSP 日志级别(`off`/`error`/`warn`/`info`/`debug`/`trace`) |
| `TYPIST_LSP_TRACE` | 用于 LSP 消息记录的 JSONL 跟踪文件路径 |
| `NO_COLOR` | 在 `typist-check` 中禁用彩色输出 |
## 示例
请参阅 `example/` 中的可运行演示:
| 文件 | 主题 |
|------|--------|
| `01_foundations.pl` | 类型别名、类型化变量/函数 |
| `02_composite_types.pl` | Struct, Union, Maybe, 参数化类型 |
| `03_generics.pl` | 泛型函数、有界量化 |
| `04_nominal_types.pl` | Newtypes, 字面量类型, 递归类型 |
| `05_algebraic_types.pl` | Datatype/ADT, 模式匹配, enum |
| `06_typeclasses.pl` | Type classes, HKT, Functor |
| `07_effects.pl` | 效应系统, handlers, protocols |
| `08_gradual_typing.pl` | 渐进式类型, 流类型 |
| `09_dsl.pl` | DSL 操作符, 构造器 |
| `10_higher_order.pl` | 高阶函数推导 |
| `11_static_errors.pl` | 用于静态分析演示的故意类型错误 |
| `12_method_chains.pl` | Struct 访问器, 不可变更新, newtype 链 |
```
carton exec -- perl example/01_foundations.pl
```
## 测试
```
# 所有测试(66 个文件)
carton exec -- prove -l t/ t/static/ t/lsp/ t/critic/
# 按类别
carton exec -- prove -l t/ # Core type system
carton exec -- prove -l t/static/ # Static analysis
carton exec -- prove -l t/lsp/ # LSP server
carton exec -- prove -l t/critic/ # Perl::Critic policy
```
## 已知限制
详细的分析内部机制请参见[静态分析内部机制](docs/internal/static-analysis.md#known-limitations)。
- **类型推导** —— 对于未注解的可变绑定,字面量类型会被宽化为基础原子类型(`my $x = 0` → `Int`, `my $x = 3.14` → `Double`)。操作符优先级不影响推导出的类型。
- **方法检查** —— 会检查实例(`$self->method()`)、跨包 struct(`$p->name()`)、类(`Person->new()`)、链式(`Name::derive($p, ...)->greet()`)、泛型以及 Record 访问器调用。Union 接收者和未类型化接收者按渐进式跳过。
- **类型窄化** —— 支持 `defined($x)`、真值判断、`isa`、`ref($x) eq/ne 'TYPE'`(带/不带括号、变量比较、Union 类型的反向窄化)以及提前返回。完整的 ref 类型:`HASH`、`ARRAY`、`SCALAR`、`CODE`、`REF`、`Regexp`、`GLOB`、`IO`、`VSTRING`,以及受祝福的类名。
- **效应系统** —— 效应推导为未注解的函数提供 LSP inlay hints(仅限直接被调用者)。协议检查跟踪带有 if/else 分支收敛、循环幂等强制以及 `match`/`handle` 主体跟踪的操作序列。
- **Typeclass instances** —— 跨文件的 instance 声明会被提取并注册以供工作区解析。静态分析记录 instance 的存在,但不验证方法的完整性;完整性检查仅在运行时运行。方法实现(coderefs)对静态路径不可用。
- **PPI 依赖** —— 诊断质量取决于 PPI 的解析准确性。
## 许可证
MIT 许可证。详情请见 [LICENSE](LICENSE)。
标签:LSP, MkDocs, Perl, Perl 5, SOC Prime, 云安全监控, 函数式编程, 开发工具, 类型注解, 类型系统, 编程语言, 编译时检查, 语法属性, 静态分析, 高阶类型