本章导读
第 1 章我们把工具装好了,还跑通了一行 print。但从"能跑一行"到"会写一个程序"之间,隔着一组基础规则:程序从哪里开始执行?语句怎么结束?注释怎么写?名字怎么起?
本章逐条讲清这些问题。学完后你应该能看懂任何一个 xlang 源文件的整体结构,知道每一行代码为什么在那个位置、以那种形式书写。这些规则在全书每一章都会用到,是名副其实的"地基"。
本章学习目标
掌握 func main(...) as int 入口形态与返回值约定;理解顶层语句与 main 的执行顺序;会用换行和分号分隔语句;会写三种注释(含 #doc 文档注释);了解标识符规则与关键字;对 import 建立初步概念。
2.1 源文件与编码
xlang 源文件约定两条:
- 扩展名是
.xl;
- 内容按 UTF-8 编码处理。
UTF-8 意味着源文件里可以直接出现中文——不止字符串里可以,标识符也可以(2.6 节会专门演示)。用任何现代编辑器(VS Code、记事本均可)保存时选择 UTF-8 编码即可,这也是当今绝大多数编辑器的默认编码。
大小写提醒:Windows 的文件系统不区分大小写,但 xlang 语言本身严格区分——这条针对的是代码里的名字,不是文件名。
2.2 程序入口 main
和 C 家族语言一样,xlang 程序有一个明确入口:main 函数。标准形态是:
func main(...) as int
print("程序启动")
return 0
end
逐部分拆解:
func:声明一个函数的关键字;
main:函数名。main 是约定俗成的入口名,编译器会特殊对待它;
(...):可变参数,表示"接收命令行传进来的所有参数",函数体内通过内置名字 arg 访问;
as int:返回类型是整数;
return 0:返回 0 表示程序正常结束;
end:函数体结束。xlang 不用大括号,所有块结构都用 end 收尾。
返回值就是进程退出码。这是可以实测验证的:
# ret.xl 内容:
# func main(...) as int
# print("准备退出")
# return 3
# end
> xl run ret.xl
准备退出
> echo 退出码是 %ERRORLEVEL% # Windows cmd;bash 用 echo $?
退出码是 3
约定和 C 完全一致:0 表示成功,非 0 表示某种失败。写脚本工具时,调用方就靠这个码判断成败。
命令行参数去哪了
第 1 章提过 xl run 可以用 -- 把参数传给程序。参数经由 ... 进入 arg,实测:
func main(...) as int
println("参数个数:", arg.length)
for a in arg
println("参数:", a)
end
return 0
end
> xl run args.xl -- Alice 42
参数个数: 3
参数: args.xl
参数: Alice
参数: 42
两个新知识点顺带预告:for ... in ... end 是遍历循环(第 9 章);arg.length 取数组长度(第 16 章)。现在只需注意一个细节:arg 的第一个元素是脚本路径本身,真正的用户参数从第二个开始——这和 C 的 argv[0] 传统一脉相承。
2.3 顶层语句与 main
第 1 章的 hello.xl 没有 main 也能跑,因为 xlang 允许在文件顶层直接书写可执行语句。那么顶层语句和 main 同时存在时,谁先谁后?实测:
print("顶层语句 1")
print("顶层语句 2")
func main(...) as int
print("main 函数内")
return 0
end
> xl run order.xl
顶层语句 1顶层语句 2main 函数内
输出顺序说明规则:先执行顶层语句,再调用 main。同时注意 print 不换行,三句话挤在了一起——想每句一行请用 println。
这个"顶层先行"的特性非常实用:脚本类程序往往不需要 main,写完语句就能跑;而当程序长大、需要退出码和参数处理时,再加上 main 也不冲突。
风格建议:小程序(几十行以内)用纯顶层语句最顺手;正式工具建议显式写出 func main(...) as int,退出码语义更完整。
2.4 语句分隔
xlang 里结束一条语句有两种方式:
- 换行——最常用,一行一条语句;
- 分号
;——把多条语句写在同一行时使用。
int a = 1
int b = 2; int c = 3 // 同一行两条语句,用分号
int d = (1 +
2) // 括号内部换行不结束语句
println(a, b, c, d)
> xl run sep.xl
1 2 3 3
第三条规则很重要:()、[]、{} 内部的换行不会结束语句。长表达式、长参数列表可以放心折行,编译器知道括号没合上、语句还没完。
反过来,同一行写两条语句而不加分号是语法错误——这是本章"常见错误"的第 1 个坑。
2.6 标识符规则
标识符就是你在代码里起的名字:变量名、函数名、类型名。规则如下:
- 由字母、数字、下划线组成,不能以数字开头;
- 大小写敏感:
Name 和 name 是两个不同的名字;
- 支持 UTF-8 标识符——中文可以直接做变量名、函数名;
- 不能使用关键字(保留字)。
int 年龄 = 18
string 名字 = "小明"
println(名字, 年龄)
int camelCase = 1 // 驼峰
int snake_case = 2 // 下划线
int _private = 3 // 下划线开头
println(camelCase, snake_case, _private)
> xl run ident.xl
小明 18
1 2 3
大小写敏感意味着 print 必须小写——写成 Print 编译器会报"未声明的标识符"(见常见错误坑 3)。
xlang 的关键字全集如下,混个眼熟即可,后面每章都会用到其中一批:
null true false
func const var auto typedef as
if then else elseif end
for in is while do
break continue return
async await yield
try catch finally throw assert
import export extern include define
class extends union derive get let
public private protected static native virtual override final
and or not
另外还有三个上下文相关的名字:self、base、create——它们在类里才有特殊含义(第 21 章),普通场景可以当普通名字用。而 object 不是保留字,它只是标准库注入的一个基础类名。
命名习惯建议
语言不强制风格,但社区惯例是:变量和函数用 snake_case 或 camelCase,类型用 PascalCase,常量可用全大写。中文标识符完全合法,但在需要与他人协作或嵌入 C 宿主的项目里,建议仍以英文标识符为主。
2.7 程序结构与 import
把本章零件组装起来,一个典型 xlang 源文件的结构是:
// 1. import 区(需要扩展库时)
import pkg.math
// 2. 声明区:常量、变量、函数、类型
const int LIMIT = 100
func helper(int x) as int
return x * 2
end
// 3. 顶层可执行语句(可选)
println("add_one(6) =", pkg.math.add_one(6))
// 4. main 入口(可选,与顶层语句共存时最后执行)
func main(...) as int
return 0
end
四个区域没有强制顺序要求,但按"import → 声明 → 语句 → main"组织可读性最好。
import 是本章唯一的"预告":标准库默认全部可用,不需要 import(所以 print 拿来就用);import 引入的是扩展库(package)。上面示例中的 pkg.math 就是发行包 extlibs 目录里自带的一个示例扩展库,这段代码笔者在发行包根目录实测输出:
> xl run imp.xl
add_one(6) = 7
现在只要建立概念:"import 把外部包的能力引进来"。扩展库的查找规则、版本管理和如何编写自己的 package,是第 35 章的主题。
深入细节
拾遗 1:顶层语句和 main 共存时,编译器做了什么
第 1 章说过 xl run 最终调用 xlang_main(xvalue arg)。当文件里同时存在顶层语句和显式 main 时,代码生成器会把你的 main 降低为内部函数 __xl6_user_main(xvalue arg),再由外层统一的 xlang_main 调度:先跑顶层语句,再调用 __xl6_user_main,并把它的返回值作为进程退出码。这解释了 2.3 节观察到的执行顺序——它不是运行时的巧合,而是代码生成阶段就排好的次序。反过来,如果 .xl 文件里没有 main,构建 exe 时编译器假定 main 由 C 源提供(混合编译场景,第 32 章);DLL 目标则完全不要求 main。
拾遗 2:arg[0] 为什么是脚本路径
2.2 节实测 arg 的第一个元素是 args.xl 这个脚本路径。这是刻意对齐 C 的 argv[0] 传统:命令行参数被转换为 xvalue 数组原样传入 xlang main,第 0 个位置保留"程序自身的名字"。好处是处理参数的代码写法在 xlang 和 C 之间可以平移——你的程序永远知道"用户参数从 arg[1] 开始"(用下标访问数组的方法见第 16 章)。
拾遗 3:关于 get / let / set 的命名限制
语言设计文档规定 get、let、set 不允许作为变量名、参数名、字段名、属性名或方法名——因为 get/let 是属性声明关键字(第 23 章),set 是内置集合类型名。笔者在写作本章时实测:当前 RC0 编译器对 int set = 1 这类简单声明并未拒绝。这是编译器实现与设计口径之间的一个小缺口,未来版本可能收紧。建议遵守设计口径:不要把这三个名字当作你自己的标识符使用,以免代码在版本收紧后失效。
⚠️ 常见错误
以下报错均为真实输出。
坑 1:同一行两条语句忘记加分号
# two.xl 内容:print("a") print("b")
> xl run two.xl
two.xl:1:12: [parse] 语法错误
原因:编译器在 print("a") 结束后遇到换行才算语句结束;同一行紧随其后的 print(第 12 列)无法解释,只能报语法错误。正确做法:一行一条语句(推荐),或写成 print("a"); print("b")。
坑 2:函数体忘记写 end
# noend.xl 内容:
# func main(...) as int
# print("x") ← 文件到此结束,没有 end
> xl run noend.xl
noend.xl:2:15: [parse] 语法错误
原因:编译器读完全部内容也没等到 end,报错位置指向它"意识到不对劲"的地方——最后一行代码的末尾(第 2 行第 15 列)。正确做法:每个 func、if、for、while、class 等块都必须有配对的 end。养成先写 end 再填中间内容的习惯。
坑 3:print 写成 Print——大小写敏感
# case.xl 内容:Print("hi")
> xl run case.xl
case.xl:1:1: [sem] undeclared identifier 'Print'
原因:语法上 Print("hi") 完全合法,所以通过了 [parse] 阶段;但语义检查([sem])时发现名叫 Print 的东西从未声明过——标准库里只有小写的 print。正确做法:改回小写。看到 undeclared identifier 时先检查拼写和大小写。
坑 4:块注释忘记闭合
# unclosed.xl 内容:
# /* 未闭合
# print("x")
> xl run unclosed.xl
unclosed.xl:2:11: [parse] 未闭合的多行注释
原因:/* 之后的所有内容——包括你的代码——都被吃进了注释,直到文件结束也没遇到 */。词法器给出了专门的错误"未闭合的多行注释",指向文件末尾。正确做法:补上 */。同类专属报错还有未闭合的字符串和时间字面量(第 3 章会遇到)。
本章小结
| 知识点 | 速查 |
| 源文件 | .xl 扩展名,UTF-8 编码。 |
| 程序入口 | func main(...) as int ... end;return 0 成功、非 0 失败;返回值即进程退出码。 |
| 命令行参数 | xl run prog.xl -- a b;arg 数组接收,arg[0] 是脚本路径,用户参数从 arg[1] 开始。 |
| 顶层语句 | 可直接写在文件顶层;与 main 共存时先执行顶层、再调用 main。 |
| 语句分隔 | 换行或 ;;() [] {} 内部换行不结束语句。 |
| 注释 | // 单行;/* */ 块注释可嵌套;#doc ... #end 结构化文档注释(desc/param/return/note/example)。 |
| 标识符 | 字母/数字/下划线,不以数字开头;大小写敏感;支持中文;不能用关键字。 |
| 命名限制 | get/let/set 按设计口径不作用户标识符;self/base/create 仅类内特殊。 |
| 程序结构 | import 区 → 声明区 → 顶层语句 → main;标准库免 import,扩展库才用 import。 |
| 观察工具 | xl parse file.xl 查看 AST;报错格式 文件:行:列: [阶段] 说明。 |
复习题
✍️ 先遮住答案想一想
func main(...) as int 中,...、as int、return 0 各自表示什么?
- 顶层语句与 main 同时存在时执行顺序如何?编译器是通过什么机制保证这个顺序的?
- xlang 有哪两种语句分隔方式?为什么长表达式在括号内可以直接折行?
- xlang 的块注释与 C 的块注释有什么关键区别?这个区别带来什么便利?
- 报错
demo.xl:1:1: [sem] undeclared identifier 'Print' 中,为什么阶段标记是 [sem] 而不是 [parse]?
编程练习
练习 1:带退出码的工具
编写 greet.xl:用 func main(...) as int 实现,如果有用户参数(arg.length 大于 1)就打印问候语并 return 0,否则打印用法说明并 return 1。分别用带参数和不带参数两种方式运行,验证退出码。
参考思路:用 if ... then ... else ... end(第 8 章正式讲,照猫画虎即可);bash 下用 echo $?、cmd 下用 echo %ERRORLEVEL% 查看退出码。体会"返回值即退出码"。
练习 2:看看语法树
写一个含变量声明和一次加法打印的小程序(如 int a = 1 加 print(a + 2)),执行 xl parse 观察 AST。在树中找到 var_decl、expr_stmt、binary "+" 三个节点,指出它们各自对应源码的哪一部分。
参考思路:AST 按缩进分层,每个节点带 @行:列 坐标。对照源码坐标找节点,建立"源码 → 树"的直觉——第 40 章讲编译管线时会回到这棵树。
练习 3:给函数写文档
给你最喜欢的一个自编小函数(例如温度换算)补上 #doc ... #end 文档注释,要求 desc:、param、return value:、example: 四个字段齐全,然后 xl run 验证程序仍然正常工作。
参考思路:#doc 必须紧贴函数声明之前;字段内容用缩进书写;example: 里的代码围栏只是文档文本,写错语法也不影响运行——这正是它与普通注释的区别之一。