Skip to content

Sprig 入门教程 ​

这门语言面向人和编码 Agent。Sprig 不试图让编码 Agent 更聪明,而是尽量减少它需要猜测的内容。对人也一样:**Agent 友好,也应当方便审查。**教程从可运行的小程序开始,每章都引用仓库中的真实源码;文档门禁会编译、运行并核对输出。

已发布实验性 Beta SDK 为 v0.5.0-beta.1。本教程针对发行 SDK 验证;开始前请看发行状态,并用 sprig capabilities --json 查询安装版本的准确能力。

1. 安装并运行第一段程序 ​

安装发行页提供的 SDK 和 JDK 17 或更高版本。SDK 不包含 JDK。创建项目后,sprig run 会检查 Sprig、生成 Java、调用 javac,再启动 JVM。

sh
sprig init hello
cd hello
sprig resolve
sprig run

预期输出:Hello, Sprig!。下面是这个程序的实际源码:

spr
# Hello world: typed functions and string concatenation.

func greet(name: String) -> String:
    return "Hello, " + name + "!"

print(greet("Ada"))

2. 绑定与类型 ​

局部变量可从初始值推断类型。let 不能重新绑定,var 可以。条件必须是 Bool;Sprig 不会把整数或字符串当作真值。

spr
# let binds once; var may be reassigned.
let name = "Ada"
var visits = 0
visits += 1

# Local bindings infer their type from the initializer.
let greeting = "Hello, " + name
print(greeting)
print(visits)

练习:添加一个不可变的 name,再计算一条问候语。故意把 String 赋给 Int,观察编译器给出的错误码,然后修正它。

3. 显式函数 ​

每个函数参数和返回类型都要写明。调用者因此能从签名看清输入、输出与可能失败的地方。

spr
# Named functions declare parameter and return types, including -> Unit.
func add(a: Int, b: Int) -> Int:
    return a + b

func factorial(n: Int) -> Int:
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(add(2, 3))
print(factorial(5))

练习:添加一个接收两个 Int 并返回较大值的函数。为边界值写两个断言。

4. 用类组织数据 ​

类字段有明确类型;构造函数使用字段名。默认值可以省略对应实参,未知或缺少必填字段会报错。

spr
class Hero:
    let name: String
    var health: Int = 100

    func heal(amount: Int) -> Unit:
        health += amount

    func describe() -> String:
        return name + " (" + health.toString() + " hp)"

# Class construction uses named fields only.
let hero = Hero(name="Ada", health=80)
hero.heal(15)
print(hero.describe())

练习:为 Rectangle 加入面积方法。确认字段类型不匹配时,编译器会在运行前拒绝程序。

5. 集合与小型转换 ​

List[T] 是只读集合,MutableList[T] 用于需要修改的阶段。显式转换会创建新的外层集合。下面的词频程序展示从文本到有序结果的完整小任务。

spr
# Word counting: mutable maps, split/trim, loops, sorting.

func count_words(text: String) -> Map[String, Int]:
    let counts: MutableMap[String, Int] = {}
    for raw_word in text.split(" "):
        let word = raw_word.trim().toLowerCase()
        if word.isEmpty():
            continue
        let current = counts.get(word)
        if current == null:
            counts.set(word, 1)
        else:
            counts.set(word, current + 1)
    return counts.toMap()

let text = "the quick brown fox jumps over the lazy dog the fox"
let counts = count_words(text)
for word in counts.keys():
    let count = counts.get(word)
    if count != null:
        print(word + ": " + count.toString())

let words = counts.keys().toMutableList()
words.sort()
print("sorted keys: " + words.toString())

练习:忽略大小写,并说明标点是如何处理的。若需要完整自然语言分词,应选择专门的文本库。

6. 失败要写进类型 ​

会失败的函数声明 throws。调用方必须捕获错误或继续声明它,失败不会悄悄变成默认值。

spr
# Recoverable failures use typed errors: func ... throws T, throw, try/catch.
func checkout(quantity: Int) -> Int throws Error:
    if quantity <= 0:
        throw Error("quantity must be positive")
    return quantity * 3

try:
    print(checkout(4))
catch problem: Error:
    print("rejected: " + problem.message)

try:
    print(checkout(0))
catch problem: Error:
    print("rejected: " + problem.message)

练习:把一个缺失配置项当作 Error 抛出;在命令行边界捕获并打印有用信息。

7. 变体、match 与可空性 ​

variant 表达有限的分支集合。match 必须覆盖每种分支,分支绑定会保留负载的静态类型。可空值要先判空收窄。

spr
variant Expr:
    Literal(value: Int)
    Add(left: Expr, right: Expr)

enum Mode:
    Fast
    Careful

# match must cover every case; there is no default branch.
func eval(expr: Expr) -> Int:
    match expr:
        case Expr.Literal as node:
            return node.value
        case Expr.Add as node:
            return eval(node.left) + eval(node.right)

func label(mode: Mode) -> String:
    match mode:
        case Mode.Fast:
            return "fast"
        case Mode.Careful:
            return "careful"

let tree = Expr.Add(
    left=Expr.Literal(value=20),
    right=Expr.Literal(value=22)
)
print(eval(tree))
print(label(Mode.Careful))
spr
# T? marks expected absence. A checked value narrows to non-null inside the branch.
func find(words: List[String], target: String) -> String?:
    for word in words:
        if word == target:
            return word
    return null

let found = find(["alpha", "beta"], "beta")
if found != null:
    print("found " + found)
else:
    print("not found")

let missing = find(["alpha"], "gamma")
print(missing == null)

练习:给表达式 AST 新增一个 Negate 分支,再检查每个 visitor 是否都处理了它。

8. 构建本地 Task Tracker ​

接下来我们把前面学到的类型、集合、异常处理和文件/JSON API 组合成一个本地命令行任务清单。程序只读写 UTF-8 JSON 文件,不依赖网络服务。先创建项目:

sh
sprig init task-tracker
cd task-tracker
# 将下面的源码保存为 src/main.spr
sprig resolve
sprig run -- add "Read the Sprig tutorial"
sprig run -- add "Build a small tool"
sprig run -- list
sprig run -- done 1
sprig run -- list

下面是完整源码。它保存数据到当前工作目录的 tasks.json;设置 SPRIG_TASKS_FILE 可指定另一个本地文件。JSON 解码会检查字段类型,文件操作使用标准库的 UTF-8 API。

spr
import "@std/files.spr" as files
import "@std/json.spr" as json
import "@std/process.spr" as process
import java.io.IOException as IOException

class Task:
    let id: Int
    let title: String
    let done: Bool

func data_path() -> String:
    let configured = process.environment("SPRIG_TASKS_FILE")
    if configured != null:
        return configured
    return "tasks.json"

func required_field(value: json.Value, name: String) -> json.Value throws Error:
    match json.find_member(value, name):
        case json.Lookup.Found as found:
            return found.value
        case json.Lookup.Missing:
            throw Error("task is missing field " + name)
        case json.Lookup.NotObject:
            throw Error("task must be a JSON object")

func as_int(value: json.Value) -> Int throws Error:
    match value:
        case json.Value.Number as number:
            let parsed = number.text.toIntOrNull()
            if parsed != null:
                return parsed
            throw Error("task id must be an integer")
        case json.Value.Null:
            throw Error("task id must be an integer")
        case json.Value.Boolean:
            throw Error("task id must be an integer")
        case json.Value.Text:
            throw Error("task id must be an integer")
        case json.Value.Array:
            throw Error("task id must be an integer")
        case json.Value.Object:
            throw Error("task id must be an integer")

func as_text(value: json.Value) -> String throws Error:
    match value:
        case json.Value.Text as text:
            return text.value
        case json.Value.Null:
            throw Error("task title must be text")
        case json.Value.Boolean:
            throw Error("task title must be text")
        case json.Value.Number:
            throw Error("task title must be text")
        case json.Value.Array:
            throw Error("task title must be text")
        case json.Value.Object:
            throw Error("task title must be text")

func as_bool(value: json.Value) -> Bool throws Error:
    match value:
        case json.Value.Boolean as boolean:
            return boolean.value
        case json.Value.Null:
            throw Error("task done must be Boolean")
        case json.Value.Number:
            throw Error("task done must be Boolean")
        case json.Value.Text:
            throw Error("task done must be Boolean")
        case json.Value.Array:
            throw Error("task done must be Boolean")
        case json.Value.Object:
            throw Error("task done must be Boolean")

func decode_task(value: json.Value) -> Task throws Error:
    return Task(
        id=as_int(required_field(value, "id")),
        title=as_text(required_field(value, "title")),
        done=as_bool(required_field(value, "done"))
    )

func load_tasks() -> List[Task] throws Error, IOException:
    let path = data_path()
    if not files.exists(path):
        return []
    let document = json.parse(files.read_utf8(path))
    var rows: List[json.Value] = []
    match document:
        case json.Value.Array as array:
            rows = array.values
        case json.Value.Null:
            throw Error("task file must contain a JSON array")
        case json.Value.Boolean:
            throw Error("task file must contain a JSON array")
        case json.Value.Number:
            throw Error("task file must contain a JSON array")
        case json.Value.Text:
            throw Error("task file must contain a JSON array")
        case json.Value.Object:
            throw Error("task file must contain a JSON array")
    let tasks: MutableList[Task] = []
    for value in rows:
        tasks.append(decode_task(value))
    return tasks.toList()

func save_tasks(tasks: List[Task]) -> Unit throws Error, IOException:
    let values: MutableList[json.Value] = []
    for task in tasks:
        values.append(json.Value.Object(members=[
            json.Member(key="id", value=json.Value.Number(text=task.id.toString())),
            json.Member(key="title", value=json.Value.Text(value=task.title)),
            json.Member(key="done", value=json.Value.Boolean(value=task.done))
        ]))
    let body = json.stringify(json.Value.Array(values=values.toList())) + "\n"
    files.write_utf8(data_path(), body)

func usage() -> Unit:
    print("Usage: sprig run -- list | add <title> | done <id>")
    print("Tasks are stored in ./tasks.json")

let args = process.arguments()
try:
    if args.size() == 0:
        usage()
    elif args.get(0) == "list":
        for task in load_tasks():
            var marker = " "
            if task.done:
                marker = "x"
            print(task.id.toString() + " [" + marker + "] " + task.title)
    elif args.get(0) == "add" and args.size() == 2:
        let tasks = load_tasks()
        let task = Task(id=tasks.size() + 1, title=args.get(1), done=false)
        let updated: MutableList[Task] = tasks.toMutableList()
        updated.append(task)
        save_tasks(updated.toList())
        print("Added " + task.id.toString() + ": " + task.title)
    elif args.get(0) == "done" and args.size() == 2:
        let raw_id = args.get(1).toIntOrNull()
        if raw_id == null:
            throw Error("task id must be an integer")
        let tasks = load_tasks()
        let updated: MutableList[Task] = []
        var found = false
        for task in tasks:
            if task.id == raw_id:
                updated.append(Task(id=task.id, title=task.title, done=true))
                found = true
            else:
                updated.append(task)
        if not found:
            throw Error("no task with id " + raw_id.toString())
        save_tasks(updated.toList())
        print("Completed " + raw_id.toString())
    else:
        usage()
        throw Error("invalid command")
catch problem: IOException:
    throw Error("file operation failed: " + problem.message)
catch problem: Error:
    throw problem

该例适合学习与小型单进程数据,不提供并发写入协调。仓库中的同一源码和独立运行验收会检查添加、列出、完成和损坏 JSON 的行为。

9. 让编译器提供证据 ​

当程序不通过检查时,先读稳定诊断,而不是猜测类型规则。这个反例故意把文本放进 Int:

spr
let count: Int = "three"

预期诊断为 SPR-TYPE-ASSIGN,期望类型为 Int,实际类型为 String。通过 sprig check --json <file> 可取得机器可读位置与类型信息;用 sprig explain SPR-TYPE-ASSIGN --json 查询修复说明。修正源程序后再次检查,不要让工具替你插入可能改变含义的转换。

sprig
let count: Int = 3

Agent 的短工作流是:读取 AGENTS.md 和当前参考 → sprig capabilities --json → 编写最小修改 → sprig check --json / sprig test → 检查 diff。具体命令是否存在,以所装版本的 capability 输出为准。

10. 调用普通 Java API ​

Sprig 编译到 JVM,能显式导入受支持的 Java 类与成员。遇到不确定的签名时,先查询本机 JDK,而不是根据记忆补全重载:

sh
sprig api java.time.LocalDate --json

接着看真实互操作示例和JVM 互操作指南。复杂框架仍由 Gradle、Maven 或 Loom 管理 classpath;Sprig 负责应用逻辑,必要时用窄 Java glue 连接当前未支持的 API 形状。

下一步 ​

Last updated:

Sprig 采用 Apache-2.0 许可证 · v0.5.0-beta.1 已作为 prerelease 发布