Skip to content

Commit aa1c3cb

Browse files
committed
Add function pointers to the ION_SPEC Eq bound table, fix method-call LSP signature help to include generic bounds, and add an integration test for Eq on function pointer types.
1 parent ed6272d commit aa1c3cb

6 files changed

Lines changed: 98 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 2026-07
44

55
- **VM-style idioms**: `match` on `&Enum` from `Vec::get_ref`; struct field assignment and `+=` on owned/`&mut` paths; method desugaring (`vec.push`, `vec.get_ref`) with correct borrows; nested generic types (`Vec<Vec<int>>`); match-arm control-flow unification (`break`/`return` with value arms); `&str` call-site coercion; enum literals in `Vec::push`/`set` without double-wrapped C; `&mut Struct` field access via `->` in codegen. Integration tests and examples `bytecode_vm`, updated `showcase`, `todo_demo`, `http_server`, `text_summary`. Fix extern call typing so `&T` arguments match `&T` parameters (no erroneous copy-type ref stripping). Fix `Option<T>` match codegen to use the scrutinee type instead of the first registered monomorph. Former negative match-arm rvalue tests now pass as positive runs.
6+
- **Trait bounds follow-up**: ION_SPEC §4.8 `Eq` row documents function pointers; integration test `test_trait_bound_eq_fn_ok`; method-call signature help threads generic bounds through `fn_hover_doc`.
67

78
## 2026-06
89

ION_SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ Syntax: `identifier : Bound [ + Bound ... ]` after each type parameter name. Bou
693693
| Bound | Meaning (structural) |
694694
|-------|----------------------|
695695
| `Copy` | Type is copied rather than moved at the ownership level (primitives, references, function pointers). |
696-
| `Eq` | Type supports `==` and `!=` with correct semantics (primitives, `String`, references, arrays and tuples of `Eq` types, structs and enums whose fields or payloads are all `Eq`). |
696+
| `Eq` | Type supports `==` and `!=` with correct semantics (primitives, `String`, references, function pointers, arrays and tuples of `Eq` types, structs and enums whose fields or payloads are all `Eq`). |
697697
| `Send` | Type may cross thread boundaries (Section 7.3). |
698698

699699
At each monomorphization site (generic call, struct or enum construction, type-alias substitution), the compiler substitutes concrete types for parameters and rejects any instantiation where a concrete type does not satisfy a declared bound. Unknown bound names are rejected at the declaration site.

src/tc/mod.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3697,7 +3697,10 @@ impl TypeChecker {
36973697
}
36983698

36993699
// Regular function call type checking
3700-
let (params, return_type_opt) = if desugared_call.callee.contains("::") {
3700+
let (params, return_type_opt, fn_type_params) = if desugared_call
3701+
.callee
3702+
.contains("::")
3703+
{
37013704
let parts: Vec<&str> = desugared_call.callee.split("::").collect();
37023705
if parts.len() != 2 {
37033706
return Err(TypeCheckError::Message(format!(
@@ -3733,12 +3736,20 @@ impl TypeChecker {
37333736
}
37343737
})?;
37353738

3736-
(func_decl.params.clone(), func_decl.return_type.clone())
3739+
(
3740+
func_decl.params.clone(),
3741+
func_decl.return_type.clone(),
3742+
func_decl.generics.clone(),
3743+
)
37373744
} else {
37383745
// Type method case - look up the function again
37393746
let func_decl =
37403747
self.lookup_method_function(&desugared_call.callee, &base_type_name)?;
3741-
(func_decl.params.clone(), func_decl.return_type.clone())
3748+
(
3749+
func_decl.params.clone(),
3750+
func_decl.return_type.clone(),
3751+
func_decl.generics.clone(),
3752+
)
37423753
}
37433754
} else {
37443755
// Simple function call - shouldn't happen for methods
@@ -3782,7 +3793,12 @@ impl TypeChecker {
37823793

37833794
self.record_signature(
37843795
method_call.span,
3785-
Self::fn_hover_doc(&desugared_call.callee, &[], &params, &return_type_opt),
3796+
Self::fn_hover_doc(
3797+
&desugared_call.callee,
3798+
&fn_type_params,
3799+
&params,
3800+
&return_type_opt,
3801+
),
37863802
None,
37873803
);
37883804

@@ -5415,6 +5431,63 @@ fn main() -> int {
54155431
);
54165432
}
54175433

5434+
#[test]
5435+
fn method_call_signature_includes_generic_bounds() {
5436+
let src = r#"
5437+
struct Pair {
5438+
first: int;
5439+
second: int;
5440+
}
5441+
5442+
fn bump(x: int) -> int {
5443+
return x + 1;
5444+
}
5445+
5446+
fn main() -> int {
5447+
let p: Pair = Pair { first: 0, second: 0 };
5448+
let f: fn(int) -> int = bump;
5449+
let g: fn(int) -> int = bump;
5450+
if p.same(f, g) {
5451+
return 0;
5452+
}
5453+
return 1;
5454+
}
5455+
"#;
5456+
let helper_src = r#"
5457+
fn same<T: Eq>(pair: Pair, a: T, b: T) -> bool {
5458+
return a == b;
5459+
}
5460+
"#;
5461+
let fn_ptr = Type::Fn {
5462+
params: vec![Type::Int],
5463+
return_type: Box::new(Type::Int),
5464+
};
5465+
let tokens = crate::lexer::Lexer::new(src).tokenize().unwrap();
5466+
let mut program = parser::Parser::new(tokens).parse().unwrap();
5467+
let helper_tokens = crate::lexer::Lexer::new(helper_src).tokenize().unwrap();
5468+
let helper = parser::Parser::new(helper_tokens).parse().unwrap();
5469+
let mut method_fn = helper.functions.into_iter().next().expect("helper fn");
5470+
method_fn.name = "Pair::same".to_string();
5471+
method_fn.params[1].ty = fn_ptr.clone();
5472+
method_fn.params[2].ty = fn_ptr;
5473+
program.functions.push(method_fn);
5474+
5475+
let mut checker = TypeChecker::new();
5476+
let (result, errors) = checker.check_program_collecting(&program);
5477+
assert!(errors.is_empty(), "{errors:?}");
5478+
let sig = result
5479+
.lsp_info
5480+
.signatures
5481+
.values()
5482+
.find(|s| s.label.contains("Pair::same"))
5483+
.expect("Pair::same method signature help");
5484+
assert!(
5485+
sig.label.contains("<T: Eq>"),
5486+
"expected generic bounds in signature help, got: {}",
5487+
sig.label
5488+
);
5489+
}
5490+
54185491
fn spans_overlap(a: Span, b: Span) -> bool {
54195492
a.line == b.line
54205493
&& a.column < b.column + (b.end - b.start)

tests/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ The test runner prints pass/fail counts when it finishes. Do not rely on hardcod
257257
- `test_trait_bound_send_ok.ion` - generic fn with `T: Send` accepts `int`
258258
- `test_trait_bound_copy_ok.ion` - generic fn with `T: Copy` accepts `int`
259259
- `test_trait_bound_eq_ok.ion` - generic fn with `T: Eq` compares ints
260+
- `test_trait_bound_eq_fn_ok.ion` - generic fn with `T: Eq` compares function pointers
260261
- `test_trait_bound_send_error.ion` - `&int` rejected for `T: Send`
261262
- `test_trait_bound_copy_error.ion` - `String` rejected for `T: Copy`
262263
- `test_trait_bound_eq_error.ion` - `Vec<int>` rejected for `T: Eq`

tests/test_expectations.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ test_str_from_literal.ion run 71
214214
test_trait_bound_send_ok.ion run 42
215215
test_trait_bound_copy_ok.ion run 7
216216
test_trait_bound_eq_ok.ion run 61
217+
test_trait_bound_eq_fn_ok.ion run 99
217218
test_trait_bound_send_error.ion error TraitBoundNotSatisfied
218219
test_trait_bound_copy_error.ion error TraitBoundNotSatisfied
219220
test_trait_bound_eq_error.ion error TraitBoundNotSatisfied
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Generic function with Eq bound: function pointers support ==
2+
fn add_one(x: int) -> int {
3+
return x + 1;
4+
}
5+
6+
fn same_fn<T: Eq>(a: T, b: T) -> bool {
7+
return a == b;
8+
}
9+
10+
fn main() -> int {
11+
let f: fn(int) -> int = add_one;
12+
let g: fn(int) -> int = add_one;
13+
if same_fn(f, g) {
14+
return 99;
15+
}
16+
return 1;
17+
}

0 commit comments

Comments
 (0)