luaur_compile_cli/functions/
main.rs1use alloc::string::String;
2use alloc::vec::Vec;
3use core::ffi::{c_char, c_int, CStr};
4use std::ffi::CString;
5
6use luaur_cli_lib::functions::get_source_files::get_source_files;
7use luaur_cli_lib::functions::set_luau_flags_default::set_luau_flags_default;
8use luaur_cli_lib::functions::set_luau_flags_flags_alt_b::set_luau_flags_c_char;
9use luaur_code_gen::enums::function_stats_flags::FunctionStatsFlags;
10use luaur_code_gen::enums::target::Target;
11use luaur_code_gen::records::lowering_stats::FunctionStats_Enable;
12use luaur_common::functions::assert_handler::assert_handler;
13
14use crate::enums::compile_format::CompileFormat;
15use crate::enums::record_stats::RecordStats;
16use crate::functions::assertion_handler::assertion_handler;
17use crate::functions::compile_file::compile_file;
18use crate::functions::display_help::display_help;
19use crate::functions::escape_filename::escape_filename;
20use crate::functions::get_compile_format::get_compile_format;
21use crate::functions::serialize_compile_stats::{serialize_compile_stats, FILE};
22use crate::records::compile_stats::CompileStats;
23use crate::records::global_options::globalOptions;
24
25extern "C" {
26 fn fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE;
27 fn fclose(stream: *mut FILE) -> c_int;
28 fn fprintf(stream: *mut FILE, format: *const c_char, ...) -> c_int;
29}
30
31unsafe extern "C" fn assertion_handler_adapter(
32 expr: *const c_char,
33 file: *const c_char,
34 line: i32,
35 function: *const c_char,
36) -> i32 {
37 assertion_handler(expr, file, line, function)
38}
39
40fn atoi_like(value: &str) -> i32 {
41 let bytes = value.as_bytes();
42 let mut i = 0;
43
44 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
45 i += 1;
46 }
47
48 let mut sign = 1i64;
49 if i < bytes.len() {
50 if bytes[i] == b'-' {
51 sign = -1;
52 i += 1;
53 } else if bytes[i] == b'+' {
54 i += 1;
55 }
56 }
57
58 let mut result = 0i64;
59 while i < bytes.len() && bytes[i].is_ascii_digit() {
60 result = result
61 .saturating_mul(10)
62 .saturating_add((bytes[i] - b'0') as i64);
63 i += 1;
64 }
65
66 (result.saturating_mul(sign)).clamp(i32::MIN as i64, i32::MAX as i64) as i32
67}
68
69pub fn main(argc: i32, argv: *mut *mut c_char) -> i32 {
70 *assert_handler() = Some(assertion_handler_adapter);
71
72 set_luau_flags_default();
73
74 let mut compile_format = CompileFormat::Text;
75 let mut assembly_target = Target::Host;
76 let mut record_stats = RecordStats::None;
77 let mut stats_file = String::from("stats.json");
78 let mut bytecode_summary = false;
79 let mut dump_constants = false;
80
81 for i in 1..argc as usize {
82 let arg_ptr = unsafe { *argv.add(i) };
83 let arg = unsafe { CStr::from_ptr(arg_ptr).to_string_lossy().into_owned() };
84
85 if arg == "-h" || arg == "--help" {
86 display_help(unsafe { *argv });
87 return 0;
88 } else if arg.starts_with("-O") {
89 let level = atoi_like(&arg[2..]);
90 if level < 0 || level > 2 {
91 eprintln!("Error: Optimization level must be between 0 and 2 inclusive.");
92 return 1;
93 }
94 unsafe {
95 globalOptions.optimizationLevel = level;
96 }
97 } else if arg.starts_with("-g") {
98 let level = atoi_like(&arg[2..]);
99 if level < 0 || level > 2 {
100 eprintln!("Error: Debug level must be between 0 and 2 inclusive.");
101 return 1;
102 }
103 unsafe {
104 globalOptions.debugLevel = level;
105 }
106 } else if arg.starts_with("-t") {
107 let level = atoi_like(&arg[2..]);
108 if level < 0 || level > 1 {
109 eprintln!("Error: Type info level must be between 0 and 1 inclusive.");
110 return 1;
111 }
112 unsafe {
113 globalOptions.typeInfoLevel = level;
114 }
115 } else if let Some(value) = arg.strip_prefix("--target=") {
116 if value == "a64" {
117 assembly_target = Target::A64;
118 } else if value == "a64_nf" {
119 assembly_target = Target::A64_NoFeatures;
120 } else if value == "x64" {
121 assembly_target = Target::X64_SystemV;
122 } else if value == "x64_ms" {
123 assembly_target = Target::X64_Windows;
124 } else {
125 eprintln!("Error: unknown target");
126 return 1;
127 }
128 } else if arg == "--timetrace" {
129 luaur_common::FFlag::DebugLuauTimeTracing.set(true);
130 } else if let Some(value) = arg.strip_prefix("--record-stats=") {
131 if value == "total" {
132 record_stats = RecordStats::Total;
133 } else if value == "file" {
134 record_stats = RecordStats::File;
135 } else if value == "function" {
136 record_stats = RecordStats::Function;
137 } else {
138 eprintln!("Error: unknown 'granularity' for '--record-stats'.");
139 return 1;
140 }
141 } else if arg.starts_with("--bytecode-summary") {
142 bytecode_summary = true;
143 } else if arg == "--dump-constants" {
144 dump_constants = true;
145 } else if let Some(value) = arg.strip_prefix("--stats-file=") {
146 stats_file = String::from(value);
147
148 if stats_file.is_empty() {
149 eprintln!("Error: filename missing for '--stats-file'.\n");
150 return 1;
151 }
152 } else if arg.starts_with("--fflags=") {
153 unsafe {
154 set_luau_flags_c_char(arg_ptr.add(9));
155 }
156 } else if arg.starts_with("--vector-lib=") {
157 unsafe {
158 globalOptions.vectorLib = arg_ptr.add(13);
159 }
160 } else if arg.starts_with("--vector-ctor=") {
161 unsafe {
162 globalOptions.vectorCtor = arg_ptr.add(14);
163 }
164 } else if arg.starts_with("--vector-type=") {
165 unsafe {
166 globalOptions.vectorType = arg_ptr.add(14);
167 }
168 } else if arg.starts_with("--parse-cst") {
169 unsafe {
170 globalOptions.parseCst = true;
171 }
172 } else if arg.starts_with("--only-parse") {
173 unsafe {
174 globalOptions.onlyParse = true;
175 }
176 } else if arg.starts_with("--") {
177 if let Some(format) = get_compile_format(&arg[2..]) {
178 compile_format = format;
179 } else {
180 eprintln!("Error: Unrecognized option '{}'.\n", arg);
181 display_help(unsafe { *argv });
182 return 1;
183 }
184 } else if arg.starts_with('-') {
185 eprintln!("Error: Unrecognized option '{}'.\n", arg);
186 display_help(unsafe { *argv });
187 return 1;
188 }
189 }
190
191 if bytecode_summary && record_stats != RecordStats::Function {
192 eprintln!("'Error: Required '--record-stats=function' for '--bytecode-summary'.");
193 return 1;
194 }
195
196 if luaur_common::FFlag::DebugLuauTimeTracing.get() {
197 eprintln!(
198 "To run with --timetrace, Luau has to be built with LUAU_ENABLE_TIME_TRACE enabled"
199 );
200 return 1;
201 }
202
203 let files = get_source_files(argc, argv);
204 let file_count = files.len();
205 let mut stats = CompileStats::default();
206
207 let mut file_stats: Vec<CompileStats> = Vec::new();
208 if record_stats == RecordStats::File || record_stats == RecordStats::Function {
209 file_stats.reserve(file_count);
210 }
211
212 let mut failed = 0;
213 let function_stats = (if record_stats == RecordStats::Function {
214 FunctionStats_Enable
215 } else {
216 0
217 }) | if bytecode_summary {
218 FunctionStatsFlags::FunctionStats_BytecodeSummary as u32
219 } else {
220 0
221 };
222
223 for path in &files {
224 let mut file_stat = CompileStats::default();
225 file_stat.lower_stats.function_stats_flags = function_stats;
226
227 let ok = match CString::new(path.as_str()) {
228 Ok(path_c) => compile_file(
229 path_c.as_ptr(),
230 compile_format,
231 assembly_target,
232 &mut file_stat,
233 dump_constants,
234 ),
235 Err(_) => {
236 eprintln!("Error opening {}", path);
237 false
238 }
239 };
240
241 if !ok {
242 failed += 1;
243 }
244
245 stats += &file_stat;
246
247 if record_stats == RecordStats::File || record_stats == RecordStats::Function {
248 file_stats.push(file_stat);
249 }
250 }
251
252 if compile_format == CompileFormat::Null {
253 println!(
254 "Compiled {} KLOC into {} KB bytecode (read {:.2}s, parse {:.2}s, compile {:.2}s)",
255 (stats.lines / 1000) as i32,
256 (stats.bytecode / 1024) as i32,
257 stats.read_time,
258 stats.parse_time,
259 stats.compile_time
260 );
261 } else if compile_format == CompileFormat::CodegenNull {
262 println!(
263 "Compiled {} KLOC into {} KB bytecode => {} KB native code ({:.2}x) (read {:.2}s, parse {:.2}s, compile {:.2}s, codegen {:.2}s)",
264 (stats.lines / 1000) as i32,
265 (stats.bytecode / 1024) as i32,
266 (stats.codegen / 1024) as i32,
267 if stats.bytecode == 0 {
268 0.0
269 } else {
270 stats.codegen as f64 / stats.bytecode as f64
271 },
272 stats.read_time,
273 stats.parse_time,
274 stats.compile_time,
275 stats.codegen_time
276 );
277
278 println!(
279 "Lowering: regalloc failed: {}, lowering failed {}; spills to stack: {}, spills to restore: {}, max spill slot {}",
280 stats.lower_stats.reg_alloc_errors,
281 stats.lower_stats.lowering_errors,
282 stats.lower_stats.spills_to_slot,
283 stats.lower_stats.spills_to_restore,
284 stats.lower_stats.max_spill_slots_used
285 );
286 }
287
288 if record_stats != RecordStats::None {
289 let stats_file_c = match CString::new(stats_file.as_str()) {
290 Ok(stats_file_c) => stats_file_c,
291 Err(_) => {
292 eprintln!("Unable to open 'stats.json'");
293 return 1;
294 }
295 };
296
297 let fp = unsafe { fopen(stats_file_c.as_ptr(), c"w".as_ptr()) };
298
299 if fp.is_null() {
300 eprintln!("Unable to open 'stats.json'");
301 return 1;
302 }
303
304 if record_stats == RecordStats::Total {
305 serialize_compile_stats(fp, &stats);
306 } else if record_stats == RecordStats::File || record_stats == RecordStats::Function {
307 unsafe {
308 fprintf(fp, c"{\n".as_ptr());
309 }
310
311 for i in 0..file_count {
312 let escaped = escape_filename(&files[i]);
313 let escaped_c = CString::new(escaped).expect("escaped filename contains NUL");
314 unsafe {
315 fprintf(fp, c" \"%s\": ".as_ptr(), escaped_c.as_ptr());
316 }
317 serialize_compile_stats(fp, &file_stats[i]);
318 unsafe {
319 if i == file_count - 1 {
320 fprintf(fp, c"\n".as_ptr());
321 } else {
322 fprintf(fp, c",\n".as_ptr());
323 }
324 }
325 }
326
327 unsafe {
328 fprintf(fp, c"}".as_ptr());
329 }
330 }
331
332 unsafe {
333 fclose(fp);
334 }
335 }
336
337 if failed != 0 {
338 1
339 } else {
340 0
341 }
342}