sml

SML { ❄ }

SML { ❄ } — SNOWARE Markup Language

SML { ❄ }

SML(SNOWARE Markup Language) 是一种声明式数据 / 配置格式,定位为 JSON / YAML / TOML 的轻量替代品。它强调可读性少仪式感:引号可选、块冒号可省、逗号可选、支持片段继承与契约校验。

仓库:snoware/sml | 多语言实现:Rust (swsml) · C (sml.c) · JavaScript (sml.mjs) · Lua (lib/sml.soup) · C++ · Python

特性一览

📖 SML 教科书

从零开始、循序渐进,也能当工具书随时查。→ 在线阅读

多语言实现对照

语言 仓库 / 文件 状态
Rust rust/ (swsml) ✅ 可用(契约系统完整,serde 桥接)
C c/sml.c ✅ 可用(契约系统已与 Rust 100% 对齐)
JavaScript js/sml.mjs ✅ 可用(零依赖 ESM,浏览器 / Node 通用,含契约与 playground)
Lua lua/lib/sml.lua ✅ 可用(Soup 生态 lib/sml.soup 同源)
C++ cpp/ ✅ 可用
Python rust/ 外另见 py 绑定 ✅ 可用

契约系统已在 Rust / C / JavaScript / C++ 四端对齐:同一份 CONFIG_CONTRACT 定义,四端解析行为一致。

快速使用

# 基础键值
firstName: John
age: 27
address:
{
    streetAddress: "21 2nd Street"
    state: NY
}

# 数组(逗号可选)
phoneNumbers: [ { type: home } { type: office } ]

# 片段继承
@base { region: cn-north-1 }
server web { &base port: 8080 }

Rust

use sml::parse;
let v = parse("name: John\nage: 27").unwrap();
assert_eq!(v["name"], "John");

C

#include "sml.h"
char err[256] = {0};
sml_value *v = sml_parse("name: John\nage: 27", err, sizeof(err));
/* v->type == SML_STR ("John") ... 用 sml_free(v) 释放 */

JavaScript

import { parse, stringify } from "./sml.mjs";
const v = parse('name: John\nage: 27');
console.log(stringify(v));

C++

#include "sml.hpp"
sml::Value v = sml::parse("name: John\nage: 27");
// v["name"].as_str() == "John" | v["age"].as_int() == 27
// 解析失败时抛 sml::ParseError(含行列位置)

C++ 实现 cpp/ 为头文件 + 单编译单元(sml.cpp),零第三方依赖;run_tests.pytest_comments.cpp / test_contracts.cpp 两套契约与注释测试。

契约系统(Contract)

契约是 SML 的「配置 Schema」:定义一组字段的类型、是否必填、默认值、取值范围,并在 @is 应用处做校验。非常适合「用 SML 做应用配置」的场景。

定义契约

@contract ResenderConfig loose {
    api_key:     str                # 必填字符串
    port:        int  default 8080 min 1 max 65535
    debug:       bool default false
    mode:        enum(active, disabled) default active
    tags:        array[str] ?      # 可选字符串数组
}

字段修饰符:

修饰符 含义
str / int / num / bool 字段类型
enum(a, b, c) 枚举,取值须在其中
array[T] 数组,元素类型 T(如 array[int]array[str]
?optional 可选字段
required 显式必填(默认即必填)
default <值> 缺失时填入该默认值(并自动视为可选)
min <数> / max <数> 数值取值范围(含端点)

应用契约

两种写法:

# 写法一:匿名块(顶层直接 @is
@contract Cfg loose { api_key: str port: int default 8080 }
@is Cfg
api_key: re_abc
port: 8080
# 写法二:字段级 / 块级 @is
server prod {
    @is Cfg
    api_key: re_prod
    port: 9090
}

严格 vs 宽松

组合契约(递归引用)

@contract Endpoint { host: str port: int }
@contract Service {
    name:  str
    main:  Endpoint          # 引用另一个契约
    peers: array[Endpoint]   # 契约数组
}
@is Service
name: gateway
main: { host: localhost port: 8080 }
peers: [ { host: a port: 1 } { host: b port: 2 } ]

契约校验失败会返回带位置的精确错误(如 contract: Service — 字段 main.port 大于最大值 65535),方便编辑器 / CLI 直接定位。

进阶特性

include 与命名空间(可裁剪设计)

设计哲学:从极简到丰富,功能可裁剪——基础能力默认开启,复杂能力(多目标 / 通配 / 正则 / 扩展名重写)必须显式 @feature enable 才生效,避免重蹈 YAML 过度复杂的覆辙。

基础形态

# 带扩展名  普通内联(内容直接并入当前作用域)
include "common.sml"
app: myapp

# 不带扩展名  默认命名空间 = 文件名(零样板隔离)
include "ui"          # 等价于 include "ui.sml" as ui
title: ui.title       # 用前缀访问

# 显式指定命名空间(覆盖默认)
include "ui.sml" as ui.form.widgets

规则很干净:带扩展名 = 内联;不带扩展名 = 命名空间(以文件名为 ns)。显式 as 始终优先。

点分路径(嵌套命名空间)

ns 支持 a.b.c 形式,等价于 Rust 的模块路径,展开为嵌套块 a { b { c { ... } } }

宏与契约也随命名空间隔离(2b)

命名空间不止隔离数据键值,还隔离宏与契约定义:被包含文件里定义的 @contract / @name / @base 必须以 ns. 前缀对外引用,调用方才能找到:

# widgets.sml 内部
@contract Button { label: str }
@name primary = { label: "OK" }

# 主文件引用时必须带前缀
@is ui.form.widgets.Button
button: &ui.form.widgets.primary

被包含文件内部对自身宏的自引用仍按本地名解析(无需前缀),只有对外暴露才需要 ns. 前缀——解析器按"当前命名空间栈"自动给宏注册表加前缀。

冲突即报错(不静默)

命名空间是独占作用域,绝不静默覆盖:

多目标与 import 别名

逗号分隔一次包含多个目标;importinclude 的等价写法(语义一致):

include "a.sml", "b.sml" as y, "c"          # 多目标,各自可有 as
import ui.buttons, admin.panel              # import 别名写法

通配与正则(需 feature 开启)

@feature enable glob
include "widgets/*.sml"        # glob 通配,按文件名排序逐个包含

@feature enable regex
include /plugins/.*\.sml/      # 正则匹配(/.../ 定界或 re: 前缀)

零拷贝切片(性能)

include 不会把文件内容深拷贝拼接成一大段文本。每个被包含文件读入后只持有其字符串切片,解析器消费一段「切片流」:遇到 include "x" as a.b 时插入零拷贝的开块字面量 a { b {、接入 x 的切片、再插入闭块 } }。文件内容只被解析一次,无中间大字符串,内存占用 = 各文件切片之和。

Feature 分层(可裁剪)

feature 能力 默认
0 include 基础 include "x.sml" 内联(带扩展名才内联)
1 namespace as ns + 点分路径 + 宏/契约隔离 + 冲突报错
1 implicit-ns 无扩展名 include "foo"as foo 默认命名空间
2 multi-include 逗号多目标 a, b, cimport 别名
2 glob-include * 通配 dir/*.sml
3 regex-include re: / /.../ 正则匹配
3 ext-rewrite -> .sml 把非 sml 文件当 sml 解析

默认仅开启 include + namespace + implicit-ns(极简三件套)。复杂能力需 @feature enable 显式 opt-in。

跨语言一致

Rust / C / JavaScript / Lua 四端共用同一语义:点分路径、宏/契约隔离、冲突报错、切片式零拷贝、feature 分层裁剪。

\u 转义

字符串支持 \u{XXXX}\uXXXX Unicode 转义,解析期转为 UTF-8:

label: "雪花 \u{2744} snow"

JSON 双向桥接

SML 与 JSON 同构(均为树状键值 / 数组),可无损互转:

import { parse, stringify } from "./sml.mjs";
// SML -> JSON
const obj = parse(smlText);            // 普通 JS 对象
const json = JSON.stringify(obj);
// JSON -> SML
const sml = stringify(JSON.parse(json));

Rust 侧通过 serde feature 提供 sml::serde::from_str / to_string,任意 #[derive(Deserialize)] 结构体都能一键反序列化。

落地应用

SML 已被多个真实项目采用:

resender 中的使用

resender 是一个 Rhai 驱动 + Slint GUI 的 Resend 邮件发送工具,它的配置文件 resender.sml 直接用 SML 契约保证结构正确:

@contract ResenderConfig loose {
    api_key:    str
    from:       str
    to:         array[str]
    subject:    str default "Hello"
    port:       int default 465  min 1 max 65535
    tls:        bool default true
}

@is ResenderConfig
api_key: re_xxxxxx
from: me@example.com
to: [ alice@example.com bob@example.com ]
subject: Weekly Report
port: 465
tls: true

其 Rust 端 src/config.rs 维护 CONFIG_CONTRACT 常量(即上面的契约文本),保存时把当前配置序列化回 SML 并自动附上 @is ResenderConfig,读取时再校验——这正是「契约即 Schema」的典型用法。

完整示例

一个接近真实的部署配置范例:

@version v1

@contract Service loose {
    name:    str
    port:    int  default 8080 min 1 max 65535
    debug:   bool default false
    peers:   array[str] ?
}

@base {
    region: cn-north-1
    timeout: 30
}

# 顶层应用契约
@is Service
name: gateway
port: 9090
debug: true
peers: [ auth billing ]

# 片段继承复用
service auth { &base port: 7100 name: auth-svc }
service billing { &base port: 7200 name: billing-svc }

# 嵌套块 + 数组
database: {
    url: "postgres://localhost:5432/app"
    pool: { min: 2 max: 16 }
}
features: [ logging metrics tracing ]

本地 Playground

不想装任何东西?直接在浏览器里试:SML Playground →

左侧写 SML(含契约),右侧实时显示解析结果或精确错误位置。