Add post-beta run and scaffold tooling
This commit is contained in:
parent
d83e20b062
commit
ee2b8e0930
40
.llm/BETA_1_TOOLING_HARDENING.md
Normal file
40
.llm/BETA_1_TOOLING_HARDENING.md
Normal 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
|
||||
|
||||
19
README.md
19
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)
|
||||
|
||||
@ -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<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",
|
||||
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<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"
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user