Files
ViperOS/vpsdk/wiki/01-overview.md
2026-07-19 12:38:20 +08:00

19 KiB
Raw Permalink Blame History

01 - 语言概述与编译流程

语言定位

Viper 是一种 系统级编程语言,其语法基于 Python,但编译目标为 LLVM IR,最终生成原生机器码。Viper 不是 Python 的超集或子集,也不是"C 的语法糖"——它是一门拥有独立类型系统、独立编译模型、独立模块体系的语言。Viper 选择 Python 语法作为表达形式,是因为 Python 是人类公认的可读性最高的语言。

当前编译器 TransPyC(用 Python 实现)使用 Python 标准库 ast 解析源文件,将精力集中在语义翻译而非词法/语法分析上。但项目已在 includes/ast 中实现完全自研的 Python 解析器(词法器 + 语法器 + AST 构造,约 4176 LOCAstTest 综合测试通过),覆盖 CPython 几乎全部语法。这是向 TransPyV(用 Viper 重写编译器自身)完全自举的关键准备——自举后编译器将脱离 Python 运行时解析、类型检查、IR 生成都由原生机器码完成。详见 13-bootstrapping.md

而实际上,下文中所有提到的 C 语言都是为了便于使用 C 的底层开发者理解。由于历史原因,在旧版本中我们使用 C 来作为中间语言,如同旧版本的 C++ 一样,但现在 target="c" 的路径已经完全删除,C 路径永久不再受维护。

核心设计决策

  1. Python 语法LLVM 语义:源文件是完全合法的 Python 语法,但通过 t 模块的类型注解系统赋予完全不同的语义 —— t.CInt 不是 Pythonint,而是 LLVMi32
  2. 类型注解即编译指令Viper 的类型注解不是可选的提示,而是编译器生成 LLVM IR 的决定性依据。x: t.CInt 生成 i32x: t.CInt | t.CPtr 生成 i32*,但无注解时会自动推导,详见下文。
  3. 两阶段编译:先从源文件提取声明接口(.pyi + .stub.ll),再使用声明接口翻译源文件为含代码的 .ll。这是 Viper 区别于简单"PythonC"工具的关键架构
  4. SHA1 命名空间:每个源文件按内容 SHA1 哈希命名,非导出函数和结构体自动加上 SHA1 前缀,从根本上消除跨模块的符号冲突
  5. 万物皆数据:为便于底层开发,实际上所有的变量和类型一般并不适用于鸭子类型,但在语义层面我们会尽可能贴近鸭子类型。

Python 的关系

特性 Python Viper
类型系统 动态类型 静态类型(通过 t 模块注解,编译时确定)
内存管理 GC 自动回收 手动管理,无 GC,无运行时
运行时 解释执行 + 字节码 编译为原生机器码(通过 LLVM
对象模型 万物皆对象 @t.Object 启用 OOP@t.CVTable 为多态vtable@t.NoVTable 为非多态继承C++ 风格,零运行时开销),普通 class 仅为结构体布局
标准库 Python 标准库 Viper 标准库(includes/+ ViperOS SDK
空值 None None(编译为 NULL/零值)
模块系统 运行时 import 编译时解析,插入 .stub.llSHA1 命名空间隔离

与 C 的关系

Viper 编译后的代码在底层等价于 C 编译后的机器码(都经过 LLVM但 Viper 在语言层面提供了 C 所没有的能力:

  • SHA1 命名空间:自动消除符号冲突,无需手动管理 static/命名前缀
  • 类型组合语法t.CConst | t.CInt | t.CPtrconst int* 更具组合性
  • 声明式内联汇编c.Asm(f"mov {c.AsmOut(x, t.ASM_DESCR.OUTPUT_REG)}, rdi") 比裸 __asm__ 更安全,也更具可读性。
  • 结构化预处理c.CIfdef/c.CEndif() 替代 #ifdef/#endif
  • 面向对象@t.Object + @t.CVTable 提供 vtable 支持的多态 OOP@t.NoVTable 提供 C++ 风格的非多态继承(零 vtable 开销PEP 695 泛型 + 递归泛型继承实现强类型自引用结构
  • 存根驱动的模块系统.pyi 存根文件实现跨模块类型解析,无需头文件
  • 更多内容:额外更多的不依赖操作系统的函数和语法

两阶段编译流程

Viper 的编译由 Projectrans.py 驱动,分为两个阶段。这是 Viper 编译模型的核心,理解两阶段编译是理解 t.CDefinet.CExportt.CInline 等关键概念的前提。

源文件 (.py) ──────────────────────────────────────────────────────
    │                                                              │
    │  ┌─────────────── 阶段一:声明提取 ───────────────┐          │
    │  │                                               │          │
    │  │  1. 计算源文件 SHA1                            │          │
    │  │  2. 生成 <SHA1>.pyi签名存根                │          │
    │  │  3. 构建结构体注册表                           │          │
    │  │  4. 生成 <SHA1>.stub.llLLVM IR 声明)        │          │
    │  │                                                │          │
    │  └────────────────────────────────────────────────┘          │
    │                                                              │
    │  ┌─────────────── 阶段二:代码翻译 ───────────────┐          │
    │  │                                                │          │
    │  │  1. 加载所有 .pyi 和 .stub.ll                  │          │
    │  │  2. 构建共享符号表                             │          │
    │  │  3. 收集内联函数符号                           │          │
    │  │  4. 翻译源文件 → <SHA1>.ll含代码           │          │
    │  │  5. llc 编译 → <SHA1>.o目标文件            │          │
    │  │  6. ld.lld 链接 → 可执行文件                   │          │
    │  │                                                │          │
    │  └────────────────────────────────────────────────┘          │
    │                                                              │
    └──────────────────────────────────────────────────────────────┘

阶段一声明提取Phase1Generator

阶段一的目标是从每个源文件提取声明接口,使其他模块在阶段二翻译时能够正确解析跨模块引用。

步骤 1可达文件发现与拓扑排序

从入口文件(main.pyproject.json 指定)出发,通过 import 语句递归遍历,找出所有可达的 .py 源文件。然后对文件进行拓扑排序,确保被依赖的模块先被处理。

main.py → import serial → import drivers.serial.uart.serial → ...

步骤 2生成 .pyi 签名存根

对每个源文件,计算其内容的 SHA1 哈希16位然后调用 PythonToStubConverter 生成签名存根文件 <SHA1>.pyi

SHA1 计算方式

sha1 = hashlib.sha1(content.encode('utf-8')).hexdigest()[:16]

存根文件的内容规则

源文件元素 存根文件中的表示
t.CDefine 常量 保留原样(含赋值):MAX_SIZE: t.CDefine = 1024
t.CDefine 函数(返回 t.CDefine 的函数) 保留完整函数体(宏展开需要)
普通函数 仅保留签名,添加 `
类定义 保留成员类型注解和方法签名
全局变量 添加 t.CExtern`x: t.CExtern
c.CIfdef/c.CEndif() 等预处理指令 保留原样
import 语句 保留原样

关键设计t.CDefine 常量和 t.CDefine 函数在存根中保留完整定义(包括赋值和函数体),因为它们是编译时常量/宏,其他模块引用时需要完整展开。普通函数只保留签名并标记 c.State(仅声明),因为函数体在阶段二生成。

步骤 3构建结构体注册表

扫描所有已生成的 .pyi 文件,提取:

  • 结构体名称集合:所有非枚举、非异常的 class 定义
  • 枚举名称集合:继承 t.CEnum 的类
  • 异常名称集合:继承 Exception 的类
  • 结构体→SHA1 映射:记录每个结构体定义在哪个模块中

这个注册表用于阶段一的声明生成和阶段二的类型解析,确保跨模块的结构体引用使用正确的 SHA1 命名空间。

步骤 4生成 .stub.ll LLVM IR 声明

对每个 .pyi 文件,由 DeclarationGenerator 生成对应的 LLVM IR 声明文件 <SHA1>.stub.ll

声明生成规则

元素 生成的 LLVM IR
普通函数 declare void @"<SHA1>.foo"(i32) — 非 CExport 函数加 SHA1 前缀
CExport 函数 declare i32 @main() — CExport 函数不加前缀,全局可见
结构体 %"<SHA1>.ClassName" = type { i32, i32* } — 类型名加 SHA1 前缀
全局变量 @varname = external global i32
t.CDefine 常量 不生成声明(编译时常量,直接内联展开)
t.CTypedef 别名 不生成声明(类型别名,在类型解析时展开)
枚举 为每个枚举成员生成 @__config_EnumName_member = external global i32

增量编译:如果 <SHA1>.pyi<SHA1>.stub.ll 已存在,则跳过生成(缓存命中)。只有源文件内容变化导致 SHA1 变化时才重新生成。

阶段二代码翻译Phase2Translator

阶段二使用阶段一生成的声明接口,将源文件翻译为包含完整代码的 LLVM IR。

步骤 1加载声明接口

加载 temp/ 目录中所有的 .pyi.stub.ll 文件,构建:

  • sig_filesSHA1 → .pyi 文件路径映射
  • stub_filesSHA1 → .stub.ll 文件路径映射
  • sha1_mapSHA1 → 源文件相对路径映射

步骤 2构建共享符号表

一次性构建所有文件共享的符号表数据,避免每个文件重复加载。将所有 .pyi 存根和 .stub.ll 声明中的类型信息注册到统一的 SymbolTable 中。

步骤 3收集内联函数符号

扫描 includes/ 目录中被引用的模块,收集标记为 t.CInline 的函数。内联函数的完整 AST body 被保存,在翻译调用点时直接展开。值得注意的是,和 C 语言不同,t.CInline 不是优化建议,而是强制性的。

步骤 4翻译源文件

对每个源文件,使用 TransPyC 翻译器将 Python AST 翻译为 LLVM IR

  1. 将对应模块的 .stub.ll 声明嵌入到生成的 .ll 文件头部
  2. 所有被引用模块的 .stub.ll 声明也被嵌入
  3. 翻译 AST 节点为 LLVM IR 指令
  4. 输出 <SHA1>.ll 文件

步骤 5编译与链接

  • llc:将每个 .ll 编译为 .o 目标文件
  • ld.lld:将所有 .o 链接为最终可执行文件

SHA1 命名空间机制

SHA1 命名空间是 Viper 解决跨模块符号冲突的核心机制。

问题

在 C 语言中多个模块可能定义同名函数或结构体导致链接时符号冲突。C 的解决方案是 static(限制可见性)和命名前缀(手动避免冲突)。

Viper 的解决方案

Viper 使用源文件内容的 SHA1 哈希作为命名空间前缀:

源文件 A.pySHA1 = a1b2c3d4e5f6g7h8中定义
    class Point: x: t.CInt; y: t.CInt
    def draw(p: Point) -> t.CVoid: ...

源文件 B.pySHA1 = i9j0k1l2m3n4o5p6中也定义
    class Point: x: t.CFloat; y: t.CFloat  # 不同的结构体!
    def draw(p: Point) -> t.CVoid: ...

编译后:
    A.py 的结构体 → %"a1b2c3d4e5f6g7h8.Point" = type { i32, i32 }
    A.py 的函数   → declare void @"a1b2c3d4e5f6g7h8.draw"(%"a1b2c3d4e5f6g7h8.Point"*)

    B.py 的结构体 → %"i9j0k1l2m3n4o5p6.Point" = type { float, float }
    B.py 的函数   → declare void @"i9j0k1l2m3n4o5p6.draw"(%"i9j0k1l2m3n4o5p6.Point"*)

没有符号冲突——即使两个模块定义了同名类型和函数,它们的 LLVM IR 符号也是不同的。即便有两个内容完全相同的文件也不会冲突,他们会被识别为同一个文件,然后进行单次编译。

SHA1 前缀的豁免:t.CExport

标记为 t.CExport 的函数不加 SHA1 前缀,保持原始函数名。这是模块向外部暴露 API/ABI 的机制:

# main.py — 入口函数,必须全局可见
def main() -> t.CInt | t.CExport:
    return 0

# 编译后define i32 @main() — 无 SHA1 前缀
# serial.py — 驱动接口,对外暴露
def init() -> t.CVoid | t.CExport:
    serial_puts("init\n")

# 编译后define void @init() — 无 SHA1 前缀,其他模块可直接调用

SHA1 与增量编译

SHA1 同时服务于增量编译:

  • 源文件内容不变 → SHA1 不变 → .pyi.stub.ll 缓存命中,跳过阶段一
  • 源文件内容变化 → SHA1 变化 → 重新生成声明接口
  • temp/_sha1_map.txt 记录 SHA1 → 源文件路径的映射,供阶段二加载

理论上,如果一个未经变动的模块的依赖路径上存在变动的模块,暂时的方法是将其简单做 SHA1 替换,暨将旧 SHA1 替换为新 SHA1但不适用于签名改变的情况不过签名改变这个未经变动的模块必然也需要改变但是由于宏展开的存在不能完全保证不会发生错误情况。且依赖路径的 SHA1 替换成功性未经完整测试,可能出现未定义行为。

t.CDefine 深度解析

t.CDefine 是 Viper 中最特殊的类型——它不是数据类型,而是编译时元指令,控制编译器的代码生成行为。

t.CDefine 常量

MAX_SIZE: t.CDefine = 1024
PAGE_SIZE: t.CDefine = 4096
FA_READ: t.CDefine = 0x01
CPU_FEATURE_FPU: t.CDefine = (1 << 0)

编译行为

  1. 阶段一:存根生成器保留 t.CDefine 常量的完整定义(包括赋值值),因为其他模块可能引用此常量
  2. 阶段一.stub.ll 声明生成器跳过 t.CDefine 常量,不生成 external global 声明
  3. 阶段二:翻译器遇到 t.CDefine 常量的引用时,直接内联其值,不生成任何加载指令

这意味着 t.CDefine 常量在 LLVM IR 层面不存在——它们在编译时被完全展开,等价于 C 的 #define 宏。

t.CDefine 函数

当函数的返回类型注解包含 t.CDefine 时,该函数被编译器视为宏函数

def MAKE_FLAG(bit) -> t.CDefine:
    return (1 << bit)

def GET_PAGE_ORDER(size) -> t.CDefine:
    return size // PAGE_SIZE

编译行为

  1. 阶段一:存根生成器保留 t.CDefine 函数的完整函数体(不像普通函数那样只保留签名)
  2. 阶段二:翻译器遇到 t.CDefine 函数的调用时,将调用内联展开为函数体的计算结果

t.CDefine 函数在 LLVM IR 中不生成函数定义——它们是纯粹的编译时宏。

t.CDefine 与类型组合

t.CDefine 可以与具体类型组合使用,为常量指定底层类型:

CODE_SEG: t.CDefine | t.CUInt16T = 0x08
DATA_SEG: t.CDefine | t.CUInt16T = 0x10

这表示常量在类型检查时被视为 t.CUInt16Tuint16_t),但在代码生成时仍然内联展开。

t.CExport 深度解析

t.CExport 控制函数的符号可见性,是 SHA1 命名空间的豁免机制。

语义

修饰 LLVM IR 函数名 链接可见性 用途
无修饰 @<SHA1>.funcname 模块内部 模块私有函数
t.CExport @funcname 全局可见 对外暴露的 API
t.CExtern @funcname(仅声明) 外部定义 引用外部函数

使用场景

# 入口函数 — 必须全局可见,链接器需要找到它
def _start() -> t.CInt | t.CExport:
    return kernel_main()

# 驱动接口 — 其他模块/应用需要调用
def init() -> t.CVoid | t.CExport:
    uart_init()

# 内部辅助函数 — 不需要全局可见,自动加 SHA1 前缀
def _helper() -> t.CInt:
    return 42

t.CExport 在存根中的表现

.pyi 存根文件中,t.CExport 函数与普通函数一样只保留签名(添加 c.State,表示单纯声明),但 t.CExport 标记被保留在返回类型注解中。阶段一的 DeclarationGenerator 检查 t.CExport 标记来决定是否添加 SHA1 前缀。

t.CInline 深度解析

t.CInline 标记函数为内联函数,编译器在调用点直接展开函数体。

语义

def fast_add(a: t.CInt, b: t.CInt) -> t.CInt | t.CInline:
    return a + b

编译行为

  1. 阶段二预收集_collect_inline_symbols 扫描 includes/ 目录中被引用的模块,收集所有 t.CInline 函数的 AST body
  2. 翻译时展开:遇到内联函数调用时,将函数体的 AST 直接嵌入调用点,替换参数为实际值
  3. 不生成独立函数:内联函数不生成 LLVM IR 函数定义(除非也被非内联调用)

t.CInlinet.CDefine 函数的区别

特性 t.CInline t.CDefine 函数
展开时机 阶段二翻译时 阶段二翻译时
类型检查 完整的参数和返回类型检查 返回类型为宏,类型检查较弱
存根表示 签名 + c.State 完整函数体
LLVM IR 可能生成函数定义(如果有非内联调用) 不生成函数定义
适用场景 性能关键的短函数 编译时常量和宏计算

目标平台

默认目标三元组: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

Hello World 示例

import t, c

def main() -> t.CInt | t.CExport:
    print("Hello, ViperOS!\n")
    return 0

编译流程:

  1. 阶段一:计算 main.py 的 SHA1生成 .pyi 存根(def main() -> t.CInt | t.CExport | c.State: pass),生成 .stub.ll 声明(declare i32 @main(),因为是 CExport 不加前缀)
  2. 阶段二:翻译 main.py<SHA1>.ll,嵌入 print 对应的 puts/printf 声明,生成 define i32 @main() 函数体
  3. 编译llc.ll 编译为 .o
  4. 链接ld.lld 链接为可执行文件