From f8c97160a8efe2d3993e2a660f76a1958a455e52 Mon Sep 17 00:00:00 2001 From: Sam Dietrich Date: Fri, 8 May 2026 12:07:47 -0700 Subject: [PATCH 1/8] add list deps after parse --- BUILDING.md | 122 +++++++++++++++-- CHANGELOG.md | 7 + compiler/hs/app/Main.hs | 98 ++++++++++++-- compiler/hs/app/Options.hs | 17 +++ compiler/hs/app/Options/CmdArgs.hs | 18 ++- test/compiler/cli/CMakeLists.txt | 86 ++++++++++-- test/compiler/cli/check_list_deps.py | 192 +++++++++++++++++++++++++++ test/compiler/cli/list_deps.k | 19 +++ 8 files changed, 529 insertions(+), 30 deletions(-) create mode 100644 test/compiler/cli/check_list_deps.py create mode 100644 test/compiler/cli/list_deps.k diff --git a/BUILDING.md b/BUILDING.md index f5f83154..1876ce6f 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -222,21 +222,125 @@ cmake \ ## Building on Windows At this time, the unit tests are only supported on Linux. However, you can build and run the compiler and -related tools on Windows. Here are the dependencies that you must install: +related tools on Windows. The following step-by-step guide assumes a PowerShell session and Visual +Studio 2022 (any edition with the "Desktop development with C++" workload, including Community). +Adjust paths as appropriate for your environment. -- C/C++ compiler. CMake should auto-detect it. Visual Studio 2022 Community Edition is a good option. -- CMake version 3.30 or later (see https://cmake.org/) -- Ninja. It's not required to use Ninja; you can use any build tool supported by CMake, but we recommend Ninja for its speed and simplicity (see https://ninja-build.org/) -- [ghcup](https://www.haskell.org/ghcup/). After installation, close and re-open your Powershell session and then run `ghcup tui` to launch the interactive version of ghcup. Install Haskell 9.6.7 and cabal 3.12.1.0. Run `cabal update` after installation. -- Boost C++ library version 1.88.0 or later (see https://www.boost.org/). After extracting the archive, you will need to change into the extracted directory and run `.\bootstrap.bat` and then `.\b2.exe install --prefix=target_directory` (substitute `target_directory` with where you want Boost installed). The proper value to pass as Boost_DIR to CMake will be something like `...\boost_1_88_0\stage\lib\cmake\Boost-1.88.0`. -To tell CMake about Boost so that the `find_package` command can find it, add `\lib\cmake` to `CMAKE_PREFIX_PATH`. For example: `-DCMAKE_PREFIX_PATH=D:\boost.1.88.0\lib\cmake` +### 1. Install Visual Studio 2022 + +Install Visual Studio 2022 with the "Desktop development with C++" workload. This provides the MSVC +compiler, CMake, and Ninja. + +To make `cl.exe`, `cmake`, and `ninja` available in your PowerShell session, enter the +"Developer PowerShell for VS 2022" environment. + +Verify the tools are on your `PATH`: + +```powershell +cmake --version +ninja --version +``` + +### 2. Install GHC and cabal via ghcup + +Run the official ghcup bootstrap script in a non-interactive form so it can be scripted. The +arguments below skip the interactive prompts and install ghcup but not GHC, cabal, stack, or HLS +(we install specific versions of GHC and cabal in the next step): + +```powershell +Set-ExecutionPolicy Bypass -Scope Process -Force +[System.Net.ServicePointManager]::SecurityProtocol = + [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 +Invoke-Command -ScriptBlock ([ScriptBlock]::Create( + (Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) ` + -ArgumentList $false,$true,$true,$false,$false,$false,$false,"","","","" +``` + +This installs ghcup at `C:\ghcup`. Add it to `PATH` for the current session, then install the +required versions of GHC and cabal, set them as the default, and update the Hackage index: + +```powershell +$env:Path = "C:\ghcup\bin;" + $env:Path +ghcup install ghc 9.6.7 +ghcup install cabal 3.12.1.0 +ghcup set ghc 9.6.7 +ghcup set cabal 3.12.1.0 +cabal update +``` + +Optionally, redirect the cabal package store to a different drive (useful if your system drive is +small): + +```powershell +cabal user-config update -a "store-dir: E:\cache\cabal" +``` + +### 3. Install Boost 1.88.0 + +Download and extract the Boost source archive. The extraction step takes a few minutes because +Boost contains tens of thousands of files: + +```powershell +New-Item -ItemType Directory -Path E:\cache\boost -Force | Out-Null +Invoke-WebRequest ` + -Uri "https://archives.boost.io/release/1.88.0/source/boost_1_88_0.zip" ` + -OutFile "E:\cache\boost_1_88_0.zip" -UseBasicParsing +Expand-Archive -Path "E:\cache\boost_1_88_0.zip" -DestinationPath "E:\cache\boost" -Force +``` + +Bootstrap and install Boost. Kanagawa only needs the Boost headers, so a headers-only install is +sufficient. The recommended approach is to use `b2` to generate the CMake config files, then use +`robocopy` to copy the headers in bulk (much faster than `b2`'s file-by-file install on Windows): + +```powershell +cd E:\cache\boost\boost_1_88_0 +.\bootstrap.bat +# Start the install to generate the CMake config files in the install prefix. +# You can cancel (Ctrl+C) once you see headers being copied -- the CMake config +# files are written early. +.\b2.exe install --prefix=E:\cache\boost\install --with-headers +# Bulk-copy all headers to the install prefix using robocopy +robocopy "E:\cache\boost\boost_1_88_0\boost" ` + "E:\cache\boost\install\include\boost-1_88\boost" /E /NFL /NDL /NJH /NP +``` + +Alternatively, you can let `b2 install` run to completion (slower, but simpler). -Run cmake generate to initialize the build system. Here's an example command line - replace the paths with values appropriate for your set-up: +### 4. Configure the build with CMake +From the repository root, run CMake generate. The example below uses +`E:\cache\boost\install` for Boost and `C:\ghcup\bin` for ghcup; adjust paths as needed: + +```powershell +cmake -S E:\git\kanagawa -B E:\git\kanagawa-build -G Ninja ` + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` + -DBoost_DIR=E:\cache\boost\install\lib\cmake\Boost-1.88.0 ` + -DGHCUP_DIR=C:\ghcup\bin ``` -cmake -S kanagawa -B kanagawa-build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_PREFIX_PATH=D:\boost.1.88.0\lib\cmake -DGHCUP_DIR=D:/ghcup + +The configure step takes a couple of minutes (CMake configures the bundled LLVM/CIRCT submodule +in addition to Kanagawa itself). When it finishes, you should see `Build files have been written +to: E:/git/kanagawa-build`. + +### 5. Build the compiler + +Build the `kanagawa_runtime` target. This compiles LLVM, MLIR, CIRCT, and the Kanagawa compiler, +and is a long-running build (tens of minutes to over an hour depending on your machine): + +```powershell +ninja -C E:\git\kanagawa-build kanagawa_runtime ``` +The resulting `kanagawa.exe` and `kanagawa-backend.dll` are staged in +`E:\git\kanagawa-build\dist\bin`. + +### Notes + +- Verilator and the RISC-V GCC cross-compiler are not available on Windows out of the box, so + the corresponding tests are disabled automatically. +- If you re-open your PowerShell session, you must re-enter the VS dev environment and re-add ghcup to `PATH` (`$env:Path = "C:\ghcup\bin;" + $env:Path`) + before running `cmake` or `ninja`. + ## Preparing a release There is a GitHub workflow (Nightly prerelease) that runs nightly at 08:00 UTC. If there have been any changes checked into main since it last run, it will prepare and publish a release. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9b51e7..88f65eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All NOTABLE changes to this project will be documented in this file. Check the release summary for a detailed history based on commits. +## [Unreleased] +### Added +- `list-deps` sub-command that enumerates the transitive `.k` source files needed to compile a program by running parse + import resolution only (no frontend, no codegen). Suitable for fast dependency tracking from build systems (e.g. CMake configure-time staleness checks). Supports plain (one path per line) and Makefile-style (`gcc -M`) output formats; writes to `--file-list ` or stdout. + +### Changed +- `compile --file-list` now refreshes the dependency manifest immediately after parsing succeeds (instead of after codegen). Failed compiles (frontend or backend errors) still update the manifest, so build systems can pick up newly added imports without requiring a successful build. + ## [1.2.0] - 2025-11-09 ### Added - MacOS (ARM64) support diff --git a/compiler/hs/app/Main.hs b/compiler/hs/app/Main.hs index f2df8ff3..020ceffb 100644 --- a/compiler/hs/app/Main.hs +++ b/compiler/hs/app/Main.hs @@ -20,13 +20,14 @@ import Language.Kanagawa.Parser.Syntax import Language.Kanagawa.PrettyPrint import Language.Kanagawa.Type import Language.Kanagawa.Warning -import Options (Layout(Pretty, Smart), Options(Compile, PrettyPrint)) +import Options (Layout(Pretty, Smart), ListDepsFormat(Make, Plain), Options(Compile, ListDeps, PrettyPrint)) import Options.CmdArgs import qualified Options as O import ParseTree import System.Directory import System.Environment (getArgs, withArgs, getProgName) import System.Exit +import System.FilePath (takeFileName) import System.IO import Text.Megaparsec.Error @@ -39,6 +40,11 @@ main = do then exitError "Missing source filename(s)" else handle options $ unwords $ prog : args +-- Filter out internal synthetic modules (those whose names start with '.', +-- e.g. .cmdargs.k, .options.k) and sort the remaining canonical paths. +filterAndSortFiles :: [FilePath] -> [FilePath] +filterAndSortFiles = filter (('.' /=) . head) . sort + handle :: Options -> String -> IO () -- Pretty print files specified via command line options @@ -70,6 +76,12 @@ handle opt@Compile{..} cmdArgs = do let (fileNames, results) = unzip parsedFiles parseErrors = lefts results exprs = map fst $ rights results + -- Refresh the dependency manifest immediately after parsing succeeds. + -- Doing this here (rather than after codegen) means the manifest is + -- updated even when frontend or codegen fails, so build systems can + -- pick up new imports without requiring a successful build. + when (not (null file_list) && not (null fileNames) && null parseErrors) $ + writeFileListPlain file_list $ filterAndSortFiles fileNames if not $ null parseErrors then exitErrors parseErrors else do @@ -95,8 +107,6 @@ handle opt@Compile{..} cmdArgs = do exitError "Error 1: Warnings treated as errors" hFlush stdout success <- compile opt cmdArgs fileNames program - when (not (null file_list) && not (null fileNames)) $ - updateFileList file_list $ filter (('.' /=) . head) $ sort fileNames if success then exitSuccess else exitFailure @@ -104,22 +114,80 @@ handle opt@Compile{..} cmdArgs = do append (NotedExp _ (SeqF x)) (NotedExp n (SeqF y)) = NotedExp n (SeqF (x ++ y)) append _ _ = undefined +-- Enumerate the transitive set of source files needed to compile the program +-- by running parse + import resolution only. Skips frontend and codegen, so +-- this is suitable as a fast dependency-list generator for build systems. +handle opt@ListDeps{..} _ = do + parsedFiles <- flip execStateT [] $ parseProgram $ getParseOptions opt + let (fileNames, results) = unzip parsedFiles + parseErrors = lefts results + if not $ null parseErrors + then exitErrors parseErrors + else do + let paths = filterAndSortFiles fileNames + rendered = case list_deps_format of + Plain -> renderFileListPlain paths + Make -> renderFileListMake (defaultMakeTarget make_target files) paths + if null file_list + then putStr rendered + else writeIfChanged file_list rendered + exitSuccess + -- Run languange server --handle LangServer{..} = runLangServer log_file -- Print usage handle opt _ = print opt -updateFileList :: FilePath -> [FilePath] -> IO () -updateFileList listfile parsedFiles = do - old <- lines <$> listfileContent - when (old /= parsedFiles) $ - writeFile listfile $ unlines parsedFiles +-- | Render a sorted list of dependency paths in the simple plain format: +-- one absolute path per line, terminated with a newline. +renderFileListPlain :: [FilePath] -> String +renderFileListPlain = unlines + +-- | Render a sorted list of dependency paths as a Makefile rule: +-- +-- : \ +-- \ +-- +-- +-- Spaces in paths are escaped with a backslash, matching the convention used +-- by gcc/clang's @-M@ output. The @@ argument is used verbatim. +renderFileListMake :: String -> [FilePath] -> String +renderFileListMake target [] = target ++ ":\n" +renderFileListMake target paths = + target ++ ":" ++ concatMap (" \\\n " ++) (map escape paths) ++ "\n" + where + escape = concatMap escapeChar + escapeChar ' ' = "\\ " + escapeChar '\t' = "\\\t" + escapeChar c = [c] + +-- | Pick a default Make target name when @--target@ was not supplied: use the +-- file name (no directory) of the first input source. +defaultMakeTarget :: String -> [FilePath] -> String +defaultMakeTarget explicit srcs + | not (null explicit) = explicit + | (s:_) <- srcs = takeFileName s + | otherwise = "deps" + +-- | Convenience wrapper used by the @compile@ path: write the plain-format +-- file list to disk only if the contents differ from what is already on +-- disk. Avoids touching mtime when nothing has changed (CMake-friendly). +writeFileListPlain :: FilePath -> [FilePath] -> IO () +writeFileListPlain listfile = writeIfChanged listfile . renderFileListPlain + +-- | Write @content@ to @path@ only if the existing file's contents differ. +-- A non-existent file is treated as having empty contents. +writeIfChanged :: FilePath -> String -> IO () +writeIfChanged path content = do + old <- listfileContent + when (old /= content) $ + writeFile path content where listfileContent = do - exists <- doesFileExist listfile + exists <- doesFileExist path if exists - then T.unpack <$> TIO.readFile listfile + then T.unpack <$> TIO.readFile path else return "" getParseOptions :: Options -> ParseOptions @@ -149,4 +217,14 @@ getParseOptions opt = case opt of , targetDevice = target_device , using = using } + ListDeps{..} -> defaultOptions + { baseLibrary = base_library + , define = define + , files = files + , importDir = import_dir + , noImplicitBase = no_implicit_base + , parseDocs = parse_docs + , targetDevice = target_device + , using = using + } _ -> error "Unsupported mode." diff --git a/compiler/hs/app/Options.hs b/compiler/hs/app/Options.hs index 70a59dd7..3b547829 100644 --- a/compiler/hs/app/Options.hs +++ b/compiler/hs/app/Options.hs @@ -7,6 +7,7 @@ Licensed under the MIT License. module Options ( Layout(..) + , ListDepsFormat(..) , Options(..) , Backend(..) ) where @@ -18,6 +19,9 @@ import Language.Kanagawa.Warning data Layout = Smart | Pretty deriving (Show, Data, Typeable) +data ListDepsFormat = Plain | Make + deriving (Show, Data, Typeable) + data Options = Options | PrettyPrint @@ -129,6 +133,19 @@ data Options , write_ir_post_opt :: Bool , skip_circt_lowering :: Bool } + | ListDeps + { base_library :: FilePath + , define :: [String] + , file_list :: FilePath + , files :: [FilePath] + , import_dir :: [FilePath] + , list_deps_format :: ListDepsFormat + , make_target :: String + , no_implicit_base :: Bool + , parse_docs :: Bool + , target_device :: String + , using :: [String] + } | LangServer { verbose :: Bool , log_file :: Maybe String diff --git a/compiler/hs/app/Options/CmdArgs.hs b/compiler/hs/app/Options/CmdArgs.hs index ff92b778..f88475cf 100644 --- a/compiler/hs/app/Options/CmdArgs.hs +++ b/compiler/hs/app/Options/CmdArgs.hs @@ -152,8 +152,24 @@ pretty = PrettyPrint name "pretty" &= help "Parse and pretty print source file(s)" +listDeps :: Options +listDeps = ListDeps + { list_deps_format = Plain &= help "Output format: plain (one path per line) or make (Makefile-style)" &= name "format" &= explicit &= typ "FORMAT" + , make_target = "" &= help "Target name for Makefile-style output; defaults to the first source file" &= name "target" &= explicit &= typ "STRING" + } &= + name "list-deps" &= + help "List the transitive .k source files needed to compile (parse only; no frontend or codegen)" &= + details [ "Usage example:" + , " Print the dependency list for source.k to stdout:" + , " kanagawa list-deps source.k" + , " Write the dependency list to a file:" + , " kanagawa list-deps --file-list=deps.txt source.k" + , " Emit Makefile-style dependencies for build-system integration:" + , " kanagawa list-deps --format=make --target=source.o --file-list=source.d source.k" + ] + mode :: Mode (CmdArgs Options) -mode = cmdArgsMode $ modes [compile &= auto, pretty] &= +mode = cmdArgsMode $ modes [compile &= auto, pretty, listDeps] &= program "kanagawa" &= verbosity &= help "Kanagawa compiler" &= diff --git a/test/compiler/cli/CMakeLists.txt b/test/compiler/cli/CMakeLists.txt index 858c4fcb..c7138bb7 100644 --- a/test/compiler/cli/CMakeLists.txt +++ b/test/compiler/cli/CMakeLists.txt @@ -4,6 +4,15 @@ # CLI-level tests that exercise specific kanagawa command-line options # by inspecting the files produced by the compiler. +# The verifier scripts are in Python; resolve the interpreter once via +# CMake's bundled finder (CIRCT already calls `find_package(Python3)`, +# which sets Python3_EXECUTABLE on every platform). Falling back to a bare +# `python3` only works on POSIX-like systems where it is on PATH. +if(NOT Python3_EXECUTABLE) + find_package(Python3 COMPONENTS Interpreter REQUIRED) +endif() +set(_cli_python "${Python3_EXECUTABLE}") + # Helper function for adding a CLI-level CTest. The test invokes the Kanagawa # compiler with the supplied OPTIONS and then runs the supplied verification # command(s) (TEST). Each verification command is its own CTest, chained @@ -16,17 +25,22 @@ # SOURCES [ ...] # [OPTIONS ...] # [OUTPUT_PREFIX ] # default: test +# [SUBCOMMAND ] # e.g. list-deps; default: omit (auto compile) +# [NO_AUTO_OUTPUT] # do not pass --output=/ # [TEST ...] # ) # # A separate output directory is created for each test under the current -# binary dir, and the compiler is invoked with --output=/. -# CTest names are formed by prefixing the test name with 'cli.' (e.g. -# cli..compile, cli..test1, cli..test2). +# binary dir, and (unless NO_AUTO_OUTPUT is set) the compiler is invoked with +# --output=/. CTest names are formed by prefixing the test +# name with 'cli.' (e.g. cli..compile, cli..test1, +# cli..test2). Test commands can reference @OUTDIR@ to substitute +# the absolute path of the per-test output directory. function(add_cli_test test_name) - set(_one OUTPUT_PREFIX) + set(_opt NO_AUTO_OUTPUT) + set(_one OUTPUT_PREFIX SUBCOMMAND) set(_multi SOURCES OPTIONS TEST) - cmake_parse_arguments(_ARG "" "${_one}" "${_multi}" ${ARGN}) + cmake_parse_arguments(_ARG "${_opt}" "${_one}" "${_multi}" ${ARGN}) if(NOT test_name) message(FATAL_ERROR "add_cli_test: missing .") @@ -41,6 +55,29 @@ function(add_cli_test test_name) set(_outdir "${CMAKE_CURRENT_BINARY_DIR}/${test_name}") set(_fixture "cli_${test_name}") + # Build the kanagawa command line, optionally prefixed by a sub-command + # and optionally suppressing the auto --output flag. @OUTDIR@ in any + # OPTIONS or SOURCES entry is substituted with the per-test output dir. + set(_compile_cmd $) + if(_ARG_SUBCOMMAND) + list(APPEND _compile_cmd ${_ARG_SUBCOMMAND}) + endif() + set(_subst_options "") + foreach(_opt IN LISTS _ARG_OPTIONS) + string(REPLACE "@OUTDIR@" "${_outdir}" _opt "${_opt}") + list(APPEND _subst_options "${_opt}") + endforeach() + list(APPEND _compile_cmd ${_subst_options}) + if(NOT _ARG_NO_AUTO_OUTPUT) + list(APPEND _compile_cmd --output=${_outdir}/${_ARG_OUTPUT_PREFIX}) + endif() + set(_subst_sources "") + foreach(_src IN LISTS _ARG_SOURCES) + string(REPLACE "@OUTDIR@" "${_outdir}" _src "${_src}") + list(APPEND _subst_sources "${_src}") + endforeach() + list(APPEND _compile_cmd ${_subst_sources}) + # Compile step: clean the output dir, recreate it, then run the compiler. # add_test only allows one COMMAND, so drive the prep + compile via # `cmake -E env --` which simply forwards to the kanagawa exe after the @@ -65,10 +102,7 @@ function(add_cli_test test_name) add_test( NAME cli.${test_name}.compile - COMMAND $ - ${_ARG_OPTIONS} - --output=${_outdir}/${_ARG_OUTPUT_PREFIX} - ${_ARG_SOURCES} + COMMAND ${_compile_cmd} ) set_tests_properties(cli.${test_name}.compile PROPERTIES FIXTURES_REQUIRED "${_fixture}_dir" @@ -79,6 +113,8 @@ function(add_cli_test test_name) set(_idx 0) foreach(_cmd IN LISTS _ARG_TEST) math(EXPR _idx "${_idx} + 1") + # @OUTDIR@ is a convenience placeholder for the per-test output dir. + string(REPLACE "@OUTDIR@" "${_outdir}" _cmd "${_cmd}") # Split the command string on spaces into a list so the first token is # the program and the remainder are arguments. separate_arguments(_cmd_list UNIX_COMMAND "${_cmd}") @@ -102,5 +138,35 @@ add_cli_test(skip_circt_lowering --import-dir=${CMAKE_SOURCE_DIR}/library --place-iterations=1 TEST - "python3 ${CMAKE_CURRENT_SOURCE_DIR}/check_skip_circt_lowering.py ${CMAKE_CURRENT_BINARY_DIR}/skip_circt_lowering" + "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_skip_circt_lowering.py ${CMAKE_CURRENT_BINARY_DIR}/skip_circt_lowering" +) + +# `list-deps` sub-command: parse + import resolution only, fast enough to run +# during cmake configure for staleness detection. The compile step here is +# the list-deps invocation itself; the verifier asserts the manifest is +# present, well-formed, and that no codegen artifacts were produced. +add_cli_test(list_deps_plain + SUBCOMMAND list-deps + NO_AUTO_OUTPUT + SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k + OPTIONS + --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k + --import-dir=${CMAKE_SOURCE_DIR}/library + --file-list=@OUTDIR@/deps.txt + TEST + "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_list_deps.py --source ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k --plain @OUTDIR@/deps.txt --no-codegen-dir @OUTDIR@" +) + +add_cli_test(list_deps_make + SUBCOMMAND list-deps + NO_AUTO_OUTPUT + SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k + OPTIONS + --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k + --import-dir=${CMAKE_SOURCE_DIR}/library + --file-list=@OUTDIR@/deps.mk + --format=make + --target=list_deps.o + TEST + "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_list_deps.py --source ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k --make @OUTDIR@/deps.mk --make-target list_deps.o --no-codegen-dir @OUTDIR@" ) diff --git a/test/compiler/cli/check_list_deps.py b/test/compiler/cli/check_list_deps.py new file mode 100644 index 00000000..ffc10a30 --- /dev/null +++ b/test/compiler/cli/check_list_deps.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +""" +Verify the outputs of a `kanagawa list-deps` invocation. + +Contract of the sub-command: parse + import resolution only. The compiler +must enumerate the transitive set of `.k` source files reachable from the +given input(s) and never invoke the frontend or codegen. Specifically: + + 1. The plain-format manifest exists and lists at least the input source + and one transitive import (the explicit `data.closure` import in + `list_deps.k` ensures the walker followed at least one edge). + 2. Every line in the manifest resolves to an existing absolute path on + disk; internal synthetic modules (`.cmdargs.k`, `.options.k`) are + excluded by the compiler. + 3. The output is sorted, contains no duplicates, and ends with a single + trailing newline. + 4. No codegen artifacts (.sv / .mlir / metadata) were produced in the + test output directory; presence of any such file would indicate the + sub-command did not short-circuit before codegen. + 5. The Make-format manifest, when supplied, starts with `:` and + uses the `\\ ` continuation convention. + +Exits non-zero on failure. +""" +import argparse +import sys +from pathlib import Path + + +def _read_lines(path: Path): + text = path.read_text() + if not text.endswith("\n"): + print(f"{path.name}: manifest does not end with a newline.") + return None + # splitlines() drops the trailing empty element produced by the final \n. + return text.splitlines() + + +def _check_plain(manifest: Path, source: Path): + ok = True + if not manifest.is_file(): + print(f"plain manifest does not exist: {manifest}") + return False + + lines = _read_lines(manifest) + if lines is None: + return False + + if not lines: + print(f"plain manifest is empty: {manifest}") + return False + + if sorted(lines) != lines: + print(f"plain manifest is not sorted: {manifest}") + ok = False + + if len(set(lines)) != len(lines): + print(f"plain manifest contains duplicates: {manifest}") + ok = False + + for line in lines: + if line.startswith("."): + print( + f"plain manifest contains an internal synthetic module " + f"(should be filtered): {line}" + ) + ok = False + # Each line should resolve to an existing file on disk. + if not Path(line).is_file(): + print( + f"plain manifest references a missing file (path may " + f"not be canonical): {line}" + ) + ok = False + + # The user's source file must appear (path is canonicalised by the + # compiler, so compare via Path). + src_resolved = source.resolve() + if src_resolved not in {Path(line) for line in lines}: + print( + f"plain manifest is missing the input source {src_resolved}; " + f"got entries: {lines}" + ) + ok = False + + # At least one transitive dependency must appear (the input file alone + # is not enough -- mini-base or data.closure must have been followed). + if len(lines) < 2: + print( + f"plain manifest has only {len(lines)} entry; expected the " + f"transitive walker to pull in at least one import." + ) + ok = False + + return ok + + +def _check_no_codegen(out_dir: Path): + ok = True + forbidden = [] + for pattern in ("*.sv", "*.mlir", "*.json", "*.dgml"): + forbidden.extend(out_dir.glob(pattern)) + if forbidden: + names = ", ".join(p.name for p in forbidden) + print( + f"list-deps must not emit codegen artifacts, but found in " + f"{out_dir}: {names}" + ) + ok = False + return ok + + +def _check_make(manifest: Path, target: str): + ok = True + if not manifest.is_file(): + print(f"make manifest does not exist: {manifest}") + return False + + text = manifest.read_text() + if not text.startswith(f"{target}:"): + print( + f"make manifest must start with '{target}:'; got: " + f"{text[: len(target) + 16]!r}" + ) + ok = False + + if not text.endswith("\n"): + print("make manifest does not end with a newline.") + ok = False + + # A non-empty deps list uses ' \ ' between deps. + # Multi-line continuation must use a backslash at end of line. + body = text[len(target) + 1 :] + if "\\\n" not in body and body.strip(): + print( + "make manifest body has no '\\' continuation; " + "expected gcc -M-style output." + ) + ok = False + + return ok + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--source", + required=True, + help="The .k source file passed to list-deps (used for membership check).", + ) + parser.add_argument( + "--plain", + help="Path to the plain-format manifest produced by list-deps.", + ) + parser.add_argument( + "--make", + help="Path to the Makefile-style manifest produced by list-deps.", + ) + parser.add_argument( + "--make-target", + default="", + help="Expected target name in the Makefile-style manifest.", + ) + parser.add_argument( + "--no-codegen-dir", + help="Directory that must contain no codegen artifacts.", + ) + args = parser.parse_args() + + ok = True + source = Path(args.source) + + if args.plain: + ok &= _check_plain(Path(args.plain), source) + + if args.make: + if not args.make_target: + print("--make-target is required when --make is provided") + ok = False + else: + ok &= _check_make(Path(args.make), args.make_target) + + if args.no_codegen_dir: + ok &= _check_no_codegen(Path(args.no_codegen_dir)) + + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/compiler/cli/list_deps.k b/test/compiler/cli/list_deps.k new file mode 100644 index 00000000..1731f63a --- /dev/null +++ b/test/compiler/cli/list_deps.k @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Minimal program used by the `list-deps` CLI test. The explicit import +// of `data.closure` proves the dependency walker reaches transitively +// imported modules (closure pulls in `data.closure.core`), and the +// implicitly imported mini-base library should also appear in the +// emitted file list. +import data.closure + +class Main +{ +public: + void main() + { + } +} + +export Main; From 1757f11bbec8aad34e835dbd5c9cab02c7c4dd4a Mon Sep 17 00:00:00 2001 From: xmaams Date: Mon, 8 Jun 2026 15:08:21 -0700 Subject: [PATCH 2/8] adjust windows instructions --- BUILDING.md | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 1876ce6f..509be00f 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -232,11 +232,10 @@ Install Visual Studio 2022 with the "Desktop development with C++" workload. Thi compiler, CMake, and Ninja. To make `cl.exe`, `cmake`, and `ninja` available in your PowerShell session, enter the -"Developer PowerShell for VS 2022" environment. - -Verify the tools are on your `PATH`: - +"Developer PowerShell for VS 2022" environment with a command like ```powershell +& "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 +# Verify the tools are on your `PATH`: cmake --version ninja --version ``` @@ -277,62 +276,54 @@ cabal user-config update -a "store-dir: E:\cache\cabal" ### 3. Install Boost 1.88.0 -Download and extract the Boost source archive. The extraction step takes a few minutes because -Boost contains tens of thousands of files: +Download and extract the Boost C++ library version 1.88.0 or greater. ```powershell -New-Item -ItemType Directory -Path E:\cache\boost -Force | Out-Null +New-Item -ItemType Directory -Path C:\cache\boost -Force | Out-Null Invoke-WebRequest ` -Uri "https://archives.boost.io/release/1.88.0/source/boost_1_88_0.zip" ` - -OutFile "E:\cache\boost_1_88_0.zip" -UseBasicParsing -Expand-Archive -Path "E:\cache\boost_1_88_0.zip" -DestinationPath "E:\cache\boost" -Force + -OutFile "C:\cache\boost_1_88_0.zip" -UseBasicParsing +Expand-Archive -Path "C:\cache\boost_1_88_0.zip" -DestinationPath "C:\cache\boost" -Force ``` Bootstrap and install Boost. Kanagawa only needs the Boost headers, so a headers-only install is -sufficient. The recommended approach is to use `b2` to generate the CMake config files, then use -`robocopy` to copy the headers in bulk (much faster than `b2`'s file-by-file install on Windows): +sufficient. If you are impatient use `b2` to generate the CMake config files, then use +`robocopy` to copy the headers in bulk: ```powershell -cd E:\cache\boost\boost_1_88_0 +cd C:\cache\boost\boost_1_88_0 .\bootstrap.bat # Start the install to generate the CMake config files in the install prefix. # You can cancel (Ctrl+C) once you see headers being copied -- the CMake config # files are written early. -.\b2.exe install --prefix=E:\cache\boost\install --with-headers +.\b2.exe install --prefix=C:\cache\boost\install --with-headers # Bulk-copy all headers to the install prefix using robocopy -robocopy "E:\cache\boost\boost_1_88_0\boost" ` - "E:\cache\boost\install\include\boost-1_88\boost" /E /NFL /NDL /NJH /NP +robocopy "C:\cache\boost\boost_1_88_0\boost" ` + "C:\cache\boost\install\include\boost-1_88\boost" /E /NFL /NDL /NJH /NP ``` -Alternatively, you can let `b2 install` run to completion (slower, but simpler). - ### 4. Configure the build with CMake From the repository root, run CMake generate. The example below uses -`E:\cache\boost\install` for Boost and `C:\ghcup\bin` for ghcup; adjust paths as needed: +`C:\cache\boost\install` for Boost and `C:\ghcup\bin` for ghcup; adjust paths as needed: ```powershell -cmake -S E:\git\kanagawa -B E:\git\kanagawa-build -G Ninja ` +cmake -S kanagawa -B kanagawa-build -G Ninja ` -DCMAKE_BUILD_TYPE=RelWithDebInfo ` - -DBoost_DIR=E:\cache\boost\install\lib\cmake\Boost-1.88.0 ` + -DBoost_DIR=C:\cache\boost\install\lib\cmake\Boost-1.88.0 ` -DGHCUP_DIR=C:\ghcup\bin ``` -The configure step takes a couple of minutes (CMake configures the bundled LLVM/CIRCT submodule -in addition to Kanagawa itself). When it finishes, you should see `Build files have been written -to: E:/git/kanagawa-build`. - ### 5. Build the compiler -Build the `kanagawa_runtime` target. This compiles LLVM, MLIR, CIRCT, and the Kanagawa compiler, -and is a long-running build (tens of minutes to over an hour depending on your machine): +Build the `kanagawa_runtime` target. This compiles LLVM, MLIR, CIRCT, and the Kanagawa compiler: ```powershell -ninja -C E:\git\kanagawa-build kanagawa_runtime +ninja -C kanagawa-build kanagawa_runtime ``` The resulting `kanagawa.exe` and `kanagawa-backend.dll` are staged in -`E:\git\kanagawa-build\dist\bin`. +`kanagawa-build\dist\bin`. ### Notes From 492d84155746d5a989ab1464be55001da8ca1b01 Mon Sep 17 00:00:00 2001 From: xmaams Date: Mon, 8 Jun 2026 15:11:00 -0700 Subject: [PATCH 3/8] adjust changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f65eea..362e093c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Check the release summary for a detailed history based on commits. ## [Unreleased] ### Added -- `list-deps` sub-command that enumerates the transitive `.k` source files needed to compile a program by running parse + import resolution only (no frontend, no codegen). Suitable for fast dependency tracking from build systems (e.g. CMake configure-time staleness checks). Supports plain (one path per line) and Makefile-style (`gcc -M`) output formats; writes to `--file-list ` or stdout. +- `list-deps` sub-command that enumerates the transitive source files by running parse + import resolution only. Suitable for fast dependency tracking from build systems (e.g. CMake configure-time staleness checks). Supports plain (one path per line) and Makefile-style (`gcc -M`) output formats; writes to `--file-list ` or stdout. ### Changed - `compile --file-list` now refreshes the dependency manifest immediately after parsing succeeds (instead of after codegen). Failed compiles (frontend or backend errors) still update the manifest, so build systems can pick up newly added imports without requiring a successful build. From c0bf661a010c5174e5cc7328ecb5b0b489f48e4b Mon Sep 17 00:00:00 2001 From: xmaams Date: Wed, 26 Aug 2026 10:27:03 -0700 Subject: [PATCH 4/8] simplify outputs --- CHANGELOG.md | 2 +- compiler/hs/app/Main.hs | 40 +++-------------------- compiler/hs/app/Options.hs | 6 ---- compiler/hs/app/Options/CmdArgs.hs | 7 +--- test/compiler/cli/CMakeLists.txt | 13 -------- test/compiler/cli/check_list_deps.py | 49 ---------------------------- 6 files changed, 6 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 362e093c..79db2a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Check the release summary for a detailed history based on commits. ## [Unreleased] ### Added -- `list-deps` sub-command that enumerates the transitive source files by running parse + import resolution only. Suitable for fast dependency tracking from build systems (e.g. CMake configure-time staleness checks). Supports plain (one path per line) and Makefile-style (`gcc -M`) output formats; writes to `--file-list ` or stdout. +- `list-deps` sub-command that enumerates the transitive source files by running parse + import resolution only. Suitable for fast dependency tracking from build systems (e.g. CMake configure-time staleness checks). Writes a plain list (one path per line) to `--file-list ` or stdout. ### Changed - `compile --file-list` now refreshes the dependency manifest immediately after parsing succeeds (instead of after codegen). Failed compiles (frontend or backend errors) still update the manifest, so build systems can pick up newly added imports without requiring a successful build. diff --git a/compiler/hs/app/Main.hs b/compiler/hs/app/Main.hs index 020ceffb..e297fb5b 100644 --- a/compiler/hs/app/Main.hs +++ b/compiler/hs/app/Main.hs @@ -20,14 +20,13 @@ import Language.Kanagawa.Parser.Syntax import Language.Kanagawa.PrettyPrint import Language.Kanagawa.Type import Language.Kanagawa.Warning -import Options (Layout(Pretty, Smart), ListDepsFormat(Make, Plain), Options(Compile, ListDeps, PrettyPrint)) +import Options (Layout(Pretty, Smart), Options(Compile, ListDeps, PrettyPrint)) import Options.CmdArgs import qualified Options as O import ParseTree import System.Directory import System.Environment (getArgs, withArgs, getProgName) import System.Exit -import System.FilePath (takeFileName) import System.IO import Text.Megaparsec.Error @@ -76,10 +75,8 @@ handle opt@Compile{..} cmdArgs = do let (fileNames, results) = unzip parsedFiles parseErrors = lefts results exprs = map fst $ rights results - -- Refresh the dependency manifest immediately after parsing succeeds. - -- Doing this here (rather than after codegen) means the manifest is - -- updated even when frontend or codegen fails, so build systems can - -- pick up new imports without requiring a successful build. + -- Refresh the dependency manifest immediately after parsing succeeds + -- so the manifest is updated even when frontend or codegen fails when (not (null file_list) && not (null fileNames) && null parseErrors) $ writeFileListPlain file_list $ filterAndSortFiles fileNames if not $ null parseErrors @@ -124,10 +121,7 @@ handle opt@ListDeps{..} _ = do if not $ null parseErrors then exitErrors parseErrors else do - let paths = filterAndSortFiles fileNames - rendered = case list_deps_format of - Plain -> renderFileListPlain paths - Make -> renderFileListMake (defaultMakeTarget make_target files) paths + let rendered = renderFileListPlain $ filterAndSortFiles fileNames if null file_list then putStr rendered else writeIfChanged file_list rendered @@ -144,32 +138,6 @@ handle opt _ = print opt renderFileListPlain :: [FilePath] -> String renderFileListPlain = unlines --- | Render a sorted list of dependency paths as a Makefile rule: --- --- : \ --- \ --- --- --- Spaces in paths are escaped with a backslash, matching the convention used --- by gcc/clang's @-M@ output. The @@ argument is used verbatim. -renderFileListMake :: String -> [FilePath] -> String -renderFileListMake target [] = target ++ ":\n" -renderFileListMake target paths = - target ++ ":" ++ concatMap (" \\\n " ++) (map escape paths) ++ "\n" - where - escape = concatMap escapeChar - escapeChar ' ' = "\\ " - escapeChar '\t' = "\\\t" - escapeChar c = [c] - --- | Pick a default Make target name when @--target@ was not supplied: use the --- file name (no directory) of the first input source. -defaultMakeTarget :: String -> [FilePath] -> String -defaultMakeTarget explicit srcs - | not (null explicit) = explicit - | (s:_) <- srcs = takeFileName s - | otherwise = "deps" - -- | Convenience wrapper used by the @compile@ path: write the plain-format -- file list to disk only if the contents differ from what is already on -- disk. Avoids touching mtime when nothing has changed (CMake-friendly). diff --git a/compiler/hs/app/Options.hs b/compiler/hs/app/Options.hs index 3b547829..4ac42373 100644 --- a/compiler/hs/app/Options.hs +++ b/compiler/hs/app/Options.hs @@ -7,7 +7,6 @@ Licensed under the MIT License. module Options ( Layout(..) - , ListDepsFormat(..) , Options(..) , Backend(..) ) where @@ -19,9 +18,6 @@ import Language.Kanagawa.Warning data Layout = Smart | Pretty deriving (Show, Data, Typeable) -data ListDepsFormat = Plain | Make - deriving (Show, Data, Typeable) - data Options = Options | PrettyPrint @@ -139,8 +135,6 @@ data Options , file_list :: FilePath , files :: [FilePath] , import_dir :: [FilePath] - , list_deps_format :: ListDepsFormat - , make_target :: String , no_implicit_base :: Bool , parse_docs :: Bool , target_device :: String diff --git a/compiler/hs/app/Options/CmdArgs.hs b/compiler/hs/app/Options/CmdArgs.hs index f88475cf..e42073e2 100644 --- a/compiler/hs/app/Options/CmdArgs.hs +++ b/compiler/hs/app/Options/CmdArgs.hs @@ -153,10 +153,7 @@ pretty = PrettyPrint help "Parse and pretty print source file(s)" listDeps :: Options -listDeps = ListDeps - { list_deps_format = Plain &= help "Output format: plain (one path per line) or make (Makefile-style)" &= name "format" &= explicit &= typ "FORMAT" - , make_target = "" &= help "Target name for Makefile-style output; defaults to the first source file" &= name "target" &= explicit &= typ "STRING" - } &= +listDeps = ListDeps{} &= name "list-deps" &= help "List the transitive .k source files needed to compile (parse only; no frontend or codegen)" &= details [ "Usage example:" @@ -164,8 +161,6 @@ listDeps = ListDeps , " kanagawa list-deps source.k" , " Write the dependency list to a file:" , " kanagawa list-deps --file-list=deps.txt source.k" - , " Emit Makefile-style dependencies for build-system integration:" - , " kanagawa list-deps --format=make --target=source.o --file-list=source.d source.k" ] mode :: Mode (CmdArgs Options) diff --git a/test/compiler/cli/CMakeLists.txt b/test/compiler/cli/CMakeLists.txt index c7138bb7..f4b1fab0 100644 --- a/test/compiler/cli/CMakeLists.txt +++ b/test/compiler/cli/CMakeLists.txt @@ -157,16 +157,3 @@ add_cli_test(list_deps_plain "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_list_deps.py --source ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k --plain @OUTDIR@/deps.txt --no-codegen-dir @OUTDIR@" ) -add_cli_test(list_deps_make - SUBCOMMAND list-deps - NO_AUTO_OUTPUT - SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k - OPTIONS - --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k - --import-dir=${CMAKE_SOURCE_DIR}/library - --file-list=@OUTDIR@/deps.mk - --format=make - --target=list_deps.o - TEST - "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_list_deps.py --source ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k --make @OUTDIR@/deps.mk --make-target list_deps.o --no-codegen-dir @OUTDIR@" -) diff --git a/test/compiler/cli/check_list_deps.py b/test/compiler/cli/check_list_deps.py index ffc10a30..f1d6e7b0 100644 --- a/test/compiler/cli/check_list_deps.py +++ b/test/compiler/cli/check_list_deps.py @@ -19,8 +19,6 @@ 4. No codegen artifacts (.sv / .mlir / metadata) were produced in the test output directory; presence of any such file would indicate the sub-command did not short-circuit before codegen. - 5. The Make-format manifest, when supplied, starts with `:` and - uses the `\\ ` continuation convention. Exits non-zero on failure. """ @@ -112,37 +110,6 @@ def _check_no_codegen(out_dir: Path): return ok -def _check_make(manifest: Path, target: str): - ok = True - if not manifest.is_file(): - print(f"make manifest does not exist: {manifest}") - return False - - text = manifest.read_text() - if not text.startswith(f"{target}:"): - print( - f"make manifest must start with '{target}:'; got: " - f"{text[: len(target) + 16]!r}" - ) - ok = False - - if not text.endswith("\n"): - print("make manifest does not end with a newline.") - ok = False - - # A non-empty deps list uses ' \ ' between deps. - # Multi-line continuation must use a backslash at end of line. - body = text[len(target) + 1 :] - if "\\\n" not in body and body.strip(): - print( - "make manifest body has no '\\' continuation; " - "expected gcc -M-style output." - ) - ok = False - - return ok - - def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( @@ -154,15 +121,6 @@ def main(): "--plain", help="Path to the plain-format manifest produced by list-deps.", ) - parser.add_argument( - "--make", - help="Path to the Makefile-style manifest produced by list-deps.", - ) - parser.add_argument( - "--make-target", - default="", - help="Expected target name in the Makefile-style manifest.", - ) parser.add_argument( "--no-codegen-dir", help="Directory that must contain no codegen artifacts.", @@ -175,13 +133,6 @@ def main(): if args.plain: ok &= _check_plain(Path(args.plain), source) - if args.make: - if not args.make_target: - print("--make-target is required when --make is provided") - ok = False - else: - ok &= _check_make(Path(args.make), args.make_target) - if args.no_codegen_dir: ok &= _check_no_codegen(Path(args.no_codegen_dir)) From 036c4f969a682e5216b276cafb4f28079ecfec0c Mon Sep 17 00:00:00 2001 From: xmaams Date: Wed, 26 Aug 2026 11:30:58 -0700 Subject: [PATCH 5/8] simplify --- compiler/hs/app/Main.hs | 53 ++++------ test/compiler/cli/CMakeLists.txt | 108 +++++++++----------- test/compiler/cli/check_list_deps.py | 143 --------------------------- test/compiler/cli/list_deps.k | 7 +- 4 files changed, 69 insertions(+), 242 deletions(-) delete mode 100644 test/compiler/cli/check_list_deps.py diff --git a/compiler/hs/app/Main.hs b/compiler/hs/app/Main.hs index e297fb5b..61223982 100644 --- a/compiler/hs/app/Main.hs +++ b/compiler/hs/app/Main.hs @@ -39,11 +39,6 @@ main = do then exitError "Missing source filename(s)" else handle options $ unwords $ prog : args --- Filter out internal synthetic modules (those whose names start with '.', --- e.g. .cmdargs.k, .options.k) and sort the remaining canonical paths. -filterAndSortFiles :: [FilePath] -> [FilePath] -filterAndSortFiles = filter (('.' /=) . head) . sort - handle :: Options -> String -> IO () -- Pretty print files specified via command line options @@ -75,13 +70,13 @@ handle opt@Compile{..} cmdArgs = do let (fileNames, results) = unzip parsedFiles parseErrors = lefts results exprs = map fst $ rights results - -- Refresh the dependency manifest immediately after parsing succeeds - -- so the manifest is updated even when frontend or codegen fails - when (not (null file_list) && not (null fileNames) && null parseErrors) $ - writeFileListPlain file_list $ filterAndSortFiles fileNames if not $ null parseErrors then exitErrors parseErrors else do + -- Refresh the dependency manifest as soon as parsing succeeds, so + -- that it is up to date even when the frontend or codegen fails. + when (not (null file_list) && not (null fileNames)) $ + writeIfChanged file_list $ renderDeps fileNames let desugared = foldr1 append $ frontend passes template_passes template_iterations exprs when dump_parse $ forM_ exprs $ print . prettyExp @@ -111,9 +106,8 @@ handle opt@Compile{..} cmdArgs = do append (NotedExp _ (SeqF x)) (NotedExp n (SeqF y)) = NotedExp n (SeqF (x ++ y)) append _ _ = undefined --- Enumerate the transitive set of source files needed to compile the program --- by running parse + import resolution only. Skips frontend and codegen, so --- this is suitable as a fast dependency-list generator for build systems. +-- Enumerate the transitive set of source files needed to compile the program. +-- Runs parse + import resolution only, skipping the frontend and codegen. handle opt@ListDeps{..} _ = do parsedFiles <- flip execStateT [] $ parseProgram $ getParseOptions opt let (fileNames, results) = unzip parsedFiles @@ -121,10 +115,10 @@ handle opt@ListDeps{..} _ = do if not $ null parseErrors then exitErrors parseErrors else do - let rendered = renderFileListPlain $ filterAndSortFiles fileNames + let deps = renderDeps fileNames if null file_list - then putStr rendered - else writeIfChanged file_list rendered + then putStr deps + else writeIfChanged file_list deps exitSuccess -- Run languange server @@ -133,30 +127,21 @@ handle opt@ListDeps{..} _ = do -- Print usage handle opt _ = print opt --- | Render a sorted list of dependency paths in the simple plain format: --- one absolute path per line, terminated with a newline. -renderFileListPlain :: [FilePath] -> String -renderFileListPlain = unlines - --- | Convenience wrapper used by the @compile@ path: write the plain-format --- file list to disk only if the contents differ from what is already on --- disk. Avoids touching mtime when nothing has changed (CMake-friendly). -writeFileListPlain :: FilePath -> [FilePath] -> IO () -writeFileListPlain listfile = writeIfChanged listfile . renderFileListPlain +-- | One sorted path per line, excluding internal synthetic modules whose +-- names start with '.' (e.g. .cmdargs.k, .options.k). +renderDeps :: [FilePath] -> String +renderDeps = unlines . filter (('.' /=) . head) . sort --- | Write @content@ to @path@ only if the existing file's contents differ. --- A non-existent file is treated as having empty contents. +-- | Write @content@ to @path@, leaving the file (and its mtime) untouched if +-- it already has those contents. writeIfChanged :: FilePath -> String -> IO () writeIfChanged path content = do - old <- listfileContent + exists <- doesFileExist path + old <- if exists + then T.unpack <$> TIO.readFile path + else return "" when (old /= content) $ writeFile path content - where - listfileContent = do - exists <- doesFileExist path - if exists - then T.unpack <$> TIO.readFile path - else return "" getParseOptions :: Options -> ParseOptions getParseOptions opt = case opt of diff --git a/test/compiler/cli/CMakeLists.txt b/test/compiler/cli/CMakeLists.txt index f4b1fab0..e3ab463d 100644 --- a/test/compiler/cli/CMakeLists.txt +++ b/test/compiler/cli/CMakeLists.txt @@ -4,15 +4,6 @@ # CLI-level tests that exercise specific kanagawa command-line options # by inspecting the files produced by the compiler. -# The verifier scripts are in Python; resolve the interpreter once via -# CMake's bundled finder (CIRCT already calls `find_package(Python3)`, -# which sets Python3_EXECUTABLE on every platform). Falling back to a bare -# `python3` only works on POSIX-like systems where it is on PATH. -if(NOT Python3_EXECUTABLE) - find_package(Python3 COMPONENTS Interpreter REQUIRED) -endif() -set(_cli_python "${Python3_EXECUTABLE}") - # Helper function for adding a CLI-level CTest. The test invokes the Kanagawa # compiler with the supplied OPTIONS and then runs the supplied verification # command(s) (TEST). Each verification command is its own CTest, chained @@ -25,22 +16,17 @@ set(_cli_python "${Python3_EXECUTABLE}") # SOURCES [ ...] # [OPTIONS ...] # [OUTPUT_PREFIX ] # default: test -# [SUBCOMMAND ] # e.g. list-deps; default: omit (auto compile) -# [NO_AUTO_OUTPUT] # do not pass --output=/ # [TEST ...] # ) # # A separate output directory is created for each test under the current -# binary dir, and (unless NO_AUTO_OUTPUT is set) the compiler is invoked with -# --output=/. CTest names are formed by prefixing the test -# name with 'cli.' (e.g. cli..compile, cli..test1, -# cli..test2). Test commands can reference @OUTDIR@ to substitute -# the absolute path of the per-test output directory. +# binary dir, and the compiler is invoked with --output=/. +# CTest names are formed by prefixing the test name with 'cli.' (e.g. +# cli..compile, cli..test1, cli..test2). function(add_cli_test test_name) - set(_opt NO_AUTO_OUTPUT) - set(_one OUTPUT_PREFIX SUBCOMMAND) + set(_one OUTPUT_PREFIX) set(_multi SOURCES OPTIONS TEST) - cmake_parse_arguments(_ARG "${_opt}" "${_one}" "${_multi}" ${ARGN}) + cmake_parse_arguments(_ARG "" "${_one}" "${_multi}" ${ARGN}) if(NOT test_name) message(FATAL_ERROR "add_cli_test: missing .") @@ -55,29 +41,6 @@ function(add_cli_test test_name) set(_outdir "${CMAKE_CURRENT_BINARY_DIR}/${test_name}") set(_fixture "cli_${test_name}") - # Build the kanagawa command line, optionally prefixed by a sub-command - # and optionally suppressing the auto --output flag. @OUTDIR@ in any - # OPTIONS or SOURCES entry is substituted with the per-test output dir. - set(_compile_cmd $) - if(_ARG_SUBCOMMAND) - list(APPEND _compile_cmd ${_ARG_SUBCOMMAND}) - endif() - set(_subst_options "") - foreach(_opt IN LISTS _ARG_OPTIONS) - string(REPLACE "@OUTDIR@" "${_outdir}" _opt "${_opt}") - list(APPEND _subst_options "${_opt}") - endforeach() - list(APPEND _compile_cmd ${_subst_options}) - if(NOT _ARG_NO_AUTO_OUTPUT) - list(APPEND _compile_cmd --output=${_outdir}/${_ARG_OUTPUT_PREFIX}) - endif() - set(_subst_sources "") - foreach(_src IN LISTS _ARG_SOURCES) - string(REPLACE "@OUTDIR@" "${_outdir}" _src "${_src}") - list(APPEND _subst_sources "${_src}") - endforeach() - list(APPEND _compile_cmd ${_subst_sources}) - # Compile step: clean the output dir, recreate it, then run the compiler. # add_test only allows one COMMAND, so drive the prep + compile via # `cmake -E env --` which simply forwards to the kanagawa exe after the @@ -102,7 +65,10 @@ function(add_cli_test test_name) add_test( NAME cli.${test_name}.compile - COMMAND ${_compile_cmd} + COMMAND $ + ${_ARG_OPTIONS} + --output=${_outdir}/${_ARG_OUTPUT_PREFIX} + ${_ARG_SOURCES} ) set_tests_properties(cli.${test_name}.compile PROPERTIES FIXTURES_REQUIRED "${_fixture}_dir" @@ -113,8 +79,6 @@ function(add_cli_test test_name) set(_idx 0) foreach(_cmd IN LISTS _ARG_TEST) math(EXPR _idx "${_idx} + 1") - # @OUTDIR@ is a convenience placeholder for the per-test output dir. - string(REPLACE "@OUTDIR@" "${_outdir}" _cmd "${_cmd}") # Split the command string on spaces into a list so the first token is # the program and the remainder are arguments. separate_arguments(_cmd_list UNIX_COMMAND "${_cmd}") @@ -138,22 +102,46 @@ add_cli_test(skip_circt_lowering --import-dir=${CMAKE_SOURCE_DIR}/library --place-iterations=1 TEST - "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_skip_circt_lowering.py ${CMAKE_CURRENT_BINARY_DIR}/skip_circt_lowering" + "python3 ${CMAKE_CURRENT_SOURCE_DIR}/check_skip_circt_lowering.py ${CMAKE_CURRENT_BINARY_DIR}/skip_circt_lowering" ) -# `list-deps` sub-command: parse + import resolution only, fast enough to run -# during cmake configure for staleness detection. The compile step here is -# the list-deps invocation itself; the verifier asserts the manifest is -# present, well-formed, and that no codegen artifacts were produced. -add_cli_test(list_deps_plain - SUBCOMMAND list-deps - NO_AUTO_OUTPUT - SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k - OPTIONS - --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k - --import-dir=${CMAKE_SOURCE_DIR}/library - --file-list=@OUTDIR@/deps.txt - TEST - "${_cli_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_list_deps.py --source ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k --plain @OUTDIR@/deps.txt --no-codegen-dir @OUTDIR@" +# `list-deps` resolves imports without running the frontend or codegen, so it +# does not fit add_cli_test's compile-and-inspect shape. Run it directly and +# diff the manifest against a golden. The compiler emits canonical native +# paths, so build the golden from the symlink-resolved source dir. +set(_list_deps_golden + library/compiler/config.k + library/compiler/device/config.k + library/compiler/device/schema.k + library/data/closure.k + library/data/closure/core.k + library/debug/print.k + library/device/mock/hardware/config.k + library/mini-base.k + test/compiler/cli/list_deps.k ) +get_filename_component(_repo_root "${CMAKE_SOURCE_DIR}" REALPATH) +set(_golden_text "") +foreach(_rel IN LISTS _list_deps_golden) + file(TO_NATIVE_PATH "${_repo_root}/${_rel}" _abs) + string(APPEND _golden_text "${_abs}\n") +endforeach() +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/list_deps.golden "${_golden_text}") +add_test( + NAME cli.list_deps.run + COMMAND $ list-deps + --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k + --import-dir=${CMAKE_SOURCE_DIR}/library + --file-list=${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt + ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k +) +set_tests_properties(cli.list_deps.run PROPERTIES FIXTURES_SETUP cli_list_deps) + +add_test( + NAME cli.list_deps.golden + COMMAND ${CMAKE_COMMAND} -E compare_files --ignore-eol + ${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt + ${CMAKE_CURRENT_BINARY_DIR}/list_deps.golden +) +set_tests_properties(cli.list_deps.golden PROPERTIES FIXTURES_REQUIRED cli_list_deps) diff --git a/test/compiler/cli/check_list_deps.py b/test/compiler/cli/check_list_deps.py deleted file mode 100644 index f1d6e7b0..00000000 --- a/test/compiler/cli/check_list_deps.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -""" -Verify the outputs of a `kanagawa list-deps` invocation. - -Contract of the sub-command: parse + import resolution only. The compiler -must enumerate the transitive set of `.k` source files reachable from the -given input(s) and never invoke the frontend or codegen. Specifically: - - 1. The plain-format manifest exists and lists at least the input source - and one transitive import (the explicit `data.closure` import in - `list_deps.k` ensures the walker followed at least one edge). - 2. Every line in the manifest resolves to an existing absolute path on - disk; internal synthetic modules (`.cmdargs.k`, `.options.k`) are - excluded by the compiler. - 3. The output is sorted, contains no duplicates, and ends with a single - trailing newline. - 4. No codegen artifacts (.sv / .mlir / metadata) were produced in the - test output directory; presence of any such file would indicate the - sub-command did not short-circuit before codegen. - -Exits non-zero on failure. -""" -import argparse -import sys -from pathlib import Path - - -def _read_lines(path: Path): - text = path.read_text() - if not text.endswith("\n"): - print(f"{path.name}: manifest does not end with a newline.") - return None - # splitlines() drops the trailing empty element produced by the final \n. - return text.splitlines() - - -def _check_plain(manifest: Path, source: Path): - ok = True - if not manifest.is_file(): - print(f"plain manifest does not exist: {manifest}") - return False - - lines = _read_lines(manifest) - if lines is None: - return False - - if not lines: - print(f"plain manifest is empty: {manifest}") - return False - - if sorted(lines) != lines: - print(f"plain manifest is not sorted: {manifest}") - ok = False - - if len(set(lines)) != len(lines): - print(f"plain manifest contains duplicates: {manifest}") - ok = False - - for line in lines: - if line.startswith("."): - print( - f"plain manifest contains an internal synthetic module " - f"(should be filtered): {line}" - ) - ok = False - # Each line should resolve to an existing file on disk. - if not Path(line).is_file(): - print( - f"plain manifest references a missing file (path may " - f"not be canonical): {line}" - ) - ok = False - - # The user's source file must appear (path is canonicalised by the - # compiler, so compare via Path). - src_resolved = source.resolve() - if src_resolved not in {Path(line) for line in lines}: - print( - f"plain manifest is missing the input source {src_resolved}; " - f"got entries: {lines}" - ) - ok = False - - # At least one transitive dependency must appear (the input file alone - # is not enough -- mini-base or data.closure must have been followed). - if len(lines) < 2: - print( - f"plain manifest has only {len(lines)} entry; expected the " - f"transitive walker to pull in at least one import." - ) - ok = False - - return ok - - -def _check_no_codegen(out_dir: Path): - ok = True - forbidden = [] - for pattern in ("*.sv", "*.mlir", "*.json", "*.dgml"): - forbidden.extend(out_dir.glob(pattern)) - if forbidden: - names = ", ".join(p.name for p in forbidden) - print( - f"list-deps must not emit codegen artifacts, but found in " - f"{out_dir}: {names}" - ) - ok = False - return ok - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--source", - required=True, - help="The .k source file passed to list-deps (used for membership check).", - ) - parser.add_argument( - "--plain", - help="Path to the plain-format manifest produced by list-deps.", - ) - parser.add_argument( - "--no-codegen-dir", - help="Directory that must contain no codegen artifacts.", - ) - args = parser.parse_args() - - ok = True - source = Path(args.source) - - if args.plain: - ok &= _check_plain(Path(args.plain), source) - - if args.no_codegen_dir: - ok &= _check_no_codegen(Path(args.no_codegen_dir)) - - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/test/compiler/cli/list_deps.k b/test/compiler/cli/list_deps.k index 1731f63a..1ce3ea1b 100644 --- a/test/compiler/cli/list_deps.k +++ b/test/compiler/cli/list_deps.k @@ -1,11 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Minimal program used by the `list-deps` CLI test. The explicit import -// of `data.closure` proves the dependency walker reaches transitively -// imported modules (closure pulls in `data.closure.core`), and the -// implicitly imported mini-base library should also appear in the -// emitted file list. +// Importing `data.closure` (which itself imports `data.closure.core`) makes +// the golden manifest cover transitively imported modules. import data.closure class Main From 31f73903da447be448e71ed5bfd6e32b6aaf0823 Mon Sep 17 00:00:00 2001 From: xmaams Date: Wed, 26 Aug 2026 11:43:51 -0700 Subject: [PATCH 6/8] revert building --- BUILDING.md | 113 +++++----------------------------------------------- 1 file changed, 9 insertions(+), 104 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 509be00f..f5f83154 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -222,116 +222,21 @@ cmake \ ## Building on Windows At this time, the unit tests are only supported on Linux. However, you can build and run the compiler and -related tools on Windows. The following step-by-step guide assumes a PowerShell session and Visual -Studio 2022 (any edition with the "Desktop development with C++" workload, including Community). -Adjust paths as appropriate for your environment. - -### 1. Install Visual Studio 2022 - -Install Visual Studio 2022 with the "Desktop development with C++" workload. This provides the MSVC -compiler, CMake, and Ninja. - -To make `cl.exe`, `cmake`, and `ninja` available in your PowerShell session, enter the -"Developer PowerShell for VS 2022" environment with a command like -```powershell -& "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -# Verify the tools are on your `PATH`: -cmake --version -ninja --version -``` - -### 2. Install GHC and cabal via ghcup - -Run the official ghcup bootstrap script in a non-interactive form so it can be scripted. The -arguments below skip the interactive prompts and install ghcup but not GHC, cabal, stack, or HLS -(we install specific versions of GHC and cabal in the next step): - -```powershell -Set-ExecutionPolicy Bypass -Scope Process -Force -[System.Net.ServicePointManager]::SecurityProtocol = - [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 -Invoke-Command -ScriptBlock ([ScriptBlock]::Create( - (Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) ` - -ArgumentList $false,$true,$true,$false,$false,$false,$false,"","","","" -``` - -This installs ghcup at `C:\ghcup`. Add it to `PATH` for the current session, then install the -required versions of GHC and cabal, set them as the default, and update the Hackage index: - -```powershell -$env:Path = "C:\ghcup\bin;" + $env:Path -ghcup install ghc 9.6.7 -ghcup install cabal 3.12.1.0 -ghcup set ghc 9.6.7 -ghcup set cabal 3.12.1.0 -cabal update -``` - -Optionally, redirect the cabal package store to a different drive (useful if your system drive is -small): - -```powershell -cabal user-config update -a "store-dir: E:\cache\cabal" -``` - -### 3. Install Boost 1.88.0 - -Download and extract the Boost C++ library version 1.88.0 or greater. +related tools on Windows. Here are the dependencies that you must install: -```powershell -New-Item -ItemType Directory -Path C:\cache\boost -Force | Out-Null -Invoke-WebRequest ` - -Uri "https://archives.boost.io/release/1.88.0/source/boost_1_88_0.zip" ` - -OutFile "C:\cache\boost_1_88_0.zip" -UseBasicParsing -Expand-Archive -Path "C:\cache\boost_1_88_0.zip" -DestinationPath "C:\cache\boost" -Force -``` - -Bootstrap and install Boost. Kanagawa only needs the Boost headers, so a headers-only install is -sufficient. If you are impatient use `b2` to generate the CMake config files, then use -`robocopy` to copy the headers in bulk: - -```powershell -cd C:\cache\boost\boost_1_88_0 -.\bootstrap.bat -# Start the install to generate the CMake config files in the install prefix. -# You can cancel (Ctrl+C) once you see headers being copied -- the CMake config -# files are written early. -.\b2.exe install --prefix=C:\cache\boost\install --with-headers -# Bulk-copy all headers to the install prefix using robocopy -robocopy "C:\cache\boost\boost_1_88_0\boost" ` - "C:\cache\boost\install\include\boost-1_88\boost" /E /NFL /NDL /NJH /NP -``` - -### 4. Configure the build with CMake +- C/C++ compiler. CMake should auto-detect it. Visual Studio 2022 Community Edition is a good option. +- CMake version 3.30 or later (see https://cmake.org/) +- Ninja. It's not required to use Ninja; you can use any build tool supported by CMake, but we recommend Ninja for its speed and simplicity (see https://ninja-build.org/) +- [ghcup](https://www.haskell.org/ghcup/). After installation, close and re-open your Powershell session and then run `ghcup tui` to launch the interactive version of ghcup. Install Haskell 9.6.7 and cabal 3.12.1.0. Run `cabal update` after installation. +- Boost C++ library version 1.88.0 or later (see https://www.boost.org/). After extracting the archive, you will need to change into the extracted directory and run `.\bootstrap.bat` and then `.\b2.exe install --prefix=target_directory` (substitute `target_directory` with where you want Boost installed). The proper value to pass as Boost_DIR to CMake will be something like `...\boost_1_88_0\stage\lib\cmake\Boost-1.88.0`. +To tell CMake about Boost so that the `find_package` command can find it, add `\lib\cmake` to `CMAKE_PREFIX_PATH`. For example: `-DCMAKE_PREFIX_PATH=D:\boost.1.88.0\lib\cmake` -From the repository root, run CMake generate. The example below uses -`C:\cache\boost\install` for Boost and `C:\ghcup\bin` for ghcup; adjust paths as needed: +Run cmake generate to initialize the build system. Here's an example command line - replace the paths with values appropriate for your set-up: -```powershell -cmake -S kanagawa -B kanagawa-build -G Ninja ` - -DCMAKE_BUILD_TYPE=RelWithDebInfo ` - -DBoost_DIR=C:\cache\boost\install\lib\cmake\Boost-1.88.0 ` - -DGHCUP_DIR=C:\ghcup\bin ``` - -### 5. Build the compiler - -Build the `kanagawa_runtime` target. This compiles LLVM, MLIR, CIRCT, and the Kanagawa compiler: - -```powershell -ninja -C kanagawa-build kanagawa_runtime +cmake -S kanagawa -B kanagawa-build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_PREFIX_PATH=D:\boost.1.88.0\lib\cmake -DGHCUP_DIR=D:/ghcup ``` -The resulting `kanagawa.exe` and `kanagawa-backend.dll` are staged in -`kanagawa-build\dist\bin`. - -### Notes - -- Verilator and the RISC-V GCC cross-compiler are not available on Windows out of the box, so - the corresponding tests are disabled automatically. -- If you re-open your PowerShell session, you must re-enter the VS dev environment and re-add ghcup to `PATH` (`$env:Path = "C:\ghcup\bin;" + $env:Path`) - before running `cmake` or `ninja`. - ## Preparing a release There is a GitHub workflow (Nightly prerelease) that runs nightly at 08:00 UTC. If there have been any changes checked into main since it last run, it will prepare and publish a release. From 6c12608cd3a0256a5e8d9f720a73b865de5d2049 Mon Sep 17 00:00:00 2001 From: xmaams Date: Wed, 26 Aug 2026 11:50:36 -0700 Subject: [PATCH 7/8] even simpler test --- build/cmake/test_helper.cmake | 47 +++++++++++++++++++++++++++ test/compiler/cli/CMakeLists.txt | 37 +++------------------ test/compiler/cli/list_deps.golden.in | 9 +++++ 3 files changed, 60 insertions(+), 33 deletions(-) create mode 100644 test/compiler/cli/list_deps.golden.in diff --git a/build/cmake/test_helper.cmake b/build/cmake/test_helper.cmake index 6172d972..e736c4ec 100644 --- a/build/cmake/test_helper.cmake +++ b/build/cmake/test_helper.cmake @@ -481,3 +481,50 @@ function(add_kanagawa_verilator_test name) endif() endfunction() + +# Helper function for tests that run a command and compare a file it produces +# against a checked-in golden. +# +# The golden is expanded with configure_file-style @VAR@ substitution and then +# converted to native path separators, so a golden can reference absolute +# locations portably via @KANAGAWA_SOURCE_DIR@ (the source tree with symlinks +# resolved, matching the canonical paths the compiler reports). +# +# Usage: +# add_golden_test( +# COMMAND [ ...] # command that writes ACTUAL +# ACTUAL # file produced by COMMAND +# GOLDEN # golden template, relative to the current source dir +# ) +# +# Adds .run and .golden CTests, chained via a fixture so +# the comparison only runs after the command succeeds. +function(add_golden_test test_name) + set(_one ACTUAL GOLDEN) + set(_multi COMMAND) + cmake_parse_arguments(_ARG "" "${_one}" "${_multi}" ${ARGN}) + + foreach(_required IN ITEMS COMMAND ACTUAL GOLDEN) + if(NOT _ARG_${_required}) + message(FATAL_ERROR "add_golden_test(${test_name}): ${_required} is required.") + endif() + endforeach() + + get_filename_component(KANAGAWA_SOURCE_DIR "${CMAKE_SOURCE_DIR}" REALPATH) + + set(_golden "${CMAKE_CURRENT_BINARY_DIR}/${test_name}.golden") + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/${_ARG_GOLDEN}" _text) + string(CONFIGURE "${_text}" _text @ONLY) + file(TO_NATIVE_PATH "${_text}" _text) + file(WRITE "${_golden}" "${_text}") + + add_test(NAME ${test_name}.run COMMAND ${_ARG_COMMAND}) + set_tests_properties(${test_name}.run PROPERTIES FIXTURES_SETUP ${test_name}) + + add_test( + NAME ${test_name}.golden + COMMAND ${CMAKE_COMMAND} -E compare_files --ignore-eol "${_ARG_ACTUAL}" "${_golden}" + ) + set_tests_properties(${test_name}.golden PROPERTIES FIXTURES_REQUIRED ${test_name}) +endfunction() + diff --git a/test/compiler/cli/CMakeLists.txt b/test/compiler/cli/CMakeLists.txt index e3ab463d..c97240cd 100644 --- a/test/compiler/cli/CMakeLists.txt +++ b/test/compiler/cli/CMakeLists.txt @@ -106,42 +106,13 @@ add_cli_test(skip_circt_lowering ) # `list-deps` resolves imports without running the frontend or codegen, so it -# does not fit add_cli_test's compile-and-inspect shape. Run it directly and -# diff the manifest against a golden. The compiler emits canonical native -# paths, so build the golden from the symlink-resolved source dir. -set(_list_deps_golden - library/compiler/config.k - library/compiler/device/config.k - library/compiler/device/schema.k - library/data/closure.k - library/data/closure/core.k - library/debug/print.k - library/device/mock/hardware/config.k - library/mini-base.k - test/compiler/cli/list_deps.k -) -get_filename_component(_repo_root "${CMAKE_SOURCE_DIR}" REALPATH) -set(_golden_text "") -foreach(_rel IN LISTS _list_deps_golden) - file(TO_NATIVE_PATH "${_repo_root}/${_rel}" _abs) - string(APPEND _golden_text "${_abs}\n") -endforeach() -file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/list_deps.golden "${_golden_text}") - -add_test( - NAME cli.list_deps.run +# does not fit add_cli_test's compile-and-inspect shape. +add_golden_test(cli.list_deps COMMAND $ list-deps --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k --import-dir=${CMAKE_SOURCE_DIR}/library --file-list=${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k + ACTUAL ${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt + GOLDEN list_deps.golden.in ) -set_tests_properties(cli.list_deps.run PROPERTIES FIXTURES_SETUP cli_list_deps) - -add_test( - NAME cli.list_deps.golden - COMMAND ${CMAKE_COMMAND} -E compare_files --ignore-eol - ${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt - ${CMAKE_CURRENT_BINARY_DIR}/list_deps.golden -) -set_tests_properties(cli.list_deps.golden PROPERTIES FIXTURES_REQUIRED cli_list_deps) diff --git a/test/compiler/cli/list_deps.golden.in b/test/compiler/cli/list_deps.golden.in new file mode 100644 index 00000000..d468fba9 --- /dev/null +++ b/test/compiler/cli/list_deps.golden.in @@ -0,0 +1,9 @@ +@KANAGAWA_SOURCE_DIR@/library/compiler/config.k +@KANAGAWA_SOURCE_DIR@/library/compiler/device/config.k +@KANAGAWA_SOURCE_DIR@/library/compiler/device/schema.k +@KANAGAWA_SOURCE_DIR@/library/data/closure.k +@KANAGAWA_SOURCE_DIR@/library/data/closure/core.k +@KANAGAWA_SOURCE_DIR@/library/debug/print.k +@KANAGAWA_SOURCE_DIR@/library/device/mock/hardware/config.k +@KANAGAWA_SOURCE_DIR@/library/mini-base.k +@KANAGAWA_SOURCE_DIR@/test/compiler/cli/list_deps.k From 62eaff0b7a4566807a11f0240ed96fe64672723c Mon Sep 17 00:00:00 2001 From: xmaams Date: Thu, 27 Aug 2026 10:51:45 -0700 Subject: [PATCH 8/8] compare to compile --- test/compiler/cli/CMakeLists.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/compiler/cli/CMakeLists.txt b/test/compiler/cli/CMakeLists.txt index c97240cd..1ac3f2a7 100644 --- a/test/compiler/cli/CMakeLists.txt +++ b/test/compiler/cli/CMakeLists.txt @@ -116,3 +116,29 @@ add_golden_test(cli.list_deps ACTUAL ${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt GOLDEN list_deps.golden.in ) + +# Generate the same file list via compile --file-list +add_test( + NAME cli.list_deps.compile_file_list + COMMAND $ compile + --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k + --import-dir=${CMAKE_SOURCE_DIR}/library + --file-list=${CMAKE_CURRENT_BINARY_DIR}/compile_file_list.txt + --output=${CMAKE_CURRENT_BINARY_DIR}/list_deps_compile + ${CMAKE_CURRENT_SOURCE_DIR}/list_deps.k +) +set_tests_properties(cli.list_deps.compile_file_list PROPERTIES + FIXTURES_REQUIRED cli.list_deps + FIXTURES_SETUP cli.list_deps.compile_file_list +) + +# Verify that `list-deps` and `compile --file-list` produce the same output +add_test( + NAME cli.list_deps.compare_file_lists + COMMAND ${CMAKE_COMMAND} -E compare_files + ${CMAKE_CURRENT_BINARY_DIR}/list_deps.txt + ${CMAKE_CURRENT_BINARY_DIR}/compile_file_list.txt +) +set_tests_properties(cli.list_deps.compare_file_lists PROPERTIES + FIXTURES_REQUIRED cli.list_deps.compile_file_list +)