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。
sprig init hello
cd hello
sprig resolve
sprig run预期输出:Hello, Sprig!。下面是这个程序的实际源码:
# Hello world: typed functions and string concatenation.
func greet(name: String) -> String:
return "Hello, " + name + "!"
print(greet("Ada"))2. 绑定与类型
局部变量可从初始值推断类型。let 不能重新绑定,var 可以。条件必须是 Bool;Sprig 不会把整数或字符串当作真值。
# 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. 显式函数
每个函数参数和返回类型都要写明。调用者因此能从签名看清输入、输出与可能失败的地方。
# 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. 用类组织数据
类字段有明确类型;构造函数使用字段名。默认值可以省略对应实参,未知或缺少必填字段会报错。
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] 用于需要修改的阶段。显式转换会创建新的外层集合。下面的词频程序展示从文本到有序结果的完整小任务。
# 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。调用方必须捕获错误或继续声明它,失败不会悄悄变成默认值。
# 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 必须覆盖每种分支,分支绑定会保留负载的静态类型。可空值要先判空收窄。
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))# 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 文件,不依赖网络服务。先创建项目:
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。
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:
let count: Int = "three"预期诊断为 SPR-TYPE-ASSIGN,期望类型为 Int,实际类型为 String。通过 sprig check --json <file> 可取得机器可读位置与类型信息;用 sprig explain SPR-TYPE-ASSIGN --json 查询修复说明。修正源程序后再次检查,不要让工具替你插入可能改变含义的转换。
let count: Int = 3Agent 的短工作流是:读取 AGENTS.md 和当前参考 → sprig capabilities --json → 编写最小修改 → sprig check --json / sprig test → 检查 diff。具体命令是否存在,以所装版本的 capability 输出为准。
10. 调用普通 Java API
Sprig 编译到 JVM,能显式导入受支持的 Java 类与成员。遇到不确定的签名时,先查询本机 JDK,而不是根据记忆补全重载:
sprig api java.time.LocalDate --json接着看真实互操作示例和JVM 互操作指南。复杂框架仍由 Gradle、Maven 或 Loom 管理 classpath;Sprig 负责应用逻辑,必要时用窄 Java glue 连接当前未支持的 API 形状。
下一步
- 语言导览:按主题快速查阅语法和当前实现边界。
- 项目与依赖:本地包、Git/Maven 依赖、锁文件。
- 示例项目:从 Task Tracker 继续到 JSON CLI、SQLite、Web 和 Maven。
- Agent 工作流与诊断参考:查询工具、稳定诊断和可复现修复。
- 功能状态与已知限制:了解已实现能力与明确限制。
