mod census;
const MIGRATE: &str = include_str!("../blue/migrate.b");
const VERBS: &str = include_str!("../blue/verbs.b");
mod config;
mod deploy;
mod diagnostics;
mod doc;
mod observe;
mod prefetch;
mod reference;
mod repl;
mod replay;
mod scaffold;
mod script;
mod watch;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use blue_lang_pkg::lock::{Freshness, ManifestRecord, LOCK_FILE};
use clap::{CommandFactory, Parser, Subcommand};
use config::BlueConfig;
#[global_allocator]
static ALLOC: blue_lang_runtime::alloc::Counting = blue_lang_runtime::alloc::Counting;
#[derive(Parser)]
#[command(
name = "blue",
version,
about = "The blue language: a Ruby/Elixir surface on tatara-lisp and Rust."
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Run {
file: PathBuf,
#[arg(long = "input", value_name = "NAME=PATH")]
inputs: Vec<String>,
#[arg(long, short = 'q')]
quiet: bool,
#[arg(long, value_enum, default_value_t = Format::Text)]
format: Format,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Fmt {
#[arg(required = true)]
file: Vec<PathBuf>,
#[arg(long)]
check: bool,
#[arg(long)]
write: bool,
},
Ast {
file: PathBuf,
#[arg(long)]
resolved: bool,
#[arg(long, requires = "resolved")]
json: bool,
},
Erase { file: PathBuf },
Check {
file: PathBuf,
#[arg(long, value_enum, default_value_t = Format::Text)]
format: Format,
#[arg(long)]
fix: bool,
},
Migrate {
#[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
files: Vec<String>,
},
Doc {
#[arg(required_unless_present = "rows")]
name: Option<String>,
#[arg(long)]
file: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(long, hide = true)]
rows: bool,
},
Verbs {
#[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
ExplainName {
name: String,
at: String,
},
Census {
#[arg(default_value = ".")]
root: PathBuf,
#[arg(long)]
findings: bool,
},
#[command(group(clap::ArgGroup::new("which").required(true).args(["code", "list"])))]
Explain {
code: Option<String>,
#[arg(long)]
list: bool,
},
Test {
file: PathBuf,
#[arg(long, value_name = "PATTERN")]
filter: Option<String>,
#[arg(long)]
coverage: bool,
#[arg(long, requires = "coverage")]
json: bool,
},
Profile {
file: PathBuf,
#[arg(long, default_value_t = 20)]
top: usize,
#[arg(long)]
json: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Trace {
def: String,
file: PathBuf,
#[arg(long, default_value_t = 200)]
limit: usize,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Deps { file: PathBuf },
Posture { file: PathBuf },
#[command(group(clap::ArgGroup::new("mode").required(true).args(["json", "confirm"])))]
Bluefile {
#[arg(long)]
json: bool,
#[arg(long)]
confirm: bool,
#[arg(required = true)]
files: Vec<PathBuf>,
},
Lock {
dirs: Vec<PathBuf>,
},
Lsp,
Serve(repl::SessionArgs),
Repl(repl::SessionArgs),
Eval {
target: String,
#[command(flatten)]
session: repl::SessionArgs,
},
Banner,
Shift { file: PathBuf },
Morph,
Reference,
Replay { trace: PathBuf },
Config(shikumi::cli::ConfigShowCommand),
New {
name: PathBuf,
#[arg(long, conflicts_with = "script")]
bidama: bool,
#[arg(long)]
script: bool,
},
Watch {
#[arg(required = true, value_name = "[CMD] PATH")]
targets: Vec<String>,
#[arg(last = true)]
args: Vec<String>,
},
Image {
#[command(subcommand)]
cmd: deploy::ImageCmd,
},
Workload {
#[command(subcommand)]
cmd: deploy::WorkloadCmd,
},
Diff(deploy::DiffArgs),
}
fn main() -> ExitCode {
let argv = script::route(std::env::args_os().collect(), &Cli::command());
match dispatch(Cli::parse_from(argv)) {
Ok(code) => code,
Err(e) => {
eprintln!("blue: {e}");
ExitCode::FAILURE
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
enum Format {
Text,
Json,
}
#[derive(Debug, thiserror::Error)]
enum CliError {
#[error("{path}: {source}")]
Read {
path: String,
#[source]
source: std::io::Error,
},
#[error("{path}: {source}")]
Write {
path: String,
#[source]
source: std::io::Error,
},
#[error("{0}")]
Blue(#[from] blue_lang_runtime::RunError),
#[error("{0}")]
Fmt(String),
#[error("{0}")]
Pkg(String),
#[error("{0}")]
Refused(#[from] blue_lang_pkg::canonical::Refusal),
#[error("{0}")]
Config(#[from] shikumi::cli::ConfigShowError),
#[error("{0}")]
Bluefile(#[from] blue_lang_pkg::BluefileError),
#[error("{0}")]
Lock(#[from] blue_lang_pkg::lock::LockError),
#[error("could not render JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("{0}")]
Deploy(#[from] deploy::DeployError),
#[error("no diagnostic code `{0}`; `blue explain --list` lists them")]
UnknownCode(String),
#[error("`blue bluefile --json` prints one manifest, and {got} Bluefiles were named")]
JsonTakesOne { got: usize },
}
#[derive(serde::Serialize)]
struct Confirmation<'a> {
bluefile: String,
#[serde(flatten)]
verdict: &'a Freshness,
}
fn read_compiled(path: &Path) -> Result<String, CliError> {
Ok(blue_lang_pkg::canonical::read_source(path)?)
}
fn read(path: &Path) -> Result<String, CliError> {
std::fs::read_to_string(path).map_err(|source| CliError::Read {
path: path.display().to_string(),
source,
})
}
fn parse(src: &str, cfg: &BlueConfig) -> Result<Vec<blue_lang_syntax::Sexp>, CliError> {
Ok(blue_lang_runtime::parse_with_depth(
src,
cfg.max_expr_depth,
)?)
}
fn parse_tree(src: &str, cfg: &BlueConfig) -> Result<Vec<blue_lang_syntax::Spanned>, CliError> {
Ok(blue_lang_runtime::parse_tree_with_depth(
src,
cfg.max_expr_depth,
)?)
}
struct Located {
file: String,
line: usize,
col: usize,
}
impl std::fmt::Display for Located {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}:", self.file, self.line, self.col)
}
}
fn scan_registry() -> Result<Option<blue_lang_pkg::git_registry::GitRegistry>, CliError> {
use blue_lang_pkg::git_registry::{GitRegistry, GitRegistryError};
for root in blue_lang_pkg::load_path::LoadPath::from_env().roots() {
let registry = match GitRegistry::scan(root) {
Ok(r) => r,
Err(GitRegistryError::Unreadable { .. }) => continue,
Err(e @ GitRegistryError::BadManifest { .. }) => {
return Err(CliError::Pkg(e.to_string()))
}
};
if !registry.is_empty() {
return Ok(Some(registry));
}
}
Ok(None)
}
fn dispatch(cli: Cli) -> Result<ExitCode, CliError> {
let cfg = config::resolve();
blue_lang_runtime::set_execution_bounds(cfg.execution_bounds());
blue_lang_proc::node::set_trace_dir(replay::trace_dir());
if let Ok(me) = std::env::current_exe() {
blue_lang_runtime::sys::set_blue_exe(me.to_string_lossy().into_owned());
}
match cli.cmd {
Cmd::Replay { trace } => replay::run(&trace).map_err(CliError::Fmt),
Cmd::Run {
file,
inputs,
quiet,
format,
args,
} => {
blue_lang_runtime::sys::set_program_args(args);
let surface = resolve_surface().map_err(CliError::Pkg)?;
let src = match &surface {
None => read_compiled(&file)?,
Some(pack) => {
let text = read(&file)?;
let english = blue_lang_syntax::parse_program(&text).ok();
let spoken = blue_lang_syntax::parse_program_in(&text, pack).ok();
if english.is_some() && english == spoken {
blue_lang_pkg::canonical::admit(&file, text)?
} else {
text
}
}
};
let loader = blue_lang_pkg::load_path::LoadPath::for_entry(&file);
let out = match blue_lang_runtime::pipeline::run_in_surface(
blue_lang_runtime::uses::Entry {
path: Some(&file),
text: &src,
},
bind_inputs(&src, &inputs, &cfg)?,
&loader,
surface.as_ref(),
) {
Err(e) if format == Format::Json => {
println!("{}", serde_json::to_string(&observe::error_json(&e))?);
return Ok(ExitCode::FAILURE);
}
out => out?,
};
if !quiet {
println!("{}", render(&out.value));
}
Ok(ExitCode::SUCCESS)
}
Cmd::Fmt { file, check, write } => {
if !check && !write && file.len() != 1 {
return Err(CliError::Fmt(
"`blue fmt` prints ONE file; name several with --check or --write".into(),
));
}
let mut failed = false;
for file in &file {
let src = read(file)?;
let formatted = match blue_lang_pkg::canonical::format_resolved(file, &src) {
Ok(f) => f,
Err(e) if check || write => {
eprintln!("blue: {}: {e}", file.display());
failed = true;
continue;
}
Err(e) => return Err(CliError::Fmt(e.to_string())),
};
if check {
if formatted.trim_end() != src.trim_end() {
eprintln!(
"{}: not formatted{}; run blue fmt --write {}",
file.display(),
blue_lang_pkg::canonical::rules_named(&src),
file.display()
);
failed = true;
}
} else if write {
std::fs::write(file, &formatted).map_err(|source| CliError::Write {
path: file.display().to_string(),
source,
})?;
} else {
print!("{formatted}");
}
}
Ok(if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
})
}
Cmd::Ast {
file,
resolved: false,
..
} => {
for form in parse(&read(&file)?, &cfg)? {
println!("{form}");
}
Ok(ExitCode::SUCCESS)
}
Cmd::Ast { file, json, .. } => {
use blue_lang_runtime::pipeline::{check_entry, Checking};
let loader = blue_lang_pkg::load_path::LoadPath::for_entry(&file);
let src = read_compiled(&file)?;
let checked = check_entry(
blue_lang_runtime::uses::Entry {
path: Some(&file),
text: &src,
},
&loader,
None,
Checking::WithTests,
)?;
let resolved = checked.resolve();
if json {
println!("{}", diagnostics::resolved_json(&checked, &resolved)?);
} else {
let tree = resolved.resolved_tree(
checked.program.forms(),
blue_lang_check::names::Rule::Namespaced,
);
for (i, form) in tree.iter().enumerate() {
if checked.program.owner_of(i)
== Some(blue_lang_runtime::uses::ResolvedProgram::ENTRY)
{
println!("{}", form.to_sexp());
}
}
}
Ok(ExitCode::SUCCESS)
}
Cmd::Erase { file } => {
let forms = parse_tree(&read(&file)?, &cfg)?;
for form in blue_lang_runtime::to_sexps(&blue_lang_runtime::erase_types(&forms)) {
println!("{form}");
}
Ok(ExitCode::SUCCESS)
}
Cmd::Check { file, format, fix } => check(&file, format, fix),
Cmd::Migrate { files } => {
blue_lang_runtime::sys::set_program_args(files);
blue_lang_runtime::pipeline::run_in_surface(
blue_lang_runtime::uses::Entry {
path: Some(Path::new("crates/blue-lang-cli/blue/migrate.b")),
text: MIGRATE,
},
blue_lang_runtime::inputs::Inputs::new(),
&blue_lang_runtime::uses::NoLoader,
None,
)?;
Ok(ExitCode::SUCCESS)
}
Cmd::Doc {
name,
file,
json,
rows,
} => {
if rows {
println!("{}", doc::rows_json()?);
return Ok(ExitCode::SUCCESS);
}
let mut args: Vec<String> = name.into_iter().collect();
if let Some(f) = file {
args.push("--file".into());
args.push(f.display().to_string());
}
if json {
args.push("--json".into());
}
blue_lang_runtime::sys::set_program_args(args);
let out = blue_lang_runtime::pipeline::run_in_surface(
blue_lang_runtime::uses::Entry {
path: Some(Path::new("crates/blue-lang-cli/blue/doc.b")),
text: doc::PROGRAM,
},
blue_lang_runtime::inputs::Inputs::new(),
&blue_lang_pkg::load_path::LoadPath::from_env().with_standard(),
None,
)?;
Ok(
if matches!(out.value, tatara_lisp_eval::Value::Bool(false)) {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
},
)
}
Cmd::Verbs { args } => {
blue_lang_runtime::sys::set_program_args(args);
let out = blue_lang_runtime::pipeline::run_in_surface(
blue_lang_runtime::uses::Entry {
path: Some(Path::new("crates/blue-lang-cli/blue/verbs.b")),
text: VERBS,
},
blue_lang_runtime::inputs::Inputs::new(),
&blue_lang_pkg::load_path::LoadPath::from_env().with_standard(),
None,
)?;
Ok(
if matches!(out.value, tatara_lisp_eval::Value::Bool(false)) {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
},
)
}
Cmd::ExplainName { name, at } => {
let (path, line) = at
.rsplit_once(':')
.and_then(|(p, l)| l.parse::<usize>().ok().map(|l| (PathBuf::from(p), l)))
.ok_or_else(|| CliError::Pkg(format!("expected FILE:LINE, got `{at}`")))?;
let src = read_compiled(&path)?;
let checked = blue_lang_runtime::pipeline::check_entry(
blue_lang_runtime::uses::Entry {
path: Some(&path),
text: &src,
},
&blue_lang_pkg::load_path::LoadPath::for_entry(&path),
None,
blue_lang_runtime::pipeline::Checking::WithTests,
)?;
let program = &checked.program;
let entry = |i: usize| {
program.owner_of(i) == Some(blue_lang_runtime::uses::ResolvedProgram::ENTRY)
};
let form = (0..program.forms().len())
.filter(|i| entry(*i))
.filter(|i| {
blue_lang_syntax::Span::line_col(&src, program.forms()[*i].span.start).0 <= line
})
.last()
.ok_or_else(|| {
CliError::Pkg(format!("no top-level form at or before line {line}"))
})?;
let own = blue_lang_runtime::pipeline::namespace_of(program, form);
println!("`{name}` at {}:{line}, in {own}:", path.display());
println!(" local a parameter or local of that name in scope at the line wins over everything below");
let mut won = false;
for (tier, nss) in checked.names.tiers_of(&name, &own, form) {
let word = format!("{tier:?}").to_lowercase();
let held = if nss.is_empty() {
"-".to_string()
} else {
nss.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
};
let verdict = match (nss.len(), won) {
(0, _) => "",
(1, false) => " <- wins",
(_, false) => " <- ambiguous (B0009)",
(_, true) => " (shadowed)",
};
if !nss.is_empty() {
won = true;
}
println!(" {word:<9} {held}{verdict}");
}
if !won {
println!(" unbound (B0001, or B0012 when a used bidama defines it)");
}
Ok(ExitCode::SUCCESS)
}
Cmd::Census { root, findings } => {
let c = census::take(&root).map_err(CliError::Pkg)?;
println!("{} file(s)", c.files);
for r in blue_lang_check::RULES
.iter()
.filter(|r| r.ratchet.is_some())
{
let measured = c
.findings
.get(&r.code)
.map_or(0, std::collections::BTreeSet::len);
println!(
"{} {:<24} measured {measured:>5} ratchet {:>5}",
r.code,
r.slug,
r.ratchet.unwrap_or(0)
);
if findings {
for f in c.findings.get(&r.code).into_iter().flatten() {
println!(" {}", f.lines().next().unwrap_or(""));
}
}
}
let bad = c.mismatches();
for (code, measured, ratchet) in &bad {
eprintln!(
"blue census: {code} measured {measured}, ratchet {ratchet}: edit the row's ratchet to {measured} if the change is the intended one"
);
}
Ok(if bad.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
Cmd::Explain { code, list } => {
if list {
for r in blue_lang_check::RULES {
println!(
"{} {:<8} {:<22} {}",
r.code,
r.severity.label(),
r.slug,
r.law
);
}
return Ok(ExitCode::SUCCESS);
}
let code = code.unwrap_or_default();
let c = blue_lang_check::Code::parse(&code).ok_or(CliError::UnknownCode(code))?;
print!("{}", blue_lang_check::Explain(c.rule()));
Ok(ExitCode::SUCCESS)
}
Cmd::Test {
file,
filter,
coverage,
json,
} => observe::test(&file, filter.as_deref(), coverage, json),
Cmd::Profile {
file,
top,
json,
args,
} => observe::profile(&file, top, json, args, &cfg),
Cmd::Trace {
def,
file,
limit,
args,
} => observe::trace(&def, &file, limit, args, &cfg),
Cmd::Deps { file } => {
let manifest = blue_lang_pkg::read_bluefile(&read(&file)?)
.map_err(|e| CliError::Pkg(e.to_string()))?;
println!("{} {}", manifest.name, manifest.version);
if manifest.manifest.needs.is_empty() {
println!(" (no dependencies)");
return Ok(ExitCode::SUCCESS);
}
for (dep, range) in &manifest.manifest.needs {
println!(" needs {dep} {range}");
}
let Some(registry) = scan_registry()? else {
println!("\nno distribution on BLUE_PATH; nothing to resolve against");
return Ok(ExitCode::SUCCESS);
};
let mut solver =
blue_lang_pkg::Solver::new(®istry).with_max_steps(cfg.solver_max_steps);
let resolution = solver
.solve(&manifest.manifest)
.map_err(|e| CliError::Pkg(e.to_string()))?;
println!("\nresolved against {} package(s):", registry.len());
for (name, version) in &resolution.picks {
println!(" {name} {version}");
}
println!(
" ({} step(s), {} skipped by learning)",
solver.steps_taken(),
solver.skipped_by_learning()
);
Ok(ExitCode::SUCCESS)
}
Cmd::Posture { file } => {
let manifest = blue_lang_pkg::read_bluefile(&read(&file)?)
.map_err(|e| CliError::Pkg(e.to_string()))?;
let floor = &manifest.floor;
println!("{} {}", manifest.name, manifest.version);
println!(" when: {:?}", floor.when);
println!(" where: {:?}", floor.place);
println!(" reach: {}", describe_reach(&floor.reach));
let grants = blue_lang_bidama::qualities_at(floor);
let lost = blue_lang_bidama::forfeits_at(floor);
let names = |qs: &std::collections::BTreeSet<blue_lang_bidama::Quality>| {
qs.iter().map(|q| q.label()).collect::<Vec<_>>().join(", ")
};
println!("\n grants: {}", names(&grants));
println!(" forfeits: {}", names(&lost));
Ok(ExitCode::SUCCESS)
}
Cmd::Bluefile {
json,
confirm: _,
files,
} => {
if json {
let [file] = files.as_slice() else {
return Err(CliError::JsonTakesOne { got: files.len() });
};
let bluefile = blue_lang_pkg::read_bluefile(&read(file)?)?;
print!(
"{}",
blue_lang_pkg::lock::render(&ManifestRecord::of(&bluefile))?
);
return Ok(ExitCode::SUCCESS);
}
let mut all_fresh = true;
for file in &files {
let src = read(file)?;
let lock_path = file.with_file_name(LOCK_FILE);
let lock_text = match std::fs::read_to_string(&lock_path) {
Ok(text) => Some(text),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(source) => {
return Err(CliError::Read {
path: lock_path.display().to_string(),
source,
})
}
};
let verdict = blue_lang_pkg::lock::confirm(&src, lock_text.as_deref())?;
println!(
"{}",
serde_json::to_string(&Confirmation {
bluefile: file.display().to_string(),
verdict: &verdict,
})?
);
if let Freshness::Stale { reason } = &verdict {
let dir = file.parent().filter(|p| !p.as_os_str().is_empty());
eprintln!(
"blue: {}: {reason}; run `blue lock {}`",
file.display(),
dir.unwrap_or(Path::new(".")).display()
);
all_fresh = false;
}
}
Ok(if all_fresh {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
Cmd::Lock { dirs } => {
let dirs = if dirs.is_empty() {
vec![PathBuf::from(".")]
} else {
dirs
};
let mut locks = Vec::with_capacity(dirs.len());
for dir in &dirs {
let src = read(&dir.join(blue_lang_pkg::MANIFEST_FILE))?;
let lock = blue_lang_pkg::lock::lock(&src, &prefetch::NixPrefetch)?;
locks.push((dir.join(LOCK_FILE), lock));
}
for (path, lock) in &locks {
std::fs::write(path, blue_lang_pkg::lock::render(lock)?).map_err(|source| {
CliError::Write {
path: path.display().to_string(),
source,
}
})?;
println!(
"{}: {} source(s) pinned",
path.display(),
lock.sources.len()
);
}
Ok(ExitCode::SUCCESS)
}
Cmd::Shift { file } => {
let reading = blue_lang_lsp::shift_of(&read(&file)?);
println!("{}", reading.summary());
if let Some(rung) = reading.rung {
println!("{}", rung.meaning());
}
if reading.factors.is_empty() {
return Ok(ExitCode::SUCCESS);
}
println!();
for f in &reading.factors {
let arrow = if f.kind.shifts_forward() { "→" } else { "·" };
println!(" {arrow} {:<24} {}", f.kind.label(), f.detail);
}
Ok(ExitCode::SUCCESS)
}
Cmd::Image { cmd } => Ok(deploy::image(cmd)?),
Cmd::Workload { cmd } => Ok(deploy::workload(cmd)?),
Cmd::Diff(args) => Ok(deploy::diff(args)),
Cmd::Reference => {
println!("{}", serde_json::to_string_pretty(&reference::reference())?);
Ok(ExitCode::SUCCESS)
}
Cmd::Morph => {
use blue_lang_bidama::{
enforcement, exclusive_pairs, minimal_exclusive_groups, qualities_at, shapes,
Quality,
};
println!("QUALITIES — where each is enforced\n");
for q in Quality::ALL {
let (layer, why) = enforcement(q);
let mark = if layer.is_enforced() { "✓" } else { "·" };
println!(" {mark} {:<30} {}", q.label(), layer.label());
println!(" {why}");
}
println!("\nMUTUALLY EXCLUSIVE — derived by enumerating the lattice\n");
for (a, b) in exclusive_pairs() {
println!(
" {:<30} ⊥ {:<30} (both on the {:?} axis)",
a.label(),
b.label(),
a.axis()
);
}
println!(
"\n Every exclusive pair shares an axis. Two qualities on DIFFERENT\n coordinates always have a posture granting both — which is what a\n per-package posture buys over one global choice."
);
let groups: Vec<Vec<Quality>> = minimal_exclusive_groups()
.into_iter()
.filter(|g| g.len() > 2)
.collect();
if !groups.is_empty() {
println!("\nPICK ANY TWO — minimal exclusive groups past a pair\n");
for g in &groups {
let names: Vec<&str> = g.iter().map(|q| q.label()).collect();
println!(
" {} (all on the {:?} axis)",
names.join(" + "),
g[0].axis()
);
}
println!(
"\n No pair inside a group is exclusive — only the whole group is.\n \
Dropping any one member names a posture that grants the rest."
);
}
println!("\nSHAPES — a language is a point; blue is the space\n");
for s in shapes() {
let q = qualities_at(&s.posture);
println!(" {:<22} {}", s.name, s.because);
let names: Vec<&str> = q.iter().map(|x| x.label()).collect();
println!(" grants: {}", names.join(", "));
}
Ok(ExitCode::SUCCESS)
}
Cmd::Banner => {
for line in blue_lang_art::wordmark(env!("CARGO_PKG_VERSION")) {
println!("{}", line.render());
}
Ok(ExitCode::SUCCESS)
}
Cmd::Config(cmd) => {
cmd.run::<BlueConfig>(config::TIER_ENV)?;
Ok(ExitCode::SUCCESS)
}
Cmd::New {
name,
bidama,
script,
} => {
let shape = match (bidama, script) {
(true, _) => scaffold::Shape::Bidama,
(_, true) => scaffold::Shape::Script,
_ => scaffold::Shape::Program,
};
println!("{}", scaffold::new(&name, shape).map_err(CliError::Pkg)?);
Ok(ExitCode::SUCCESS)
}
Cmd::Watch { targets, args } => {
let (verb, paths) = watch::parse(&targets).map_err(CliError::Pkg)?;
watch::watch(verb, &paths, &args).map_err(CliError::Pkg)
}
Cmd::Serve(session) => repl::serve(&session).map_err(CliError::Pkg),
Cmd::Repl(session) => repl::repl(&session).map_err(CliError::Pkg),
Cmd::Eval { target, session } => repl::eval(&target, &session).map_err(CliError::Pkg),
Cmd::Lsp => {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
blue_lang_lsp::Server::with_loader(Box::new(
blue_lang_pkg::load_path::LoadPath::from_env().with_standard(),
))
.serve(stdin.lock(), stdout.lock())
.map_err(|source| CliError::Write {
path: "<stdio>".to_string(),
source,
})?;
Ok(ExitCode::SUCCESS)
}
}
}
fn check(file: &Path, format: Format, fix: bool) -> Result<ExitCode, CliError> {
use blue_lang_runtime::pipeline::{check_entry, render, syntax_diagnostic, Checking};
let raw = read(file)?;
if let Some(d) = syntax_diagnostic(&raw) {
match format {
Format::Json => print!("{}", diagnostics::syntax_json(file, &raw, &d)?),
Format::Text => {
let (line, col) = blue_lang_syntax::Span::line_col(&raw, d.span.start);
eprintln!(
"{} {d}",
Located {
file: file.display().to_string(),
line,
col,
}
);
}
}
return Ok(ExitCode::FAILURE);
}
let loader = blue_lang_pkg::load_path::LoadPath::for_entry(file);
let mut src = if fix {
blue_lang_pkg::canonical::read_layout(file)?
} else {
std::fs::read_to_string(file).map_err(|source| CliError::Read {
path: file.display().to_string(),
source,
})?
};
fn entry<'a>(file: &'a Path, text: &'a str) -> blue_lang_runtime::uses::Entry<'a> {
blue_lang_runtime::uses::Entry {
path: Some(file),
text,
}
}
let rules =
blue_lang_pkg::project_rules::discover(file).map_err(|e| CliError::Pkg(e.to_string()))?;
let project_pass = |checked: &blue_lang_runtime::pipeline::Checked| {
blue_lang_pkg::project_rules::check(
&checked.program,
&checked.call_explicit_forms(),
&rules,
&loader,
blue_lang_runtime::project_rules::RULE_STEPS,
)
};
let mut checked = check_entry(entry(file, &src), &loader, None, Checking::WithTests)?;
let mut project = project_pass(&checked);
for _ in 0..if fix { 4 } else { 0 } {
let (fixed, applied) =
diagnostics::apply_machine_fixes(&checked.program, &checked.outcome, &project, &src);
if applied == 0 {
break;
}
{
std::fs::write(file, &fixed).map_err(|source| CliError::Write {
path: file.display().to_string(),
source,
})?;
eprintln!("blue: applied {applied} fix(es) to {}", file.display());
src = blue_lang_pkg::canonical::read_layout(file)?;
checked = check_entry(entry(file, &src), &loader, None, Checking::WithTests)?;
project = project_pass(&checked);
}
}
if fix {
if let Ok(canonical) = blue_lang_pkg::canonical::format_resolved(file, &src) {
if canonical.trim_end() != src.trim_end() {
std::fs::write(file, &canonical).map_err(|source| CliError::Write {
path: file.display().to_string(),
source,
})?;
eprintln!("blue: formatted {}", file.display());
src = canonical;
checked = check_entry(entry(file, &src), &loader, None, Checking::WithTests)?;
project = project_pass(&checked);
}
}
}
let mut with_form = checked.outcome.clone();
let entry_form = (0..checked.program.forms().len())
.find(|i| {
checked.program.owner_of(*i) == Some(blue_lang_runtime::uses::ResolvedProgram::ENTRY)
})
.unwrap_or(0);
with_form.diagnostics.extend(
blue_lang_runtime::pipeline::form_diagnostics(&src)
.into_iter()
.map(|d| d.at_top_level(entry_form)),
);
let outcome = &with_form;
match format {
Format::Json => {
print!("{}", diagnostics::json_lines(&checked.program, outcome)?);
print!(
"{}",
diagnostics::overrides_json(&checked, &checked.resolve())?
);
print!(
"{}",
diagnostics::project_json_lines(&checked.program, &project)?
);
}
Format::Text => {
println!("typed declarations: {}", outcome.stats.typed_decls);
println!("nodes analysed: {}", outcome.stats.visited);
println!("seams: {}", outcome.seams.len());
println!("names resolved: {}", outcome.stats.names_resolved);
println!(
"waived: {}",
outcome.waived.len() + project.waived.len()
);
if !rules.is_empty() {
println!("project rules: {}", rules.len());
}
for seam in &outcome.seams {
let (line, col) = blue_lang_syntax::Span::line_col(&src, seam.span.start);
println!(
" {} seam at {} expects {:?}",
Located {
file: file.display().to_string(),
line,
col,
},
seam.at,
seam.expected
);
}
for w in &outcome.waived {
println!(
" {}",
render(
&checked.program,
&blue_lang_check::Diagnostic::new(
w.diagnostic.code,
format!("waived: {} ({})", w.diagnostic.message, w.waiver.reason),
w.diagnostic.span,
)
.at_top_level(w.diagnostic.top_level)
)
);
}
for w in &project.waived {
let mut shown = w.finding.clone();
shown.message = format!("waived: {} ({})", shown.message, w.waiver.reason);
shown.help = None;
println!(
" {}",
diagnostics::render_finding(&checked.program, &shown)
);
}
for d in outcome.diagnostics.iter().chain(&project.diagnostics) {
eprintln!("{}", render(&checked.program, d));
}
for f in &project.findings {
eprintln!("{}", diagnostics::render_finding(&checked.program, f));
}
for f in &project.failures {
eprintln!("{}", diagnostics::render_failure(&checked.program, f));
}
}
}
Ok(if outcome.ok() && project.ok() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
fn bind_inputs(
src: &str,
pairs: &[String],
cfg: &BlueConfig,
) -> Result<blue_lang_runtime::Inputs, CliError> {
let forms = parse(src, cfg)?;
let declared = blue_lang_runtime::declarations(&forms);
let mut inputs = blue_lang_runtime::Inputs::new();
for pair in pairs {
let (name, path) = pair.split_once('=').ok_or_else(|| {
CliError::Pkg("--input expects NAME=PATH, e.g. --input schema=./schema.json".into())
})?;
let decl = declared.iter().find(|d| d.name == name).ok_or_else(|| {
CliError::Pkg(
"`".to_string()
+ name
+ "` was supplied but the program never declares it. Add definput(\"" + name + "\", \"b3:…\").",
)
})?;
let bytes = std::fs::read(path).map_err(|source| CliError::Read {
path: path.to_string(),
source,
})?;
inputs
.bind(decl, bytes)
.map_err(|e| CliError::Pkg(e.to_string()))?;
}
if let Some(missing) = declared.iter().find(|d| inputs.get(&d.name).is_none()) {
return Err(CliError::Pkg(
"input `".to_string()
+ &missing.name
+ "` is declared but no bytes were supplied — pass --input "
+ &missing.name
+ "=<path>",
));
}
Ok(inputs)
}
fn describe_reach(r: &blue_lang_waku::Reach) -> String {
match r {
blue_lang_waku::Reach::Unrestricted => "unrestricted".to_string(),
blue_lang_waku::Reach::Only(caps) => {
let list = caps
.iter()
.map(|c| c.label())
.collect::<Vec<_>>()
.join(", ");
let mut out = String::with_capacity(list.len() + 6);
out.push_str("only ");
out.push_str(&list);
out
}
}
}
fn render(v: &tatara_lisp_eval::Value) -> String {
use tatara_lisp_eval::Value;
match v {
Value::Nil => "nil".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(x) => x.to_string(),
Value::Str(s) => s.to_string(),
Value::Symbol(s) => s.to_string(),
Value::Keyword(k) => {
let mut out = String::with_capacity(k.len() + 1);
out.push(':');
out.push_str(k);
out
}
Value::List(items) => {
let inner = items.iter().map(render).collect::<Vec<_>>().join(", ");
let mut out = String::with_capacity(inner.len() + 2);
out.push('[');
out.push_str(&inner);
out.push(']');
out
}
other => format!("{other:?}"),
}
}
fn resolve_surface() -> Result<Option<blue_lang_syntax::yakugo::Yakugo>, String> {
if let Ok(tag) = std::env::var("BLUE_LANG") {
if tag.is_empty() {
return Ok(None);
}
return match blue_lang_syntax::yakugo::pack_for_locale(&tag)? {
Some(p) => Ok(Some(p)),
None => Err(format!(
"BLUE_LANG=\"{tag}\" names no surface. Available: {}",
blue_lang_syntax::yakugo::BUILTIN_PACKS
.iter()
.map(|(t, _)| *t)
.collect::<Vec<_>>()
.join(", ")
)),
};
}
for var in ["LC_ALL", "LC_MESSAGES", "LANG"] {
if let Ok(loc) = std::env::var(var) {
if let Some(p) = blue_lang_syntax::yakugo::pack_for_locale(&loc)? {
return Ok(Some(p));
}
}
}
Ok(None)
}