diff --git a/Makefile b/Makefile index 1bb75831..7549fe2b 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,21 @@ # make layout Rust layout(layout_file) # make json Go json 扩展(json-rwirext 可执行文件) # make oldhero Go 旧 runtime(kvlang,兼容保留) +# make test 全量 tutorial 回归(C runtime + Rust term,递归跑所有子目录 kv) # make all 全部 # make clean 清理 bin/ 与各构建目录 BIN := bin KVSPACE_LIB ?= kvspace-c -.PHONY: all runtime term run layout json oldhero clean +.PHONY: all runtime term run layout json oldhero test clean all: runtime term run layout json +test: KVSPACE_LIB := kvspace_durable +test: runtime term run layout + python3 tutorial/test.py --no-build + runtime: cmake -S runtime -B build/runtime -DCMAKE_BUILD_TYPE=Release -DKVSPACE_LIB=$(KVSPACE_LIB) cmake --build build/runtime --target kvlang_runtime -j diff --git a/deepx-design b/deepx-design index 93c7d4ec..ccc0c5d6 160000 --- a/deepx-design +++ b/deepx-design @@ -1 +1 @@ -Subproject commit 93c7d4eca430ca8a73d3032e53f3ba634725ea03 +Subproject commit ccc0c5d6313e1554436dd6d520ee24d783c4c9db diff --git a/layout/src/lib.rs b/layout/src/lib.rs index b4c4ad60..e3295440 100644 --- a/layout/src/lib.rs +++ b/layout/src/lib.rs @@ -10,6 +10,7 @@ pub mod ast; pub mod scanner; pub mod parser; pub mod builtin; +pub mod type_expr; pub mod lower; pub mod code; diff --git a/layout/src/parser.rs b/layout/src/parser.rs index a03d5b7d..102f7ebe 100644 --- a/layout/src/parser.rs +++ b/layout/src/parser.rs @@ -400,7 +400,7 @@ impl Parser { fn check_param_types(&mut self, sig: &FuncSig) { for param in &sig.params { - if !valid_kindexp(¶m.ty) { + if !crate::type_expr::valid_type_expr(¶m.ty) { self.errors.push(Diagnostic { pos: Pos { line: 0, col: 0 }, message: format!("func {}: param {:?}: {} (got {:?})", sig.name, param.name, type_error(¶m.ty), param.ty), @@ -413,7 +413,7 @@ impl Parser { } } for ret in &sig.returns { - if !valid_kindexp(&ret.ty) { + if !crate::type_expr::valid_type_expr(&ret.ty) { self.errors.push(Diagnostic { pos: Pos { line: 0, col: 0 }, message: format!("func {}: return value {:?}: {} (got {:?})", sig.name, ret.name, type_error(&ret.ty), ret.ty), @@ -1327,58 +1327,12 @@ fn attach_comments(st: Stmt, comments: Vec) -> Stmt { st } -fn valid_kinds() -> &'static [&'static str] { - &[ - "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64", - "bool", "char/utf32", "char/utf8", "char/ascii", "any", - ] -} - -fn valid_kindexp(t: &str) -> bool { - let mut t = t; - while !t.is_empty() { - match t.as_bytes()[0] { - b'*' | b'@' => t = &t[1..], - b'[' => { - let end = match t.find(']') { - Some(e) => e, - None => return false, - }; - if !t[1..end].is_empty() && !valid_dims(&t[1..end]) { - return false; - } - t = &t[end + 1..]; - } - _ => return valid_kinds().contains(&t), - } - } - false -} - -fn valid_dims(s: &str) -> bool { - for d in s.split(',') { - if d.is_empty() { - return false; - } - if !d.bytes().all(|b| b.is_ascii_digit()) { - return false; - } - } - true -} - fn is_array_kindexp(t: &str) -> bool { t.contains('[') } -fn type_error(kind: &str) -> String { - if kind == "int" || kind == "float" { - return "ambiguous type — use int64 or float64 instead".to_string(); - } - if kind == "string" || kind == "bytes" { - return "unknown type — use char/utf32 instead".to_string(); - } - "unknown type — valid: int8/16/32/64, uint8/16/32/64, float32/64, bool, char/utf32, any, []T, [N]T, *T".to_string() +fn type_error(_kind: &str) -> String { + "unknown type — valid: int8/16/32/64, uint8/16/32/64, float32/64, bool, char/utf32, dict, index, char, any, []T, [2,3]T, [?,N]T, A|B".to_string() } fn walk_read_only( diff --git a/layout/src/type_expr.rs b/layout/src/type_expr.rs new file mode 100644 index 00000000..3ba7f82d --- /dev/null +++ b/layout/src/type_expr.rs @@ -0,0 +1,170 @@ +//! 签名类型表达式(runtime篇-07,修订:无家族简写):语法校验 + 值匹配。 +//! +//! type = atom ("|" atom)* +//! atom = [dims] ( any | kind ) +//! dims = "[]" | "[" dim ("," dim)* "]" +//! dim = integer | "?" +//! any = "any" # 通配,匹配任意 kind +//! kind = 精确 kind 串 # 见 [`known_kind`] +//! +//! 铁律:不提供 int/uint/float/num 数值家族(位宽开放,int4/fp8/fp16…), +//! 也不提供 char 编码简写(编码须写明确,如 char/utf8、char/utf32)。多态靠显式 "|" 枚举。 + +/// 精确 kind 集合(对齐 runtime kind 常量,不含 None)。 +fn known_kind(k: &str) -> bool { + matches!( + k, + "bool" | "int8" | "int16" | "int32" | "int64" | "uint8" | "uint16" | "uint32" | "uint64" + | "float32" | "float64" | "char/utf32" | "char/utf8" | "char/ascii" | "dict" | "index" + | "extindex" | "rwir" | "rwfunc" | "scope" | "time" | "duration" + ) +} + +fn valid_base(s: &str) -> bool { + if s.is_empty() { + return false; + } + s == "any" || known_kind(s) +} + +fn valid_dim(s: &str) -> bool { + if s.is_empty() { + return false; + } + s == "?" || s.bytes().all(|b| b.is_ascii_digit()) +} + +fn valid_dims(s: &str) -> bool { + s.is_empty() || s.split(',').all(valid_dim) +} + +fn valid_atom(s: &str) -> bool { + if s.is_empty() { + return false; + } + if let Some(rest) = s.strip_prefix('[') { + let end = match rest.find(']') { + Some(e) => e, + None => return false, + }; + if !valid_dims(&rest[..end]) { + return false; + } + let base = &rest[end + 1..]; + return !base.is_empty() && valid_base(base); + } + valid_base(s) +} + +/// 类型表达式语法校验(装载期)。 +pub fn valid_type_expr(expr: &str) -> bool { + !expr.is_empty() && expr.split('|').all(valid_atom) +} + +fn base_match(s: &str, kind: &str) -> bool { + match s { + "any" => true, + _ => s == kind, + } +} + +fn match_shape(s: &str, ndim: i32, dims: &[i32]) -> bool { + if s.is_empty() { + return ndim >= 1; + } + let parts: Vec<&str> = s.split(',').collect(); + if parts.len() as i32 != ndim { + return false; + } + for (i, p) in parts.iter().enumerate() { + if *p == "?" { + continue; + } + if p.parse::().ok() != Some(dims[i]) { + return false; + } + } + true +} + +/// ndim = -1 表示「已消费 dims,不再判 ndim」(递归哨兵)。 +fn match_atom(s: &str, kind: &str, ndim: i32, dims: &[i32]) -> bool { + if let Some(rest) = s.strip_prefix('[') { + let end = match rest.find(']') { + Some(e) => e, + None => return false, + }; + if !match_shape(&rest[..end], ndim, dims) { + return false; + } + return match_atom(&rest[end + 1..], kind, -1, &[]); + } + if ndim >= 0 && ndim != 0 { + return false; + } + base_match(s, kind) +} + +/// 值(kind/ndim/dims)是否匹配类型表达式:任一 atom 命中即 true。 +pub fn match_type(expr: &str, kind: &str, ndim: i32, dims: &[i32]) -> bool { + !expr.is_empty() && expr.split('|').any(|atom| match_atom(atom, kind, ndim, dims)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid() { + for e in [ + "int64", "uint8", "float32", "bool", "any", + "char/utf8", "char/utf32", "char/ascii", "dict", "index", + "[]float32", "[2]float32", "[2,3]float32", "[2,3,4]float64", + "[?,768]float32", "[?,?]int8", + "int64|float64", "[2,3]float32|float32", "[]float32|[]float64", + "bool|char/utf8", "index|dict", + ] { + assert!(valid_type_expr(e), "{e} should be valid"); + } + } + + #[test] + fn invalid() { + for e in [ + "", "int|", "|int", "int||float64", "|", "[]", "[2]", "[?]", + "[2", "2]", "[2,]float32", "[,2]float32", "[2 3]float32", + "*int64", "@int64", "int64*", "float64|", "int ", "float32,float64", + "int", "uint", "float", "num", "char", "int4", "fp8", "fp16", "string", "charbyte", + ] { + assert!(!valid_type_expr(e), "{e} should be invalid"); + } + } + + #[test] + fn matching() { + let cases = [ + ("int64", "int64", 0, &[][..], true), + ("int64", "float64", 0, &[], false), + ("any", "dict", 0, &[], true), + ("any", "int4", 0, &[], true), + ("char/utf8", "char/utf8", 0, &[], true), + ("char/utf8", "char/utf32", 0, &[], false), + ("int64|float64", "float64", 0, &[], true), + ("int64|float64", "bool", 0, &[], false), + ("[]float32", "float32", 1, &[5], true), + ("[2,3]float32", "float32", 2, &[2, 3], true), + ("[2,3]float32", "float32", 2, &[2, 4], false), + ("[?,768]float32", "float32", 2, &[100, 768], true), + ("[?,768]float32", "float32", 2, &[100, 512], false), + ("[2,3]float32|float32", "float32", 0, &[], true), + ("[2,3]float32|float32", "float64", 0, &[], false), + ("[]float32|[]float64", "float64", 1, &[10], true), + ("bool|char/utf8", "char/utf8", 0, &[], true), + ("index|dict", "index", 0, &[], true), + ]; + for (expr, kind, ndim, dims, want) in cases { + let got = match_type(expr, kind, ndim, dims); + assert_eq!(got, want, "match_type({expr}, {kind}, ndim={ndim}, dims={dims:?})"); + } + } +} diff --git a/layout/tests/type_expr_test.rs b/layout/tests/type_expr_test.rs new file mode 100644 index 00000000..65b2a357 --- /dev/null +++ b/layout/tests/type_expr_test.rs @@ -0,0 +1,23 @@ +use kvlang_layout::parser::{self}; + +#[test] +fn parse_type_expression_signature() { + let src = "rwfunc f(A:int64|float64, B:[2,3]float32, C:[?,768]float32) -> (D:[]float32) {\n A -> D\n}\n"; + let (file, diags) = parser::parse_code(src).unwrap(); + let msgs: Vec = diags.iter().map(|d| d.message.clone()).collect(); + assert!(!parser::has_errors(&diags), "unexpected errors: {:?}", msgs); + + let sig = &file.funcs[0].sig; + assert_eq!(sig.name, "f"); + let tys: Vec<&str> = sig.params.iter().map(|p| p.ty.as_str()).collect(); + assert_eq!(tys, vec!["int64|float64", "[2,3]float32", "[?,768]float32"]); + let rets: Vec<&str> = sig.returns.iter().map(|p| p.ty.as_str()).collect(); + assert_eq!(rets, vec!["[]float32"]); +} + +#[test] +fn reject_malformed_type_expression() { + let src = "rwfunc f(A:[2,3) -> () {\n}\n"; + let (_, diags) = parser::parse_code(src).unwrap(); + assert!(parser::has_errors(&diags), "expected errors for malformed type"); +} diff --git a/oldhero/parser/parser.go b/oldhero/parser/parser.go index 41a31344..d6973449 100644 --- a/oldhero/parser/parser.go +++ b/oldhero/parser/parser.go @@ -23,6 +23,7 @@ import ( "oldhero/ast" "oldhero/keytree" + "oldhero/rwir" "oldhero/symbol" ) @@ -275,54 +276,6 @@ func (p *parser) parseFunc() ast.Func { return fn } -// validKinds kvlang 合法基础类型名(权威来源,与 kvspace.XValue.Kind() 对齐)。 -// num / any 是类型类(多态):num = int|float 联合,any = 任意类型;非真实落盘 kind,仅签名声明。 -var validKinds = map[string]bool{ - "int8": true, "int16": true, "int32": true, "int64": true, - "uint8": true, "uint16": true, "uint32": true, "uint64": true, - "float32": true, "float64": true, - "bool": true, "char/utf32": true, "char/utf8": true, "char/ascii": true, - "any": true, -} - -// validKindexp 校验类型表达式(kindexp):前缀修饰符序列 + 基础 kind。 -// 文法:kindexp ::= kind | '*' kindexp | '@' kindexp | '[' ']' kindexp | '[' dims ']' kindexp -func validKindexp(t string) bool { - for t != "" { - switch t[0] { - case '*', '@': - t = t[1:] - case '[': - end := strings.IndexByte(t, ']') - if end < 0 { - return false - } - if inner := t[1:end]; inner != "" && !validDims(inner) { - return false - } - t = t[end+1:] - default: - return validKinds[t] - } - } - return false -} - -// validDims 校验维度列表(逗号分隔的非负整数)。 -func validDims(s string) bool { - for _, d := range strings.Split(s, ",") { - if d == "" { - return false - } - for i := 0; i < len(d); i++ { - if d[i] < '0' || d[i] > '9' { - return false - } - } - } - return true -} - // isArrayKindexp 判断 kindexp 是否含数组修饰符([] [N])。 func isArrayKindexp(t string) bool { return strings.Contains(t, "[") @@ -331,23 +284,17 @@ func isArrayKindexp(t string) bool { // checkParamTypes 确保所有参数和返回值都有显式类型标注且类型名合法。 func (p *parser) checkParamTypes(sig *ast.FuncSig) { typeError := func(kind string) string { - if kind == "int" || kind == "float" { - return "ambiguous type — use int64 or float64 instead" - } - if kind == "string" || kind == "bytes" { - return "unknown type — use char/utf32 instead" - } - return "unknown type — valid: int8/16/32/64, uint8/16/32/64, float32/64, bool, char/utf32, any, []T, [N]T, *T" + return "unknown type — valid: int8/16/32/64, uint8/16/32/64, float32/64, bool, char/utf32, dict, index, char, any, []T, [2,3]T, [?,N]T, A|B" } for _, param := range sig.Params { - if !validKindexp(param.Type) { + if !rwir.ValidTypeExpr(param.Type) { p.errors = append(p.errors, Diagnostic{Message: fmt.Sprintf( "func %s: param %q: %s (got %q)", sig.Name, param.Name, typeError(param.Type), param.Type)}) } } for _, ret := range sig.Returns { - if !validKindexp(ret.Type) { + if !rwir.ValidTypeExpr(ret.Type) { p.errors = append(p.errors, Diagnostic{Message: fmt.Sprintf( "func %s: return value %q: %s (got %q)", sig.Name, ret.Name, typeError(ret.Type), ret.Type)}) diff --git a/oldhero/parser/parser_test.go b/oldhero/parser/parser_test.go new file mode 100644 index 00000000..66cc8294 --- /dev/null +++ b/oldhero/parser/parser_test.go @@ -0,0 +1,52 @@ +package parser + +import ( + "strings" + "testing" +) + +func TestParseTypeExpressionSignature(t *testing.T) { + src := "rwfunc f(A:int64|float64, B:[2,3]float32, C:[?,768]float32) -> (D:[]float32) {\n A -> D\n}\n" + file, diags, err := ParseCode(strings.NewReader(src)) + if err != nil { + t.Fatalf("ParseCode: %v", err) + } + if HasErrors(diags) { + t.Fatalf("unexpected errors: %v", diags) + } + sig := file.Funcs[0].Sig + if sig.Name != "f" { + t.Fatalf("name = %q", sig.Name) + } + var tys []string + for _, p := range sig.Params { + tys = append(tys, p.Type) + } + want := []string{"int64|float64", "[2,3]float32", "[?,768]float32"} + if len(tys) != len(want) { + t.Fatalf("params = %v, want %v", tys, want) + } + for i := range want { + if tys[i] != want[i] { + t.Fatalf("param[%d].Type = %q, want %q", i, tys[i], want[i]) + } + } + var rets []string + for _, p := range sig.Returns { + rets = append(rets, p.Type) + } + if len(rets) != 1 || rets[0] != "[]float32" { + t.Fatalf("returns = %v", rets) + } +} + +func TestRejectMalformedTypeExpression(t *testing.T) { + src := "rwfunc f(A:[2,3) -> () {\n}\n" + _, diags, err := ParseCode(strings.NewReader(src)) + if err != nil { + t.Fatalf("ParseCode: %v", err) + } + if !HasErrors(diags) { + t.Fatalf("expected errors for malformed type") + } +} diff --git a/oldhero/rwir/builtin/utf8/utf8.go b/oldhero/rwir/builtin/utf8/utf8.go index 89e794a8..1502bab7 100644 --- a/oldhero/rwir/builtin/utf8/utf8.go +++ b/oldhero/rwir/builtin/utf8/utf8.go @@ -20,12 +20,12 @@ func String(v kvspace.XValue) string { return string(Bytes(v)) } -// Len returns the number of UTF-8 runes in the charbyte array. +// Len returns the number of UTF-8 runes in the char/utf8 array. func Len(v kvspace.XValue) int { return utf8.RuneCount(Bytes(v)) } -// At returns the i-th rune as a charbyte (UTF-8 encoding of a single char). +// At returns the i-th rune as a char/utf8 (UTF-8 encoding of a single char). func At(v kvspace.XValue, i int) kvspace.XValue { b := Bytes(v) var idx int @@ -40,7 +40,7 @@ func At(v kvspace.XValue, i int) kvspace.XValue { return kvspace.NewCharByte([]byte(string(runeToUTF8(r, sz)))...) } -// Set returns a new charbyte with the i-th rune replaced. +// Set returns a new char/utf8 with the i-th rune replaced. func Set(v kvspace.XValue, i int, ch kvspace.XValue) kvspace.XValue { b := Bytes(v) chb := Bytes(ch) diff --git a/oldhero/rwir/type_expr.go b/oldhero/rwir/type_expr.go new file mode 100644 index 00000000..7314b99b --- /dev/null +++ b/oldhero/rwir/type_expr.go @@ -0,0 +1,162 @@ +package rwir + +import ( + "strconv" + "strings" +) + +// 签名类型表达式(runtime篇-07,修订:无数值家族):语法校验 + 值匹配。 +// +// type = atom ("|" atom)* +// atom = [dims] ( any | char | kind ) +// dims = "[]" | "[" dim ("," dim)* "]" +// dim = integer | "?" +// any = "any" # 通配,匹配任意 kind +// char = "char" # char/ 前缀(utf32/utf8/ascii) +// kind = 精确 kind 串 # 见 knownKinds +// +// 铁律:不提供 int/uint/float/num 数值家族——位宽是开放集合(int4/fp8/fp16…), +// 封闭枚举会漏、开放前缀会收进 runtime 尚不支持的 kind。多态靠显式 "|" 枚举。 + +func isCharKind(k string) bool { return strings.HasPrefix(k, "char/") } + +// 精确 kind 集合(对齐 runtime kind 常量,不含 None)。 +var knownKinds = map[string]bool{ + "bool": true, + "int8": true, "int16": true, "int32": true, "int64": true, + "uint8": true, "uint16": true, "uint32": true, "uint64": true, + "float32": true, "float64": true, + "char/utf32": true, "char/utf8": true, "char/ascii": true, + "dict": true, "index": true, "extindex": true, + "rwir": true, "rwfunc": true, "scope": true, "time": true, "duration": true, +} + +func validBase(s string) bool { + if s == "" { + return false + } + if s == "any" || s == "char" { + return true + } + return knownKinds[s] +} + +func validDim(s string) bool { + if s == "" { + return false + } + if s == "?" { + return true + } + for i := 0; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + return false + } + } + return true +} + +func validDims(s string) bool { + if s == "" { + return true + } + for _, d := range strings.Split(s, ",") { + if !validDim(d) { + return false + } + } + return true +} + +func validAtom(s string) bool { + if s == "" { + return false + } + if s[0] == '[' { + end := strings.IndexByte(s, ']') + if end < 0 { + return false + } + if !validDims(s[1:end]) { + return false + } + base := s[end+1:] + return base != "" && validBase(base) + } + return validBase(s) +} + +// ValidTypeExpr 判断类型表达式字符串是否语法合法(装载期校验)。 +func ValidTypeExpr(expr string) bool { + if expr == "" { + return false + } + for _, atom := range strings.Split(expr, "|") { + if !validAtom(atom) { + return false + } + } + return true +} + +func baseMatch(s, kind string) bool { + switch s { + case "any": + return true + case "char": + return isCharKind(kind) + default: + return s == kind + } +} + +func matchShape(shape string, ndim int, dims []int32) bool { + if shape == "" { + return ndim >= 1 + } + parts := strings.Split(shape, ",") + if len(parts) != ndim { + return false + } + for i, p := range parts { + if p == "?" { + continue + } + n, err := strconv.ParseInt(p, 10, 32) + if err != nil || int32(n) != dims[i] { + return false + } + } + return true +} + +// matchAtom:ndim = -1 表示「已消费 dims,不再判 ndim」(递归哨兵)。 +func matchAtom(atom, kind string, ndim int, dims []int32) bool { + if strings.HasPrefix(atom, "[") { + end := strings.IndexByte(atom, ']') + if end < 0 { + return false + } + if !matchShape(atom[1:end], ndim, dims) { + return false + } + return matchAtom(atom[end+1:], kind, -1, nil) + } + if ndim >= 0 && ndim != 0 { + return false + } + return baseMatch(atom, kind) +} + +// MatchType 判断值(kind/ndim/dims)是否匹配类型表达式:任一 atom 命中即 true。 +func MatchType(expr, kind string, ndim int, dims []int32) bool { + if expr == "" { + return false + } + for _, atom := range strings.Split(expr, "|") { + if matchAtom(atom, kind, ndim, dims) { + return true + } + } + return false +} diff --git a/oldhero/rwir/type_expr_test.go b/oldhero/rwir/type_expr_test.go new file mode 100644 index 00000000..fa1f2c50 --- /dev/null +++ b/oldhero/rwir/type_expr_test.go @@ -0,0 +1,74 @@ +package rwir + +import "testing" + +func TestValidTypeExpr(t *testing.T) { + valid := []string{ + "int64", "uint8", "float32", "bool", "char", "any", + "char/utf8", "char/utf32", "char/ascii", "dict", "index", "extindex", + "[]float32", "[2]float32", "[2,3]float32", "[2,3,4]float64", + "[?,768]float32", "[?,?]int8", + "int64|float64", "[2,3]float32|float32", "[]float32|[]float64", + "bool|char", "index|dict", "[]float32|int64|bool", + } + for _, e := range valid { + if !ValidTypeExpr(e) { + t.Errorf("ValidTypeExpr(%q) = false, want true", e) + } + } + + invalid := []string{ + "", "int|", "|int", "int||float64", "|", "[]", "[2]", "[?]", + "[2", "2]", "[2,]float32", "[,2]float32", "[2 3]float32", + "*int64", "@int64", "int64*", "float64|", + "int ", "float32,float64", + "int", "uint", "float", "num", "int4", "fp8", "fp16", "string", "charbyte", + } + for _, e := range invalid { + if ValidTypeExpr(e) { + t.Errorf("ValidTypeExpr(%q) = true, want false", e) + } + } +} + +func TestMatchType(t *testing.T) { + cases := []struct { + expr string + kind string + ndim int + dims []int32 + want bool + }{ + {"int64", "int64", 0, nil, true}, + {"int64", "float64", 0, nil, false}, + {"any", "dict", 0, nil, true}, + {"any", "int4", 0, nil, true}, + {"char", "char/utf8", 0, nil, true}, + {"char", "char/utf32", 0, nil, true}, + {"char", "int8", 0, nil, false}, + {"int64|float64", "float64", 0, nil, true}, + {"int64|float64", "bool", 0, nil, false}, + {"[]float32", "float32", 1, []int32{5}, true}, + {"[]float32", "float32", 0, nil, false}, + {"[2]float32", "float32", 1, []int32{2}, true}, + {"[2]float32", "float32", 1, []int32{3}, false}, + {"[2,3]float32", "float32", 2, []int32{2, 3}, true}, + {"[2,3]float32", "float32", 2, []int32{2, 4}, false}, + {"[2,3]float32", "float32", 1, []int32{2}, false}, + {"[?,768]float32", "float32", 2, []int32{100, 768}, true}, + {"[?,768]float32", "float32", 2, []int32{100, 512}, false}, + {"[2,3]float32|float32", "float32", 0, nil, true}, + {"[2,3]float32|float32", "float32", 2, []int32{2, 3}, true}, + {"[2,3]float32|float32", "float64", 0, nil, false}, + {"[]float32|[]float64", "float64", 1, []int32{10}, true}, + {"[]float32|[]float64", "int32", 1, []int32{10}, false}, + {"bool|char", "char/utf8", 0, nil, true}, + {"index|dict", "index", 0, nil, true}, + } + for _, c := range cases { + if got := MatchType(c.expr, c.kind, c.ndim, c.dims); got != c.want { + t.Errorf("MatchType(%q, %q, ndim=%d, dims=%v) = %v, want %v", + c.expr, c.kind, c.ndim, c.dims, got, c.want) + } + } +} diff --git a/oldhero/rwirext/term/serve.go b/oldhero/rwirext/term/serve.go index ad300f49..77acf5db 100644 --- a/oldhero/rwirext/term/serve.go +++ b/oldhero/rwirext/term/serve.go @@ -27,7 +27,7 @@ var ops = []op{ {name: "print", sig: "rwir print(A:any, ...) -> ()", nosep: true, rawnl: true}, {name: "println", sig: "rwir println(A:any, ...) -> ()"}, {name: "cerr", sig: "rwir cerr(A:any, ...) -> ()", cerr: true}, - {name: "input", sig: "rwir input(prompt:charbyte?) -> (C:charbyte)", input: true}, + {name: "input", sig: "rwir input(prompt:char/utf8?) -> (C:char/utf8)", input: true}, } var rt = ext.Ext{Ops: toOps(), Exec: exec} diff --git a/runtime-rwirext/rust/term/src/main.rs b/runtime-rwirext/rust/term/src/main.rs index 7b655e45..d965339f 100644 --- a/runtime-rwirext/rust/term/src/main.rs +++ b/runtime-rwirext/rust/term/src/main.rs @@ -1,24 +1,38 @@ -//! term 扩展 runtime:第一个通过 C ABI(kvlang_rwext)嵌入 C runtime 的扩展。 -//! 用 Rust 实现,独立进程常驻 serve,注册 print/println/cerr 并执行外部 rwir。 +//! term 扩展 runtime:模式2(runtime 主导 + term 嵌入,单线程函数调用)。 +//! 专注一个 vthread 的 print/println/cerr:bootstrap 拿 vid 后循环 +//! execute_vthread(vid)(runtime 主导执行,遇 ext rwir 直接返回 pc) +//! → RunSeq 连续处理己方 print → 写回 vthread pc → 继续, +//! 直到 vthread done,term 退出进程。 -use std::ffi::{c_char, c_int, CStr, CString}; -use std::time::Duration; +use std::ffi::{c_char, c_int, c_void, CStr, CString}; +use std::io::Write; +use std::ptr::null_mut; #[repr(C)] -struct rwext_conn { +struct kvlang_rt { _p: [u8; 0], } +// 对齐 C 的 struct rwext_conn { kv_t *kv; }(单指针)。 +#[repr(C)] +struct rwext_conn { + kv: *mut c_void, +} + +unsafe impl Send for kvlang_rt {} unsafe impl Send for rwext_conn {} unsafe extern "C" { - fn rwext_connect(dsn: *const c_char) -> *mut rwext_conn; - fn rwext_disconnect(c: *mut rwext_conn); - fn rwext_register(c: *mut rwext_conn, opcode: *const c_char, nr: i32, nw: i32, sig: *const c_char) -> c_int; - fn rwext_list(c: *mut rwext_conn, prefix: *const c_char) -> *mut c_char; - fn rwext_get(c: *mut rwext_conn, key: *const c_char) -> *mut c_char; + // kvlang_rt ABI(主导执行) + fn kvlang_rt_connect(dsn: *const c_char) -> *mut kvlang_rt; + fn kvlang_rt_kv(rt: *mut kvlang_rt) -> *mut c_void; + fn kvlang_rt_bootstrap(rt: *mut kvlang_rt, funcname: *const c_char, + args: *const *const c_char, nargs: c_int) -> *mut c_char; + fn kvlang_rt_execute_vthread(rt: *mut kvlang_rt, vid: *const c_char, out_pc: *mut *mut c_char) -> c_int; + + // rwext ABI(注册 / 处理 print) + fn rwext_register(c: *mut rwext_conn, opcode: *const c_char, nr: c_int, nw: c_int, sig: *const c_char) -> c_int; fn rwext_set(c: *mut rwext_conn, key: *const c_char, val: *const c_char) -> c_int; - fn rwext_del(c: *mut rwext_conn, key: *const c_char) -> c_int; fn rwext_print_line(c: *mut rwext_conn, pc: *const c_char, rawnl: *mut c_int, cerr: *mut c_int) -> *mut c_char; fn rwext_next_pc(pc: *const c_char) -> *mut c_char; } @@ -58,73 +72,72 @@ fn register(c: *mut rwext_conn) { } } -fn serve_op(c: *mut rwext_conn, op: &Op) { - let base = format!("/lib/{}", op.name); - let children = take(unsafe { rwext_list(c, cs(&format!("{base}/")).as_ptr()) }); - for child in children.split('\n') { - if !child.starts_with(".todo<") || !child.ends_with('>') { - continue; +fn print_line(line: &str, rawnl: i32, is_cerr: i32) { + if is_cerr != 0 { + eprint!("{line}"); + if rawnl == 0 { + eprintln!(); + } + std::io::stderr().flush().ok(); + } else { + print!("{line}"); + if rawnl == 0 { + println!(); + } + std::io::stdout().flush().ok(); + } +} + +fn main() { + let dsn = std::env::var("KVSPACE").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); + let funcname = std::env::args().nth(1).unwrap_or_else(|| "main".to_string()); + + let rt = unsafe { kvlang_rt_connect(cs(&dsn).as_ptr()) }; + if rt.is_null() { + eprintln!("term: kvlang_rt_connect failed: {dsn}"); + std::process::exit(1); + } + let kv = unsafe { kvlang_rt_kv(rt) }; + let mut conn = rwext_conn { kv }; + + register(&mut conn); + + let vid = unsafe { kvlang_rt_bootstrap(rt, cs(&funcname).as_ptr(), null_mut(), 0) }; + if vid.is_null() { + eprintln!("term: bootstrap {funcname} failed"); + std::process::exit(1); + } + let vid = take(vid); + let vpc = format!("/vthread/{vid}/\u{2025}pc"); + + loop { + // runtime 主导执行,遇 ext rwir 直接返回 pc + let mut pc: *mut c_char = null_mut(); + let rc = unsafe { kvlang_rt_execute_vthread(rt, cs(&vid).as_ptr(), &mut pc) }; + if rc == 0 { + break; // vthread done + } + if rc != 1 { + break; // 错误 } - let vid = &child[6..child.len() - 1]; - let todo_key = format!("{base}/{child}"); - let pcid = take(unsafe { rwext_get(c, cs(&todo_key).as_ptr()) }); - let (pc, id) = match pcid.rfind('|') { - Some(i) => (&pcid[..i], &pcid[i + 1..]), - None => (pcid.as_str(), ""), - }; - - let mut cur = pc.to_string(); + + // RunSeq:连续处理己方 print,遇非己方停下(c 停在非己方 pc) + let mut c = take(pc); loop { let mut rawnl = 0i32; let mut is_cerr = 0i32; - let p = unsafe { rwext_print_line(c, cs(&cur).as_ptr(), &mut rawnl, &mut is_cerr) }; + let p = unsafe { rwext_print_line(&mut conn, cs(&c).as_ptr(), &mut rawnl, &mut is_cerr) }; if p.is_null() { break; } let line = take(p); - if is_cerr != 0 { - eprint!("{line}"); - if rawnl == 0 { - eprintln!(); - } - } else { - print!("{line}"); - if rawnl == 0 { - println!(); - } - } - cur = take(unsafe { rwext_next_pc(cs(&cur).as_ptr()) }); + print_line(&line, rawnl, is_cerr); + c = take(unsafe { rwext_next_pc(cs(&c).as_ptr()) }); } - let vt_pc = format!("/vthread/{vid}/\u{2025}pc"); + // 写回非己方 pc,让 runtime 从它继续 unsafe { - rwext_set(c, cs(&vt_pc).as_ptr(), cs(&cur).as_ptr()); - let done_key = format!("{base}/.done<{vid}>"); - rwext_set(c, cs(&done_key).as_ptr(), cs(id).as_ptr()); - rwext_del(c, cs(&todo_key).as_ptr()); - } - } -} - -struct Conn(*mut rwext_conn); -unsafe impl Send for Conn {} - -fn serve(conn: Conn) { - register(conn.0); - loop { - for op in OPS { - serve_op(conn.0, op); + rwext_set(&mut conn, cs(&vpc).as_ptr(), cs(&c).as_ptr()); } - std::thread::sleep(Duration::from_millis(500)); - } -} - -fn main() { - let dsn = std::env::args().nth(1).unwrap_or_else(|| "redis://127.0.0.1:6379".to_string()); - let c = unsafe { rwext_connect(cs(&dsn).as_ptr()) }; - if c.is_null() { - eprintln!("term: rwext_connect failed: {dsn}"); - std::process::exit(1); } - serve(Conn(c)); } diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 8d12cd91..284180a4 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -22,7 +22,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${BIN_DIR}") add_library(kvlang_runtime SHARED src/strbuf.c src/xvalue.c src/kv.c src/keytree.c src/rwir.c - src/vthread.c src/logx.c src/builtin.c src/kvcpu.c src/runtime.c src/rwext.c) + src/vthread.c src/logx.c src/builtin.c src/kvcpu.c src/runtime.c src/rwext.c + src/type_expr.c) target_include_directories(kvlang_runtime PUBLIC include src) target_compile_definitions(kvlang_runtime PRIVATE _GNU_SOURCE) diff --git a/runtime/include/kvlang_runtime.h b/runtime/include/kvlang_runtime.h index ee9efc4e..9de9a09e 100644 --- a/runtime/include/kvlang_runtime.h +++ b/runtime/include/kvlang_runtime.h @@ -9,6 +9,15 @@ void *kvlang_rt_kv(kvlang_rt *rt); /* 内部 kv 句柄,供 rwirext 扩展用 int kvlang_rt_execute_pc(kvlang_rt *rt, const char *pc); +/* 模式2(runtime 主导 + term 嵌入):分配 vthread 并 bootstrap,返回 vid(malloc)。 + * term 专注这一个 vid 的 ext rwir 处理。 */ +char *kvlang_rt_bootstrap(kvlang_rt *rt, const char *funcname, + const char *const *args, int nargs); + +/* 模式2:从 vid 的当前 pc 执行 vthread,遇 ext rwir 不再 handoff/watch,直接返回。 + * 返回值:1=遇 ext rwir(*out_pc=该 PC,malloc,调用方 free);0=vthread done;-1=错误。 */ +int kvlang_rt_execute_vthread(kvlang_rt *rt, const char *vid, char **out_pc); + int kvlang_rt_execute(kvlang_rt *rt, const char *funcname, const char *const *args, int nargs, char **ret, char *err, uint32_t err_cap); diff --git a/runtime/include/kvlang_rwext.h b/runtime/include/kvlang_rwext.h index 51ed2a75..30c34885 100644 --- a/runtime/include/kvlang_rwext.h +++ b/runtime/include/kvlang_rwext.h @@ -1,5 +1,6 @@ #pragma once #include +#include /* kvlang 扩展 runtime ABI:供第三方语言(Rust/Python/Go)通过 C ABI 嵌入 C runtime, * 实现自定义 rwirext(如 term 的 print)。opaque handle + C 字符串,不暴露内部结构。 */ @@ -52,3 +53,9 @@ char *rwext_resolve_read(rwext_conn *c, const char *pc, int idx); /* 解析写参 idx 为 KV 路径(路径 → 直接返回;变量 → 帧槽路径)。 */ char *rwext_resolve_write(rwext_conn *c, const char *pc, int idx); + +/* 签名类型表达式(runtime篇-07)——供扩展做实参类型判定。 */ +/* 语法校验:type = atom("|"atom)*, atom = [dims](family|kind), dims="[]"|"["dim(","dim)*"]", dim=int|"?"。 */ +bool type_expr_valid(const char *expr); +/* 值判定:kind 为实际落盘 kind 串,ndim 为秩(标量 0),dims 为各维长(标量传 NULL)。 */ +bool type_expr_match(const char *expr, const char *kind, int32_t ndim, const int32_t *dims); diff --git a/runtime/src/kvcpu.c b/runtime/src/kvcpu.c index a088455e..837f23ef 100644 --- a/runtime/src/kvcpu.c +++ b/runtime/src/kvcpu.c @@ -327,7 +327,7 @@ static bool is_copy_op(const char *opcode) { return strcmp(opcode, "=") == 0; } -static bool is_user_rwir(kv_t *kv, const char *opcode) { +static bool is_ext_rwir(kv_t *kv, const char *opcode) { if (opcode[0] == '/') return false; char *rk = kt_rwir(opcode); xval_t v; xv_zero(&v); @@ -431,7 +431,8 @@ char *kvcpu_bootstrap(kv_t *kv, const char *vtid, const char *funcname, return ep; } -int kvcpu_execute(kv_t *kv, const char *pc) { +int kvcpu_execute_mode(kv_t *kv, const char *pc, kvmode_t mode, char **out_pc) { + if (out_pc) *out_pc = NULL; sbuf_t vtid_b; sb_init(&vtid_b); const char *vtid = kt_vtid_from_pc(pc, &vtid_b); if (vtid[0] == 0) { sb_free(&vtid_b); return -1; } @@ -502,7 +503,13 @@ int kvcpu_execute(kv_t *kv, const char *pc) { exec_err = bi_native(&f); } else if (is_copy_op(inst.opcode)) { exec_err = bi_execute_copy(kv, vtid, cur, &inst); - } else if (is_user_rwir(kv, inst.opcode)) { + } else if (is_ext_rwir(kv, inst.opcode)) { + if (mode == KVMODE_RETURN) { + if (out_pc) *out_pc = strdup(cur); + free(fr); rwir_inst_free(&inst); + free(cur); sb_free(&vtid_b); + return 1; + } exec_err = handoff_external_rwir(kv, vtid, cur, &inst); } else { /* 用户函数 → call */ @@ -535,3 +542,8 @@ int kvcpu_execute(kv_t *kv, const char *pc) { sb_free(&vtid_b); return rc; } + +int kvcpu_execute(kv_t *kv, const char *pc) { + int rc = kvcpu_execute_mode(kv, pc, KVMODE_WATCH, NULL); + return rc == 1 ? 0 : rc; /* WATCH 模式不返回 1,防御性归一 */ +} diff --git a/runtime/src/runtime.c b/runtime/src/runtime.c index 9e54748a..2ce99251 100644 --- a/runtime/src/runtime.c +++ b/runtime/src/runtime.c @@ -25,6 +25,21 @@ int kvlang_rt_execute_pc(kvlang_rt *rt, const char *pc) { return kvcpu_execute(rt->kv, pc); } +static char *read_vthread_pc(kv_t *kv, const char *vid) { + sbuf_t key; sb_init(&key); kt_vthread_pc(vid, &key); + xval_t v; xv_zero(&v); kv_get_one(kv, key.p, &v); + char *pc = xv_none(&v) ? strdup("") : xv_value_string(&v); + xv_free(&v); sb_free(&key); + return pc; +} + +int kvlang_rt_execute_vthread(kvlang_rt *rt, const char *vid, char **out_pc) { + char *pc = read_vthread_pc(rt->kv, vid); + int rc = kvcpu_execute_mode(rt->kv, pc, KVMODE_RETURN, out_pc); + free(pc); + return rc; +} + static char *alloc_vtid(kv_t *kv) { sbuf_t seq; sb_init(&seq); sb_puts(&seq, VTHREAD_ROOT "/" RUNTIME_MEMBER_SEP "seq"); @@ -43,6 +58,25 @@ static char *alloc_vtid(kv_t *kv) { return strdup(buf); } +char *kvlang_rt_bootstrap(kvlang_rt *rt, const char *funcname, + const char *const *args, int nargs) { + kv_t *kv = rt->kv; + char *vtid = alloc_vtid(kv); + sbuf_t vtroot; sb_init(&vtroot); kt_vthread(vtid, &vtroot); + char *stack_vt = kt_stack(vtroot.p); + char e[256]; + kv_mkindex(kv, stack_vt, e, sizeof e); + char *first_pc = kvcpu_bootstrap(kv, vtid, funcname, args, nargs); + if (!first_pc) { + free(stack_vt); free(vtid); sb_free(&vtroot); + return NULL; + } + vt_set(kv, vtid, first_pc, "init"); + free(first_pc); + free(stack_vt); sb_free(&vtroot); + return vtid; +} + int kvlang_rt_execute(kvlang_rt *rt, const char *funcname, const char *const *args, int nargs, char **ret, char *err, uint32_t err_cap) { diff --git a/runtime/src/runtime_internal.h b/runtime/src/runtime_internal.h index c17c6d75..be5efb09 100644 --- a/runtime/src/runtime_internal.h +++ b/runtime/src/runtime_internal.h @@ -123,6 +123,9 @@ bool xv_is_int_kind(const char *kind); bool xv_is_uint_kind(const char *kind); bool xv_is_float_kind(const char *kind); bool xv_is_num_kind(const char *kind); +/* 签名类型表达式(runtime篇-07)校验/匹配 */ +bool type_expr_valid(const char *expr); +bool type_expr_match(const char *expr, const char *kind, int32_t ndim, const int32_t *dims); int64_t xv_as_int64(const xval_t *v); double xv_as_float64(const xval_t *v); uint64_t xv_as_uint64(const xval_t *v); @@ -242,7 +245,14 @@ void display(const xval_t *v, char **out); /* malloc,对 /* ── kvcpu ─────────────────────────────────────────────────────────── */ -int kvcpu_execute(kv_t *kv, const char *pc); +/* 两种执行模式(详见 runtime篇-05): + * KVMODE_WATCH 模式1:runtime 主导,遇 ext rwir → handoff(.todo) + watch(.done) 阻塞(2 线程) + * KVMODE_RETURN 模式2:扩展主导,遇 ext rwir → 不 handoff 不 watch,返回该 ext rwir 的 PC(单线程函数调用) + * kvcpu_execute_mode 返回值:-1 错误;0 正常结束(done);1 遇 ext rwir(仅 KVMODE_RETURN,*out_pc=其 PC)。 */ +typedef enum { KVMODE_WATCH = 0, KVMODE_RETURN = 1 } kvmode_t; + +int kvcpu_execute_mode(kv_t *kv, const char *pc, kvmode_t mode, char **out_pc); +int kvcpu_execute(kv_t *kv, const char *pc); /* = KVMODE_WATCH,out_pc 忽略 */ char *kvcpu_bootstrap(kv_t *kv, const char *vtid, const char *funcname, const char *const *args, int nargs); /* ── logx ──────────────────────────────────────────────────────────── */ diff --git a/runtime/src/type_expr.c b/runtime/src/type_expr.c new file mode 100644 index 00000000..51d30255 --- /dev/null +++ b/runtime/src/type_expr.c @@ -0,0 +1,146 @@ +#include "runtime_internal.h" + +/* ── 签名类型表达式(runtime篇-07,修订:无家族简写)────────────────── + * type = atom ("|" atom)* + * atom = [dims] ( any | kind ) + * dims = "[]" | "[" dim ("," dim)* "]" + * dim = integer | "?" + * any = "any" # 通配,匹配任意 kind + * kind = 精确 kind 串 # 见 known_kind + * + * 铁律:不提供 int/uint/float/num 数值家族(位宽开放集合,int4/fp8/fp16…), + * 也不提供 char 编码简写(编码须写明确,如 char/utf8、char/utf32)。 + * 多态靠显式 "|" 枚举(如 int8|int16|int32|int64)。 + */ + +static bool kind_eq(const char *s, size_t len, const char *k) { + size_t kl = strlen(k); + return len == kl && memcmp(s, k, len) == 0; +} + +/* 精确 kind 集合(对齐 runtime kind 常量,不含 None)。 */ +static bool known_kind(const char *s, size_t len) { + return kind_eq(s, len, "bool") || + kind_eq(s, len, "int8") || kind_eq(s, len, "int16") || + kind_eq(s, len, "int32") || kind_eq(s, len, "int64") || + kind_eq(s, len, "uint8") || kind_eq(s, len, "uint16") || + kind_eq(s, len, "uint32") || kind_eq(s, len, "uint64") || + kind_eq(s, len, "float32") || kind_eq(s, len, "float64") || + kind_eq(s, len, "char/utf32") || kind_eq(s, len, "char/utf8") || + kind_eq(s, len, "char/ascii") || + kind_eq(s, len, "dict") || kind_eq(s, len, "index") || + kind_eq(s, len, "extindex") || + kind_eq(s, len, "rwir") || kind_eq(s, len, "rwfunc") || + kind_eq(s, len, "scope") || kind_eq(s, len, "time") || + kind_eq(s, len, "duration"); +} + +/* base = any | kind(kind 为精确合法 kind 串) */ +static bool valid_base(const char *s, size_t len) { + if (len == 0) return false; + if (len == 3 && strncmp(s, "any", 3) == 0) return true; + return known_kind(s, len); +} + +static bool valid_dim(const char *s, size_t len) { + if (len == 0) return false; + if (len == 1 && s[0] == '?') return true; + for (size_t i = 0; i < len; i++) + if (s[i] < '0' || s[i] > '9') return false; + return true; +} + +/* dims = ε(空 [] = 1 维任意,等价 [?]) | dim ("," dim)* */ +static bool valid_dims(const char *s, size_t len) { + if (len == 0) return true; + const char *p = s, *end = s + len; + while (p < end) { + const char *comma = memchr(p, ',', (size_t)(end - p)); + size_t seg = comma ? (size_t)(comma - p) : (size_t)(end - p); + if (!valid_dim(p, seg)) return false; + p += seg + (comma ? 1 : 0); + } + return true; +} + +/* atom = [dims] base */ +static bool valid_atom(const char *s, size_t len) { + if (len == 0) return false; + const char *p = s; + if (*p == '[') { + const char *end = memchr(p, ']', len); + if (!end) return false; + if (!valid_dims(p + 1, (size_t)(end - p - 1))) return false; + p = end + 1; + if (p >= s + len) return false; /* 缺 base */ + } + return valid_base(p, (size_t)(s + len - p)); +} + +/* 类型表达式语法校验(装载期)。 */ +bool type_expr_valid(const char *expr) { + if (!expr || !*expr) return false; + const char *p = expr; + for (;;) { + const char *pipe = strchr(p, '|'); + size_t len = pipe ? (size_t)(pipe - p) : strlen(p); + if (!valid_atom(p, len)) return false; + if (!pipe) break; + p = pipe + 1; + if (*p == '\0') return false; /* 尾随 '|' → 空 atom */ + } + return true; +} + +/* any/kind 判定(kind 为运行时实际落盘 kind 串)。 */ +static bool base_match(const char *s, size_t len, const char *kind) { + if (len == 3 && strncmp(s, "any", 3) == 0) return true; + return kind_eq(s, len, kind); +} + +/* match_shape:shape="" → ndim>=1;否则维数须一致且逐维 ?(跳过)或精确相等。 */ +static bool match_shape(const char *s, size_t len, int32_t ndim, const int32_t *dims) { + if (len == 0) return ndim >= 1; + int count = 1; + for (size_t i = 0; i < len; i++) if (s[i] == ',') count++; + if (count != ndim) return false; + const char *p = s, *end = s + len; + int i = 0; + while (p < end && i < ndim) { + const char *comma = memchr(p, ',', (size_t)(end - p)); + size_t seg = comma ? (size_t)(comma - p) : (size_t)(end - p); + if (!(seg == 1 && p[0] == '?')) { + long v = 0; + for (size_t j = 0; j < seg; j++) v = v * 10 + (p[j] - '0'); + if (v != dims[i]) return false; + } + p += seg + (comma ? 1 : 0); + i++; + } + return true; +} + +/* match_atom:ndim = -1 表示「已消费 dims,不再判 ndim」(递归哨兵)。 */ +static bool match_atom(const char *s, size_t len, const char *kind, int32_t ndim, const int32_t *dims) { + if (s[0] == '[') { + const char *end = memchr(s, ']', len); + if (!end) return false; + if (!match_shape(s + 1, (size_t)(end - s - 1), ndim, dims)) return false; + return match_atom(end + 1, (size_t)(s + len - end - 1), kind, -1, NULL); + } + if (ndim >= 0 && ndim != 0) return false; /* 标量 */ + return base_match(s, len, kind); +} + +/* 值(kind/ndim/dims)是否匹配类型表达式:任一 atom 命中即 true。 */ +bool type_expr_match(const char *expr, const char *kind, int32_t ndim, const int32_t *dims) { + if (!expr || !kind) return false; + const char *p = expr; + while (p && *p) { + const char *pipe = strchr(p, '|'); + size_t len = pipe ? (size_t)(pipe - p) : strlen(p); + if (match_atom(p, len, kind, ndim, dims)) return true; + p = pipe ? pipe + 1 : NULL; + } + return false; +} diff --git a/tutorial/01-basics/time.kv b/tutorial/01-basics/time.kv index f243541c..0f88e996 100644 --- a/tutorial/01-basics/time.kv +++ b/tutorial/01-basics/time.kv @@ -5,7 +5,7 @@ # t0 < t1: true # t0 > t1: false # delta >= 0 ns: true -# delta ms = 0 +# delta ms = # delta s = 0 # t0 + delta == t1: true # 1000 ms diff --git a/tutorial/test.py b/tutorial/test.py index 911752bb..0c411dbb 100755 --- a/tutorial/test.py +++ b/tutorial/test.py @@ -20,9 +20,8 @@ ROOT = Path(__file__).resolve().parent.parent KV = str(ROOT / "kvlang") RUST_BIN = str(ROOT / "target" / "debug" / "kvlang") -LAYOUT_BIN = os.environ.get("KVLANG_LAYOUT_BIN", - str(ROOT / "layout" / "target" / "debug" / "examples" / "layout_file")) -CRUN_BIN = os.environ.get("KVBIN", str(ROOT / "runtime" / "test" / "run")) +LAYOUT_BIN = os.environ.get("KVLANG_LAYOUT_BIN", str(ROOT / "bin" / "layout_file")) +TERM_BIN = os.environ.get("KVLANG_TERM_BIN", str(ROOT / "bin" / "term")) _C_DSN = os.environ.get("KVSPACE", "redis://127.0.0.1:6379") SHM_PATH = "/tmp/kvlang_rust_test" FAIL_CSV = (ROOT / "tutorial" / "test_failures.csv").resolve() @@ -264,12 +263,12 @@ def _c_test_file(f: Path, expects: list[str], env: dict) -> tuple[bool, str]: return False, f"layout failed: {layout.stderr.strip()[:100]}" entry = _detect_entry(layout.stdout) try: - crun = subprocess.run([CRUN_BIN, entry], capture_output=True, text=True, + crun = subprocess.run([TERM_BIN, entry], capture_output=True, text=True, timeout=120, cwd=str(ROOT), env={**env, "KVSPACE": _C_DSN}) except subprocess.TimeoutExpired: return False, "timeout" if crun.returncode != 0: - return False, f"c runtime exit {crun.returncode}: {crun.stderr.strip()[:100]}" + return False, f"term exit {crun.returncode}: {crun.stderr.strip()[:100]}" for pat in expects: if pat not in crun.stdout: return False, f"want {pat!r}" @@ -282,8 +281,8 @@ def main(): ap.add_argument("--no-build", action="store_true", help="skip make build") ap.add_argument("--errorexit", action="store_true", help="exit on first error") ap.add_argument("--bench", action="store_true", help="benchmark matching .kv/.py/.c files") - ap.add_argument("--runtime", default="go", choices=("go", "rust", "c"), - help="runtime to test (default: go; env KVLANG_RUNTIME=c 等价 --runtime c)") + ap.add_argument("--runtime", default="c", choices=("go", "rust", "c"), + help="runtime to test (default: c; env KVLANG_RUNTIME=c 等价 --runtime c)") args = ap.parse_args() if os.environ.get("KVLANG_RUNTIME") == "c": args.runtime = "c" diff --git a/tutorial/test_c_runtime.py b/tutorial/test_c_runtime.py deleted file mode 100644 index b1b4bcb1..00000000 --- a/tutorial/test_c_runtime.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -"""C runtime 验证:Rust layout → redis → C runtime,对比 .kv 头注释期望输出。 - -用法:python3 tutorial/test_c_runtime.py [filter] -前置:redis 运行中;layout 示例 + C runtime 已构建。 -""" -import re -import subprocess -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -LAYOUT = str(ROOT / "layout" / "target" / "debug" / "examples" / "layout_file") -CRUN = str(ROOT / "runtime" / "test" / "run") - - -def parse_expects(f: Path) -> list[str]: - pats = [] - in_block = False - for line in f.read_text().splitlines(): - if line.startswith("# 期望输出"): - in_block = True - continue - if in_block: - if line.startswith("# ") or line.startswith("# \t"): - p = line[2:].strip() - p = re.sub(r"\s*\(.*\)\s*$", "", p) - if p: - pats.append(p) - elif not line.startswith("#"): - break - return pats - - -def needs_skip(f: Path) -> bool: - for line in f.read_text().splitlines(): - if line.startswith("# extern") or line.startswith("# wip"): - return True - if line and not line.startswith("#"): - return False - return False - - -def detect_entry(out: str) -> str: - m = re.search(r"ENTRY=(\S+)", out) - return m.group(1) if m else "init" - - -def flush(): - subprocess.run(["redis-cli", "-p", "6379", "FLUSHALL"], capture_output=True, timeout=5) - - -def layout(f: Path): - return subprocess.run([LAYOUT, str(f)], capture_output=True, text=True, timeout=60) - - -def crun(entry: str): - cmd = [CRUN, entry] - return subprocess.run(cmd, capture_output=True, text=True, timeout=30) - - -def main() -> int: - filt = sys.argv[1] if len(sys.argv) > 1 else "" - files = sorted(ROOT.glob("tutorial/**/*.kv")) - passed = failed = skipped = 0 - fails = [] - for f in files: - if filt and filt not in str(f): - continue - if needs_skip(f): - skipped += 1 - continue - expects = parse_expects(f) - if not expects: - continue - print(f" {f.name}", file=sys.stderr, flush=True) - - flush() - lr = layout(f) - if lr.returncode != 0: - failed += 1 - fails.append((f.name, f"[layout] {lr.stderr.strip()[-160:]}")) - continue - entry = detect_entry(lr.stdout) - try: - cr = crun(entry) - except subprocess.TimeoutExpired: - failed += 1 - fails.append((f.name, "[timeout]")) - continue - if cr.returncode != 0: - failed += 1 - fails.append((f.name, f"[run rc={cr.returncode}] {cr.stderr.strip()[-160:]}")) - continue - ok = all(e in cr.stdout for e in expects) - if ok: - passed += 1 - else: - failed += 1 - fails.append((f.name, f"[entry={entry}] expect={expects} got={cr.stdout.strip()[-160:]!r}")) - - print(f"\nPASS {passed} FAIL {failed} SKIP {skipped}") - for name, why in fails: - print(f" FAIL {name}: {why}") - return 1 if failed else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tutorial/test_crosslang.py b/tutorial/test_crosslang.py deleted file mode 100644 index d46a3161..00000000 --- a/tutorial/test_crosslang.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -"""跨语言验证:Rust layout → redis → Go runtime,对比 .kv 头注释期望输出。 - -用法:python3 tutorial/test_crosslang.py [filter] -前置:redis 运行中;layout 示例已构建(kvlang/layout/target/debug/examples/layout_file)。 -""" -import re -import subprocess -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -KV = str(ROOT / "kvlang") -RUST_LAYOUT = str(ROOT / "layout" / "target" / "debug" / "examples" / "layout_file") - - -def parse_expects(f: Path) -> list[str]: - pats = [] - in_block = False - for line in f.read_text().splitlines(): - if line.startswith("# 期望输出"): - in_block = True - continue - if in_block: - if line.startswith("# ") or line.startswith("# \t"): - p = line[2:].strip() - p = re.sub(r"\s*\(.*\)\s*$", "", p) - if p: - pats.append(p) - elif not line.startswith("#"): - break - return pats - - -def needs_skip(f: Path) -> bool: - for line in f.read_text().splitlines(): - if line.startswith("# extern") or line.startswith("# wip"): - return True - if line and not line.startswith("#"): - return False - return False - - -def detect_entry(layout_stdout: str) -> str: - # layout_file 输出 ENTRY=...(复刻 Go findEntry) - m = re.search(r"ENTRY=(\S+)", layout_stdout) - return m.group(1) if m else "init" - - -def flush() -> None: - subprocess.run(["redis-cli", "-p", "6379", "FLUSHALL"], capture_output=True, timeout=5) - - -def rust_layout(f: Path): - return subprocess.run([RUST_LAYOUT, str(f)], capture_output=True, text=True, timeout=60) - - -def go_run(entry: str): - # 裸 "init" 入口:run 无参数 → runLib("","init");lib 块入口:run X.init - cmd = f"{KV} run" if entry == "init" else f"{KV} run {entry}" - return subprocess.run( - ["script", "-qec", cmd, "/dev/null"], capture_output=True, text=True, timeout=20 - ) - - -def main() -> int: - filt = sys.argv[1] if len(sys.argv) > 1 else "" - files = sorted(ROOT.glob("tutorial/**/*.kv")) - passed = failed = skipped = 0 - fails = [] - for f in files: - if filt and filt not in str(f): - continue - if needs_skip(f): - skipped += 1 - continue - expects = parse_expects(f) - if not expects: - continue - print(f" {f.name}", file=sys.stderr, flush=True) - - flush() - lr = rust_layout(f) - if lr.returncode != 0: - failed += 1 - fails.append((f.name, f"[layout] {lr.stderr.strip()[-160:]}")) - continue - entry = detect_entry(lr.stdout) - try: - gr = go_run(entry) - except subprocess.TimeoutExpired: - failed += 1 - fails.append((f.name, "[timeout]")) - continue - if gr.returncode != 0 and not gr.stdout: - failed += 1 - fails.append((f.name, f"[run rc={gr.returncode}] {gr.stderr.strip()[-160:]}")) - continue - ok = all(e in gr.stdout for e in expects) - if ok: - passed += 1 - else: - failed += 1 - fails.append((f.name, f"[entry={entry}] expect={expects} got={gr.stdout.strip()[-160:]!r}")) - - print(f"\nPASS {passed} FAIL {failed} SKIP {skipped}") - for name, why in fails: - print(f" FAIL {name}: {why}") - return 1 if failed else 0 - - -if __name__ == "__main__": - sys.exit(main())