Add post-beta run and scaffold tooling

This commit is contained in:
sanjin 2026-05-22 11:34:51 +02:00
parent d83e20b062
commit ee2b8e0930
10 changed files with 652 additions and 41 deletions

View File

@ -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 <file.slo|project>` 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/<name>` by default and supports `-o
<binary>` for an explicit executable path.
- `glagol run ... -- <args>` forwards program arguments to the produced
executable.
- `glagol clean <file.slo|project>` 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

View File

@ -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)

View File

@ -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,13 +674,39 @@ fn run_build_from_llvm(
None,
);
};
if let Some(parent) = Path::new(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
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);
}
fn run_native_from_llvm(
invocation: Invocation,
llvm_ir: String,
project_artifact: Option<project::ProjectArtifact>,
foreign_imports: Vec<project::ProjectArtifactCImport>,
) -> ! {
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",
@ -626,6 +719,80 @@ fn run_build_from_llvm(
);
}
}
}
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.display()
);
emit_message_diagnostic(
&message,
"OutputWriteFailed",
ExitCode::ArtifactFailure,
invocation,
PrimaryOutput::Diagnostics {
text: message.as_str(),
},
None,
);
}
}
let runtime = runtime_path();
if !runtime.is_file() {
@ -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<String>,
project_template: scaffold::ProjectTemplate,
link_c_paths: Vec<String>,
test_filter: Option<String>,
run_args: Vec<String>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -843,8 +999,10 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
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<Args, ParseError> {
.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<Args, ParseError> {
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<Args, ParseError> {
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<Args, ParseError> {
);
}
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<Args, ParseError> {
);
}
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 <dir>`",
@ -1162,8 +1376,10 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
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<project::ProjectArtifactCImport> {
let Ok(tokens) = lexer::lex(file, source) else {
return Vec::new();
@ -2102,6 +2399,6 @@ fn normalized_output_path(path: &str) -> Option<PathBuf> {
fn print_usage() {
eprintln!(
"usage: glagol [check|fmt|test|build] [--json-diagnostics] [--no-color] [--manifest <path>] [--link-c <path>] [-o <path>] [--filter <substring>] <file.slo|project>\n glagol fmt [--check|--write] <file.slo|project>\n glagol new <project-dir> [--name <name>]\n glagol doc <file.slo|project> -o <dir>\n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o <path>] [--manifest <path>] [--filter <substring>] <file.slo>\n glagol --version"
"usage: glagol [check|fmt|test|build|run|clean] [--json-diagnostics] [--no-color] [--manifest <path>] [--link-c <path>] [-o <path>] [--filter <substring>] <file.slo|project> [-- <program-args>...]\n glagol fmt [--check|--write] <file.slo|project>\n glagol new <project-dir> [--name <name>] [--template binary|library|workspace]\n glagol doc <file.slo|project> -o <dir>\n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o <path>] [--manifest <path>] [--filter <substring>] <file.slo>\n glagol --version"
);
}

View File

@ -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<Self> {
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<bool, Diagnostic> {
let mut entries = fs::read_dir(path).map_err(|err| io_diagnostic(path, err))?;
match entries.next() {

View File

@ -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");

View File

@ -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

View File

@ -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 <file.slo|project>` compiles through the existing hosted native
build path, runs the produced executable, forwards stdout/stderr, and returns
the program exit status
- `glagol clean <file.slo|project>` 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`

View File

@ -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 <file.slo|project>` builds and executes through the hosted native
toolchain using `.slovo/build` when `-o` is not provided
- `glagol clean <file.slo|project>` 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`

View File

@ -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 <file.slo|project>` 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
<binary>` may override the generated executable path.
`glagol clean <file.slo|project>` 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 <project-dir> --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.

View File

@ -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"