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, 云安全监控, 函数式编程, 开发工具, 类型注解, 类型系统, 编程语言, 编译时检查, 语法属性, 静态分析, 高阶类型