diff --git a/.llm/BETA_1_TOOLING_HARDENING.md b/.llm/BETA_1_TOOLING_HARDENING.md new file mode 100644 index 0000000..bd585e5 --- /dev/null +++ b/.llm/BETA_1_TOOLING_HARDENING.md @@ -0,0 +1,40 @@ +# Beta.1 Tooling Hardening Scope + +This file tracks the first post-`1.0.0-beta` tooling bundle. It is committed on +`main` but must not be tagged as `1.0.0-beta.1` until the connected bundle is +complete and the full release gate passes near publication. + +## Implemented In This Slice + +- `glagol run ` builds through the existing native hosted + path, executes the result, forwards stdout/stderr, and exits with the program + status. +- `glagol run` writes to `.slovo/build/` by default and supports `-o + ` for an explicit executable path. +- `glagol run ... -- ` forwards program arguments to the produced + executable. +- `glagol clean ` removes generated `.slovo/build` artifacts. +- `glagol new --template binary|library|workspace` supports the existing + binary scaffold plus checkable/testable library and local workspace + scaffolds. +- The release gate prints a concise success line after docs, formatting, tests, + promotion, binary, and LLVM smoke checks pass. + +## Explicitly Out Of Scope + +- no source-language syntax change +- no networking or runtime resource model +- no package registry behavior +- no stable ABI/layout promise +- no stable install layout promise until the install-path portion of this + tooling bundle is finished + +## Remaining Before Tagging `1.0.0-beta.1` + +- document and gate public install layout for `glagol`, `runtime/`, and + `lib/std` +- add a minimal install or packaging command/script if the existing build flow + is not enough +- rerender publication PDFs only if documentation release text changes +- run the full release gate from a clean checkout state + diff --git a/README.md b/README.md index 9c3e72b..1a32b15 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,25 @@ Build a native executable when Clang is available: SLOVO_STD_PATH="$PWD/lib/std" ./compiler/target/debug/glagol build hello -o hello/bin ``` +## Post-Beta Main Additions + +The `main` branch contains unreleased tooling work intended for a future +`1.0.0-beta.1` bundle. + +Build and execute in one step: + +```bash +SLOVO_STD_PATH="$PWD/lib/std" ./compiler/target/debug/glagol run hello +SLOVO_STD_PATH="$PWD/lib/std" ./compiler/target/debug/glagol clean hello +``` + +Create alternate project shapes: + +```bash +./compiler/target/debug/glagol new numbers --template library +./compiler/target/debug/glagol new workspace-demo --template workspace +``` + ## Documentation - [Language Manifest](docs/language/MANIFEST.md) diff --git a/compiler/src/main.rs b/compiler/src/main.rs index effe939..7781045 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -17,7 +17,8 @@ mod types; mod unsafe_ops; use std::{ - env, fs, io, + env, fs, + io::{self, Write}, panic::{self, AssertUnwindSafe}, path::{Path, PathBuf}, process::{self, Command as ProcessCommand}, @@ -87,6 +88,9 @@ fn run_invocation_inner(invocation: Invocation) -> ! { if invocation.mode == Mode::New { run_new(invocation); } + if invocation.mode == Mode::Clean { + run_clean(invocation); + } if invocation.mode == Mode::Doc { run_doc(invocation); } @@ -94,14 +98,15 @@ fn run_invocation_inner(invocation: Invocation) -> ! { run_fmt_action(invocation); } - let project_capable_mode = matches!(invocation.mode, Mode::Check | Mode::Build) + let project_capable_mode = matches!(invocation.mode, Mode::Check | Mode::Build | Mode::Run) || (invocation.mode == Mode::RunTests && invocation.manifest_mode_name == "test"); if project_capable_mode && project::is_project_input(&invocation.path) { match invocation.mode { Mode::Check => run_project_check(invocation), Mode::RunTests => run_project_test(invocation), Mode::Build => run_project_build(invocation), - _ => unreachable!("project mode is selected only for check/test/build"), + Mode::Run => run_project_run(invocation), + _ => unreachable!("project mode is selected only for check/test/build/run"), } } @@ -124,6 +129,7 @@ fn run_invocation_inner(invocation: Invocation) -> ! { match invocation.mode { Mode::Build => run_build(invocation, &source), + Mode::Run => run_run(invocation, &source), mode => run_text_mode(invocation, mode, &source), } } @@ -197,6 +203,14 @@ fn run_project_build(invocation: Invocation) -> ! { run_build_from_llvm(invocation, output.text, Some(output.artifact), Vec::new()); } +fn run_project_run(invocation: Invocation) -> ! { + let output = match project::compile_to_llvm(&invocation.path) { + Ok(output) => output, + Err(failure) => exit_project_failure(invocation, failure), + }; + run_native_from_llvm(invocation, output.text, Some(output.artifact), Vec::new()); +} + fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFailure) -> ! { let rendered = render_source_diagnostics_multi( &failure.diagnostics, @@ -235,7 +249,9 @@ fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! { Mode::CheckTests => driver::check_tests(&invocation.path, source), Mode::RunTests => unreachable!("test mode is handled separately"), Mode::Build => unreachable!("build is handled separately"), + Mode::Run => unreachable!("run is handled separately"), Mode::New => unreachable!("new is handled separately"), + Mode::Clean => unreachable!("clean is handled separately"), Mode::Doc => unreachable!("doc is handled separately"), }; @@ -300,7 +316,11 @@ fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! { } fn run_new(invocation: Invocation) -> ! { - match scaffold::create_project(&invocation.path, invocation.project_name.as_deref()) { + match scaffold::create_project( + &invocation.path, + invocation.project_name.as_deref(), + invocation.project_template, + ) { Ok(()) => { write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None); process::exit(0); @@ -322,6 +342,28 @@ fn run_new(invocation: Invocation) -> ! { } } +fn run_clean(invocation: Invocation) -> ! { + let build_dir = generated_build_dir(&invocation.path); + if build_dir.exists() { + if let Err(err) = fs::remove_dir_all(&build_dir) { + let message = format!("cannot remove `{}`: {}", build_dir.display(), err); + emit_message_diagnostic( + &message, + "CleanFailed", + ExitCode::ArtifactFailure, + &invocation, + PrimaryOutput::Diagnostics { + text: message.as_str(), + }, + None, + ); + } + } + + write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None); + process::exit(0); +} + fn run_doc(invocation: Invocation) -> ! { let Some(output_dir) = invocation.output_path.as_deref() else { emit_message_diagnostic( @@ -589,6 +631,31 @@ fn run_build(invocation: Invocation, source: &str) -> ! { run_build_from_llvm(invocation, llvm_ir, None, foreign_imports); } +fn run_run(invocation: Invocation, source: &str) -> ! { + let foreign_imports = c_imports_for_manifest(&invocation.path, source); + let llvm_ir = match driver::compile_to_llvm(&invocation.path, source) { + Ok(output) => output, + Err(diagnostics) => { + let rendered = render_source_diagnostics(&diagnostics, source, invocation.diagnostics); + eprint!("{}", rendered.stderr); + write_manifest_if_requested_with_foreign_imports( + &invocation, + false, + PrimaryOutput::Diagnostics { + text: &rendered.machine_text, + }, + None, + None, + &foreign_imports, + None, + ); + process::exit(ExitCode::SourceFailure.code()); + } + }; + + run_native_from_llvm(invocation, llvm_ir, None, foreign_imports); +} + fn run_build_from_llvm( invocation: Invocation, llvm_ir: String, @@ -607,18 +674,118 @@ fn run_build_from_llvm( None, ); }; + let output_path = PathBuf::from(output_path); + let native = build_native_executable_or_exit(&invocation, llvm_ir, &output_path); + let output_display = native.output_path.display().to_string(); + write_manifest_if_requested_with_foreign_imports( + &invocation, + true, + PrimaryOutput::Path { + kind: "native-executable", + path: &output_display, + }, + None, + Some(BuildInfo { + clang: &native.clang, + runtime: &native.runtime, + c_inputs: &invocation.link_c_paths, + }), + &foreign_imports, + project_artifact.as_ref(), + ); + process::exit(0); +} - if let Some(parent) = Path::new(output_path).parent() { +fn run_native_from_llvm( + invocation: Invocation, + llvm_ir: String, + project_artifact: Option, + foreign_imports: Vec, +) -> ! { + let output_path = run_output_path(&invocation, project_artifact.as_ref()); + if invocation.output_path.is_none() { + if let Some(parent) = output_path.parent() { + if let Err(err) = fs::create_dir_all(parent) { + let message = format!("cannot create `{}`: {}", parent.display(), err); + emit_message_diagnostic( + &message, + "OutputWriteFailed", + ExitCode::ArtifactFailure, + &invocation, + PrimaryOutput::Diagnostics { + text: message.as_str(), + }, + None, + ); + } + } + } + + let native = build_native_executable_or_exit(&invocation, llvm_ir, &output_path); + let run_output = match ProcessCommand::new(&native.output_path) + .args(&invocation.run_args) + .output() + { + Ok(output) => output, + Err(err) => { + let message = format!("cannot run `{}`: {}", native.output_path.display(), err); + emit_message_diagnostic( + &message, + "RunFailed", + ExitCode::ArtifactFailure, + &invocation, + PrimaryOutput::Diagnostics { + text: message.as_str(), + }, + None, + ); + } + }; + + let _ = io::stdout().write_all(&run_output.stdout); + let _ = io::stderr().write_all(&run_output.stderr); + let stdout = String::from_utf8_lossy(&run_output.stdout).to_string(); + write_manifest_if_requested_with_foreign_imports( + &invocation, + run_output.status.success(), + PrimaryOutput::Stdout { + kind: Mode::Run.output_kind(), + text: &stdout, + }, + None, + Some(BuildInfo { + clang: &native.clang, + runtime: &native.runtime, + c_inputs: &invocation.link_c_paths, + }), + &foreign_imports, + project_artifact.as_ref(), + ); + process::exit(run_output.status.code().unwrap_or(1)); +} + +struct NativeBuild { + output_path: PathBuf, + clang: String, + runtime: PathBuf, +} + +fn build_native_executable_or_exit( + invocation: &Invocation, + llvm_ir: String, + output_path: &Path, +) -> NativeBuild { + if let Some(parent) = output_path.parent() { if !parent.as_os_str().is_empty() && !parent.is_dir() { let message = format!( "cannot write `{}`: parent directory does not exist", - output_path + output_path.display() ); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -634,7 +801,7 @@ fn run_build_from_llvm( &message, "ToolchainUnavailable", ExitCode::Toolchain, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -650,7 +817,7 @@ fn run_build_from_llvm( &message, "InputReadFailed", ExitCode::SourceFailure, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -659,12 +826,11 @@ fn run_build_from_llvm( } } - let output = Path::new(output_path); - let output_dir = output + let output_dir = output_path .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); - let stem = output + let stem = output_path .file_name() .and_then(|name| name.to_str()) .unwrap_or("glagol-output"); @@ -682,7 +848,7 @@ fn run_build_from_llvm( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -707,7 +873,7 @@ fn run_build_from_llvm( &message, "ToolchainUnavailable", ExitCode::Toolchain, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -721,7 +887,7 @@ fn run_build_from_llvm( &message, "ToolchainUnavailable", ExitCode::Toolchain, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -747,7 +913,7 @@ fn run_build_from_llvm( &message, "ToolchainUnavailable", ExitCode::Toolchain, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -758,12 +924,12 @@ fn run_build_from_llvm( if let Err(err) = fs::rename(&temp_binary, output_path) { let _ = fs::remove_file(&temp_llvm); let _ = fs::remove_file(&temp_binary); - let message = format!("cannot write `{}`: {}", output_path, err); + let message = format!("cannot write `{}`: {}", output_path.display(), err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, - &invocation, + invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, @@ -772,23 +938,11 @@ fn run_build_from_llvm( } let _ = fs::remove_file(&temp_llvm); - write_manifest_if_requested_with_foreign_imports( - &invocation, - true, - PrimaryOutput::Path { - kind: "native-executable", - path: output_path, - }, - None, - Some(BuildInfo { - clang: &clang, - runtime: &runtime, - c_inputs: &invocation.link_c_paths, - }), - &foreign_imports, - project_artifact.as_ref(), - ); - process::exit(0); + NativeBuild { + output_path: output_path.to_path_buf(), + clang, + runtime, + } } #[derive(Clone)] @@ -809,8 +963,10 @@ struct Invocation { command_line: String, fmt_action: FmtAction, project_name: Option, + project_template: scaffold::ProjectTemplate, link_c_paths: Vec, test_filter: Option, + run_args: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -843,8 +999,10 @@ fn parse_args(raw_args: &[String]) -> Result { let mut diagnostics = DiagnosticFormat::TextAndSexpr; let mut fmt_action = FmtAction::Stdout; let mut project_name = None; + let mut project_template = scaffold::ProjectTemplate::Binary; let mut link_c_paths = Vec::new(); let mut test_filter = None; + let mut run_args = Vec::new(); let mut no_color = false; let command_line = raw_args.join(" "); let mut iter = raw_args @@ -855,6 +1013,11 @@ fn parse_args(raw_args: &[String]) -> Result { .into_iter(); while let Some(arg) = iter.next() { + if arg == "--" { + run_args.extend(iter); + break; + } + match arg.as_str() { "-h" | "--help" => return Ok(Args::Help), "--version" => return Ok(Args::Version), @@ -987,6 +1150,26 @@ fn parse_args(raw_args: &[String]) -> Result { command_line: command_line.clone(), })?); } + "--template" => { + let value = iter.next().ok_or_else(|| ParseError { + message: "`--template` requires one of: binary, library, workspace".to_string(), + manifest_path: manifest_path.clone(), + diagnostics, + command_line: command_line.clone(), + })?; + let Some(parsed) = scaffold::ProjectTemplate::parse(&value) else { + return parse_error( + format!( + "unsupported project template `{}`; expected binary, library, or workspace", + value + ), + manifest_path, + diagnostics, + command_line, + ); + }; + project_template = parsed; + } "--manifest" => { if manifest_path.is_some() { return parse_error( @@ -1027,12 +1210,16 @@ fn parse_args(raw_args: &[String]) -> Result { command_line: command_line.clone(), })?); } - "check" | "fmt" | "test" | "build" | "new" | "doc" if path.is_none() => { + "check" | "fmt" | "test" | "build" | "run" | "clean" | "new" | "doc" + if path.is_none() => + { let next = match arg.as_str() { "check" => Mode::Check, "fmt" => Mode::Format, "test" => Mode::RunTests, "build" => Mode::Build, + "run" => Mode::Run, + "clean" => Mode::Clean, "new" => Mode::New, "doc" => Mode::Doc, _ => unreachable!(), @@ -1114,9 +1301,18 @@ fn parse_args(raw_args: &[String]) -> Result { ); } - if !link_c_paths.is_empty() && mode != Mode::Build { + if project_template != scaffold::ProjectTemplate::Binary && mode != Mode::New { return parse_error( - "`--link-c` is only supported with `build`", + "`--template` is only supported with `new`", + manifest_path, + diagnostics, + command_line, + ); + } + + if !link_c_paths.is_empty() && !matches!(mode, Mode::Build | Mode::Run) { + return parse_error( + "`--link-c` is only supported with `build` and `run`", manifest_path, diagnostics, command_line, @@ -1132,6 +1328,24 @@ fn parse_args(raw_args: &[String]) -> Result { ); } + if !run_args.is_empty() && mode != Mode::Run { + return parse_error( + "`--` program arguments are only supported with `run`", + manifest_path, + diagnostics, + command_line, + ); + } + + if mode == Mode::Clean && output_path.is_some() { + return parse_error( + "`clean` does not support `-o`", + manifest_path, + diagnostics, + command_line, + ); + } + if mode == Mode::Doc && output_path.is_none() { return parse_error( "`doc` requires `-o `", @@ -1162,8 +1376,10 @@ fn parse_args(raw_args: &[String]) -> Result { command_line, fmt_action, project_name, + project_template, link_c_paths, test_filter, + run_args, })) } @@ -1233,8 +1449,10 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! { }, fmt_action: FmtAction::Stdout, project_name: None, + project_template: scaffold::ProjectTemplate::Binary, link_c_paths: Vec::new(), test_filter: None, + run_args: Vec::new(), }; write_manifest_or_exit( manifest_path, @@ -1267,6 +1485,8 @@ enum Mode { CheckTests, RunTests, Build, + Run, + Clean, New, Doc, } @@ -1283,6 +1503,8 @@ impl Mode { Self::CheckTests => "check-tests", Self::RunTests => "test", Self::Build => "build", + Self::Run => "run", + Self::Clean => "clean", Self::New => "new", Self::Doc => "doc", } @@ -1297,6 +1519,8 @@ impl Mode { Self::InspectLoweringSurface | Self::InspectLoweringChecked => "lowering-inspector", Self::CheckTests | Self::RunTests => "stdout", Self::Build => "native-executable", + Self::Run => "program-stdout", + Self::Clean => "no-output", Self::New => "no-output", Self::Doc => "documentation", } @@ -2001,6 +2225,79 @@ fn runtime_path() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../runtime/runtime.c") } +fn run_output_path( + invocation: &Invocation, + project_artifact: Option<&project::ProjectArtifact>, +) -> PathBuf { + if let Some(output_path) = invocation.output_path.as_deref() { + return PathBuf::from(output_path); + } + + let stem = project_artifact + .map(|artifact| artifact.project_name.as_str()) + .or_else(|| { + Path::new(&invocation.path) + .file_stem() + .and_then(|stem| stem.to_str()) + }) + .map(sanitize_output_stem) + .filter(|stem| !stem.is_empty()) + .unwrap_or_else(|| "slovo-program".to_string()); + generated_build_dir(&invocation.path).join(format!("{}{}", stem, env::consts::EXE_SUFFIX)) +} + +fn generated_build_dir(input: &str) -> PathBuf { + generated_build_root(input).join(".slovo").join("build") +} + +fn generated_build_root(input: &str) -> PathBuf { + let path = Path::new(input); + if project::is_project_input(input) { + if path.is_dir() { + return path.to_path_buf(); + } + return path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")) + .to_path_buf(); + } + path.parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")) + .to_path_buf() +} + +fn sanitize_output_stem(value: &str) -> String { + let mut out = String::new(); + let mut previous_dash = false; + for ch in value.chars().flat_map(char::to_lowercase) { + let mapped = if ch.is_ascii_lowercase() || ch.is_ascii_digit() { + Some(ch) + } else if ch == '-' || ch == '_' || ch == '.' || ch.is_whitespace() { + Some('-') + } else { + None + }; + let Some(ch) = mapped else { + continue; + }; + if ch == '-' { + if !out.is_empty() && !previous_dash { + out.push('-'); + previous_dash = true; + } + } else { + out.push(ch); + previous_dash = false; + } + } + while out.ends_with('-') { + out.pop(); + } + out +} + fn c_imports_for_manifest(file: &str, source: &str) -> Vec { let Ok(tokens) = lexer::lex(file, source) else { return Vec::new(); @@ -2102,6 +2399,6 @@ fn normalized_output_path(path: &str) -> Option { fn print_usage() { eprintln!( - "usage: glagol [check|fmt|test|build] [--json-diagnostics] [--no-color] [--manifest ] [--link-c ] [-o ] [--filter ] \n glagol fmt [--check|--write] \n glagol new [--name ]\n glagol doc -o \n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o ] [--manifest ] [--filter ] \n glagol --version" + "usage: glagol [check|fmt|test|build|run|clean] [--json-diagnostics] [--no-color] [--manifest ] [--link-c ] [-o ] [--filter ] [-- ...]\n glagol fmt [--check|--write] \n glagol new [--name ] [--template binary|library|workspace]\n glagol doc -o \n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o ] [--manifest ] [--filter ] \n glagol --version" ); } diff --git a/compiler/src/scaffold.rs b/compiler/src/scaffold.rs index f09fd13..13bfc67 100644 --- a/compiler/src/scaffold.rs +++ b/compiler/src/scaffold.rs @@ -5,7 +5,29 @@ use std::{ use crate::diag::Diagnostic; -pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), Diagnostic> { +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ProjectTemplate { + Binary, + Library, + Workspace, +} + +impl ProjectTemplate { + pub fn parse(value: &str) -> Option { + match value { + "binary" => Some(Self::Binary), + "library" => Some(Self::Library), + "workspace" => Some(Self::Workspace), + _ => None, + } + } +} + +pub fn create_project( + target: &str, + explicit_name: Option<&str>, + template: ProjectTemplate, +) -> Result<(), Diagnostic> { if target.trim().is_empty() { return Err(Diagnostic::new( target, @@ -49,6 +71,14 @@ pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), D )); } + match template { + ProjectTemplate::Binary => create_binary_project(root, target, &name), + ProjectTemplate::Library => create_library_project(root, target, &name), + ProjectTemplate::Workspace => create_workspace_project(root, target), + } +} + +fn create_binary_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> { let src = root.join("src"); create_dir_all_checked(&src, target)?; write_checked( @@ -66,6 +96,56 @@ pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), D ) } +fn create_library_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> { + let src = root.join("src"); + create_dir_all_checked(&src, target)?; + write_checked( + &root.join("slovo.toml"), + &format!( + "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"lib\"\n", + name + ), + target, + )?; + write_checked( + &src.join("lib.slo"), + "(module lib (export answer double))\n\n(fn answer () -> i32\n 42)\n\n(fn double ((value i32)) -> i32\n (+ value value))\n\n(test \"answer is stable\"\n (= (answer) 42))\n\n(test \"double works\"\n (= (double 21) 42))\n", + target, + ) +} + +fn create_workspace_project(root: &Path, target: &str) -> Result<(), Diagnostic> { + let app_src = root.join("packages/app/src"); + let lib_src = root.join("packages/libutil/src"); + create_dir_all_checked(&app_src, target)?; + create_dir_all_checked(&lib_src, target)?; + write_checked( + &root.join("slovo.toml"), + "[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\n", + target, + )?; + write_checked( + &root.join("packages/libutil/slovo.toml"), + "[package]\nname = \"libutil\"\nversion = \"0.1.0\"\n", + target, + )?; + write_checked( + &root.join("packages/libutil/src/libutil.slo"), + "(module libutil (export answer label))\n\n(fn answer () -> i32\n 42)\n\n(fn label () -> string\n \"libutil\")\n\n(test \"answer is stable\"\n (= (answer) 42))\n", + target, + )?; + write_checked( + &root.join("packages/app/slovo.toml"), + "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nlibutil = { path = \"../libutil\" }\n", + target, + )?; + write_checked( + &root.join("packages/app/src/main.slo"), + "(module main)\n\n(import libutil.libutil (answer label))\n\n(fn main () -> i32\n (if (= (answer) 42)\n 0\n 1))\n\n(test \"app uses libutil\"\n (if (= (label) \"libutil\")\n (= (answer) 42)\n false))\n", + target, + ) +} + fn has_entries(path: &Path) -> Result { let mut entries = fs::read_dir(path).map_err(|err| io_diagnostic(path, err))?; match entries.next() { diff --git a/compiler/tests/dx_v1_7.rs b/compiler/tests/dx_v1_7.rs index 0a24c1e..5b7b036 100644 --- a/compiler/tests/dx_v1_7.rs +++ b/compiler/tests/dx_v1_7.rs @@ -70,6 +70,121 @@ fn new_creates_minimal_valid_project() { } } +#[test] +fn run_builds_executes_and_clean_removes_generated_artifacts() { + let project = unique_path("run-project"); + + let new_output = run_glagol(["new".as_ref(), project.as_os_str()]); + assert_success("glagol new for run", &new_output); + + let run = run_glagol(["run".as_ref(), project.as_os_str()]); + if run.status.success() { + assert!(run.stdout.is_empty(), "run wrote stdout"); + assert!(run.stderr.is_empty(), "run wrote stderr"); + assert!( + project.join(".slovo/build").is_dir(), + "run did not create generated build directory" + ); + } else { + assert_stderr_contains("generated project run", &run, "ToolchainUnavailable"); + fs::create_dir_all(project.join(".slovo/build")).expect("create synthetic build dir"); + fs::write(project.join(".slovo/build/stale"), "").expect("write stale build file"); + } + + let clean = run_glagol(["clean".as_ref(), project.as_os_str()]); + assert_success("glagol clean", &clean); + assert!(clean.stdout.is_empty(), "clean wrote stdout"); + assert!(clean.stderr.is_empty(), "clean wrote stderr"); + assert!( + !project.join(".slovo/build").exists(), + "clean left generated build directory behind" + ); +} + +#[test] +fn run_forwards_program_arguments_when_host_toolchain_is_available() { + let project = write_project( + "run-args-project", + &[], + "(module main)\n\n(import std.process (argc))\n\n(fn main () -> i32\n (if (= (argc) 3)\n 0\n 1))\n", + ); + + let run = run_glagol([ + "run".as_ref(), + project.as_os_str(), + "--".as_ref(), + "alpha".as_ref(), + "beta".as_ref(), + ]); + + if run.status.success() { + assert!(run.stdout.is_empty(), "run args wrote stdout"); + assert!(run.stderr.is_empty(), "run args wrote stderr"); + } else { + assert_stderr_contains("run args project", &run, "ToolchainUnavailable"); + } +} + +#[test] +fn new_library_template_creates_checkable_testable_library_project() { + let project = unique_path("library-template"); + + let output = run_glagol([ + "new".as_ref(), + project.as_os_str(), + "--template".as_ref(), + "library".as_ref(), + "--name".as_ref(), + "numbers".as_ref(), + ]); + + assert_success("glagol new --template library", &output); + assert_eq!( + fs::read_to_string(project.join("slovo.toml")).expect("read library manifest"), + "[project]\nname = \"numbers\"\nsource_root = \"src\"\nentry = \"lib\"\n" + ); + let source = fs::read_to_string(project.join("src/lib.slo")).expect("read library source"); + assert!(source.contains("(module lib (export answer double))")); + + let check = run_glagol(["check".as_ref(), project.as_os_str()]); + assert_success("library template check", &check); + let test = run_glagol(["test".as_ref(), project.as_os_str()]); + assert_success("library template test", &test); + assert_eq!( + String::from_utf8_lossy(&test.stdout), + "test \"answer is stable\" ... ok\ntest \"double works\" ... ok\n2 test(s) passed\n" + ); +} + +#[test] +fn new_workspace_template_creates_local_package_workspace() { + let workspace = unique_path("workspace-template"); + + let output = run_glagol([ + "new".as_ref(), + workspace.as_os_str(), + "--template".as_ref(), + "workspace".as_ref(), + ]); + + assert_success("glagol new --template workspace", &output); + assert_eq!( + fs::read_to_string(workspace.join("slovo.toml")).expect("read workspace manifest"), + "[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\n" + ); + assert!(workspace.join("packages/app/slovo.toml").is_file()); + assert!(workspace.join("packages/libutil/slovo.toml").is_file()); + + let check = run_glagol(["check".as_ref(), workspace.as_os_str()]); + assert_success("workspace template check", &check); + let test = run_glagol(["test".as_ref(), workspace.as_os_str()]); + assert_success("workspace template test", &test); + let stdout = String::from_utf8_lossy(&test.stdout); + assert!(stdout.contains("test \"app uses libutil\" ... ok")); + assert!(stdout.contains("test \"answer is stable\" ... ok")); + assert!(stdout.contains("2 test(s) passed")); +} + #[test] fn new_rejects_non_empty_target_with_structured_diagnostic() { let project = unique_path("new-non-empty"); diff --git a/docs/POST_BETA_ROADMAP.md b/docs/POST_BETA_ROADMAP.md index c06778a..25992f5 100644 --- a/docs/POST_BETA_ROADMAP.md +++ b/docs/POST_BETA_ROADMAP.md @@ -36,6 +36,12 @@ Work: - make release gates print a concise final summary - keep PDF rendering explicit and non-mutating by default +Current main-branch progress after `1.0.0-beta`: `glagol run`, +`glagol clean`, `glagol new --template binary|library|workspace`, README +coverage, focused DX tests, and a concise release-gate success line are +implemented. Install-path polish remains in this tooling bundle before a +`1.0.0-beta.1` tag. + Why first: it reduces friction for every later feature and gives users a better way to exercise the beta. @@ -199,4 +205,3 @@ complete first: - optimizing compiler claims - web framework or HTTP server framework - broad Unicode/string normalization policy - diff --git a/docs/compiler/RELEASE_NOTES.md b/docs/compiler/RELEASE_NOTES.md index 9f9a53b..51e8c29 100644 --- a/docs/compiler/RELEASE_NOTES.md +++ b/docs/compiler/RELEASE_NOTES.md @@ -8,6 +8,22 @@ Historical `exp-*` releases listed here are experimental maturity milestones. The pushed tag `v2.0.0-beta.1` is historical. It remains an experimental integration/readiness release, not the first real beta. +## Unreleased + +Post-beta main currently contains the first tooling-hardening slice for a +future `1.0.0-beta.1` bundle: + +- `glagol run ` compiles through the existing hosted native + build path, runs the produced executable, forwards stdout/stderr, and returns + the program exit status +- `glagol clean ` removes generated `.slovo/build` artifacts +- `glagol new --template binary|library|workspace` adds library and local + workspace scaffolds beside the existing binary default +- the release gate prints a concise final success summary + +This is a toolchain workflow slice only. It does not claim a new stable ABI, +runtime resource model, networking surface, or package registry. + ## 1.0.0-beta Release label: `1.0.0-beta` diff --git a/docs/language/RELEASE_NOTES.md b/docs/language/RELEASE_NOTES.md index 80d0bf4..9ec2361 100644 --- a/docs/language/RELEASE_NOTES.md +++ b/docs/language/RELEASE_NOTES.md @@ -13,6 +13,22 @@ final unsigned precursor scope from `exp-125` and promotes the current project/package, stdlib-source, collection, composite-data, diagnostics, formatter, docs, and governance surface to beta maturity. +## Unreleased + +Post-beta main currently contains tooling hardening intended for a future +`1.0.0-beta.1` bundle: + +- `glagol run ` builds and executes through the hosted native + toolchain using `.slovo/build` when `-o` is not provided +- `glagol clean ` removes generated `.slovo/build` artifacts +- `glagol new --template binary|library|workspace` scaffolds binary projects, + library projects, and local package workspaces using existing manifest rules +- the release gate prints a concise final success line after all checks pass + +This unreleased slice does not add source-language syntax, stable ABI/layout +guarantees, networking, package registry behavior, or a stable standard-library +freeze. + ## 1.0.0-beta Release label: `1.0.0-beta` diff --git a/docs/language/SPEC-v1.md b/docs/language/SPEC-v1.md index ee4c171..538eb20 100644 --- a/docs/language/SPEC-v1.md +++ b/docs/language/SPEC-v1.md @@ -932,6 +932,27 @@ Glagol release without network, tag, push, or release-publication side effects. The normative v1.7 release contract is `.llm/V1_7_DEVELOPER_EXPERIENCE_HARDENING.md`. +### 4.4.1 Post-Beta Tooling Additions + +Post-beta main adds tooling-only conveniences intended for a future +`1.0.0-beta.1` bundle. These commands do not change source syntax or typed-core +semantics. + +`glagol run ` builds through the same hosted native path as +`glagol build`, writes an executable under `.slovo/build` by default, executes +it, forwards stdout/stderr, and exits with the program exit status. `-o +` may override the generated executable path. + +`glagol clean ` removes the generated `.slovo/build` +directory for the given source file directory or project root. It does not +remove custom `-o` build outputs. + +`glagol new --template binary|library|workspace` supports three +scaffold shapes. `binary` is the default project with `src/main.slo`. +`library` creates a checkable/testable project with `src/lib.slo`. `workspace` +creates a local two-package workspace using the existing `[workspace]`, +`[package]`, and local path dependency rules. + ## 4.5 v2.0.0-beta.1 Experimental Integration Readiness Status: current experimental Slovo-side release contract, released 2026-05-17. diff --git a/scripts/release-gate.sh b/scripts/release-gate.sh index 8680bb8..33ef7b1 100755 --- a/scripts/release-gate.sh +++ b/scripts/release-gate.sh @@ -60,3 +60,5 @@ cargo test cargo test --test promotion_gate -- --ignored cargo test --test binary_smoke -- --ignored cargo test --test llvm_smoke -- --ignored + +echo "release gate passed: docs, fmt, tests, promotion, binary, and LLVM smoke checks completed"