Skip to main content

cabin_driver/
lower.rs

1//! Lower the semantic [`BuildAction`] IR into concrete command argv
2//! for a [`Dialect`].
3//!
4//! This is the single point where compile/archive/link intent becomes
5//! a real command line. [`lower()`] dispatches on the dialect; the
6//! GNU/Clang and MSVC spellings live side by side below. The planner,
7//! the IR, and the Ninja writer never spell a flag themselves — they
8//! call [`lower()`] (or [`compile_argv`] for the compilation database).
9
10use camino::Utf8PathBuf;
11
12use cabin_core::{OptLevel, SourceLanguage};
13
14use crate::action::{ArchiveAction, BuildAction, CompileAction, CompileMode, LinkAction};
15use crate::dialect::Dialect;
16
17/// A fully-lowered action: the backend artifact the Ninja writer
18/// renders.
19///
20/// Mirrors the IR action shape — argv plus the metadata Ninja needs —
21/// but is *produced* by lowering, never authored directly by the
22/// planner.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct LoweredAction {
25    /// Categorization the backend switches on to pick a rule.
26    pub kind: LoweredActionKind,
27    /// Inputs that participate in the command.
28    pub inputs: Vec<Utf8PathBuf>,
29    /// Inputs the action implicitly depends on but that are not
30    /// arguments.
31    pub implicit_inputs: Vec<Utf8PathBuf>,
32    /// Files this action produces.
33    pub outputs: Vec<Utf8PathBuf>,
34    /// Optional Makefile-style depfile path. Only the GNU/Clang
35    /// dialect populates this; the MSVC dialect tracks dependencies
36    /// through Ninja's `deps = msvc` and leaves it `None`.
37    pub depfile: Option<Utf8PathBuf>,
38    /// Argv-style command, ready to be shell-quoted by the backend.
39    pub command: Vec<String>,
40    /// Short, human-readable description for build output.
41    pub description: String,
42}
43
44/// Categorization of a lowered action. A closed set; new variants
45/// require explicit handling by every backend.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum LoweredActionKind {
48    /// Compile a single C translation unit (`.c`) to an object file.
49    CompileC,
50    /// Compile a single C++ translation unit to an object file.
51    CompileCpp,
52    /// Parse + semantic-check a C translation unit. Produces no
53    /// object file; a stamp output records success.
54    SyntaxCheckC,
55    /// Parse + semantic-check a C++ translation unit. Produces no
56    /// object file; a stamp output records success.
57    SyntaxCheckCpp,
58    /// Archive a set of object files into a static library.
59    ArchiveStaticLibrary,
60    /// Link object files plus static archives into an executable.
61    LinkExecutable,
62}
63
64/// Lower one semantic [`BuildAction`] for `dialect`.
65///
66/// Infallible: every path in the semantic IR is already
67/// [`camino::Utf8Path`], so embedding it in a command line cannot
68/// fail.
69#[must_use]
70pub fn lower(dialect: Dialect, action: &BuildAction) -> LoweredAction {
71    match action {
72        BuildAction::Compile(compile) => lower_compile(dialect, compile),
73        BuildAction::Archive(archive) => lower_archive(dialect, archive),
74        BuildAction::Link(link) => lower_link(dialect, link),
75    }
76}
77
78/// Build the unwrapped compiler argv for a compile action in
79/// `dialect` — the form recorded in `compile_commands.json` (no
80/// compiler-cache wrapper). [`lower()`] prepends the wrapper on top of
81/// this for the run command.
82#[must_use]
83pub fn compile_argv(dialect: Dialect, compile: &CompileAction) -> Vec<String> {
84    match dialect {
85        Dialect::GnuLike => compile_argv_gnu(compile),
86        Dialect::Msvc => compile_argv_msvc(compile),
87    }
88}
89
90fn lower_compile(dialect: Dialect, compile: &CompileAction) -> LoweredAction {
91    // The compiler-cache wrapper is a run-command-only prefix and is
92    // applied here, never in the shared argv builder (so
93    // `compile_commands.json` keeps the underlying compiler).
94    let mut command = compile_argv(dialect, compile);
95    if let Some(wrapper) = &compile.compiler_wrapper {
96        command.insert(0, wrapper.as_str().to_owned());
97    }
98    let (kind, outputs) = match &compile.mode {
99        CompileMode::Object => {
100            let kind = match compile.language {
101                SourceLanguage::C => LoweredActionKind::CompileC,
102                SourceLanguage::Cxx => LoweredActionKind::CompileCpp,
103            };
104            (kind, vec![compile.object.clone()])
105        }
106        CompileMode::SyntaxOnly { stamp } => {
107            let kind = match compile.language {
108                SourceLanguage::C => LoweredActionKind::SyntaxCheckC,
109                SourceLanguage::Cxx => LoweredActionKind::SyntaxCheckCpp,
110            };
111            (kind, vec![stamp.clone()])
112        }
113    };
114    // Only the GNU/Clang dialect emits a Makefile depfile; MSVC
115    // tracks headers via `/showIncludes` (Ninja `deps = msvc`).
116    let depfile = match dialect {
117        Dialect::GnuLike => compile.depfile.clone(),
118        Dialect::Msvc => None,
119    };
120    LoweredAction {
121        kind,
122        inputs: vec![compile.source.clone()],
123        implicit_inputs: compile.implicit_inputs.clone(),
124        outputs,
125        depfile,
126        command,
127        description: compile.description.clone(),
128    }
129}
130
131// ---------------------------------------------------------------
132// GNU / Clang dialect.
133// ---------------------------------------------------------------
134
135fn gnu_std_flag(language: SourceLanguage) -> &'static str {
136    match language {
137        SourceLanguage::C => "-std=c11",
138        SourceLanguage::Cxx => "-std=c++17",
139    }
140}
141
142/// GNU/Clang compile argv. The layout is fixed so it reproduces the
143/// historic command lines byte-for-byte: driver, standard, profile
144/// (`-O<n>` / `-g` / `-DNDEBUG`), the `-MMD -MF <depfile>` (plus
145/// `-MT <stamp>` in syntax-only mode) dependency block, defines,
146/// includes, escape-hatch flags, and the mode-specific tail.
147fn compile_argv_gnu(compile: &CompileAction) -> Vec<String> {
148    let args = &compile.arguments;
149    let mut out: Vec<String> = Vec::new();
150    out.push(compile.compiler.as_str().to_owned());
151    out.push(gnu_std_flag(compile.language).to_owned());
152    out.push(args.opt_level.as_flag().to_owned());
153    if args.debug_info {
154        out.push("-g".to_owned());
155    }
156    if args.define_ndebug {
157        out.push("-DNDEBUG".to_owned());
158    }
159    if let Some(depfile) = &compile.depfile {
160        out.push("-MMD".to_owned());
161        out.push("-MF".to_owned());
162        out.push(depfile.as_str().to_owned());
163        // In syntax-only mode the depfile records the stamp (not an
164        // object) as its target, so header edits still invalidate the
165        // check via Ninja's `deps = gcc` machinery.
166        if let CompileMode::SyntaxOnly { stamp } = &compile.mode {
167            out.push("-MT".to_owned());
168            out.push(stamp.as_str().to_owned());
169        }
170    }
171    for define in &args.defines {
172        out.push(format!("-D{define}"));
173    }
174    for include in &args.include_dirs {
175        out.push("-I".to_owned());
176        out.push(include.as_str().to_owned());
177    }
178    out.extend(args.extra_flags.iter().cloned());
179    match &compile.mode {
180        CompileMode::Object => {
181            out.push("-c".to_owned());
182            out.push(compile.source.as_str().to_owned());
183            out.push("-o".to_owned());
184            out.push(compile.object.as_str().to_owned());
185        }
186        CompileMode::SyntaxOnly { .. } => {
187            out.push(compile.source.as_str().to_owned());
188            out.push("-fsyntax-only".to_owned());
189        }
190    }
191    out
192}
193
194fn lower_archive_gnu(archive: &ArchiveAction) -> Vec<String> {
195    // GNU `ar` archives with the `crs` mode flags (create, replace,
196    // write index): `ar crs <lib> <obj>...`.
197    let mut command = vec![
198        archive.archiver.as_str().to_owned(),
199        "crs".to_owned(),
200        archive.output.as_str().to_owned(),
201    ];
202    for input in &archive.inputs {
203        command.push(input.as_str().to_owned());
204    }
205    command
206}
207
208fn lower_link_gnu(link: &LinkAction) -> Vec<String> {
209    // `<driver> <inputs...> <ldflags...> -l<lib>... -o <exe>`.
210    // System libraries follow the archives so a static library's
211    // dependencies resolve left-to-right under GNU `ld`.
212    let mut command = vec![link.linker.as_str().to_owned()];
213    for input in &link.inputs {
214        command.push(input.as_str().to_owned());
215    }
216    command.extend(link.arguments.iter().cloned());
217    for lib in &link.link_libs {
218        command.push(format!("-l{lib}"));
219    }
220    command.push("-o".to_owned());
221    command.push(link.output.as_str().to_owned());
222    command
223}
224
225// ---------------------------------------------------------------
226// MSVC (`cl.exe` / `lib.exe`) dialect.
227// ---------------------------------------------------------------
228
229fn msvc_std_flag(language: SourceLanguage) -> &'static str {
230    match language {
231        SourceLanguage::C => "/std:c11",
232        SourceLanguage::Cxx => "/std:c++17",
233    }
234}
235
236/// The `cl.exe` flag that forces the source's language, prepended to the
237/// file name (`/Tp<file>` for C++, `/Tc<file>` for C).
238///
239/// `cl` infers language from extension and only defaults `.cpp` / `.cxx`
240/// to C++; Cabin also classifies `.cc`, `.c++`, and `.C` as C++ (and
241/// `.c` as C). Driving the language explicitly makes the translation
242/// unit follow Cabin's own source classification rather than `cl`'s
243/// extension table, so every supported extension compiles as the
244/// language Cabin intends.
245fn msvc_source_flag(language: SourceLanguage) -> &'static str {
246    match language {
247        SourceLanguage::C => "/Tc",
248        SourceLanguage::Cxx => "/Tp",
249    }
250}
251
252fn msvc_opt_flag(opt: OptLevel) -> &'static str {
253    match opt {
254        OptLevel::O0 => "/Od",
255        OptLevel::O1 | OptLevel::S | OptLevel::Z => "/O1",
256        // cl.exe has no `/O3`; `/O2` is its maximum speed setting.
257        OptLevel::O2 | OptLevel::O3 => "/O2",
258    }
259}
260
261/// MSVC `cl.exe` compile argv. Mirrors the GNU layout with MSVC
262/// spellings: `/std:` standard, `/EHsc` for C++ exceptions, `/O`
263/// optimization, `/Z7` debug info (embedded in the object so parallel
264/// compiles never contend on a shared PDB), `/showIncludes` for
265/// dependency discovery (no Makefile depfile), `/D` defines, `/I`
266/// includes, escape-hatch flags, and the mode-specific tail
267/// (`/c /Tp<src> /Fo<obj>` or `/Tp<src> /Zs`, with `/Tc` for C).
268fn compile_argv_msvc(compile: &CompileAction) -> Vec<String> {
269    let args = &compile.arguments;
270    let mut out: Vec<String> = vec![compile.compiler.as_str().to_owned(), "/nologo".to_owned()];
271    out.push(msvc_std_flag(compile.language).to_owned());
272    if compile.language == SourceLanguage::Cxx {
273        out.push("/EHsc".to_owned());
274    }
275    out.push(msvc_opt_flag(args.opt_level).to_owned());
276    if args.debug_info {
277        out.push("/Z7".to_owned());
278    }
279    if args.define_ndebug {
280        out.push("/DNDEBUG".to_owned());
281    }
282    // `/showIncludes` drives Ninja's `deps = msvc`. Emitted whenever
283    // the planner asked for dependency tracking, matching the GNU
284    // dialect's `-MMD -MF` condition.
285    if compile.depfile.is_some() {
286        out.push("/showIncludes".to_owned());
287    }
288    for define in &args.defines {
289        out.push(format!("/D{define}"));
290    }
291    for include in &args.include_dirs {
292        out.push("/I".to_owned());
293        out.push(include.as_str().to_owned());
294    }
295    out.extend(args.extra_flags.iter().cloned());
296    let source = format!(
297        "{}{}",
298        msvc_source_flag(compile.language),
299        compile.source.as_str()
300    );
301    match &compile.mode {
302        CompileMode::Object => {
303            out.push("/c".to_owned());
304            out.push(source);
305            out.push(format!("/Fo{}", compile.object));
306        }
307        CompileMode::SyntaxOnly { .. } => {
308            out.push(source);
309            out.push("/Zs".to_owned());
310        }
311    }
312    out
313}
314
315fn lower_archive_msvc(archive: &ArchiveAction) -> Vec<String> {
316    // `lib /nologo /OUT:<lib> <obj>...`.
317    let mut command = vec![
318        archive.archiver.as_str().to_owned(),
319        "/nologo".to_owned(),
320        format!("/OUT:{}", archive.output),
321    ];
322    for input in &archive.inputs {
323        command.push(input.as_str().to_owned());
324    }
325    command
326}
327
328fn lower_link_msvc(link: &LinkAction) -> Vec<String> {
329    // `<driver> /nologo <inputs...> <lib>.lib... /Fe<exe> [/link <ldflags...>]`.
330    // cl.exe consumes object and `.lib` inputs positionally and
331    // forwards `/link` options to the linker, so system libraries are
332    // spelled `<name>.lib` and passed as positional inputs after the
333    // archives rather than as GNU `-l<name>` flags.
334    let mut command = vec![link.linker.as_str().to_owned(), "/nologo".to_owned()];
335    for input in &link.inputs {
336        command.push(input.as_str().to_owned());
337    }
338    for lib in &link.link_libs {
339        command.push(format!("{lib}.lib"));
340    }
341    command.push(format!("/Fe{}", link.output));
342    if !link.arguments.is_empty() {
343        command.push("/link".to_owned());
344        command.extend(link.arguments.iter().cloned());
345    }
346    command
347}
348
349// ---------------------------------------------------------------
350// Archive / link dispatch.
351// ---------------------------------------------------------------
352
353fn lower_archive(dialect: Dialect, archive: &ArchiveAction) -> LoweredAction {
354    let command = match dialect {
355        Dialect::GnuLike => lower_archive_gnu(archive),
356        Dialect::Msvc => lower_archive_msvc(archive),
357    };
358    LoweredAction {
359        kind: LoweredActionKind::ArchiveStaticLibrary,
360        inputs: archive.inputs.clone(),
361        implicit_inputs: Vec::new(),
362        outputs: vec![archive.output.clone()],
363        depfile: None,
364        command,
365        description: archive.description.clone(),
366    }
367}
368
369fn lower_link(dialect: Dialect, link: &LinkAction) -> LoweredAction {
370    let command = match dialect {
371        Dialect::GnuLike => lower_link_gnu(link),
372        Dialect::Msvc => lower_link_msvc(link),
373    };
374    LoweredAction {
375        kind: LoweredActionKind::LinkExecutable,
376        inputs: link.inputs.clone(),
377        implicit_inputs: link.implicit_inputs.clone(),
378        outputs: vec![link.output.clone()],
379        depfile: None,
380        command,
381        description: link.description.clone(),
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::action::CompileArguments;
389
390    fn strs(v: &[&str]) -> Vec<String> {
391        v.iter().map(|s| (*s).to_owned()).collect()
392    }
393
394    /// A C++ compile exercising every argv segment: optimization +
395    /// debug + NDEBUG, depfile, a define, an include dir, and an
396    /// escape-hatch flag.
397    fn cxx_compile(mode: CompileMode) -> CompileAction {
398        CompileAction {
399            language: SourceLanguage::Cxx,
400            source: Utf8PathBuf::from("/abs/src/main.cc"),
401            object: Utf8PathBuf::from("/abs/build/main.o"),
402            mode,
403            implicit_inputs: vec![Utf8PathBuf::from("/abs/build/generated.h")],
404            depfile: Some(Utf8PathBuf::from("/abs/build/main.o.d")),
405            compiler: Utf8PathBuf::from("/usr/bin/g++"),
406            compiler_wrapper: None,
407            arguments: CompileArguments {
408                opt_level: OptLevel::O0,
409                debug_info: false,
410                define_ndebug: false,
411                include_dirs: vec![Utf8PathBuf::from("/abs/include")],
412                defines: strs(&["FOO=1"]),
413                extra_flags: strs(&["-Wall"]),
414            },
415            description: "CXX /abs/build/main.o".to_owned(),
416        }
417    }
418
419    #[test]
420    fn gnu_object_mode_lowers_to_historic_compile_argv() {
421        let lowered = lower(
422            Dialect::GnuLike,
423            &BuildAction::Compile(cxx_compile(CompileMode::Object)),
424        );
425        assert_eq!(lowered.kind, LoweredActionKind::CompileCpp);
426        assert_eq!(
427            lowered.outputs,
428            vec![Utf8PathBuf::from("/abs/build/main.o")]
429        );
430        assert_eq!(lowered.inputs, vec![Utf8PathBuf::from("/abs/src/main.cc")]);
431        assert_eq!(
432            lowered.depfile,
433            Some(Utf8PathBuf::from("/abs/build/main.o.d"))
434        );
435        assert_eq!(
436            lowered.command,
437            strs(&[
438                "/usr/bin/g++",
439                "-std=c++17",
440                "-O0",
441                "-MMD",
442                "-MF",
443                "/abs/build/main.o.d",
444                "-DFOO=1",
445                "-I",
446                "/abs/include",
447                "-Wall",
448                "-c",
449                "/abs/src/main.cc",
450                "-o",
451                "/abs/build/main.o",
452            ])
453        );
454    }
455
456    #[test]
457    fn gnu_debug_and_ndebug_flags_follow_std_and_opt() {
458        let mut c = cxx_compile(CompileMode::Object);
459        c.arguments.opt_level = OptLevel::O3;
460        c.arguments.debug_info = true;
461        c.arguments.define_ndebug = true;
462        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c));
463        assert_eq!(
464            &lowered.command[1..5],
465            &strs(&["-std=c++17", "-O3", "-g", "-DNDEBUG"])[..]
466        );
467    }
468
469    #[test]
470    fn gnu_syntax_only_mode_lowers_to_historic_check_argv() {
471        let stamp = Utf8PathBuf::from("/abs/build/main.o.check");
472        let lowered = lower(
473            Dialect::GnuLike,
474            &BuildAction::Compile(cxx_compile(CompileMode::SyntaxOnly {
475                stamp: stamp.clone(),
476            })),
477        );
478        assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
479        assert_eq!(lowered.outputs, vec![stamp]);
480        assert_eq!(
481            lowered.command,
482            strs(&[
483                "/usr/bin/g++",
484                "-std=c++17",
485                "-O0",
486                "-MMD",
487                "-MF",
488                "/abs/build/main.o.d",
489                "-MT",
490                "/abs/build/main.o.check",
491                "-DFOO=1",
492                "-I",
493                "/abs/include",
494                "-Wall",
495                "/abs/src/main.cc",
496                "-fsyntax-only",
497            ])
498        );
499    }
500
501    #[test]
502    fn c_compile_lowers_to_c_kinds_and_c_standard() {
503        let mut c = cxx_compile(CompileMode::Object);
504        c.language = SourceLanguage::C;
505        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c.clone()));
506        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
507        assert_eq!(lowered.command[1], "-std=c11");
508        c.mode = CompileMode::SyntaxOnly {
509            stamp: Utf8PathBuf::from("/abs/build/main.o.check"),
510        };
511        assert_eq!(
512            lower(Dialect::GnuLike, &BuildAction::Compile(c)).kind,
513            LoweredActionKind::SyntaxCheckC
514        );
515    }
516
517    #[test]
518    fn compiler_wrapper_prefixes_only_the_run_command() {
519        let mut c = cxx_compile(CompileMode::Object);
520        c.compiler_wrapper = Some(Utf8PathBuf::from("/usr/local/bin/ccache"));
521        let lowered = lower(Dialect::GnuLike, &BuildAction::Compile(c.clone()));
522        assert_eq!(lowered.command[0], "/usr/local/bin/ccache");
523        assert_eq!(lowered.command[1], "/usr/bin/g++");
524        // The shared argv builder (used for compile_commands) never
525        // sees the wrapper.
526        let unwrapped = compile_argv(Dialect::GnuLike, &c);
527        assert_eq!(unwrapped[0], "/usr/bin/g++");
528        assert!(!unwrapped.iter().any(|a| a == "/usr/local/bin/ccache"));
529    }
530
531    #[test]
532    fn gnu_archive_lowers_to_ar_crs_command() {
533        let action = BuildAction::Archive(ArchiveAction {
534            archiver: Utf8PathBuf::from("/usr/bin/ar"),
535            output: Utf8PathBuf::from("/abs/build/libfoo.a"),
536            inputs: vec![
537                Utf8PathBuf::from("/abs/build/a.o"),
538                Utf8PathBuf::from("/abs/build/b.o"),
539            ],
540            description: "AR /abs/build/libfoo.a".to_owned(),
541        });
542        let lowered = lower(Dialect::GnuLike, &action);
543        assert_eq!(lowered.kind, LoweredActionKind::ArchiveStaticLibrary);
544        assert_eq!(
545            lowered.command,
546            strs(&[
547                "/usr/bin/ar",
548                "crs",
549                "/abs/build/libfoo.a",
550                "/abs/build/a.o",
551                "/abs/build/b.o",
552            ])
553        );
554    }
555
556    #[test]
557    fn gnu_link_lowers_to_driver_inputs_ldflags_output() {
558        let action = BuildAction::Link(LinkAction {
559            linker: Utf8PathBuf::from("/usr/bin/g++"),
560            output: Utf8PathBuf::from("/abs/build/app"),
561            inputs: vec![
562                Utf8PathBuf::from("/abs/build/main.o"),
563                Utf8PathBuf::from("/abs/build/libfoo.a"),
564            ],
565            implicit_inputs: vec![],
566            arguments: strs(&["-Wl,--as-needed"]),
567            link_libs: vec![],
568            description: "LINK /abs/build/app".to_owned(),
569        });
570        let lowered = lower(Dialect::GnuLike, &action);
571        assert_eq!(lowered.kind, LoweredActionKind::LinkExecutable);
572        assert_eq!(
573            lowered.command,
574            strs(&[
575                "/usr/bin/g++",
576                "/abs/build/main.o",
577                "/abs/build/libfoo.a",
578                "-Wl,--as-needed",
579                "-o",
580                "/abs/build/app",
581            ])
582        );
583    }
584
585    // -----------------------------------------------------------
586    // MSVC dialect.
587    // -----------------------------------------------------------
588
589    /// The MSVC analogue of [`cxx_compile`], with MSVC-spelled
590    /// escape-hatch flags and `cl` paths.
591    fn msvc_cxx_compile(mode: CompileMode) -> CompileAction {
592        CompileAction {
593            language: SourceLanguage::Cxx,
594            source: Utf8PathBuf::from("C:/src/main.cc"),
595            object: Utf8PathBuf::from("C:/build/main.obj"),
596            mode,
597            implicit_inputs: vec![],
598            depfile: Some(Utf8PathBuf::from("C:/build/main.obj.d")),
599            compiler: Utf8PathBuf::from("cl.exe"),
600            compiler_wrapper: None,
601            arguments: CompileArguments {
602                opt_level: OptLevel::O2,
603                debug_info: true,
604                define_ndebug: true,
605                include_dirs: vec![Utf8PathBuf::from("C:/include")],
606                defines: strs(&["FOO=1"]),
607                extra_flags: strs(&["/W4"]),
608            },
609            description: "CXX C:/build/main.obj".to_owned(),
610        }
611    }
612
613    #[test]
614    fn msvc_object_mode_lowers_to_cl_argv_without_depfile() {
615        let lowered = lower(
616            Dialect::Msvc,
617            &BuildAction::Compile(msvc_cxx_compile(CompileMode::Object)),
618        );
619        assert_eq!(lowered.kind, LoweredActionKind::CompileCpp);
620        // MSVC tracks headers via `/showIncludes`, so no Makefile
621        // depfile is carried on the lowered action.
622        assert_eq!(lowered.depfile, None);
623        assert_eq!(
624            lowered.command,
625            strs(&[
626                "cl.exe",
627                "/nologo",
628                "/std:c++17",
629                "/EHsc",
630                "/O2",
631                "/Z7",
632                "/DNDEBUG",
633                "/showIncludes",
634                "/DFOO=1",
635                "/I",
636                "C:/include",
637                "/W4",
638                "/c",
639                "/TpC:/src/main.cc",
640                "/FoC:/build/main.obj",
641            ])
642        );
643    }
644
645    #[test]
646    fn msvc_syntax_only_uses_zs_and_no_output() {
647        let stamp = Utf8PathBuf::from("C:/build/main.obj.check");
648        let lowered = lower(
649            Dialect::Msvc,
650            &BuildAction::Compile(msvc_cxx_compile(CompileMode::SyntaxOnly {
651                stamp: stamp.clone(),
652            })),
653        );
654        assert_eq!(lowered.kind, LoweredActionKind::SyntaxCheckCpp);
655        assert_eq!(lowered.outputs, vec![stamp]);
656        let tail = &lowered.command[lowered.command.len() - 2..];
657        assert_eq!(tail, &strs(&["/TpC:/src/main.cc", "/Zs"])[..]);
658        assert!(!lowered.command.iter().any(|a| a.starts_with("/Fo")));
659    }
660
661    #[test]
662    fn msvc_c_compile_uses_c_standard_and_no_ehsc() {
663        let mut c = msvc_cxx_compile(CompileMode::Object);
664        c.language = SourceLanguage::C;
665        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(c));
666        assert_eq!(lowered.kind, LoweredActionKind::CompileC);
667        assert!(lowered.command.iter().any(|a| a == "/std:c11"));
668        assert!(!lowered.command.iter().any(|a| a == "/EHsc"));
669    }
670
671    #[test]
672    fn msvc_forces_source_language_per_classification() {
673        // `cl` only defaults `.cpp`/`.cxx` to C++; Cabin drives the
674        // language explicitly so any supported extension compiles as the
675        // language Cabin classified it. The source token carries `/Tp`
676        // for C++ and `/Tc` for C, with no bare source argument.
677        let cxx = lower(
678            Dialect::Msvc,
679            &BuildAction::Compile(msvc_cxx_compile(CompileMode::Object)),
680        );
681        assert!(cxx.command.iter().any(|a| a == "/TpC:/src/main.cc"));
682        assert!(!cxx.command.iter().any(|a| a == "C:/src/main.cc"));
683
684        let mut c_action = msvc_cxx_compile(CompileMode::Object);
685        c_action.language = SourceLanguage::C;
686        let c = lower(Dialect::Msvc, &BuildAction::Compile(c_action));
687        assert!(c.command.iter().any(|a| a == "/TcC:/src/main.cc"));
688        assert!(!c.command.iter().any(|a| a == "/TpC:/src/main.cc"));
689    }
690
691    #[test]
692    fn msvc_debug_off_omits_z7_and_opt_maps_o3_to_o2() {
693        let mut c = msvc_cxx_compile(CompileMode::Object);
694        c.arguments.debug_info = false;
695        c.arguments.define_ndebug = false;
696        c.arguments.opt_level = OptLevel::O3;
697        let lowered = lower(Dialect::Msvc, &BuildAction::Compile(c));
698        assert!(!lowered.command.iter().any(|a| a == "/Z7"));
699        assert!(!lowered.command.iter().any(|a| a == "/DNDEBUG"));
700        assert!(lowered.command.iter().any(|a| a == "/O2"));
701    }
702
703    #[test]
704    fn msvc_archive_lowers_to_lib_out_command() {
705        let action = BuildAction::Archive(ArchiveAction {
706            archiver: Utf8PathBuf::from("lib.exe"),
707            output: Utf8PathBuf::from("C:/build/foo.lib"),
708            inputs: vec![
709                Utf8PathBuf::from("C:/build/a.obj"),
710                Utf8PathBuf::from("C:/build/b.obj"),
711            ],
712            description: "AR C:/build/foo.lib".to_owned(),
713        });
714        let lowered = lower(Dialect::Msvc, &action);
715        assert_eq!(
716            lowered.command,
717            strs(&[
718                "lib.exe",
719                "/nologo",
720                "/OUT:C:/build/foo.lib",
721                "C:/build/a.obj",
722                "C:/build/b.obj",
723            ])
724        );
725    }
726
727    #[test]
728    fn msvc_link_lowers_to_cl_fe_with_link_options() {
729        let action = BuildAction::Link(LinkAction {
730            linker: Utf8PathBuf::from("cl.exe"),
731            output: Utf8PathBuf::from("C:/build/app.exe"),
732            inputs: vec![
733                Utf8PathBuf::from("C:/build/main.obj"),
734                Utf8PathBuf::from("C:/build/foo.lib"),
735            ],
736            implicit_inputs: vec![],
737            arguments: strs(&["/SUBSYSTEM:CONSOLE"]),
738            link_libs: vec![],
739            description: "LINK C:/build/app.exe".to_owned(),
740        });
741        let lowered = lower(Dialect::Msvc, &action);
742        assert_eq!(
743            lowered.command,
744            strs(&[
745                "cl.exe",
746                "/nologo",
747                "C:/build/main.obj",
748                "C:/build/foo.lib",
749                "/FeC:/build/app.exe",
750                "/link",
751                "/SUBSYSTEM:CONSOLE",
752            ])
753        );
754    }
755
756    #[test]
757    fn msvc_link_without_ldflags_omits_link_separator() {
758        let action = BuildAction::Link(LinkAction {
759            linker: Utf8PathBuf::from("cl.exe"),
760            output: Utf8PathBuf::from("C:/build/app.exe"),
761            inputs: vec![Utf8PathBuf::from("C:/build/main.obj")],
762            implicit_inputs: vec![],
763            arguments: vec![],
764            link_libs: vec![],
765            description: "LINK C:/build/app.exe".to_owned(),
766        });
767        let lowered = lower(Dialect::Msvc, &action);
768        assert_eq!(
769            lowered.command,
770            strs(&[
771                "cl.exe",
772                "/nologo",
773                "C:/build/main.obj",
774                "/FeC:/build/app.exe",
775            ])
776        );
777    }
778
779    #[test]
780    fn gnu_link_lowers_link_libs_after_archives() {
781        // System libraries are spelled `-l<name>` and placed after the
782        // archive inputs and ldflags so GNU `ld` resolves them
783        // left-to-right against the archives that reference them.
784        let action = BuildAction::Link(LinkAction {
785            linker: Utf8PathBuf::from("/usr/bin/cc"),
786            output: Utf8PathBuf::from("/abs/build/app"),
787            inputs: vec![
788                Utf8PathBuf::from("/abs/build/main.o"),
789                Utf8PathBuf::from("/abs/build/libsqlite3.a"),
790            ],
791            implicit_inputs: vec![],
792            arguments: vec![],
793            link_libs: strs(&["pthread", "m"]),
794            description: "LINK /abs/build/app".to_owned(),
795        });
796        let lowered = lower(Dialect::GnuLike, &action);
797        assert_eq!(
798            lowered.command,
799            strs(&[
800                "/usr/bin/cc",
801                "/abs/build/main.o",
802                "/abs/build/libsqlite3.a",
803                "-lpthread",
804                "-lm",
805                "-o",
806                "/abs/build/app",
807            ])
808        );
809    }
810
811    #[test]
812    fn msvc_link_lowers_link_libs_as_dot_lib_inputs() {
813        // MSVC `link` has no `-l<name>`; system libraries are spelled
814        // `<name>.lib` and passed as positional inputs (after the
815        // archives, before `/Fe`), not GNU-style flags.
816        let action = BuildAction::Link(LinkAction {
817            linker: Utf8PathBuf::from("cl.exe"),
818            output: Utf8PathBuf::from("C:/build/app.exe"),
819            inputs: vec![Utf8PathBuf::from("C:/build/main.obj")],
820            implicit_inputs: vec![],
821            arguments: vec![],
822            link_libs: strs(&["user32"]),
823            description: "LINK C:/build/app.exe".to_owned(),
824        });
825        let lowered = lower(Dialect::Msvc, &action);
826        assert_eq!(
827            lowered.command,
828            strs(&[
829                "cl.exe",
830                "/nologo",
831                "C:/build/main.obj",
832                "user32.lib",
833                "/FeC:/build/app.exe",
834            ])
835        );
836    }
837}