use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use crate::value::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSpec {
Any,
ContractRef(String),
Str,
Int,
Num,
Bool,
Array(Box<TypeSpec>),
Enum(Vec<String>),
}
impl TypeSpec {
fn name(&self) -> String {
match self {
TypeSpec::Any => "any".into(),
TypeSpec::Str => "str".into(),
TypeSpec::Int => "int".into(),
TypeSpec::Num => "num".into(),
TypeSpec::Bool => "bool".into(),
TypeSpec::Array(inner) => format!("[{}]", inner.name()),
TypeSpec::Enum(vals) => format!("enum [{}]", vals.join(" ")),
TypeSpec::ContractRef(name) => name.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct FieldSpec {
pub ty: TypeSpec,
pub required: bool,
pub default: Option<Value>,
pub min: Option<f64>,
pub max: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Contract {
pub name: String,
pub fields: BTreeMap<String, FieldSpec>,
pub allow_extra: bool,
}
fn check_type(
contract: &str,
field: &str,
spec: &FieldSpec,
v: &Value,
contracts: &BTreeMap<String, Contract>,
) -> Result<(), String> {
if let TypeSpec::ContractRef(ref_name) = &spec.ty {
return match v {
Value::Object(_) => {
let mut sub = match v {
Value::Object(m) => m.clone(),
_ => unreachable!(),
};
let target = contracts.get(ref_name).ok_or_else(|| {
format!(
"sml: 字段 `{}` 引用了未定义的契约 `{}`(契约 `{}`)",
field, ref_name, contract
)
})?;
apply_contract(target, &mut sub, contracts)?;
Ok(())
}
_ => Err(format!(
"sml: 字段 `{}` 应为块并按契约 `{}` 校验,实际为 {}(契约 `{}`)",
field,
ref_name,
value_kind(v),
contract
)),
};
}
let ok = match (&spec.ty, v) {
(TypeSpec::Any, _) => true,
(TypeSpec::Str, Value::Str(_)) => true,
(TypeSpec::Int, Value::Int(_)) => true,
(TypeSpec::Num, Value::Int(_)) | (TypeSpec::Num, Value::Float(_)) => true,
(TypeSpec::Bool, Value::Bool(_)) => true,
(TypeSpec::Enum(vals), Value::Str(s)) => vals.iter().any(|x| x == s),
(TypeSpec::Enum(vals), Value::Int(i)) => vals.iter().any(|x| x == &i.to_string()),
(TypeSpec::Array(inner), Value::Array(items)) => items.iter().all(|it| {
check_type(
contract,
field,
&FieldSpec { ty: (**inner).clone(), required: true, default: None, min: None, max: None },
it,
contracts,
)
.is_ok()
}),
_ => false,
};
if !ok {
return Err(format!(
"sml: 字段 `{}` 类型应为 {},实际为 {}(契约 `{}`)",
field,
spec.ty.name(),
value_kind(v),
contract
));
}
if spec.min.is_some() || spec.max.is_some() {
let n = match v {
Value::Int(i) => Some(*i as f64),
Value::Float(f) => Some(*f),
_ => None,
};
if let Some(n) = n {
if let Some(lo) = spec.min {
if n < lo {
return Err(format!(
"sml: 字段 `{}` 值 {} 小于下界 {}(契约 `{}`)",
field, n, lo, contract
));
}
}
if let Some(hi) = spec.max {
if n > hi {
return Err(format!(
"sml: 字段 `{}` 值 {} 大于上界 {}(契约 `{}`)",
field, n, hi, contract
));
}
}
}
}
Ok(())
}
fn value_kind(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Str(_) => "str",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn apply_contract(
c: &Contract,
node: &mut BTreeMap<String, Value>,
contracts: &BTreeMap<String, Contract>,
) -> Result<(), String> {
if !c.allow_extra {
for k in node.keys() {
if !c.fields.contains_key(k) {
return Err(format!(
"sml: 字段 `{}` 未在契约 `{}` 中声明(严格模式;如需允许额外字段请在契约名后写 `loose`)",
k, c.name
));
}
}
}
for (k, spec) in &c.fields {
match node.get(k) {
None => {
if let Some(d) = &spec.default {
node.insert(k.clone(), d.clone());
} else if spec.required {
return Err(format!(
"sml: 字段 `{}` 必填但缺失(契约 `{}`)",
k, c.name
));
}
}
Some(v) => {
if matches!(spec.ty, TypeSpec::ContractRef(_)) {
check_type(&c.name, k, spec, v, contracts)?;
let mut sub = match v {
Value::Object(m) => m.clone(),
_ => unreachable!("check_type 已保证为块"),
};
check_type_contract_ref(&c.name, k, spec, &mut sub, contracts)?;
node.insert(k.clone(), Value::Object(sub));
} else {
check_type(&c.name, k, spec, v, contracts)?;
}
}
}
}
Ok(())
}
fn check_type_contract_ref(
contract: &str,
field: &str,
spec: &FieldSpec,
sub: &mut BTreeMap<String, Value>,
contracts: &BTreeMap<String, Contract>,
) -> Result<(), String> {
let ref_name = match &spec.ty {
TypeSpec::ContractRef(n) => n.clone(),
_ => return Ok(()),
};
let target = contracts.get(&ref_name).ok_or_else(|| {
format!(
"sml: 字段 `{}` 引用了未定义的契约 `{}`(契约 `{}`)",
field, ref_name, contract
)
})?;
check_type(contract, field, spec, &Value::Object(sub.clone()), contracts)?;
apply_contract(target, sub, contracts)
}
#[derive(Debug, Clone, PartialEq)]
enum Tok {
LBrace, RBrace, LBrack, RBrack, Comma, Colon, At, Str(String), Word(String), }
fn tokenize(text: &str) -> Result<Vec<Tok>, String> {
let mut toks = Vec::new();
let mut chars = text.chars().peekable();
let mut buf = String::new();
let mut flush = |buf: &mut String, toks: &mut Vec<Tok>| {
if !buf.is_empty() {
toks.push(Tok::Word(std::mem::take(buf)));
}
};
while let Some(c) = chars.next() {
match c {
'#' => {
for c2 in chars.by_ref() {
if c2 == '\n' {
break;
}
}
}
'-' => {
if chars.peek() == Some(&'-') {
chars.next(); for c2 in chars.by_ref() {
if c2 == '\n' {
break;
}
}
} else {
buf.push(c);
}
}
'/' => {
match chars.peek() {
Some('/') => {
chars.next(); for c2 in chars.by_ref() {
if c2 == '\n' {
break;
}
}
}
Some('*') => {
chars.next(); loop {
match chars.next() {
Some('*') => {
if chars.peek() == Some(&'/') {
chars.next();
break;
}
}
Some(_) => {}
None => break,
}
}
}
_ => buf.push(c),
}
}
'_' => {
if chars.peek() == Some(&'*') {
chars.next(); loop {
match chars.next() {
Some('*') => {
if chars.peek() == Some(&'_') {
chars.next();
break;
}
}
Some(_) => {}
None => break,
}
}
} else {
buf.push(c);
}
}
'"' => {
flush(&mut buf, &mut toks);
let mut s = String::new();
loop {
match chars.next() {
Some('"') => break,
Some('\\') => {
match chars.next() {
Some('n') => s.push('\n'),
Some('t') => s.push('\t'),
Some('r') => s.push('\r'),
Some('0') => s.push('\0'),
Some('"') => s.push('"'),
Some('\\') => s.push('\\'),
Some('u') => {
let mut hex = String::new();
if chars.peek() == Some(&'{') {
chars.next();
for c2 in chars.by_ref() {
if c2 == '}' {
break;
}
hex.push(c2);
}
} else {
for _ in 0..4 {
if let Some(c2) = chars.next() {
hex.push(c2);
} else {
return Err(format!(
"sml: 字符串转义 \\u 缺少足够的十六进制数字(期望 4 位,得 {hex:?})"
));
}
}
}
if hex.is_empty() {
return Err("sml: 字符串转义 \\u 后缺少十六进制数字".to_string());
}
let cp = u32::from_str_radix(&hex, 16).map_err(|_| {
format!("sml: 字符串转义 \\u 含非十六进制数字:{hex:?}")
})?;
let ch = char::from_u32(cp).ok_or_else(|| {
format!("sml: 字符串转义 \\u 得到非法 Unicode 码点:U+{cp:04X}")
})?;
s.push(ch);
}
Some(other) => s.push(other),
None => {
return Err(
"sml: 字符串中的转义符 \\ 后遇到文件结束".to_string()
)
}
}
}
Some(other) => s.push(other),
None => return Err("sml: 字符串未闭合(缺少结束引号 \")".to_string()),
}
}
toks.push(Tok::Str(s));
}
'{' => {
flush(&mut buf, &mut toks);
toks.push(Tok::LBrace);
}
'}' => {
flush(&mut buf, &mut toks);
toks.push(Tok::RBrace);
}
'[' => {
flush(&mut buf, &mut toks);
toks.push(Tok::LBrack);
}
']' => {
flush(&mut buf, &mut toks);
toks.push(Tok::RBrack);
}
',' => {
flush(&mut buf, &mut toks);
toks.push(Tok::Comma);
}
':' => {
flush(&mut buf, &mut toks);
toks.push(Tok::Colon);
}
'@' => {
if buf.is_empty() {
toks.push(Tok::At);
} else {
buf.push(c);
}
}
' ' | '\t' | '\n' | '\r' => {
flush(&mut buf, &mut toks);
}
_ => {
buf.push(c);
}
}
}
flush(&mut buf, &mut toks);
Ok(toks)
}
fn coerce_word(
w: &str,
fragments: &BTreeMap<String, Value>,
features: FeatureSet,
ns_prefix: &str,
) -> Result<Value, String> {
match w {
"true" => return Ok(Value::Bool(true)),
"false" => return Ok(Value::Bool(false)),
"null" => return Ok(Value::Null),
_ => {}
}
if let Some(ev) = w.strip_prefix("$env.") {
if !features.has(Feature::Env) {
return Err(format!("sml: 当前特性集禁用了 `$env`(env),裸词 `{}` 无法解析", w));
}
return Ok(Value::Str(std::env::var(ev).unwrap_or_default()));
}
if let Some(name) = w.strip_prefix('&') {
if !features.has(Feature::Fragment) {
return Err(format!("sml: 当前特性集禁用了片段引用(fragment),`{}` 无法解析", w));
}
if let Some(v) = fragments.get(name) {
return Ok(v.clone());
}
if !ns_prefix.is_empty() {
let mut probe = ns_prefix.to_string();
loop {
let full = format!("{probe}.{name}");
if let Some(v) = fragments.get(&full) {
return Ok(v.clone());
}
match probe.rfind('.') {
Some(idx) => probe.truncate(idx),
None => break,
}
}
}
return Ok(Value::Str(w.to_string()));
}
if let Ok(i) = w.parse::<i64>() {
return Ok(Value::Int(i));
}
let looks_int = !w.contains(['.', 'e', 'E']) && w.chars().all(|c| c.is_ascii_digit() || c == '+');
if looks_int {
if let Ok(u) = w.parse::<u64>() {
if u > i64::MAX as u64 {
return Ok(Value::Str(w.to_string()));
}
}
return Ok(Value::Str(w.to_string()));
}
if let Ok(f) = w.parse::<f64>() {
return Ok(Value::Float(f));
}
if !features.has(Feature::BarewordStr) {
return Err(format!(
"sml: 字符串必须加引号,裸词 `{}` 应写作 `\"{}\"`(特性 bareword-string 已禁用)",
w, w
));
}
Ok(Value::Str(w.to_string()))
}
struct Parser {
toks: Vec<Tok>,
i: usize,
fragments: BTreeMap<String, Value>,
contracts: BTreeMap<String, Contract>,
features: FeatureSet,
depth: usize,
ns_stack: Vec<String>,
}
impl Parser {
fn ns_prefix(&self) -> String {
if self.ns_stack.is_empty() {
String::new()
} else {
self.ns_stack.join(".")
}
}
fn qualify(&self, name: &str) -> String {
let p = self.ns_prefix();
if p.is_empty() {
name.to_string()
} else {
format!("{p}.{name}")
}
}
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.i)
}
fn next(&mut self) -> Option<Tok> {
let t = self.toks.get(self.i).cloned();
if t.is_some() {
self.i += 1;
}
t
}
fn parse_contract_body(&mut self) -> Result<BTreeMap<String, FieldSpec>, String> {
let mut fields: BTreeMap<String, FieldSpec> = BTreeMap::new();
loop {
match self.peek().cloned() {
None | Some(Tok::RBrace) => {
self.next();
break;
}
Some(Tok::Comma) => {
self.next();
}
_ => {
let key = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => {
return Err(format!("sml: 契约字段期望键, 得 {:?}", other))
}
};
if self.peek() == Some(&Tok::Colon) {
self.next();
} else {
return Err(format!("sml: 契约字段 `{}` 后须有冒号", key));
}
let spec = self.parse_field_spec()?;
fields.insert(key, spec);
}
}
}
Ok(fields)
}
fn parse_field_spec(&mut self) -> Result<FieldSpec, String> {
let ty = match self.next() {
Some(Tok::Word(w)) => match w.as_str() {
"str" => TypeSpec::Str,
"int" => TypeSpec::Int,
"num" => TypeSpec::Num,
"bool" => TypeSpec::Bool,
"any" => TypeSpec::Any,
"enum" => {
if self.peek() != Some(&Tok::LBrack) {
return Err("sml: `enum` 后须为 [ ... ]".into());
}
self.next();
let mut vals = Vec::new();
loop {
match self.peek().cloned() {
None | Some(Tok::RBrack) => {
self.next();
break;
}
Some(Tok::Comma) => {
self.next();
}
Some(Tok::Word(s)) | Some(Tok::Str(s)) => {
vals.push(s);
self.next();
}
_ => {
self.next();
}
}
}
TypeSpec::Enum(vals)
}
other => TypeSpec::ContractRef(other.to_string()),
},
Some(Tok::LBrack) => {
let inner = match self.next() {
Some(Tok::Word(w)) => match w.as_str() {
"str" => TypeSpec::Str,
"int" => TypeSpec::Int,
"num" => TypeSpec::Num,
"bool" => TypeSpec::Bool,
"any" => TypeSpec::Any,
other => {
return Err(format!("sml: 未知数组元素类型 `{}`", other))
}
},
other => {
return Err(format!("sml: 数组元素类型期望标识符, 得 {:?}", other))
}
};
if self.peek() == Some(&Tok::RBrack) {
self.next();
}
TypeSpec::Array(Box::new(inner))
}
other => return Err(format!("sml: 字段类型期望标识符, 得 {:?}", other)),
};
let mut required = true;
let mut default = None;
let mut min = None;
let mut max = None;
loop {
let is_next_field = matches!(self.peek(), Some(Tok::Word(_)))
&& matches!(self.toks.get(self.i + 1), Some(Tok::Colon));
if is_next_field {
break;
}
match self.peek().cloned() {
Some(Tok::Word(w)) => match w.as_str() {
"optional" => {
required = false;
self.next();
}
"required" => {
required = true;
self.next();
}
"default" => {
self.next();
default = Some(match self.next() {
Some(Tok::Word(w2)) => coerce_word(&w2, &self.fragments, self.features, &self.ns_prefix())?,
Some(Tok::Str(s)) => Value::Str(s),
other => {
return Err(format!("sml: default 期望值, 得 {:?}", other))
}
});
}
"min" => {
self.next();
min = Some(self.parse_spec_number()?);
}
"max" => {
self.next();
max = Some(self.parse_spec_number()?);
}
_ => break,
},
_ => break,
}
}
Ok(FieldSpec { ty, required, default, min, max })
}
fn parse_spec_number(&mut self) -> Result<f64, String> {
match self.next() {
Some(Tok::Word(w)) => {
w.parse::<f64>().map_err(|_| format!("sml: 期望数字, 得 `{}`", w))
}
other => Err(format!("sml: 期望数字, 得 {:?}", other)),
}
}
fn parse_block(&mut self, closing: Option<Tok>) -> Result<Value, String> {
if self.depth >= MAX_VALUE_DEPTH {
return Err(format!(
"sml: 嵌套过深(超过 {} 层),疑似递归或恶意输入",
MAX_VALUE_DEPTH
));
}
self.depth += 1;
let r = self.parse_block_inner(closing);
self.depth -= 1;
r
}
fn parse_block_inner(&mut self, closing: Option<Tok>) -> Result<Value, String> {
let mut node: BTreeMap<String, Value> = BTreeMap::new();
let mut applied_contract: Option<String> = None;
loop {
let tok = match self.peek().cloned() {
None => break,
Some(t) => t,
};
match tok {
Tok::RBrace | Tok::RBrack => {
if let Some(cl) = &closing {
if *cl == tok {
self.next();
break;
}
}
break;
}
Tok::Comma => {
self.next();
}
Tok::At => {
self.next();
let fname = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
_ => return Err("sml: @ 后需片段名".into()),
};
if self.peek() == Some(&Tok::Colon) {
self.next();
}
if fname == "contract" {
if !self.features.has(Feature::Contract) {
return Err("@contract 需要特性 `contract`,但当前特性集已禁用".into());
}
let cname = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => {
return Err(format!("sml: @contract 后须契约名, 得 {:?}", other))
}
};
let mut allow_extra = false;
if let Some(Tok::Word(w)) = self.peek().cloned() {
if w == "loose" {
allow_extra = true;
self.next();
}
}
if self.peek() != Some(&Tok::LBrace) {
return Err(format!("sml: @contract {} 后须 {{ ... }}", cname));
}
self.next();
let fields = self.parse_contract_body()?;
self.contracts.insert(
self.qualify(&cname),
Contract {
name: self.qualify(&cname),
fields,
allow_extra,
},
);
continue;
}
if fname == "is" {
if !self.features.has(Feature::Contract) {
return Err("@is 需要特性 `contract`,但当前特性集已禁用".into());
}
let cname = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => {
return Err(format!("sml: @is 后须契约名, 得 {:?}", other))
}
};
let resolved = if self.contracts.contains_key(&cname) {
cname.clone()
} else {
self.qualify(&cname)
};
applied_contract = Some(resolved);
continue;
}
let mut ftype: Option<String> = None;
let mut farg: Option<String> = None;
if let Some(Tok::Word(s)) = self.peek().cloned() {
if *self.peek().unwrap() != Tok::LBrace {
self.next();
ftype = Some(s);
if let Some(Tok::Word(s2)) = self.peek().cloned() {
if *self.peek().unwrap() != Tok::LBrace {
self.next();
farg = Some(s2);
}
}
}
}
if self.peek() == Some(&Tok::LBrace) {
self.next();
let mut sub = match self.parse_block(Some(Tok::RBrace))? {
Value::Object(m) => m,
other => {
let mut m = BTreeMap::new();
m.insert("_value".into(), other);
m
}
};
if let Some(t) = ftype {
sub.insert("__type".into(), Value::Str(t));
}
if let Some(a) = farg {
sub.insert("__name".into(), Value::Str(a));
}
if !self.features.has(Feature::Fragment) {
return Err(format!(
"sml: 片段定义 `@{}` 需要特性 `fragment`,但当前特性集已禁用",
fname
));
}
self.fragments.insert(self.qualify(&fname), Value::Object(sub));
}
}
_ => {
let key = match self.next() {
Some(Tok::Word(s)) | Some(Tok::Str(s)) => s,
other => return Err(format!("sml: 期望键, 得 {:?}", other)),
};
let colon = self.peek() == Some(&Tok::Colon);
if colon {
self.next();
}
let val = self.parse_value(&key, colon)?;
if let Some(existing) = node.get_mut(&key) {
match existing {
Value::Array(a) => a.push(val),
_ => {
let old = node.remove(&key).unwrap();
node.insert(key, Value::Array(vec![old, val]));
}
}
} else {
node.insert(key, val);
}
}
}
}
if let Some(cname) = applied_contract {
let c = self
.contracts
.get(&cname)
.cloned()
.ok_or_else(|| format!("sml: 未定义的契约 `{}`", cname))?;
apply_contract(&c, &mut node, &self.contracts)?;
}
Ok(Value::Object(node))
}
fn parse_value(&mut self, key: &str, colon: bool) -> Result<Value, String> {
if !colon && matches!(self.peek(), Some(Tok::Word(_))) {
let mut probe = self.i;
let mut found_block = false;
while probe < self.toks.len() {
match &self.toks[probe] {
Tok::Word(_) | Tok::Str(_) => probe += 1,
Tok::LBrace => {
found_block = true;
break;
}
_ => break,
}
}
if found_block {
let mut args: Vec<Value> = Vec::new();
while let Some(t) = self.peek().cloned() {
match t {
Tok::Word(w) => {
args.push(coerce_word(&w, &self.fragments, self.features, &self.ns_prefix())?);
self.next();
}
Tok::Str(_) => {
if let Some(Tok::Str(s)) = self.next() {
args.push(Value::Str(s));
}
}
_ => break,
}
}
if self.peek() == Some(&Tok::LBrace) {
self.next();
self.ns_stack.push(key.to_string());
let mut sub = self.parse_block(Some(Tok::RBrace))?;
self.ns_stack.pop();
if let Value::Object(m) = &mut sub {
m.insert("__type".into(), Value::Str(key.to_string()));
if args.len() == 1 {
m.insert("__name".into(), args.remove(0));
}
}
return Ok(sub);
}
}
}
match self.peek().cloned() {
Some(Tok::LBrace) => {
self.next();
self.parse_block(Some(Tok::RBrace))
}
Some(Tok::LBrack) => {
self.next();
self.parse_array()
}
Some(tok @ (Tok::Word(_) | Tok::Str(_))) => {
let v = match tok {
Tok::Word(w) => coerce_word(&w, &self.fragments, self.features, &self.ns_prefix())?,
Tok::Str(s) => {
let ev = s.strip_prefix("$env.");
match ev {
Some(name) => Value::Str(std::env::var(name).unwrap_or_default()),
None => Value::Str(s),
}
}
_ => unreachable!(),
};
self.next();
Ok(v)
}
Some(Tok::RBrace) | Some(Tok::RBrack) | Some(Tok::Comma) | None => {
if colon {
Ok(Value::Null)
} else {
Ok(coerce_word(key, &self.fragments, self.features, &self.ns_prefix())?)
}
}
_ => Err("sml: 语法错误".into()),
}
}
fn parse_array(&mut self) -> Result<Value, String> {
if self.depth >= MAX_VALUE_DEPTH {
return Err(format!(
"sml: 嵌套过深(超过 {} 层),疑似递归或恶意输入",
MAX_VALUE_DEPTH
));
}
self.depth += 1;
let r = self.parse_array_inner();
self.depth -= 1;
r
}
fn parse_array_inner(&mut self) -> Result<Value, String> {
let mut arr = Vec::new();
loop {
match self.peek().cloned() {
None => break,
Some(Tok::RBrack) => {
self.next();
break;
}
Some(Tok::Comma) => {
self.next();
}
Some(Tok::LBrace) => {
self.next();
arr.push(self.parse_block(Some(Tok::RBrace))?);
}
Some(Tok::LBrack) => {
self.next();
arr.push(self.parse_array_inner()?);
}
Some(Tok::Word(w)) => {
arr.push(coerce_word(&w, &self.fragments, self.features, &self.ns_prefix())?);
self.next();
}
Some(Tok::Str(_)) => {
if let Some(Tok::Str(s)) = self.next() {
arr.push(Value::Str(s));
}
}
_ => break,
}
}
Ok(Value::Array(arr))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Version {
V1,
V2,
V3,
}
impl Version {
pub const CURRENT: Version = Version::V3;
pub fn strict_strings(self) -> bool {
self >= Version::V2
}
pub(crate) fn from_word(w: &str) -> Option<Version> {
match w {
"v1" | "1" => Some(Version::V1),
"v2" | "2" => Some(Version::V2),
"v3" | "3" => Some(Version::V3),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Version::V1 => "v1",
Version::V2 => "v2",
Version::V3 => "v3",
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Feature {
BarewordStr,
Include,
Env,
Contract,
Fragment,
TopArray,
Namespace,
ImplicitNs,
MultiInclude,
GlobInclude,
RegexInclude,
ExtRewrite,
}
pub fn feature_names() -> Vec<&'static str> {
FEATURES.iter().map(|(n, _)| *n).collect()
}
pub static FEATURES: &[(&str, Feature)] = &[
("bareword-string", Feature::BarewordStr),
("include", Feature::Include),
("env", Feature::Env),
("contract", Feature::Contract),
("fragment", Feature::Fragment),
("top-level-array", Feature::TopArray),
("namespace", Feature::Namespace),
("implicit-ns", Feature::ImplicitNs),
("multi-include", Feature::MultiInclude),
("glob-include", Feature::GlobInclude),
("regex-include", Feature::RegexInclude),
("ext-rewrite", Feature::ExtRewrite),
];
impl Feature {
pub fn from_name(name: &str) -> Option<Feature> {
FEATURES.iter().find(|(n, _)| *n == name).map(|(_, f)| *f)
}
pub fn name(self) -> &'static str {
FEATURES
.iter()
.find(|(_, f)| *f == self)
.map(|(n, _)| *n)
.unwrap_or("<unknown>")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FeatureSet(u64);
impl FeatureSet {
pub fn all() -> FeatureSet {
let mut m = 0u64;
for (_, f) in FEATURES {
m |= 1 << (*f as u8);
}
FeatureSet(m)
}
pub fn baseline() -> FeatureSet {
FeatureSet::none()
.with(Feature::BarewordStr)
.with(Feature::Include)
.with(Feature::Env)
.with(Feature::Contract)
.with(Feature::Fragment)
.with(Feature::TopArray)
.with(Feature::Namespace)
.with(Feature::ImplicitNs)
}
pub fn none() -> FeatureSet {
FeatureSet(0)
}
pub fn for_version(v: Version) -> FeatureSet {
let mut s = FeatureSet::baseline();
if v.strict_strings() {
s = s.without(Feature::BarewordStr);
} else {
s = s.with(Feature::BarewordStr);
}
s
}
pub fn has(self, f: Feature) -> bool {
(self.0 & (1 << (f as u8))) != 0
}
pub fn with(self, f: Feature) -> FeatureSet {
FeatureSet(self.0 | (1 << (f as u8)))
}
pub fn without(self, f: Feature) -> FeatureSet {
FeatureSet(self.0 & !(1 << (f as u8)))
}
pub fn intersection(self, other: FeatureSet) -> FeatureSet {
FeatureSet(self.0 & other.0)
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
}
impl fmt::Display for FeatureSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for (n, feat) in FEATURES {
if self.has(*feat) {
if !first {
f.write_str(",")?;
}
f.write_str(n)?;
first = false;
}
}
if first {
f.write_str("<none>")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FeatureMode {
Default,
Whitelist,
Blacklist,
}
fn tok_word(t: &Tok) -> String {
match t {
Tok::Word(s) | Tok::Str(s) => s.clone(),
_ => String::new(),
}
}
fn apply_feature_directive(
line: &str,
feats: &mut FeatureSet,
mode: &mut FeatureMode,
base: &mut Option<Version>,
) -> Result<bool, String> {
let content = strip_line_comment(line).trim();
let toks = match tokenize(content) {
Ok(t) => t,
Err(_) => return Ok(false),
};
if toks.is_empty() || toks[0] != Tok::At {
return Ok(false);
}
let words: Vec<String> = toks
.iter()
.map(|t| match t {
Tok::At => "@".to_string(),
other => tok_word(other),
})
.collect();
let head = format!("{}{}", words.first().map(|s| s.as_str()).unwrap_or(""), words.get(1).map(|s| s.as_str()).unwrap_or(""));
if head != "@feature" {
return Ok(false);
}
let words: Vec<String> = words[1..].to_vec();
if words.len() < 2 {
return Err("@feature 指令缺少参数".into());
}
let arg = words[1].as_str();
let names = |from: usize| -> Vec<String> {
words[from..]
.join(",")
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
};
match arg {
"base" => {
let v = Version::from_word(words.get(2).map(|s| s.as_str()).unwrap_or(""))
.ok_or_else(|| {
format!(
"@feature base 需要 v1/v2/v3,收到 `{}`",
words.get(2).cloned().unwrap_or_default()
)
})?;
*feats = FeatureSet::for_version(v);
*base = Some(v);
Ok(true)
}
"mode" => {
let m = words.get(2).map(|s| s.as_str()).unwrap_or("");
*mode = match m {
"whitelist" => FeatureMode::Whitelist,
"blacklist" => FeatureMode::Blacklist,
_ => return Err(format!("@feature mode 需要 whitelist/blacklist,收到 `{m}`")),
};
if *mode == FeatureMode::Whitelist {
*feats = FeatureSet::none();
}
Ok(true)
}
"enable" => {
for n in names(2) {
let f = Feature::from_name(&n).ok_or_else(|| {
format!(
"未知特性 `{n}`,可用:{}",
FEATURES.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(", ")
)
})?;
*feats = feats.with(f);
}
Ok(true)
}
"disable" => {
for n in names(2) {
let f = Feature::from_name(&n).ok_or_else(|| {
format!(
"未知特性 `{n}`,可用:{}",
FEATURES.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(", ")
)
})?;
*feats = feats.without(f);
}
Ok(true)
}
"whitelist" => {
*mode = FeatureMode::Whitelist;
let mut s = FeatureSet::none();
for n in names(2) {
let f = Feature::from_name(&n).ok_or_else(|| {
format!(
"未知特性 `{n}`,可用:{}",
FEATURES.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(", ")
)
})?;
s = s.with(f);
}
*feats = s;
Ok(true)
}
"blacklist" => {
let mut s = FeatureSet::all();
for n in names(2) {
let f = Feature::from_name(&n).ok_or_else(|| {
format!(
"未知特性 `{n}`,可用:{}",
FEATURES.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(", ")
)
})?;
s = s.without(f);
}
*feats = s;
Ok(true)
}
_ => Err(format!("未知 @feature 子命令 `{arg}`,可用 base/mode/enable/disable")),
}
}
fn strip_features(text: &str) -> Result<(String, FeatureSet, Option<Version>, bool), String> {
let mut out = String::new();
let mut feats = FeatureSet::all();
let mut mode = FeatureMode::Default;
let mut base: Option<Version> = None;
let mut had_feature = false;
for line in text.lines() {
match apply_feature_directive(line, &mut feats, &mut mode, &mut base) {
Ok(true) => {
had_feature = true;
continue; }
Ok(false) => {}
Err(e) => return Err(e), }
out.push_str(line);
out.push('\n');
}
Ok((out, feats, base, had_feature))
}
fn version_directive(line: &str) -> Result<Option<String>, String> {
let content = strip_line_comment(line).trim();
let toks = match tokenize(content) {
Ok(t) => t,
Err(_) => return Ok(None),
};
match toks.as_slice() {
[Tok::At, Tok::Word(w), Tok::Word(v)] if w == "version" => Ok(Some(v.clone())),
[Tok::At, Tok::Word(w), Tok::Str(v)] if w == "version" => Ok(Some(v.clone())),
[Tok::At, Tok::Word(w), ..] if w == "version" => Err(
"`@version` 是版本声明指令,须写作 `@version v1`;`version` 不可作为片段名".into(),
),
_ => Ok(None),
}
}
pub(crate) fn strip_version(text: &str) -> Result<(String, Option<Version>), String> {
let mut declared: Option<Version> = None;
let mut rest = String::new();
for line in text.lines() {
if let Some(lit) = version_directive(line)? {
let v = Version::from_word(&lit).ok_or_else(|| {
format!(
"不支持的 SML 版本 `{lit}`(本实现支持 {})",
Version::CURRENT.name()
)
})?;
match declared {
None => declared = Some(v),
Some(prev) if prev != v => {
return Err(format!("@version 冲突:{} 与 {}", prev.name(), v.name()))
}
Some(_) => {}
}
continue;
}
rest.push_str(line);
rest.push('\n');
}
Ok((rest, declared))
}
fn features_for(v: Version, feats: FeatureSet, had_feature: bool) -> FeatureSet {
if had_feature {
feats
} else {
FeatureSet::for_version(v)
}
}
pub fn parse_versioned(text: &str) -> Result<(Value, Version), String> {
let (rest, declared) = strip_version(text)?;
let (rest, feats, base, had) = strip_features(&rest)?;
let v = declared.or(base).unwrap_or(Version::V1);
let feats = features_for(v, feats, had);
Ok((parse_impl(&rest, v, feats)?, v))
}
pub fn parse_file_versioned(path: impl AsRef<Path>) -> Result<(Value, Version), String> {
let path = path.as_ref();
let text =
std::fs::read_to_string(path).map_err(|e| format!("读取失败 {}: {e}", path.display()))?;
let base = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let (rest, declared) = strip_version(&text)?;
let (rest, feats, base_ver, had) = strip_features(&rest)?;
let allowed = FeatureSet::all().intersection(feats);
let v = declared.or(base_ver).unwrap_or(Version::V1);
let feats = features_for(v, allowed, had);
let toks = resolve_includes(&rest, &base, allowed)?;
let val = parse_impl_tokens(toks, v, feats)?;
Ok((val, v))
}
pub fn parse(text: &str) -> Result<Value, String> {
let (rest, declared) = strip_version(text)?;
let (rest, feats, base, had) = strip_features(&rest)?;
let v = declared.or(base).unwrap_or(Version::V1);
let feats = features_for(v, feats, had);
parse_impl(&rest, v, feats)
}
pub fn parse_allowed(
text: &str,
allowed: &[Version],
) -> Result<Value, String> {
let (rest, declared) = strip_version(text)?;
let (rest, feats, base, had) = strip_features(&rest)?;
let v = declared.or(base).unwrap_or(Version::V1);
if !allowed.contains(&v) {
return Err(format!(
"sml: 文档声明版本 {} 不在本库接受的版本范围 {{{}}} 内",
v.name(),
allowed
.iter()
.map(|x| x.name())
.collect::<Vec<_>>()
.join(", ")
));
}
let feats = features_for(v, feats, had);
parse_impl(&rest, v, feats)
}
pub fn parse_with_features(
text: &str,
allowed: FeatureSet,
) -> Result<(Value, FeatureSet), String> {
let (rest, declared) = strip_version(text)?;
let (rest, feats, base, had) = strip_features(&rest)?;
let v = declared.or(base).unwrap_or(Version::V1);
let feats = features_for(v, feats, had);
let effective = feats.intersection(allowed);
if effective.is_empty() {
return Err(format!(
"sml: 文档请求的特性 {feats} 与调用方允许的特性 {allowed} 无交集"
));
}
let val = parse_impl(&rest, v, effective)?;
Ok((val, effective))
}
fn parse_impl(text: &str, version: Version, features: FeatureSet) -> Result<Value, String> {
let toks = tokenize(text)?;
parse_impl_tokens(toks, version, features)
}
fn parse_impl_tokens(
toks: Vec<Tok>,
version: Version,
features: FeatureSet,
) -> Result<Value, String> {
let mut p = Parser {
toks,
i: 0,
fragments: BTreeMap::new(),
contracts: BTreeMap::new(),
features,
depth: 0,
ns_stack: Vec::new(),
};
match p.peek() {
Some(Tok::LBrack) => {
if !p.features.has(Feature::TopArray) {
return Err("sml: 顶层数组需要特性 `top-level-array`,但当前特性集已禁用".into());
}
p.next();
p.parse_array()
}
Some(Tok::LBrace) => {
p.next();
p.parse_block(Some(Tok::RBrace))
}
_ => p.parse_block(None),
}
}
const MAX_VALUE_DEPTH: usize = 128;
const MAX_INCLUDE_DEPTH: usize = 32;
pub(crate) fn strip_line_comment(line: &str) -> &str {
let bytes = line.as_bytes();
let mut i = 0;
let mut in_quote = false;
while i < bytes.len() {
match bytes[i] {
b'"' => in_quote = !in_quote,
b'\\' if in_quote => i += 1,
b'#' if !in_quote => return &line[..i],
_ => {}
}
i += 1;
}
line
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncludeTarget {
pub raw: String,
pub namespace: Option<String>,
pub via_import: bool,
pub keys: Option<Vec<String>>,
}
pub(crate) fn parse_include_line(line: &str, features: FeatureSet) -> Result<Option<Vec<IncludeTarget>>, String> {
let content = strip_line_comment(line).trim();
let content = content.strip_prefix('@').unwrap_or(content).trim_start();
let (via_import, rest) = if let Some(r) = content.strip_prefix("include ") {
(false, r.trim_start())
} else if let Some(r) = content.strip_prefix("import ") {
(true, r.trim_start())
} else {
return Ok(None);
};
if !features.has(Feature::Include) {
return Ok(None);
}
let mut targets: Vec<IncludeTarget> = Vec::new();
let mut rest = rest;
loop {
let (raw, ns, keys, tail) = if rest.trim_start().starts_with('{') {
let (keys, after) = parse_key_list(rest.trim_start())?;
let after = after.trim_start();
let (ns, after) = if let Some(stripped) = after.strip_prefix("as ") {
let (n, t) = match next_token(stripped.trim_start()) {
Some((n, t)) => (Some(n), t.trim_start()),
None => return Ok(None),
};
(n, t)
} else {
(None, after)
};
let after = after.trim_start();
let after = match after.strip_prefix("in ") {
Some(a) => a.trim_start(),
None => {
return Err(
"sml: `import { keys } ...` 必须接 `in \"file\"` 指定目标文件".into(),
)
}
};
let (path, t) = match next_token(after) {
Some((p, t)) => (p, t),
None => return Ok(None),
};
(path, ns, Some(keys), t)
} else {
let (path, tail0) = match next_token(rest) {
Some((p, t)) => (p, t),
None => {
if targets.is_empty() && rest.trim().is_empty() {
return Ok(None);
} else {
break;
}
}
};
let mut r = tail0.trim_start();
let mut ns: Option<String> = None;
if let Some(stripped) = r.strip_prefix("as ") {
let (n, t) = match next_token(stripped.trim_start()) {
Some((n, t)) => (n, t),
None => return Ok(None),
};
ns = Some(n);
r = t.trim_start();
}
let keys = if r.starts_with('{') {
let (k, after) = parse_key_list(r)?;
r = after.trim_start();
Some(k)
} else {
None
};
(path, ns, keys, r)
};
targets.push(finalize_target(
raw,
ns,
via_import,
features,
keys,
));
if let Some(stripped) = tail.strip_prefix(',') {
if !features.has(Feature::MultiInclude) {
return Ok(None);
}
rest = stripped.trim_start();
continue;
} else {
rest = tail;
break;
}
}
if targets.is_empty() {
return Ok(None);
}
for t in &targets {
if t.keys.is_some() && (t.raw.contains('*') || t.raw.starts_with("re:")) {
return Err(
"sml: 部分引用 `{ keys }` 不能配合 glob/regex 通配(请指定单个文件)".into(),
);
}
if t.raw.starts_with("re:") {
if !features.has(Feature::RegexInclude) {
return Err("sml: 正则 include 需要特性 `regex-include`(请 @feature enable regex-include)".into());
}
continue;
}
if t.raw.contains('*') && !features.has(Feature::GlobInclude) {
return Err("sml: 通配 include 需要特性 `glob-include`(请 @feature enable glob-include)".into());
}
}
Ok(Some(targets))
}
fn next_token(s: &str) -> Option<(String, &str)> {
let s = s.trim_start();
if s.is_empty() {
return None;
}
if s.starts_with('"') {
let bytes = s.as_bytes();
let mut i = 1;
let mut out = String::new();
while i < bytes.len() {
if bytes[i] == b'"' {
i += 1;
break;
}
if bytes[i] == b'\\' && i + 1 < bytes.len() {
i += 1;
out.push(bytes[i] as char);
i += 1;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
Some((out, &s[i..]))
} else {
let end = s
.find(|c: char| c.is_whitespace() || c == ',')
.unwrap_or(s.len());
let (tok, tail) = s.split_at(end);
Some((tok.trim().to_string(), tail))
}
}
fn parse_key_list(s: &str) -> Result<(Vec<String>, &str), String> {
let s = s.trim_start();
let Some(body) = s.strip_prefix('{') else {
return Err("sml: 期望 `{ key1, key2, ... }` 键列表".into());
};
let close = body.find('}').ok_or("sml: 键列表缺少闭合 `}`")?;
let inner = &body[..close];
let mut keys: Vec<String> = Vec::new();
for part in inner.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(q) = part.strip_prefix('"') {
let q = q.strip_suffix('"').unwrap_or(q);
keys.push(q.to_string());
} else {
keys.push(part.to_string());
}
}
if keys.is_empty() {
return Err("sml: 键列表不能为空(至少指定一个键)".into());
}
Ok((keys, &body[close + 1..]))
}
fn finalize_target(
raw: String,
ns: Option<String>,
via_import: bool,
features: FeatureSet,
keys: Option<Vec<String>>,
) -> IncludeTarget {
let namespace = match ns {
Some(n) => Some(n),
None => {
if keys.is_some() {
None
} else if via_import || (features.has(Feature::ImplicitNs) && !raw.contains('.')) {
Some(raw.clone())
} else {
None
}
}
};
IncludeTarget {
raw,
namespace,
via_import,
keys,
}
}
fn resolve_target_paths(
t: &IncludeTarget,
base: &Path,
features: FeatureSet,
) -> Result<Vec<PathBuf>, String> {
if let Some(pat) = t.raw.strip_prefix("re:") {
if !features.has(Feature::RegexInclude) {
return Err("sml: 正则 include 需要特性 `regex-include`(请 @feature enable regex-include)".into());
}
let pat = pat.trim_matches('"');
let pat = pat.replace('/', std::path::MAIN_SEPARATOR_STR);
let (dir, pat) = split_dir(&pat);
return glob_or_regex_dir(&base.join(dir), pat, Some(pat), features);
}
if t.raw.contains('*') {
if !features.has(Feature::GlobInclude) {
return Err("sml: 通配 include 需要特性 `glob-include`(请 @feature enable glob-include)".into());
}
let normalized = t.raw.replace('/', std::path::MAIN_SEPARATOR_STR);
let (dir, pat) = split_dir(&normalized);
return glob_or_regex_dir(&base.join(dir), pat, None, features);
}
let path = if t.via_import {
if t.raw.contains(std::path::MAIN_SEPARATOR) || t.raw.ends_with(".sml") {
base.join(&t.raw)
} else {
let rel = t
.raw
.split('.')
.collect::<Vec<_>>()
.join(std::path::MAIN_SEPARATOR_STR);
base.join(rel).with_extension("sml")
}
} else if t.raw.contains('.') {
let _ = features.has(Feature::ExtRewrite);
base.join(&t.raw)
} else {
base.join(format!("{}.sml", t.raw))
};
Ok(vec![path])
}
fn split_dir(pat: &str) -> (&str, &str) {
match pat.rfind(std::path::MAIN_SEPARATOR) {
Some(idx) => (&pat[..idx], &pat[idx + 1..]),
None => ("", pat),
}
}
fn glob_or_regex_dir(
base: &Path,
pattern: &str,
regex: Option<&str>,
_features: FeatureSet,
) -> Result<Vec<PathBuf>, String> {
let mut hits: Vec<PathBuf> = Vec::new();
let entries = std::fs::read_dir(base)
.map_err(|e| format!("include 目录读取失败 {}: {e}", base.display()))?;
let re = regex.map(|r| compile_regex(r));
for ent in entries {
let ent = ent.map_err(|e| format!("include 目录遍历失败: {e}"))?;
let p = ent.path();
if p.is_dir() {
continue; }
let name = match p.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
let matched = if let Some(re) = &re {
regex_matches(re, name)
} else {
let pat_file = pattern.rsplit(std::path::MAIN_SEPARATOR).next().unwrap_or(pattern);
glob_matches(pat_file, name)
};
if matched {
hits.push(p);
}
}
hits.sort();
Ok(hits)
}
fn glob_matches(pattern: &str, text: &str) -> bool {
let segs: Vec<&str> = pattern.split('*').collect();
if segs.is_empty() {
return text.is_empty();
}
let mut pos = 0usize;
if !pattern.starts_with('*') {
if !text[pos..].starts_with(segs[0]) {
return false;
}
pos += segs[0].len();
}
for seg in &segs[if pattern.starts_with('*') { 0 } else { 1 }..] {
if seg.is_empty() {
continue;
}
match text[pos..].find(seg) {
Some(idx) => pos += idx + seg.len(),
None => return false,
}
}
if !pattern.ends_with('*') {
if pos != text.len() {
return false;
}
}
true
}
struct MiniRegex {
pattern: String,
}
fn compile_regex(pat: &str) -> MiniRegex {
MiniRegex {
pattern: pat.to_string(),
}
}
fn regex_matches(re: &MiniRegex, text: &str) -> bool {
let pat = &re.pattern;
let anchored_start = pat.starts_with('^');
let anchored_end = pat.ends_with('$');
let p = if anchored_start { &pat[1..] } else { pat };
let p = if anchored_end { &p[..p.len().saturating_sub(1)] } else { p };
if anchored_start {
backtrack_match(p, text, 0).is_some()
} else {
for start in 0..=text.len() {
if backtrack_match(p, text, start).is_some() {
if !anchored_end {
return true;
}
if backtrack_match(p, text, start) == Some(text.len()) {
return true;
}
}
}
false
}
}
fn backtrack_match(pat: &str, text: &str, ti: usize) -> Option<usize> {
let pchars: Vec<char> = pat.chars().collect();
let tchars: Vec<char> = text.chars().collect();
fn go(pchars: &[char], tchars: &[char], pi: usize, ti: usize) -> Option<usize> {
let mut pi = pi;
let mut ti = ti;
while pi < pchars.len() {
match pchars[pi] {
'\\' => {
if pi + 1 >= pchars.len() {
return None;
}
let pc = pchars[pi + 1];
if ti >= tchars.len() || tchars[ti] != pc {
return None;
}
pi += 2;
ti += 1;
}
'.' => {
if ti >= tchars.len() {
return None;
}
pi += 1;
ti += 1;
}
'*' => {
let prev = if pi >= 1 { Some(pchars[pi - 1]) } else { None };
if ti < tchars.len() {
let mut end = ti;
match prev {
Some('.') => {
while end < tchars.len() {
end += 1;
}
}
Some(c) if c != '\\' => {
while end < tchars.len() && tchars[end] == c {
end += 1;
}
}
_ => {}
}
let mut e = end;
while e >= ti {
if let Some(r) = go(pchars, tchars, pi + 1, e) {
return Some(r);
}
if e == ti {
break;
}
e -= 1;
}
}
return go(pchars, tchars, pi + 1, ti);
}
'+' => {
if ti >= tchars.len() {
return None;
}
let prev = pchars.get(pi.wrapping_sub(1)).copied();
let mut consumed = 0;
match prev {
Some('.') => {
if ti >= tchars.len() {
return None;
}
consumed = 1;
}
Some(c) if c != '\\' => {
if tchars[ti] != c {
return None;
}
consumed = 1;
while ti + consumed < tchars.len()
&& tchars[ti + consumed] == c
{
consumed += 1;
}
}
_ => return None,
}
pi += 1;
ti += consumed;
}
'?' => {
let prev = pchars.get(pi.wrapping_sub(1)).copied();
if ti < tchars.len() {
match prev {
Some('.') => {
pi += 1;
ti += 1;
}
Some(c) if c != '\\' => {
if tchars[ti] == c {
pi += 1;
ti += 1;
} else {
pi += 1; }
}
_ => {
pi += 1; }
}
} else {
pi += 1;
}
}
'[' => {
let mut j = pi + 1;
let negate = if j < pchars.len() && pchars[j] == '^' {
j += 1;
true
} else {
false
};
let mut cls = Vec::new();
while j < pchars.len() && pchars[j] != ']' {
if j + 2 < pchars.len()
&& pchars[j + 1] == '-'
&& pchars[j + 2] != ']'
{
let lo = pchars[j];
let hi = pchars[j + 2];
cls.push((lo, hi));
j += 3;
} else {
cls.push((pchars[j], pchars[j]));
j += 1;
}
}
if j >= pchars.len() {
return None; }
if ti >= tchars.len() {
return None;
}
let c = tchars[ti];
let in_cls = cls.iter().any(|(lo, hi)| c >= *lo && c <= *hi);
let ok = if negate { !in_cls } else { in_cls };
if !ok {
return None;
}
pi = j + 1;
ti += 1;
}
c => {
if ti >= tchars.len() || tchars[ti] != c {
return None;
}
pi += 1;
ti += 1;
}
}
}
Some(ti)
}
go(&pchars, &tchars, 0, ti)
}
pub fn resolve_includes(
text: &str,
base: &Path,
features: FeatureSet,
) -> Result<Vec<Tok>, String> {
let mut stack: Vec<PathBuf> = Vec::new();
let mut toks: Vec<Tok> = Vec::new();
expand_includes(text, base, &mut stack, features, &mut toks)?;
Ok(toks)
}
fn expand_includes(
text: &str,
base: &Path,
stack: &mut Vec<PathBuf>,
features: FeatureSet,
out: &mut Vec<Tok>,
) -> Result<(), String> {
if stack.len() >= MAX_INCLUDE_DEPTH {
return Err(format!("include 嵌套超过 {MAX_INCLUDE_DEPTH} 层"));
}
for line in text.lines() {
match parse_include_line(line, features)? {
Some(targets) => {
if !features.has(Feature::Include) {
return Err("sml: 当前特性集禁用了 include(include 特性)".into());
}
for t in targets {
if t.namespace.is_some() && !features.has(Feature::Namespace) {
return Err(
"sml: 当前特性集禁用了命名空间包含(namespace 特性)".into(),
);
}
let paths = resolve_target_paths(&t, base, features)?;
for path in paths {
let canon = path.canonicalize().map_err(|e| {
format!("include 无法定位 {}: {e}", path.display())
})?;
if stack.iter().any(|p| p == &canon) {
return Err(format!("include 循环引用: {}", canon.display()));
}
let content = std::fs::read_to_string(&canon)
.map_err(|e| format!("include 读取失败 {}: {e}", canon.display()))?;
let child_base = canon
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
stack.push(canon.clone());
let mut inner =
expand_file_tokens(&content, &child_base, stack, features)?;
if let Some(keys) = &t.keys {
inner = filter_top_level_keys(inner, keys);
}
if let Some(ns) = &t.namespace {
for seg in ns.split('.') {
out.push(Tok::Word(seg.to_string()));
out.push(Tok::LBrace);
}
out.extend(inner);
for _ in ns.split('.') {
out.push(Tok::RBrace);
}
} else {
out.extend(inner);
}
stack.pop();
}
}
}
None => {
let line_toks = tokenize(line).map_err(|e| {
format!("include 预处理词法错误:{e}(于行:{line})")
})?;
out.extend(line_toks);
}
}
}
Ok(())
}
fn filter_top_level_keys(toks: Vec<Tok>, keys: &[String]) -> Vec<Tok> {
let key_set: std::collections::HashSet<&str> = keys.iter().map(|s| s.as_str()).collect();
let mut out: Vec<Tok> = Vec::with_capacity(toks.len());
let mut i = 0;
let n = toks.len();
while i < n {
if !matches!(toks[i], Tok::Word(_) | Tok::Str(_)) {
out.push(toks[i].clone());
i += 1;
continue;
}
let key_name = match &toks[i] {
Tok::Word(w) => w.clone(),
Tok::Str(s) => s.clone(),
_ => unreachable!(),
};
let j = if i + 1 < n {
match &toks[i + 1] {
Tok::Colon => {
if i + 2 < n {
match &toks[i + 2] {
Tok::LBrace | Tok::LBrack => {
let mut depth = 1i32;
let mut k = i + 3;
while k < n {
match &toks[k] {
Tok::LBrace | Tok::LBrack => depth += 1,
Tok::RBrace | Tok::RBrack => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
k += 1;
}
(k + 1).min(n)
}
_ => i + 3,
}
} else {
i + 2
}
}
Tok::LBrace | Tok::LBrack => {
let mut depth = 1i32;
let mut k = i + 2;
while k < n {
match &toks[k] {
Tok::LBrace | Tok::LBrack => depth += 1,
Tok::RBrace | Tok::RBrack => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
k += 1;
}
(k + 1).min(n)
}
_ => i + 1,
}
} else {
i + 1
};
if key_set.contains(key_name.as_str()) {
for t in &toks[i..j] {
out.push(t.clone());
}
}
i = j;
}
out
}
fn expand_file_tokens(
content: &str,
base: &Path,
stack: &mut Vec<PathBuf>,
features: FeatureSet,
) -> Result<Vec<Tok>, String> {
let cleaned: String = content
.lines()
.filter(|l| {
let t = strip_line_comment(l).trim();
let t = t.strip_prefix('@').unwrap_or(t).trim_start();
!(t.starts_with("version") || t.starts_with("feature"))
})
.collect::<Vec<_>>()
.join("\n");
let mut toks = Vec::new();
expand_includes(&cleaned, base, stack, features, &mut toks)?;
Ok(toks)
}
pub fn parse_file(path: impl AsRef<Path>) -> Result<Value, String> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| format!("读取失败 {}: {e}", path.display()))?;
let base = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let (rest, declared) = strip_version(&text)?;
let (rest, feats, base_ver, had) = strip_features(&rest)?;
let v = declared.or(base_ver).unwrap_or(Version::V1);
let feats = features_for(v, feats, had);
let allowed = FeatureSet::all().intersection(feats);
let toks = resolve_includes(&rest, &base, allowed)?;
parse_impl_tokens(toks, v, allowed)
}
pub fn loads(text: &str) -> Result<Value, ParseError> {
parse(text).map_err(ParseError)
}
#[derive(Debug)]
pub struct ParseError(pub String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "sml parse error: {}", self.0)
}
}
impl std::error::Error for ParseError {}
fn needs_quote(s: &str) -> bool {
if s.is_empty() {
return true;
}
match s {
"true" | "false" | "null" | "inf" | "nan" => return true,
_ => {}
}
if s.parse::<i64>().is_ok() || s.parse::<f64>().is_ok() {
return true;
}
if s.starts_with("--")
|| s.starts_with("//")
|| s.starts_with("/*")
|| s.starts_with("*/")
|| s.starts_with("*")
|| s.starts_with("_*")
{
return true;
}
if s.contains([' ', '\t', '\n', '\r', ':', '#', '{', '}', ',', '[', ']', '"', '\\', '/', '*'])
{
return true;
}
match s.chars().next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return true,
}
!s.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
}
fn quote_if_needed(s: &str) -> String {
if needs_quote(s) {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
} else {
s.to_string()
}
}
fn dump_block(m: &BTreeMap<String, Value>, indent: usize, out: &mut String) {
if m.is_empty() {
out.push_str("{}");
return;
}
out.push_str(&format!("\n{}{{", " ".repeat(indent)));
for (k, val) in m {
out.push_str(&format!(
"\n{}{}: ",
" ".repeat(indent + 1),
quote_if_needed(k)
));
dump_value(val, indent + 1, out);
}
out.push_str(&format!("\n{}}}", " ".repeat(indent)));
}
fn dump_value(v: &Value, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
match v {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(f) => {
if f.fract() == 0.0 {
out.push_str(&format!("{:.1}", f));
} else {
out.push_str(&format!("{}", f));
}
}
Value::Str(s) => out.push_str("e_if_needed(s)),
Value::Array(a) => {
if a.is_empty() {
out.push_str("[]");
} else {
out.push('[');
for e in a {
out.push('\n');
out.push_str(&format!("{}{}", " ".repeat(indent + 1), dump_inline(e)));
}
out.push_str(&format!("\n{}]", pad));
}
}
Value::Object(m) => dump_block(m, indent, out),
}
}
fn dump_scalar(v: &Value) -> String {
match v {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Str(s) => quote_if_needed(s),
_ => "".into(),
}
}
fn dump_inline(v: &Value) -> String {
match v {
Value::Object(m) => {
let parts: Vec<String> = m
.iter()
.map(|(k, val)| format!("{}: {}", k, dump_inline(val)))
.collect();
format!("{{ {} }}", parts.join(", "))
}
Value::Array(a) => {
let parts: Vec<String> = a.iter().map(dump_inline).collect();
format!("[ {} ]", parts.join(", "))
}
other => dump_scalar(other),
}
}
pub fn to_sml(v: &Value) -> String {
let mut out = String::new();
if let Value::Object(m) = v {
if m.contains_key("__type") {
dump_block(m, 0, &mut out);
} else {
for (k, val) in m {
out.push_str(&format!("{}: ", quote_if_needed(k)));
dump_value(val, 0, &mut out);
out.push('\n');
}
}
} else {
out.push_str(&dump_inline(v));
}
out
}