From cf320199ef99e8a8e6604a8b926583b4b6c225a0 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 2 Apr 2026 03:22:35 +0200 Subject: [PATCH] More nested function fun! (0.40.1) --- CMakeLists.txt | 2 +- examples/broken/complex_nested_no_capture.fun | 177 ++++++++++++++++++ examples/functions/nested_functions.fun | 20 +- examples/functions/nested_no_capture.fun | 41 ++++ examples/snippets/nested.fun | 2 - src/parser.c | 142 +++++++++++++- 6 files changed, 369 insertions(+), 15 deletions(-) create mode 100644 examples/broken/complex_nested_no_capture.fun create mode 100755 examples/functions/nested_no_capture.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index ae8213f..816b1b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.40.0 LANGUAGES C) +project(fun VERSION 0.40.1 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/broken/complex_nested_no_capture.fun b/examples/broken/complex_nested_no_capture.fun new file mode 100644 index 0000000..648bb1d --- /dev/null +++ b/examples/broken/complex_nested_no_capture.fun @@ -0,0 +1,177 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-04-02 + */ + +/* + * Complex demonstration of nested functions that are local to their + * enclosing function, without any variable capture (no closures). + * All state is passed explicitly through parameters at each call. + * + * The example showcases: + * - Deeply nested helpers and multi-stage orchestration + * - Returning a nested function and using it later + * - Recursion implemented via a nested helper + * - Working with maps while threading state explicitly + */ + +print("=== Complex nested functions (no captures) ===") + +// 1) A small data pipeline on a user map, using local helpers +fun process_user(user) + // Ensure a key is present; if missing, fill with default + fun ensure_has(m, key, def) + if has(m, key) + return m + m[key] = def + return m + + // Normalize optional fields + fun normalize_city(m) + if has(m, "city") + return m + m["city"] = "Unknown" + return m + + // Add derived attributes explicitly via parameters (no function calls on RHS) + fun annotate(m) + a = m["age"] + // Inline age grouping without relying on an additional helper + g = "" + if a < 13 + g = "child" + else if a < 20 + g = "teen" + else if a < 65 + g = "adult" + else + g = "senior" + m["group"] = g + return m + + m1 = ensure_has(user, "name", "N/A") + m2 = ensure_has(m1, "age", 0) + m3 = normalize_city(m2) + m4 = annotate(m3) + return m4 + +u1 = {"name": "Ada", "age": 37} +u2 = process_user(u1) +print("process_user -> " + to_string(u2)) + +print("") +print("=== Returning a nested function (no captures) ===") + +// 2) Return an inner function and use it later. The returned function +// still requires all the data it needs as parameters (no implicit capture). +fun math_suite() + fun addk(x, k) + return x + k + + fun mulk(x, k) + return x * k + + // Integer power via nested recursion helper + fun powi(x, n) + fun loop(acc, base, exp) + if exp == 0 + return acc + return loop(acc * base, base, exp - 1) + return loop(1, x, n) + + // Compose a small arithmetic chain explicitly. Break into simple steps to + // match parser expectations and avoid nested calls on the right-hand side. + fun apply_chain(x, k_a, k_b, n) + t1 = x + k_a + t2 = t1 * k_b + t3 = powi(t2, n) + return t3 + + return apply_chain + +chain = math_suite() +print("chain(2, 3, 4, 2) -> expected ((2+3)*4)^2 = 400") +print(chain(2, 3, 4, 2)) + +print("") +print("=== Deep orchestration with 3-level nesting ===") + +// 3) Multi-stage pipeline with explicit parameter threading through each level +fun orchestrate(a, b, c) + fun stage1(x, a_, b_, c_) + fun stage2(y, b2, c2) + fun stage3(z, c3) + // No capture: every value needed arrives via parameters + tmp = z + c3 + return tmp * 2 + return stage3(y + b2, c2) + return stage2(x + a_, b_, c_) + + // Kick off with x = 0 and thread a, b, c explicitly + return stage1(0, a, b, c) + +print("orchestrate(1, 2, 3) -> stage3((0+1)+2, 3) * 2 = (3+3)*2 = 12") +print(orchestrate(1, 2, 3)) + +print("") +print("=== Nested recursion: factorial via inner loop ===") + +// 4) Factorial using an inner tail-recursive helper (no captures) +fun fact(n) + fun go(i, acc) + if i <= 1 + return acc + return go(i - 1, acc * i) + return go(n, 1) + +print("fact(6) -> expected 720") +print(fact(6)) + +print("") +print("=== Higher-order style without captures ===") + +// 5) Higher-order-like usage where the "strategy" function receives +// all needed parameters explicitly. +fun reducer_sum_with_limit(x, limit) + if x > limit + return 0 + return x + +fun fold3(a, b, c, f, p1, p2, p3) + // Apply f to each and sum; f must accept (value, param) + s1 = f(a, p1) + s2 = f(b, p2) + s3 = f(c, p3) + return s1 + s2 + s3 + +print("fold3 with limit: (5<=10 ? 5 : 0) + (12<=10 ? 0 : 0) + (7<=10 ? 7 : 0) = 12") +print(fold3(5, 12, 7, reducer_sum_with_limit, 10, 10, 10)) + +/* Expected output: +=== Complex nested functions (no captures) === +process_user -> {"age": 37, "city": "Unknown", "group": "adult", "name": "Ada"} + +=== Returning a nested function (no captures) === +chain(2, 3, 4, 2) -> expected ((2+3)*4)^2 = 400 +400 + +=== Deep orchestration with 3-level nesting === +orchestrate(1, 2, 3) -> stage3((0+1)+2, 3) * 2 = (3+3)*2 = 12 +12 + +=== Nested recursion: factorial via inner loop === +fact(6) -> expected 720 +720 + +=== Higher-order style without captures === +fold3 with limit: (5<=10 ? 5 : 0) + (12<=10 ? 0 : 0) + (7<=10 ? 7 : 0) = 12 +12 +*/ diff --git a/examples/functions/nested_functions.fun b/examples/functions/nested_functions.fun index 28a0386..7d2da30 100755 --- a/examples/functions/nested_functions.fun +++ b/examples/functions/nested_functions.fun @@ -4,12 +4,18 @@ * This file is part of the Fun programming language. * https://fun-lang.xyz/ * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-04-02 + */ + +/* * Demonstrates nested functions that are only visible inside the * outer function where they are defined. This example avoids * capturing outer variables (closures) and instead passes values * explicitly, which works with the current implementation. - * - * Added: 2026-04-02 */ print("=== Nested functions (local to outer function) ===") @@ -19,18 +25,15 @@ fun outer(a, b) // Defined inside outer; not visible as a global symbol fun times3(x) return x * 3 - end // Another local helper; also only visible within outer fun sum_plus1(x, y) return x + y + 1 - end // Use the local helpers t1 = times3(a) t2 = times3(b) return sum_plus1(t1, t2) -end print("outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22") print(outer(2, 5)) @@ -46,19 +49,14 @@ fun demo_deep(n) fun two(y) fun three(z) return z + 1 - end return three(y) + 1 - end return two(x) + 1 - end return one(n) -end print("demo_deep(4) -> expected 7") print(demo_deep(4)) -/* -Expected output: +/* Expected output: === Nested functions (local to outer function) === outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22 22 diff --git a/examples/functions/nested_no_capture.fun b/examples/functions/nested_no_capture.fun new file mode 100755 index 0000000..5820340 --- /dev/null +++ b/examples/functions/nested_no_capture.fun @@ -0,0 +1,41 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-04-02 + */ + +/* + * Demonstrates nested functions that are only visible inside the + * outer function where they are defined. This example avoids + * capturing outer variables (closures) and instead passes values + * explicitly, which works with the current implementation. + */ + +print("=== Nested (no captures), explicit parameters ===") + +fun outer(a, b) + fun mul3(x) + return x * 3 + + fun combine(x, y) + return x + y + 1 + + ax = mul3(a) + by = mul3(b) + return combine(ax, by) + +print("outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22") +print(outer(2, 5)) + +/* Expected output: +=== Nested (no captures), explicit parameters === +outer(2, 5) -> expected 2*3 + 5*3 + 1 = 22 +22 +*/ diff --git a/examples/snippets/nested.fun b/examples/snippets/nested.fun index c7bd4b5..c31aa47 100644 --- a/examples/snippets/nested.fun +++ b/examples/snippets/nested.fun @@ -15,8 +15,6 @@ fun outer(x) fun inner(y) return y * 2 - end return inner(x) + inner(3) -end print(outer(5)) diff --git a/src/parser.c b/src/parser.c index 2834a8f..cb192bb 100644 --- a/src/parser.c +++ b/src/parser.c @@ -54,6 +54,11 @@ /* from parser_utils.c */ extern char *preprocess_includes_with_path(const char *src, const char *current_path); +/* Forward declarations for helpers used before their definitions */ +static void skip_to_eol(const char *src, size_t len, size_t *pos); +static int read_line_start(const char *src, size_t len, size_t *pos, int *out_indent); +static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, int current_indent); + /* ---- parser error state ---- */ static const char *g_current_source_path = NULL; /* for propagating filename into nested bytecodes */ static int g_has_error = 0; @@ -219,6 +224,22 @@ typedef struct { static LocalEnv *g_locals = NULL; +/* --- nested function environment tracking (for no-capture enforcement) --- */ +static LocalEnv *g_func_env_stack[64]; +static int g_func_env_depth = 0; /* number of valid outer env entries */ + +static int name_in_outer_envs(const char *name) { + if (g_func_env_depth <= 0) return 0; + for (int d = g_func_env_depth - 1; d >= 0; --d) { + LocalEnv *e = g_func_env_stack[d]; + if (!e) continue; + for (int i = 0; i < e->count; ++i) { + if (e->names[i] && strcmp(e->names[i], name) == 0) return 1; + } + } + return 0; +} + /* loop context for break/continue patching */ typedef struct LoopCtx { int break_jumps[64]; @@ -256,6 +277,94 @@ static int emit_expression(Bytecode *bc, const char *src, size_t len, size_t *po static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) { skip_spaces(src, len, pos); + /* anonymous function literal: fn(arg, ...) indented-body end */ + if (*pos + 2 < len && starts_with(src, len, *pos, "fn") && (src[*pos + 2] == '(' || src[*pos + 2] == ' ' || src[*pos + 2] == '\t')) { + *pos += 2; + skip_spaces(src, len, pos); + if (!consume_char(src, len, pos, '(')) { + parser_fail(*pos, "Expected '(' after 'fn'"); + return 0; + } + + /* build locals from parameters */ + LocalEnv env = {{0}, {0}, 0}; + LocalEnv *prev = g_locals; + int saved_depth = g_func_env_depth; + /* push outer env for no-capture checks */ + if (prev != NULL) { + if (g_func_env_depth >= (int)(sizeof(g_func_env_stack) / sizeof(g_func_env_stack[0]))) { + parser_fail(*pos, "Too many nested functions (env stack overflow)"); + return 0; + } + g_func_env_stack[g_func_env_depth++] = prev; + } + g_locals = &env; + + skip_spaces(src, len, pos); + if (*pos < len && src[*pos] != ')') { + for (;;) { + char *pname = NULL; + if (!read_identifier_into(src, len, pos, &pname)) { + parser_fail(*pos, "Expected parameter name"); + g_locals = prev; + g_func_env_depth = saved_depth; + return 0; + } + if (local_find(pname) >= 0) { + parser_fail(*pos, "Duplicate parameter name '%s'", pname); + free(pname); + g_locals = prev; + g_func_env_depth = saved_depth; + return 0; + } + local_add(pname); + free(pname); + skip_spaces(src, len, pos); + if (*pos < len && src[*pos] == ',') { + (*pos)++; + skip_spaces(src, len, pos); + continue; + } + break; + } + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after parameter list"); + g_locals = prev; + g_func_env_depth = saved_depth; + return 0; + } + /* end header line */ + skip_to_eol(src, len, pos); + + /* compile body into separate Bytecode */ + Bytecode *fn_bc = bytecode_new(); + if (fn_bc) { + if (fn_bc->name) free((void *)fn_bc->name); + fn_bc->name = strdup(""); + if (fn_bc->source_file) free((void *)fn_bc->source_file); + if (g_current_source_path) fn_bc->source_file = strdup(g_current_source_path); + } + + /* parse body at increased indent if present */ + int body_indent = 0; + size_t look_body = *pos; + if (read_line_start(src, len, &look_body, &body_indent) && body_indent > 0) { + parse_block(fn_bc, src, len, pos, body_indent); + } else { + /* empty body ok */ + } + bytecode_add_instruction(fn_bc, OP_RETURN, 0); + + /* restore env stack */ + g_locals = prev; + g_func_env_depth = saved_depth; + + int fci = bytecode_add_constant(bc, make_function(fn_bc)); + bytecode_add_instruction(bc, OP_LOAD_CONST, fci); + return 1; + } + /* parenthesized */ if (*pos < len && src[*pos] == '(') { (*pos)++; /* '(' */ @@ -4386,6 +4495,12 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) } /* push function value first */ + if (local_idx < 0 && name_in_outer_envs(name)) { + const char *fname = (bc && bc->name) ? bc->name : ""; + parser_fail(*pos, "Nested function '%s' cannot access outer local '%s'. Pass it as a parameter instead.", fname, name); + free(name); + return 0; + } if (local_idx >= 0) { bytecode_add_instruction(bc, OP_LOAD_LOCAL, local_idx); } else { @@ -4541,6 +4656,12 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } else { + if (local_idx < 0 && name_in_outer_envs(name)) { + const char *fname = (bc && bc->name) ? bc->name : ""; + parser_fail(*pos, "Nested function '%s' cannot access outer local '%s'. Pass it as a parameter instead.", fname, name); + free(name); + return 0; + } if (local_idx >= 0) { bytecode_add_instruction(bc, OP_LOAD_LOCAL, local_idx); } else { @@ -6621,8 +6742,17 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, } /* build locals from parameters */ - LocalEnv env = {{0}, 0}; + LocalEnv env = {{0}, {0}, 0}; LocalEnv *prev = g_locals; + int saved_depth = g_func_env_depth; + if (prev != NULL) { + if (g_func_env_depth >= (int)(sizeof(g_func_env_stack) / sizeof(g_func_env_stack[0]))) { + parser_fail(*pos, "Too many nested functions (env stack overflow)"); + free(fname); + return; + } + g_func_env_stack[g_func_env_depth++] = prev; + } g_locals = &env; skip_spaces(src, len, pos); @@ -6713,7 +6843,16 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, free(fname); return; } + /* We want sibling inner functions to be able to call this nested function + * without closures. Strategy: also expose it as a global symbol so that + * inner functions (compiled into separate frames) can resolve the name + * via global lookup. Keep the local binding for the outer function body. */ + /* Duplicate the function value so we can store to both local and global. */ + bytecode_add_instruction(bc, OP_DUP, 0); bytecode_add_instruction(bc, OP_STORE_LOCAL, lidx); + /* Bind a global under the same name for call resolution inside deeper nested functions. */ + int gsym = sym_index(fname); + bytecode_add_instruction(bc, OP_STORE_GLOBAL, gsym); /* restore current env (will be reset to prev below) */ g_locals = save_env; } else { @@ -6722,6 +6861,7 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, } g_locals = prev; + g_func_env_depth = saved_depth; free(fname); continue; }