mod config;
mod prefetch;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use blue_lang_pkg::lock::{Freshness, ManifestRecord, LOCK_FILE};
use clap::{Parser, Subcommand};
use config::BlueConfig;
#[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>,
},
Fmt {
file: PathBuf,
#[arg(long)]
check: bool,
#[arg(long)]
write: bool,
},
Ast { file: PathBuf },
Erase { file: PathBuf },
Check { file: PathBuf },
Test { file: PathBuf },
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,
Banner,
Shift { file: PathBuf },
Morph,
Config(shikumi::cli::ConfigShowCommand),
}
fn main() -> ExitCode {
match dispatch(Cli::parse()) {
Ok(code) => code,
Err(e) => {
eprintln!("blue: {e}");
ExitCode::FAILURE
}
}
}
#[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}")]
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("`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(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();
match cli.cmd {
Cmd::Run { file, inputs } => {
let src = read(&file)?;
let loader = blue_lang_pkg::load_path::LoadPath::from_env();
let surface = resolve_surface().map_err(CliError::Pkg)?;
let out = 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(),
)?;
println!("{}", render(&out.value));
Ok(ExitCode::SUCCESS)
}
Cmd::Fmt { file, check, write } => {
let src = read(&file)?;
let formatted = blue_lang_fmt::format_source_lossless(&src)
.map_err(|e| CliError::Fmt(e.to_string()))?;
if check {
if formatted.trim_end() == src.trim_end() {
return Ok(ExitCode::SUCCESS);
}
eprintln!("{}: not formatted", file.display());
return Ok(ExitCode::FAILURE);
}
if write {
let formatted = blue_lang_fmt::format_source_lossless(&src)
.map_err(|e| CliError::Fmt(e.to_string()))?;
std::fs::write(&file, &formatted).map_err(|source| CliError::Write {
path: file.display().to_string(),
source,
})?;
} else {
print!("{formatted}");
}
Ok(ExitCode::SUCCESS)
}
Cmd::Ast { file } => {
for form in parse(&read(&file)?, &cfg)? {
println!("{form}");
}
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 } => {
let src = read(&file)?;
let forms = parse_tree(&src, &cfg)?;
let outcome = blue_lang_check::check_program(&forms);
println!("typed declarations: {}", outcome.stats.typed_decls);
println!("nodes analysed: {}", outcome.stats.visited);
println!("seams: {}", outcome.seams.len());
let at = |span: blue_lang_syntax::Span| {
let (line, col) = blue_lang_syntax::Span::line_col(&src, span.start);
Located {
file: file.display().to_string(),
line,
col,
}
};
for seam in &outcome.seams {
println!(
" {} seam at {} expects {:?}",
at(seam.span),
seam.at,
seam.expected
);
}
for d in &outcome.diagnostics {
eprintln!("{} error: {d}", at(d.span));
}
Ok(if outcome.ok() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
Cmd::Test { file } => {
let src = read(&file)?;
let forms = parse_tree(&src, &cfg)?;
let program = blue_lang_runtime::uses::resolve_uses(
forms,
blue_lang_runtime::uses::Entry {
path: Some(&file),
text: &src,
},
&blue_lang_pkg::load_path::LoadPath::from_env(),
)
.map_err(blue_lang_runtime::pipeline::RunError::Import)?;
let report = blue_lang_test::run(&program.sexps());
for failure in &report.failures {
eprintln!("{failure}");
}
println!(
"{} test(s): {} passed, {} failed",
report.total(),
report.passed,
report.failures.len()
);
if report.total() == 0 {
eprintln!(
"{}: no `test` blocks, so nothing was tested",
file.display()
);
return Ok(ExitCode::FAILURE);
}
Ok(if report.ok() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
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::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::Lsp => {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
blue_lang_lsp::Server::new()
.serve(stdin.lock(), stdout.lock())
.map_err(|source| CliError::Write {
path: "<stdio>".to_string(),
source,
})?;
Ok(ExitCode::SUCCESS)
}
}
}
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)
}