diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-16 08:06:09 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-16 08:06:09 +0000 |
| commit | d74e6f5278e67f42e9a020f81c9eb0272f624618 (patch) | |
| tree | 935eaa12e239bc0b4409fdd948d16ab52feec9e3 | |
| parent | c9f3a6a0b7d781fe73b48a5765fa8b6e7ca3c2f7 (diff) | |
feat(driver)!: select LLVM IR output with -S
Save native executables by default and LLVM IR with -S, using source-adjacent
.out and .ll names. Keep -r independent of output selection and update the
manuals and three-platform nightly smoke checks.
BREAKING CHANGE: plain compilation and -o now save native executables.
Use -S to save textual LLVM IR.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| -rw-r--r-- | .github/workflows/nightly-build.yml | 46 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | AGENTS/ARCH.md | 69 | ||||
| -rw-r--r-- | CMakeLists.txt | 12 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | cmake/NativeCompiler.cmake | 3 | ||||
| -rw-r--r-- | doc/build.md | 14 | ||||
| -rw-r--r-- | doc/cpsc.1.md | 52 | ||||
| -rw-r--r-- | doc/install.md | 2 | ||||
| -rw-r--r-- | tools/driver/NativeCompiler.cc | 10 | ||||
| -rw-r--r-- | tools/driver/NativeCompiler.h | 2 | ||||
| -rw-r--r-- | tools/driver/main.cc | 34 | ||||
| -rw-r--r-- | tools/driver/prepare-native-runtime.py | 2 |
13 files changed, 171 insertions, 79 deletions
diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 8681b59..13f942d 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -59,7 +59,8 @@ jobs: | sudo tee /etc/apt/sources.list.d/cpsc-llvm.list > /dev/null sudo apt-get update sudo apt-get install -y --no-install-recommends \ - build-essential cmake clang-23 llvm-23-dev \ + build-essential cmake clang-23 llvm-23-dev lld-23 \ + libclang-23-dev liblld-23-dev libpolly-23-dev python3 \ zlib1g-dev libzstd-dev libncurses-dev libedit-dev pandoc - name: Build the embedded release package @@ -86,6 +87,19 @@ jobs: printf '%s\n' 'OUTPUT "nightly smoke"' > "$RUNNER_TEMP/nightly-smoke.cps" output=$(env -i PATH=/usr/bin:/bin "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.cps" -r) test "$output" = 'nightly smoke' + env -i PATH=/nonexistent "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.cps" + output=$("$RUNNER_TEMP/nightly-smoke.out") + test "$output" = 'nightly smoke' + env -i PATH=/nonexistent "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.cps" -S + grep -q '^define i32 @main' "$RUNNER_TEMP/nightly-smoke.ll" + for binary in "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.out"; do + while IFS= read -r library; do + case "$library" in + libc.so.*|libm.so.*|libmvec.so.*|ld-linux*.so.*) ;; + *) echo "Unexpected runtime dependency: $library" >&2; exit 1 ;; + esac + done < <(readelf -d "$binary" | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p') + done - name: Upload the compiler package run: | @@ -109,12 +123,13 @@ jobs: - name: Install LLVM 23 and packaging dependencies run: | test "$(uname -m)" = arm64 - brew install llvm@23 pandoc zlib zstd ncurses + brew install llvm@23 lld pandoc zlib zstd ncurses python test -x "$(brew --prefix llvm@23)/bin/clang++" - name: Build the embedded release package run: | llvm_prefix=$(brew --prefix llvm@23) + lld_prefix=$(brew --prefix lld) zlib_prefix=$(brew --prefix zlib) zstd_prefix=$(brew --prefix zstd) ncurses_prefix=$(brew --prefix ncurses) @@ -124,7 +139,7 @@ jobs: -DCMAKE_C_COMPILER="$llvm_prefix/bin/clang" \ -DCMAKE_CXX_COMPILER="$llvm_prefix/bin/clang++" \ -DLLVM_DIR="$llvm_prefix/lib/cmake/llvm" \ - -DCMAKE_PREFIX_PATH="$llvm_prefix;$zlib_prefix;$zstd_prefix;$ncurses_prefix" + -DCMAKE_PREFIX_PATH="$llvm_prefix;$lld_prefix;$zlib_prefix;$zstd_prefix;$ncurses_prefix" cmake --build build --target release --parallel 3 - name: Smoke test the packaged compiler @@ -150,6 +165,17 @@ jobs: printf '%s\n' 'OUTPUT "nightly smoke"' > "$RUNNER_TEMP/nightly-smoke.cps" output=$(env -i PATH=/usr/bin:/bin "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.cps" -r) test "$output" = 'nightly smoke' + env -i PATH=/nonexistent "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.cps" + output=$("$RUNNER_TEMP/nightly-smoke.out") + test "$output" = 'nightly smoke' + env -i PATH=/nonexistent "$package_dir/cpsc" "$RUNNER_TEMP/nightly-smoke.cps" -S + grep -q '^define i32 @main' "$RUNNER_TEMP/nightly-smoke.ll" + while IFS= read -r library; do + case "$library" in + /usr/lib/*|/System/Library/*|/Library/Apple/System/Library/*) ;; + *) echo "Unexpected program dependency: $library" >&2; exit 1 ;; + esac + done < <(otool -L "$RUNNER_TEMP/nightly-smoke.out" | tail -n +2 | awk '{print $1}') - name: Upload the macOS package run: | @@ -182,6 +208,8 @@ jobs: mingw-w64-x86_64-make mingw-w64-x86_64-clang mingw-w64-x86_64-llvm + mingw-w64-x86_64-lld + mingw-w64-x86_64-python mingw-w64-x86_64-libffi mingw-w64-x86_64-libxml2 mingw-w64-x86_64-zlib @@ -227,6 +255,18 @@ jobs: $output = & (Join-Path $packageDir 'cpsc.exe') $source -r if ($LASTEXITCODE -ne 0) { throw "Compiler smoke test failed: $LASTEXITCODE" } if (($output -join "`n") -cne 'nightly smoke') { throw "Unexpected output: $output" } + $native = Join-Path $env:RUNNER_TEMP 'nightly-smoke.exe' + & (Join-Path $packageDir 'cpsc.exe') $source -o $native + if ($LASTEXITCODE -ne 0) { throw "Native compilation failed: $LASTEXITCODE" } + $output = & $native + if ($LASTEXITCODE -ne 0) { throw "Native program failed: $LASTEXITCODE" } + if (($output -join "`n") -cne 'nightly smoke') { throw "Unexpected native output: $output" } + & (Join-Path $packageDir 'cpsc.exe') $source -S + if ($LASTEXITCODE -ne 0) { throw "IR compilation failed: $LASTEXITCODE" } + $ir = [System.IO.Path]::ChangeExtension($source, '.ll') + if (-not (Select-String -LiteralPath $ir -Pattern '^define i32 @main' -Quiet)) { + throw 'Missing LLVM IR main function' + } } finally { $env:PATH = $savedPath } @@ -41,7 +41,7 @@ *.dwo # Generated LLVM IR -*.ir +*.ll build/ .vscode/ diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md index c4cb8d6..30e4f90 100644 --- a/AGENTS/ARCH.md +++ b/AGENTS/ARCH.md @@ -1,12 +1,12 @@ # Architecture -`cpsc` compiles CIE A-Level 9618 pseudocode to LLVM IR. Classic three-stage +`cpsc` compiles CIE A-Level 9618 pseudocode to native executables or LLVM IR. Classic three-stage Kaleidoscope-style design, C++17, built with LLVM 23 and Clang 23. CMake rejects other major versions and non-Clang compilers. Execution is selected at build time: external lli, an embedded static LLVM engine, or disabled. ``` -file ──> Lexer ──> Parser ──> vector<unique_ptr<StmtAST>> ──> CodeGen ──> textual LLVM IR in a file +file ──> Lexer ──> Parser ──> vector<unique_ptr<StmtAST>> ──> CodeGen ──> LLVM IR ──> NativeCompiler (object emission + embedded LLD) (istream) (recursive (flat program list; (one llvm::Module "cps_module", descent) FunctionDefAST mixed in) everything else inside i32 main()) ``` @@ -32,9 +32,10 @@ uses C++ promotions, except that / always returns pseudocode REAL. ## Build & run -The compiler emits textual IR. Initial configuration must explicitly select -`CPSC_RUN_MODE=PATH`, `EMBEDDED` or `NONE` (no default). All modes need LLVM -development libraries to build; NONE still generates IR. Native binaries remain manual. +The compiler emits native executables by default and textual `.ll` IR with `-S`. +Initial configuration must explicitly select `CPSC_RUN_MODE=PATH`, `EMBEDDED` or +`NONE` (no default). All modes need LLVM/Clang/LLD 23 development libraries, +Python 3.9+ and native C/C++ development files to build; NONE supports both outputs. The following commands use Debian/Ubuntu's versioned compiler names: ```bash @@ -42,10 +43,11 @@ The following commands use Debian/Ubuntu's versioned compiler names: cmake -S . -B build -DCPSC_RUN_MODE=EMBEDDED \ -DCMAKE_C_COMPILER=clang-23 -DCMAKE_CXX_COMPILER=clang++-23 cmake --build build -build/cpsc prog.cps -o prog.ll # default without -o: prog.ir beside the source -build/cpsc prog.cps -r # execute using the selected build mode -build/cpsc prog.cps -r -o prog.ir # save IR, then run it -clang++-23 prog.ll -o prog && ./prog +build/cpsc prog.cps # save prog.out beside the source +build/cpsc prog.cps -S # save prog.ll beside the source +build/cpsc prog.cps -r # run without saving output +build/cpsc prog.cps -r -S -o prog.ll # save IR, then run it +build/cpsc prog.cps -o prog && ./prog ``` `doc/build.md` recommends which packages to install and which to download; @@ -69,7 +71,8 @@ managed root. Downloads belong to the package-manager commands in `doc/build.md` not to CMake. The Windows Pandoc ZIP is downloaded directly from its official release because MSYS2 has no Pandoc package. Only the Linux build is verified. -Generated IR uses the C++ exception ABI; native binaries must link its runtime +Generated IR uses the C++ exception ABI; native compilation links its runtime +automatically. Manually linking emitted `.ll` files still requires a C++ driver (use LLVM 23's `clang++`, or `clang++-23` on Debian/Ubuntu). TRY/CATCH/ENDTRY and bare THROW are compiler extensions. Runtime checks print the source line and error reason to stdout before throwing @@ -79,14 +82,19 @@ Exception execution is verified on Linux with LLVM 23; Windows/macOS are unverif Driver behavior (`tools/driver/main.cc`): -- One source file, with optional `-o path` and `-r`; options may precede or follow the source. +- One source file, with optional `-S`, `-o path` and `-r`; options may precede or follow the source. No stdin source input. Missing input, including no arguments, prints `cpsc: fatal error: no input files` and `compilation terminated.`, then returns 1. - `-h` anywhere in the arguments prints one documentation pointer to stdout and returns 0 without compiling: `man 1 cpsc` on Unix/macOS, or Start Menu > cpsc > cpsc Help on Windows (selected by `_WIN32`). -- IR is written only after successful parsing, code generation and module - verification. Diagnostics stay on stderr; failures do not overwrite existing IR. +- Output is written only after successful parsing, code generation and module + verification. Diagnostics stay on stderr; source errors preserve existing output. + Native linking uses a staged file beside the output and renames it on success. +- `-S` selects textual IR; otherwise output is a native executable. Without `-o`, + replace the source extension with `.ll` or `.out` beside the source on every OS. + `-o` paths are used literally and never select the format. `-r` alone saves nothing; + either `-S` or `-o` requests saving before execution. - Lexer, Parser and CodeGen track error flags (`hadError()`); any error, including invalid IR from `verifyModule`, makes the driver return 1. File/argument errors also return 1. Ordinary compiler errors use `error:`; failures that prevent @@ -107,27 +115,39 @@ Driver behavior (`tools/driver/main.cc`): The engine owns a clone of CodeGen's module; CodeGen retains the context and must outlive the engine. Static C++ exception ABI symbols are explicitly registered for JIT lookup; MCJIT registers the generated unwind frames. - The host target is initialized only for execution. + The host target is initialized for native output or embedded execution. No external runner or PATH lookup is used; `-r` alone runs in memory. -- In NONE mode, `-r` returns 1 before compiling or writing output; ordinary IR - compilation remains available. Only EMBEDDED links execution-engine components. +- In NONE mode, `-r` returns 1 before compiling or writing output; native and IR + compilation remain available. Only EMBEDDED uses the in-process execution engine. - Both execution modes return the program's exit status and inherit standard - streams. `-r -o` saves IR before executing and keeps it on execution failure. + streams. `-r -o` saves a binary; adding `-S` saves IR instead. Saved output is + kept on execution failure. Execution still uses the configured runner. -EMBEDDED requires static LLVM archives. `cmake/EmbeddedLLVM.cmake` also selects +All modes require static LLVM, Clang Driver and LLD archives. `cmake/EmbeddedLLVM.cmake` also selects static compression/terminal support archives when LLVM uses them. Linux Clang builds link the C++ runtime statically; MinGW uses `-static`. The tested Debian LLVM 23 executable has only libc, libm and the system loader as dynamic dependencies. -macOS EMBEDDED links with `-dead_strip_dylibs` to omit unused dylibs inherited +macOS links with `-dead_strip_dylibs` to omit unused dylibs inherited from LLVM's support interface, including Homebrew's Z3. Keep the nightly package's non-system dependency check; required dylibs are not removed by this option. Windows EMBEDDED is untested. +`NativeCompiler` clones the module, sets the host triple/DataLayout and emits a PIC +object. Clang Driver constructs one platform linker job, which is dispatched to +embedded LLD without launching a process. `cmake/NativeCompiler.cmake` runs +`tools/driver/prepare-native-runtime.py` at configure time to capture the build +toolchain's link inputs and compress them into generated C++ data. Native compilation +extracts startup objects, runtime archives and system link interfaces to a temporary +directory; GNU linker scripts are relocated. macOS embeds SDK library stubs, not +headers or system runtime dylibs. No user development toolchain is required. +Linux statically links non-system runtimes but dynamically links glibc; do not use +global `-static` there. Generated binaries require a compatible host system runtime. + Windows build instructions retain MSYS2 MINGW64. Its LLVM/Clang package names and compiler executable names are unversioned; CMake still requires major 23. The build guide explicitly selects `/mingw64/bin/clang.exe` and `clang++.exe` -through `cygpath -m`. MINGW64 Clang retains the GNU linker and C++ library; -switching to CLANG64 would also change the C/C++ runtime and is a separate task. +through `cygpath -m`. MINGW64 retains the GNU C++ runtime; cpsc links with LLD. Switching to +CLANG64 would change the C/C++ runtime and is a separate task. On Linux, `make -C build install` builds the manual and calls the POSIX `install.sh` to copy `cpsc` into `CMAKE_INSTALL_PREFIX/bin` and all manuals into @@ -196,14 +216,14 @@ source downloads remain available. The Linux compiler and plugin jobs use Ubuntu 24.04 x86_64; the macOS compiler job uses a macOS 15 arm64 runner, and Windows uses Windows Server 2025 x86_64. All four check out the push SHA and run independently after cleanup. All three -compiler jobs use LLVM/Clang 23, Release and EMBEDDED settings, Pandoc, and the +compiler jobs use LLVM/Clang/LLD 23, Python, Release and EMBEDDED settings, Pandoc, and the existing `release` target. Linux obtains its toolchain from apt.llvm.org; macOS uses Homebrew's LLVM 23. Windows uses `msys2/setup-msys2@v2` with MINGW64, MSYS2 LLVM/Clang packages, explicit native compiler and LLVM paths, and the MinGW Makefiles generator. It downloads the official Pandoc 3.12.1 Windows x86_64 ZIP into `build/deps/packages/` for CMake to extract. Each compiler job extracts and checks its archive, then runs a small OUTPUT program with the -packaged binary. Windows checks its ZIP in PowerShell with PATH limited to +packaged binary and checks native generation plus `.ll` output without tool lookup. Windows checks its ZIP in PowerShell with PATH limited to Windows system directories, excluding MSYS2 and LLVM. The macOS check also verifies arm64 and rejects non-system dynamic libraries. The plugin uses Node.js 22, `npm ci --ignore-scripts` and @@ -232,7 +252,8 @@ do not modify the real release for local verification. | `lib/CodeGen/CTypeGen.cc` | CodeGen methods for native C scalar conversions, casts, arithmetic and input/output | | `lib/CodeGen/*Handler.cc`, `FunctionGen.cc`, `RuntimeCheck.cc`, `TypeSystem.cc` | Helpers owned by CodeGen (including exception emission) | | `CMakeLists.txt` | 3 static libs (CPSLexer, CPSParser, CPSCodeGen) + `cpsc`. **New .cc files must be added to the source lists here** | -| `tools/driver/EmbeddedRunner.cc`, `cmake/EmbeddedLLVM.cmake` | EMBEDDED-only MCJIT runner and static support-library selection | +| `tools/driver/NativeCompiler.cc`, `cmake/NativeCompiler.cmake` | Host object generation, embedded Clang/LLD linking and compressed link resources | +| `tools/driver/EmbeddedRunner.cc`, `cmake/EmbeddedLLVM.cmake` | EMBEDDED-only MCJIT runner; static support-library selection for all modes | | `cmake/LocalDependencies.cmake`, `cmake/*Packages.cmake` | Local package extraction and platform relocation | | `cmake/DependencyPaths.cmake`, `cmake/FindDependency.cmake` | Shared local paths and local-first/system-fallback lookup | | `cmake/BuildTools.cmake`, `cmake/LLVMDependencies.cmake` | Compiler/build tool selection and LLVM dependency discovery | diff --git a/CMakeLists.txt b/CMakeLists.txt index f875856..9deae11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,13 +68,13 @@ add_library(CPSCodeGen STATIC llvm_map_components_to_libnames(llvm_libs core support native passes) if(CPSC_RUN_MODE STREQUAL "EMBEDDED") llvm_map_components_to_libnames(llvm_libs core support native passes mcjit) - foreach(llvm_lib IN LISTS llvm_libs) - get_target_property(llvm_type ${llvm_lib} TYPE) - if(NOT llvm_type STREQUAL "STATIC_LIBRARY") - message(FATAL_ERROR "EMBEDDED requires static LLVM libraries: ${llvm_lib}") - endif() - endforeach() endif() +foreach(llvm_lib IN LISTS llvm_libs) + get_target_property(llvm_type ${llvm_lib} TYPE) + if(NOT llvm_type STREQUAL "STATIC_LIBRARY") + message(FATAL_ERROR "cpsc requires static LLVM libraries: ${llvm_lib}") + endif() +endforeach() include(cmake/EmbeddedLLVM.cmake) add_executable(cpsc tools/driver/main.cc) @@ -10,7 +10,7 @@ Details can be found here: [Pseudocode Guide for Teachers](https://www.cambridge ## Features -- **LLVM compilation**: Generate LLVM IR and run programs when execution is enabled in the build. +- **LLVM compilation**: Generate native executables by default, save LLVM IR with `-S`, or run programs with `-r` when execution is enabled in the build. Default and temporary IR files use `.ll`. - **Runtime checks**: Throw exceptions for array bounds errors, division by zero, file misuse, enum range errors and null pointer dereferences. - **Exception handling**: Catch exceptions with stack unwinding using the compiler extensions [TRY/CATCH/ENDTRY and THROW](doc/language/keyword/cpsc-keyword-try.7.md). - **Arrays**: Support multiple dimensions and custom lower and upper bounds. diff --git a/cmake/NativeCompiler.cmake b/cmake/NativeCompiler.cmake index 8371ee2..0a2ad70 100644 --- a/cmake/NativeCompiler.cmake +++ b/cmake/NativeCompiler.cmake @@ -3,6 +3,9 @@ find_package(Clang REQUIRED CONFIG HINTS "${LLVM_INSTALL_PREFIX}/lib/cmake/clang cpsc_find_dependency(PATH LLD_DIR NAMES LLDConfig.cmake PATH_SUFFIXES lib/cmake/lld cmake) find_package(LLD REQUIRED CONFIG HINTS "${LLVM_INSTALL_PREFIX}/lib/cmake/lld") find_package(Python3 3.9 REQUIRED COMPONENTS Interpreter) +if(NOT LLVM_ENABLE_ZLIB) + message(FATAL_ERROR "Native link resources require LLVM built with zlib support") +endif() if(MINGW) set(cpsc_native_platform windows) diff --git a/doc/build.md b/doc/build.md index 08b4f24..d9f94c6 100644 --- a/doc/build.md +++ b/doc/build.md @@ -56,7 +56,7 @@ mkdir -p build/deps/packages cd build/deps/packages apt-get download \ llvm-23-dev llvm-23 llvm-23-runtime llvm-23-tools llvm-23-linker-tools \ - libllvm23 libclang-cpp23 libclang-23-dev liblld-23-dev libpolly-23-dev libz3-dev libz3-4 \ + libllvm23 libclang-cpp23 libclang-23-dev liblld-23-dev liblld-23 libpolly-23-dev libz3-dev libz3-4 \ libffi-dev libffi8 libxml2-dev libxml2 \ zlib1g-dev zlib1g libzstd-dev libzstd1 libncurses-dev libtinfo6 ) @@ -64,7 +64,8 @@ mkdir -p build/deps/packages `apt-get download` also downloads explicitly named packages that are already installed. The LLVM companion packages satisfy its CMake imports, while the -development support packages supply the static archives used by `EMBEDDED`. +development support packages supply required static archives. Clang, LLD and Polly +archives are needed for embedded native linking in every execution mode. ### Windows (MSYS2 MINGW64) @@ -92,8 +93,7 @@ and [LLVM package](https://packages.msys2.org/packages/mingw-w64-x86_64-llvm) currently provide 23.1.3. MSYS2 keeps these package names unversioned; its compiler executables are `clang.exe` and `clang++.exe`, not `clang-23.exe` or `clang++-23.exe`. CMake rejects another major version even when the package -name is unchanged. MINGW64 Clang uses the GNU linker and C++ library supplied -by its package dependencies; cpsc itself is compiled with Clang 23. +name is unchanged. MINGW64 Clang defaults to the GNU linker and C++ library supplied by its package dependencies. cpsc is compiled with Clang 23 and linked with LLD, retaining the GNU C++ runtime. Before downloading LLVM, confirm that these repository packages' `Version` fields all start with `23.`: @@ -132,7 +132,7 @@ relocation tools with `xcode-select --install` if needed. The LLVM bottle below provides Clang 23. Install CMake with Homebrew: ```bash -brew install cmake python lld +brew install cmake python ``` Recommended binary-bottle downloads to the build directory: @@ -159,7 +159,7 @@ initial configuration; there is no default. The choice is saved in CMake's cache |---|---| | `PATH` | Run external `lli` from LLVM 23 in PATH; reject `-r` before compilation if it is missing or its version cannot be validated. | | `EMBEDDED` | Run with the statically linked LLVM engine inside `cpsc`; no PATH lookup. | -| `NONE` | Reject `-r`; compiling to IR still works. | +| `NONE` | Reject `-r`; native and IR compilation still work. | Linux: @@ -209,6 +209,8 @@ required archive is a configure error. Linux also links the C++ runtime statical and MinGW uses static linking. The tested Debian/LLVM 23 executable needs only the system libc, libm and dynamic loader. Build for the intended OS and CPU. +All execution modes embed the Clang driver, LLD and the startup objects, runtime archives and system-library link inputs needed for native output. Python prepares these compressed resources during configuration; it is not needed by users. Native output requires no installed compiler, linker or development SDK. On Linux, generated programs statically link the C++ runtime and dynamically link glibc and its system libraries, so they require a compatible system glibc. Do not use global `-static` on Linux. macOS embeds SDK library stubs and uses operating-system runtime libraries; Windows embeds MinGW link inputs and statically links non-system runtimes. + For `PATH` mode, add the extracted LLVM 23 `lli` directory to your shell's PATH after configuration. Only `-r` checks the selected `lli --version`, with a five-second timeout. Missing, wrong or unrecognized versions fail before source diff --git a/doc/cpsc.1.md b/doc/cpsc.1.md index c58f1ad..92f73b5 100644 --- a/doc/cpsc.1.md +++ b/doc/cpsc.1.md @@ -6,15 +6,15 @@ cpsc - compile and run CIE pseudocode # SYNOPSIS -**cpsc** [**-r**] [**-o** *output*] *source* +**cpsc** [**-r**] [**-S**] [**-o** *output*] *source* **cpsc** **-h** # DESCRIPTION -Compile one pseudocode source file to LLVM IR. Options may appear before or after the source path. Source input from stdin is not supported. +Compile one pseudocode source file to a native executable, or to textual LLVM IR with **-S**. Options may appear before or after the source path. Source input from stdin is not supported. -Without **-o** or **-r**, replace the source filename's extension with `.ir` and save the output beside the source file. +Without **-o**, save beside the source file, replacing its extension with `.out` for an executable or `.ll` for IR. This naming also applies on Windows. With **-r** alone, run without saving a file. # OPTIONS @@ -22,39 +22,54 @@ Without **-o** or **-r**, replace the source filename's extension with `.ir` and : Print only the documentation entry point: `man 1 cpsc` on Unix/macOS, or Start Menu > cpsc > cpsc Help on Windows. Return successfully without compiling, even when other arguments are present. **-o** *output* -: Save IR to the specified path. May be specified only once. With **-r**, keep this file even if execution fails. +: Save to the specified path. Without **-S**, save a native executable; with **-S**, save LLVM IR. Use the path exactly as given; its extension does not select the output format. May be specified only once. With **-r**, keep this file even if execution fails. + +**-S** +: Save textual LLVM IR instead of a native executable. Without **-o**, use the source filename with a `.ll` extension beside the source file. May be combined with **-r** to save IR and run the program. **-r** -: Run the compiled program with the runner selected at build time (see EXECUTION MODES). Without **-o**, do not save permanent IR. The program inherits stdin, stdout and stderr; `INPUT` can read terminal or piped input. +: Run the compiled program with the runner selected at build time (see EXECUTION MODES). Save output first when **-S** or **-o** is also present; otherwise do not save a file. The program inherits stdin, stdout and stderr; `INPUT` can read terminal or piped input. + +# NATIVE COMPILATION + +Native compilation uses LLVM, the Clang driver and LLD embedded in `cpsc`. Required startup objects, runtime archives and system-library link inputs are embedded too and unpacked temporarily when linking. No external compiler, linker or development SDK is needed. Generate programs for the compiler's own operating system and CPU architecture; cross-compilation is not supported. + +On Linux, both `cpsc` and generated executables statically link the C++ runtime and retain dynamic dependencies on glibc and its system libraries. They require a compatible system glibc; static linking does not remove that requirement. Windows links non-system runtimes statically and uses Windows system DLLs. macOS uses operating-system libraries, including the system C++ runtime. # EXECUTION MODES -The executable has one of the following execution modes, selected when it was built. All modes can compile to LLVM IR; the mode determines whether and how **-r** runs the program. +The executable has one of the following execution modes, selected when it was built. All modes can generate native executables and LLVM IR; the mode determines whether and how **-r** runs the program. Running uses the selected runner even when a native executable is also saved. **PATH** -: Run `lli` from LLVM 23 in PATH. With **-r**, check the selected executable's version before compilation or output writing; a missing executable, a different major version or a failed or unrecognized version check is a fatal error. Version detection has a five-second timeout. Without **-r**, no external tools are checked. This mode does not invoke or check `clang` or `clang++`. Without **-o**, write temporary IR and delete it afterward, including on execution failure. +: Run `lli` from LLVM 23 in PATH. With **-r**, check the selected executable's version before compilation or output writing; a missing executable, a different major version or a failed or unrecognized version check is a fatal error. Version detection has a five-second timeout. Without **-r**, no external tools are checked. Execution uses temporary `.ll` IR unless **-S** saves IR; temporary IR is deleted afterward, including on execution failure. **EMBEDDED** -: Run the statically linked LLVM engine inside `cpsc`, without looking for `lli` in PATH. Without **-o**, execute the module in memory. Users do not need LLVM or `lli` installed; the executable may still depend on the operating system's C runtime libraries. +: Run the statically linked LLVM engine inside `cpsc`, without looking for `lli` in PATH. Execute the module in memory. Users do not need LLVM or `lli` installed; the executable may still depend on the operating system's runtime libraries. **NONE** -: Reject **-r** with an error before compilation or output writing. Compilation to IR remains available. +: Reject **-r** with an error before compilation or output writing. Native compilation and IR output remain available. # EXAMPLES -Compile and save `test.ir` beside `test.cps`: +Compile and save `test.out` beside `test.cps`: ```sh cpsc test.cps ``` -Choose the output path: +Choose the executable's output path: + +```sh +cpsc test.cps -o result +``` + +Save LLVM IR as `test.ll` beside `test.cps`: ```sh -cpsc test.cps -o result.ir +cpsc test.cps -S ``` -Run without saving permanent IR: +Run without saving output: ```sh cpsc test.cps -r @@ -63,15 +78,14 @@ cpsc test.cps -r Save IR and run it: ```sh -cpsc test.cps -r -o result.ir +cpsc test.cps -r -S -o result.ll ``` -Build a native program with Clang 23's C++ driver so the exception runtime is linked. Use `clang++` from the LLVM 23 installation; Debian/Ubuntu also provides the versioned name `clang++-23`. MSYS2 MINGW64 and Homebrew use the unversioned name. +Save a native program and run it on Unix: ```sh -cpsc test.cps -o test.ll -clang++ test.ll -o test -./test +cpsc test.cps -o test.out +./test.out ``` # EXIT STATUS @@ -88,7 +102,7 @@ An uncaught program exception returns 1 after printing `Unhandled exception. Pro # NOTES -Compiler diagnostics go to stderr. Failed compilation does not write IR. Successful compilation overwrites an existing output file, but an output path that refers to the source file (including a symbolic or hard link) is rejected. +Compiler diagnostics go to stderr. Failed source compilation does not write output; failed native linking does not replace an existing output file. Successful compilation overwrites an existing output file, but an output path that refers to the source file (including a symbolic or hard link) is rejected. Missing input, including invoking `cpsc` without arguments, reports `cpsc: fatal error: no input files`. Ordinary lexical, syntax and semantic errors use `error:`; failures that prevent compilation, such as file I/O errors, use `fatal error:`. Compilation and argument failures end with one `compilation terminated.` line. Execution-stage failures do not print that line. diff --git a/doc/install.md b/doc/install.md index a8e8a2e..9b59ee1 100644 --- a/doc/install.md +++ b/doc/install.md @@ -82,7 +82,7 @@ time in UTC followed by its short ID: `yyyymmdd-hhmmss-commit`. The version is read each time packaging runs. The package uses the current build's execution mode and contains `cpsc`, `doc/cpsc.1`, `doc/language/**/*.7`, `install.sh` and `LICENSE`. Packaging builds the compiler and manual automatically; it requires Git and Pandoc. -Use `EMBEDDED` when recipients should not need LLVM or `lli` at run time. +All modes generate executables without an installed compiler, linker or development SDK. Use `EMBEDDED` when recipients should also be able to use `-r` without LLVM or `lli` installed. Extract the archive with `tar -xJf`, enter the extracted directory, then run `sudo ./install.sh`, or `./install.sh --prefix "$HOME/.local"` for a local installation. diff --git a/tools/driver/NativeCompiler.cc b/tools/driver/NativeCompiler.cc index f2ea3dc..8d798d7 100644 --- a/tools/driver/NativeCompiler.cc +++ b/tools/driver/NativeCompiler.cc @@ -60,7 +60,7 @@ class DriverDiagnostics : public clang::DiagnosticConsumer { } // namespace -bool compileNative(const cps::CodeGen &CG, llvm::StringRef OutputPath) { +bool compileNative(const cps::CodeGen &CG, llvm::StringRef OutputPath, const char *Argv0) { TemporaryDirectory Runtime; if (auto EC = llvm::sys::fs::createUniqueDirectory("cpsc-native", Runtime.Path)) return fail("cannot create native compilation directory: " + EC.message()); @@ -157,10 +157,12 @@ bool compileNative(const cps::CodeGen &CG, llvm::StringRef OutputPath) { DriverDiagnostics Printer; clang::DiagnosticsEngine Diagnostics(new clang::DiagnosticIDs, Options, &Printer, false); clang::driver::Driver Driver("cpsc", Triple.str(), Diagnostics); - // The standard linker flavor constructs a job without checking for an - // installed linker. Its platform arguments are passed to embedded LLD. + // Clang validates the linker executable even for an unlaunched job. Use + // cpsc's own path; only the job's arguments are passed to embedded LLD. std::vector<std::string> Args = {"cpsc", "--driver-mode=g++", "-nostdlib", "-nostdlib++", - "-fuse-ld=ld", "-o", std::string(StagedPath), std::string(ObjectPath)}; + "-fuse-ld=lld", "-o", std::string(StagedPath), std::string(ObjectPath)}; + Args.push_back("--ld-path=" + llvm::sys::fs::getMainExecutable( + Argv0, reinterpret_cast<void *>(&compileNative))); for (const auto &Arg : *Metadata.getArray("arguments")) { Args.push_back("-Xlinker"); Args.push_back(relocate(*Arg.getAsString())); diff --git a/tools/driver/NativeCompiler.h b/tools/driver/NativeCompiler.h index 17933e9..5e13c4d 100644 --- a/tools/driver/NativeCompiler.h +++ b/tools/driver/NativeCompiler.h @@ -6,4 +6,4 @@ namespace cps { class CodeGen; } // Generate a host executable without an external compiler, linker or SDK. // Failure leaves any existing output intact and reports a diagnostic. -bool compileNative(const cps::CodeGen &CG, llvm::StringRef OutputPath); +bool compileNative(const cps::CodeGen &CG, llvm::StringRef OutputPath, const char *Argv0); diff --git a/tools/driver/main.cc b/tools/driver/main.cc index 56986ec..a81fb30 100644 --- a/tools/driver/main.cc +++ b/tools/driver/main.cc @@ -1,6 +1,7 @@ #include "cps/Lexer.h" #include "cps/Parser.h" #include "cps/CodeGen.h" +#include "NativeCompiler.h" #ifdef CPSC_RUN_EMBEDDED #include "EmbeddedRunner.h" #endif @@ -87,10 +88,13 @@ int main(int argc, char **argv) { std::string InputPath; std::string OutputPath; bool Run = false; + bool EmitIR = false; for (int I = 1; I < argc; ++I) { std::string Arg = argv[I]; if (Arg == "-r") { Run = true; + } else if (Arg == "-S") { + EmitIR = true; } else if (Arg == "-o") { if (!OutputPath.empty()) { llvm::errs() << "cpsc: error: -o may only be specified once\n"; @@ -133,9 +137,10 @@ int main(int argc, char **argv) { return compilationTerminated(); #endif } - if (OutputPath.empty() && !Run) { + const bool Save = !Run || EmitIR || !OutputPath.empty(); + if (OutputPath.empty() && Save) { llvm::SmallString<256> DefaultPath(InputPath); - llvm::sys::path::replace_extension(DefaultPath, ".ir"); + llvm::sys::path::replace_extension(DefaultPath, EmitIR ? ".ll" : ".out"); OutputPath = std::string(DefaultPath); } @@ -144,7 +149,7 @@ int main(int argc, char **argv) { llvm::errs() << "cpsc: fatal error: cannot open source file '" << InputPath << "'\n"; return compilationTerminated(); } - if (llvm::sys::fs::equivalent(InputPath, OutputPath)) { + if (Save && llvm::sys::fs::equivalent(InputPath, OutputPath)) { llvm::errs() << "cpsc: error: output file must not be the source file\n"; return compilationTerminated(); } @@ -164,24 +169,29 @@ int main(int argc, char **argv) { if (CG.hadError()) return compilationTerminated(); + if (Save && !EmitIR) { + if (!compileNative(CG, OutputPath, argv[0])) return compilationTerminated(); + if (!Run) return 0; + } + std::string IRPath = Save && EmitIR ? OutputPath : std::string(); #ifdef CPSC_RUN_EMBEDDED - if (Run && OutputPath.empty()) return runEmbedded(CG); + if (Run && IRPath.empty()) return runEmbedded(CG); #endif std::error_code EC; int OutputFD; llvm::FileRemover TemporaryOutput; - if (OutputPath.empty()) { + if (IRPath.empty()) { llvm::SmallString<256> TemporaryPath; - EC = llvm::sys::fs::createTemporaryFile("cpsc", "ir", OutputFD, TemporaryPath); + EC = llvm::sys::fs::createTemporaryFile("cpsc", "ll", OutputFD, TemporaryPath); if (!EC) { - OutputPath = std::string(TemporaryPath); - TemporaryOutput.setFile(OutputPath); + IRPath = std::string(TemporaryPath); + TemporaryOutput.setFile(IRPath); } } else { - EC = llvm::sys::fs::openFileForWrite(OutputPath, OutputFD); + EC = llvm::sys::fs::openFileForWrite(IRPath, OutputFD); } if (EC) { - llvm::errs() << "cpsc: fatal error: cannot open output file '" << OutputPath + llvm::errs() << "cpsc: fatal error: cannot open output file '" << IRPath << "': " << EC.message() << "\n"; return compilationTerminated(); } @@ -189,7 +199,7 @@ int main(int argc, char **argv) { CG.print(Output); Output.close(); if (Output.has_error()) { - llvm::errs() << "cpsc: fatal error: cannot write output file '" << OutputPath + llvm::errs() << "cpsc: fatal error: cannot write output file '" << IRPath << "': " << Output.error().message() << "\n"; Output.clear_error(); return compilationTerminated(); @@ -197,7 +207,7 @@ int main(int argc, char **argv) { if (!Run) return 0; #ifdef CPSC_RUN_PATH - llvm::SmallString<256> RunPath(OutputPath); + llvm::SmallString<256> RunPath(IRPath); if (auto PathError = llvm::sys::fs::make_absolute(RunPath)) { llvm::errs() << "cpsc: error: cannot resolve IR path: " << PathError.message() << "\n"; return 1; diff --git a/tools/driver/prepare-native-runtime.py b/tools/driver/prepare-native-runtime.py index bd1c8d7..10dfb7b 100644 --- a/tools/driver/prepare-native-runtime.py +++ b/tools/driver/prepare-native-runtime.py @@ -40,7 +40,7 @@ def main(): raise RuntimeError("Expected one native linker command: " + trace.stderr) command = commands[0][1:] directories = [Path(arg[2:]) for arg in command if arg.startswith("-L")] - target = compiler("-dumpmachine").stdout.strip() + target = compiler("-print-effective-triple").stdout.strip() files = {} scripts = set() arguments = [] |
