第 2 章 / 共 58 章 卷一 起步

第一个程序与程序结构

逐行解剖一个完整的 xlang 程序,搞清楚每一部分扮演什么角色。

本章导读

第 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 函数。标准形态是:

main.xl
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,实测:

args.xl
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 同时存在时,谁先谁后?实测:

order.xl
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 里结束一条语句有两种方式:

  • 换行——最常用,一行一条语句;
  • 分号 ;——把多条语句写在同一行时使用。
sep.xl
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.5 三种注释

xlang 有三种注释,用途各不相同。

单行注释 //

从 // 到行尾都是注释,可独占一行,也可跟在代码后面。

块注释 /* ... */,支持嵌套

comment.xl
// 单行注释
/* 多行注释
   /* 嵌套的注释 */
   仍然注释 */
print("ok")  // 行尾注释

嵌套是 xlang 块注释区别于 C 的重要一点:C 的 /* */ 不能嵌套(第一个 */ 就收尾),注释掉一段本身含块注释的代码时会出错;xlang 会数配对,内层 /* */ 闭合后外层依然有效,实测上面的文件正常输出 ok。注释整块代码时只管首尾各包一层即可。

文档注释 #doc ... #end

写在函数(或类)前面的结构化文档,不只是给人看——它会进入编译器的语义校验:

doc.xl
#doc
desc:
    把两个整数相加。
param a:
    第一个加数。
param b:
    第二个加数。
return value:
    两数之和。
example:
    ```xlang
    print(add(1, 2))
    ```
#end
func add(int a, int b) as int
    return a + b
end

print(add(1, 2))
> xl run doc.xl 3

支持的字段:desc:(功能描述)、param 名字:(参数说明)、return 名字:(返回值说明)、note:(备注)、example:(示例)。字段内容按缩进收集多行文本;example: 里的代码围栏只作为文档文本保存,不会被当作代码解析。

初学阶段可以只用前两种注释。#doc 是写库和团队协作时的利器,知道它的存在即可,第 42 章工程实践会再用。

2.6 标识符规则

标识符就是你在代码里起的名字:变量名、函数名、类型名。规则如下:

  • 由字母、数字、下划线组成,不能以数字开头;
  • 大小写敏感:Name 和 name 是两个不同的名字;
  • 支持 UTF-8 标识符——中文可以直接做变量名、函数名;
  • 不能使用关键字(保留字)。
ident.xl —— 各种合法标识符
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;报错格式 文件:行:列: [阶段] 说明。

复习题

✍️ 先遮住答案想一想
  1. func main(...) as int 中,...、as int、return 0 各自表示什么?
  2. 顶层语句与 main 同时存在时执行顺序如何?编译器是通过什么机制保证这个顺序的?
  3. xlang 有哪两种语句分隔方式?为什么长表达式在括号内可以直接折行?
  4. xlang 的块注释与 C 的块注释有什么关键区别?这个区别带来什么便利?
  5. 报错 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: 里的代码围栏只是文档文本,写错语法也不影响运行——这正是它与普通注释的区别之一。