Skip to content

Commit ba9122a

Browse files
committed
Add capture-free fn literals lowered to static C function pointers.
Reject outer-scope capture with ClosureCapture; update ION_SPEC, integration tests, showcase example, and project skills. Capturing closures remain deferred.
1 parent 47e8dc3 commit ba9122a

23 files changed

Lines changed: 709 additions & 118 deletions

.cursor/skills/adding-ion-features/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Critical checks to preserve. Patterns below match **CLI stderr** (`Type check er
8181
| `if`/`while` non-bool | bool condition errors |
8282
| Module visibility | `Cannot access non-public` |
8383
| `unsafe` for extern | `must be inside an unsafe block` |
84+
| Fn literal capture | `ClosureCapture` |
8485

8586
Add new `TypeCheckError` variants only when existing ones can't express the failure clearly.
8687

.cursor/skills/ion-integration-tests/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ The harness greps **compiler CLI stderr** for the pattern (not LSP diagnostic te
119119
| Non-public import | `Cannot access non-public` |
120120
| Extern without unsafe | `must be inside an unsafe block` |
121121
| Non-bool if | `bool.*if condition\|if condition.*bool` |
122+
| Fn literal capture | `ClosureCapture` |
122123

123124
### `test_error` PARTIAL pass trap
124125

.cursor/skills/ion-lang/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,5 @@ Read these when the task matches:
105105
- Multi-file mode: `--mode multi --output <name> <main.ion>` generates per-module `.c`/`.h`.
106106
- Stdlib lives in `stdlib/` (`io.ion`, `fmt.ion`, `fs.ion`, `result.ion`); imported module functions are emitted as `{alias}_{name}` in single-file merge mode (e.g. `io::print_int` -> `io_print_int`).
107107
- Integration tests: add `tests/test_*.ion` plus one row in `tests/test_expectations.tsv` (see `ion-integration-tests` skill). Run `cd tests && ./test_runner.sh` to verify.
108+
- Fn literals are capture-free only; references to outer bindings are rejected with `ClosureCapture`.
108109
- LSP parses the **open buffer** in memory (lexer → parser), then `load_imports` which **fully `parse_module`s imported files from disk** (per-import errors are published). Parser/tc/import changes may need LSP updates (`ion-lsp-vscode` skill).

.cursor/skills/writing-ion-code/SKILL.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ let result: int = recv(&mut rx_back_mut);
112112

113113
`spawn` captures owned values by move. `T` in `channel<T>()` must be `Send`.
114114

115+
**Fn literals (capture-free)**
116+
117+
```ion
118+
let f: fn(int) -> int = fn(x: int) -> int { return x + 5; };
119+
return f(7);
120+
```
121+
122+
Fn literals lower to static C functions and must not reference outer bindings (owned or by reference). Use named functions with extra parameters for customized behavior. See [tests/test_fn_literal_basic.ion](../../../tests/test_fn_literal_basic.ion) and [tests/test_fn_literal_callback.ion](../../../tests/test_fn_literal_callback.ion).
123+
115124
**Unsafe and FFI**
116125

117126
All `extern "C"` calls belong inside `unsafe { ... }`. See [tests/test_ffi_basic.ion](../../../tests/test_ffi_basic.ion).
@@ -147,7 +156,7 @@ For programs under `tests/`, follow the `ion-integration-tests` skill (`test_exp
147156

148157
These are **not** in Ion today. Check ION_SPEC.md section 10.2 before using anything similar:
149158

150-
- Closures, fn literals, or `impl` blocks in user code
159+
- Capturing closures (fn literals that reference outer variables), or `impl` blocks in user code
151160
- Traits, trait bounds, or `where` clauses on generics
152161
- Returning `&T` / `&mut T` or `Option<&T>` from functions
153162
- References in struct fields, enum payloads, or channels

ION_SPEC.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,10 @@ primary_expr = identifier
432432
| "(" , expr , ")"
433433
| struct_lit
434434
| enum_lit
435-
| array_lit ;
435+
| array_lit
436+
| fn_literal ;
437+
438+
fn_literal = "fn" , "(" , params? , ")" , [ "->" , type_expr ] , block ;
436439
437440
array_lit = "[" , ( expr_list | ( expr , ";" , int_lit ) ) , "]" ;
438441
expr_list = expr , { "," , expr } ;
@@ -612,7 +615,11 @@ The corresponding function type is:
612615
fn(T1, T2) -> R
613616
```
614617

615-
Function types are **first-class**: they may be stored in variables, passed as arguments, and returned. However, **function values may not capture references that would violate the no-escape rule** (see Section 5.4).
618+
Function types are **first-class**: they may be stored in variables, passed as arguments, and returned.
619+
620+
**Fn literals** (capture-free only): an expression `fn(params) [-> R] { ... }` has type `fn(T1, T2, ...) -> R` matching its signature. The body may reference only parameters and locals declared inside the literal; any use of a binding from an outer scope is a compile-time error (`ClosureCapture`). Fn literals lower to plain C function pointers (each site gets a unique `static` function); there is no environment payload and no heap allocation.
621+
622+
Named functions and capture-free fn literals may not capture references that would violate the no-escape rule (see Section 5.4). **Capturing closures** (literals that move owned state from outer scopes) are not implemented; use named functions with extra parameters, explicit context structs, or `spawn` for move-only thread capture.
616623

617624
#### 4.4 Type Inference
618625

@@ -778,11 +785,13 @@ fn main() {
778785
```ion
779786
fn make_printer(x: &int) -> fn() {
780787
return fn() {
781-
println("x = {}", x); // would capture x by reference
788+
println("x = {}", *x); // ERROR: ClosureCapture (cannot reference outer x)
782789
}; // ERROR: closure escapes with reference
783790
}
784791
```
785792

793+
Capturing closures are not implemented; the compiler rejects any outer binding referenced from a fn literal body.
794+
786795
**Example (valid – borrow within function):**
787796

788797
```ion
@@ -836,7 +845,7 @@ Uninitialized `Box`/`Vec`/`String` bindings are zero-initialized to `NULL` so dr
836845

837846
Ion does **not** perform implicit heap allocation for:
838847

839-
- Captured closures
848+
- Captured closures (not implemented; fn literals are capture-free and use static functions only)
840849
- Slices or views
841850
- Temporaries (beyond what is required for expression evaluation)
842851

@@ -1193,7 +1202,7 @@ Build with `cargo build --release --bin ion-lsp`. Rebuild after compiler or LSP
11931202
- Match guards on the same variant are lowered to a single `switch` case with sequential `if` checks
11941203
- LSP go-to-definition for built-in methods (`Vec::push`, `String::len`, etc.) has no target (signature hover only)
11951204
- LSP go-to-definition for type names in type annotations (no source spans on `Type` AST nodes)
1196-
- Function types: named functions only; no fn literals/closures, no generic `fn(T) -> R` type parameters, no method values as fn pointers
1205+
- Function types: capture-free fn literals implemented; no capturing closures, no generic `fn(T) -> R` type parameters, no method values as fn pointers
11971206
- Tuple values: no nested tuples, `==` on tuples, struct fields holding tuples, or generic `(T1, T2)` parameters
11981207

11991208
### 11. Future Work (Non-Normative)
@@ -1204,6 +1213,7 @@ The following features are **not planned** for the current compiler:
12041213
- Complex trait or typeclass systems.
12051214
- Macros and compile-time metaprogramming.
12061215
- Advanced iterator pipelines and zero-cost abstractions beyond the basics.
1216+
- Capturing closures (fn literals that move owned environment from outer scopes).
12071217

12081218
Any such addition must:
12091219

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ For `http_server.ion`, add `-Drecv_sys=recv -Dsend_sys=send` when linking.
193193
| [examples/hello_world_safe.ion](examples/hello_world_safe.ion) | stdlib `io` module |
194194
| [examples/spawn_channel.ion](examples/spawn_channel.ion) | `spawn` with cross-thread channels |
195195
| [examples/http_server.ion](examples/http_server.ion) | Sockets, FFI, concurrent clients via `spawn` |
196-
| [examples/showcase.ion](examples/showcase.ion) | Mixed language features: tuples, `+=`, `push_byte`, spawn/channels |
196+
| [examples/showcase.ion](examples/showcase.ion) | Mixed language features: tuples, `+=`, `push_byte`, spawn/channels, capture-free fn literals |
197197
| [examples/access_log.ion](examples/access_log.ion) | Log parsing, `loop`/`break`, match guards, spawn, channels, fmt/io |
198198
| [examples/minimal.ion](examples/minimal.ion) | Smallest valid program |
199199
| [examples/channel_worker.ion](examples/channel_worker.ion) | Channel worker: `spawn` sums jobs from a channel |

examples/showcase.c

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ typedef struct {
1212
ion_receiver_t rx;
1313
ion_sender_t tx_back;
1414
} ion_spawn_ctx_0;
15+
static int ion_fn_lit_0(int x);
1516

1617
typedef struct Vec_int {
1718
void* data;
@@ -118,6 +119,7 @@ int reference_example(void);
118119
int generic_example(void);
119120
int tuple_example(void);
120121
int spawn_channel_example(void);
122+
int fn_literal_example(void);
121123
int complex_example(void);
122124
int main(void);
123125
int get_first_int(Pair_int pair);
@@ -494,6 +496,19 @@ int spawn_channel_example(void) {
494496
return ret_val;
495497
}
496498

499+
int fn_literal_example(void) {
500+
int ret_val = 0;
501+
int (*twice)(int) = ion_fn_lit_0;
502+
if (twice(21) != 42) {
503+
ret_val = 1;
504+
goto epilogue;
505+
}
506+
ret_val = 0;
507+
goto epilogue;
508+
epilogue:
509+
return ret_val;
510+
}
511+
497512
int complex_example(void) {
498513
int ret_val = 0;
499514
Vec_int* numbers = ((Vec_int*)(ion_vec_new(sizeof(int))));
@@ -595,10 +610,14 @@ int main(void) {
595610
ret_val = 9;
596611
goto epilogue;
597612
}
598-
if (complex_example() != 0) {
613+
if (fn_literal_example() != 0) {
599614
ret_val = 10;
600615
goto epilogue;
601616
}
617+
if (complex_example() != 0) {
618+
ret_val = 11;
619+
goto epilogue;
620+
}
602621
ret_val = 0;
603622
goto epilogue;
604623
epilogue:
@@ -630,3 +649,11 @@ static void* ion_spawn_entry_0(void* arg) {
630649
return NULL;
631650
}
632651

652+
static int ion_fn_lit_0(int x) {
653+
int ret_val = 0;
654+
ret_val = (x * 2);
655+
goto ion_fn_lit_0_epilogue;
656+
ion_fn_lit_0_epilogue:
657+
return ret_val;
658+
}
659+

examples/showcase.ion

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,20 @@ fn spawn_channel_example() -> int {
332332
}
333333

334334
// ============================================
335-
// 13. COMPLEX EXAMPLE - Combining features
335+
// 13. FN LITERALS - Capture-free function pointers
336+
// ============================================
337+
fn fn_literal_example() -> int {
338+
let twice: fn(int) -> int = fn(x: int) -> int {
339+
return x * 2;
340+
};
341+
if twice(21) != 42 {
342+
return 1;
343+
}
344+
return 0;
345+
}
346+
347+
// ============================================
348+
// 14. COMPLEX EXAMPLE - Combining features
336349
// ============================================
337350
fn complex_example() -> int {
338351
let mut numbers: Vec<int> = Vec::new();
@@ -426,9 +439,13 @@ fn main() -> int {
426439
return 9;
427440
}
428441

429-
if complex_example() != 0 {
442+
if fn_literal_example() != 0 {
430443
return 10;
431444
}
432445

446+
if complex_example() != 0 {
447+
return 11;
448+
}
449+
433450
return 0;
434451
}

src/ast/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,15 @@ pub enum Expr {
276276
Index(IndexExpr),
277277
Cast(CastExpr),
278278
Assign(AssignExpr),
279+
FnLiteral(FnLiteralExpr),
280+
}
281+
282+
#[derive(Debug, Clone)]
283+
pub struct FnLiteralExpr {
284+
pub params: Vec<Param>,
285+
pub return_type: Option<Type>,
286+
pub body: Block,
287+
pub span: Span,
279288
}
280289

281290
#[derive(Debug, Clone)]

0 commit comments

Comments
 (0)