30 KiB
01 - 语言概述与编译流程
注:本章为原本的
01-overview.md
语言定位
Viper 是一种 系统级编程语言,其语法基于 Python,但编译目标为 LLVM IR(注意,在计划中,我们期望迁移到 MLIR,这点是 Viper 的预期受众方向所要求的,Viper 期望走 边缘 AI等路线,仅提及),最终生成原生机器码。Viper 不是 Python 的超集或子集,也不是"C 的语法糖"——它是一门拥有独立类型系统、独立编译模型、独立模块体系的语言。Viper 选择 Python 语法作为表达形式,是因为至少在作者角度认为,ABC-LIKE 的可读性比 C-LIKE 要高得多,作者想要证明 ABC-LIKE 比 C-LIKE 更好,不过这是来自于作者很早期的不成熟想法。实际上,两者并没有实际可比性,二者语法均可以映射同一层的语义,其工程表达力不受语法家族限制。语法家族不决定你能表达什么,但决定你多容易表达。Viper 的赌注是:ABC-LIKE 的可读性与低样板收益,值得用工程手段(软关键字、自研前端、LSP 存根)去换取底层语义的落地能力。
当前编译器 TransPyC(用 Python 实现)早期使用 Python 标准库 ast 解析源文件,将精力集中在语义翻译而非词法/语法分析上。自举已完成:ViperC(用 Viper 重写编译器自身)先由 TPC 编译出 viperc.exe,再由其自举编译自身(Phase A/B/C 已跑通),编译已脱离 Python 运行时——解析、类型检查、IR 生成均由原生机器码完成。解析器为 includes/ast 的完全自研词法/语法器(词法器 + 语法器 + AST 构造,AstTest 综合测试通过),覆盖 CPython 几乎全部语法。详见 13-bootstrapping.md。
而实际上,下文中所有提到的 C 语言都是为了便于使用 C 的底层开发者理解。由于历史原因,在旧版本中我们使用 C 来作为中间语言,如同旧版本的 C++ 一样,但现在 target="c" 的路径已经完全删除,C 路径永久不再受维护。
成熟度与当前任务(重要):Viper 的语法在最新版本已经相当稳定,对现实工程已经可用——17-flagship-project.md 中的 ViperC(自举编译器)、ViperOS(裸机操作系统)、ViperMNIST(深度学习)等都是实证。因此当前的主要工作不是继续改动语法,而是更正内部的存量代码:把历史遗留的旧写法(
t.C*/c.*/T | t.CPtr等)迁移到新语法,详见 02-type-system.md 类型书写规范 与仓库根目录的新规范。Viper 的前身是一个玩具项目(把 Python 转成 C),但我们期望 Viper 不再是:它拥有独立的类型系统、两阶段编译模型、
SHA1命名空间模块体系与自举编译器,目标是成为一门可用于真实系统开发的编程语言。
核心设计决策
- Python 语法,LLVM 语义:源文件语法近似 CPython(并非完全合法,差异见下文「与 CPython 语法的关系」),但注解中的
int/uint/int64等类型名被赋予与Python完全不同的意义——它们不表示 Python 的动态整数,而是直接对应LLVM的定宽类型(如int=i32、int64=i64) - 类型注解即编译指令:Viper 的类型注解不是可选的提示,而是编译器生成
LLVM IR的决定性依据。x: int生成i32,x: int*生成i32*,但无注解时会自动推导,详见下文。 - 两阶段编译:先从源文件提取声明接口(
.vpi+.stub.ll),再使用声明接口翻译源文件为含代码的.ll。这是Viper区别于简单"Python转C"工具的关键架构 SHA1命名空间:每个源文件按内容SHA1哈希命名,非导出函数和结构体自动加上SHA1前缀,从根本上消除跨模块的符号冲突- 万物皆数据:为便于底层开发,实际上所有的变量和类型一般并不适用于鸭子类型,但在语义层面我们会尽可能贴近鸭子类型。
与 Python 的关系
| 特性 | Python |
Viper |
|---|---|---|
| 类型系统 | 动态类型 | 静态类型(由类型注解在编译时确定) |
| 内存管理 | GC 自动回收 |
手动管理,无 GC,无运行时 |
| 运行时 | 解释执行 + 字节码 | 编译为原生机器码(通过 LLVM) |
| 对象模型 | 万物皆对象 | 类内定义方法即启用 OOP(自动判定 IsOOP);virtual class 为多态(vtable),novirtual class 为非多态继承(C++ 风格,零运行时开销);普通 class(无方法)仅为结构体布局。@t.Object 默认不写、避免使用 |
| 标准库 | Python 标准库 |
Viper 标准库(includes/)+ ViperOS SDK |
| 空值 | None |
None(编译为 NULL/零值) |
| 模块系统 | 运行时 import |
编译时解析,插入 .stub.ll,SHA1 命名空间隔离 |
与 C 的关系
Viper 编译后的代码在底层等价于 C 编译后的机器码(都经过 LLVM),但 Viper 在语言层面提供了 C 所没有的能力:
- SHA1 命名空间:自动消除符号冲突,无需手动管理
static/命名前缀 - 类型书写:短名别名(
int/uchar/uint64/bool…)+T*指针 + 前缀描述符(const/volatile/__attribute__(...)),接近 C 且比链式t.CConst | t.CInt | t.CPtr更简洁 - 声明式内联汇编:
__asm__(f"mov {asmout(x, ASM_DESCR.OUTPUT_REG)}, rdi")比裸__asm__更安全、更具可读性(旧写法c.Asm/c.AsmOut等价)。 - 结构化预处理:原生关键字
cif/celif/celse缩进块替代#if/#ifdef/#ifndef(用defined(NAME)做宏存在性判断),块结束即#endif(无独立的c.CEndif())。 - 面向对象:类内方法即 OOP;
virtual class提供vtable支持的多态;novirtual class提供 C++ 风格的非多态继承(零 vtable 开销);PEP 695 泛型 + 递归泛型继承实现强类型自引用结构 - 存根驱动的模块系统:
.vpi存根文件实现跨模块类型解析,无需头文件 - 更多内容:额外更多的不依赖操作系统的函数和语法
FFI/导出新写法:推荐使用语言软关键字
extern/export——extern def foo(...) -> T仅声明外部函数(实现来自外部),export def以裸名导出符号。解析器会把它们改写为返回注解中的存储类标记(extern→T | t.State,export→t.CExport),因此t.State/t.CExtern/t.CExport标记并未废弃,仍是内部承载(手写可用)。
C-LIKE 语法(逐步支持)
Viper 的语法形态以 Python(ABC 系)为蓝本,但自研语法器 includes/ast 将逐步支持 C-LIKE 写法(C 风格的分隔与声明形式)。作者认为这是值得投入的方向,因此两种写法及其语义都会被长期维护、保持一致——不存在弃用某一端的计划。
就实际工程而言,仍然推荐 ABC-LIKE 写法:它与 Python 习惯一致、可读性最好,也是本仓库全部工程与文档采用的形态。
两阶段编译流程
Viper 的编译由入口驱动(TPC:Projectrans.py;VPC:App/main.vp),分为两个阶段。这是 Viper 编译模型的核心,理解两阶段编译是理解 t.CDefine、t.CExport、t.CInline 等关键概念的前提。
源文件 (.vp) ──────────────────────────────────────────────────────
│ │
│ ┌─────────────── 阶段一:声明提取 ───────────────┐ │
│ │ │ │
│ │ 1. 计算源文件 SHA1 │ │
│ │ 2. 生成 <SHA1>.vpi(签名存根) │ │
│ │ 3. 构建结构体注册表 │ │
│ │ 4. 生成 <SHA1>.stub.ll(LLVM IR 声明) │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────── 阶段二:代码翻译 ───────────────┐ │
│ │ │ │
│ │ 1. 加载所有 .vpi 和 .stub.ll │ │
│ │ 2. 构建共享符号表 │ │
│ │ 3. 收集内联函数符号 │ │
│ │ 4. 翻译源文件 → <SHA1>.ll(含代码) │ │
│ │ 5. llc 编译 → <SHA1>.o(目标文件) │ │
│ │ 6. ld.lld 链接 → 可执行文件 │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
阶段一:声明提取(Phase1Generator)
阶段一的目标是从每个源文件提取声明接口,使其他模块在阶段二翻译时能够正确解析跨模块引用。
步骤 1:可达文件发现与拓扑排序
从工程根出发,通过 import 语句递归遍历,找出所有可达的 .vp 源文件,再对文件进行拓扑排序,确保被依赖的模块先被处理。工程根由工程配置的 source_dir 指定(ViperC 与 ViperC/Test 用 project.vpj,Test/* 用同格式的 project.json);也可用 --src <入口文件>,此时构建配置由依赖闭包内的 viperbuild 提供(见 12-compilation-system.md)。不存在「只编一个文件」的单文件模式。
main.vp → import serial → import drivers.serial.uart.serial → ...
步骤 2:生成 .vpi 签名存根
对每个源文件,计算其内容的 SHA1 哈希(16位),然后调用 PythonToStubConverter 生成签名存根文件 <SHA1>.vpi。
SHA1 计算方式:
sha1 = hashlib.sha1(content.encode('utf-8')).hexdigest()[:16]
存根文件的内容规则:
| 源文件元素 | 存根文件中的表示 |
|---|---|
define 常量(旧写法 t.CDefine) |
保留原样(含赋值):define MAX_SIZE = 1024 |
宏函数(define F(...),或返回 t.CDefine) |
保留完整函数体(宏展开需要) |
| 普通函数 | 仅保留签名:def foo(x: int) -> void: pass |
类 / struct 定义 |
保留成员类型注解和方法签名 |
| 全局变量 | TPC 一代 PythonToStubConverter 会添加 t.CExtern(extern x: int 的旧标记形态);VPC 自研 StubGen/Converter.vp 不再添加——跨模块 external declare 由 VpiReader 依 vpi 全局自动注入 |
cif / celif / celse 条件编译块(含 defined(NAME) 谓词) |
保留原样(旧的 c.CIfdef 等已删除) |
import 语句 |
保留原样 |
关键设计:define 常量与宏函数在存根中保留完整定义(包括赋值和函数体),因为它们是编译时常量/宏,其他模块引用时需要完整展开。普通函数只保留签名(仅声明),因为函数体在阶段二生成。
步骤 3:构建结构体注册表
扫描所有已生成的 .vpi 文件,提取:
- 结构体名称集合:所有非枚举、非异常的
class定义 - 枚举名称集合:继承
t.CEnum的类 - 异常名称集合:继承
Exception的类 - 结构体→SHA1 映射:记录每个结构体定义在哪个模块中
这个注册表用于阶段一的声明生成和阶段二的类型解析,确保跨模块的结构体引用使用正确的 SHA1 命名空间。
步骤 4:生成 .stub.ll LLVM IR 声明
对每个 .vpi 文件,由 DeclarationGenerator 生成对应的 LLVM IR 声明文件 <SHA1>.stub.ll。
声明生成规则:
| 元素 | 生成的 LLVM IR |
|---|---|
| 普通函数 | declare void @"<SHA1>.foo"(i32) — 未 export 的函数加 SHA1 前缀 |
export 函数(内部 t.CExport) |
declare i32 @main() — 不加前缀,全局可见 |
| 结构体 | %"<SHA1>.ClassName" = type { i32, i32* } — 类型名加 SHA1 前缀 |
| 全局变量 | @varname = external global i32 |
define 常量(内部 t.CDefine) |
不生成声明(编译时常量,直接内联展开) |
typedef 别名(内部 t.CTypedef) |
不生成声明(类型别名,在类型解析时展开) |
| 枚举 | 仅旧 Python DeclarationGenerator 行为:为每个枚举成员生成 @__config_EnumName_member = external global i32;现枚举由 ClassGenEnum 注册为编译期常量,不再生成 |
增量编译:如果 <SHA1>.vpi 或 <SHA1>.stub.ll 已存在,则跳过生成(缓存命中)。只有源文件内容变化导致 SHA1 变化时才重新生成。
阶段二:代码翻译(Phase2Translator)
阶段二使用阶段一生成的声明接口,将源文件翻译为包含完整代码的 LLVM IR。
步骤 1:加载声明接口
加载 temp/ 目录中所有的 .vpi 和 .stub.ll 文件,构建:
sig_files:SHA1 →.vpi文件路径映射stub_files:SHA1 →.stub.ll文件路径映射sha1_map:SHA1 → 源文件相对路径映射
步骤 2:构建共享符号表
一次性构建所有文件共享的符号表数据,避免每个文件重复加载。将所有 .vpi 存根和 .stub.ll 声明中的类型信息注册到统一的 SymbolTable 中。
步骤 3:收集内联函数符号
扫描 includes/ 目录中被引用的模块,收集标记为内联的函数(旧标记 t.CInline)。内联函数的完整 AST body 被保存,在翻译调用点时直接展开。值得注意的是,和 C 语言不同,这种前端内联不是优化建议,而是强制性的。
注:上述
_collect_inline_symbols预收集机制属 TPC Python 侧实现;ViperC 无对应实现——t.CInline仅作为存储类标记,实际内联经@c.Attribute(t.attr.always_inline())。
步骤 4:翻译源文件
对每个源文件,使用对应翻译器(TPC TransPyC / VPC lib/core/Gen)将 AST 翻译为 LLVM IR:
- 将对应模块的
.stub.ll声明嵌入到生成的.ll文件头部 - 所有被引用模块的
.stub.ll声明也被嵌入 - 翻译 AST 节点为 LLVM IR 指令
- 输出
<SHA1>.ll文件
步骤 5:编译与链接
llc:将每个.ll编译为.o目标文件- 链接:将所有
.o链接为最终可执行文件。链接器由工程配置linker.cmd决定——ViperC 宿主工程为clang++;ld.lld/x86_64-none-elf用于 ViperOS 内核工程
SHA1 命名空间机制
SHA1 命名空间是 Viper 解决跨模块符号冲突的核心机制。
问题
在 C 语言中,多个模块可能定义同名函数或结构体,导致链接时符号冲突。C 的解决方案是 static(限制可见性)和命名前缀(手动避免冲突)。
Viper 的解决方案
Viper 使用源文件内容的 SHA1 哈希作为命名空间前缀:
源文件 A.vp(SHA1 = a1b2c3d4e5f6g7h8)中定义:
struct Point: x: int; y: int
def draw(p: Point*) -> void: ...
源文件 B.vp(SHA1 = i9j0k1l2m3n4o5p6)中也定义:
struct Point: x: float; y: float # 不同的结构体!
def draw(p: Point*) -> void: ...
编译后:
A.vp 的结构体 → %"a1b2c3d4e5f6g7h8.Point" = type { i32, i32 }
A.vp 的函数 → declare void @"a1b2c3d4e5f6g7h8.draw"(%"a1b2c3d4e5f6g7h8.Point"*)
B.vp 的结构体 → %"i9j0k1l2m3n4o5p6.Point" = type { float, float }
B.vp 的函数 → declare void @"i9j0k1l2m3n4o5p6.draw"(%"i9j0k1l2m3n4o5p6.Point"*)
没有符号冲突——即使两个模块定义了同名类型和函数,它们的 LLVM IR 符号也是不同的。即便有两个内容完全相同的文件也不会冲突,他们会被识别为同一个文件,然后进行单次编译。
SHA1 前缀的豁免:export
用 export 声明的函数(内部标记 t.CExport)不加 SHA1 前缀,保持原始函数名。这是模块向外部暴露 API/ABI 的机制:
# main.vp — 入口函数,必须全局可见
export def main() -> int:
return 0
# 编译后:define i32 @main() — 无 SHA1 前缀
# serial.vp — 驱动接口,对外暴露
export def init() -> void:
serial_puts("init\n")
# 编译后:define void @init() — 无 SHA1 前缀,其他模块可直接调用
SHA1 与增量编译
SHA1 同时服务于增量编译:
- 源文件内容不变 → SHA1 不变 →
.vpi和.stub.ll缓存命中,跳过阶段一 - 源文件内容变化 → SHA1 变化 → 重新生成声明接口
temp/_sha1_map.txt记录 SHA1 → 源文件路径的映射(仅人类可读);阶段二映射在内存中(Sha1Store/StubMergerStore)
理论上,如果一个未经变动的模块的依赖路径上存在变动的模块,暂时的方法是将其简单做 SHA1 替换,暨将旧 SHA1 替换为新 SHA1,但不适用于签名改变的情况,不过签名改变这个未经变动的模块必然也需要改变,但是由于宏展开的存在,不能完全保证不会发生错误情况。且依赖路径的 SHA1 替换成功性未经完整测试,可能出现未定义行为。
编译期常量与宏(define / t.CDefine)深度解析
t.CDefine 是编译时元指令(推荐用软关键字 define 书写),控制编译器的代码生成行为——它不是真实的数据类型。
define 常量
define MAX_SIZE = 1024
define PAGE_SIZE = 4096
define FA_READ = 0x01
define CPU_FEATURE_FPU = (1 << 0)
推荐写法是软关键字
define X = v(等价旧写法X: t.CDefine = v)。
编译行为:
- 阶段一:存根生成器保留 CDefine 常量的完整定义(包括赋值值),因为其他模块可能引用此常量
- 阶段一:
.stub.ll声明生成器跳过 CDefine 常量,不生成external global声明 - 阶段二:翻译器遇到 CDefine 常量的引用时,直接内联其值,不生成任何加载指令
这意味着 CDefine 常量在 LLVM IR 层面不存在——它们在编译时被完全展开,等价于 C 的 #define 宏。
宏函数(define F(...))
当函数用 define F(...) 声明(或返回类型注解含 t.CDefine)时,被编译器视为宏函数:
define MAKE_FLAG(bit):
return (1 << bit)
define GET_PAGE_ORDER(size):
return size // PAGE_SIZE
define F(...)会自动追加t.CDefine返回注解;等价旧写法def F(...) -> t.CDefine:。注意与define X = v(常量)区分:带(是宏函数、带=是常量。
编译行为:
- 阶段一:存根生成器保留宏函数的完整函数体(不像普通函数那样只保留签名)
- 阶段二:翻译器遇到宏函数的调用时,将调用内联展开为函数体的计算结果
宏函数在 LLVM IR 中不生成函数定义——它们是纯粹的编译时宏。
define X: T = v 与类型组合
define 可以带类型注解,为常量指定底层类型(内部等价于把 t.CDefine 与具体类型组合):
define CODE_SEG: uint16 = 0x08
define DATA_SEG: uint16 = 0x10
这表示常量在类型检查时被视为 uint16(uint16_t),但在代码生成时仍然内联展开。
旧写法
CODE_SEG: t.CDefine | t.CUInt16T = 0x08等价,手写可用但不推荐。
export 深度解析(内部标记 t.CExport)
export 软关键字控制函数的符号可见性,是 SHA1 命名空间的豁免机制(内部改写为返回注解中的 t.CExport 标记)。
语义
| 写法 | LLVM IR 函数名 | 链接可见性 | 用途 |
|---|---|---|---|
| 无修饰 | @<SHA1>.funcname |
模块内部 | 模块私有函数 |
export def f(...) |
@funcname |
全局可见 | 对外暴露的 API |
extern def f(...) |
@funcname(仅声明) |
外部定义 | 引用外部函数 |
手写等价形态:
def f(...) -> T | t.CExport(导出)、def f(...) -> T | t.State(外部声明)。
使用场景
# 入口函数 — 必须全局可见,链接器需要找到它
export def _start() -> int:
return kernel_main()
# 驱动接口 — 其他模块/应用需要调用
export def init() -> void:
uart_init()
# 内部辅助函数 — 不需要全局可见,自动加 SHA1 前缀
def _helper() -> int:
return 42
export 在存根中的表现
在 .vpi 存根文件中,export 函数与普通函数一样只保留签名(不添加 extern 存储类,表示单纯声明),但内部 t.CExport 标记被保留在返回类型注解中。阶段一的 DeclarationGenerator 检查该标记来决定是否添加 SHA1 前缀。
inline 深度解析(旧标记 t.CInline)
推荐写法是 inline 软关键字:inline def f(...) 等价于给函数加 @t.attr.always_inline(LLVM alwaysinline)——这是编译期提示落到 IR 函数属性,由 LLVM 决定实际内联,不是“调用点展开”。
inline def fast_add(a: int, b: int) -> int:
return a + b
旧的
-> T | t.CInline(t.CInline)不是内联指令、也不产生调用点展开,它只是一个存储类标记,已不推荐;做真正内联请用inline。
inline 与 define F(...) 的区别
| 特性 | inline def f(...) |
define F(...)(宏函数) |
|---|---|---|
| 展开时机 | LLVM 优化阶段(alwaysinline 属性) |
阶段二翻译时(前端内联) |
| 类型检查 | 完整的参数和返回类型检查 | 返回类型为宏,类型检查较弱 |
| 存根表示 | 仅签名(不添加 extern 存储类) |
完整函数体 |
| LLVM IR | 生成函数定义 + alwaysinline 属性 |
不生成函数定义 |
| 适用场景 | 性能关键的短函数 | 编译时常量和宏计算 |
目标平台
默认目标三元组:x86_64-none-elf
数据布局:e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128
为什么暂时深度绑定 Windows? 纯属计划与精力有限——工具链、链接器与运行时验证目前集中在 Windows(
llc+ mingwclang++),并非语言设计上的限制;实际大量代码是可跨平台复用的,后续按需逐步扩展到其它平台。
软关键字一览
以下均为语言层软关键字(由 includes/ast 解析器识别,不是 t 模块的类型注解),内部改写为既有标记:
| 软关键字 | 形态 | 展开 / 语义 |
|---|---|---|
extern |
extern def f(...) / extern NAME: T |
返回注解加 t.State;仅声明 / 外部全局(禁初值) |
export |
export def f(...) / export NAME: T [= v] |
加 t.CExport;裸名导出 |
static |
static def f(...) / static x: T = v |
类型联合最左插 t.CStatic;禁止跨模块 |
inline |
inline def f(...) |
合成 @t.attr.always_inline |
virtual / novirtual |
virtual class C / novirtual def m |
类级 = 该类所有方法的默认级别;方法级 = 只改单个方法(覆盖类级默认) |
const / volatile |
const T / volatile T(注解前缀) |
合成 t.CConst / t.CVolatile;volatile 落 LLVM volatile |
__attribute__(...) |
注解前缀或 @__attribute__(...) |
脱糖为 t.attr.x(比 GCC 少一层括号) |
define |
define X = v / define X: T = v |
编译期常量(自动加 t.CDefine) |
define |
define F(...) -> T |
宏函数(自动加 t.CDefine 返回注解) |
typedef |
typedef A = B |
等价 A: t.CTypedef = B |
enum / union / renum |
enum E: / union U: / renum R: |
等价 class X(t.CEnum / t.CUnion / t.REnum) |
struct |
struct S: |
纯数据结构体:字段规则同 class,但禁 virtual/novirtual、禁 def;可嵌套、可在 renum 内作变体 |
array |
array[E, N](仅注解位置) |
等价 t.CArray[E, N] |
[N]T |
注解前缀数组 | 等价 t.CArray[T, N] |
变量级
static/extern/export与函数级同源;extern变量不允许带初始值。旧的注解位static(名字: static 类型)已不再支持。
t.C*/c.*正在被逐步淘汰:类型改短名别名(int/uchar/uint64/bool…),指针改T*,指针操作改&/*,详见 02-type-system.md 类型书写规范。
inline关键字:写inline def f(...),等价于给函数加@t.attr.always_inline(LLVMalwaysinline)——它是编译期提示落到 IR 函数属性,不是“调用点展开”。旧的-> T | t.CInline只是存储类标记,不产生内联。
与 CPython 语法的关系(Vipeye)
Viper 原本的语法完全符合 CPython——源文件是合法的 Python 语法,早期直接用 Python 的 ast 解析。转折点是 Vipeye(基于 pyright 的 VSCode Viper LSP 分析插件)的建立。
这个转折是正向的:正因为有了 Vipeye,Viper 的语法设计不再受「必须保持为合法 Python」这一约束,才敢于引入一批 Python 语法容纳不下的关键字与写法——软关键字 extern/export/static/inline/virtual/novirtual/volatile/define/typedef/enum/union/renum/struct/array,后缀指针 T*、前缀数组 [N]T、描述符 const/volatile/__attribute__(...)、指针算子 &/*。也正因为如此,这批语法已经与标准 Python 有了部分出入,而这种出入是设计自由度的体现,而非兼容性缺陷。
Vipeye 时刻跟进 Viper 的语法更改:语言侧新增/调整关键字与语法后,Vipeye(经
t.vpi/c.vpi存根)会同步跟进,保证 LSP 的诊断与补全与现行编译器一致。结论:把 Viper 当 Python 写「大概率能解析」,但不要假定完全等价;语法请以本 wiki 为准。
附带项目与工具
| 项目 | 说明 |
|---|---|
| Vipeye | 基于 pyright 的 VSCode Viper LSP 分析插件(外部项目,本工作区不含其源码) |
| VET | Viper Environment Tool:校验产物确由 ViperC 工具链生成,并常驻扫盘发现 Viper 产物 |
| Venom | Windows 桌面应用最小模板(UTF-8 控制台 + Hello World → app.exe) |
| ViperTemplateProject | 同上定位的 Windows 桌面应用模板 |
| ViperApps | 示例应用集(Qt6 / MNIST / FLTK / Requests / OpenSSL / ViperOS 等) |
| Test | TPC 测试工程集(68 个,project.json);ViperC/Test 为 VPC 自测套件 |
| TransPyC | 第一代编译器(CPython 实现,自举种子) |
| ViperC | 第二代编译器(Viper 自举,现行编译器) |
| Tools | 辅助脚本;sendaiandsplit.py 可合并/分割源码,ai 模式产出带弱时效性声明的 AI 快照 |
| wiki | 本文档;MergeWiki.py 可把全部章节合并成 AI 快照 |
章节延伸
- 交互式 REPL(
--repl)见 15-repl.md; - 栈回溯与调试信息(
--traceback/ DWARF)见 16-traceback.md; - 用 Viper 写成的旗舰工程(ViperC / VET / ViperOS / ViperMNIST)见 17-flagship-project.md。
Hello World 示例(标准写法)
export def main() -> int:
print("Hello, World!")
return 0
不要把下面这种旧写法当标准示例(它混用了
t模块与旧注解):import t, c def main() -> t.CInt | t.CExport: print("Hello, ViperOS!\n") return 0标准写法以
export def main() -> int:为准。
编译流程:
- 阶段一:计算
main.vp的 SHA1,生成.vpi存根(def main() -> int: pass),生成.stub.ll声明declare i32 @main() - 阶段二:翻译
main.vp为<SHA1>.ll,嵌入print输出所需的声明,生成define i32 @main()函数体 - 编译:
llc将.ll编译为.o - 链接:按工程配置的
linker.cmd链接为可执行文件
main 的特殊处理
main 无论是否写 export,都会被按裸名(不加模块 SHA1 前缀)处理、导出为 @main——因为链接器 / CRT 需要找到它。这是编译器内建特例,后续可能会更改(届时可能要求显式 export)。
print 是编译器内建
print(...) 目前是编译器内建:做类型感知的格式化输出(str/%s、int、float、struct 各按类型选格式),默认追加换行,最终经 printf 族函数输出;它不是普通函数调用,也不直接映射到 printf 本身。后续计划把它下降到标准库、用 Viper 代码实现(届时语义以标准库为准)。