|
| 1 | +# Pine Script 类型推断系统设计文档 |
| 2 | + |
| 3 | +## 功能概述 |
| 4 | + |
| 5 | +VSCode 扩展提供了完整的 Pine Script v6 类型推断系统,支持: |
| 6 | +- ✅ 变量声明类型推断 |
| 7 | +- ✅ 解构赋值类型推断 |
| 8 | +- ✅ 用户自定义函数元组返回值推断 |
| 9 | +- ✅ Inlay Hints 类型提示显示 |
| 10 | +- ✅ 所有作用域支持(全局、函数、循环、条件语句) |
| 11 | + |
| 12 | +## 核心架构 |
| 13 | + |
| 14 | +### 1. 三阶段验证流程 |
| 15 | + |
| 16 | +``` |
| 17 | +输入: Pine Script 代码 |
| 18 | + ↓ |
| 19 | +[Parser] 解析 → AST |
| 20 | + ↓ |
| 21 | +[AstValidator.validate()] |
| 22 | + ↓ |
| 23 | +第一阶段: collectDeclarations(ast) |
| 24 | + - 遍历 AST,收集所有声明 |
| 25 | + - 推断变量类型(literal/expression) |
| 26 | + - 保存函数声明引用 |
| 27 | + - 构建符号表 |
| 28 | + ↓ |
| 29 | +第二阶段: validateStatement(ast) |
| 30 | + - 验证表达式和语句 |
| 31 | + - 标记变量使用情况 |
| 32 | + - 进入/退出作用域 |
| 33 | + ↓ |
| 34 | +第三阶段: checkUnusedVariables() |
| 35 | + - 检查未使用的变量 |
| 36 | + ↓ |
| 37 | +第四阶段: buildSymbolMap() ✅ 已优化 |
| 38 | + - 构建 name:line → symbol 的映射 |
| 39 | + - 供 InlayHintsProvider 使用 |
| 40 | + ↓ |
| 41 | +输出: 验证错误列表 + 完整符号表 + 符号映射 |
| 42 | +``` |
| 43 | + |
| 44 | +### 2. 关键组件 |
| 45 | + |
| 46 | +#### AstValidator (src/parser/astValidator.ts) |
| 47 | +核心验证器,负责: |
| 48 | +- 符号表管理 |
| 49 | +- 类型推断 |
| 50 | +- 错误收集 |
| 51 | + |
| 52 | +**关键字段**: |
| 53 | +```typescript |
| 54 | +private symbolTable: SymbolTable; // 符号表 |
| 55 | +private functionDeclarations: Map; // 函数声明缓存 |
| 56 | +private expressionTypes: Map; // 表达式类型缓存 |
| 57 | +private symbolMap: Map<string, SymbolInfo>; // 符号映射缓存 ✅ 已优化 |
| 58 | +``` |
| 59 | + |
| 60 | +**关键方法**: |
| 61 | +```typescript |
| 62 | +validate(ast: Program): ValidationError[] // 主验证流程 |
| 63 | +inferFunctionTupleReturnTypes(funcName): PineType[] // 用户函数元组推断 |
| 64 | +inferExpressionTypeWithLocals(expr, localTypes) // 独立类型推断 |
| 65 | +buildSymbolMap(): void // 构建符号映射 ✅ 已优化 |
| 66 | +getSymbolMap(): Map<string, SymbolInfo> // 获取符号映射 ✅ 已优化 |
| 67 | +``` |
| 68 | + |
| 69 | +#### SymbolTable (src/parser/symbolTable.ts) |
| 70 | +作用域管理,符号存储: |
| 71 | +```typescript |
| 72 | +class Scope { |
| 73 | + private symbols: Map<string, Symbol>; // 当前作用域符号 |
| 74 | + private parent: Scope | null; // 父作用域 |
| 75 | + private children: Scope[]; // 子作用域 |
| 76 | +} |
| 77 | + |
| 78 | +class SymbolTable { |
| 79 | + private globalScope: Scope; // 全局作用域 |
| 80 | + private currentScope: Scope; // 当前作用域 |
| 81 | + |
| 82 | + enterScope() // 进入子作用域 |
| 83 | + exitScope() // 退出到父作用域 |
| 84 | + getAllSymbols() // 递归获取所有作用域的符号 |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +#### InlayHintsProvider (src/inlayHintsProvider.ts) |
| 89 | +类型提示显示: |
| 90 | +- 解析文档 → 验证 → 获取缓存的符号映射 ✅ 已优化 |
| 91 | +- 递归遍历 AST,为每个变量声明创建 InlayHint |
| 92 | +- 使用 symbolMap 快速查找符号类型 |
| 93 | + |
| 94 | +**优化前**: |
| 95 | +```typescript |
| 96 | +const allSymbols = symbolTable.getAllSymbols(); |
| 97 | +const symbolMap = new Map(); // 每次都重建 |
| 98 | +for (const symbol of allSymbols) { |
| 99 | + symbolMap.set(`${symbol.name}:${symbol.line}`, symbol); |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +**优化后** ✅: |
| 104 | +```typescript |
| 105 | +const symbolMap = validator.getSymbolMap(); // 直接使用缓存 |
| 106 | +``` |
| 107 | + |
| 108 | +## 用户自定义函数元组推断 |
| 109 | + |
| 110 | +### 问题 |
| 111 | +```pine |
| 112 | +myFunction() => |
| 113 | + a = 10 |
| 114 | + b = true |
| 115 | + [a, b] |
| 116 | +
|
| 117 | +[x, y] = myFunction() // x 和 y 的类型是? |
| 118 | +``` |
| 119 | + |
| 120 | +### 解决方案: 独立类型推断系统 |
| 121 | + |
| 122 | +**核心方法**: `inferFunctionTupleReturnTypes(funcName: string): PineType[]` |
| 123 | + |
| 124 | +```typescript |
| 125 | +1. 从 functionDeclarations 获取函数 AST |
| 126 | +2. 创建临时 localTypes Map (不修改符号表) |
| 127 | +3. 添加参数类型到 localTypes |
| 128 | +4. 递归遍历函数体,收集变量类型: |
| 129 | + - 使用 inferExpressionTypeWithLocals() |
| 130 | + - 优先查找 localTypes,再查找符号表 |
| 131 | +5. 提取函数返回的数组字面量 |
| 132 | +6. 推断数组每个元素的类型 |
| 133 | +7. 返回类型数组 |
| 134 | +``` |
| 135 | + |
| 136 | +**关键优势**: |
| 137 | +- ✅ 不干扰符号表的作用域管理 |
| 138 | +- ✅ 不影响变量使用标记 |
| 139 | +- ✅ 支持嵌套作用域(if/for/while) |
| 140 | + |
| 141 | +### 示例流程 |
| 142 | + |
| 143 | +```pine |
| 144 | +detect_high_low(...) => |
| 145 | + cycle_len = bar_index - cycle_start + 1 // series<int> |
| 146 | + is_top = false // bool |
| 147 | + is_bottom = false // bool |
| 148 | + [is_top, is_bottom] |
| 149 | +
|
| 150 | +[kdj_top, kdj_bottom] = detect_high_low(...) |
| 151 | +``` |
| 152 | + |
| 153 | +**推断步骤**: |
| 154 | +1. 解构赋值触发 → 调用 `inferFunctionTupleReturnTypes('detect_high_low')` |
| 155 | +2. 创建 localTypes: `{'cycle_len': 'series<int>', 'is_top': 'bool', 'is_bottom': 'bool'}` |
| 156 | +3. 返回表达式: `[is_top, is_bottom]` |
| 157 | +4. 推断元素类型: `[inferType(is_top), inferType(is_bottom)]` → `['bool', 'bool']` |
| 158 | +5. 赋值给变量: `kdj_top: 'bool'`, `kdj_bottom: 'bool'` |
| 159 | + |
| 160 | +## Inlay Hints 显示机制 |
| 161 | + |
| 162 | +### 作用域查找问题 |
| 163 | + |
| 164 | +**问题**: `symbolTable.lookup()` 只能向上查找父作用域,无法访问已退出的子作用域。 |
| 165 | + |
| 166 | +``` |
| 167 | +全局作用域 |
| 168 | + ├─ 函数 A 作用域 (已退出) |
| 169 | + │ ├─ 变量 x |
| 170 | + │ └─ 变量 y |
| 171 | + └─ 当前作用域 (全局) |
| 172 | +
|
| 173 | +lookup('x') → 找不到! (因为函数作用域已退出) |
| 174 | +``` |
| 175 | + |
| 176 | +### 解决方案: Symbol Map |
| 177 | + |
| 178 | +```typescript |
| 179 | +// 获取所有符号(包括子作用域) |
| 180 | +const allSymbols = symbolTable.getAllSymbols(); |
| 181 | + |
| 182 | +// 创建 Map: "name:line" → symbol |
| 183 | +const symbolMap = new Map<string, Symbol>(); |
| 184 | +for (const symbol of allSymbols) { |
| 185 | + symbolMap.set(`${symbol.name}:${symbol.line}`, symbol); |
| 186 | +} |
| 187 | + |
| 188 | +// 查找时使用 name+line 作为 key |
| 189 | +const symbol = symbolMap.get(`${varName}:${varLine}`); |
| 190 | +``` |
| 191 | + |
| 192 | +**优势**: |
| 193 | +- ✅ 可以访问所有作用域的符号 |
| 194 | +- ✅ 使用行号区分同名变量 |
| 195 | +- ✅ O(1) 查找性能 |
| 196 | + |
| 197 | +## 性能优化建议 |
| 198 | + |
| 199 | +### 当前实现 |
| 200 | +```typescript |
| 201 | +validate(ast) { |
| 202 | + // 第一遍: 收集声明 |
| 203 | + for (const stmt of ast.body) { |
| 204 | + collectDeclarations(stmt); // 遍历整个 AST |
| 205 | + } |
| 206 | + |
| 207 | + // 第二遍: 验证 |
| 208 | + for (const stmt of ast.body) { |
| 209 | + validateStatement(stmt); // 再次遍历整个 AST |
| 210 | + } |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +### 可能的优化(未实现) |
| 215 | + |
| 216 | +#### 方案1: 单遍遍历 |
| 217 | +合并收集和验证,但需要处理前向引用: |
| 218 | +```typescript |
| 219 | +validate(ast) { |
| 220 | + // 预先收集函数声明 |
| 221 | + collectFunctionDeclarations(ast); |
| 222 | + |
| 223 | + // 单遍处理 |
| 224 | + for (const stmt of ast.body) { |
| 225 | + processStatement(stmt); // 收集 + 验证 |
| 226 | + } |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +⚠️ **问题**: Pine Script 支持在声明前使用函数,需要两阶段。 |
| 231 | + |
| 232 | +#### 方案2: 缓存优化 |
| 233 | +```typescript |
| 234 | +private expressionTypes: Map<Expression, PineType>; // ✅ 已实现 |
| 235 | +``` |
| 236 | +避免重复推断相同表达式的类型。 |
| 237 | + |
| 238 | +## 注意事项 |
| 239 | + |
| 240 | +### 1. 作用域管理 |
| 241 | +- ✅ 函数创建新作用域 |
| 242 | +- ✅ for/while 循环创建新作用域 |
| 243 | +- ❌ if/else 语句**不创建**新作用域(Pine Script 特性) |
| 244 | + |
| 245 | +### 2. 类型推断限制 |
| 246 | + |
| 247 | +**当前支持**: |
| 248 | +- ✅ 字面量类型推断 |
| 249 | +- ✅ 内置函数返回类型 |
| 250 | +- ✅ 二元/一元运算表达式类型 |
| 251 | +- ✅ 用户函数元组返回(数组字面量) |
| 252 | + |
| 253 | +**当前不支持**: |
| 254 | +- ❌ 间接返回数组 (返回变量而非字面量) |
| 255 | +- ❌ 条件返回不同类型 |
| 256 | +- ❌ 复杂的类型联合/交集 |
| 257 | + |
| 258 | +示例: |
| 259 | +```pine |
| 260 | +// ❌ 不支持 |
| 261 | +myFunc() => |
| 262 | + arr = [1, 2] |
| 263 | + arr // 返回变量 |
| 264 | +
|
| 265 | +// ✅ 支持 |
| 266 | +myFunc() => |
| 267 | + [1, 2] // 直接返回字面量 |
| 268 | +``` |
| 269 | + |
| 270 | +### 3. 参数类型假设 |
| 271 | + |
| 272 | +```typescript |
| 273 | +// 第一个参数假设为 series<float> |
| 274 | +const paramType = i === 0 ? 'series<float>' : 'unknown'; |
| 275 | +``` |
| 276 | + |
| 277 | +原因: Pine Script 函数通常第一个参数是指标序列。 |
| 278 | + |
| 279 | +⚠️ **改进空间**: 可以分析函数调用点的实参类型来推断参数类型。 |
| 280 | + |
| 281 | +### 4. Symbol Map Key 设计 |
| 282 | + |
| 283 | +使用 `name:line` 作为 key: |
| 284 | +```typescript |
| 285 | +const key = `${symbol.name}:${symbol.line}`; |
| 286 | +``` |
| 287 | + |
| 288 | +**优势**: 区分不同作用域的同名变量 |
| 289 | +**限制**: 假设同一行不会有多个同名变量声明(合理假设) |
| 290 | + |
| 291 | +## 测试 |
| 292 | + |
| 293 | +运行类型推断测试: |
| 294 | +```bash |
| 295 | +npm run build:tsc |
| 296 | +node test-type-inference.js |
| 297 | +``` |
| 298 | + |
| 299 | +测试文件: |
| 300 | +- `user-function-return-tuple.pine` - 用户函数元组测试 |
| 301 | +- `samples/detect-high-low.pine` - 真实场景测试 |
| 302 | + |
| 303 | +## 扩展点 |
| 304 | + |
| 305 | +### 添加新的类型推断规则 |
| 306 | + |
| 307 | +在 `AstValidator.inferExpressionType()` 中添加: |
| 308 | +```typescript |
| 309 | +case 'NewExpressionType': |
| 310 | + // 添加推断逻辑 |
| 311 | + type = inferNewType(expr); |
| 312 | + break; |
| 313 | +``` |
| 314 | + |
| 315 | +### 添加新的内置函数返回类型 |
| 316 | + |
| 317 | +在 `AstValidator.addKnownReturnTypes()` 中添加: |
| 318 | +```typescript |
| 319 | +'namespace.function': 'return_type', |
| 320 | +``` |
| 321 | + |
| 322 | +### 支持更复杂的函数分析 |
| 323 | + |
| 324 | +修改 `inferFunctionTupleReturnTypes()`: |
| 325 | +1. 支持追踪变量赋值链 |
| 326 | +2. 分析多个 return 路径 |
| 327 | +3. 递归分析嵌套函数调用 |
| 328 | + |
| 329 | +## 总结 |
| 330 | + |
| 331 | +当前实现在**简洁性**和**功能完整性**之间取得了良好平衡: |
| 332 | +- ✅ 两阶段验证清晰简单 |
| 333 | +- ✅ 独立类型推断不干扰符号表 |
| 334 | +- ✅ Symbol Map 解决作用域访问问题 |
| 335 | +- ✅ 支持主要使用场景(90%+) |
| 336 | + |
| 337 | +进一步优化需要权衡复杂度和收益。 |
0 commit comments