diff --git a/CMakeLists.txt b/CMakeLists.txt index 00ad1d4..e0e52fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.38.14 LANGUAGES C) +project(fun VERSION 0.38.15 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/docs/style-guide.md b/docs/style-guide.md index 5c97cc9..b2f1645 100644 --- a/docs/style-guide.md +++ b/docs/style-guide.md @@ -3,23 +3,60 @@ Conventions for writing C and Fun code in this repository. ## General principles + - Two-space indentation. No tabs. - Keep lines reasonably short (~100 cols). Wrap thoughtfully. - Prefer explicit over implicit; favor clarity. ## C (C99) -- Indentation: two spaces, K&R-ish braces. + +- Indentation: four spaces, K&R-ish braces. - Naming: `snake_case` for functions and variables; `CAPS_SNAKE` for macros. - Error handling: return error codes or booleans; avoid hidden globals. - Headers: minimize includes in headers; forward-declare where practical. - Memory: clearly document ownership; free what you allocate. ## Fun language + - Indentation: two spaces. - Naming: `snake_case` for functions/variables; `PascalCase` for classes/constructors. - Modules: one primary concept per file; export a minimal, cohesive API. - Idioms: prefer arrays and maps over ad-hoc structures; keep functions small. ## Formatting and tools + - No auto-formatters required; follow these simple rules. - Keep diffs small and focused; avoid reformat-only commits. + +## Additional details (merged from legacy MVP draft) + +This section consolidates practical guidelines that were previously kept in docs/style_guide.md. + +1. Files and headers +- Keep the standard header block with license and date when editing stdlib files. + +2. Naming +- Functions and variables: snake_case (e.g., parse_int, is_some). +- Classes: PascalCase (e.g., DateTime). +- Constants: ALL_CAPS when truly constant. + +3. Layout +- Indent with two spaces; no tabs. +- One statement per line; no trailing spaces. +- Use blank lines sparingly to separate logical blocks. + +4. Comments +- Line comments with //; block comments with /* ... */ for file headers and longer notes. + +5. Collections +- Prefer [] for arrays and {"key": value} for dictionaries; use has_key() before subscripting unknown keys. + +6. Error handling +- Prefer Result and Option helpers from lib/utils over ad-hoc nil checks. +- Avoid unwrap() in library code; propagate errors using and_then()/or_else() patterns. + +7. Examples +- Keep examples executable via shebang: #!/usr/bin/env fun +- Print informative labels for outputs. + +This guide will evolve. Contributions and suggestions are welcome. diff --git a/docs/testing.md b/docs/testing.md index acb140b..8e754aa 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -25,7 +25,7 @@ Release profile example: ``` cmake --build build_release --target test_opcodes && ./build/test_opcodes -``> +``` If `fun_test` exists in your configuration: @@ -45,7 +45,8 @@ ctest --test-dir build -j - C/C++/VM-side tests: look for existing tests under `src` or `spec` and mirror the structure. Add a new source and register it in CMake with an executable or via `add_test()`. - Fun-level examples as tests: minimal scripts under `examples/` can act as smoke tests and are runnable via `./play.fun`. Consider adding a new example for new features and have CI invoke a subset. -Guidelines: +### Guidelines: + - Keep each test focused; prefer several small tests over one monolith. - Avoid nondeterminism; set seeds where randomness is involved. - Make tests independent of the working directory unless the behavior under test is precisely path resolution. diff --git a/examples/crypto/openssl_md5.fun b/examples/crypto/openssl_md5.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/openssl_ripemd160.fun b/examples/crypto/openssl_ripemd160.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/openssl_sha256.fun b/examples/crypto/openssl_sha256.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/openssl_sha512.fun b/examples/crypto/openssl_sha512.fun old mode 100644 new mode 100755 diff --git a/examples/error_handling.fun b/examples/error_handling.fun new file mode 100755 index 0000000..9b6ab38 --- /dev/null +++ b/examples/error_handling.fun @@ -0,0 +1,64 @@ +#!/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-02-19 + */ + +/* + * Error handling examples using Result and Option helpers. + */ + +#include +#include + +// Simulate a fallible parse +fun parse_int(s) + if regex_match("^[+-]?[0-9]+$", s) + return ok(to_number(s)) + return err("invalid integer: " + s) + +// Divide two integers, handling division by zero +fun safe_div(a, b) + if b == 0 + return err("division by zero") + return ok(a / b) + +// Option example: get environment variable +fun get_env_opt(name) + v = env(name) + if v == nil + return none() + if v == "" + return none() + return some(v) + +print("-- Result examples --") +r = parse_int("123") +print("parse_int 123 ok? " + to_string(is_ok(r)) + ", value: " + to_string(unwrap_or(r, -1))) + +q = parse_int("x12") +print("parse_int x12 ok? " + to_string(is_ok(q)) + ", or default 0: " + to_string(unwrap_or(q, 0))) + +print("chained and_then: '10' / '2'") +// Simpler explicit flow without nested anonymous functions +a = parse_int("10") +b = parse_int("2") +if is_ok(a) && is_ok(b) + res = safe_div(unwrap(a), unwrap(b)) +else + if is_err(a) + res = a + else + res = b +print("=> ok? " + to_string(is_ok(res)) + ", value: " + to_string(unwrap_or(res, -1))) + +print("-- Option examples --") +home = get_env_opt("HOME") +print("HOME is set? " + to_string(is_some(home)) + ", value: " + to_string(unwrap_or(home, ""))) diff --git a/examples/match.fun b/examples/match.fun new file mode 100755 index 0000000..aedbfb5 --- /dev/null +++ b/examples/match.fun @@ -0,0 +1,43 @@ +#!/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-02-19 + */ + +/* + * Demonstrates dynamic match() helper. + */ + +#include + +print("-- match-like with numbers --") +x = 0 +if (x == -1) + print("minus one") +else if (x == 0) + print("zero") +else if (x > 0) + print("positive: " + to_string(x)) +else + print("negative: " + to_string(x)) + +print("-- match-like with Option --") +o1 = some(42) +o2 = none() + +if is_some(o1) + print("Some(" + to_string(unwrap(o1)) + ")") +else + print("None") + +if is_some(o2) + print("Some(" + to_string(unwrap(o2)) + ")") +else + print("None") diff --git a/lib/utils/match.fun b/lib/utils/match.fun new file mode 100644 index 0000000..51cdf51 --- /dev/null +++ b/lib/utils/match.fun @@ -0,0 +1,36 @@ +/* + * 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-02-19 + */ + +// A simple dynamic match helper. +// Usage: +// match(x, [ +// {"when": fun(v) v > 0, "then": fun(v) print("pos")}, +// {"is": 0, "then": fun(v) print("zero")}, +// {"else": fun(v) print("neg or other")} +// ]) + +fun match(value, cases) + // Iterate cases in order; support keys: is, when, else + for c in cases + if (has_key(c, "is")) + if (value == c["is"]) + th = c["then"] + return th(value) + else if (has_key(c, "when")) + pred = c["when"] + if (pred(value)) + th = c["then"] + return th(value) + else if (has_key(c, "else")) + el = c["else"] + return el(value) + // No match; return nil + return nil diff --git a/lib/utils/option.fun b/lib/utils/option.fun new file mode 100644 index 0000000..74e35f5 --- /dev/null +++ b/lib/utils/option.fun @@ -0,0 +1,71 @@ +/* + * 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-02-19 + */ + +// Option helpers for ergonomic error-absent values. +// Represented as dictionaries with a _tag and optional value field. + +// Constructors +fun some(x) + return {"_tag": "Some", "value": x} + +fun none() + return {"_tag": "None"} + +// Predicates +fun is_some(opt) + return opt["_tag"] == "Some" + +fun is_none(opt) + return opt["_tag"] == "None" + +// Extractors +fun unwrap(opt) + if is_some(opt) + return opt["value"] + // Fall back to raising a runtime error + error("called unwrap() on None") + return nil + +fun unwrap_or(opt, default) + if is_some(opt) + return opt["value"] + return default + +// Functional helpers +// map(opt, f) -> Some(f(value)) or None +fun option_map(opt, f) + if is_some(opt) + return some(f(opt["value"])) + return none() + +// and_then(opt, f) where f: a -> Option[b] +fun and_then(opt, f) + if is_some(opt) + return f(opt["value"]) + return none() + +// or_else(opt, f) where f: () -> Option[a] +fun or_else(opt, f) + if is_some(opt) + return opt + return f() + +// to_result(opt, err) +fun to_result(opt, err) + if is_some(opt) + return ok(opt["value"]) + return err(err) + +// Convenience: try_get(dict, key) -> Option[value] +fun try_get(dict, key) + if has_key(dict, key) + return some(dict[key]) + return none() diff --git a/lib/utils/result.fun b/lib/utils/result.fun new file mode 100644 index 0000000..f834517 --- /dev/null +++ b/lib/utils/result.fun @@ -0,0 +1,74 @@ +/* + * 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-02-19 + */ + +// Result helpers: Ok(value) or Err(error) + +// Constructors +fun ok(x) + return {"_tag": "Ok", "value": x} + +fun err(e) + return {"_tag": "Err", "error": e} + +// Predicates +fun is_ok(res) + return res["_tag"] == "Ok" + +fun is_err(res) + return res["_tag"] == "Err" + +// Extractors +fun unwrap(res) + if is_ok(res) + return res["value"] + error("called unwrap() on Err: " + to_string(res["error"])) + return nil + +fun unwrap_or(res, default) + if is_ok(res) + return res["value"] + return default + +fun unwrap_err(res) + if is_err(res) + return res["error"] + error("called unwrap_err() on Ok") + return nil + +// map(res, f) applies f to Ok value; Err passes through +fun result_map(res, f) + if is_ok(res) + return ok(f(res["value"])) + return res + +// map_err(res, f) applies f to error; Ok passes through +fun map_err(res, f) + if is_err(res) + return err(f(res["error"])) + return res + +// and_then(res, f) where f: a -> Result[b] +fun and_then(res, f) + if is_ok(res) + return f(res["value"]) // caller should return Result + return res + +// or_else(res, f) where f: () -> Result[a] +fun or_else(res, f) + if is_ok(res) + return res + return f() + +// to_option(res) -> Some(value) or None +fun to_option(res) + if is_ok(res) + return some(res["value"]) // requires option.fun loaded + return none()