use std::path::{Path, PathBuf};
use std::process::ExitCode;
use cairn_lang_core::CAIRN_VERSION;
use cairn_lang_core::block_array::{BlockArray, BlockArrayIr, lower_to_block_array};
use cairn_lang_core::check::LineStarts;
use cairn_lang_core::lock::{
HashHex, LockEdition, LockInputs, LockPlacement, LockTarget, LockWalkway, Lockfile,
hash_resolved_ir, hash_source,
};
use cairn_lang_core::resolve::{EditionPortability, VersionAxes, compute_axes, resolve};
use cairn_lang_core::{Edition, Severity, check, lower, parse};
use cairn_lang_formats::bedrock_structure::{ParityNote, build_mcstructure_tag, write_mcstructure};
use cairn_lang_formats::data_version::{
BedrockTarget, JavaTarget, resolve_bedrock_target, resolve_java_target,
};
use cairn_lang_formats::java_structure::{
Compound, OutputExt, build_structure_tag, output_filename, write_compound_gzip,
};
use cairn_lang_formats::portability::{portability_for_bedrock, portability_for_java};
use cairn_lang_formats::registry::{RegistryPack, builtin_bedrock, builtin_java};
use cairn_lang_redstone::{
PlacementStage, compile_crossing, compile_delay, compile_edition_netlist, compile_netlist,
compile_placement, compile_routing, synthesize,
};
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(
name = "cairn",
version = CAIRN_VERSION,
about = "Compile .crn build descriptions to Minecraft NBT and back",
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Parse {
file: PathBuf,
#[arg(long, value_enum, default_value_t = Format::Json)]
format: Format,
},
Check {
file: PathBuf,
#[arg(long, value_enum)]
edition: Option<EditionArg>,
#[arg(long, value_enum, default_value_t = CheckFormat::Text)]
format: CheckFormat,
},
Info {
file: PathBuf,
#[arg(long, value_delimiter = ',', default_values_t = vec!["java".to_owned(), "bedrock".to_owned()])]
editions: Vec<String>,
#[arg(long, value_enum, default_value_t = InfoFormat::Text)]
format: InfoFormat,
},
Lower {
file: PathBuf,
#[arg(long, value_enum, default_value_t = LowerFormat::Ascii)]
format: LowerFormat,
},
Compile {
file: PathBuf,
#[arg(long, value_enum)]
edition: EditionArg,
#[arg(long, default_value = "latest")]
target: String,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
lock: Option<PathBuf>,
},
Synth {
file: PathBuf,
#[arg(long)]
experimental_logic_synth: bool,
#[arg(long, value_enum, default_value_t = SynthStage::Logic)]
stage: SynthStage,
#[arg(long, value_enum)]
edition: Option<EditionArg>,
},
}
#[derive(Copy, Clone, ValueEnum)]
enum SynthStage {
Logic,
Netlist,
Edition,
Placement,
Route,
Delay,
Crossing,
}
#[derive(Copy, Clone, ValueEnum)]
enum Format {
Json,
Debug,
}
#[derive(Copy, Clone, ValueEnum)]
enum CheckFormat {
Text,
Json,
}
#[derive(Copy, Clone, ValueEnum)]
enum InfoFormat {
Text,
Json,
}
#[derive(Copy, Clone, ValueEnum)]
enum EditionArg {
Java,
Bedrock,
}
impl EditionArg {
fn as_lock_edition(self) -> LockEdition {
match self {
EditionArg::Java => LockEdition::Java,
EditionArg::Bedrock => LockEdition::Bedrock,
}
}
fn as_edition(self) -> Edition {
match self {
EditionArg::Java => Edition::Java,
EditionArg::Bedrock => Edition::Bedrock,
}
}
}
#[derive(Copy, Clone, ValueEnum)]
enum LowerFormat {
Ascii,
Json,
Debug,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Some(Command::Parse { file, format }) => run_parse(&file, format),
Some(Command::Check {
file,
edition,
format,
}) => run_check(&file, edition, format),
Some(Command::Info {
file,
editions,
format,
}) => run_info(&file, &editions, format),
Some(Command::Lower { file, format }) => run_lower(&file, format),
Some(Command::Synth {
file,
experimental_logic_synth,
stage,
edition,
}) => run_synth(&file, experimental_logic_synth, stage, edition),
Some(Command::Compile {
file,
edition,
target,
out,
lock,
}) => run_compile(&file, edition, &target, out.as_deref(), lock.as_deref()),
None => {
eprintln!("error: a subcommand is required (try `cairn --help`)");
ExitCode::from(2)
}
}
}
fn run_parse(file: &Path, format: Format) -> ExitCode {
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(err) => {
eprintln!("error: cannot read `{}`: {err}", file.display());
return match err.kind() {
std::io::ErrorKind::NotFound => ExitCode::from(2),
_ => ExitCode::from(1),
};
}
};
let module = match parse(&source) {
Ok(m) => m,
Err(err) => {
let position = err.position();
eprintln!(
"error: {}:{}: {}",
file.display(),
position,
err.user_message(),
);
return ExitCode::from(1);
}
};
match format {
Format::Json => match serde_json::to_string_pretty(&module) {
Ok(json) => {
println!("{json}");
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("error: failed to serialise AST as JSON: {err}");
ExitCode::from(1)
}
},
Format::Debug => {
println!("{module:#?}");
ExitCode::SUCCESS
}
}
}
fn run_check(file: &Path, edition: Option<EditionArg>, format: CheckFormat) -> ExitCode {
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(err) => {
eprintln!("error: cannot read `{}`: {err}", file.display());
return match err.kind() {
std::io::ErrorKind::NotFound => ExitCode::from(2),
_ => ExitCode::from(1),
};
}
};
let module = match parse(&source) {
Ok(m) => m,
Err(err) => {
eprintln!(
"error: {}:{}: {}",
file.display(),
err.position(),
err.user_message(),
);
return ExitCode::from(1);
}
};
let ir = lower(&module);
let diagnostics = check(&module, &ir, edition.map(EditionArg::as_edition));
let has_error = diagnostics.iter().any(|d| d.severity == Severity::Error);
let lines = LineStarts::new(&source);
match format {
CheckFormat::Text => {
for d in &diagnostics {
let pos = lines.position(&source, d.span.start);
println!(
"{}:{}: {}[{}]: {}",
file.display(),
pos,
d.severity.as_str(),
d.code.as_str(),
d.primary,
);
for note in &d.notes {
if let Some(span) = note.span.as_ref() {
let note_pos = lines.position(&source, span.start);
println!("{}:{}: note: {}", file.display(), note_pos, note.message);
} else {
println!(" note: {}", note.message);
}
}
}
}
CheckFormat::Json => {
let rendered: Vec<_> = diagnostics
.iter()
.map(|d| d.render(&source, &lines))
.collect();
match serde_json::to_string_pretty(&rendered) {
Ok(json) => println!("{json}"),
Err(err) => {
eprintln!("error: failed to serialise diagnostics as JSON: {err}");
return ExitCode::from(1);
}
}
}
}
if has_error {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn run_info(file: &Path, editions: &[String], format: InfoFormat) -> ExitCode {
if editions.iter().any(|e| e.trim().is_empty()) {
eprintln!("error: --editions value must not contain empty entries");
return ExitCode::from(2);
}
for e in editions {
if let Err(err) = e.parse::<Edition>() {
eprintln!("error: {err}");
return ExitCode::from(2);
}
}
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(err) => {
eprintln!("error: cannot read `{}`: {err}", file.display());
return match err.kind() {
std::io::ErrorKind::NotFound => ExitCode::from(2),
_ => ExitCode::from(1),
};
}
};
let module = match parse(&source) {
Ok(m) => m,
Err(err) => {
eprintln!(
"error: {}:{}: {}",
file.display(),
err.position(),
err.user_message(),
);
return ExitCode::from(1);
}
};
let ir = lower(&module);
let resolution = resolve(&ir, None);
let mut block_ir = lower_to_block_array(&ir, &resolution, Some(&builtin_java().materials));
let mut combined = resolution.diagnostics.clone();
combined.append(&mut block_ir.diagnostics);
let lines = LineStarts::new(&source);
let mut has_error = false;
for d in &combined {
let pos = lines.position(&source, d.span.start);
eprintln!(
"{}:{}: {}[{}]: {}",
file.display(),
pos,
d.severity.as_str(),
d.code.as_str(),
d.primary,
);
for note in &d.notes {
eprintln!(" note: {}", note.message);
}
if d.severity == Severity::Error {
has_error = true;
}
}
if has_error {
return ExitCode::from(1);
}
let mut per_edition: Vec<EditionPortability> = Vec::with_capacity(editions.len());
for e in editions {
let edition: Edition = e.parse().expect("validated above");
let per_edition_resolution = resolve(&ir, Some(edition));
let materials = match edition {
Edition::Java => &builtin_java().materials,
Edition::Bedrock => &builtin_bedrock().materials,
};
let per_block_ir = lower_to_block_array(&ir, &per_edition_resolution, Some(materials));
let counts = match edition {
Edition::Java => portability_for_java(&per_block_ir),
Edition::Bedrock => portability_for_bedrock(&per_block_ir),
};
per_edition.push(EditionPortability {
edition,
portable: counts.portable,
degraded: counts.degraded,
unsupported: counts.unsupported,
});
}
let axes = compute_axes(&module, &ir, &resolution, per_edition);
match format {
InfoFormat::Text => {
print_text(&axes);
ExitCode::SUCCESS
}
InfoFormat::Json => match serde_json::to_string_pretty(&axes) {
Ok(json) => {
println!("{json}");
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("error: failed to serialise version axes as JSON: {err}");
ExitCode::from(1)
}
},
}
}
fn print_text(axes: &VersionAxes) {
println!(
"registry compatibility: {} .. {}",
axes.registry_compat.min, axes.registry_compat.max,
);
let portability_line = if axes.edition_portability.is_empty() {
String::from("(no editions requested)")
} else {
axes.edition_portability
.iter()
.map(|ep| {
format!(
"{}: portable: {} degraded: {} unsupported: {}",
capitalise(ep.edition.as_str()),
ep.portable,
ep.degraded,
ep.unsupported,
)
})
.collect::<Vec<_>>()
.join(" ")
};
println!("edition portability: {portability_line}");
let semantic_line = if axes.semantic_sensitive.is_empty() {
String::from("(none)")
} else {
axes.semantic_sensitive
.iter()
.map(|f| format!("{}({} @{})", f.member, f.reason, f.boundary_version))
.collect::<Vec<_>>()
.join(", ")
};
println!("semantic-sensitive: {semantic_line}");
}
fn capitalise(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn run_lower(file: &Path, format: LowerFormat) -> ExitCode {
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(err) => {
eprintln!("error: cannot read `{}`: {err}", file.display());
return match err.kind() {
std::io::ErrorKind::NotFound => ExitCode::from(2),
_ => ExitCode::from(1),
};
}
};
let module = match parse(&source) {
Ok(m) => m,
Err(err) => {
eprintln!(
"error: {}:{}: {}",
file.display(),
err.position(),
err.user_message(),
);
return ExitCode::from(1);
}
};
let ir = lower(&module);
let resolution = resolve(&ir, None);
let mut block_ir = lower_to_block_array(&ir, &resolution, Some(&builtin_java().materials));
let mut combined = resolution.diagnostics;
combined.append(&mut block_ir.diagnostics);
block_ir.diagnostics = combined;
let lines = LineStarts::new(&source);
let mut has_error = false;
for d in &block_ir.diagnostics {
let pos = lines.position(&source, d.span.start);
eprintln!(
"{}:{}: {}[{}]: {}",
file.display(),
pos,
d.severity.as_str(),
d.code.as_str(),
d.primary,
);
for note in &d.notes {
eprintln!(" note: {}", note.message);
}
if d.severity == Severity::Error {
has_error = true;
}
}
let success_exit = if has_error {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
};
match format {
LowerFormat::Ascii => {
print_block_ir_ascii(&block_ir);
success_exit
}
LowerFormat::Json => match serde_json::to_string_pretty(&block_ir) {
Ok(json) => {
println!("{json}");
success_exit
}
Err(err) => {
eprintln!("error: failed to serialise block-array IR as JSON: {err}");
ExitCode::from(1)
}
},
LowerFormat::Debug => {
println!("{block_ir:#?}");
success_exit
}
}
}
fn run_synth(
file: &Path,
experimental_flag: bool,
stage: SynthStage,
edition: Option<EditionArg>,
) -> ExitCode {
if !experimental_flag {
eprintln!(
"error: `cairn synth` is an internal / experimental surface; pass --experimental-logic-synth to opt in",
);
return ExitCode::from(2);
}
if !stage_requires_edition(stage) && edition.is_some() {
eprintln!(
"error: `--edition` is only meaningful with {}; the {} stages are edition-neutral",
edition_required_stage_list(),
edition_neutral_stage_list(),
);
return ExitCode::from(2);
}
let source = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(err) => {
eprintln!("error: cannot read `{}`: {err}", file.display());
return match err.kind() {
std::io::ErrorKind::NotFound => ExitCode::from(2),
_ => ExitCode::from(1),
};
}
};
let module = match parse(&source) {
Ok(m) => m,
Err(err) => {
eprintln!(
"error: {}:{}: {}",
file.display(),
err.position(),
err.user_message(),
);
return ExitCode::from(1);
}
};
let ir = lower(&module);
let lines = LineStarts::new(&source);
let resolution = resolve(&ir, None);
let mut has_error = report_core_diagnostics(file, &source, &lines, &resolution.diagnostics);
let check_diagnostics = check(&module, &ir, None);
if report_core_diagnostics(file, &source, &lines, &check_diagnostics) {
has_error = true;
}
if has_error {
return ExitCode::from(1);
}
let synth = synthesize(&ir);
if report_synth_diagnostics(file, &source, &lines, &synth.diagnostics) {
return ExitCode::from(1);
}
let (json, label) =
match dispatch_synth_stage(stage, edition, &synth, &ir, file, &source, &lines) {
Ok(pair) => pair,
Err(code) => return code,
};
match json {
Ok(text) => {
println!("{text}");
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("error: failed to serialise {label} as JSON: {err}");
ExitCode::from(1)
}
}
}
fn dispatch_synth_stage(
stage: SynthStage,
edition: Option<EditionArg>,
synth: &cairn_lang_redstone::SynthOutput,
ir: &cairn_lang_core::IntentModule,
file: &Path,
source: &str,
lines: &LineStarts,
) -> Result<(serde_json::Result<String>, &'static str), ExitCode> {
if matches!(stage, SynthStage::Logic) {
return Ok((serde_json::to_string_pretty(&synth.scoped), "Logic IR"));
}
let edition = if stage_requires_edition(stage) {
Some(require_edition(edition, stage_cli_name(stage))?.as_edition())
} else {
None
};
let netlist = compile_netlist(&synth.scoped);
let edition = match (edition, stage) {
(Some(edition), _) => edition,
(None, SynthStage::Netlist) => {
return Ok((serde_json::to_string_pretty(&netlist), "Netlist IR"));
}
(None, SynthStage::Logic) => unreachable!("the Logic guard above returns"),
(
None,
SynthStage::Edition
| SynthStage::Placement
| SynthStage::Route
| SynthStage::Delay
| SynthStage::Crossing,
) => unreachable!(
"stage_requires_edition holds here, so the gate above resolved an edition or returned"
),
};
let edition_netlist = compile_edition_netlist(&netlist, edition);
if matches!(stage, SynthStage::Edition) {
return Ok((
serde_json::to_string_pretty(&edition_netlist),
"Edition Netlist IR",
));
}
let placement = compile_placement(&edition_netlist, ir);
if report_synth_diagnostics(file, source, lines, &placement.diagnostics) {
return Err(ExitCode::from(1));
}
if matches!(stage, SynthStage::Placement) {
return Ok((
serde_json::to_string_pretty(&placement.scoped),
"Placement IR",
));
}
let routing = compile_routing(&placement.scoped);
if report_synth_diagnostics(file, source, lines, &routing.diagnostics) {
return Err(ExitCode::from(1));
}
if matches!(stage, SynthStage::Route) {
return Ok((
serde_json::to_string_pretty(&routing.scoped),
"Routed Placement IR",
));
}
let delay = compile_delay(&routing.scoped);
if report_synth_diagnostics(file, source, lines, &delay.diagnostics) {
return Err(ExitCode::from(1));
}
if matches!(stage, SynthStage::Delay) {
return Ok((
serde_json::to_string_pretty(&delay.scoped),
"Delayed Placement IR",
));
}
let crossing = compile_crossing(&delay.scoped);
if report_synth_diagnostics(file, source, lines, &crossing.diagnostics) {
return Err(ExitCode::from(1));
}
match stage {
SynthStage::Crossing => Ok((
serde_json::to_string_pretty(&crossing.scoped),
"Legalized Placement IR",
)),
SynthStage::Logic
| SynthStage::Netlist
| SynthStage::Edition
| SynthStage::Placement
| SynthStage::Route
| SynthStage::Delay => {
unreachable!("earlier guards return for non-Crossing stages")
}
}
}
fn stage_cli_name(stage: SynthStage) -> &'static str {
match stage {
SynthStage::Logic => "logic",
SynthStage::Netlist => "netlist",
SynthStage::Edition => "edition",
SynthStage::Placement => PlacementStage::Placement.as_str(),
SynthStage::Route => PlacementStage::Route.as_str(),
SynthStage::Delay => PlacementStage::Delay.as_str(),
SynthStage::Crossing => PlacementStage::Crossing.as_str(),
}
}
fn stage_requires_edition(stage: SynthStage) -> bool {
match stage {
SynthStage::Logic | SynthStage::Netlist => false,
SynthStage::Edition
| SynthStage::Placement
| SynthStage::Route
| SynthStage::Delay
| SynthStage::Crossing => true,
}
}
fn edition_required_stage_list() -> String {
join_stages(stage_requires_edition, "or", |name| {
format!("`--stage {name}`")
})
}
fn edition_neutral_stage_list() -> String {
join_stages(
|stage| !stage_requires_edition(stage),
"and",
|name| format!("`{name}`"),
)
}
fn join_stages(
select: impl Fn(SynthStage) -> bool,
conjunction: &str,
render: impl Fn(&str) -> String,
) -> String {
let items: Vec<String> = SynthStage::value_variants()
.iter()
.copied()
.filter(|stage| select(*stage))
.map(|stage| render(stage_cli_name(stage)))
.collect();
match items.split_last() {
None => String::new(),
Some((last, [])) => last.clone(),
Some((last, [only])) => format!("{only} {conjunction} {last}"),
Some((last, rest)) => format!("{}, {conjunction} {last}", rest.join(", ")),
}
}
fn require_edition(edition: Option<EditionArg>, stage_name: &str) -> Result<EditionArg, ExitCode> {
edition.ok_or_else(|| {
eprintln!(
"error: `cairn synth --stage {stage_name}` requires --edition <java|bedrock> (this stage picks the target-edition cell realisation, so the flag is not optional)",
);
ExitCode::from(2)
})
}
fn report_core_diagnostics(
file: &Path,
source: &str,
lines: &LineStarts,
diagnostics: &[cairn_lang_core::check::Diagnostic],
) -> bool {
let mut has_error = false;
for d in diagnostics {
let pos = lines.position(source, d.span.start);
eprintln!(
"{}:{}: {}[{}]: {}",
file.display(),
pos,
d.severity.as_str(),
d.code.as_str(),
d.primary,
);
for note in &d.notes {
if let Some(span) = note.span.as_ref() {
let note_pos = lines.position(source, span.start);
eprintln!("{}:{}: note: {}", file.display(), note_pos, note.message);
} else {
eprintln!(" note: {}", note.message);
}
}
if d.severity == Severity::Error {
has_error = true;
}
}
has_error
}
fn report_synth_diagnostics(
file: &Path,
source: &str,
lines: &LineStarts,
diagnostics: &[cairn_lang_redstone::Diagnostic],
) -> bool {
let mut has_error = false;
for d in diagnostics {
let pos = lines.position(source, d.span.start);
eprintln!(
"{}:{}: {}[{}]: {}",
file.display(),
pos,
d.severity.as_str(),
d.code.as_str(),
d.primary,
);
for note in &d.notes {
if let Some(span) = note.span.as_ref() {
let note_pos = lines.position(source, span.start);
eprintln!("{}:{}: note: {}", file.display(), note_pos, note.message);
} else {
eprintln!(" note: {}", note.message);
}
}
if d.severity == Severity::Error {
has_error = true;
}
}
has_error
}
fn print_block_ir_ascii(block_ir: &BlockArrayIr) {
if block_ir.structures.is_empty() {
println!("(no structures lowered)");
return;
}
for (key, ba) in &block_ir.structures {
println!("{key} dims={}x{}x{}", ba.dims.x, ba.dims.y, ba.dims.z);
println!(" palette:");
for (i, state) in ba.palette.entries.iter().enumerate() {
let glyph = ascii_glyph(i);
if state.properties.is_empty() {
println!(" [{i:>3}] {glyph} {}", state.id);
} else {
let props = state
.properties
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(",");
println!(" [{i:>3}] {glyph} {}[{props}]", state.id);
}
}
for y in 0..ba.dims.y {
println!(" y={y}");
print_y_slice(ba, y);
}
}
}
const ASCII_ALPHABET: &[u8] = b"#abcdefghijklmnopqrstuvwxyz0123456789";
fn ascii_glyph(palette_index: usize) -> char {
if palette_index == 0 {
return '.';
}
ASCII_ALPHABET
.get(palette_index - 1)
.copied()
.map_or('?', char::from)
}
fn print_y_slice(ba: &BlockArray, y: u32) {
for z in 0..ba.dims.z {
let mut row = String::with_capacity(ba.dims.x as usize);
for x in 0..ba.dims.x {
let i = ba.dims.index(x, y, z).expect("in-range coordinate");
row.push(ascii_glyph(usize::from(ba.voxels[i].0)));
}
println!(" {row}");
}
}
enum ResolvedTarget {
Java(JavaTarget),
Bedrock(BedrockTarget),
}
impl ResolvedTarget {
fn output_ext(&self) -> OutputExt {
match self {
ResolvedTarget::Java(_) => OutputExt::Nbt,
ResolvedTarget::Bedrock(_) => OutputExt::Mcstructure,
}
}
fn build_tag(&self, ba: &BlockArray) -> Result<(Compound, Vec<ParityNote>), String> {
match self {
ResolvedTarget::Java(t) => build_structure_tag(ba, t)
.map(|tag| (tag, Vec::new()))
.map_err(|e| e.to_string()),
ResolvedTarget::Bedrock(t) => build_mcstructure_tag(ba, t).map_err(|e| e.to_string()),
}
}
fn write_tag<W: std::io::Write>(
&self,
writer: &mut W,
tag: &Compound,
) -> Result<(), std::io::Error> {
let encoded = match self {
ResolvedTarget::Java(_) => write_compound_gzip(writer, tag),
ResolvedTarget::Bedrock(_) => write_mcstructure(writer, tag),
};
encoded.map_err(|e| std::io::Error::other(format!("nbt encode: {e}")))
}
fn mc_version(&self) -> &str {
match self {
ResolvedTarget::Java(t) => &t.mc_version,
ResolvedTarget::Bedrock(t) => &t.mc_version,
}
}
fn version_int(&self) -> i32 {
match self {
ResolvedTarget::Java(t) => t.data_version,
ResolvedTarget::Bedrock(t) => t.block_version,
}
}
fn registry_pack(&self) -> &'static RegistryPack {
match self {
ResolvedTarget::Java(_) => builtin_java(),
ResolvedTarget::Bedrock(_) => builtin_bedrock(),
}
}
}
fn run_compile(
file: &Path,
edition: EditionArg,
target: &str,
out: Option<&Path>,
lock: Option<&Path>,
) -> ExitCode {
let (source, block_ir) = match load_and_lower(file, edition) {
Ok(pair) => pair,
Err(code) => return code,
};
if report_lowering_diagnostics(file, &source, &block_ir) {
return ExitCode::from(1);
}
let target = match resolve_target(edition, target) {
Ok(t) => t,
Err(code) => return code,
};
let out_dir = match prepare_out_dir(file, out) {
Ok(d) => d,
Err(code) => return code,
};
let prepared = match prepare_artifacts(&block_ir, &target, &out_dir) {
Ok(p) => p,
Err(code) => return code,
};
let lock_path = lock.map_or_else(|| default_lock_path(file), Path::to_path_buf);
write_artifacts_and_lock(&prepared, &source, &block_ir, edition, &target, &lock_path)
}
fn load_and_lower(file: &Path, edition: EditionArg) -> Result<(String, BlockArrayIr), ExitCode> {
let source = std::fs::read_to_string(file).map_err(|err| {
eprintln!("error: cannot read `{}`: {err}", file.display());
match err.kind() {
std::io::ErrorKind::NotFound => ExitCode::from(2),
_ => ExitCode::from(1),
}
})?;
let module = parse(&source).map_err(|err| {
eprintln!(
"error: {}:{}: {}",
file.display(),
err.position(),
err.user_message(),
);
ExitCode::from(1)
})?;
let ir = lower(&module);
let resolution = resolve(&ir, Some(edition.as_edition()));
let materials = match edition {
EditionArg::Java => &builtin_java().materials,
EditionArg::Bedrock => &builtin_bedrock().materials,
};
let mut block_ir = lower_to_block_array(&ir, &resolution, Some(materials));
let mut combined = resolution.diagnostics;
combined.append(&mut block_ir.diagnostics);
block_ir.diagnostics = combined;
Ok((source, block_ir))
}
fn report_lowering_diagnostics(file: &Path, source: &str, block_ir: &BlockArrayIr) -> bool {
let lines = LineStarts::new(source);
let mut has_error = false;
for d in &block_ir.diagnostics {
let pos = lines.position(source, d.span.start);
eprintln!(
"{}:{}: {}[{}]: {}",
file.display(),
pos,
d.severity.as_str(),
d.code.as_str(),
d.primary,
);
for note in &d.notes {
eprintln!(" note: {}", note.message);
}
if d.severity == Severity::Error {
has_error = true;
}
}
has_error
}
fn resolve_target(edition: EditionArg, target: &str) -> Result<ResolvedTarget, ExitCode> {
match edition {
EditionArg::Java => resolve_java_target(target)
.map(ResolvedTarget::Java)
.map_err(|err| {
eprintln!("error: {err}");
ExitCode::from(1)
}),
EditionArg::Bedrock => resolve_bedrock_target(target)
.map(ResolvedTarget::Bedrock)
.map_err(|err| {
eprintln!("error: {err}");
ExitCode::from(1)
}),
}
}
fn prepare_out_dir(file: &Path, requested: Option<&Path>) -> Result<PathBuf, ExitCode> {
let Some(out_dir) = resolve_out_dir(file, requested) else {
eprintln!(
"error: source `{}` has no parent directory and --out was not given",
file.display(),
);
return Err(ExitCode::from(1));
};
std::fs::create_dir_all(&out_dir).map_err(|err| {
eprintln!(
"error: cannot create output directory `{}`: {err}",
out_dir.display(),
);
ExitCode::from(1)
})?;
Ok(out_dir)
}
fn prepare_artifacts(
block_ir: &BlockArrayIr,
target: &ResolvedTarget,
out_dir: &Path,
) -> Result<Vec<(PathBuf, Compound)>, ExitCode> {
let mut prepared = Vec::with_capacity(block_ir.structures.len());
let mut seen_paths: std::collections::HashMap<PathBuf, String> =
std::collections::HashMap::with_capacity(block_ir.structures.len());
for (scope, ba) in &block_ir.structures {
let (tag, degraded) = target.build_tag(ba).map_err(|err| {
eprintln!("error: building `{scope}`: {err}");
ExitCode::from(1)
})?;
for note in degraded {
eprintln!(
"warning[W_INTENT_DEGRADED]: {scope}: {id}: {message}",
id = note.id,
message = note.message,
);
}
let path = out_dir.join(output_filename(scope, target.output_ext()));
if let Some(first) = seen_paths.insert(path.clone(), scope.clone()) {
eprintln!(
"error: output filename `{}` collides between scopes `{first}` and `{scope}`",
path.display(),
);
return Err(ExitCode::from(1));
}
prepared.push((path, tag));
}
Ok(prepared)
}
fn write_artifacts_and_lock(
prepared: &[(PathBuf, Compound)],
source: &str,
block_ir: &BlockArrayIr,
edition: EditionArg,
target: &ResolvedTarget,
lock_path: &Path,
) -> ExitCode {
let mut written: Vec<PathBuf> = Vec::with_capacity(prepared.len());
for (path, tag) in prepared {
if let Err(err) = write_tag_atomically(path, tag, target) {
rollback(&written, None);
eprintln!("error: writing `{}`: {err}", path.display());
return ExitCode::from(1);
}
written.push(path.clone());
}
let lockfile = match build_lockfile(source, block_ir, edition, target) {
Ok(lf) => lf,
Err(err) => {
rollback(&written, None);
eprintln!("error: {err}");
return ExitCode::from(1);
}
};
if let Err(err) = lockfile.write_to_path(lock_path) {
rollback(&written, None);
eprintln!("error: writing lockfile `{}`: {err}", lock_path.display());
return ExitCode::from(1);
}
for path in &written {
println!("wrote {}", path.display());
}
println!("wrote {}", lock_path.display());
ExitCode::SUCCESS
}
fn resolve_out_dir(source: &Path, requested: Option<&Path>) -> Option<PathBuf> {
if let Some(p) = requested {
return Some(p.to_path_buf());
}
let parent = source.parent()?;
Some(if parent.as_os_str().is_empty() {
PathBuf::from(".")
} else {
parent.to_path_buf()
})
}
fn write_tag_atomically(
final_path: &Path,
tag: &Compound,
target: &ResolvedTarget,
) -> Result<(), std::io::Error> {
use std::io::Write as _;
let mut tmp_path = final_path.as_os_str().to_owned();
tmp_path.push(".tmp");
let tmp_path = PathBuf::from(tmp_path);
let result = (|| {
let mut f = std::fs::File::create(&tmp_path)?;
target.write_tag(&mut f, tag)?;
f.flush()?;
f.sync_all()?;
drop(f);
std::fs::rename(&tmp_path, final_path)
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp_path);
}
result
}
fn rollback(written: &[PathBuf], lock_path: Option<&Path>) {
for path in written {
let _ = std::fs::remove_file(path);
}
if let Some(p) = lock_path {
let _ = std::fs::remove_file(p);
}
}
fn default_lock_path(source: &Path) -> PathBuf {
let mut p = source.as_os_str().to_owned();
p.push(".lock");
PathBuf::from(p)
}
fn build_lockfile(
source: &str,
block_ir: &BlockArrayIr,
edition: EditionArg,
target: &ResolvedTarget,
) -> Result<Lockfile, cairn_lang_core::lock::HashError> {
Ok(Lockfile {
source_hash: hash_source(source),
cairn_version: CAIRN_VERSION.to_owned(),
target: LockTarget {
edition: edition.as_lock_edition(),
mc_version: target.mc_version().to_owned(),
data_version: target.version_int(),
},
inputs: LockInputs {
registry_pack_hash: target.registry_pack().bytes_hash.clone(),
constraint_catalog_hash: HashHex::zero(),
},
resolved_ir_hash: hash_resolved_ir(block_ir)?,
verified: true,
member_version_sensitivity: vec![],
placements: block_ir
.placements
.values()
.map(|p| LockPlacement {
site: p.site.clone(),
id: p.place_id.clone(),
def: p.source_def.clone(),
theme: p.theme.clone(),
origin: [p.origin.0, p.origin.1, p.origin.2],
dims: [p.dims.x, p.dims.y, p.dims.z],
})
.collect(),
walkways: block_ir
.walkways
.values()
.map(|w| {
let d = w.footprint.to_dims_y1();
LockWalkway {
site: w.site.clone(),
from: w.from.clone(),
to: w.to.clone(),
path_material: w.path_material.clone(),
origin: [w.origin.0, w.origin.1, w.origin.2],
dims: [d.x, d.y, d.z],
}
})
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn placement_stage_names_match_clap() {
for (stage, placement) in [
(SynthStage::Placement, PlacementStage::Placement),
(SynthStage::Route, PlacementStage::Route),
(SynthStage::Delay, PlacementStage::Delay),
(SynthStage::Crossing, PlacementStage::Crossing),
] {
let clap_name = stage
.to_possible_value()
.expect("no SynthStage variant is skipped");
assert_eq!(clap_name.get_name(), placement.as_str());
assert_eq!(stage_cli_name(stage), placement.as_str());
}
}
#[test]
fn edition_neutral_stage_names_match_clap() {
for stage in [SynthStage::Logic, SynthStage::Netlist, SynthStage::Edition] {
let clap_name = stage
.to_possible_value()
.expect("no SynthStage variant is skipped");
assert_eq!(clap_name.get_name(), stage_cli_name(stage));
}
}
#[test]
fn edition_required_stages_match_the_documented_set() {
for stage in SynthStage::value_variants() {
let name = stage
.to_possible_value()
.expect("no SynthStage variant is skipped");
let expected = match name.get_name() {
"logic" | "netlist" => false,
"edition" | "placement" | "route" | "delay" | "crossing" => true,
other => panic!("unclassified --stage value `{other}`"),
};
assert_eq!(
stage_requires_edition(*stage),
expected,
"--stage {} edition-dependence",
name.get_name(),
);
}
}
#[test]
fn stray_edition_message_lists_read_as_english() {
assert_eq!(
edition_required_stage_list(),
"`--stage edition`, `--stage placement`, `--stage route`, \
`--stage delay`, or `--stage crossing`",
);
assert_eq!(edition_neutral_stage_list(), "`logic` and `netlist`");
}
}