Skip to content

Commit eb2bac6

Browse files
committed
fix: validate applied struct type arg arity
1 parent fea36dc commit eb2bac6

File tree

7 files changed

+202
-21
lines changed

7 files changed

+202
-21
lines changed

calcit/test-generics.cirru

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
|Wrapped $ %{} :CodeEntry (:doc |) (:schema nil)
2222
:code $ quote
2323
defenum Wrapped
24-
:pair $ :: Pair :number :string
24+
:pair Pair
2525
:none
2626
:examples $ []
2727
|main! $ %{} :CodeEntry (:doc |) (:schema nil)

docs/CalcitAgent.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ let
662662
有两种方式标注函数返回类型:
663663

664664
- **紧凑模式(推荐)**:紧跟在参数列表后的类型标签。
665-
- **正式模式**:使用 `hint-fn`(通常放在函数体开头)。
665+
- **正式模式**局部 `fn` 使用 `hint-fn`(通常放在函数体开头);顶层 `defn` / `defmacro` 使用 `:schema`
666666
- 泛型变量:`hint-fn $ {} (:generics $ [] 'T 'S)`
667667
- 旧 clause 写法(如 `(hint-fn (return-type ...))` / `(generics ...)` / `(type-vars ...)`)已不再支持,会直接报错。
668668

@@ -727,18 +727,18 @@ cr eval 'foldl (list 1 2 3) 0 &+'
727727
let
728728
; 可选参数
729729
greet $ fn (name)
730-
assert-type name $ :: :optional :string
730+
hint-fn $ {} (:args $ [] (:: :optional :string)) (:return :string)
731731
str "|Hello " (or name "|Guest")
732732
733733
; 变长参数
734734
sum $ fn (& xs)
735-
assert-type xs $ :: :& :number
735+
hint-fn $ {} (:rest :number) (:return :number)
736736
reduce xs 0 &+
737737
738738
; Record 约束 (使用 defstruct 定义结构体)
739739
User $ defstruct User (:name :string)
740740
get-name $ fn (u)
741-
assert-type u User
741+
hint-fn $ {} (:args $ [] (:: :record User)) (:return :string)
742742
get u :name
743743
println $ greet |Alice
744744
println $ sum 1 2 3
@@ -794,6 +794,8 @@ let
794794
- `:return :string` 对应整个 `join-str` 的返回值
795795
- 内部辅助函数 `%join-str` 不是顶层定义,所以继续用 `hint-fn`
796796

797+
可以简单记忆为:**namespace 上的定义看 `:schema`,函数体内部的辅助函数看 `hint-fn`**
798+
797799
推荐工作流:
798800

799801
```bash
@@ -1456,20 +1458,20 @@ cr eval 'let ((x 1)) (+ x 2)'
14561458

14571459
### 错误信息对照表
14581460

1459-
| 错误信息 | 原因 | 解决方法 |
1460-
| ------------------------------------------------ | ------------------------------------------------- | -------------------------------------------------------------- |
1461-
| `Path index X out of bounds` | 路径索引已过期(操作后变化) | 重新运行 `cr query search` 获取最新路径 |
1462-
| `tag-match expected tuple` | 传入 vector 而非 tuple | 改用 `::` 语法,如 `:: :event-name data` |
1463-
| `unknown symbol: xxx` | 符号未定义或未 import | `cr query find xxx` 确认位置,`cr edit add-import` 引入 |
1464-
| `expects pairs in list for let` | `let` 绑定语法错误 | 改为 `let ((x val)) body`(双层括号) |
1465-
| `cannot be used as operator` | 末尾符号被当作函数调用 | 改用 `, acc` 前缀传递值,或用函数包裹 |
1466-
| `unknown data for foldl-shortcut` | 参数顺序错误(Calcit vs Clojure 差异) | Calcit 集合在第一位:`map data fn` |
1467-
| `Do not include ':require' as prefix` | `cr edit imports` 格式错误 | 去掉 `:require` 前缀,直接传 `src-ns :refer $ sym` |
1468-
| `Namespace name mismatch` | `add-ns -e` 名称不一致 | ns 表达式名称必须与位置参数完全一致 |
1469-
| 字符串被拆分成多个 token | 没有用 `\|``"` 包裹 | 使用 `\|complete string``"complete string` |
1470-
| `unexpected format` | Cirru 语法错误 |`cr cirru parse '<code>'` 验证语法 |
1471-
| `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 检查参数类型标注,或用 `assert-type` 确认预期类型 |
1472-
| `schema mismatch while preprocessing definition` | `:schema``defn` / `defmacro` / 参数个数不一致 | 修正 `:kind``:args``:rest`,或让代码定义与 schema 保持一致 |
1461+
| 错误信息 | 原因 | 解决方法 |
1462+
| ------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------ |
1463+
| `Path index X out of bounds` | 路径索引已过期(操作后变化) | 重新运行 `cr query search` 获取最新路径 |
1464+
| `tag-match expected tuple` | 传入 vector 而非 tuple | 改用 `::` 语法,如 `:: :event-name data` |
1465+
| `unknown symbol: xxx` | 符号未定义或未 import | `cr query find xxx` 确认位置,`cr edit add-import` 引入 |
1466+
| `expects pairs in list for let` | `let` 绑定语法错误 | 改为 `let ((x val)) body`(双层括号) |
1467+
| `cannot be used as operator` | 末尾符号被当作函数调用 | 改用 `, acc` 前缀传递值,或用函数包裹 |
1468+
| `unknown data for foldl-shortcut` | 参数顺序错误(Calcit vs Clojure 差异) | Calcit 集合在第一位:`map data fn` |
1469+
| `Do not include ':require' as prefix` | `cr edit imports` 格式错误 | 去掉 `:require` 前缀,直接传 `src-ns :refer $ sym` |
1470+
| `Namespace name mismatch` | `add-ns -e` 名称不一致 | ns 表达式名称必须与位置参数完全一致 |
1471+
| 字符串被拆分成多个 token | 没有用 `\|``"` 包裹 | 使用 `\|complete string``"complete string` |
1472+
| `unexpected format` | Cirru 语法错误 |`cr cirru parse '<code>'` 验证语法 |
1473+
| `Type warning` 导致 eval 失败 | 类型不匹配(阻断执行) | 优先检查 `:schema` / `hint-fn` 的参数标注;局部值再用 `assert-type` 复核 |
1474+
| `schema mismatch while preprocessing definition` | `:schema``defn` / `defmacro` / 参数个数不一致 | 修正 `:kind``:args``:rest`,或让代码定义与 schema 保持一致 |
14731475

14741476
### 调试常用命令
14751477

editing-history/2026-0313-2216-validate-struct-type-arg-arity.md

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bin/cr_tests/type_fail.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ fn type_fail_schema_mismatch_fixtures_report_error_code() {
8787
}
8888
}
8989

90+
9091
#[test]
9192
fn type_fail_call_arg_fixture_reports_warning_code() {
9293
let _guard = lock_fixture_tests();

src/builtins/records.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,14 @@ pub fn new_struct(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
190190
}
191191
};
192192
let field_type = CalcitTypeAnnotation::parse_type_annotation_form_with_generics(type_expr, generics.as_slice());
193+
if let Err(e) = field_type.validate_applied_type_args() {
194+
let hint = format_proc_examples_hint(&CalcitProc::NativeStructNew).unwrap_or_default();
195+
return CalcitErr::err_str_with_hint(
196+
CalcitErrKind::Type,
197+
format!("&struct::new field `{field_name}` has invalid type annotation: {e}"),
198+
hint,
199+
);
200+
}
193201
fields.push((field_name, field_type));
194202
}
195203
(Some(_), None, _) => {

src/calcit/sum_type.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,11 @@ impl CalcitEnum {
137137
Calcit::List(items) => {
138138
let mut payloads: Vec<Arc<CalcitTypeAnnotation>> = Vec::with_capacity(items.len());
139139
for item in items.iter() {
140-
payloads.push(CalcitTypeAnnotation::parse_type_annotation_form(item));
140+
let parsed = CalcitTypeAnnotation::parse_type_annotation_form(item);
141+
parsed
142+
.validate_applied_type_args()
143+
.map_err(|e| format!("enum variant `{tag}` has invalid payload type annotation: {e}"))?;
144+
payloads.push(parsed);
141145
}
142146
Ok(payloads)
143147
}
@@ -152,7 +156,7 @@ impl CalcitEnum {
152156
#[cfg(test)]
153157
mod tests {
154158
use super::*;
155-
use crate::calcit::{CalcitList, CalcitStruct, CalcitTypeAnnotation};
159+
use crate::calcit::{CalcitList, CalcitStruct, CalcitTuple, CalcitTypeAnnotation};
156160

157161
fn empty_list() -> Calcit {
158162
Calcit::List(Arc::new(CalcitList::Vector(vec![])))
@@ -186,4 +190,25 @@ mod tests {
186190
}
187191
assert_eq!(enum_proto.find_variant_by_name("ok").unwrap().arity(), 0);
188192
}
193+
194+
#[test]
195+
fn rejects_non_generic_struct_type_args_in_payloads() {
196+
let pair = CalcitStruct::from_fields(EdnTag::new("Pair"), vec![EdnTag::new("left"), EdnTag::new("right")]);
197+
let applied_pair = Calcit::Tuple(CalcitTuple {
198+
tag: Arc::new(Calcit::Struct(pair)),
199+
extra: vec![Calcit::tag("number"), Calcit::tag("string")],
200+
sum_type: None,
201+
});
202+
let record = CalcitRecord {
203+
struct_ref: Arc::new(CalcitStruct::from_fields(EdnTag::new("Wrapped"), vec![EdnTag::new("pair")])),
204+
values: Arc::new(vec![list_from(vec![applied_pair])]),
205+
};
206+
207+
let err = CalcitEnum::from_record(record).expect_err("non-generic struct should reject type args in enum payloads");
208+
assert!(
209+
err.contains("enum variant `pair` has invalid payload type annotation")
210+
&& err.contains("struct `Pair` is not generic but received 2 type argument(s)"),
211+
"unexpected error: {err}"
212+
);
213+
}
189214
}

src/calcit/type_annotation.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,76 @@ pub enum CalcitTypeAnnotation {
143143
}
144144

145145
impl CalcitTypeAnnotation {
146+
pub(crate) fn validate_applied_type_args(&self) -> Result<(), String> {
147+
match self {
148+
Self::List(inner) | Self::Set(inner) | Self::Ref(inner) | Self::Variadic(inner) | Self::Optional(inner) => {
149+
inner.validate_applied_type_args()
150+
}
151+
Self::Map(key, value) => {
152+
key.validate_applied_type_args()?;
153+
value.validate_applied_type_args()
154+
}
155+
Self::Fn(signature) => signature.validate_applied_type_args(),
156+
Self::Struct(base, args) => {
157+
for arg in args.iter() {
158+
arg.validate_applied_type_args()?;
159+
}
160+
161+
let expected = base.generics.len();
162+
let actual = args.len();
163+
if expected == 0 {
164+
if actual > 0 {
165+
return Err(format!(
166+
"struct `{}` is not generic but received {actual} type argument(s)",
167+
base.name
168+
));
169+
}
170+
} else if actual != expected {
171+
return Err(format!(
172+
"struct `{}` expects {expected} type argument(s), but received {actual}",
173+
base.name
174+
));
175+
}
176+
177+
Ok(())
178+
}
179+
Self::Enum(enum_def, args) => {
180+
for arg in args.iter() {
181+
arg.validate_applied_type_args()?;
182+
}
183+
184+
if !args.is_empty() {
185+
return Err(format!(
186+
"enum `{}` is not generic but received {} type argument(s)",
187+
enum_def.name(),
188+
args.len()
189+
));
190+
}
191+
192+
Ok(())
193+
}
194+
Self::TypeRef(_, args) => {
195+
for arg in args.iter() {
196+
arg.validate_applied_type_args()?;
197+
}
198+
Ok(())
199+
}
200+
Self::Record(_) | Self::Tuple(_) | Self::Trait(_) | Self::TraitSet(_) | Self::Custom(_) => Ok(()),
201+
Self::Bool
202+
| Self::Number
203+
| Self::String
204+
| Self::Symbol
205+
| Self::Tag
206+
| Self::DynTuple
207+
| Self::DynFn
208+
| Self::Buffer
209+
| Self::CirruQuote
210+
| Self::Dynamic
211+
| Self::TypeVar(_)
212+
| Self::Unit => Ok(()),
213+
}
214+
}
215+
146216
fn custom_keyword_matches(custom: &Calcit, keyword: &str) -> bool {
147217
match custom {
148218
Calcit::Tag(tag) => tag.ref_str().trim_start_matches(':') == keyword,
@@ -2545,6 +2615,46 @@ mod tests {
25452615
CalcitTypeAnnotation::Dynamic
25462616
));
25472617
}
2618+
2619+
#[test]
2620+
fn rejects_type_args_on_non_generic_struct_annotation() {
2621+
let pair = CalcitStruct {
2622+
name: EdnTag::new("Pair"),
2623+
fields: Arc::new(vec![]),
2624+
field_types: Arc::new(vec![]),
2625+
generics: Arc::new(vec![]),
2626+
impls: vec![],
2627+
};
2628+
let annotation = CalcitTypeAnnotation::Struct(
2629+
Arc::new(pair),
2630+
Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number), Arc::new(CalcitTypeAnnotation::String)]),
2631+
);
2632+
2633+
let err = annotation
2634+
.validate_applied_type_args()
2635+
.expect_err("non-generic struct should reject type args");
2636+
assert!(err.contains("struct `Pair` is not generic"), "unexpected error: {err}");
2637+
}
2638+
2639+
#[test]
2640+
fn rejects_wrong_arity_on_generic_struct_annotation() {
2641+
let pair = CalcitStruct {
2642+
name: EdnTag::new("Pair"),
2643+
fields: Arc::new(vec![]),
2644+
field_types: Arc::new(vec![]),
2645+
generics: Arc::new(vec![Arc::from("A"), Arc::from("B")]),
2646+
impls: vec![],
2647+
};
2648+
let annotation = CalcitTypeAnnotation::Struct(Arc::new(pair), Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)]));
2649+
2650+
let err = annotation
2651+
.validate_applied_type_args()
2652+
.expect_err("generic struct should enforce arity");
2653+
assert!(
2654+
err.contains("expects 2 type argument(s), but received 1"),
2655+
"unexpected error: {err}"
2656+
);
2657+
}
25482658
}
25492659

25502660
impl fmt::Display for CalcitTypeAnnotation {
@@ -2715,6 +2825,17 @@ pub struct CalcitFnTypeAnnotation {
27152825
}
27162826

27172827
impl CalcitFnTypeAnnotation {
2828+
pub(crate) fn validate_applied_type_args(&self) -> Result<(), String> {
2829+
for arg in &self.arg_types {
2830+
arg.validate_applied_type_args()?;
2831+
}
2832+
self.return_type.validate_applied_type_args()?;
2833+
if let Some(rest) = &self.rest_type {
2834+
rest.validate_applied_type_args()?;
2835+
}
2836+
Ok(())
2837+
}
2838+
27182839
fn to_inline_type_schema_edn(&self) -> Edn {
27192840
let args: Vec<Edn> = self.arg_types.iter().map(|t| t.to_type_edn()).collect();
27202841
let mut map = EdnMapView::default();

0 commit comments

Comments
 (0)