From 25bad3cb8b3d04efd1922dce1c42b1f8acb30453 Mon Sep 17 00:00:00 2001 From: sanjin Date: Fri, 22 May 2026 12:53:11 +0200 Subject: [PATCH] Add stdlib composition example --- .gitignore | 1 + .llm/BETA_3_STDLIB_STABILIZATION.md | 31 ++++ README.md | 7 + .../tests/standard_stdlib_composition_beta.rs | 167 ++++++++++++++++++ docs/POST_BETA_ROADMAP.md | 6 + docs/language/examples/README.md | 7 +- .../projects/stdlib-composition/slovo.toml | 4 + .../projects/stdlib-composition/src/main.slo | 74 ++++++++ 8 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 .llm/BETA_3_STDLIB_STABILIZATION.md create mode 100644 compiler/tests/standard_stdlib_composition_beta.rs create mode 100644 examples/projects/stdlib-composition/slovo.toml create mode 100644 examples/projects/stdlib-composition/src/main.slo diff --git a/.gitignore b/.gitignore index 79cf6c0..31f571b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ compiler/target/ # Benchmark and local runtime artifacts benchmarks/**/build/ compiler/glagol-std-layout-local-fs-alpha.txt +**/.slovo/ __pycache__/ *.pyc diff --git a/.llm/BETA_3_STDLIB_STABILIZATION.md b/.llm/BETA_3_STDLIB_STABILIZATION.md new file mode 100644 index 0000000..7e550d1 --- /dev/null +++ b/.llm/BETA_3_STDLIB_STABILIZATION.md @@ -0,0 +1,31 @@ +# Beta 3 Standard Library Stabilization + +## Scope + +This post-`1.0.0-beta.2` slice stabilizes the existing standard-library surface +before adding new large API families. + +## Current Work + +- Generate `docs/language/STDLIB_API.md` from `lib/std/*.slo`. +- Gate the generated catalog in `scripts/release-gate.sh`. +- Add `examples/projects/stdlib-composition` as a realistic multi-module + command-line project. +- Keep imports explicit and beta-scoped; do not claim stable std APIs, stable + ABI, automatic prelude imports, richer host error codes, or general resource + ownership semantics. + +## Acceptance Gates + +- `./scripts/render-stdlib-api-doc.sh` +- `git diff --check` +- `cargo test --test standard_stdlib_composition_beta` +- `./scripts/release-gate.sh` + +## Next Decisions + +- Finish naming/tier review for duplicated concrete helper families. +- Identify helpers that should pause until generics rather than expanding more + type-specific copies. +- Release this connected slice as the next beta tag only after the catalog, + composition example, docs, and final review all pass together. diff --git a/README.md b/README.md index 3131673..a0e47df 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,13 @@ async IO, platform error codes, or stable handle ABI/layout. Directory creation is intentionally narrow and does not imply directory enumeration or recursive filesystem APIs. +## Post-beta Mainline + +Current `main` after `1.0.0-beta.2` has started the standard-library +stabilization slice. It includes a generated standard-library API catalog and +`examples/projects/stdlib-composition`, a checked/tested/run-capable program +that composes `std.fs`, `std.string`, `std.math`, and `std.io`. + ## Documentation - [Language Manifest](docs/language/MANIFEST.md) diff --git a/compiler/tests/standard_stdlib_composition_beta.rs b/compiler/tests/standard_stdlib_composition_beta.rs new file mode 100644 index 0000000..71c88f2 --- /dev/null +++ b/compiler/tests/standard_stdlib_composition_beta.rs @@ -0,0 +1,167 @@ +use std::{ + ffi::OsStr, + fs, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::atomic::{AtomicUsize, Ordering}, +}; + +static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0); + +const EXPECTED_TEST_OUTPUT: &str = concat!( + "test \"stdlib composition reads parses and cleans\" ... ok\n", + "test \"stdlib composition clamps and squares\" ... ok\n", + "test \"stdlib composition main score\" ... ok\n", + "3 test(s) passed\n", +); + +#[test] +fn stdlib_composition_project_checks_tests_docs_and_runs() { + let compiler_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let repo_root = compiler_root.parent().expect("compiler has repo parent"); + let project = repo_root.join("examples/projects/stdlib-composition"); + let source = read(&project.join("src/main.slo")); + + assert!( + !project.join("src/fs.slo").exists() + && !project.join("src/string.slo").exists() + && !project.join("src/math.slo").exists() + && !project.join("src/io.slo").exists(), + "stdlib composition must use repo-root std imports, not local module copies" + ); + assert!( + source.contains( + "(import std.fs (write_text_status read_text_result exists is_file remove_file_ok))" + ) && source.contains("(import std.string (concat parse_i32_result))") + && source.contains("(import std.math (clamp_i32 square_i32))") + && source.contains("(import std.io (print_i32_zero))"), + "stdlib composition fixture must compose several explicit std imports" + ); + assert!( + source.contains("(fn score_file_roundtrip_ok") + && source.contains("(fn computed_score") + && source.contains("(fn main_score") + && source.contains("(fn cleanup_score_file"), + "stdlib composition fixture must keep realistic read/parse/compute/cleanup flow visible" + ); + + let fmt = run_glagol([ + OsStr::new("fmt"), + OsStr::new("--check"), + project.as_os_str(), + ]); + assert_success("stdlib composition fmt --check", &fmt); + + let check = run_glagol([OsStr::new("check"), project.as_os_str()]); + assert_success_stdout(check, "", "stdlib composition check"); + + let test_cwd = temp_root("test"); + let test = run_glagol_in([OsStr::new("test"), project.as_os_str()], &test_cwd); + assert_success_stdout(test, EXPECTED_TEST_OUTPUT, "stdlib composition test"); + assert!( + !test_cwd.join("slovo-stdlib-composition-beta.txt").exists(), + "stdlib composition tests must clean their file fixture" + ); + + let docs = temp_root("docs"); + let doc = run_glagol([ + OsStr::new("doc"), + project.as_os_str(), + OsStr::new("-o"), + docs.as_os_str(), + ]); + assert_success("stdlib composition doc", &doc); + let doc_index = read(&docs.join("index.md")); + assert!(doc_index.contains("## Module main")); + assert!(doc_index.contains("- `computed_score")); + assert!(doc_index.contains("- `stdlib composition clamps and squares`")); + + let run_cwd = temp_root("run"); + let run = run_glagol_in([OsStr::new("run"), project.as_os_str()], &run_cwd); + if run.status.success() { + assert_eq!(String::from_utf8_lossy(&run.stdout), "100\n"); + assert!( + run.stderr.is_empty(), + "stdlib composition run wrote stderr:\n{}", + String::from_utf8_lossy(&run.stderr) + ); + assert!( + !run_cwd.join("slovo-stdlib-composition-beta.txt").exists(), + "stdlib composition run must clean its file fixture" + ); + } else { + assert_stderr_contains("stdlib composition run", &run, "ToolchainUnavailable"); + } +} + +fn run_glagol(args: I) -> Output +where + I: IntoIterator, + S: AsRef, +{ + run_glagol_in(args, Path::new(env!("CARGO_MANIFEST_DIR"))) +} + +fn run_glagol_in(args: I, cwd: &Path) -> Output +where + I: IntoIterator, + S: AsRef, +{ + fs::create_dir_all(cwd).expect("create command cwd"); + Command::new(env!("CARGO_BIN_EXE_glagol")) + .args(args) + .current_dir(cwd) + .output() + .expect("run glagol") +} + +fn temp_root(name: &str) -> PathBuf { + let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed); + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock before UNIX_EPOCH") + .as_nanos(); + std::env::temp_dir().join(format!( + "glagol-stdlib-composition-{}-{}-{}-{}", + std::process::id(), + nanos, + id, + name + )) +} + +fn read(path: &Path) -> String { + fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err)) +} + +fn assert_success(context: &str, output: &Output) { + assert!( + output.status.success(), + "{} failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + context, + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn assert_success_stdout(output: Output, expected: &str, context: &str) { + assert_success(context, &output); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + expected, + "{}", + context + ); +} + +fn assert_stderr_contains(context: &str, output: &Output, needle: &str) { + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains(needle), + "{} stderr did not contain `{}`:\n{}", + context, + needle, + stderr + ); +} diff --git a/docs/POST_BETA_ROADMAP.md b/docs/POST_BETA_ROADMAP.md index 842e834..3f3e443 100644 --- a/docs/POST_BETA_ROADMAP.md +++ b/docs/POST_BETA_ROADMAP.md @@ -87,6 +87,12 @@ Work: - identify helpers that should wait for generics instead of being copied across concrete type families +Started on `main` after `1.0.0-beta.2`: `docs/language/STDLIB_API.md` is +generated from `lib/std/*.slo` and guarded by `scripts/release-gate.sh`. +`examples/projects/stdlib-composition` adds a realistic command-line project +that composes `std.fs`, `std.string`, `std.math`, and `std.io` through explicit +standard imports, with focused check/test/doc/run coverage. + Why third: stdlib growth is already broad enough that naming and stability tiers matter more than adding another isolated helper group. diff --git a/docs/language/examples/README.md b/docs/language/examples/README.md index 2809b92..4f4728c 100644 --- a/docs/language/examples/README.md +++ b/docs/language/examples/README.md @@ -8,7 +8,9 @@ Entries in this section are current compiler support under the matching release notes. The current compiler-supported language baseline is `1.0.0-beta`; `1.0.0-beta.1` adds tooling/install hardening without changing these source-language fixtures. `1.0.0-beta.2` adds beta-scoped -runtime/resource foundation APIs. The language baseline absorbs the final +runtime/resource foundation APIs. Current `main` after `1.0.0-beta.2` also +adds the generated stdlib API catalog and the checked +`projects/stdlib-composition/` example. The language baseline absorbs the final exp-125 unsigned precursor scope alongside the already promoted project/package, stdlib-source, collection, composite-data, formatter, and diagnostics surface. @@ -282,6 +284,9 @@ compiler-supported targets with matching Glagol coverage: `projects/std-import-env/`, `projects/std-import-fs/`, `projects/std-import-process/`, `projects/std-import-cli/`, `projects/std-import-result/`, and `projects/std-import-option/` +- `projects/stdlib-composition/`, a realistic command-line example that uses + explicit `std.fs`, `std.string`, `std.math`, and `std.io` imports together + for file write/read, parse, compute, cleanup, and hosted `glagol run` output `supported/unsafe.slo` is the current lexical unsafe promotion fixture. Glagol can parse it, lower it, type-check it, format it, emit LLVM for safe forms diff --git a/examples/projects/stdlib-composition/slovo.toml b/examples/projects/stdlib-composition/slovo.toml new file mode 100644 index 0000000..10cdc53 --- /dev/null +++ b/examples/projects/stdlib-composition/slovo.toml @@ -0,0 +1,4 @@ +[project] +name = "stdlib-composition" +source_root = "src" +entry = "main" diff --git a/examples/projects/stdlib-composition/src/main.slo b/examples/projects/stdlib-composition/src/main.slo new file mode 100644 index 0000000..227fac9 --- /dev/null +++ b/examples/projects/stdlib-composition/src/main.slo @@ -0,0 +1,74 @@ +(module main) + +(import std.fs (write_text_status read_text_result exists is_file remove_file_ok)) + +(import std.io (print_i32_zero)) + +(import std.math (clamp_i32 square_i32)) + +(import std.string (concat parse_i32_result)) + +(fn score_path () -> string + "slovo-stdlib-composition-beta.txt") + +(fn score_text () -> string + (concat "4" "2")) + +(fn seed_score_file () -> bool + (= (write_text_status (score_path) (score_text)) 0)) + +(fn read_score_result () -> (result i32 i32) + (if (seed_score_file) + (match (read_text_result (score_path)) + ((ok text) + (parse_i32_result text)) + ((err code) + (err i32 i32 code))) + (err i32 i32 1))) + +(fn read_score_or_zero () -> i32 + (match (read_score_result) + ((ok value) + value) + ((err code) + 0))) + +(fn bounded_score () -> i32 + (clamp_i32 (read_score_or_zero) 0 10)) + +(fn computed_score () -> i32 + (square_i32 (bounded_score))) + +(fn cleanup_score_file () -> bool + (if (exists (score_path)) + (remove_file_ok (score_path)) + true)) + +(fn score_file_roundtrip_ok () -> bool + (if (= (read_score_or_zero) 42) + (if (exists (score_path)) + (if (is_file (score_path)) + (cleanup_score_file) + false) + false) + false)) + +(fn main_score () -> i32 + (let value i32 (computed_score)) + (cleanup_score_file) + value) + +(fn main () -> i32 + (let value i32 (main_score)) + (print_i32_zero value)) + +(test "stdlib composition reads parses and cleans" + (score_file_roundtrip_ok)) + +(test "stdlib composition clamps and squares" + (if (= (computed_score) 100) + (cleanup_score_file) + false)) + +(test "stdlib composition main score" + (= (main_score) 100))