2025-09-17 20:33:19 +09:00
use std ::fs ::File ;
use std ::io ::{ Read , Write } ;
use std ::path ::{ Path , PathBuf } ;
use std ::process ::Command ;
use anyhow ::{ bail , Context , Result } ;
use clap ::{ ArgAction , Parser } ;
#[ derive(Parser, Debug) ]
2025-09-18 03:57:25 +09:00
#[ command(
name = " ny-llvmc " ,
about = " Nyash LLVM compiler (llvmlite harness wrapper) "
) ]
2025-09-17 20:33:19 +09:00
struct Args {
/// MIR JSON input file path (use '-' to read from stdin). When omitted with --dummy, a dummy ny_main is emitted.
#[ arg(long = " in " , value_name = " FILE " , default_value = " - " ) ]
infile : String ,
2025-09-18 03:57:25 +09:00
/// Output path. For `--emit obj`, this is an object (.o). For `--emit exe`, this is an executable path.
2025-09-17 20:33:19 +09:00
#[ arg(long, value_name = " FILE " ) ]
out : PathBuf ,
/// Generate a dummy object (ny_main -> i32 0). Ignores --in when set.
#[ arg(long, action = ArgAction::SetTrue) ]
dummy : bool ,
/// Path to Python harness script (defaults to tools/llvmlite_harness.py in CWD)
#[ arg(long, value_name = " FILE " ) ]
harness : Option < PathBuf > ,
2025-09-18 03:57:25 +09:00
/// Emit kind: 'obj' (default) or 'exe'.
#[ arg(long, value_name = " {obj|exe} " , default_value = " obj " ) ]
emit : String ,
2025-09-24 12:57:33 +09:00
/// Path to directory containing libnyash_kernel.a when emitting an executable. If omitted, searches target/release then crates/nyash_kernel/target/release.
2025-09-18 03:57:25 +09:00
#[ arg(long, value_name = " DIR " ) ]
nyrt : Option < PathBuf > ,
/// Extra linker libs/flags appended when emitting an executable (single string, space-separated).
#[ arg(long, value_name = " FLAGS " ) ]
libs : Option < String > ,
2025-09-17 20:33:19 +09:00
}
fn main ( ) -> Result < ( ) > {
let args = Args ::parse ( ) ;
// Ensure parent dir exists
if let Some ( parent ) = args . out . parent ( ) {
std ::fs ::create_dir_all ( parent ) . ok ( ) ;
}
// Resolve harness path
let harness_path = if let Some ( p ) = args . harness . clone ( ) {
p
} else {
PathBuf ::from ( " tools/llvmlite_harness.py " )
} ;
2025-09-18 03:57:25 +09:00
// Determine emit kind
let emit_exe = matches! ( args . emit . as_str ( ) , " exe " | " EXE " ) ;
2025-09-17 20:33:19 +09:00
if args . dummy {
2025-09-18 03:57:25 +09:00
// Dummy ny_main: always go through harness to produce an object then link if requested
let obj_path = if emit_exe {
// derive a temporary .o path next to output
let mut p = args . out . clone ( ) ;
p . set_extension ( " o " ) ;
p
} else {
args . out . clone ( )
} ;
run_harness_dummy ( & harness_path , & obj_path )
2025-09-17 20:33:19 +09:00
. with_context ( | | " failed to run harness in dummy mode " ) ? ;
2025-09-18 03:57:25 +09:00
if emit_exe {
link_executable ( & obj_path , & args . out , args . nyrt . as_ref ( ) , args . libs . as_deref ( ) ) ? ;
println! ( " [ny-llvmc] executable written: {} " , args . out . display ( ) ) ;
} else {
println! ( " [ny-llvmc] dummy object written: {} " , obj_path . display ( ) ) ;
}
2025-09-17 20:33:19 +09:00
return Ok ( ( ) ) ;
}
// Prepare input JSON path: either from file or stdin -> temp file
let mut temp_path : Option < PathBuf > = None ;
let input_path = if args . infile = = " - " {
let mut buf = String ::new ( ) ;
std ::io ::stdin ( )
. read_to_string ( & mut buf )
. context ( " reading MIR JSON from stdin " ) ? ;
// Basic sanity check that it's JSON
2025-09-18 03:57:25 +09:00
let _ : serde_json ::Value =
serde_json ::from_str ( & buf ) . context ( " stdin does not contain valid JSON " ) ? ;
2025-09-17 20:33:19 +09:00
let tmp = std ::env ::temp_dir ( ) . join ( " ny_llvmc_stdin.json " ) ;
let mut f = File ::create ( & tmp ) . context ( " create temp json file " ) ? ;
f . write_all ( buf . as_bytes ( ) ) . context ( " write temp json " ) ? ;
temp_path = Some ( tmp . clone ( ) ) ;
tmp
} else {
PathBuf ::from ( & args . infile )
} ;
if ! input_path . exists ( ) {
bail! ( " input JSON not found: {} " , input_path . display ( ) ) ;
}
2025-09-18 03:57:25 +09:00
// Produce object first
let obj_path = if emit_exe {
let mut p = args . out . clone ( ) ;
p . set_extension ( " o " ) ;
p
} else {
args . out . clone ( )
} ;
run_harness_in ( & harness_path , & input_path , & obj_path ) . with_context ( | | {
format! (
" failed to compile MIR JSON via harness: {} " ,
input_path . display ( )
)
} ) ? ;
if emit_exe {
link_executable ( & obj_path , & args . out , args . nyrt . as_ref ( ) , args . libs . as_deref ( ) ) ? ;
println! ( " [ny-llvmc] executable written: {} " , args . out . display ( ) ) ;
} else {
println! ( " [ny-llvmc] object written: {} " , obj_path . display ( ) ) ;
}
2025-09-17 20:33:19 +09:00
// Cleanup temp file if used
if let Some ( p ) = temp_path {
let _ = std ::fs ::remove_file ( p ) ;
}
Ok ( ( ) )
}
fn run_harness_dummy ( harness : & Path , out : & Path ) -> Result < ( ) > {
ensure_python ( ) ? ;
let status = Command ::new ( " python3 " )
. arg ( harness )
. arg ( " --out " )
. arg ( out )
. status ( )
. context ( " failed to execute python harness (dummy) " ) ? ;
if ! status . success ( ) {
bail! ( " harness exited with status: {:?} " , status . code ( ) ) ;
}
Ok ( ( ) )
}
fn run_harness_in ( harness : & Path , input : & Path , out : & Path ) -> Result < ( ) > {
ensure_python ( ) ? ;
let status = Command ::new ( " python3 " )
. arg ( harness )
. arg ( " --in " )
. arg ( input )
. arg ( " --out " )
. arg ( out )
. status ( )
. context ( " failed to execute python harness " ) ? ;
if ! status . success ( ) {
bail! ( " harness exited with status: {:?} " , status . code ( ) ) ;
}
Ok ( ( ) )
}
fn ensure_python ( ) -> Result < ( ) > {
match Command ::new ( " python3 " ) . arg ( " --version " ) . output ( ) {
Ok ( out ) if out . status . success ( ) = > Ok ( ( ) ) ,
_ = > bail! ( " python3 not found in PATH (required for llvmlite harness) " ) ,
}
}
2025-09-18 03:57:25 +09:00
fn link_executable ( obj : & Path , out_exe : & Path , nyrt_dir_opt : Option < & PathBuf > , extra_libs : Option < & str > ) -> Result < ( ) > {
// Resolve nyRT static lib
let nyrt_dir = if let Some ( dir ) = nyrt_dir_opt {
dir . clone ( )
} else {
2025-09-24 12:57:33 +09:00
// try target/release then crates/nyash_kernel/target/release
2025-09-18 03:57:25 +09:00
let a = PathBuf ::from ( " target/release " ) ;
2025-09-24 12:57:33 +09:00
let b = PathBuf ::from ( " crates/nyash_kernel/target/release " ) ;
if a . join ( " libnyash_kernel.a " ) . exists ( ) { a } else { b }
2025-09-18 03:57:25 +09:00
} ;
2025-09-24 12:57:33 +09:00
let libnyrt = nyrt_dir . join ( " libnyash_kernel.a " ) ;
2025-09-18 03:57:25 +09:00
if ! libnyrt . exists ( ) {
2025-09-24 12:57:33 +09:00
bail! ( " libnyash_kernel.a not found in {} (use --nyrt to specify) " , nyrt_dir . display ( ) ) ;
2025-09-18 03:57:25 +09:00
}
// Choose a C linker
let linker = [ " cc " , " clang " , " gcc " ] . into_iter ( ) . find ( | c | Command ::new ( c ) . arg ( " --version " ) . output ( ) . map ( | o | o . status . success ( ) ) . unwrap_or ( false ) ) . unwrap_or ( " cc " ) ;
let mut cmd = Command ::new ( linker ) ;
cmd . arg ( " -o " ) . arg ( out_exe ) ;
cmd . arg ( obj ) ;
2025-09-24 12:57:33 +09:00
// Whole-archive libnyash_kernel to ensure all objects are linked
2025-09-18 03:57:25 +09:00
cmd . arg ( " -Wl,--whole-archive " ) . arg ( & libnyrt ) . arg ( " -Wl,--no-whole-archive " ) ;
// Common libs on Linux
cmd . arg ( " -ldl " ) . arg ( " -lpthread " ) . arg ( " -lm " ) ;
if let Some ( extras ) = extra_libs {
for tok in extras . split_whitespace ( ) {
cmd . arg ( tok ) ;
}
}
let status = cmd . status ( ) . context ( " failed to invoke system linker " ) ? ;
if ! status . success ( ) {
bail! ( " linker exited with status: {:?} " , status . code ( ) ) ;
}
Ok ( ( ) )
}