数据截至 (上游 commit 5a6a215cab71)
OpenUI Lang — 语言与文法
本章讲什么: 先把 OpenUI Lang 这门语言「认全」——它有哪些语句、表达式、特殊调用;再讲文本怎么经「词法 → 表达式解析 → 语句分类」变成内部的 AST。这是后面所有章节的共同词汇。
1. 这门语言长什么样(零基础)
OpenUI Lang 的全部语法可以浓缩成一句话:「每行一条 名字 = 表达式 的语句,表达式可以是字面量、组件调用、或运算式。」
一个完整例子(# 示意,展示语言全貌):
$days = "7" // ① 可变状态变量
sales = Query("get_sales", {days: $days}, {rows: []}) // ② 数据查询
total = @Sum(sales.rows.value) // ③ 内置函数 + 数组拔字段
root = Card([picker, kpi, chart]) // ④ 入口,组件树
picker = Select("days", [SelectItem("7","7天"), SelectItem("30","30天")], null, null, $days)
kpi = TextContent("合计:" + total) // ⑤ 字符串拼接
chart = BarChart(sales.rows.label, sales.rows.value)
语言里只有几类「成分」,逐一认识:
| 成分 | 写法 | 含义 |
|---|---|---|
| 值语句 | name = Expr | 定义一个可被别处引用的名字 |
| 状态变量 | $name = default | 可读写的响应式变量(双向绑定用) |
| 组件调用 | Card(arg1, arg2) | PascalCase 名 + 位置参数 |
| 数据查询 | x = Query(tool, args, defaults, refresh?) | 声明一次读取 |
| 写操作 | x = Mutation(tool, args) | 声明一次写(按钮触发) |
| 内置函数 | @Sum(...)、@Each(...) | @ 前缀,数据/迭代工具 |
| 引用 | chart(裸名字) | 引用另一条语句的值 |
| 表达式 | a + b、x ? y : z、obj.field、arr[0] | 运算、三元、成员/下标访问 |
两条最容易踩的规则
- 参数按位置,不按名字。
Stack([children], "row", "l")对;Stack([children], direction:"row")错且会被静默吞掉。这是省 token 的核心代价。 - 每个名字(除 root)必 须被别人引用,否则被丢弃。 定义了
chart却没放进任何父组件的 children,它不会渲染——这是「可达性」规则,见第 02 章。
大小写有意义:PascalCase = 组件类型名,小写开头 = 变量引用。词法器靠首字母区分(parser/lexer.ts:347-349)。
2. AST:文本最终变成什么
解析的终点是一棵 AST(抽象语法树)。OpenUI Lang 的 AST 节点是一个判别联合,k 字段是判别符(parser/ast.ts:30-47,ASTNode)。挑关键几类:
k | 代表 | 例子 |
|---|---|---|
Comp | 组件/函数调用 | Header("Hi") |
Str/Num/Bool/Null | 字面量 | "hi" / 42 |
Arr/Obj | 数组/对象 | [a,b] / {k:v} |
Ref | 引用另一条语句 | chart |
StateRef | $变量 引用 | $days |
RuntimeRef | 指向 Query/Mutation(运行时解析) | — |
BinOp/UnaryOp/Ternary | 运算 / 取反 / 三元 | a+b / !x / c?a:b |
Member/Index | obj.field / arr[0] | — |
Assign | $x = ...(状态赋值) | — |
一个关键区分:有些节点在解析时就能算成死值,有些必须留到运行时。后者叫「运行时表达式节点」(StateRef/RuntimeRef/BinOp/Ternary/Member/Index/Assign/UnaryOp),由 isRuntimeExpr 判定(parser/ast.ts:66-80)。它们会原样保留进 ElementNode 的 props,等第 04 章的 evaluator 来算。
3. 三步:从文本到 AST
一次 parse(input) 内部走三步(parser/parser.ts:413-433,parse):
原始文本
│ preprocess: 去 markdown 围栏 + 去注释 + trim
▼
① autoClose ── 补全未闭合的串/括号(流式安全)
│
▼
② tokenize ── 词法:切成 Token 流
│
▼
③ split ── 按 depth-0 换行切成「一条条 RawStmt」
│
▼
对每条:parseExpression(Pratt) → classifyStatement
│
▼
Map<id, Statement> ── 符号表
│ materialize(见第 02 章)
▼
ParseResult { root, meta, stateDeclarations, queryStatements, ... }
下面拆开 ②③ 和分类这步;autoClose 与 materialize 放第 02 章细讲。
3.1 词法:tokenize
它要解决的小问题: 把 Header("Hi") 这串字符切成 [Type(Header), LParen, Str(Hi), RParen]