diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9b51e7..79db2a2f 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 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. + ## [1.2.0] - 2025-11-09 ### Added - MacOS (ARM64) support 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/compiler/hs/app/Main.hs b/compiler/hs/app/Main.hs index f2df8ff3..61223982 100644 --- a/compiler/hs/app/Main.hs +++ b/compiler/hs/app/Main.hs @@ -20,7 +20,7 @@ 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), Options(Compile, ListDeps, PrettyPrint)) import Options.CmdArgs import qualified Options as O import ParseTree @@ -73,6 +73,10 @@ handle opt@Compile{..} cmdArgs = do 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 @@ -95,8 +99,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,23 +106,42 @@ 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. +-- 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 + parseErrors = lefts results + if not $ null parseErrors + then exitErrors parseErrors + else do + let deps = renderDeps fileNames + if null file_list + then putStr deps + else writeIfChanged file_list deps + 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 - where - listfileContent = do - exists <- doesFileExist listfile - if exists - then T.unpack <$> TIO.readFile listfile - else return "" +-- | 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@, leaving the file (and its mtime) untouched if +-- it already has those contents. +writeIfChanged :: FilePath -> String -> IO () +writeIfChanged path content = do + exists <- doesFileExist path + old <- if exists + then T.unpack <$> TIO.readFile path + else return "" + when (old /= content) $ + writeFile path content getParseOptions :: Options -> ParseOptions getParseOptions opt = case opt of @@ -149,4 +170,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..4ac42373 100644 --- a/compiler/hs/app/Options.hs +++ b/compiler/hs/app/Options.hs @@ -129,6 +129,17 @@ 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] + , 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..e42073e2 100644 --- a/compiler/hs/app/Options/CmdArgs.hs +++ b/compiler/hs/app/Options/CmdArgs.hs @@ -152,8 +152,19 @@ pretty = PrettyPrint name "pretty" &= help "Parse and pretty print source file(s)" +listDeps :: Options +listDeps = ListDeps{} &= + 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" + ] + 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..1ac3f2a7 100644 --- a/test/compiler/cli/CMakeLists.txt +++ b/test/compiler/cli/CMakeLists.txt @@ -104,3 +104,41 @@ add_cli_test(skip_circt_lowering TEST "python3 ${CMAKE_CURRENT_SOURCE_DIR}/check_skip_circt_lowering.py ${CMAKE_CURRENT_BINARY_DIR}/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. +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 +) + +# 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 +) 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 diff --git a/test/compiler/cli/list_deps.k b/test/compiler/cli/list_deps.k new file mode 100644 index 00000000..1ce3ea1b --- /dev/null +++ b/test/compiler/cli/list_deps.k @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Importing `data.closure` (which itself imports `data.closure.core`) makes +// the golden manifest cover transitively imported modules. +import data.closure + +class Main +{ +public: + void main() + { + } +} + +export Main;