Merge branch 'main' of github.com:roc-lang/roc into upgrade-to-zig-0.15.1

This commit is contained in:
Anton-4 2025-09-22 16:55:15 +02:00
commit d791285e11
No known key found for this signature in database
GPG key ID: 0971D718C0A9B937
79 changed files with 4750 additions and 713 deletions

View file

@ -32,9 +32,9 @@ jobs:
- name: setup Roc
run: |
curl -s -OL https://github.com/roc-lang/roc/releases/download/alpha3-rolling/roc-linux_x86_64-alpha3-rolling.tar.gz
tar -xf roc-linux_x86_64-alpha3-rolling.tar.gz
rm roc-linux_x86_64-alpha3-rolling.tar.gz
curl -s -OL https://github.com/roc-lang/roc/releases/download/alpha4-rolling/roc-linux_x86_64-alpha4-rolling.tar.gz
tar -xf roc-linux_x86_64-alpha4-rolling.tar.gz
rm roc-linux_x86_64-alpha4-rolling.tar.gz
cd roc_nightly-*
# make roc binary available
echo "$(pwd)" >> $GITHUB_PATH

View file

@ -56,5 +56,9 @@ jobs:
git fetch --tags
latestTag=$(git describe --tags $(git rev-list --tags --max-count=1))
git checkout $latestTag
# remove things that don't work on musl
rm ./examples/file-accessed-modified-created-time.roc
sed -i.bak -e '/time_accessed!,$/d' -e '/time_modified!,$/d' -e '/time_created!,$/d' -e '/^time_accessed!/,/^$/d' -e '/^time_modified!/,/^$/d' -e '/^time_created!/,/^$/d' -e '/^import Utc exposing \[Utc\]$/d' ./platform/File.roc
rm ./platform/File.roc.bak
sed -i 's/x86_64/arm64/g' ./ci/test_latest_release.sh
EXAMPLES_DIR=./examples/ ./ci/test_latest_release.sh
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} EXAMPLES_DIR=./examples/ ./ci/test_latest_release.sh

140
.github/workflows/ci_cross_compile.yml vendored Normal file
View file

@ -0,0 +1,140 @@
on:
workflow_call:
name: Cross Compilation Test
# Do not add permissions here! Configure them at the job level!
permissions: {}
jobs:
# Step 1: Cross-compile musl and glibc targets from different host platforms
cross-compile:
runs-on: ${{ matrix.host }}
strategy:
fail-fast: false
matrix:
host: [
ubuntu-22.04, # Linux x64 host
macos-13, # macOS x64 host
macos-15, # macOS ARM64 host
windows-2022, # Windows x64 host
]
target: [
x64musl, # Linux x86_64 musl
arm64musl, # Linux ARM64 musl
x64glibc, # Linux x86_64 glibc
arm64glibc, # Linux ARM64 glibc
]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: mlugg/setup-zig@475c97be87a204e6c57fe851f970bd02005a70f0
with:
version: 0.14.1
use-cache: false
- name: Setup MSVC (Windows)
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Build roc compiler
id: build1
run: zig build
continue-on-error: true
- name: Build roc compiler (retry on EndOfStream)
if: failure() && contains(steps.build1.outputs.stderr, 'EndOfStream')
id: build2
run: zig build
- name: Cross-compile int platform (Unix)
if: runner.os != 'Windows'
run: |
echo "Cross-compiling from ${{ matrix.host }} to ${{ matrix.target }}"
./zig-out/bin/roc build --target=${{ matrix.target }} --output=int_app_${{ matrix.target }}_${{ matrix.host }} test/int/app.roc
- name: Cross-compile int platform (Windows)
if: runner.os == 'Windows'
run: |
echo "Cross-compiling from ${{ matrix.host }} to ${{ matrix.target }}"
zig-out\bin\roc.exe build --target=${{ matrix.target }} --output=int_app_${{ matrix.target }}_${{ matrix.host }} test/int/app.roc
- name: Upload cross-compiled executables
uses: actions/upload-artifact@v4 # ratchet:actions/upload-artifact@v4
with:
name: cross-compiled-${{ matrix.host }}-${{ matrix.target }}
path: |
int_app_${{ matrix.target }}_*
retention-days: 1
# Step 2: Test cross-compiled executables on actual target platforms
test-cross-compiled:
needs: cross-compile
runs-on: ${{ matrix.target_os }}
strategy:
fail-fast: false
matrix:
include:
# Test x64musl executables on Linux x64
- target: x64musl
target_os: ubuntu-22.04
arch: x64
# Test arm64musl executables on Linux ARM64
- target: arm64musl
target_os: ubuntu-24.04-arm
arch: arm64
# Test x64glibc executables on Linux x64
- target: x64glibc
target_os: ubuntu-22.04
arch: x64
# Test arm64glibc executables on Linux ARM64
- target: arm64glibc
target_os: ubuntu-24.04-arm
arch: arm64
steps:
- name: Download all cross-compiled artifacts
uses: actions/download-artifact@v4
with:
pattern: cross-compiled-*-${{ matrix.target }}
merge-multiple: true
- name: List downloaded files
run: |
echo "Downloaded cross-compiled executables:"
ls -la *_${{ matrix.target }}* || echo "No files found"
- name: Test cross-compiled executables from all hosts
run: |
success_count=0
total_count=0
echo "Testing ${{ matrix.target }} executables on ${{ matrix.target_os }} (${{ matrix.arch }})"
# Test int apps from all host platforms
for int_app in int_app_${{ matrix.target }}_*; do
if [ -f "$int_app" ]; then
echo ""
echo "Testing $int_app:"
chmod +x "$int_app"
if ./"$int_app"; then
echo "✅ $int_app: SUCCESS"
success_count=$((success_count + 1))
else
echo "❌ $int_app: FAILED"
fi
total_count=$((total_count + 1))
fi
done
echo ""
echo "Summary: $success_count/$total_count executables passed"
if [ $success_count -eq $total_count ] && [ $total_count -gt 0 ]; then
echo "🎉 All cross-compiled executables work correctly!"
else
echo "💥 Some cross-compiled executables failed"
exit 1
fi

View file

@ -38,7 +38,6 @@ jobs:
- name: zig check
run: |
# -Dllvm incurs a costly download step, leave that for later.
# Just the do super fast check step for now.
zig build -Dno-bin -Dfuzz -Dtracy=./tracy
@ -99,7 +98,7 @@ jobs:
- name: build roc
run: |
zig build -Dllvm -Dfuzz -Dsystem-afl=false
zig build -Dfuzz -Dsystem-afl=false
- name: Run Test Platforms (Unix)
if: runner.os != 'Windows'
@ -119,6 +118,11 @@ jobs:
zig-out\bin\roc.exe --no-cache test/str/app.roc
zig-out\bin\roc.exe --no-cache test/int/app.roc
- name: Build Test Platforms (cross-compile)
if: runner.os != 'Windows'
run: |
./ci/test_int_platform.sh
- name: roc executable minimal check (Unix)
if: runner.os != 'Windows'
run: |
@ -134,7 +138,7 @@ jobs:
- name: zig tests
run: |
zig build test -Dllvm -Dfuzz -Dsystem-afl=false
zig build test -Dfuzz -Dsystem-afl=false
- name: Check for snapshot changes
run: |
@ -223,4 +227,8 @@ jobs:
- name: cross compile with llvm
run: |
./ci/retry_flaky.sh zig build -Dtarget=${{ matrix.target }} -Dllvm
./ci/retry_flaky.sh zig build -Dtarget=${{ matrix.target }}
# Test cross-compilation with Roc's cross-compilation system (musl + glibc)
roc-cross-compile:
uses: ./.github/workflows/ci_cross_compile.yml

View file

@ -1,5 +1,5 @@
on:
#pull_request:
# pull_request:
workflow_dispatch:
schedule:
- cron: "0 9 * * *"
@ -12,18 +12,35 @@ permissions: {}
jobs:
build:
name: build and package nightly release
runs-on: [self-hosted, Linux, ARM64]
runs-on: [ubuntu-22.04-arm]
timeout-minutes: 110
steps:
- uses: actions/checkout@v4
- name: Update PATH to use zig 13
- uses: mlugg/setup-zig@475c97be87a204e6c57fe851f970bd02005a70f0
with:
version: 0.13.0
- name: install apt dependencies
run: |
echo "PATH=/home/username/Downloads/zig-linux-aarch64-0.13.0:$PATH" >> $GITHUB_ENV
sudo apt -y install wget git
sudo apt -y install libunwind-dev pkg-config zlib1g-dev
sudo apt -y install unzip # for www/build.sh
sudo apt -y install lsb-release software-properties-common gnupg # for llvm
- run: zig version
- name: install llvm
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 18
sudo rm -rf /usr/bin/clang
sudo ln -s /usr/bin/clang-18 /usr/bin/clang
sudo ln -s /usr/bin/lld-18 /usr/bin/ld.lld
sudo apt -y install libpolly-18-dev # required by llvm-sys crate
- name: create version.txt
run: ./ci/write_version.sh

1
.gitignore vendored
View file

@ -32,6 +32,7 @@ zig-out
*.rs.bk
*.o
*.a
*.s
*.so
*.so.*
*.obj

214
build.zig
View file

@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const modules = @import("src/build/modules.zig");
const glibc_stub_build = @import("src/build/glibc_stub.zig");
const Dependency = std.Build.Dependency;
const Import = std.Build.Module.Import;
const InstallDir = std.Build.InstallDir;
@ -31,7 +32,7 @@ pub fn build(b: *std.Build) void {
// llvm configuration
const use_system_llvm = b.option(bool, "system-llvm", "Attempt to automatically detect and use system installed llvm") orelse false;
const enable_llvm = b.option(bool, "llvm", "Build roc with the llvm backend") orelse use_system_llvm;
const enable_llvm = !use_system_llvm; // removed build flag `-Dllvm`, we include LLVM libraries by default now
const user_llvm_path = b.option([]const u8, "llvm-path", "Path to llvm. This path must contain the bin, lib, and include directory.");
// Since zig afl is broken currently, default to system afl.
const use_system_afl = b.option(bool, "system-afl", "Attempt to automatically detect and use system installed afl++") orelse true;
@ -364,8 +365,8 @@ fn addMainExe(
.pic = true, // Enable Position Independent Code for PIE compatibility
}),
});
test_platform_host_lib.linkLibC();
test_platform_host_lib.root_module.addImport("builtins", roc_modules.builtins);
// Force bundle compiler-rt to resolve runtime symbols like __main
test_platform_host_lib.bundle_compiler_rt = true;
@ -375,7 +376,7 @@ fn addMainExe(
copy_test_host.addCopyFileToSource(test_platform_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/str/platform", test_host_filename }));
b.getInstallStep().dependOn(&copy_test_host.step);
// Create test platform host static library (int)
// Create test platform host static library (int) - native target
const test_platform_int_host_lib = b.addLibrary(.{
.name = "test_platform_int_host",
.linkage = .static,
@ -387,8 +388,9 @@ fn addMainExe(
.pic = true, // Enable Position Independent Code for PIE compatibility
}),
});
test_platform_int_host_lib.linkLibC();
test_platform_int_host_lib.root_module.addImport("builtins", roc_modules.builtins);
// Force bundle compiler-rt to resolve runtime symbols like __main
test_platform_int_host_lib.bundle_compiler_rt = true;
// Copy the int test platform host library to the source directory
const copy_test_int_host = b.addUpdateSourceFiles();
@ -396,8 +398,48 @@ fn addMainExe(
copy_test_int_host.addCopyFileToSource(test_platform_int_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/int/platform", test_int_host_filename }));
b.getInstallStep().dependOn(&copy_test_int_host.step);
// Cross-compile int platform host libraries for musl and glibc targets
const cross_compile_targets = [_]struct { name: []const u8, query: std.Target.Query }{
.{ .name = "x64musl", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl } },
.{ .name = "arm64musl", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl } },
.{ .name = "x64glibc", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu } },
.{ .name = "arm64glibc", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu } },
};
for (cross_compile_targets) |cross_target| {
const cross_resolved_target = b.resolveTargetQuery(cross_target.query);
// Create cross-compiled int host library
const cross_int_host_lib = b.addLibrary(.{
.name = b.fmt("test_platform_int_host_{s}", .{cross_target.name}),
.root_module = b.createModule(.{
.root_source_file = b.path("test/int/platform/host.zig"),
.target = cross_resolved_target,
.optimize = optimize,
.strip = true,
.pic = true,
}),
.linkage = .static,
});
cross_int_host_lib.root_module.addImport("builtins", roc_modules.builtins);
cross_int_host_lib.bundle_compiler_rt = true;
// Copy to target-specific directory
const copy_cross_int_host = b.addUpdateSourceFiles();
copy_cross_int_host.addCopyFileToSource(cross_int_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/int/platform/targets", cross_target.name, "libhost.a" }));
b.getInstallStep().dependOn(&copy_cross_int_host.step);
// Generate glibc stubs for gnu targets
if (cross_target.query.abi == .gnu) {
const glibc_stub = generateGlibcStub(b, cross_resolved_target, cross_target.name);
if (glibc_stub) |stub| {
b.getInstallStep().dependOn(&stub.step);
}
}
}
// Create builtins static library at build time with minimal dependencies
const builtins_lib = b.addLibrary(.{
const builtins_obj = b.addLibrary(.{
.name = "roc_builtins",
.linkage = .static,
.root_module = b.createModule(.{
@ -408,41 +450,36 @@ fn addMainExe(
.pic = true, // Enable Position Independent Code for PIE compatibility
}),
});
// Add the builtins module so it can import "builtins"
builtins_lib.root_module.addImport("builtins", roc_modules.builtins);
// Force bundle compiler-rt to resolve math symbols
builtins_lib.bundle_compiler_rt = true;
// Create shim static library at build time
// Create shim static library at build time - fully static without libc
//
// NOTE we do NOT link libC here to avoid dynamic dependency on libC
const shim_lib = b.addLibrary(.{
.name = "roc_shim",
.linkage = .static,
.name = "roc_interpreter_shim",
.root_module = b.createModule(.{
.root_source_file = b.path("src/interpreter_shim/main.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
.strip = true,
.pic = true, // Enable Position Independent Code for PIE compatibility
}),
.linkage = .static,
});
shim_lib.linkLibC();
// Add all modules from roc_modules that the shim needs
roc_modules.addAll(shim_lib);
// Link against the pre-built builtins library
shim_lib.linkLibrary(builtins_lib);
// Force bundle compiler-rt to resolve math symbols
shim_lib.addObject(builtins_obj);
// Bundle compiler-rt for our math symbols
shim_lib.bundle_compiler_rt = true;
// Install shim library to the output directory
const install_shim = b.addInstallArtifact(shim_lib, .{});
b.getInstallStep().dependOn(&install_shim.step);
// We need to copy the shim library to the src/ directory for embedding as binary data
// Copy the shim library to the src/ directory for embedding as binary data
// This is because @embedFile happens at compile time and needs the file to exist already
// and zig doesn't permit embedding files from directories outside the source tree.
const copy_shim = b.addUpdateSourceFiles();
const shim_filename = if (target.result.os.tag == .windows) "roc_shim.lib" else "libroc_shim.a";
copy_shim.addCopyFileToSource(shim_lib.getEmittedBin(), b.pathJoin(&.{ "src/cli", shim_filename }));
const interpreter_shim_filename = if (target.result.os.tag == .windows) "roc_interpreter_shim.lib" else "libroc_interpreter_shim.a";
copy_shim.addCopyFileToSource(shim_lib.getEmittedBin(), b.pathJoin(&.{ "src/cli", interpreter_shim_filename }));
exe.step.dependOn(&copy_shim.step);
const config = b.addOptions();
@ -891,3 +928,138 @@ fn getCompilerVersion(b: *std.Build, optimize: OptimizeMode) []const u8 {
// Git not available or failed, use fallback
return std.fmt.allocPrint(b.allocator, "{s}-no-git", .{build_mode}) catch build_mode;
}
/// Generate glibc stubs at build time for cross-compilation
///
/// This is a minimal implementation that generates essential symbols needed for basic
/// cross-compilation to glibc targets. It creates assembly stubs with required symbols
/// like __libc_start_main, abort, getauxval, and _IO_stdin_used.
///
/// Future work: Parse Zig's abilists file to generate comprehensive
/// symbol coverage with proper versioning (e.g., symbol@@GLIBC_2.17). The abilists
/// contains thousands of glibc symbols across different versions and architectures
/// that could provide more complete stub coverage for complex applications.
fn generateGlibcStub(b: *std.Build, target: ResolvedTarget, target_name: []const u8) ?*Step.UpdateSourceFiles {
// Generate assembly stub with comprehensive symbols using the new build module
var assembly_buf = std.ArrayList(u8).init(b.allocator);
defer assembly_buf.deinit();
const writer = assembly_buf.writer();
const target_arch = target.result.cpu.arch;
const target_abi = target.result.abi;
glibc_stub_build.generateComprehensiveStub(b.allocator, writer, target_arch, target_abi) catch |err| {
std.log.warn("Failed to generate comprehensive stub assembly for {s}: {}, using minimal ELF", .{ target_name, err });
// Fall back to minimal ELF
const stub_content = switch (target.result.cpu.arch) {
.aarch64 => createMinimalElfArm64(),
.x86_64 => createMinimalElfX64(),
else => return null,
};
const write_stub = b.addWriteFiles();
const libc_so_6 = write_stub.add("libc.so.6", stub_content);
const libc_so = write_stub.add("libc.so", stub_content);
const copy_stubs = b.addUpdateSourceFiles();
copy_stubs.addCopyFileToSource(libc_so_6, b.pathJoin(&.{ "test/int/platform/targets", target_name, "libc.so.6" }));
copy_stubs.addCopyFileToSource(libc_so, b.pathJoin(&.{ "test/int/platform/targets", target_name, "libc.so" }));
copy_stubs.step.dependOn(&write_stub.step);
return copy_stubs;
};
// Write the assembly file to the targets directory
const write_stub = b.addWriteFiles();
const asm_file = write_stub.add("libc_stub.s", assembly_buf.items);
// Compile the assembly into a proper shared library using Zig's build system
const libc_stub = glibc_stub_build.compileAssemblyStub(b, asm_file, target, .ReleaseSmall);
// Copy the generated files to the target directory
const copy_stubs = b.addUpdateSourceFiles();
copy_stubs.addCopyFileToSource(libc_stub.getEmittedBin(), b.pathJoin(&.{ "test/int/platform/targets", target_name, "libc.so.6" }));
copy_stubs.addCopyFileToSource(libc_stub.getEmittedBin(), b.pathJoin(&.{ "test/int/platform/targets", target_name, "libc.so" }));
copy_stubs.addCopyFileToSource(asm_file, b.pathJoin(&.{ "test/int/platform/targets", target_name, "libc_stub.s" }));
copy_stubs.step.dependOn(&libc_stub.step);
copy_stubs.step.dependOn(&write_stub.step);
return copy_stubs;
}
/// Create a minimal ELF shared object for ARM64
fn createMinimalElfArm64() []const u8 {
// ARM64 minimal ELF shared object
return &[_]u8{
// ELF Header (64 bytes)
0x7F, 'E', 'L', 'F', // e_ident[EI_MAG0..3] - ELF magic
2, // e_ident[EI_CLASS] - ELFCLASS64
1, // e_ident[EI_DATA] - ELFDATA2LSB (little endian)
1, // e_ident[EI_VERSION] - EV_CURRENT
0, // e_ident[EI_OSABI] - ELFOSABI_NONE
0, // e_ident[EI_ABIVERSION]
0, 0, 0, 0, 0, 0, 0, // e_ident[EI_PAD] - padding
0x03, 0x00, // e_type - ET_DYN (shared object)
0xB7, 0x00, // e_machine - EM_AARCH64
0x01, 0x00, 0x00, 0x00, // e_version - EV_CURRENT
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_entry (not used for shared obj)
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_phoff - program header offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_shoff - section header offset
0x00, 0x00, 0x00, 0x00, // e_flags
0x40, 0x00, // e_ehsize - ELF header size
0x38, 0x00, // e_phentsize - program header entry size
0x01, 0x00, // e_phnum - number of program headers
0x40, 0x00, // e_shentsize - section header entry size
0x00, 0x00, // e_shnum - number of section headers
0x00, 0x00, // e_shstrndx - section header string table index
// Program Header (56 bytes) - PT_LOAD
0x01, 0x00, 0x00, 0x00, // p_type - PT_LOAD
0x05, 0x00, 0x00, 0x00, // p_flags - PF_R | PF_X
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_vaddr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_paddr
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_filesz
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_memsz
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_align
};
}
/// Create a minimal ELF shared object for x86-64
fn createMinimalElfX64() []const u8 {
// x86-64 minimal ELF shared object
return &[_]u8{
// ELF Header (64 bytes)
0x7F, 'E', 'L', 'F', // e_ident[EI_MAG0..3] - ELF magic
2, // e_ident[EI_CLASS] - ELFCLASS64
1, // e_ident[EI_DATA] - ELFDATA2LSB (little endian)
1, // e_ident[EI_VERSION] - EV_CURRENT
0, // e_ident[EI_OSABI] - ELFOSABI_NONE
0, // e_ident[EI_ABIVERSION]
0, 0, 0, 0, 0, 0, 0, // e_ident[EI_PAD] - padding
0x03, 0x00, // e_type - ET_DYN (shared object)
0x3E, 0x00, // e_machine - EM_X86_64
0x01, 0x00, 0x00, 0x00, // e_version - EV_CURRENT
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_entry (not used for shared obj)
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_phoff - program header offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_shoff - section header offset
0x00, 0x00, 0x00, 0x00, // e_flags
0x40, 0x00, // e_ehsize - ELF header size
0x38, 0x00, // e_phentsize - program header entry size
0x01, 0x00, // e_phnum - number of program headers
0x40, 0x00, // e_shentsize - section header entry size
0x00, 0x00, // e_shnum - number of section headers
0x00, 0x00, // e_shstrndx - section header string table index
// Program Header (56 bytes) - PT_LOAD
0x01, 0x00, 0x00, 0x00, // p_type - PT_LOAD
0x05, 0x00, 0x00, 0x00, // p_flags - PF_R | PF_X
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_offset
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_vaddr
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_paddr
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_filesz
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_memsz
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_align
};
}

View file

@ -1,8 +1,8 @@
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
import cli.Stderr
import cli.Path
import cli.File
import "../Glossary.md" as glossary_as_str : Str
# This script checks if all markdown links that point to files or dirs are valid for the file Glossary.md
@ -81,12 +81,8 @@ check_link! = |link_str|
# TODO check links to other markdown headers as well, e.g. #tokenization
Ok({})
else
path = Path.from_str(link_str)
_ = File.exists!(link_str) ? |_| BadLink(link_str)
when Path.type!(path) is
Ok(_) ->
Ok({})
Err(_) ->
Err(BadLink(link_str))

399
ci/test_int_platform.sh Executable file
View file

@ -0,0 +1,399 @@
#!/usr/bin/env bash
set -euo pipefail
# Colors for output (minimal usage)
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
# Test configuration
ROC_CLI="./zig-out/bin/roc"
INT_APP="test/int/app.roc"
TEST_OUTPUT_DIR="tmp_test_outputs"
# Supported targets for cross-compilation
CROSS_TARGETS=(
"x64musl"
"arm64musl"
"x64glibc"
"arm64glibc"
)
# Test results tracking
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
FAILED_TESTS=()
print_header() {
echo "================================"
echo " Roc Int Platform Test Suite "
echo "================================"
echo
}
print_section() {
echo ">>> $1"
}
print_success() {
echo -e "${GREEN}PASS${NC} $1"
}
print_error() {
echo -e "${RED}FAIL${NC} $1"
}
print_info() {
echo "INFO $1"
}
# Portable timeout wrapper:
# - Uses GNU coreutils 'timeout' if available
# - Falls back to 'gtimeout' (Homebrew coreutils on macOS)
# - Otherwise uses a shell-based timer that sends SIGTERM after N seconds
# Usage: run_with_timeout <seconds> <command> [args...]
run_with_timeout() {
local seconds="$1"; shift
if command -v timeout >/dev/null 2>&1; then
timeout "${seconds}s" "$@"
return $?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "${seconds}s" "$@"
return $?
else
( "$@" ) &
local cmd_pid=$!
( sleep "$seconds"; kill -0 "$cmd_pid" 2>/dev/null && kill -TERM "$cmd_pid" 2>/dev/null ) &
local timer_pid=$!
wait "$cmd_pid"
local exit_code=$?
kill -TERM "$timer_pid" 2>/dev/null || true
return "$exit_code"
fi
}
cleanup() {
if [ -d "$TEST_OUTPUT_DIR" ]; then
rm -rf "$TEST_OUTPUT_DIR"
fi
}
setup() {
# Create output directory
mkdir -p "$TEST_OUTPUT_DIR"
# Check if roc CLI exists
if [ ! -f "$ROC_CLI" ]; then
print_error "Roc CLI not found at $ROC_CLI"
print_info "Please run 'zig build' first to build the Roc compiler"
exit 1
fi
# Check if int app exists
if [ ! -f "$INT_APP" ]; then
print_error "Int test app not found at $INT_APP"
exit 1
fi
}
run_test() {
local test_name="$1"
local test_cmd="$2"
local expected_output="$3"
TESTS_RUN=$((TESTS_RUN + 1))
print_info "Running: $test_name"
echo " Command: $test_cmd"
if eval "$test_cmd" > "$TEST_OUTPUT_DIR/test_$TESTS_RUN.out" 2>&1; then
if [ -n "$expected_output" ]; then
# Check if expected output is present
if grep -q "$expected_output" "$TEST_OUTPUT_DIR/test_$TESTS_RUN.out"; then
print_success "$test_name"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
else
print_error "$test_name - Expected output not found"
echo " Expected: $expected_output"
echo " Got (first 5 lines):"
cat "$TEST_OUTPUT_DIR/test_$TESTS_RUN.out" | head -5
echo " NOTE: For complete output, run: cat $TEST_OUTPUT_DIR/test_$TESTS_RUN.out"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$test_name")
return 1
fi
else
print_success "$test_name"
TESTS_PASSED=$((TESTS_PASSED + 1))
return 0
fi
else
print_error "$test_name - Command failed"
# Show more complete output for arm64glibc debugging
if [[ "$test_name" == *"arm64glibc"* ]]; then
echo " Complete error output for arm64glibc debugging:"
cat "$TEST_OUTPUT_DIR/test_$TESTS_RUN.out"
else
echo " Error output (first 10 lines):"
cat "$TEST_OUTPUT_DIR/test_$TESTS_RUN.out" | head -10
echo " NOTE: This is a summary of the error output."
echo " For complete output, run: cat $TEST_OUTPUT_DIR/test_$TESTS_RUN.out"
fi
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$test_name")
return 1
fi
}
test_native_execution() {
print_section "Testing Native Build and Execution"
local native_output="$TEST_OUTPUT_DIR/int_app_native"
# Test native build (should work on current platform)
run_test "Native build" \
"$ROC_CLI build --output=$native_output $INT_APP" \
""
# Verify the executable was created
if [ ! -f "$native_output" ]; then
print_error "Native executable not created"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("native executable creation")
return 1
fi
print_success "Native executable created"
# Show executable info
if command -v file >/dev/null 2>&1; then
echo " File type: $(file "$native_output")"
fi
# Make sure it's executable
chmod +x "$native_output"
# Test execution - the int platform should run the host which calls the app functions
print_info "Testing native execution..."
local exec_output="$TEST_OUTPUT_DIR/native_exec.out"
if run_with_timeout 10 "$native_output" > "$exec_output" 2>&1; then
local exit_code=$?
if [ $exit_code -eq 0 ]; then
print_success "Native executable runs and exits successfully"
# Show what the executable outputs (useful for debugging)
if [ -s "$exec_output" ]; then
echo " Output:"
head -5 "$exec_output" | sed 's/^/ /'
fi
TESTS_PASSED=$((TESTS_PASSED + 1))
else
print_error "Native executable exited with code $exit_code"
echo " Output (first 10 lines):"
head -10 "$exec_output" | sed 's/^/ /'
echo " NOTE: For complete output, run: cat $exec_output"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("native execution exit code")
fi
else
print_error "Native executable timed out or crashed"
echo " Output (first 10 lines):"
head -10 "$exec_output" | sed 's/^/ /'
echo " NOTE: For complete output, run: cat $exec_output"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("native execution timeout")
fi
TESTS_RUN=$((TESTS_RUN + 1))
}
test_cross_compilation() {
print_section "Testing Cross-Compilation"
for target in "${CROSS_TARGETS[@]}"; do
local output_name="$TEST_OUTPUT_DIR/int_app_$target"
# Test cross-compilation build
run_test "Cross-compile to $target" \
"$ROC_CLI build --target=$target --output=$output_name $INT_APP" \
""
# Check if the executable was created
if [ -f "$output_name" ]; then
print_success "Executable created for $target"
# Show some info about the generated executable
if command -v file >/dev/null 2>&1; then
echo " File info: $(file "$output_name")"
fi
if command -v ldd >/dev/null 2>&1 && [[ "$target" == *"$(uname -m)"* ]]; then
echo " Dependencies:"
ldd "$output_name" 2>/dev/null | head -5 || echo " (static or incompatible)"
fi
else
print_error "Executable not created for $target"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$target executable creation")
fi
done
}
test_platform_build() {
print_section "Testing Platform Build System"
# Test that platform libraries are built
run_test "Build platform libraries" \
"zig build" \
""
# Check that target directories exist with expected files
for target in "${CROSS_TARGETS[@]}"; do
local target_dir="test/int/platform/targets/$target"
if [ -d "$target_dir" ]; then
print_success "Target directory exists: $target"
# Check for expected files
local expected_files=("libhost.a")
if [[ "$target" == *"glibc"* ]]; then
expected_files+=("libc.so.6" "libc.so" "libc_stub.s")
fi
for file in "${expected_files[@]}"; do
if [ -f "$target_dir/$file" ]; then
echo " $file: present"
else
print_error " $file missing in $target"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$target/$file")
fi
done
else
print_error "Target directory missing: $target"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$target directory")
fi
done
}
test_glibc_stubs() {
print_section "Testing Glibc Stub Generation"
for target in "x64glibc" "arm64glibc"; do
local stub_file="test/int/platform/targets/$target/libc_stub.s"
if [ -f "$stub_file" ]; then
print_success "Glibc stub exists: $target"
# Check that essential symbols are present
local essential_symbols=("__libc_start_main" "abort" "getauxval" "_IO_stdin_used")
local missing_symbols=0
for symbol in "${essential_symbols[@]}"; do
if grep -q "$symbol" "$stub_file"; then
echo " $symbol: present"
else
print_error " Symbol $symbol missing from $target"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$target $symbol")
missing_symbols=$((missing_symbols + 1))
fi
done
if [ $missing_symbols -eq 0 ]; then
echo " All essential symbols present"
fi
# Check architecture-specific instructions
if [[ "$target" == "x64glibc" ]]; then
if grep -q "xor %rax" "$stub_file"; then
echo " x86_64 assembly: correct"
else
print_error " x86_64 assembly instructions missing from $target"
fi
elif [[ "$target" == "arm64glibc" ]]; then
if grep -q "mov x0" "$stub_file"; then
echo " ARM64 assembly: correct"
else
print_error " ARM64 assembly instructions missing from $target"
fi
fi
else
print_error "Glibc stub missing: $target"
TESTS_FAILED=$((TESTS_FAILED + 1))
FAILED_TESTS+=("$target stub")
fi
done
}
print_summary() {
echo
print_section "Test Summary"
echo "Total tests: $TESTS_RUN"
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
if [ $TESTS_FAILED -gt 0 ]; then
echo
echo "Failed tests:"
for failed_test in "${FAILED_TESTS[@]}"; do
echo " - $failed_test"
done
echo
print_error "Some tests failed"
return 1
else
echo
print_success "All tests passed"
return 0
fi
}
main() {
print_header
# Setup
setup
trap cleanup EXIT
# Run test suites
test_platform_build
test_glibc_stubs
test_cross_compilation
test_native_execution
# Print summary and exit with appropriate code
if print_summary; then
exit 0
else
exit 1
fi
}
# Handle command line arguments
case "${1:-}" in
--help|-h)
echo "Usage: $0 [--help]"
echo
echo "Test script for Roc's int platform cross-compilation."
echo "This script tests:"
echo " - Platform build system"
echo " - Glibc stub generation"
echo " - Native execution"
echo " - Cross-compilation to all supported targets"
echo
echo "Make sure to run 'zig build' first to build the Roc compiler."
exit 0
;;
*)
main "$@"
;;
esac

View file

@ -1,4 +1,4 @@
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Arg exposing [Arg]
import cli.File
@ -32,23 +32,39 @@ main! = |raw_args|
median_results = calculate_medians(all_timing_data)
# calculate bench file hash so we're aware of changes
bench_file_hash_out = run_cmd_w_output!("sha256sum", ["src/PROFILING/bench_repeated_check.roc"])?
bench_file_hash_out =
Cmd.new("sha256sum")
|> Cmd.arg("src/PROFILING/bench_repeated_check.roc")
|> Cmd.exec_output!()?
bench_file_hash =
bench_file_hash_out
bench_file_hash_out.stdout_utf8
|> Str.split_on(" ")
|> List.get(0)?
# Get the current commit hash
commit_hash_out = run_cmd_w_output!("git", ["rev-parse", "HEAD"])?
commit_hash = Str.trim(commit_hash_out)
commit_hash_out =
Cmd.new("git")
|> Cmd.args(["rev-parse", "HEAD"])
|> Cmd.exec_output!()?
commit_hash = Str.trim(commit_hash_out.stdout_utf8)
# Get zig version
zig_version_out = run_cmd_w_output!("zig", ["version"])?
zig_version = Str.trim(zig_version_out)
zig_version_out =
Cmd.new("zig")
|> Cmd.arg("version")
|> Cmd.exec_output!()?
zig_version = Str.trim(zig_version_out.stdout_utf8)
# Get operating system with version
operating_system_out = run_cmd_w_output!("uname", ["-sr"])?
operating_system = Str.trim(operating_system_out)
operating_system_out =
Cmd.new("uname")
|> Cmd.args(["-sr"])
|> Cmd.exec_output!()?
operating_system = Str.trim(operating_system_out.stdout_utf8)
# Create the AllBenchmarkData record
benchmark_data : AllBenchmarkData
@ -66,7 +82,12 @@ main! = |raw_args|
run_benchmark_command! : {} => Result Str _
run_benchmark_command! = |{}|
run_cmd_w_output!("./zig-out/bin/roc", ["check", "src/PROFILING/bench_repeated_check.roc", "--time", "--no-cache"])
bench_output =
Cmd.new("./zig-out/bin/roc")
|> Cmd.args(["check", "src/PROFILING/bench_repeated_check.roc", "--time", "--no-cache"])
|> Cmd.exec_output!()?
Ok(bench_output.stdout_utf8)
parse_bench_stdout : Str -> Result TimingData _
parse_bench_stdout = |output|
@ -258,30 +279,6 @@ AllBenchmarkData : {
median_results : MedianResults,
}
run_cmd_w_output! : Str, List Str => Result Str [BadCmdOutput(Str)]_
run_cmd_w_output! = |cmd_str, args|
cmd_out =
Cmd.new(cmd_str)
|> Cmd.args(args)
|> Cmd.output!()
stdout_utf8 = Str.from_utf8_lossy(cmd_out.stdout)
when cmd_out.status is
Ok(0) ->
Ok(stdout_utf8)
_ ->
stderr_utf8 = Str.from_utf8_lossy(cmd_out.stderr)
err_data =
"""
Cmd `${cmd_str} ${Str.join_with(args, " ")}` failed:
- status: ${Inspect.to_str(cmd_out.status)}
- stdout: ${stdout_utf8}
- stderr: ${stderr_utf8}
"""
Err(BadCmdOutput(err_data))
# Test functions
expect
test_lines = [

86
src/build/glibc_stub.zig Normal file
View file

@ -0,0 +1,86 @@
//! GNU libc stub generation for test platforms
const std = @import("std");
/// Generate assembly stub with essential libc symbols
pub fn generateComprehensiveStub(
allocator: std.mem.Allocator,
writer: anytype,
target_arch: std.Target.Cpu.Arch,
target_abi: std.Target.Abi,
) !void {
_ = allocator;
_ = target_abi;
const ptr_width: u32 = switch (target_arch) {
.x86_64, .aarch64 => 8,
else => 4,
};
try writer.writeAll(".text\n");
// Generate __sysctl symbol
try writer.print(".balign 8\n.globl __sysctl\n.type __sysctl, %function\n__sysctl:", .{});
switch (target_arch) {
.x86_64 => try writer.writeAll(" xor %rax, %rax\n ret\n\n"),
.aarch64 => try writer.writeAll(" mov x0, #0\n ret\n\n"),
else => try writer.writeAll(" ret\n\n"),
}
// Essential libc symbols that must be present
const essential_symbols = [_][]const u8{ "__libc_start_main", "abort", "getauxval" };
for (essential_symbols) |symbol| {
try writer.print(".balign 8\n.globl {s}\n.type {s}, %function\n{s}:\n", .{ symbol, symbol, symbol });
if (std.mem.eql(u8, symbol, "abort")) {
// abort should exit with code 1
switch (target_arch) {
.x86_64 => try writer.writeAll(" mov $1, %rdi\n mov $60, %rax\n syscall\n\n"),
.aarch64 => try writer.writeAll(" mov x0, #1\n mov x8, #93\n svc #0\n\n"),
else => try writer.writeAll(" ret\n\n"),
}
} else {
// Other symbols return 0
switch (target_arch) {
.x86_64 => try writer.writeAll(" xor %rax, %rax\n ret\n\n"),
.aarch64 => try writer.writeAll(" mov x0, #0\n ret\n\n"),
else => try writer.writeAll(" ret\n\n"),
}
}
}
// Add data section
try writer.writeAll(".data\n");
try writer.print("_IO_stdin_used: ", .{});
if (ptr_width == 8) {
try writer.writeAll(".quad 1\n");
} else {
try writer.writeAll(".long 1\n");
}
}
/// Compile assembly stub to shared library using Zig's build system
pub fn compileAssemblyStub(
b: *std.Build,
asm_path: std.Build.LazyPath,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) *std.Build.Step.Compile {
// Create a shared library compilation
const lib = b.addSharedLibrary(.{
.name = "c",
.target = target,
.optimize = optimize,
.version = std.SemanticVersion{ .major = 6, .minor = 0, .patch = 0 },
});
// Add the assembly file as a source
lib.addAssemblyFile(asm_path);
// Set shared library properties
lib.linker_allow_shlib_undefined = true;
lib.pie = false; // Shared libraries should not be PIE
return lib;
}

View file

@ -16,6 +16,7 @@ pub const ModuleTest = struct {
pub const ModuleType = enum {
collections,
base,
roc_src,
types,
builtins,
compile,
@ -45,6 +46,7 @@ pub const ModuleType = enum {
.tracy => &.{ .build_options, .builtins },
.collections => &.{},
.base => &.{.collections},
.roc_src => &.{},
.types => &.{ .base, .collections },
.reporting => &.{ .collections, .base },
.parse => &.{ .tracy, .collections, .base, .reporting },
@ -68,6 +70,7 @@ pub const ModuleType = enum {
pub const RocModules = struct {
collections: *Module,
base: *Module,
roc_src: *Module,
types: *Module,
builtins: *Module,
compile: *Module,
@ -95,6 +98,7 @@ pub const RocModules = struct {
.{ .root_source_file = b.path("src/collections/mod.zig") },
),
.base = b.addModule("base", .{ .root_source_file = b.path("src/base/mod.zig") }),
.roc_src = b.addModule("roc_src", .{ .root_source_file = b.path("src/roc_src/mod.zig") }),
.types = b.addModule("types", .{ .root_source_file = b.path("src/types/mod.zig") }),
.builtins = b.addModule("builtins", .{ .root_source_file = b.path("src/builtins/mod.zig") }),
.compile = b.addModule("compile", .{ .root_source_file = b.path("src/compile/mod.zig") }),
@ -206,6 +210,7 @@ pub const RocModules = struct {
return switch (module_type) {
.collections => self.collections,
.base => self.base,
.roc_src => self.roc_src,
.types => self.types,
.builtins => self.builtins,
.compile => self.compile,

View file

@ -409,6 +409,15 @@ void ZigLLVMParseCommandLineOptions(size_t argc, const char *const *argv) {
cl::ParseCommandLineOptions(argc, argv);
}
// Initialize all LLVM targets for compilation
void ZigLLVMInitializeAllTargets() {
LLVMInitializeAllTargetInfos();
LLVMInitializeAllTargets();
LLVMInitializeAllTargetMCs();
LLVMInitializeAllAsmParsers();
LLVMInitializeAllAsmPrinters();
}
void ZigLLVMSetModulePICLevel(LLVMModuleRef module) {
unwrap(module)->setPICLevel(PICLevel::Level::BigPIC);
}

View file

@ -47,6 +47,8 @@ ZIG_EXTERN_C void ZigLLVMSetOptBisectLimit(LLVMContextRef context_ref, int limit
ZIG_EXTERN_C void ZigLLVMEnableBrokenDebugInfoCheck(LLVMContextRef context_ref);
ZIG_EXTERN_C bool ZigLLVMGetBrokenDebugInfo(LLVMContextRef context_ref);
ZIG_EXTERN_C void ZigLLVMInitializeAllTargets();
enum ZigLLVMTailCallKind {
ZigLLVMTailCallKindNone,
ZigLLVMTailCallKindTail,

View file

@ -65,6 +65,15 @@ pub const RocOps = extern struct {
self.roc_crashed(&roc_crashed_args, self.env);
}
/// Helper function to send debug output to the host.
pub fn dbg(self: *RocOps, msg: []const u8) void {
const roc_dbg_args = RocDbg{
.utf8_bytes = @constCast(msg.ptr),
.len = msg.len,
};
self.roc_dbg(&roc_dbg_args, self.env);
}
pub fn alloc(self: *RocOps, alignment: usize, length: usize) *anyopaque {
var roc_alloc_args = RocAlloc{
.alignment = alignment,

View file

@ -286,17 +286,3 @@ fn exportDecFn(comptime func: anytype, comptime func_name: []const u8) void {
fn exportUtilsFn(comptime func: anytype, comptime func_name: []const u8) void {
exportBuiltinFn(func, "utils." ++ func_name);
}
// Custom panic function, as builtin Zig version errors during LLVM verification
/// Panic function for the Roc builtins C interface.
/// This function handles runtime errors and panics in a way that's compatible
/// with the C ABI and doesn't interfere with LLVM verification.
pub fn panic(message: []const u8, stacktrace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
if (comptime builtin.target.cpu.arch != .wasm32) {
std.debug.print("\nSomehow in unreachable zig panic!\nThis is a roc standard library bug\n{s}: {?}", .{ message, stacktrace });
std.process.abort();
} else {
// Can't call abort or print from wasm. Just leave it as unreachable.
unreachable;
}
}

View file

@ -570,9 +570,10 @@ pub fn canonicalizeFile(
.package => |h| try self.createExposedScope(h.exposes),
.platform => |h| try self.createExposedScope(h.exposes),
.hosted => |h| try self.createExposedScope(h.exposes),
.app => {
.app => |h| {
// App headers have 'provides' instead of 'exposes'
// TODO: Handle app provides differently
// but we need to track the provided functions for export
try self.createExposedScope(h.provides);
},
.malformed => {
// Skip malformed headers
@ -935,6 +936,9 @@ pub fn canonicalizeFile(
self.env.all_defs = try self.env.store.defSpanFrom(scratch_defs_start);
self.env.all_statements = try self.env.store.statementSpanFrom(scratch_statements_start);
// Create the span of exported defs by finding definitions that correspond to exposed items
try self.populateExports();
// Assert that everything is in-sync
self.env.debugAssertArraysInSync();
@ -1128,6 +1132,31 @@ fn createExposedScope(
}
}
fn populateExports(self: *Self) std.mem.Allocator.Error!void {
// Start a new scratch space for exports
const scratch_exports_start = self.env.store.scratchDefTop();
// Use the already-created all_defs span
const defs_slice = self.env.store.sliceDefs(self.env.all_defs);
// Check each definition to see if it corresponds to an exposed item
for (defs_slice) |def_idx| {
const def = self.env.store.getDef(def_idx);
const pattern = self.env.store.getPattern(def.pattern);
if (pattern == .assign) {
// Check if this definition's identifier is in the exposed items
if (self.env.common.exposed_items.containsById(self.env.gpa, @bitCast(pattern.assign.ident))) {
// Add this definition to the exports scratch space
try self.env.store.addScratchDef(def_idx);
}
}
}
// Create the exports span from the scratch space
self.env.exports = try self.env.store.defSpanFrom(scratch_exports_start);
}
fn checkExposedButNotImplemented(self: *Self) std.mem.Allocator.Error!void {
// Check for remaining exposed identifiers
var ident_iter = self.exposed_ident_texts.iterator();

View file

@ -41,6 +41,8 @@ types: TypeStore,
all_defs: CIR.Def.Span,
/// All the top-level statements in the module (populated by canonicalization)
all_statements: CIR.Statement.Span,
/// Definitions that are exported by this module (populated by canonicalization)
exports: CIR.Def.Span,
/// All external declarations referenced in this module
external_decls: CIR.ExternalDecl.SafeList,
/// Store for interned module imports
@ -59,6 +61,7 @@ pub fn initCIRFields(self: *Self, gpa: std.mem.Allocator, module_name: []const u
_ = gpa; // unused since we don't create new allocations
self.all_defs = .{ .span = .{ .start = 0, .len = 0 } };
self.all_statements = .{ .span = .{ .start = 0, .len = 0 } };
self.exports = .{ .span = .{ .start = 0, .len = 0 } };
// Note: external_decls already exists from ModuleEnv.init(), so we don't create a new one
self.imports = CIR.Import.Store.init();
self.module_name = module_name;
@ -81,6 +84,7 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!
.types = try TypeStore.initCapacity(gpa, 2048, 512),
.all_defs = .{ .span = .{ .start = 0, .len = 0 } },
.all_statements = .{ .span = .{ .start = 0, .len = 0 } },
.exports = .{ .span = .{ .start = 0, .len = 0 } },
.external_decls = try CIR.ExternalDecl.SafeList.initCapacity(gpa, 16),
.imports = CIR.Import.Store.init(),
.module_name = "", // Will be set later during canonicalization
@ -1016,6 +1020,7 @@ pub fn serialize(
.types = (try self.types.serialize(allocator, writer)).*,
.all_defs = self.all_defs,
.all_statements = self.all_statements,
.exports = self.exports,
.external_decls = (try self.external_decls.serialize(allocator, writer)).*,
.imports = (try self.imports.serialize(allocator, writer)).*,
.module_name = "", // Will be set when deserializing
@ -1057,6 +1062,7 @@ pub const Serialized = struct {
types: TypeStore.Serialized,
all_defs: CIR.Def.Span,
all_statements: CIR.Statement.Span,
exports: CIR.Def.Span,
external_decls: CIR.ExternalDecl.SafeList.Serialized,
imports: CIR.Import.Store.Serialized,
module_name: []const u8, // Serialized as zeros, provided during deserialization
@ -1079,6 +1085,7 @@ pub const Serialized = struct {
// Copy simple values directly
self.all_defs = env.all_defs;
self.all_statements = env.all_statements;
self.exports = env.exports;
try self.external_decls.serialize(&env.external_decls, allocator, writer);
try self.imports.serialize(&env.imports, allocator, writer);
@ -1113,6 +1120,7 @@ pub const Serialized = struct {
.types = self.types.deserialize(offset).*,
.all_defs = self.all_defs,
.all_statements = self.all_statements,
.exports = self.exports,
.external_decls = self.external_decls.deserialize(offset).*,
.imports = self.imports.deserialize(offset, gpa).*,
.module_name = module_name,

273
src/cli/app_stub.zig Normal file
View file

@ -0,0 +1,273 @@
//! Generates app stub libraries for cross-compilation
//! These stubs provide the Roc app entrypoints that the platform host expects to call
const std = @import("std");
const builtin = @import("builtin");
const target_mod = @import("target.zig");
const builder = @import("builder.zig");
const RocTarget = target_mod.RocTarget;
const Allocator = std.mem.Allocator;
// Check if LLVM is available at compile time
const llvm_available = if (@import("builtin").is_test) false else @import("config").llvm;
/// Platform entrypoint information
pub const PlatformEntrypoint = struct {
name: []const u8, // Function name like "addInts", "processString"
};
/// Generate an app stub object file containing implementations for platform-expected entrypoints
pub fn generateAppStubObject(
allocator: Allocator,
output_dir: []const u8,
entrypoints: []const PlatformEntrypoint,
target: RocTarget,
) ![]const u8 {
// Check if LLVM is available
if (!llvm_available) {
return error.LLVMNotAvailable;
}
const std_zig_llvm = @import("std").zig.llvm;
const Builder = std_zig_llvm.Builder;
// Create LLVM Builder
var llvm_builder = try Builder.init(.{
.allocator = allocator,
.name = "roc_app_stub",
});
defer llvm_builder.deinit();
// Generate the app stub functions
try createAppStubs(&llvm_builder, entrypoints, target);
// Generate paths for temporary files
const bitcode_path = try std.fs.path.join(allocator, &.{ output_dir, "app_stub.bc" });
defer allocator.free(bitcode_path);
const object_filename = try std.fmt.allocPrint(allocator, "app_stub_{s}.o", .{@tagName(target)});
const object_path = try std.fs.path.join(allocator, &.{ output_dir, object_filename });
// Don't defer free object_path since we return it
// Generate bitcode
const producer = Builder.Producer{
.name = "Roc App Stub Generator",
.version = .{ .major = 1, .minor = 0, .patch = 0 },
};
const bitcode = try llvm_builder.toBitcode(allocator, producer);
defer allocator.free(bitcode);
// Write bitcode to file
const bc_file = try std.fs.cwd().createFile(bitcode_path, .{});
defer bc_file.close();
// Convert u32 array to bytes for writing
const bytes = std.mem.sliceAsBytes(bitcode);
try bc_file.writeAll(bytes);
std.log.debug("Wrote bitcode file: {s} ({} bytes)", .{ bitcode_path, bytes.len });
// Compile bitcode to object file using LLVM
// For native compilation, use empty CPU to let LLVM choose the default
// For cross-compilation, use "generic" for maximum compatibility
const detected_native = target_mod.RocTarget.detectNative();
const is_native = target == detected_native;
const cpu_name = if (is_native) "" else "generic";
std.log.debug("Native target: {}, Request target: {}, Is native: {}", .{ detected_native, target, is_native });
std.log.debug("Using CPU: '{s}'", .{cpu_name});
const compile_config = builder.CompileConfig{
.input_path = bitcode_path,
.output_path = object_path,
.optimization = .size,
.target = target,
.cpu = cpu_name,
.features = "",
};
std.log.debug("About to call compileBitcodeToObject...", .{});
const success = builder.compileBitcodeToObject(allocator, compile_config) catch |err| {
std.log.err("Failed to compile bitcode to object: {}", .{err});
allocator.free(object_path);
return err;
};
std.log.debug("compileBitcodeToObject returned: {}", .{success});
if (!success) {
std.log.err("Bitcode compilation returned false without error", .{});
allocator.free(object_path);
return error.CompilationFailed;
}
std.log.debug("Generated app stub object: {s}", .{object_path});
return object_path;
}
/// Creates app stub functions in LLVM IR
fn createAppStubs(llvm_builder: *std.zig.llvm.Builder, entrypoints: []const PlatformEntrypoint, target: RocTarget) !void {
// Create pointer type
const ptr_type = try llvm_builder.ptrType(.default);
// Add stub for each platform entrypoint
for (entrypoints) |entrypoint| {
try addRocCallAbiStub(llvm_builder, ptr_type, entrypoint.name, target);
}
}
/// Add an app entrypoint stub that follows the RocCall ABI
/// RocCall ABI: void roc__<name>(ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.C) void;
fn addRocCallAbiStub(
llvm_builder: *std.zig.llvm.Builder,
ptr_type: std.zig.llvm.Builder.Type,
name: []const u8,
target: RocTarget,
) !void {
const Builder = std.zig.llvm.Builder;
const WipFunction = Builder.WipFunction;
// RocCall ABI signature: void roc__<name>(ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.C) void
const params = [_]Builder.Type{ ptr_type, ptr_type, ptr_type };
const fn_type = try llvm_builder.fnType(.void, &params, .normal);
// Build the function name with roc__ prefix
const base_name = try std.fmt.allocPrint(llvm_builder.gpa, "roc__{s}", .{name});
defer llvm_builder.gpa.free(base_name);
// Add platform-specific prefix if needed (e.g., underscore for macOS)
const full_name = if (target.isMacOS())
try std.fmt.allocPrint(llvm_builder.gpa, "_{s}", .{base_name})
else
try llvm_builder.gpa.dupe(u8, base_name);
defer llvm_builder.gpa.free(full_name);
const fn_name = try llvm_builder.strtabString(full_name);
const func = try llvm_builder.addFunction(fn_type, fn_name, .default);
// Use external linkage so the symbol is visible to the linker
func.setLinkage(.external, llvm_builder);
var wip = try WipFunction.init(llvm_builder, .{
.function = func,
.strip = false,
});
defer wip.deinit();
const entry = try wip.block(0, "entry");
wip.cursor = .{ .block = entry };
// Generate actual implementation based on function name
if (std.mem.eql(u8, name, "addInts")) {
try addIntsImplementation(&wip, llvm_builder);
} else if (std.mem.eql(u8, name, "multiplyInts")) {
try multiplyIntsImplementation(&wip, llvm_builder);
} else if (std.mem.eql(u8, name, "processString")) {
// processString not supported in cross-compilation stubs - only int platform supported
_ = try wip.retVoid();
} else {
// Default: just return void for unknown functions
_ = try wip.retVoid();
}
try wip.finish();
}
/// Get the expected app entrypoints for known test platforms based on host.zig files
pub fn getTestPlatformEntrypoints(allocator: Allocator, platform_type: []const u8) ![]PlatformEntrypoint {
if (std.mem.eql(u8, platform_type, "int")) {
// Based on test/int/platform/host.zig:
// extern fn roc__addInts(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.C) void;
// extern fn roc__multiplyInts(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.C) void;
const entrypoints = try allocator.alloc(PlatformEntrypoint, 2);
entrypoints[0] = PlatformEntrypoint{ .name = "addInts" };
entrypoints[1] = PlatformEntrypoint{ .name = "multiplyInts" };
return entrypoints;
}
// Only int platform supported for cross-compilation
return error.PlatformNotSupported;
}
/// Detect platform type from file path
pub fn detectPlatformType(platform_path: []const u8) []const u8 {
// Use cross-platform path checking
var iter = std.fs.path.componentIterator(platform_path) catch return "unknown";
while (iter.next()) |component| {
if (std.mem.eql(u8, component.name, "int")) {
return "int";
} else if (std.mem.eql(u8, component.name, "str")) {
return "str";
}
}
return "unknown";
}
/// Generate implementation for addInts: loads two i64s from arg_ptr, adds them, stores result to ret_ptr
fn addIntsImplementation(wip: *std.zig.llvm.Builder.WipFunction, llvm_builder: *std.zig.llvm.Builder) !void {
// Get function parameters: ops, ret_ptr, arg_ptr
const ret_ptr = wip.arg(1); // ret_ptr: *anyopaque -> where to store the i64 result
const arg_ptr = wip.arg(2); // arg_ptr: *anyopaque -> points to struct { a: i64, b: i64 }
// Cast arg_ptr to pointer to struct { i64, i64 }
const i64_type = .i64;
const args_struct_type = try llvm_builder.structType(.normal, &[_]std.zig.llvm.Builder.Type{ i64_type, i64_type });
const args_ptr_type = try llvm_builder.ptrType(.default);
const args_ptr = try wip.cast(.bitcast, arg_ptr, args_ptr_type, "args_ptr");
// Load the two i64 values from the args struct
const zero = try llvm_builder.intConst(.i32, 0);
const one = try llvm_builder.intConst(.i32, 1);
const a_ptr = try wip.gep(.inbounds, args_struct_type, args_ptr, &[_]std.zig.llvm.Builder.Value{ zero.toValue(), zero.toValue() }, "a_ptr");
const b_ptr = try wip.gep(.inbounds, args_struct_type, args_ptr, &[_]std.zig.llvm.Builder.Value{ zero.toValue(), one.toValue() }, "b_ptr");
const a = try wip.load(.normal, i64_type, a_ptr, .default, "a");
const b = try wip.load(.normal, i64_type, b_ptr, .default, "b");
// Add the two values
const result = try wip.bin(.add, a, b, "result");
// Cast ret_ptr and store the result
const ret_i64_ptr = try wip.cast(.bitcast, ret_ptr, args_ptr_type, "ret_i64_ptr");
_ = try wip.store(.normal, result, ret_i64_ptr, .default);
// Return void
_ = try wip.retVoid();
}
/// Generate implementation for multiplyInts: loads two i64s from arg_ptr, multiplies them, stores result to ret_ptr
fn multiplyIntsImplementation(wip: *std.zig.llvm.Builder.WipFunction, llvm_builder: *std.zig.llvm.Builder) !void {
// Get function parameters: ops, ret_ptr, arg_ptr
const ret_ptr = wip.arg(1); // ret_ptr: *anyopaque -> where to store the i64 result
const arg_ptr = wip.arg(2); // arg_ptr: *anyopaque -> points to struct { a: i64, b: i64 }
// Cast arg_ptr to pointer to struct { i64, i64 }
const i64_type = .i64;
const args_struct_type = try llvm_builder.structType(.normal, &[_]std.zig.llvm.Builder.Type{ i64_type, i64_type });
const args_ptr_type = try llvm_builder.ptrType(.default);
const args_ptr = try wip.cast(.bitcast, arg_ptr, args_ptr_type, "args_ptr");
// Load the two i64 values from the args struct
const zero = try llvm_builder.intConst(.i32, 0);
const one = try llvm_builder.intConst(.i32, 1);
const a_ptr = try wip.gep(.inbounds, args_struct_type, args_ptr, &[_]std.zig.llvm.Builder.Value{ zero.toValue(), zero.toValue() }, "a_ptr");
const b_ptr = try wip.gep(.inbounds, args_struct_type, args_ptr, &[_]std.zig.llvm.Builder.Value{ zero.toValue(), one.toValue() }, "b_ptr");
const a = try wip.load(.normal, i64_type, a_ptr, .default, "a");
const b = try wip.load(.normal, i64_type, b_ptr, .default, "b");
// Multiply the two values
const result = try wip.bin(.mul, a, b, "result");
// Cast ret_ptr and store the result
const ret_i64_ptr = try wip.cast(.bitcast, ret_ptr, args_ptr_type, "ret_i64_ptr");
_ = try wip.store(.normal, result, ret_i64_ptr, .default);
// Return void
_ = try wip.retVoid();
}

265
src/cli/builder.zig Normal file
View file

@ -0,0 +1,265 @@
//! LLVM-based compilation infrastructure for Roc
const std = @import("std");
const builtin = @import("builtin");
const target = @import("target.zig");
const Allocator = std.mem.Allocator;
// Re-export RocTarget from target.zig for backward compatibility
pub const RocTarget = target.RocTarget;
/// Optimization levels for compilation
pub const OptimizationLevel = enum {
none, // --opt none (no optimizations)
size, // --opt size (optimize for binary size)
speed, // --opt speed (aggressive performance optimizations)
/// Convert to LLVM optimization level
fn toLLVMLevel(self: OptimizationLevel) c_int {
return switch (self) {
.none => LLVMCodeGenLevelNone,
.size => LLVMCodeGenLevelLess,
.speed => LLVMCodeGenLevelAggressive,
};
}
};
/// Configuration for compiling LLVM bitcode to object files
pub const CompileConfig = struct {
input_path: []const u8,
output_path: []const u8,
optimization: OptimizationLevel,
target: RocTarget,
cpu: []const u8 = "",
features: []const u8 = "",
/// Check if compiling for the current machine
pub fn isNative(self: CompileConfig) bool {
return self.target == RocTarget.detectNative();
}
};
// Check if LLVM is available at compile time
const llvm_available = if (@import("builtin").is_test) false else @import("config").llvm;
// LLVM ABI Types
const ZigLLVMABIType = enum(c_int) {
ZigLLVMABITypeDefault = 0,
ZigLLVMABITypeSoft,
ZigLLVMABITypeHard,
};
// LLVM Code Generation Optimization Levels
const LLVMCodeGenLevelNone: c_int = 0;
const LLVMCodeGenLevelLess: c_int = 1;
const LLVMCodeGenLevelDefault: c_int = 2;
const LLVMCodeGenLevelAggressive: c_int = 3;
// LLVM Relocation Models
const LLVMRelocDefault: c_int = 0;
const LLVMRelocStatic: c_int = 1;
const LLVMRelocPIC: c_int = 2;
const LLVMRelocDynamicNoPic: c_int = 3;
const LLVMRelocROPI: c_int = 4;
const LLVMRelocRWPI: c_int = 5;
const LLVMRelocROPI_RWPI: c_int = 6;
// LLVM Code Models
const LLVMCodeModelDefault: c_int = 0;
const LLVMCodeModelJITDefault: c_int = 1;
const LLVMCodeModelTiny: c_int = 2;
const LLVMCodeModelSmall: c_int = 3;
const LLVMCodeModelKernel: c_int = 4;
const LLVMCodeModelMedium: c_int = 5;
const LLVMCodeModelLarge: c_int = 6;
// External C functions from zig_llvm.cpp and LLVM C API - only available when LLVM is enabled
const llvm_externs = if (llvm_available) struct {
extern fn ZigLLVMTargetMachineEmitToFile(
targ_machine_ref: ?*anyopaque,
module_ref: ?*anyopaque,
error_message: *[*:0]u8,
is_debug: bool,
is_small: bool,
time_report: bool,
tsan: bool,
lto: bool,
asm_filename: ?[*:0]const u8,
bin_filename: ?[*:0]const u8,
llvm_ir_filename: ?[*:0]const u8,
bitcode_filename: ?[*:0]const u8,
) bool;
extern fn ZigLLVMCreateTargetMachine(
target_ref: ?*anyopaque,
triple: [*:0]const u8,
cpu: [*:0]const u8,
features: [*:0]const u8,
level: c_int, // LLVMCodeGenOptLevel
reloc: c_int, // LLVMRelocMode
code_model: c_int, // LLVMCodeModel
function_sections: bool,
data_sections: bool,
float_abi: ZigLLVMABIType,
abi_name: ?[*:0]const u8,
) ?*anyopaque;
// LLVM wrapper functions
extern fn ZigLLVMInitializeAllTargets() void;
// LLVM C API functions
extern fn LLVMGetDefaultTargetTriple() [*:0]u8;
extern fn LLVMGetTargetFromTriple(triple: [*:0]const u8, target: *?*anyopaque, error_message: *[*:0]u8) c_int;
extern fn LLVMDisposeMessage(message: [*:0]u8) void;
extern fn LLVMCreateMemoryBufferWithContentsOfFile(path: [*:0]const u8, out_mem_buf: *?*anyopaque, out_message: *[*:0]u8) c_int;
extern fn LLVMParseBitcode(mem_buf: ?*anyopaque, out_module: *?*anyopaque, out_message: *[*:0]u8) c_int;
extern fn LLVMDisposeMemoryBuffer(mem_buf: ?*anyopaque) void;
extern fn LLVMDisposeModule(module: ?*anyopaque) void;
extern fn LLVMDisposeTargetMachine(target_machine: ?*anyopaque) void;
extern fn LLVMSetTarget(module: ?*anyopaque, triple: [*:0]const u8) void;
} else struct {};
/// Initialize LLVM targets (must be called once before using LLVM)
pub fn initializeLLVM() void {
if (comptime !llvm_available) {
return;
}
const externs = llvm_externs;
externs.ZigLLVMInitializeAllTargets();
}
/// Compile LLVM bitcode file to object file
pub fn compileBitcodeToObject(gpa: Allocator, config: CompileConfig) !bool {
if (comptime !llvm_available) {
std.log.err("LLVM is not available at compile time", .{});
return error.LLVMNotAvailable;
}
const externs = llvm_externs;
std.log.debug("Starting bitcode to object compilation", .{});
std.log.debug("Input: {s} -> Output: {s}", .{ config.input_path, config.output_path });
std.log.debug("Target: {} ({s})", .{ config.target, config.target.toTriple() });
std.log.debug("Optimization: {}", .{config.optimization});
std.log.debug("CPU: '{s}', Features: '{s}'", .{ config.cpu, config.features });
// Verify input file exists
std.fs.cwd().access(config.input_path, .{}) catch |err| {
std.log.err("Input bitcode file does not exist or is not accessible: {s}, error: {}", .{ config.input_path, err });
return false;
};
// 1. Initialize LLVM targets
std.log.debug("Initializing LLVM targets...", .{});
initializeLLVM();
std.log.debug("LLVM targets initialized successfully", .{});
// 2. Load bitcode file
std.log.debug("Loading bitcode file: {s}", .{config.input_path});
var mem_buf: ?*anyopaque = null;
var error_message: [*:0]u8 = undefined;
const bitcode_path_z = try gpa.dupeZ(u8, config.input_path);
defer gpa.free(bitcode_path_z);
if (externs.LLVMCreateMemoryBufferWithContentsOfFile(bitcode_path_z.ptr, &mem_buf, &error_message) != 0) {
std.log.err("Failed to load bitcode file: {s}", .{error_message});
externs.LLVMDisposeMessage(error_message);
return false;
}
defer if (mem_buf) |buf| externs.LLVMDisposeMemoryBuffer(buf);
std.log.debug("Bitcode file loaded successfully", .{});
// 3. Parse bitcode into module
std.log.debug("Parsing bitcode into LLVM module...", .{});
var module: ?*anyopaque = null;
if (externs.LLVMParseBitcode(mem_buf, &module, &error_message) != 0) {
std.log.err("Failed to parse bitcode: {s}", .{error_message});
externs.LLVMDisposeMessage(error_message);
return false;
}
defer if (module) |mod| externs.LLVMDisposeModule(mod);
std.log.debug("Bitcode parsed successfully", .{});
// 4. Get target triple and set it on the module
const target_triple = config.target.toTriple();
const target_triple_z = try gpa.dupeZ(u8, target_triple);
defer gpa.free(target_triple_z);
std.log.debug("Setting target triple on module: {s}", .{target_triple});
externs.LLVMSetTarget(module, target_triple_z.ptr);
std.log.debug("Target triple set successfully", .{});
// 5. Create target
std.log.debug("Getting LLVM target for triple: {s}", .{target_triple});
var llvm_target: ?*anyopaque = null;
if (externs.LLVMGetTargetFromTriple(target_triple_z.ptr, &llvm_target, &error_message) != 0) {
std.log.err("Failed to get target from triple: {s}", .{error_message});
externs.LLVMDisposeMessage(error_message);
return false;
}
std.log.debug("LLVM target obtained successfully", .{});
// 6. Create target machine
const cpu_z = try gpa.dupeZ(u8, config.cpu);
defer gpa.free(cpu_z);
const features_z = try gpa.dupeZ(u8, config.features);
defer gpa.free(features_z);
std.log.debug("Creating target machine with CPU='{s}', Features='{s}'", .{ config.cpu, config.features });
const target_machine = externs.ZigLLVMCreateTargetMachine(
llvm_target,
target_triple_z.ptr,
cpu_z.ptr,
features_z.ptr,
config.optimization.toLLVMLevel(),
LLVMRelocDefault,
LLVMCodeModelDefault,
false, // function_sections
false, // data_sections
.ZigLLVMABITypeDefault, // float_abi
null, // abi_name
);
if (target_machine == null) {
std.log.err("Failed to create target machine for triple='{s}', cpu='{s}', features='{s}'", .{ target_triple, config.cpu, config.features });
return false;
}
defer externs.LLVMDisposeTargetMachine(target_machine);
std.log.debug("Target machine created successfully", .{});
// 7. Prepare output path
const object_path_z = try gpa.dupeZ(u8, config.output_path);
defer gpa.free(object_path_z);
// 8. Emit object file
std.log.debug("Emitting object file to: {s}", .{config.output_path});
var emit_error_message: [*:0]u8 = undefined;
const emit_result = externs.ZigLLVMTargetMachineEmitToFile(
target_machine,
module,
&emit_error_message,
false, // is_debug
config.optimization == .size, // is_small
false, // time_report
false, // tsan
false, // lto
null, // asm_filename
object_path_z.ptr, // bin_filename
null, // llvm_ir_filename
null, // bitcode_filename
);
if (emit_result) {
std.log.err("Failed to emit object file to '{s}': {s}", .{ config.output_path, emit_error_message });
externs.LLVMDisposeMessage(emit_error_message);
return false;
}
std.log.debug("Successfully compiled bitcode to object file: {s}", .{config.output_path});
return true;
}
/// Check if LLVM is available
pub fn isLLVMAvailable() bool {
return llvm_available;
}

View file

@ -64,6 +64,7 @@ pub const OptLevel = enum {
pub const RunArgs = struct {
path: []const u8, // the path of the roc file to be executed
opt: OptLevel = .dev, // the optimization level
target: ?[]const u8 = null, // the target to compile for (e.g., x64musl, x64glibc)
app_args: []const []const u8 = &[_][]const u8{}, // any arguments to be passed to roc application being run
no_cache: bool = false, // bypass the executable cache
};
@ -81,6 +82,7 @@ pub const CheckArgs = struct {
pub const BuildArgs = struct {
path: []const u8, // the path to the roc file to be built
opt: OptLevel, // the optimization level
target: ?[]const u8 = null, // the target to compile for (e.g., x64musl, x64glibc)
output: ?[]const u8 = null, // the path where the output binary should be created
z_bench_tokenize: ?[]const u8 = null, // benchmark tokenizer on a file or directory
z_bench_parse: ?[]const u8 = null, // benchmark parser on a file or directory
@ -165,6 +167,7 @@ const main_help =
\\ e.g. `roc run -- arg1 arg2`
\\Options:
\\ --opt=<size|speed|dev> Optimize the build process for binary size, execution speed, or compilation speed. Defaults to compilation speed (dev)
\\ --target=<target> Target to compile for (e.g., x64musl, x64glibc, arm64musl). Defaults to native target with musl for static linking
\\
;
@ -219,6 +222,7 @@ fn parseCheck(args: []const []const u8) CliArgs {
fn parseBuild(args: []const []const u8) CliArgs {
var path: ?[]const u8 = null;
var opt: OptLevel = .dev;
var target: ?[]const u8 = null;
var output: ?[]const u8 = null;
var z_bench_tokenize: ?[]const u8 = null;
var z_bench_parse: ?[]const u8 = null;
@ -235,11 +239,18 @@ fn parseBuild(args: []const []const u8) CliArgs {
\\Options:
\\ --output=<output> The full path to the output binary, including filename. To specify directory only, specify a path that ends in a directory separator (e.g. a slash)
\\ --opt=<size|speed|dev> Optimize the build process for binary size, execution speed, or compilation speed. Defaults to compilation speed (dev)
\\ --target=<target> Target to compile for (e.g., x64musl, x64glibc, arm64musl). Defaults to native target with musl for static linking
\\ --z-bench-tokenize=<path> Benchmark tokenizer on a file or directory
\\ --z-bench-parse=<path> Benchmark parser on a file or directory
\\ -h, --help Print help
\\
};
} else if (mem.startsWith(u8, arg, "--target")) {
if (getFlagValue(arg)) |value| {
target = value;
} else {
return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--target" } } };
}
} else if (mem.startsWith(u8, arg, "--output")) {
if (getFlagValue(arg)) |value| {
output = value;
@ -275,7 +286,7 @@ fn parseBuild(args: []const []const u8) CliArgs {
path = arg;
}
}
return CliArgs{ .build = BuildArgs{ .path = path orelse "main.roc", .opt = opt, .output = output, .z_bench_tokenize = z_bench_tokenize, .z_bench_parse = z_bench_parse } };
return CliArgs{ .build = BuildArgs{ .path = path orelse "main.roc", .opt = opt, .target = target, .output = output, .z_bench_tokenize = z_bench_tokenize, .z_bench_parse = z_bench_parse } };
}
fn parseBundle(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs {
@ -599,6 +610,7 @@ fn parseDocs(args: []const []const u8) CliArgs {
fn parseRun(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs {
var path: ?[]const u8 = null;
var opt: OptLevel = .dev;
var target: ?[]const u8 = null;
var no_cache: bool = false;
var app_args = std.array_list.Managed([]const u8).init(gpa);
for (args) |arg| {
@ -610,6 +622,12 @@ fn parseRun(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Erro
// We need to free the paths here because we aren't returning the .format variant
app_args.deinit();
return CliArgs.version;
} else if (mem.startsWith(u8, arg, "--target")) {
if (getFlagValue(arg)) |value| {
target = value;
} else {
return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--target" } } };
}
} else if (mem.startsWith(u8, arg, "--opt")) {
if (getFlagValue(arg)) |value| {
if (OptLevel.from_str(value)) |level| {
@ -630,7 +648,7 @@ fn parseRun(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Erro
}
}
}
return CliArgs{ .run = RunArgs{ .path = path orelse "main.roc", .opt = opt, .app_args = try app_args.toOwnedSlice(), .no_cache = no_cache } };
return CliArgs{ .run = RunArgs{ .path = path orelse "main.roc", .opt = opt, .target = target, .app_args = try app_args.toOwnedSlice(), .no_cache = no_cache } };
}
fn isHelpFlag(arg: []const u8) bool {

View file

@ -0,0 +1,165 @@
//! Cross-compilation support and validation for Roc CLI
//! Handles host detection, target validation, and capability matrix
const std = @import("std");
const builtin = @import("builtin");
const target_mod = @import("target.zig");
const RocTarget = target_mod.RocTarget;
/// Result of cross-compilation validation
pub const CrossCompilationResult = union(enum) {
supported: void,
unsupported_host_target: struct {
host: RocTarget,
reason: []const u8,
},
unsupported_cross_compilation: struct {
host: RocTarget,
target: RocTarget,
reason: []const u8,
},
missing_toolchain: struct {
host: RocTarget,
target: RocTarget,
required_tools: []const []const u8,
},
};
/// Cross-compilation capability matrix
pub const CrossCompilationMatrix = struct {
/// Targets that support static linking (musl) - these should work from any host
pub const musl_targets = [_]RocTarget{
.x64musl,
.arm64musl,
};
/// Targets that require dynamic linking (glibc) - more complex cross-compilation
pub const glibc_targets = [_]RocTarget{
.x64glibc,
.arm64glibc,
};
/// Windows targets - require MinGW or similar toolchain
pub const windows_targets = [_]RocTarget{
// Future: .x64windows, .arm64windows
};
/// macOS targets - require OSXCross or similar toolchain
pub const macos_targets = [_]RocTarget{
// Future: .x64macos, .arm64macos
};
};
/// Detect the host target platform
pub fn detectHostTarget() RocTarget {
return switch (builtin.target.cpu.arch) {
.x86_64 => switch (builtin.target.os.tag) {
.linux => .x64glibc, // Default to glibc on Linux hosts
.windows => .x64win,
.macos => .x64mac,
else => .x64glibc,
},
.aarch64 => switch (builtin.target.os.tag) {
.linux => .arm64glibc,
.windows => .arm64win,
.macos => .arm64mac,
else => .arm64glibc,
},
else => .x64glibc, // Fallback
};
}
/// Check if a target is supported for static linking (musl)
pub fn isMuslTarget(target: RocTarget) bool {
return switch (target) {
.x64musl, .arm64musl => true,
else => false,
};
}
/// Check if a target requires dynamic linking (glibc)
pub fn isGlibcTarget(target: RocTarget) bool {
return switch (target) {
.x64glibc, .arm64glibc => true,
else => false,
};
}
/// Validate cross-compilation from host to target
pub fn validateCrossCompilation(host: RocTarget, target: RocTarget) CrossCompilationResult {
// Native compilation (host == target) is always supported
if (host == target) {
return CrossCompilationResult{ .supported = {} };
}
// Support both musl and glibc targets for cross-compilation
if (isMuslTarget(target) or isGlibcTarget(target)) {
return CrossCompilationResult{ .supported = {} };
}
// Windows and macOS cross-compilation not yet supported
return CrossCompilationResult{
.unsupported_cross_compilation = .{
.host = host,
.target = target,
.reason = "Windows and macOS cross-compilation not yet implemented. Please use Linux targets (x64musl, arm64musl, x64glibc, arm64glibc) or log an issue at https://github.com/roc-lang/roc/issues",
},
};
}
/// Get host capabilities (what this host can cross-compile to)
pub fn getHostCapabilities(host: RocTarget) []const RocTarget {
_ = host; // For now, all hosts have the same capabilities
// Support both musl and glibc targets from any host
const all_targets = CrossCompilationMatrix.musl_targets ++ CrossCompilationMatrix.glibc_targets;
return &all_targets;
}
/// Print supported targets for the current host
pub fn printSupportedTargets(writer: anytype, host: RocTarget) !void {
const capabilities = getHostCapabilities(host);
try writer.print("Supported cross-compilation targets from {s}:\n", .{@tagName(host)});
for (capabilities) |target| {
try writer.print(" {s} ({s})\n", .{ @tagName(target), target.toTriple() });
}
try writer.print("\nUnsupported targets (not yet implemented):\n", .{});
const unsupported = [_][]const u8{
"x64windows, arm64windows (Windows cross-compilation)",
"x64macos, arm64macos (macOS cross-compilation)",
};
for (unsupported) |target_desc| {
try writer.print(" {s}\n", .{target_desc});
}
try writer.print("\nTo request support for additional targets, please log an issue at:\n", .{});
try writer.print("https://github.com/roc-lang/roc/issues\n", .{});
}
/// Print cross-compilation error with helpful context
pub fn printCrossCompilationError(writer: anytype, result: CrossCompilationResult) !void {
switch (result) {
.supported => {}, // No error
.unsupported_host_target => |info| {
try writer.print("Error: Unsupported host platform '{s}'\n", .{@tagName(info.host)});
try writer.print("Reason: {s}\n", .{info.reason});
},
.unsupported_cross_compilation => |info| {
try writer.print("Error: Cross-compilation from {s} to {s} is not supported\n", .{ @tagName(info.host), @tagName(info.target) });
try writer.print("Reason: {s}\n", .{info.reason});
try writer.print("\n", .{});
try printSupportedTargets(writer, info.host);
},
.missing_toolchain => |info| {
try writer.print("Error: Missing required toolchain for cross-compilation from {s} to {s}\n", .{ @tagName(info.host), @tagName(info.target) });
try writer.print("Required tools:\n", .{});
for (info.required_tools) |tool| {
try writer.print(" {s}\n", .{tool});
}
},
}
}

389
src/cli/libc_finder.zig Normal file
View file

@ -0,0 +1,389 @@
//! Finds libc and dynamic linker paths on Linux systems
//!
//! Only used when building natively (not cross-compiling for another target)
//!
//! TODO can we improve this or make it more reliable? this implementation probably
//! needs some work but it will be hard to know until we have more users testing roc
//! on different systems.
const std = @import("std");
const builtin = @import("builtin");
const fs = std.fs;
const process = std.process;
/// Information about the system's libc installation
pub const LibcInfo = struct {
/// Path to the dynamic linker (e.g., /lib64/ld-linux-x86-64.so.2)
dynamic_linker: []const u8,
/// Path to libc library (e.g., /lib/x86_64-linux-gnu/libc.so.6)
libc_path: []const u8,
/// Directory containing libc and CRT files
lib_dir: []const u8,
/// System architecture (e.g., "x86_64", "aarch64")
arch: []const u8,
/// Allocator used for all allocations
allocator: std.mem.Allocator,
pub fn deinit(self: *LibcInfo) void {
self.allocator.free(self.dynamic_linker);
self.allocator.free(self.libc_path);
self.allocator.free(self.lib_dir);
self.allocator.free(self.arch);
}
};
/// Validate that a path is safe (absolute and no traversal)
fn validatePath(path: []const u8) bool {
if (!fs.path.isAbsolute(path)) return false;
if (std.mem.indexOf(u8, path, "../") != null) return false;
return true;
}
/// Get the dynamic linker name for the given architecture
fn getDynamicLinkerName(arch: []const u8) []const u8 {
if (std.mem.eql(u8, arch, "x86_64")) {
return "ld-linux-x86-64.so.2";
} else if (std.mem.eql(u8, arch, "aarch64")) {
return "ld-linux-aarch64.so.1";
} else if (std.mem.startsWith(u8, arch, "arm")) {
return "ld-linux-armhf.so.3";
} else if (std.mem.eql(u8, arch, "i686") or std.mem.eql(u8, arch, "i386")) {
return "ld-linux.so.2";
} else {
return "ld-linux.so.2";
}
}
/// Main entry point - finds libc and dynamic linker
pub fn findLibc(allocator: std.mem.Allocator) !LibcInfo {
// Try compiler-based detection first (most reliable)
if (try findViaCompiler(allocator)) |info| {
return info;
}
// Fall back to filesystem search
return try findViaFilesystem(allocator);
}
/// Find libc using compiler queries (gcc/clang)
fn findViaCompiler(allocator: std.mem.Allocator) !?LibcInfo {
const compilers = [_][]const u8{ "gcc", "clang", "cc" };
// Get architecture first
const arch = try getArchitecture(allocator);
defer allocator.free(arch);
// Get the expected dynamic linker name for this architecture
const ld_name = getDynamicLinkerName(arch);
for (compilers) |compiler| {
// Try to get dynamic linker path from compiler
const ld_cmd = try std.fmt.allocPrint(allocator, "-print-file-name={s}", .{ld_name});
defer allocator.free(ld_cmd);
const ld_result = process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ compiler, ld_cmd },
}) catch continue;
defer allocator.free(ld_result.stdout);
defer allocator.free(ld_result.stderr);
// Try to get libc path from compiler
const libc_result = process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ compiler, "-print-file-name=libc.so" },
}) catch continue;
defer allocator.free(libc_result.stdout);
defer allocator.free(libc_result.stderr);
const libc_path = std.mem.trimRight(u8, libc_result.stdout, "\n\r \t");
if (libc_path.len == 0 or std.mem.eql(u8, libc_path, "libc.so")) continue;
// Validate path for security
if (!validatePath(libc_path)) continue;
// Verify the file exists and close it properly
const libc_file = fs.openFileAbsolute(libc_path, .{}) catch continue;
libc_file.close();
const lib_dir = fs.path.dirname(libc_path) orelse continue;
// Find dynamic linker
const dynamic_linker = try findDynamicLinker(allocator, arch, lib_dir) orelse continue;
defer allocator.free(dynamic_linker);
// Validate dynamic linker path
if (!validatePath(dynamic_linker)) continue;
return LibcInfo{
.dynamic_linker = try allocator.dupe(u8, dynamic_linker),
.libc_path = try allocator.dupe(u8, libc_path),
.lib_dir = try allocator.dupe(u8, lib_dir),
.arch = try allocator.dupe(u8, arch),
.allocator = allocator,
};
}
return null;
}
/// Find libc by searching the filesystem
fn findViaFilesystem(allocator: std.mem.Allocator) !LibcInfo {
// Get architecture and duplicate it for later use
const arch_temp = try getArchitecture(allocator);
defer allocator.free(arch_temp);
const arch = try allocator.dupe(u8, arch_temp);
errdefer allocator.free(arch);
const search_paths = try getSearchPaths(allocator, arch);
defer {
for (search_paths.items) |path| {
allocator.free(path);
}
search_paths.deinit();
}
// Search for libc in standard paths
for (search_paths.items) |lib_dir| {
var dir = fs.openDirAbsolute(lib_dir, .{}) catch continue;
defer dir.close();
// Support both glibc and musl
const libc_names = [_][]const u8{
"libc.so.6", // glibc
"libc.musl-x86_64.so.1", // musl x86_64
"libc.musl-aarch64.so.1", // musl aarch64
"libc.musl-arm.so.1", // musl arm
"libc.so",
"libc.a",
};
for (libc_names) |libc_name| {
const libc_path = try fs.path.join(allocator, &[_][]const u8{ lib_dir, libc_name });
defer allocator.free(libc_path);
// Check if file exists and close it properly
const libc_file = fs.openFileAbsolute(libc_path, .{}) catch continue;
libc_file.close();
// Try to find dynamic linker
const dynamic_linker = try findDynamicLinker(allocator, arch, lib_dir) orelse continue;
errdefer allocator.free(dynamic_linker);
// Validate paths for security
if (!validatePath(libc_path) or !validatePath(dynamic_linker)) {
allocator.free(dynamic_linker);
continue;
}
return LibcInfo{
.dynamic_linker = dynamic_linker,
.libc_path = try allocator.dupe(u8, libc_path),
.lib_dir = try allocator.dupe(u8, lib_dir),
.arch = arch, // Transfer ownership
.allocator = allocator,
};
}
}
allocator.free(arch);
return error.LibcNotFound;
}
/// Find the dynamic linker for the given architecture
fn findDynamicLinker(allocator: std.mem.Allocator, arch: []const u8, lib_dir: []const u8) !?[]const u8 {
// Map architecture to dynamic linker names (including musl)
const ld_names = if (std.mem.eql(u8, arch, "x86_64"))
&[_][]const u8{ "ld-linux-x86-64.so.2", "ld-musl-x86_64.so.1", "ld-linux.so.2" }
else if (std.mem.eql(u8, arch, "aarch64"))
&[_][]const u8{ "ld-linux-aarch64.so.1", "ld-musl-aarch64.so.1", "ld-linux.so.1" }
else if (std.mem.startsWith(u8, arch, "arm"))
&[_][]const u8{ "ld-linux-armhf.so.3", "ld-musl-arm.so.1", "ld-linux.so.3" }
else if (std.mem.eql(u8, arch, "i686") or std.mem.eql(u8, arch, "i386"))
&[_][]const u8{ "ld-linux.so.2", "ld-musl-i386.so.1" }
else
&[_][]const u8{ "ld-linux.so.2", "ld.so.1" };
// Search in the lib directory first
for (ld_names) |ld_name| {
const path = try fs.path.join(allocator, &[_][]const u8{ lib_dir, ld_name });
defer allocator.free(path);
if (fs.openFileAbsolute(path, .{})) |file| {
file.close();
return try allocator.dupe(u8, path);
} else |_| {}
}
// Search in common locations
const common_paths = if (std.mem.eql(u8, arch, "x86_64"))
&[_][]const u8{ "/lib64", "/lib/x86_64-linux-gnu", "/lib" }
else if (std.mem.eql(u8, arch, "aarch64"))
&[_][]const u8{ "/lib", "/lib/aarch64-linux-gnu", "/lib64" }
else if (std.mem.startsWith(u8, arch, "arm"))
&[_][]const u8{ "/lib", "/lib/arm-linux-gnueabihf", "/lib32" }
else
&[_][]const u8{ "/lib", "/lib32" };
for (common_paths) |search_dir| {
for (ld_names) |ld_name| {
const path = try fs.path.join(allocator, &[_][]const u8{ search_dir, ld_name });
defer allocator.free(path);
if (fs.openFileAbsolute(path, .{})) |file| {
file.close();
return try allocator.dupe(u8, path);
} else |_| {}
}
}
return null;
}
/// Get system architecture using uname
fn getArchitecture(allocator: std.mem.Allocator) ![]const u8 {
const result = try process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "uname", "-m" },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const arch = std.mem.trimRight(u8, result.stdout, "\n\r \t");
return allocator.dupe(u8, arch);
}
/// Get library search paths for the given architecture
fn getSearchPaths(allocator: std.mem.Allocator, arch: []const u8) !std.ArrayList([]const u8) {
var paths = std.ArrayList([]const u8).init(allocator);
errdefer {
for (paths.items) |path| {
allocator.free(path);
}
paths.deinit();
}
// Get multiarch triplet if possible
const triplet = try getMultiarchTriplet(allocator, arch);
defer allocator.free(triplet);
// Add multiarch paths
try paths.append(try std.fmt.allocPrint(allocator, "/lib/{s}", .{triplet}));
try paths.append(try std.fmt.allocPrint(allocator, "/usr/lib/{s}", .{triplet}));
// Add architecture-specific paths
if (std.mem.eql(u8, arch, "x86_64")) {
try paths.append(try allocator.dupe(u8, "/lib64"));
try paths.append(try allocator.dupe(u8, "/usr/lib64"));
try paths.append(try allocator.dupe(u8, "/lib/x86_64-linux-gnu"));
try paths.append(try allocator.dupe(u8, "/usr/lib/x86_64-linux-gnu"));
} else if (std.mem.eql(u8, arch, "aarch64")) {
try paths.append(try allocator.dupe(u8, "/lib64"));
try paths.append(try allocator.dupe(u8, "/usr/lib64"));
try paths.append(try allocator.dupe(u8, "/lib/aarch64-linux-gnu"));
try paths.append(try allocator.dupe(u8, "/usr/lib/aarch64-linux-gnu"));
} else if (std.mem.startsWith(u8, arch, "arm")) {
try paths.append(try allocator.dupe(u8, "/lib32"));
try paths.append(try allocator.dupe(u8, "/usr/lib32"));
try paths.append(try allocator.dupe(u8, "/lib/arm-linux-gnueabihf"));
try paths.append(try allocator.dupe(u8, "/usr/lib/arm-linux-gnueabihf"));
}
// Add generic paths
try paths.append(try allocator.dupe(u8, "/lib"));
try paths.append(try allocator.dupe(u8, "/usr/lib"));
try paths.append(try allocator.dupe(u8, "/usr/local/lib"));
// Add musl-specific paths
try paths.append(try allocator.dupe(u8, "/lib/musl"));
try paths.append(try allocator.dupe(u8, "/usr/lib/musl"));
return paths;
}
/// Get multiarch triplet (e.g., x86_64-linux-gnu)
fn getMultiarchTriplet(allocator: std.mem.Allocator, arch: []const u8) ![]const u8 {
// Try to get from gcc first
const result = process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "gcc", "-dumpmachine" },
}) catch |err| switch (err) {
error.FileNotFound => {
// Fallback to common triplets
if (std.mem.eql(u8, arch, "x86_64")) {
return allocator.dupe(u8, "x86_64-linux-gnu");
} else if (std.mem.eql(u8, arch, "aarch64")) {
return allocator.dupe(u8, "aarch64-linux-gnu");
} else if (std.mem.startsWith(u8, arch, "arm")) {
return allocator.dupe(u8, "arm-linux-gnueabihf");
} else {
return allocator.dupe(u8, arch);
}
},
else => return err,
};
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const triplet = std.mem.trimRight(u8, result.stdout, "\n\r \t");
return allocator.dupe(u8, triplet);
}
/// Find a file in a directory
fn findFile(allocator: std.mem.Allocator, dir_path: []const u8, filename: []const u8) !?[]const u8 {
const full_path = try fs.path.join(allocator, &[_][]const u8{ dir_path, filename });
defer allocator.free(full_path);
if (fs.openFileAbsolute(full_path, .{})) |file| {
file.close();
return try allocator.dupe(u8, full_path);
} else |_| {
return null;
}
}
test "libc detection integration test" {
if (builtin.os.tag != .linux) return error.SkipZigTest;
const allocator = std.testing.allocator;
const libc_info = findLibc(allocator) catch |err| switch (err) {
error.LibcNotFound => {
std.log.warn("Libc not found on this system - this may be expected in some environments", .{});
return error.SkipZigTest;
},
else => return err,
};
defer {
var info = libc_info;
info.deinit();
}
// Verify we got valid information
try std.testing.expect(libc_info.arch.len > 0);
try std.testing.expect(libc_info.dynamic_linker.len > 0);
try std.testing.expect(libc_info.libc_path.len > 0);
try std.testing.expect(libc_info.lib_dir.len > 0);
// Verify paths are valid
try std.testing.expect(validatePath(libc_info.dynamic_linker));
try std.testing.expect(validatePath(libc_info.libc_path));
// Verify the dynamic linker file exists and is accessible
const ld_file = fs.openFileAbsolute(libc_info.dynamic_linker, .{}) catch |err| {
std.log.err("Dynamic linker not accessible at {s}: {}", .{ libc_info.dynamic_linker, err });
return err;
};
ld_file.close();
// Verify the libc file exists and is accessible
const libc_file = fs.openFileAbsolute(libc_info.libc_path, .{}) catch |err| {
std.log.err("Libc not accessible at {s}: {}", .{ libc_info.libc_path, err });
return err;
};
libc_file.close();
}

View file

@ -5,6 +5,7 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const libc_finder = @import("libc_finder.zig");
/// External C functions from zig_llvm.cpp - only available when LLVM is enabled
const llvm_available = if (@import("builtin").is_test) false else @import("config").llvm;
@ -33,6 +34,33 @@ pub const TargetFormat = enum {
else => .elf,
};
}
/// Detect target format from OS tag
pub fn detectFromOs(os: std.Target.Os.Tag) TargetFormat {
return switch (os) {
.windows => .coff,
.macos, .ios, .watchos, .tvos => .macho,
.wasi => .wasm,
else => .elf,
};
}
};
/// Target ABI for runtime-configurable linking
pub const TargetAbi = enum {
musl,
gnu,
/// Convert from RocTarget to TargetAbi
pub fn fromRocTarget(roc_target: anytype) TargetAbi {
// Use string matching to avoid circular imports
const target_str = @tagName(roc_target);
if (std.mem.endsWith(u8, target_str, "musl")) {
return .musl;
} else {
return .gnu;
}
}
};
/// Configuration for linking operation
@ -40,12 +68,27 @@ pub const LinkConfig = struct {
/// Target format to use for linking
target_format: TargetFormat = TargetFormat.detectFromSystem(),
/// Target ABI - determines static vs dynamic linking strategy
target_abi: ?TargetAbi = null, // null means detect from system
/// Target OS tag - for cross-compilation support
target_os: ?std.Target.Os.Tag = null, // null means detect from system
/// Target CPU architecture - for cross-compilation support
target_arch: ?std.Target.Cpu.Arch = null, // null means detect from system
/// Output executable path
output_path: []const u8,
/// Input object files to link
object_files: []const []const u8,
/// Platform-provided files to link before object files (e.g., Scrt1.o, crti.o, host.o)
platform_files_pre: []const []const u8 = &.{},
/// Platform-provided files to link after object files (e.g., crtn.o)
platform_files_post: []const []const u8 = &.{},
/// Additional linker flags
extra_args: []const []const u8 = &.{},
@ -75,7 +118,11 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void {
defer args.deinit();
// Add platform-specific linker name and arguments
switch (builtin.target.os.tag) {
// Use target OS if provided, otherwise fall back to host OS
const target_os = config.target_os orelse builtin.target.os.tag;
const target_arch = config.target_arch orelse builtin.target.cpu.arch;
switch (target_os) {
.macos => {
// Add linker name for macOS
try args.append("ld64.lld");
@ -89,7 +136,7 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void {
// Add architecture flag
try args.append("-arch");
switch (builtin.target.cpu.arch) {
switch (target_arch) {
.aarch64 => try args.append("arm64"),
.x86_64 => try args.append("x86_64"),
else => try args.append("arm64"), // default to arm64
@ -104,6 +151,9 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void {
// Add SDK path
try args.append("-syslibroot");
try args.append("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk");
// Link against system libraries on macOS
try args.append("-lSystem");
},
.linux => {
// Add linker name for Linux
@ -113,12 +163,58 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void {
try args.append("-o");
try args.append(config.output_path);
// Suppress LLD warnings
// Prevent hidden linker behaviour -- only explicit platfor mdependencies
try args.append("-nostdlib");
// Remove unused sections to reduce binary size
try args.append("--gc-sections");
// TODO make the confirugable instead of using comments
// Suppress linker warnings
try args.append("-w");
// Verbose linker for debugging (uncomment as needed)
// try args.append("--verbose");
// try args.append("--print-map");
// try args.append("--error-limit=0");
// Use static linking to avoid dynamic linker dependency issues
// Determine target ABI
const target_abi = config.target_abi orelse if (builtin.target.abi == .musl) TargetAbi.musl else TargetAbi.gnu;
switch (target_abi) {
.musl => {
// Static musl linking
try args.append("-static");
},
.gnu => {
// Dynamic GNU linking - dynamic linker path is handled by caller
// for cross-compilation. Only detect locally for native builds
if (config.extra_args.len == 0) {
// Native build - try to detect dynamic linker
if (libc_finder.findLibc(allocator)) |libc_info| {
// We need to copy the path since args holds references
const dynamic_linker = try allocator.dupe(u8, libc_info.dynamic_linker);
// Clean up libc_info after copying what we need
var info = libc_info;
info.deinit();
try args.append("-dynamic-linker");
try args.append(dynamic_linker);
} else |err| {
// Fallback to hardcoded path based on architecture
std.log.warn("Failed to detect libc: {}, using fallback", .{err});
try args.append("-dynamic-linker");
const fallback_ld = switch (builtin.target.cpu.arch) {
.x86_64 => "/lib64/ld-linux-x86-64.so.2",
.aarch64 => "/lib/ld-linux-aarch64.so.1",
.x86 => "/lib/ld-linux.so.2",
else => "/lib/ld-linux.so.2",
};
try args.append(fallback_ld);
}
}
// Otherwise, dynamic linker is set via extra_args from caller
},
}
},
.windows => {
// Add linker name for Windows COFF
try args.append("lld-link");
@ -131,7 +227,7 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void {
try args.append("/subsystem:console");
// Add machine type based on target architecture
switch (builtin.target.cpu.arch) {
switch (target_arch) {
.x86_64 => try args.append("/machine:x64"),
.x86 => try args.append("/machine:x86"),
.aarch64 => try args.append("/machine:arm64"),
@ -160,16 +256,32 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void {
},
}
// Add platform-provided files that come before object files
for (config.platform_files_pre) |platform_file| {
try args.append(platform_file);
}
// Add object files
for (config.object_files) |obj_file| {
try args.append(obj_file);
}
// Add platform-provided files that come after object files
for (config.platform_files_post) |platform_file| {
try args.append(platform_file);
}
// Add any extra arguments
for (config.extra_args) |extra_arg| {
try args.append(extra_arg);
}
// Debug: Print the linker command
std.log.debug("Linker command:", .{});
for (args.items) |arg| {
std.log.debug(" {s}", .{arg});
}
// Convert to null-terminated strings for C API
var c_args = allocator.alloc([*:0]const u8, args.items.len) catch return LinkError.OutOfMemory;
defer allocator.free(c_args);
@ -253,6 +365,8 @@ test "link config creation" {
try std.testing.expect(config.target_format == TargetFormat.detectFromSystem());
try std.testing.expectEqualStrings("test_output", config.output_path);
try std.testing.expectEqual(@as(usize, 2), config.object_files.len);
try std.testing.expectEqual(@as(usize, 0), config.platform_files_pre.len);
try std.testing.expectEqual(@as(usize, 0), config.platform_files_post.len);
}
test "target format detection" {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,164 @@
//! Helpers for using Zig's LLVM Builder API to generate a shim library for the
//! Roc interpreter that translates from the platform host API.
const std = @import("std");
const Builder = std.zig.llvm.Builder;
const WipFunction = Builder.WipFunction;
const builtin = @import("builtin");
/// Represents a single entrypoint that a Roc platform host expects to call.
/// Each entrypoint corresponds to a specific function the host can invoke,
/// such as "init", "render", "update", etc.
pub const EntryPoint = struct {
/// The name of the entrypoint function (without the "roc__" prefix).
/// This will be used to generate the exported function name.
/// For example, "init" becomes "roc__init".
name: []const u8,
/// The unique index for this entrypoint that gets passed to roc_entrypoint.
/// This allows the Roc runtime to dispatch to the correct implementation
/// based on which exported function was called by the host.
idx: u32,
};
/// Adds the extern declaration for `roc_entrypoint` to the LLVM module.
///
/// This function creates the declaration for the single entry point that all
/// Roc platform functions will delegate to. The Roc interpreter provides
/// the actual implementation of this function, which acts as a dispatcher
/// based on the entry_idx parameter.
fn addRocEntrypoint(builder: *Builder) !Builder.Function.Index {
// Create pointer type for generic pointers (i8* in LLVM)
const ptr_type = try builder.ptrType(.default);
// Create the roc_entrypoint function type:
// void roc_entrypoint(u32 entry_idx, RocOps* ops, void* ret_ptr, void* arg_ptr)
const entrypoint_params = [_]Builder.Type{ .i32, ptr_type, ptr_type, ptr_type };
const entrypoint_type = try builder.fnType(.void, &entrypoint_params, .normal);
// Create function name with platform-specific prefix
const base_name = "roc_entrypoint";
const fn_name_str = if (builtin.target.os.tag == .macos)
try std.fmt.allocPrint(builder.gpa, "_{s}", .{base_name})
else
try builder.gpa.dupe(u8, base_name);
defer builder.gpa.free(fn_name_str);
const fn_name = try builder.strtabString(fn_name_str);
// Add the extern function declaration (no body)
const entrypoint_fn = try builder.addFunction(entrypoint_type, fn_name, .default);
entrypoint_fn.setLinkage(.external, builder);
return entrypoint_fn;
}
/// Generates a single exported platform function that delegates to roc_entrypoint.
///
/// This creates the "glue" functions that a Roc platform host expects to find when
/// linking against a Roc application. Each generated function follows the exact
/// Roc Host ABI specification and simply forwards the call to the interpreter's `roc_entrypoint`
/// with the appropriate index.
///
/// For example, if name="render" and entry_idx=1, this generates:
/// ```llvm
/// define void @roc__render(ptr %ops, ptr %ret_ptr, ptr %arg_ptr) {
/// call void @roc_entrypoint(i32 1, ptr %ops, ptr %ret_ptr, ptr %arg_ptr)
/// ret void
/// }
/// ```
///
/// This delegation pattern allows:
/// 1. The host to call specific named functions (roc__init, roc__render, etc.)
/// 2. The pre-built Roc interpreter to handle all calls through a single dispatch mechanism
/// 3. Efficient code generation since each wrapper is just a simple function call
/// 4. Easy addition/removal of platform functions without changing the pre-built interpreter binary which is embedded in the roc cli executable.
fn addRocExportedFunction(builder: *Builder, entrypoint_fn: Builder.Function.Index, name: []const u8, entry_idx: u32) !Builder.Function.Index {
// Create pointer type for generic pointers
const ptr_type = try builder.ptrType(.default);
// Create the Roc function type following the ABI:
// void roc_function(RocOps* ops, void* ret_ptr, void* arg_ptr)
const roc_fn_params = [_]Builder.Type{ ptr_type, ptr_type, ptr_type };
const roc_fn_type = try builder.fnType(.void, &roc_fn_params, .normal);
// Create function name with roc__ prefix and platform-specific prefix
const base_name = try std.fmt.allocPrint(builder.gpa, "roc__{s}", .{name});
defer builder.gpa.free(base_name);
const full_name = if (builtin.target.os.tag == .macos)
try std.fmt.allocPrint(builder.gpa, "_{s}", .{base_name})
else
try builder.gpa.dupe(u8, base_name);
defer builder.gpa.free(full_name);
const fn_name = try builder.strtabString(full_name);
// Add the function to the module with external linkage
const roc_fn = try builder.addFunction(roc_fn_type, fn_name, .default);
roc_fn.setLinkage(.external, builder);
// Create a work-in-progress function to add instructions
var wip = try WipFunction.init(builder, .{
.function = roc_fn,
.strip = false,
});
defer wip.deinit();
// Create the entry basic block
const entry_block = try wip.block(0, "entry");
wip.cursor = .{ .block = entry_block };
// Get the function parameters
const ops_ptr = wip.arg(0); // RocOps pointer
const ret_ptr = wip.arg(1); // Return value pointer
const arg_ptr = wip.arg(2); // Arguments pointer
// Create constant for entry_idx
const idx_const = try builder.intConst(.i32, entry_idx);
// Call roc_entrypoint(entry_idx, ops, ret_ptr, arg_ptr)
const call_args = [_]Builder.Value{ idx_const.toValue(), ops_ptr, ret_ptr, arg_ptr };
_ = try wip.call(.normal, .ccc, .none, entrypoint_fn.typeOf(builder), entrypoint_fn.toValue(builder), &call_args, "");
// Return void
_ = try wip.retVoid();
// Finish building the function
try wip.finish();
return roc_fn;
}
/// Creates a complete Roc platform library with all necessary entrypoints.
///
/// This generates a shim that translates between the pre-built roc interpreter
/// which has a single `roc_entrypoint`, and the API defined by the platform with the
/// specific entrypoints the host expects to link with.
///
/// The generated library structure follows this pattern:
/// ```llvm
/// ; External function that provided by the pre-built roc interpreter
/// declare void @roc_entrypoint(i32 %entry_idx, ptr %ops, ptr %ret_ptr, ptr %arg_ptr)
///
/// ; Platform functions that the host expects to be linked with
/// define void @roc__init(ptr %ops, ptr %ret_ptr, ptr %arg_ptr) {
/// call void @roc_entrypoint(i32 0, ptr %ops, ptr %ret_ptr, ptr %arg_ptr)
/// ret void
/// }
///
/// define void @roc__render(ptr %ops, ptr %ret_ptr, ptr %arg_ptr) {
/// call void @roc_entrypoint(i32 1, ptr %ops, ptr %ret_ptr, ptr %arg_ptr)
/// ret void
/// }
/// ; ... etc for each entrypoint
/// ```
///
/// The generated library is then compiled using LLVM to an object file and linked with
/// both the host and the Roc interpreter to create a dev build executable.
pub fn createInterpreterShim(builder: *Builder, entrypoints: []const EntryPoint) !void {
// Add the extern roc_entrypoint declaration
const entrypoint_fn = try addRocEntrypoint(builder);
// Add each exported entrypoint function
for (entrypoints) |entry| {
_ = try addRocExportedFunction(builder, entrypoint_fn, entry.name, entry.idx);
}
}

288
src/cli/target.zig Normal file
View file

@ -0,0 +1,288 @@
//! Roc target definitions and system library path resolution
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
/// Roc's simplified targets
pub const RocTarget = enum {
// x64 (x86_64) targets
x64mac,
x64win,
x64freebsd,
x64openbsd,
x64netbsd,
x64musl,
x64glibc,
x64linux,
x64elf,
// arm64 (aarch64) targets
arm64mac,
arm64win,
arm64linux,
arm64musl,
arm64glibc,
// arm32 targets
arm32linux,
arm32musl,
// WebAssembly
wasm32,
/// Parse target from string
pub fn fromString(str: []const u8) ?RocTarget {
const enum_info = @typeInfo(RocTarget);
inline for (enum_info.@"enum".fields) |field| {
if (std.mem.eql(u8, str, field.name)) {
return @enumFromInt(field.value);
}
}
return null;
}
/// Get the OS tag for this RocTarget
pub fn toOsTag(self: RocTarget) std.Target.Os.Tag {
return switch (self) {
// x64 targets
.x64mac, .arm64mac => .macos,
.x64win, .arm64win => .windows,
.x64freebsd => .freebsd,
.x64openbsd => .openbsd,
.x64netbsd => .netbsd,
.x64musl, .x64glibc, .x64linux, .x64elf, .arm64musl, .arm64glibc, .arm64linux, .arm32musl, .arm32linux => .linux,
.wasm32 => .wasi,
};
}
/// Get the CPU architecture for this RocTarget
pub fn toCpuArch(self: RocTarget) std.Target.Cpu.Arch {
return switch (self) {
// x64 targets
.x64mac, .x64win, .x64freebsd, .x64openbsd, .x64netbsd, .x64musl, .x64glibc, .x64linux, .x64elf => .x86_64,
// arm64 targets
.arm64mac, .arm64win, .arm64linux, .arm64musl, .arm64glibc => .aarch64,
// arm32 targets
.arm32linux, .arm32musl => .arm,
// WebAssembly
.wasm32 => .wasm32,
};
}
/// Convert Roc target to LLVM target triple
pub fn toTriple(self: RocTarget) []const u8 {
return switch (self) {
// x64 targets
.x64mac => "x86_64-apple-darwin",
.x64win => "x86_64-pc-windows-msvc",
.x64freebsd => "x86_64-unknown-freebsd",
.x64openbsd => "x86_64-unknown-openbsd",
.x64netbsd => "x86_64-unknown-netbsd",
.x64musl => "x86_64-unknown-linux-musl",
.x64glibc => "x86_64-unknown-linux-gnu",
.x64linux => "x86_64-unknown-linux-gnu",
.x64elf => "x86_64-unknown-none-elf",
// arm64 targets
.arm64mac => "aarch64-apple-darwin",
.arm64win => "aarch64-pc-windows-msvc",
.arm64linux => "aarch64-unknown-linux-gnu",
.arm64musl => "aarch64-unknown-linux-musl",
.arm64glibc => "aarch64-unknown-linux-gnu",
// arm32 targets
.arm32linux => "arm-unknown-linux-gnueabihf",
.arm32musl => "arm-unknown-linux-musleabihf",
// WebAssembly
.wasm32 => "wasm32-unknown-unknown",
};
}
/// Detect the current system's Roc target
pub fn detectNative() RocTarget {
const os = builtin.target.os.tag;
const arch = builtin.target.cpu.arch;
const abi = builtin.target.abi;
// Handle architecture first
switch (arch) {
.x86_64 => {
switch (os) {
.macos => return .x64mac,
.windows => return .x64win,
.freebsd => return .x64freebsd,
.openbsd => return .x64openbsd,
.netbsd => return .x64netbsd,
.linux => {
// Check ABI to determine musl vs glibc
return switch (abi) {
.musl, .musleabi, .musleabihf => .x64musl,
.gnu, .gnueabi, .gnueabihf, .gnux32 => .x64glibc,
else => .x64musl, // Default to musl for static linking
};
},
else => return .x64elf, // Generic fallback
}
},
.aarch64, .aarch64_be => {
switch (os) {
.macos => return .arm64mac,
.windows => return .arm64win,
.linux => {
// Check ABI to determine musl vs glibc
return switch (abi) {
.musl, .musleabi, .musleabihf => .arm64musl,
.gnu, .gnueabi, .gnueabihf => .arm64glibc,
else => .arm64musl, // Default to musl for static linking
};
},
else => return .arm64linux, // Generic ARM64 Linux
}
},
.arm => {
switch (os) {
.linux => {
// Default to musl for static linking
return .arm32musl;
},
else => return .arm32linux, // Generic ARM32 Linux
}
},
.wasm32 => return .wasm32,
else => {
// Default fallback based on OS
switch (os) {
.macos => return .x64mac,
.windows => return .x64win,
.linux => return .x64musl, // Default to musl
else => return .x64elf,
}
},
}
}
/// Check if target uses dynamic linking (glibc targets)
pub fn isDynamic(self: RocTarget) bool {
return switch (self) {
.x64glibc, .arm64glibc, .x64linux, .arm64linux, .arm32linux => true,
else => false,
};
}
/// Check if target uses static linking (musl targets)
pub fn isStatic(self: RocTarget) bool {
return switch (self) {
.x64musl, .arm64musl, .arm32musl => true,
else => false,
};
}
/// Check if target is macOS
pub fn isMacOS(self: RocTarget) bool {
return switch (self) {
.x64mac, .arm64mac => true,
else => false,
};
}
/// Check if target is Windows
pub fn isWindows(self: RocTarget) bool {
return switch (self) {
.x64win, .arm64win => true,
else => false,
};
}
/// Check if target is Linux-based
pub fn isLinux(self: RocTarget) bool {
return switch (self) {
.x64musl, .x64glibc, .x64linux, .arm64musl, .arm64glibc, .arm64linux, .arm32musl, .arm32linux => true,
else => false,
};
}
/// Get the dynamic linker path for this target
pub fn getDynamicLinkerPath(self: RocTarget) ![]const u8 {
return switch (self) {
// x64 glibc targets
.x64glibc, .x64linux => "/lib64/ld-linux-x86-64.so.2",
// arm64 glibc targets
.arm64glibc, .arm64linux => "/lib/ld-linux-aarch64.so.1",
// arm32 glibc targets
.arm32linux => "/lib/ld-linux-armhf.so.3",
// Static linking targets don't need dynamic linker
.x64musl, .arm64musl, .arm32musl => return error.StaticLinkingTarget,
// macOS uses dyld
.x64mac, .arm64mac => "/usr/lib/dyld",
// Windows doesn't use ELF-style dynamic linker
.x64win, .arm64win => return error.WindowsTarget,
// BSD variants
.x64freebsd => "/libexec/ld-elf.so.1",
.x64openbsd => "/usr/libexec/ld.so",
.x64netbsd => "/usr/libexec/ld.elf_so",
// Generic ELF doesn't have a specific linker
.x64elf => return error.NoKnownLinkerPath,
// WebAssembly doesn't use dynamic linker
.wasm32 => return error.WebAssemblyTarget,
};
}
};
/// CRT (C runtime) file paths for linking
pub const CRTFiles = struct {
crt1_o: ?[]const u8 = null, // crt1.o or Scrt1.o (for PIE)
crti_o: ?[]const u8 = null, // crti.o
crtn_o: ?[]const u8 = null, // crtn.o
libc_a: ?[]const u8 = null, // libc.a (for static linking)
};
/// Get vendored CRT object files for a platform target
/// All CRT files must be provided by the platform in its targets/ directory
pub fn getVendoredCRTFiles(allocator: Allocator, target: RocTarget, platform_dir: []const u8) !CRTFiles {
// macOS and Windows targets don't need vendored CRT files - they use system libraries
if (target.isMacOS() or target.isWindows()) {
return CRTFiles{}; // Return empty CRTFiles struct
}
// Build path to the vendored CRT files
const target_subdir = switch (target) {
.x64musl => "x64musl",
.x64glibc => "x64glibc",
.arm64musl => "arm64musl",
.arm64glibc => "arm64glibc",
.arm32musl => "arm32musl",
.arm32linux => "arm32glibc",
else => return error.UnsupportedTargetForPlatform,
};
const targets_dir = try std.fs.path.join(allocator, &[_][]const u8{ platform_dir, "targets", target_subdir });
var result = CRTFiles{};
if (target.isStatic()) {
// For musl static linking
result.crt1_o = try std.fs.path.join(allocator, &[_][]const u8{ targets_dir, "crt1.o" });
result.libc_a = try std.fs.path.join(allocator, &[_][]const u8{ targets_dir, "libc.a" });
} else {
// For glibc dynamic linking
result.crt1_o = try std.fs.path.join(allocator, &[_][]const u8{ targets_dir, "Scrt1.o" });
result.crti_o = try std.fs.path.join(allocator, &[_][]const u8{ targets_dir, "crti.o" });
result.crtn_o = try std.fs.path.join(allocator, &[_][]const u8{ targets_dir, "crtn.o" });
}
return result;
}

View file

@ -31,7 +31,7 @@ test "platform resolution - basic cli platform" {
defer allocator.free(roc_path);
// This should return NoPlatformFound since we don't have the actual CLI platform installed
const result = main.resolvePlatformHost(allocator, roc_path);
const result = main.resolvePlatformPaths(allocator, roc_path);
try testing.expectError(error.NoPlatformFound, result);
}
@ -56,7 +56,7 @@ test "platform resolution - no platform in file" {
const roc_path = try temp_dir.dir.realpathAlloc(allocator, "test.roc");
defer allocator.free(roc_path);
const result = main.resolvePlatformHost(allocator, roc_path);
const result = main.resolvePlatformPaths(allocator, roc_path);
try testing.expectError(error.NoPlatformFound, result);
}
@ -65,7 +65,7 @@ test "platform resolution - file not found" {
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const result = main.resolvePlatformHost(allocator, "nonexistent.roc");
const result = main.resolvePlatformPaths(allocator, "nonexistent.roc");
try testing.expectError(error.NoPlatformFound, result);
}
@ -91,7 +91,7 @@ test "platform resolution - URL platform not supported" {
const roc_path = try temp_dir.dir.realpathAlloc(allocator, "test.roc");
defer allocator.free(roc_path);
const result = main.resolvePlatformHost(allocator, roc_path);
const result = main.resolvePlatformPaths(allocator, roc_path);
try testing.expectError(error.PlatformNotSupported, result);
}
@ -142,7 +142,7 @@ test "integration - shared memory setup and parsing" {
try testing.expect(shm_handle.size > 0);
try testing.expect(@intFromPtr(shm_handle.ptr) != 0);
std.log.info("Integration test: Successfully set up shared memory with size: {} bytes\n", .{shm_handle.size});
std.log.debug("Integration test: Successfully set up shared memory with size: {} bytes\n", .{shm_handle.size});
}
test "integration - compilation pipeline for different expressions" {
@ -198,7 +198,7 @@ test "integration - compilation pipeline for different expressions" {
// Verify shared memory was set up successfully
try testing.expect(shm_handle.size > 0);
std.log.info("Successfully compiled expression: '{s}' (shared memory size: {} bytes)\n", .{ roc_content, shm_handle.size });
std.log.debug("Successfully compiled expression: '{s}' (shared memory size: {} bytes)\n", .{ roc_content, shm_handle.size });
}
}
@ -244,8 +244,8 @@ test "integration - error handling in compilation" {
_ = posix.close(shm_handle.fd);
}
}
std.log.info("Compilation succeeded even with invalid syntax (size: {} bytes)\n", .{shm_handle.size});
std.log.debug("Compilation succeeded even with invalid syntax (size: {} bytes)\n", .{shm_handle.size});
} else |err| {
std.log.info("Compilation failed as expected with error: {}\n", .{err});
std.log.debug("Compilation failed as expected with error: {}\n", .{err});
}
}

View file

@ -87,6 +87,7 @@ test "ModuleEnv.Serialized roundtrip" {
.types = deserialized_ptr.types.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))).*,
.all_defs = deserialized_ptr.all_defs,
.all_statements = deserialized_ptr.all_statements,
.exports = deserialized_ptr.exports,
.external_decls = deserialized_ptr.external_decls.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))).*,
.imports = deserialized_ptr.imports.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), deser_alloc).*,
.module_name = "TestModule",

View file

@ -7,6 +7,7 @@ pub const Stack = @import("stack.zig").Stack;
pub const StackOverflow = @import("stack.zig").StackOverflow;
pub const StackValue = @import("StackValue.zig");
pub const EvalError = @import("interpreter.zig").EvalError;
pub const TestRunner = @import("test_runner.zig").TestRunner;
test "eval tests" {
std.testing.refAllDecls(@This());

238
src/eval/test_runner.zig Normal file
View file

@ -0,0 +1,238 @@
//! Runs expect expressions
//!
//! This module is a wrapper around the interpreter used to simplify evaluating expect expressions.
const std = @import("std");
const base = @import("base");
const builtins = @import("builtins");
const can = @import("can");
const stack = @import("stack.zig");
const layout = @import("layout");
const types = @import("types");
const Interpreter = @import("interpreter.zig").Interpreter;
const EvalError = @import("interpreter.zig").EvalError;
const RocOps = builtins.host_abi.RocOps;
const RocAlloc = builtins.host_abi.RocAlloc;
const RocDealloc = builtins.host_abi.RocDealloc;
const RocRealloc = builtins.host_abi.RocRealloc;
const RocDbg = builtins.host_abi.RocDbg;
const RocExpectFailed = builtins.host_abi.RocExpectFailed;
const RocCrashed = builtins.host_abi.RocCrashed;
const ModuleEnv = can.ModuleEnv;
const Allocator = std.mem.Allocator;
const LayoutStore = layout.Store;
const TypeStore = types.store.Store;
const CIR = can.CIR;
fn testRocAlloc(alloc_args: *RocAlloc, env: *anyopaque) callconv(.C) void {
const test_env: *TestRunner = @ptrCast(@alignCast(env));
const align_enum = std.mem.Alignment.fromByteUnits(@as(usize, @intCast(alloc_args.alignment)));
const size_storage_bytes = @max(alloc_args.alignment, @alignOf(usize));
const total_size = alloc_args.length + size_storage_bytes;
const result = test_env.allocator.rawAlloc(total_size, align_enum, @returnAddress());
const base_ptr = result orelse {
std.debug.panic("Out of memory during testRocAlloc", .{});
};
const size_ptr: *usize = @ptrFromInt(@intFromPtr(base_ptr) + size_storage_bytes - @sizeOf(usize));
size_ptr.* = total_size;
alloc_args.answer = @ptrFromInt(@intFromPtr(base_ptr) + size_storage_bytes);
}
fn testRocDealloc(dealloc_args: *RocDealloc, env: *anyopaque) callconv(.C) void {
const test_env: *TestRunner = @ptrCast(@alignCast(env));
const size_storage_bytes = @max(dealloc_args.alignment, @alignOf(usize));
const size_ptr: *const usize = @ptrFromInt(@intFromPtr(dealloc_args.ptr) - @sizeOf(usize));
const total_size = size_ptr.*;
const base_ptr: [*]u8 = @ptrFromInt(@intFromPtr(dealloc_args.ptr) - size_storage_bytes);
const log2_align = std.math.log2_int(u32, @intCast(dealloc_args.alignment));
const align_enum: std.mem.Alignment = @enumFromInt(log2_align);
const slice = @as([*]u8, @ptrCast(base_ptr))[0..total_size];
test_env.allocator.rawFree(slice, align_enum, @returnAddress());
}
fn testRocRealloc(realloc_args: *RocRealloc, env: *anyopaque) callconv(.C) void {
const test_env: *TestRunner = @ptrCast(@alignCast(env));
const size_storage_bytes = @max(realloc_args.alignment, @alignOf(usize));
const old_size_ptr: *const usize = @ptrFromInt(@intFromPtr(realloc_args.answer) - @sizeOf(usize));
const old_total_size = old_size_ptr.*;
const old_base_ptr: [*]u8 = @ptrFromInt(@intFromPtr(realloc_args.answer) - size_storage_bytes);
const new_total_size = realloc_args.new_length + size_storage_bytes;
const old_slice = @as([*]u8, @ptrCast(old_base_ptr))[0..old_total_size];
const new_slice = test_env.allocator.realloc(old_slice, new_total_size) catch {
std.debug.panic("Out of memory during testRocRealloc", .{});
};
const new_size_ptr: *usize = @ptrFromInt(@intFromPtr(new_slice.ptr) + size_storage_bytes - @sizeOf(usize));
new_size_ptr.* = new_total_size;
realloc_args.answer = @ptrFromInt(@intFromPtr(new_slice.ptr) + size_storage_bytes);
}
fn testRocDbg(dbg_args: *const RocDbg, env: *anyopaque) callconv(.C) void {
_ = dbg_args;
_ = env;
@panic("testRocDbg not implemented yet");
}
fn testRocExpectFailed(expect_args: *const RocExpectFailed, env: *anyopaque) callconv(.C) void {
_ = expect_args;
_ = env;
@panic("testRocExpectFailed not implemented yet");
}
fn testRocCrashed(crashed_args: *const RocCrashed, env: *anyopaque) callconv(.C) void {
const test_env: *TestRunner = @ptrCast(@alignCast(env));
const msg_slice = crashed_args.utf8_bytes[0..crashed_args.len];
test_env.interpreter.has_crashed = true;
const owned_msg = test_env.allocator.dupe(u8, msg_slice) catch {
test_env.interpreter.crash_message = "Failed to store crash message";
return;
};
test_env.interpreter.crash_message = owned_msg;
}
const Evaluation = enum {
passed,
failed,
not_a_bool,
};
// Track test results
const TestResult = struct {
passed: bool,
region: base.Region,
error_msg: ?[]const u8 = null,
};
const TestSummary = struct {
passed: u32,
failed: u32,
};
/// A test runner that can evaluate expect expressions in a module.
pub const TestRunner = struct {
allocator: Allocator,
env: *const ModuleEnv,
interpreter: Interpreter,
roc_ops: ?RocOps,
test_results: std.ArrayList(TestResult),
pub fn init(
allocator: std.mem.Allocator,
cir: *const ModuleEnv,
stack_memory: *stack.Stack,
layout_cache: *LayoutStore,
type_store: *TypeStore,
) !TestRunner {
const runner = TestRunner{
.allocator = allocator,
.env = cir,
.interpreter = try Interpreter.init(allocator, cir, stack_memory, layout_cache, type_store),
.roc_ops = null,
.test_results = std.ArrayList(TestResult).init(allocator),
};
return runner;
}
pub fn deinit(self: *TestRunner) void {
self.interpreter.deinit(self.get_ops());
self.test_results.deinit();
}
fn get_ops(self: *TestRunner) *RocOps {
if (self.roc_ops == null) {
self.roc_ops = RocOps{
.env = @ptrCast(self),
.roc_alloc = testRocAlloc,
.roc_dealloc = testRocDealloc,
.roc_realloc = testRocRealloc,
.roc_dbg = testRocDbg,
.roc_expect_failed = testRocExpectFailed,
.roc_crashed = testRocCrashed,
.host_fns = undefined, // Not used in tests
};
}
return &(self.roc_ops.?);
}
/// Evaluates a single expect expression, returning whether it passed, failed or did not evaluate to a boolean.
pub fn eval(self: *TestRunner, expr_idx: CIR.Expr.Idx) EvalError!Evaluation {
const result = try self.interpreter.eval(expr_idx, self.get_ops());
if (result.layout.tag == .scalar and result.layout.data.scalar.tag == .bool) {
const is_true = result.asBool();
if (is_true) {
return Evaluation.passed;
} else {
return Evaluation.failed;
}
} else {
return Evaluation.not_a_bool;
}
}
/// Evaluates all expect statements in the module, returning a summary of the results.
/// Detailed results can be found in `test_results`.
pub fn eval_all(self: *TestRunner) !TestSummary {
var passed: u32 = 0;
var failed: u32 = 0;
self.test_results.clearAndFree();
const statements = self.env.store.sliceStatements(self.env.all_statements);
for (statements) |stmt_idx| {
const stmt = self.env.store.getStatement(stmt_idx);
if (stmt == .s_expect) {
const region = self.env.store.getStatementRegion(stmt_idx);
// TODO this can probably be optimized. Maybe run tests in parallel?
const result = self.eval(stmt.s_expect.body) catch |err| {
failed += 1;
const error_msg = try std.fmt.allocPrint(self.allocator, "Test evaluation failed: {}", .{err});
try self.test_results.append(.{ .region = region, .passed = false, .error_msg = error_msg });
continue;
};
switch (result) {
.not_a_bool => {
failed += 1;
const error_msg = try std.fmt.allocPrint(self.allocator, "Test did not evaluate to a boolean", .{});
try self.test_results.append(.{ .region = region, .passed = false, .error_msg = error_msg });
},
.failed => {
failed += 1;
try self.test_results.append(.{ .region = region, .passed = false });
},
.passed => {
passed += 1;
try self.test_results.append(.{ .region = region, .passed = true });
},
}
}
}
return .{
.passed = passed,
.failed = failed,
};
}
/// Write a html report of the test results to the given writer.
pub fn write_html_report(self: *const TestRunner, writer: std.io.AnyWriter) !void {
if (self.test_results.items.len > 0) {
try writer.writeAll("<div class=\"test-results\">\n");
for (self.test_results.items) |result| {
const region_info = self.env.calcRegionInfo(result.region);
const line_number = region_info.start_line_idx + 1;
try writer.writeAll("<span class=\"test-evaluation\">");
if (result.passed) {
try writer.writeAll("<span class=\"test-passed\">PASSED</span>");
} else {
try writer.writeAll("<span class=\"test-failed\">FAILED</span>");
}
try writer.print("<span class=\"source-range\" data-start-byte=\"{d}\" data-end-byte=\"{d}\">@{d}</span>\n", .{ result.region.start.offset, result.region.end.offset, line_number });
try writer.print("<span class=\"test-message\">{s}</span>\n", .{result.error_msg orelse ""});
try writer.writeAll("</span>\n");
}
try writer.writeAll("</div>\n");
}
}
};

View file

@ -1620,10 +1620,10 @@ const Formatter = struct {
}
try fmt.formatCollection(
provides.region,
.square,
AST.ExposedItem.Idx,
fmt.ast.store.exposedItemSlice(.{ .span = provides.span }),
Formatter.formatExposedItem,
.curly,
AST.RecordField.Idx,
fmt.ast.store.recordFieldSlice(.{ .span = provides.span }),
Formatter.formatRecordField,
);
},
.malformed => {},
@ -2228,6 +2228,19 @@ const Formatter = struct {
return fmt.collectionWillBeMultiline(AST.RecordField.Idx, p.packages);
},
.platform => |p| {
if (fmt.collectionWillBeMultiline(AST.ExposedItem.Idx, p.requires_rigids)) {
return true;
}
if (fmt.collectionWillBeMultiline(AST.ExposedItem.Idx, p.exposes)) {
return true;
}
if (fmt.collectionWillBeMultiline(AST.RecordField.Idx, p.packages)) {
return true;
}
return fmt.collectionWillBeMultiline(AST.RecordField.Idx, p.provides);
},
else => return false,
}
},
@ -2256,6 +2269,10 @@ const Formatter = struct {
const record_field_slice = fmt.ast.store.recordFieldSlice(.{ .span = collection.span });
return fmt.nodesWillBeMultiline(AST.RecordField.Idx, record_field_slice);
},
AST.ExposedItem.Idx => {
const exposed_item_slice = fmt.ast.store.exposedItemSlice(.{ .span = collection.span });
return fmt.nodesWillBeMultiline(AST.ExposedItem.Idx, exposed_item_slice);
},
else => return false,
}
}

View file

@ -13,6 +13,12 @@ const layout = @import("layout");
const ipc = @import("ipc");
const SharedMemoryAllocator = ipc.SharedMemoryAllocator;
// Global state for shared memory - initialized once per process
var shared_memory_initialized: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
var global_shm: ?SharedMemoryAllocator = null;
var global_env_ptr: ?*ModuleEnv = null;
var shm_mutex: std.Thread.Mutex = .{};
const Stack = eval.Stack;
const LayoutStore = layout.Store;
const CIR = can.CIR;
@ -26,6 +32,15 @@ const safe_memory = base.safe_memory;
const FIRST_ALLOC_OFFSET = 504; // 0x1f8 - First allocation starts at this offset
const MODULE_ENV_OFFSET = 0x10; // 8 bytes for u64, 4 bytes for u32, 4 bytes padding
// Header structure that matches the one in main.zig
const Header = struct {
parent_base_addr: u64,
entry_count: u32,
_padding: u32, // Ensure 8-byte alignment
def_indices_offset: u64,
module_env_offset: u64,
};
/// Comprehensive error handling for the shim
const ShimError = error{
SharedMemoryError,
@ -42,67 +57,123 @@ const ShimError = error{
BugUnboxedFlexVar,
BugUnboxedRigidVar,
UnsupportedResultType,
InvalidEntryIndex,
} || safe_memory.MemoryError || eval.EvalError;
/// Exported symbol that reads ModuleEnv from shared memory and evaluates it
/// Returns a RocStr to the caller
/// Expected format in shared memory: [u64 parent_address][ModuleEnv data]
export fn roc_entrypoint(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void {
evaluateFromSharedMemory(ops, ret_ptr, arg_ptr) catch |err| {
std.log.err("Error evaluating from shared memory: {s}", .{@errorName(err)});
/// Expected format in shared memory: [u64 parent_address][u32 entry_count][ModuleEnv data][u32[] def_indices]
export fn roc_entrypoint(entry_idx: u32, ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void {
evaluateFromSharedMemory(entry_idx, ops, ret_ptr, arg_ptr) catch |err| {
var buf: [256]u8 = undefined;
const msg2 = std.fmt.bufPrint(&buf, "Error evaluating from shared memory: {s}", .{@errorName(err)}) catch "Error evaluating from shared memory";
ops.crash(msg2);
};
}
/// Cross-platform shared memory evaluation
fn evaluateFromSharedMemory(roc_ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) ShimError!void {
/// Initialize shared memory and ModuleEnv once per process
fn initializeSharedMemoryOnce(roc_ops: *RocOps) ShimError!void {
// Fast path: if already initialized, return immediately
if (shared_memory_initialized.load(.acquire)) {
return;
}
// Slow path: acquire mutex and check again (double-checked locking)
shm_mutex.lock();
defer shm_mutex.unlock();
// Check again in case another thread initialized while we were waiting
if (shared_memory_initialized.load(.acquire)) {
return;
}
const allocator = std.heap.page_allocator;
var buf: [256]u8 = undefined;
// Get page size
const page_size = SharedMemoryAllocator.getSystemPageSize() catch 4096;
// Create shared memory allocator from coordination info
var shm = SharedMemoryAllocator.fromCoordination(allocator, page_size) catch |err| {
std.log.err("Failed to create shared memory allocator: {s}", .{@errorName(err)});
const msg2 = std.fmt.bufPrint(&buf, "Failed to create shared memory allocator: {s}", .{@errorName(err)}) catch "Failed to create shared memory allocator";
roc_ops.crash(msg2);
return error.SharedMemoryError;
};
defer shm.deinit(allocator);
// Set up ModuleEnv from shared memory
const env_ptr = try setupModuleEnv(&shm);
const env_ptr = try setupModuleEnv(&shm, roc_ops);
// Set up interpreter infrastructure
var interpreter = try createInterpreter(env_ptr);
// Store globals
global_shm = shm;
global_env_ptr = env_ptr;
// Mark as initialized (release semantics ensure all writes above are visible)
shared_memory_initialized.store(true, .release);
}
/// Cross-platform shared memory evaluation
fn evaluateFromSharedMemory(entry_idx: u32, roc_ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) ShimError!void {
// Initialize shared memory once per process
try initializeSharedMemoryOnce(roc_ops);
// Use the global shared memory and environment
const shm = global_shm.?;
const env_ptr = global_env_ptr.?;
// Set up interpreter infrastructure (per-call, as it's lightweight)
var interpreter = try createInterpreter(env_ptr, roc_ops);
defer interpreter.deinit(roc_ops);
// Get expression info from shared memory
// Get expression info from shared memory using entry_idx
const base_ptr = shm.getBasePtr();
const expr_idx: CIR.Expr.Idx = @enumFromInt(
safe_memory.safeRead(u32, base_ptr, FIRST_ALLOC_OFFSET + @sizeOf(u64), shm.total_size) catch {
var buf: [256]u8 = undefined;
// Read the header structure from shared memory
const header_addr = @intFromPtr(base_ptr) + FIRST_ALLOC_OFFSET;
const header_ptr: *const Header = @ptrFromInt(header_addr);
if (entry_idx >= header_ptr.entry_count) {
const err_msg = std.fmt.bufPrint(&buf, "Invalid entry_idx {} >= entry_count {}", .{ entry_idx, header_ptr.entry_count }) catch "Invalid entry_idx";
roc_ops.crash(err_msg);
return error.InvalidEntryIndex;
}
const def_offset = header_ptr.def_indices_offset + entry_idx * @sizeOf(u32);
const def_idx_raw = safe_memory.safeRead(u32, base_ptr, @intCast(def_offset), shm.total_size) catch |err| {
const read_err = std.fmt.bufPrint(&buf, "Failed to read def_idx: {}", .{err}) catch "Failed to read def_idx";
roc_ops.crash(read_err);
return error.MemoryLayoutInvalid;
},
);
};
const def_idx: CIR.Def.Idx = @enumFromInt(def_idx_raw);
// Get the definition and extract its expression
const def = env_ptr.store.getDef(def_idx);
const expr_idx = def.expr;
// Evaluate the expression (with optional arguments)
try interpreter.evaluateExpression(expr_idx, ret_ptr, roc_ops, arg_ptr);
}
/// Set up ModuleEnv from shared memory with proper relocation
fn setupModuleEnv(shm: *SharedMemoryAllocator) ShimError!*ModuleEnv {
// Validate memory layout
const min_required_size = FIRST_ALLOC_OFFSET + @sizeOf(u64) + @sizeOf(u32) + MODULE_ENV_OFFSET + @sizeOf(ModuleEnv);
fn setupModuleEnv(shm: *SharedMemoryAllocator, roc_ops: *RocOps) ShimError!*ModuleEnv {
// Validate memory layout - we need at least space for the header
const min_required_size = FIRST_ALLOC_OFFSET + @sizeOf(Header);
if (shm.total_size < min_required_size) {
std.log.err("Invalid memory layout: size {} is too small (minimum required: {})", .{ shm.total_size, min_required_size });
var buf: [256]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "Invalid memory layout: size {} is too small (minimum required: {})", .{ shm.total_size, min_required_size }) catch "Invalid memory layout";
roc_ops.crash(msg);
return error.MemoryLayoutInvalid;
}
var buf: [256]u8 = undefined;
// Get base pointer
const base_ptr = shm.getBasePtr();
// Read parent's shared memory base address and calculate relocation offset
const data_ptr = base_ptr + FIRST_ALLOC_OFFSET;
const parent_base_addr = safe_memory.safeRead(u64, base_ptr, FIRST_ALLOC_OFFSET, shm.total_size) catch {
return error.MemoryLayoutInvalid;
};
// Read parent's shared memory base address from header and calculate relocation offset
const header_addr = @intFromPtr(base_ptr) + FIRST_ALLOC_OFFSET;
const header_ptr: *const Header = @ptrFromInt(header_addr);
const parent_base_addr = header_ptr.parent_base_addr;
// Calculate relocation offset
const child_base_addr = @intFromPtr(base_ptr);
@ -110,25 +181,19 @@ fn setupModuleEnv(shm: *SharedMemoryAllocator) ShimError!*ModuleEnv {
// Sanity check for overflow potential
if (@abs(offset) > std.math.maxInt(isize) / 2) {
std.log.err("Relocation offset too large: {}", .{offset});
const err_msg = std.fmt.bufPrint(&buf, "Relocation offset too large: {}", .{offset}) catch "Relocation offset too large";
roc_ops.crash(err_msg);
return error.ModuleEnvSetupFailed;
}
// Get ModuleEnv pointer and set it up
const env_addr = @intFromPtr(data_ptr) + MODULE_ENV_OFFSET;
// Get ModuleEnv pointer from the offset stored in the header
const env_addr = @intFromPtr(base_ptr) + @as(usize, @intCast(header_ptr.module_env_offset));
const env_ptr: *ModuleEnv = @ptrFromInt(env_addr);
// Set up the environment
env_ptr.gpa = std.heap.page_allocator;
env_ptr.relocate(offset);
// TODO Relocate strings manually if they exist
// if (env_ptr.source.len > 0) {
// const old_source_ptr = @intFromPtr(env_ptr.source.ptr);
// const new_source_ptr = @as(isize, @intCast(old_source_ptr)) + offset;
// env_ptr.source.ptr = @ptrFromInt(@as(usize, @intCast(new_source_ptr)));
// }
if (env_ptr.module_name.len > 0) {
const old_module_ptr = @intFromPtr(env_ptr.module_name.ptr);
const new_module_ptr = @as(isize, @intCast(old_module_ptr)) + offset;
@ -139,31 +204,31 @@ fn setupModuleEnv(shm: *SharedMemoryAllocator) ShimError!*ModuleEnv {
}
/// Create and initialize interpreter with heap-allocated stable objects
fn createInterpreter(env_ptr: *ModuleEnv) ShimError!Interpreter {
fn createInterpreter(env_ptr: *ModuleEnv, roc_ops: *RocOps) ShimError!Interpreter {
const allocator = std.heap.page_allocator;
// Allocate stack on heap to ensure stable address
const eval_stack = allocator.create(Stack) catch {
std.log.err("Stack allocation failed", .{});
roc_ops.crash("INTERPRETER SHIM: Stack allocation failed");
return error.InterpreterSetupFailed;
};
errdefer allocator.destroy(eval_stack);
eval_stack.* = Stack.initCapacity(allocator, 64 * 1024) catch {
std.log.err("Stack initialization failed", .{});
roc_ops.crash("INTERPRETER SHIM: Stack initialization failed");
return error.InterpreterSetupFailed;
};
errdefer eval_stack.deinit();
// Allocate layout cache on heap to ensure stable address
const layout_cache = allocator.create(LayoutStore) catch {
std.log.err("Layout cache allocation failed", .{});
roc_ops.crash("INTERPRETER SHIM: Layout cache allocation failed");
return error.InterpreterSetupFailed;
};
errdefer allocator.destroy(layout_cache);
layout_cache.* = LayoutStore.init(env_ptr, &env_ptr.types) catch {
std.log.err("Layout cache initialization failed", .{});
roc_ops.crash("INTERPRETER SHIM: Layout cache initialization failed");
return error.InterpreterSetupFailed;
};
errdefer layout_cache.deinit();
@ -176,7 +241,7 @@ fn createInterpreter(env_ptr: *ModuleEnv) ShimError!Interpreter {
layout_cache,
&env_ptr.types,
) catch {
std.log.err("Interpreter initialization failed", .{});
roc_ops.crash("INTERPRETER SHIM: Interpreter initialization failed");
return error.InterpreterSetupFailed;
};
errdefer interpreter.deinit();

View file

@ -606,6 +606,8 @@ pub const Diagnostic = struct {
expected_provides,
expected_provides_close_square,
expected_provides_open_square,
expected_provides_close_curly,
expected_provides_open_curly,
expected_requires,
expected_requires_rigids_close_curly,
expected_requires_rigids_open_curly,
@ -1647,12 +1649,13 @@ pub const Header = union(enum) {
// Provides
const provides = ast.store.getCollection(a.provides);
const provides_items = ast.store.recordFieldSlice(.{ .span = provides.span });
const provides_begin = tree.beginNode();
try tree.pushStaticAtom("provides");
try ast.appendRegionInfoToSexprTree(env, tree, provides.region);
const attrs6 = tree.beginNode();
for (ast.store.exposedItemSlice(.{ .span = provides.span })) |exposed| {
const item = ast.store.getExposedItem(exposed);
for (provides_items) |item_idx| {
const item = ast.store.getRecordField(item_idx);
try item.pushToSExprTree(gpa, env, ast, tree);
}
try tree.endNode(provides_begin, attrs6);

View file

@ -441,6 +441,9 @@ pub const Tag = enum {
/// Collection of packages fields
collection_packages,
/// Collection of record fields
collection_record_fields,
/// Collection of where clauses
collection_where_clause,

View file

@ -513,26 +513,26 @@ pub fn parsePlatformHeader(self: *Parser) Error!AST.Header.Idx {
);
};
const provides_start = self.pos;
self.expect(.OpenSquare) catch {
self.expect(.OpenCurly) catch {
return try self.pushMalformed(
AST.Header.Idx,
.expected_provides_open_square,
.expected_provides_open_curly,
self.pos,
);
};
const provides_top = self.store.scratchExposedItemTop();
const provides_top = self.store.scratchRecordFieldTop();
self.parseCollectionSpan(
AST.ExposedItem.Idx,
.CloseSquare,
NodeStore.addScratchExposedItem,
Parser.parseExposedItem,
AST.RecordField.Idx,
.CloseCurly,
NodeStore.addScratchRecordField,
Parser.parseRecordField,
) catch |err| {
switch (err) {
error.ExpectedNotFound => {
self.store.clearScratchExposedItemsFrom(provides_top);
self.store.clearScratchRecordFieldsFrom(provides_top);
return try self.pushMalformed(
AST.Header.Idx,
.expected_provides_close_square,
.expected_provides_close_curly,
provides_start,
);
},
@ -540,9 +540,9 @@ pub fn parsePlatformHeader(self: *Parser) Error!AST.Header.Idx {
error.TooNested => return error.TooNested,
}
};
const provides_span = try self.store.exposedItemSpanFrom(provides_top);
const provides_span = try self.store.recordFieldSpanFrom(provides_top);
const provides = try self.store.addCollection(
.collection_exposed,
.collection_record_fields,
.{
.span = provides_span.span,
.region = .{ .start = provides_start, .end = self.pos },

View file

@ -21,6 +21,7 @@ const build_options = @import("build_options");
const parse = @import("parse");
const reporting = @import("reporting");
const repl = @import("repl");
const eval = @import("eval");
const types = @import("types");
const compile = @import("compile");
const can = @import("can");
@ -28,6 +29,7 @@ const check = @import("check");
const unbundle = @import("unbundle");
const fmt = @import("fmt");
const WasmFilesystem = @import("WasmFilesystem.zig");
const layout = @import("layout");
const Can = can.Can;
const Check = check.Check;
@ -38,6 +40,7 @@ const problem = check.problem;
const AST = parse.AST;
const Repl = repl.Repl;
const RocOps = builtins.host_abi.RocOps;
const TestRunner = eval.TestRunner;
// A fixed-size buffer to act as the heap inside the WASM linear memory.
var wasm_heap_memory: [64 * 1024 * 1024]u8 = undefined; // 64MB heap
@ -61,6 +64,7 @@ const MessageType = enum {
QUERY_TYPES,
QUERY_FORMATTED,
GET_HOVER_INFO,
EVALUATE_TESTS,
RESET,
INIT_REPL,
REPL_STEP,
@ -75,6 +79,7 @@ const MessageType = enum {
if (std.mem.eql(u8, str, "QUERY_TYPES")) return .QUERY_TYPES;
if (std.mem.eql(u8, str, "QUERY_FORMATTED")) return .QUERY_FORMATTED;
if (std.mem.eql(u8, str, "GET_HOVER_INFO")) return .GET_HOVER_INFO;
if (std.mem.eql(u8, str, "EVALUATE_TESTS")) return .EVALUATE_TESTS;
if (std.mem.eql(u8, str, "RESET")) return .RESET;
if (std.mem.eql(u8, str, "INIT_REPL")) return .INIT_REPL;
if (std.mem.eql(u8, str, "REPL_STEP")) return .REPL_STEP;
@ -649,6 +654,9 @@ fn handleLoadedState(message_type: MessageType, message_json: std.json.Value, re
.GET_HOVER_INFO => {
try writeHoverInfoResponse(response_buffer, data, message_json);
},
.EVALUATE_TESTS => {
try writeEvaluateTestsResponse(response_buffer, data);
},
.RESET => {
resetGlobalState();
@ -1333,6 +1341,56 @@ fn writeCanCirResponse(response_buffer: []u8, data: CompilerStageData) ResponseW
try resp_writer.finalize();
}
fn writeEvaluateTestsResponse(response_buffer: []u8, data: CompilerStageData) ResponseWriteError!void {
// use arena for test evaluation
var env = data.module_env;
var local_arena = std.heap.ArenaAllocator.init(allocator);
defer local_arena.deinit();
// Create interpreter infrastructure for test evaluation
var stack_memory = eval.Stack.initCapacity(local_arena.allocator(), 1024) catch {
try writeErrorResponse(response_buffer, .ERROR, "Failed to create stack memory.");
return;
};
var layout_cache = layout.Store.init(env, &env.types) catch {
try writeErrorResponse(response_buffer, .ERROR, "FFailed to create layout cache.");
return;
};
var test_runner = TestRunner.init(local_arena.allocator(), env, &stack_memory, &layout_cache, &env.types) catch {
try writeErrorResponse(response_buffer, .ERROR, "Failed to initialize test runner.");
return;
};
defer test_runner.deinit();
_ = test_runner.eval_all() catch {
try writeErrorResponse(response_buffer, .ERROR, "Failed to evaluate tests.");
return;
};
var html_buffer = std.ArrayList(u8).init(local_arena.allocator());
const html_writer = html_buffer.writer().any();
test_runner.write_html_report(html_writer) catch {
try writeErrorResponse(response_buffer, .ERROR, "Failed to generate test report.");
return;
};
var resp_writer = ResponseWriter{ .buffer = response_buffer };
resp_writer.pos = @sizeOf(u32);
const w = resp_writer.writer();
try w.writeAll("{\"status\":\"SUCCESS\",\"data\":\"");
try writeJsonString(w, html_buffer.items);
try w.writeAll("\"}");
try resp_writer.finalize();
return;
}
const HoverInfo = struct {
name: []const u8,
type_str: []const u8,

22
src/roc_src/Span.zig Normal file
View file

@ -0,0 +1,22 @@
//! A slice of a module's source code, used for highlighting code in diagnostics.
const std = @import("std");
const Self = @This();
start: u32,
len: u32,
/// Write the debug format of a span to a writer.
pub fn format(
self: *const Self,
comptime fmt: []const u8,
_: std.fmt.FormatOptions,
writer: std.io.AnyWriter,
) !void {
if (fmt.len != 0) {
std.fmt.invalidFmtError(fmt, self);
}
try writer.print("@{}-{}", .{ self.start, self.start + self.len });
}

11
src/roc_src/mod.zig Normal file
View file

@ -0,0 +1,11 @@
//! Working with Roc source code - whether in files or individual strings.
const std = @import("std");
pub const Span = @import("Span.zig");
/// A slice of bytes representing source code, aligned for 128-bit SIMD and
/// guaranteed to end in a newline, so that syntax-elements which end in newlines
/// (e.g. comments, multiline string literals) don't need to check for EOF,
/// they can just end on newline and that's it. Also means we can always lookahead
/// 1 byte from any non-whitespace byte without exceeding the slice's bounds.
pub const Bytes = [:'\n']align(16) const u8;

View file

@ -49,7 +49,7 @@ const rand = prng.random();
/// Logs a message if verbose logging is enabled.
fn log(comptime fmt_str: []const u8, args: anytype) void {
if (verbose_log) {
std.log.info(fmt_str, args);
std.log.debug(fmt_str, args);
}
}
@ -719,7 +719,7 @@ pub fn main() !void {
\\Arguments:
\\ snapshot_paths Paths to snapshot files or directories
;
std.log.info(usage, .{});
std.log.debug(usage, .{});
std.process.exit(0);
} else {
try snapshot_paths.append(arg);
@ -796,7 +796,7 @@ pub fn main() !void {
const duration_ms = timer.read() / std.time.ns_per_ms;
std.log.info(
std.log.debug(
"collected {d} items in {d} ms, processed {d} snapshots in {d} ms.",
.{ work_list.items.len, collect_duration_ms, result.success, duration_ms },
);

View file

@ -6,7 +6,7 @@ This directory contains a primitive test platform for Roc and demonstrates how t
- **Description**: Takes two random integers from the host and returns their product
```bash
zig build -Dllvm
zig build
# Run (ignore cached files)
./zig-out/bin/roc --no-cache test/int/app.roc

View file

@ -1,4 +1,7 @@
app [main] { pf: platform "./platform/main.roc" }
app [addInts, multiplyInts] { pf: platform "./platform/main.roc" }
main : I64, I64 -> I64
main = |a, b| a * b
addInts : I64, I64 -> I64
addInts = |a, b| a + b
multiplyInts : I64, I64 -> I64
multiplyInts = |a, b| a * b

View file

@ -58,22 +58,40 @@ fn rocCrashedFn(roc_crashed: *const builtins.host_abi.RocCrashed, env: *anyopaqu
@panic(message);
}
// External symbol provided by the Roc runtime object file
// External symbols provided by the Roc runtime object file
// Follows RocCall ABI: ops, ret_ptr, then argument pointers
extern fn roc_entrypoint(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
extern fn roc__addInts(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
extern fn roc__multiplyInts(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
// Windows __main stub for MinGW-style initialization
pub export fn __main() void {}
// OS-specific entry point handling
comptime {
// Export main for all platforms
@export(&main, .{ .name = "main" });
/// Arguments struct for passing two integers to Roc as a tuple
const Args = struct {
a: i64,
b: i64,
};
// Windows MinGW/MSVCRT compatibility: export __main stub
if (@import("builtin").os.tag == .windows) {
@export(&__main, .{ .name = "__main" });
}
}
// Windows MinGW/MSVCRT compatibility stub
// The C runtime on Windows calls __main from main for constructor initialization
fn __main() callconv(.C) void {}
// C compatible main for runtime
fn main(argc: c_int, argv: [*][*:0]u8) callconv(.C) c_int {
_ = argc;
_ = argv;
platform_main() catch |err| {
std.io.getStdErr().writer().print("HOST ERROR: {?}", .{err}) catch unreachable;
return 1;
};
return 0;
}
/// Platform host entrypoint -- this is where the roc application starts and does platform things
/// before the platform calls into Roc to do application-specific things.
pub fn main() !void {
fn platform_main() !void {
var host_env = HostEnv{
.arena = std.heap.ArenaAllocator.init(std.heap.page_allocator),
};
@ -98,28 +116,53 @@ pub fn main() !void {
const a = rand.random().intRangeAtMost(i64, 0, 100);
const b = rand.random().intRangeAtMost(i64, 0, 100);
// Create arguments struct - Roc expects arguments as a tuple
var args = Args{
.a = a,
.b = b,
};
// Arguments struct for passing two integers to Roc as a tuple
const Args = extern struct { a: i64, b: i64 };
var args = Args{ .a = a, .b = b };
// Call the Roc entrypoint - pass argument pointer for functions, null for values
var result: i64 = undefined;
roc_entrypoint(&roc_ops, @as(*anyopaque, @ptrCast(&result)), @as(*anyopaque, @ptrCast(&args)));
// Calculate expected result
const expected = a *% b; // Use wrapping multiplication to match Roc behavior
// Print interesting display
try stdout.print("Generated numbers: a = {}, b = {}\n", .{ a, b });
try stdout.print("Expected result: {}\n", .{expected});
try stdout.print("Roc computed: {}\n", .{result});
if (result == expected) {
try stdout.print("\x1b[32mSUCCESS\x1b[0m: Results match!\n", .{});
// Test first entrypoint: addInts (entry_idx = 0)
try stdout.print("\n=== Testing addInts (entry_idx = 0) ===\n", .{});
var add_result: i64 = undefined;
roc__addInts(&roc_ops, @as(*anyopaque, @ptrCast(&add_result)), @as(*anyopaque, @ptrCast(&args)));
const expected_add = a +% b; // Use wrapping addition to match Roc behavior
try stdout.print("Expected add result: {}\n", .{expected_add});
try stdout.print("Roc computed add: {}\n", .{add_result});
var success_count: u32 = 0;
if (add_result == expected_add) {
try stdout.print("\x1b[32mSUCCESS\x1b[0m: addInts results match!\n", .{});
success_count += 1;
} else {
try stdout.print("\x1b[31mFAIL\x1b[0m: Results differ!\n", .{});
try stdout.print("\x1b[31mFAIL\x1b[0m: addInts results differ!\n", .{});
}
// Test second entrypoint: multiplyInts (entry_idx = 1)
try stdout.print("\n=== Testing multiplyInts (entry_idx = 1) ===\n", .{});
var multiply_result: i64 = undefined;
roc__multiplyInts(&roc_ops, @as(*anyopaque, @ptrCast(&multiply_result)), @as(*anyopaque, @ptrCast(&args)));
const expected_multiply = a *% b; // Use wrapping multiplication to match Roc behavior
try stdout.print("Expected multiply result: {}\n", .{expected_multiply});
try stdout.print("Roc computed multiply: {}\n", .{multiply_result});
if (multiply_result == expected_multiply) {
try stdout.print("\x1b[32mSUCCESS\x1b[0m: multiplyInts results match!\n", .{});
success_count += 1;
} else {
try stdout.print("\x1b[31mFAIL\x1b[0m: multiplyInts results differ!\n", .{});
}
// Final summary
try stdout.print("\n=== FINAL RESULT ===\n", .{});
if (success_count == 2) {
try stdout.print("\x1b[32mALL TESTS PASSED\x1b[0m: Both entrypoints work correctly!\n", .{});
} else {
try stdout.print("\x1b[31mSOME TESTS FAILED\x1b[0m: {}/2 tests passed\n", .{success_count});
std.process.exit(1);
}
}

View file

@ -1,8 +1,9 @@
platform ""
requires {} { main : I64, I64 -> I64 }
requires {} { addInts : I64, I64 -> I64, multiplyInts : I64, I64 -> I64 }
exposes []
packages {}
imports []
provides [main]
provides { addInts: "addInts", multiplyInts: "multiplyInts" }
main : I64, I64 -> I64
addInts : I64, I64 -> I64
multiplyInts : I64, I64 -> I64

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -5,7 +5,7 @@ type=file
~~~
# SOURCE
~~~roc
app [main!] { pf: platform "../basic-cli/platform.roc" }
app [] { pf: platform "../basic-cli/platform.roc" }
a = 5
b = a + 1
@ -16,7 +16,7 @@ NIL
NIL
# TOKENS
~~~zig
KwApp(1:1-1:4),OpenSquare(1:5-1:6),LowerIdent(1:6-1:11),CloseSquare(1:11-1:12),OpenCurly(1:13-1:14),LowerIdent(1:15-1:17),OpColon(1:17-1:18),KwPlatform(1:19-1:27),StringStart(1:28-1:29),StringPart(1:29-1:54),StringEnd(1:54-1:55),CloseCurly(1:56-1:57),
KwApp(1:1-1:4),OpenSquare(1:5-1:6),CloseSquare(1:6-1:7),OpenCurly(1:8-1:9),LowerIdent(1:10-1:12),OpColon(1:12-1:13),KwPlatform(1:14-1:22),StringStart(1:23-1:24),StringPart(1:24-1:49),StringEnd(1:49-1:50),CloseCurly(1:51-1:52),
LowerIdent(3:1-3:2),OpAssign(3:3-3:4),Int(3:5-3:6),
LowerIdent(4:1-4:2),OpAssign(4:3-4:4),LowerIdent(4:5-4:6),OpPlus(4:7-4:8),Int(4:9-4:10),
EndOfFile(5:1-5:1),
@ -24,17 +24,15 @@ EndOfFile(5:1-5:1),
# PARSE
~~~clojure
(file @1.1-4.10
(app @1.1-1.57
(provides @1.5-1.12
(exposed-lower-ident @1.6-1.11
(text "main!")))
(record-field @1.15-1.55 (name "pf")
(e-string @1.28-1.55
(e-string-part @1.29-1.54 (raw "../basic-cli/platform.roc"))))
(packages @1.13-1.57
(record-field @1.15-1.55 (name "pf")
(e-string @1.28-1.55
(e-string-part @1.29-1.54 (raw "../basic-cli/platform.roc"))))))
(app @1.1-1.52
(provides @1.5-1.7)
(record-field @1.10-1.50 (name "pf")
(e-string @1.23-1.50
(e-string-part @1.24-1.49 (raw "../basic-cli/platform.roc"))))
(packages @1.8-1.52
(record-field @1.10-1.50 (name "pf")
(e-string @1.23-1.50
(e-string-part @1.24-1.49 (raw "../basic-cli/platform.roc"))))))
(statements
(s-decl @3.1-3.6
(p-ident @3.1-3.2 (raw "a"))

View file

@ -14,9 +14,29 @@ app [
}
~~~
# EXPECTED
NIL
EXPOSED BUT NOT DEFINED - app.md:3:2:3:5
EXPOSED BUT NOT DEFINED - app.md:2:2:2:5
# PROBLEMS
NIL
**EXPOSED BUT NOT DEFINED**
The module header says that `a2!` is exposed, but it is not defined anywhere in this module.
**app.md:3:2:3:5:**
```roc
a2!,
```
^^^
You can fix this by either defining `a2!` in this module, or by removing it from the list of exposed values.
**EXPOSED BUT NOT DEFINED**
The module header says that `a1!` is exposed, but it is not defined anywhere in this module.
**app.md:2:2:2:5:**
```roc
a1!,
```
^^^
You can fix this by either defining `a1!` in this module, or by removing it from the list of exposed values.
# TOKENS
~~~zig
KwApp(1:1-1:4),OpenSquare(1:5-1:6),

View file

@ -22,10 +22,10 @@ platform "pf"
pa2: "pa2",
}
# imports [I1.{ I11, I12, }, I2.{ I21, I22, },]
provides [
pr1,
pr2,
]
provides {
pr1: "not implemented",
pr2: "not implemented",
}
~~~
# EXPECTED
EXPOSED BUT NOT DEFINED - platform.md:10:3:10:5
@ -69,10 +69,10 @@ KwPackages(13:2-13:10),OpenCurly(13:11-13:12),
LowerIdent(14:3-14:6),OpColon(14:6-14:7),StringStart(14:8-14:9),StringPart(14:9-14:12),StringEnd(14:12-14:13),Comma(14:13-14:14),
LowerIdent(15:3-15:6),OpColon(15:6-15:7),StringStart(15:8-15:9),StringPart(15:9-15:12),StringEnd(15:12-15:13),Comma(15:13-15:14),
CloseCurly(16:2-16:3),
KwProvides(18:2-18:10),OpenSquare(18:11-18:12),
LowerIdent(19:3-19:6),Comma(19:6-19:7),
LowerIdent(20:3-20:6),Comma(20:6-20:7),
CloseSquare(21:2-21:3),
KwProvides(18:2-18:10),OpenCurly(18:11-18:12),
LowerIdent(19:3-19:6),OpColon(19:6-19:7),StringStart(19:8-19:9),StringPart(19:9-19:24),StringEnd(19:24-19:25),Comma(19:25-19:26),
LowerIdent(20:3-20:6),OpColon(20:6-20:7),StringStart(20:8-20:9),StringPart(20:9-20:24),StringEnd(20:24-20:25),Comma(20:25-20:26),
CloseCurly(21:2-21:3),
EndOfFile(22:1-22:1),
~~~
# PARSE
@ -102,10 +102,12 @@ EndOfFile(22:1-22:1),
(e-string @15.8-15.13
(e-string-part @15.9-15.12 (raw "pa2")))))
(provides @18.11-21.3
(exposed-lower-ident @19.3-19.6
(text "pr1"))
(exposed-lower-ident @20.3-20.6
(text "pr2"))))
(record-field @19.3-19.25 (name "pr1")
(e-string @19.8-19.25
(e-string-part @19.9-19.24 (raw "not implemented"))))
(record-field @20.3-20.25 (name "pr2")
(e-string @20.8-20.25
(e-string-part @20.9-20.24 (raw "not implemented"))))))
(statements))
~~~
# FORMATTED

View file

@ -14,9 +14,29 @@ app [
}
~~~
# EXPECTED
NIL
EXPOSED BUT NOT DEFINED - app.md:3:2:3:5
EXPOSED BUT NOT DEFINED - app.md:2:2:2:5
# PROBLEMS
NIL
**EXPOSED BUT NOT DEFINED**
The module header says that `a2!` is exposed, but it is not defined anywhere in this module.
**app.md:3:2:3:5:**
```roc
a2!
```
^^^
You can fix this by either defining `a2!` in this module, or by removing it from the list of exposed values.
**EXPOSED BUT NOT DEFINED**
The module header says that `a1!` is exposed, but it is not defined anywhere in this module.
**app.md:2:2:2:5:**
```roc
a1!,
```
^^^
You can fix this by either defining `a1!` in this module, or by removing it from the list of exposed values.
# TOKENS
~~~zig
KwApp(1:1-1:4),OpenSquare(1:5-1:6),

View file

@ -22,10 +22,10 @@ platform "pf"
pa2: "pa2"
}
# imports [I1.{ I11, I12, }, I2.{ I21, I22, },]
provides [
pr1,
pr2
]
provides {
pr1: "not implemented",
pr2: "not implemented",
}
~~~
# EXPECTED
EXPOSED BUT NOT DEFINED - platform.md:10:3:10:5
@ -69,10 +69,10 @@ KwPackages(13:2-13:10),OpenCurly(13:11-13:12),
LowerIdent(14:3-14:6),OpColon(14:6-14:7),StringStart(14:8-14:9),StringPart(14:9-14:12),StringEnd(14:12-14:13),Comma(14:13-14:14),
LowerIdent(15:3-15:6),OpColon(15:6-15:7),StringStart(15:8-15:9),StringPart(15:9-15:12),StringEnd(15:12-15:13),
CloseCurly(16:2-16:3),
KwProvides(18:2-18:10),OpenSquare(18:11-18:12),
LowerIdent(19:3-19:6),Comma(19:6-19:7),
LowerIdent(20:3-20:6),
CloseSquare(21:2-21:3),
KwProvides(18:2-18:10),OpenCurly(18:11-18:12),
LowerIdent(19:3-19:6),OpColon(19:6-19:7),StringStart(19:8-19:9),StringPart(19:9-19:24),StringEnd(19:24-19:25),Comma(19:25-19:26),
LowerIdent(20:3-20:6),OpColon(20:6-20:7),StringStart(20:8-20:9),StringPart(20:9-20:24),StringEnd(20:24-20:25),Comma(20:25-20:26),
CloseCurly(21:2-21:3),
EndOfFile(22:1-22:1),
~~~
# PARSE
@ -102,10 +102,12 @@ EndOfFile(22:1-22:1),
(e-string @15.8-15.13
(e-string-part @15.9-15.12 (raw "pa2")))))
(provides @18.11-21.3
(exposed-lower-ident @19.3-19.6
(text "pr1"))
(exposed-lower-ident @20.3-20.6
(text "pr2"))))
(record-field @19.3-19.25 (name "pr1")
(e-string @19.8-19.25
(e-string-part @19.9-19.24 (raw "not implemented"))))
(record-field @20.3-20.25 (name "pr2")
(e-string @20.8-20.25
(e-string-part @20.9-20.24 (raw "not implemented"))))))
(statements))
~~~
# FORMATTED
@ -127,10 +129,10 @@ platform "pf"
pa2: "pa2",
}
# imports [I1.{ I11, I12, }, I2.{ I21, I22, },]
provides [
pr1,
pr2,
]
provides {
pr1: "not implemented",
pr2: "not implemented",
}
~~~
# CANONICALIZE
~~~clojure

View file

@ -8,9 +8,29 @@ type=file
app [a1!, a2!] { pf: platform "../basic-cli/main.roc", a: "a" }
~~~
# EXPECTED
NIL
EXPOSED BUT NOT DEFINED - app.md:1:11:1:14
EXPOSED BUT NOT DEFINED - app.md:1:6:1:9
# PROBLEMS
NIL
**EXPOSED BUT NOT DEFINED**
The module header says that `a2!` is exposed, but it is not defined anywhere in this module.
**app.md:1:11:1:14:**
```roc
app [a1!, a2!] { pf: platform "../basic-cli/main.roc", a: "a" }
```
^^^
You can fix this by either defining `a2!` in this module, or by removing it from the list of exposed values.
**EXPOSED BUT NOT DEFINED**
The module header says that `a1!` is exposed, but it is not defined anywhere in this module.
**app.md:1:6:1:9:**
```roc
app [a1!, a2!] { pf: platform "../basic-cli/main.roc", a: "a" }
```
^^^
You can fix this by either defining `a1!` in this module, or by removing it from the list of exposed values.
# TOKENS
~~~zig
KwApp(1:1-1:4),OpenSquare(1:5-1:6),LowerIdent(1:6-1:9),Comma(1:9-1:10),LowerIdent(1:11-1:14),CloseSquare(1:14-1:15),OpenCurly(1:16-1:17),LowerIdent(1:18-1:20),OpColon(1:20-1:21),KwPlatform(1:22-1:30),StringStart(1:31-1:32),StringPart(1:32-1:53),StringEnd(1:53-1:54),Comma(1:54-1:55),LowerIdent(1:56-1:57),OpColon(1:57-1:58),StringStart(1:59-1:60),StringPart(1:60-1:61),StringEnd(1:61-1:62),CloseCurly(1:63-1:64),

View file

@ -10,7 +10,7 @@ platform "pf"
exposes [E1, E2]
packages { pa1: "pa1", pa2: "pa2" }
# imports [I1.{ I11, I12 }, I2.{ I21, I22 }]
provides [pr1, pr2]
provides { pr1: "not implemented", pr2: "not implemented" }
~~~
# EXPECTED
EXPOSED BUT NOT DEFINED - platform.md:3:11:3:13
@ -42,13 +42,13 @@ KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:13),StringEnd(1:13-
KwRequires(2:2-2:10),OpenCurly(2:11-2:12),UpperIdent(2:13-2:15),Comma(2:15-2:16),UpperIdent(2:17-2:19),CloseCurly(2:20-2:21),OpenCurly(2:22-2:23),LowerIdent(2:24-2:26),OpColon(2:27-2:28),UpperIdent(2:29-2:31),OpArrow(2:32-2:34),UpperIdent(2:35-2:37),Comma(2:37-2:38),LowerIdent(2:39-2:41),OpColon(2:42-2:43),UpperIdent(2:44-2:46),OpArrow(2:47-2:49),UpperIdent(2:50-2:52),CloseCurly(2:53-2:54),
KwExposes(3:2-3:9),OpenSquare(3:10-3:11),UpperIdent(3:11-3:13),Comma(3:13-3:14),UpperIdent(3:15-3:17),CloseSquare(3:17-3:18),
KwPackages(4:2-4:10),OpenCurly(4:11-4:12),LowerIdent(4:13-4:16),OpColon(4:16-4:17),StringStart(4:18-4:19),StringPart(4:19-4:22),StringEnd(4:22-4:23),Comma(4:23-4:24),LowerIdent(4:25-4:28),OpColon(4:28-4:29),StringStart(4:30-4:31),StringPart(4:31-4:34),StringEnd(4:34-4:35),CloseCurly(4:36-4:37),
KwProvides(6:2-6:10),OpenSquare(6:11-6:12),LowerIdent(6:12-6:15),Comma(6:15-6:16),LowerIdent(6:17-6:20),CloseSquare(6:20-6:21),
KwProvides(6:2-6:10),OpenCurly(6:11-6:12),LowerIdent(6:13-6:16),OpColon(6:16-6:17),StringStart(6:18-6:19),StringPart(6:19-6:34),StringEnd(6:34-6:35),Comma(6:35-6:36),LowerIdent(6:37-6:40),OpColon(6:40-6:41),StringStart(6:42-6:43),StringPart(6:43-6:58),StringEnd(6:58-6:59),CloseCurly(6:60-6:61),
EndOfFile(7:1-7:1),
~~~
# PARSE
~~~clojure
(file @1.1-6.21
(platform @1.1-6.21 (name "pf")
(file @1.1-6.61
(platform @1.1-6.61 (name "pf")
(rigids @2.11-2.21
(exposed-upper-ident @2.13-2.15 (text "R1"))
(exposed-upper-ident @2.17-2.19 (text "R2")))
@ -71,11 +71,13 @@ EndOfFile(7:1-7:1),
(record-field @4.25-4.35 (name "pa2")
(e-string @4.30-4.35
(e-string-part @4.31-4.34 (raw "pa2")))))
(provides @6.11-6.21
(exposed-lower-ident @6.12-6.15
(text "pr1"))
(exposed-lower-ident @6.17-6.20
(text "pr2"))))
(provides @6.11-6.61
(record-field @6.13-6.35 (name "pr1")
(e-string @6.18-6.35
(e-string-part @6.19-6.34 (raw "not implemented"))))
(record-field @6.37-6.59 (name "pr2")
(e-string @6.42-6.59
(e-string-part @6.43-6.58 (raw "not implemented"))))))
(statements))
~~~
# FORMATTED

View file

@ -8,9 +8,29 @@ type=file
app [a1!, a2!,] { pf: platform "../basic-cli/main.roc", a: "a", }
~~~
# EXPECTED
NIL
EXPOSED BUT NOT DEFINED - app.md:1:11:1:14
EXPOSED BUT NOT DEFINED - app.md:1:6:1:9
# PROBLEMS
NIL
**EXPOSED BUT NOT DEFINED**
The module header says that `a2!` is exposed, but it is not defined anywhere in this module.
**app.md:1:11:1:14:**
```roc
app [a1!, a2!,] { pf: platform "../basic-cli/main.roc", a: "a", }
```
^^^
You can fix this by either defining `a2!` in this module, or by removing it from the list of exposed values.
**EXPOSED BUT NOT DEFINED**
The module header says that `a1!` is exposed, but it is not defined anywhere in this module.
**app.md:1:6:1:9:**
```roc
app [a1!, a2!,] { pf: platform "../basic-cli/main.roc", a: "a", }
```
^^^
You can fix this by either defining `a1!` in this module, or by removing it from the list of exposed values.
# TOKENS
~~~zig
KwApp(1:1-1:4),OpenSquare(1:5-1:6),LowerIdent(1:6-1:9),Comma(1:9-1:10),LowerIdent(1:11-1:14),Comma(1:14-1:15),CloseSquare(1:15-1:16),OpenCurly(1:17-1:18),LowerIdent(1:19-1:21),OpColon(1:21-1:22),KwPlatform(1:23-1:31),StringStart(1:32-1:33),StringPart(1:33-1:54),StringEnd(1:54-1:55),Comma(1:55-1:56),LowerIdent(1:57-1:58),OpColon(1:58-1:59),StringStart(1:60-1:61),StringPart(1:61-1:62),StringEnd(1:62-1:63),Comma(1:63-1:64),CloseCurly(1:65-1:66),

View file

@ -10,7 +10,7 @@ platform "pf"
exposes [E1, E2,]
packages { pa1: "pa1", pa2: "pa2", }
# imports [I1.{ I11, I12, }, I2.{ I21, I22, },]
provides [pr1, pr2,]
provides { pr1: "not implemented", pr2: "not implemented", }
~~~
# EXPECTED
EXPOSED BUT NOT DEFINED - platform.md:3:11:3:13
@ -42,13 +42,13 @@ KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:13),StringEnd(1:13-
KwRequires(2:2-2:10),OpenCurly(2:11-2:12),UpperIdent(2:13-2:15),Comma(2:15-2:16),UpperIdent(2:17-2:19),Comma(2:19-2:20),CloseCurly(2:21-2:22),OpenCurly(2:23-2:24),LowerIdent(2:25-2:27),OpColon(2:28-2:29),UpperIdent(2:30-2:32),OpArrow(2:33-2:35),UpperIdent(2:36-2:38),Comma(2:38-2:39),LowerIdent(2:40-2:42),OpColon(2:43-2:44),UpperIdent(2:45-2:47),OpArrow(2:48-2:50),UpperIdent(2:51-2:53),Comma(2:53-2:54),CloseCurly(2:55-2:56),
KwExposes(3:2-3:9),OpenSquare(3:10-3:11),UpperIdent(3:11-3:13),Comma(3:13-3:14),UpperIdent(3:15-3:17),Comma(3:17-3:18),CloseSquare(3:18-3:19),
KwPackages(4:2-4:10),OpenCurly(4:11-4:12),LowerIdent(4:13-4:16),OpColon(4:16-4:17),StringStart(4:18-4:19),StringPart(4:19-4:22),StringEnd(4:22-4:23),Comma(4:23-4:24),LowerIdent(4:25-4:28),OpColon(4:28-4:29),StringStart(4:30-4:31),StringPart(4:31-4:34),StringEnd(4:34-4:35),Comma(4:35-4:36),CloseCurly(4:37-4:38),
KwProvides(6:2-6:10),OpenSquare(6:11-6:12),LowerIdent(6:12-6:15),Comma(6:15-6:16),LowerIdent(6:17-6:20),Comma(6:20-6:21),CloseSquare(6:21-6:22),
KwProvides(6:2-6:10),OpenCurly(6:11-6:12),LowerIdent(6:13-6:16),OpColon(6:16-6:17),StringStart(6:18-6:19),StringPart(6:19-6:34),StringEnd(6:34-6:35),Comma(6:35-6:36),LowerIdent(6:37-6:40),OpColon(6:40-6:41),StringStart(6:42-6:43),StringPart(6:43-6:58),StringEnd(6:58-6:59),Comma(6:59-6:60),CloseCurly(6:61-6:62),
EndOfFile(7:1-7:1),
~~~
# PARSE
~~~clojure
(file @1.1-6.22
(platform @1.1-6.22 (name "pf")
(file @1.1-6.62
(platform @1.1-6.62 (name "pf")
(rigids @2.11-2.22
(exposed-upper-ident @2.13-2.15 (text "R1"))
(exposed-upper-ident @2.17-2.19 (text "R2")))
@ -71,11 +71,13 @@ EndOfFile(7:1-7:1),
(record-field @4.25-4.35 (name "pa2")
(e-string @4.30-4.35
(e-string-part @4.31-4.34 (raw "pa2")))))
(provides @6.11-6.22
(exposed-lower-ident @6.12-6.15
(text "pr1"))
(exposed-lower-ident @6.17-6.20
(text "pr2"))))
(provides @6.11-6.62
(record-field @6.13-6.35 (name "pr1")
(e-string @6.18-6.35
(e-string-part @6.19-6.34 (raw "not implemented"))))
(record-field @6.37-6.59 (name "pr2")
(e-string @6.42-6.59
(e-string-part @6.43-6.58 (raw "not implemented"))))))
(statements))
~~~
# FORMATTED
@ -97,10 +99,10 @@ platform "pf"
pa2: "pa2",
}
# imports [I1.{ I11, I12, }, I2.{ I21, I22, },]
provides [
pr1,
pr2,
]
provides {
pr1: "not implemented",
pr2: "not implemented",
}
~~~
# CANONICALIZE
~~~clojure

View file

@ -185,6 +185,7 @@ UNDECLARED TYPE - fuzz_crash_019.md:116:5:116:6
UNDEFINED VARIABLE - fuzz_crash_019.md:119:2:119:5
UNDEFINED VARIABLE - fuzz_crash_019.md:120:1:120:2
UNDEFINED VARIABLE - fuzz_crash_019.md:120:6:120:9
EXPOSED BUT NOT DEFINED - fuzz_crash_019.md:2:6:2:11
INCOMPATIBLE MATCH PATTERNS - fuzz_crash_019.md:52:2:52:2
TYPE MISMATCH - fuzz_crash_019.md:84:2:86:3
# PROBLEMS
@ -824,6 +825,16 @@ h == foo
^^^
**EXPOSED BUT NOT DEFINED**
The module header says that `main!` is exposed, but it is not defined anywhere in this module.
**fuzz_crash_019.md:2:6:2:11:**
```roc
app [main!] { pf: platform "c" }
```
^^^^^
You can fix this by either defining `main!` in this module, or by removing it from the list of exposed values.
**INCOMPATIBLE MATCH PATTERNS**
The pattern in the fourth branch of this `match` differs from previous ones:
**fuzz_crash_019.md:52:2:**

View file

@ -186,6 +186,7 @@ UNDECLARED TYPE - fuzz_crash_020.md:116:5:116:6
UNDEFINED VARIABLE - fuzz_crash_020.md:119:2:119:5
UNDEFINED VARIABLE - fuzz_crash_020.md:120:1:120:2
UNDEFINED VARIABLE - fuzz_crash_020.md:120:6:120:9
EXPOSED BUT NOT DEFINED - fuzz_crash_020.md:2:6:2:11
INCOMPATIBLE MATCH PATTERNS - fuzz_crash_020.md:52:2:52:2
# PROBLEMS
**PARSE ERROR**
@ -835,6 +836,16 @@ h == foo
^^^
**EXPOSED BUT NOT DEFINED**
The module header says that `main!` is exposed, but it is not defined anywhere in this module.
**fuzz_crash_020.md:2:6:2:11:**
```roc
app [main!] { pf: platform "c" }
```
^^^^^
You can fix this by either defining `main!` in this module, or by removing it from the list of exposed values.
**INCOMPATIBLE MATCH PATTERNS**
The pattern in the fourth branch of this `match` differs from previous ones:
**fuzz_crash_020.md:52:2:**

View file

@ -8,17 +8,17 @@ type=file
platform""requires{}{}exposes[]packages{}provides[
~~~
# EXPECTED
PARSE ERROR - fuzz_crash_045.md:2:1:2:1
PARSE ERROR - fuzz_crash_045.md:1:50:1:51
# PROBLEMS
**PARSE ERROR**
A parsing error occurred: `expected_provides_close_square`
A parsing error occurred: `expected_provides_open_curly`
This is an unexpected parsing error. Please check your syntax.
**fuzz_crash_045.md:2:1:2:1:**
**fuzz_crash_045.md:1:50:1:51:**
```roc
platform""requires{}{}exposes[]packages{}provides[
```
^
^
# TOKENS
@ -29,7 +29,7 @@ EndOfFile(2:1-2:1),
# PARSE
~~~clojure
(file @1.1-1.51
(malformed-header @1.50-1.51 (tag "expected_provides_close_square"))
(malformed-header @1.50-1.51 (tag "expected_provides_open_curly"))
(statements))
~~~
# FORMATTED

View file

@ -9,7 +9,7 @@ platform "foo"
requires {} {}
exposes []
packages {}
provides []
provides {}
~~~
# EXPECTED
NIL
@ -21,7 +21,7 @@ KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:14),StringEnd(1:14-
KwRequires(2:2-2:10),OpenCurly(2:11-2:12),CloseCurly(2:12-2:13),OpenCurly(2:14-2:15),CloseCurly(2:15-2:16),
KwExposes(3:2-3:9),OpenSquare(3:10-3:11),CloseSquare(3:11-3:12),
KwPackages(4:2-4:10),OpenCurly(4:11-4:12),CloseCurly(4:12-4:13),
KwProvides(5:2-5:10),OpenSquare(5:11-5:12),CloseSquare(5:12-5:13),
KwProvides(5:2-5:10),OpenCurly(5:11-5:12),CloseCurly(5:12-5:13),
EndOfFile(6:1-6:1),
~~~
# PARSE

View file

@ -5,7 +5,7 @@ type=file
~~~
# SOURCE
~~~roc
platform "foo" requires {} {} exposes [] packages {} provides []
platform "foo" requires {} {} exposes [] packages {} provides {}
~~~
# EXPECTED
NIL
@ -13,7 +13,7 @@ NIL
NIL
# TOKENS
~~~zig
KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:14),StringEnd(1:14-1:15),KwRequires(1:16-1:24),OpenCurly(1:25-1:26),CloseCurly(1:26-1:27),OpenCurly(1:28-1:29),CloseCurly(1:29-1:30),KwExposes(1:31-1:38),OpenSquare(1:39-1:40),CloseSquare(1:40-1:41),KwPackages(1:42-1:50),OpenCurly(1:51-1:52),CloseCurly(1:52-1:53),KwProvides(1:54-1:62),OpenSquare(1:63-1:64),CloseSquare(1:64-1:65),
KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:14),StringEnd(1:14-1:15),KwRequires(1:16-1:24),OpenCurly(1:25-1:26),CloseCurly(1:26-1:27),OpenCurly(1:28-1:29),CloseCurly(1:29-1:30),KwExposes(1:31-1:38),OpenSquare(1:39-1:40),CloseSquare(1:40-1:41),KwPackages(1:42-1:50),OpenCurly(1:51-1:52),CloseCurly(1:52-1:53),KwProvides(1:54-1:62),OpenCurly(1:63-1:64),CloseCurly(1:64-1:65),
EndOfFile(2:1-2:1),
~~~
# PARSE
@ -33,7 +33,7 @@ platform "foo"
requires {} {}
exposes []
packages {}
provides []
provides {}
~~~
# CANONICALIZE
~~~clojure

View file

@ -9,7 +9,7 @@ platform "foo"
requires {} {}
exposes []
packages {}
provides []
provides {}
~~~
# EXPECTED
NIL
@ -21,7 +21,7 @@ KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:14),StringEnd(1:14-
KwRequires(2:2-2:10),OpenCurly(2:11-2:12),CloseCurly(2:12-2:13),OpenCurly(2:14-2:15),CloseCurly(2:15-2:16),
KwExposes(3:2-3:9),OpenSquare(3:10-3:11),CloseSquare(3:11-3:12),
KwPackages(4:2-4:10),OpenCurly(4:11-4:12),CloseCurly(4:12-4:13),
KwProvides(5:2-5:10),OpenSquare(5:11-5:12),CloseSquare(5:12-5:13),
KwProvides(5:2-5:10),OpenCurly(5:11-5:12),CloseCurly(5:12-5:13),
EndOfFile(6:1-6:1),
~~~
# PARSE

View file

@ -23,9 +23,9 @@ platform # Comment after platform keyword
some_pkg: "../some_pkg.roc", # Comment after package
} # Comment after packages close
provides # Comment after provides keyword
[ # Comment after provides open
bar, # Comment after exposed item
]
{ # Comment after provides open
bar: "roc__bar", # Comment after provides entry
}
~~~
# EXPECTED
EXPOSED BUT NOT DEFINED - platform_header_nonempty_1.md:12:4:12:7
@ -60,9 +60,9 @@ OpenCurly(15:3-15:4),
LowerIdent(16:4-16:12),OpColon(16:12-16:13),StringStart(16:14-16:15),StringPart(16:15-16:30),StringEnd(16:30-16:31),Comma(16:31-16:32),
CloseCurly(17:3-17:4),
KwProvides(18:2-18:10),
OpenSquare(19:3-19:4),
LowerIdent(20:4-20:7),Comma(20:7-20:8),
CloseSquare(21:3-21:4),
OpenCurly(19:3-19:4),
LowerIdent(20:4-20:7),OpColon(20:7-20:8),StringStart(20:9-20:10),StringPart(20:10-20:18),StringEnd(20:18-20:19),Comma(20:19-20:20),
CloseCurly(21:3-21:4),
EndOfFile(22:1-22:1),
~~~
# PARSE
@ -86,8 +86,9 @@ EndOfFile(22:1-22:1),
(e-string @16.14-16.31
(e-string-part @16.15-16.30 (raw "../some_pkg.roc")))))
(provides @19.3-21.4
(exposed-lower-ident @20.4-20.7
(text "bar"))))
(record-field @20.4-20.19 (name "bar")
(e-string @20.9-20.19
(e-string-part @20.10-20.18 (raw "roc__bar"))))))
(statements))
~~~
# FORMATTED

View file

@ -9,7 +9,7 @@ platform ""
requires {} { main : Str -> Str }
exposes []
packages {}
provides [entrypoint]
provides { entrypoint: "roc__entrypoint" }
entrypoint : Str -> Str
entrypoint = main
@ -34,7 +34,7 @@ KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:11),StringEnd(1:11-
KwRequires(2:2-2:10),OpenCurly(2:11-2:12),CloseCurly(2:12-2:13),OpenCurly(2:14-2:15),LowerIdent(2:16-2:20),OpColon(2:21-2:22),UpperIdent(2:23-2:26),OpArrow(2:27-2:29),UpperIdent(2:30-2:33),CloseCurly(2:34-2:35),
KwExposes(3:2-3:9),OpenSquare(3:10-3:11),CloseSquare(3:11-3:12),
KwPackages(4:2-4:10),OpenCurly(4:11-4:12),CloseCurly(4:12-4:13),
KwProvides(5:2-5:10),OpenSquare(5:11-5:12),LowerIdent(5:12-5:22),CloseSquare(5:22-5:23),
KwProvides(5:2-5:10),OpenCurly(5:11-5:12),LowerIdent(5:13-5:23),OpColon(5:23-5:24),StringStart(5:25-5:26),StringPart(5:26-5:41),StringEnd(5:41-5:42),CloseCurly(5:43-5:44),
LowerIdent(7:1-7:11),OpColon(7:12-7:13),UpperIdent(7:14-7:17),OpArrow(7:18-7:20),UpperIdent(7:21-7:24),
LowerIdent(8:1-8:11),OpAssign(8:12-8:13),LowerIdent(8:14-8:18),
EndOfFile(9:1-9:1),
@ -42,7 +42,7 @@ EndOfFile(9:1-9:1),
# PARSE
~~~clojure
(file @1.1-8.18
(platform @1.1-5.23 (name "")
(platform @1.1-5.44 (name "")
(rigids @2.11-2.13)
(ty-record @2.14-2.35
(anno-record-field @2.16-2.33 (name "main")
@ -51,9 +51,10 @@ EndOfFile(9:1-9:1),
(ty @2.30-2.33 (name "Str")))))
(exposes @3.10-3.12)
(packages @4.11-4.13)
(provides @5.11-5.23
(exposed-lower-ident @5.12-5.22
(text "entrypoint"))))
(provides @5.11-5.44
(record-field @5.13-5.42 (name "entrypoint")
(e-string @5.25-5.42
(e-string-part @5.26-5.41 (raw "roc__entrypoint"))))))
(statements
(s-type-anno @7.1-7.24 (name "entrypoint")
(ty-fn @7.14-7.24

View file

@ -0,0 +1,73 @@
# META
~~~ini
description=the int test platform
type=file
~~~
# SOURCE
~~~roc
platform ""
requires {} { multiplyInts : I64, I64 -> I64 }
exposes []
packages {}
provides { multiplyInts: "multiplyInts" }
multiplyInts : I64, I64 -> I64
~~~
# EXPECTED
NIL
# PROBLEMS
NIL
# TOKENS
~~~zig
KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:11),StringEnd(1:11-1:12),
KwRequires(2:5-2:13),OpenCurly(2:14-2:15),CloseCurly(2:15-2:16),OpenCurly(2:17-2:18),LowerIdent(2:19-2:31),OpColon(2:32-2:33),UpperIdent(2:34-2:37),Comma(2:37-2:38),UpperIdent(2:39-2:42),OpArrow(2:43-2:45),UpperIdent(2:46-2:49),CloseCurly(2:50-2:51),
KwExposes(3:5-3:12),OpenSquare(3:13-3:14),CloseSquare(3:14-3:15),
KwPackages(4:5-4:13),OpenCurly(4:14-4:15),CloseCurly(4:15-4:16),
KwProvides(5:5-5:13),OpenCurly(5:14-5:15),LowerIdent(5:16-5:28),OpColon(5:28-5:29),StringStart(5:30-5:31),StringPart(5:31-5:43),StringEnd(5:43-5:44),CloseCurly(5:45-5:46),
LowerIdent(7:1-7:13),OpColon(7:14-7:15),UpperIdent(7:16-7:19),Comma(7:19-7:20),UpperIdent(7:21-7:24),OpArrow(7:25-7:27),UpperIdent(7:28-7:31),
EndOfFile(8:1-8:1),
~~~
# PARSE
~~~clojure
(file @1.1-7.31
(platform @1.1-5.46 (name "")
(rigids @2.14-2.16)
(ty-record @2.17-2.51
(anno-record-field @2.19-2.49 (name "multiplyInts")
(ty-fn @2.34-2.49
(ty @2.34-2.37 (name "I64"))
(ty @2.39-2.42 (name "I64"))
(ty @2.46-2.49 (name "I64")))))
(exposes @3.13-3.15)
(packages @4.14-4.16)
(provides @5.14-5.46
(record-field @5.16-5.44 (name "multiplyInts")
(e-string @5.30-5.44
(e-string-part @5.31-5.43 (raw "multiplyInts"))))))
(statements
(s-type-anno @7.1-7.31 (name "multiplyInts")
(ty-fn @7.16-7.31
(ty @7.16-7.19 (name "I64"))
(ty @7.21-7.24 (name "I64"))
(ty @7.28-7.31 (name "I64"))))))
~~~
# FORMATTED
~~~roc
platform ""
requires {} { multiplyInts : I64, I64 -> I64 }
exposes []
packages {}
provides { multiplyInts: "multiplyInts" }
multiplyInts : I64, I64 -> I64
~~~
# CANONICALIZE
~~~clojure
(can-ir (empty true))
~~~
# TYPES
~~~clojure
(inferred-types
(defs)
(expressions))
~~~

View file

@ -0,0 +1,71 @@
# META
~~~ini
description=the str test platform
type=file
~~~
# SOURCE
~~~roc
platform ""
requires {} { processString : Str -> Str }
exposes []
packages {}
provides { processString: "processString" }
processString : Str -> Str
~~~
# EXPECTED
NIL
# PROBLEMS
NIL
# TOKENS
~~~zig
KwPlatform(1:1-1:9),StringStart(1:10-1:11),StringPart(1:11-1:11),StringEnd(1:11-1:12),
KwRequires(2:5-2:13),OpenCurly(2:14-2:15),CloseCurly(2:15-2:16),OpenCurly(2:17-2:18),LowerIdent(2:19-2:32),OpColon(2:33-2:34),UpperIdent(2:35-2:38),OpArrow(2:39-2:41),UpperIdent(2:42-2:45),CloseCurly(2:46-2:47),
KwExposes(3:5-3:12),OpenSquare(3:13-3:14),CloseSquare(3:14-3:15),
KwPackages(4:5-4:13),OpenCurly(4:14-4:15),CloseCurly(4:15-4:16),
KwProvides(5:5-5:13),OpenCurly(5:14-5:15),LowerIdent(5:16-5:29),OpColon(5:29-5:30),StringStart(5:31-5:32),StringPart(5:32-5:45),StringEnd(5:45-5:46),CloseCurly(5:47-5:48),
LowerIdent(7:1-7:14),OpColon(7:15-7:16),UpperIdent(7:17-7:20),OpArrow(7:21-7:23),UpperIdent(7:24-7:27),
EndOfFile(8:1-8:1),
~~~
# PARSE
~~~clojure
(file @1.1-7.27
(platform @1.1-5.48 (name "")
(rigids @2.14-2.16)
(ty-record @2.17-2.47
(anno-record-field @2.19-2.45 (name "processString")
(ty-fn @2.35-2.45
(ty @2.35-2.38 (name "Str"))
(ty @2.42-2.45 (name "Str")))))
(exposes @3.13-3.15)
(packages @4.14-4.16)
(provides @5.14-5.48
(record-field @5.16-5.46 (name "processString")
(e-string @5.31-5.46
(e-string-part @5.32-5.45 (raw "processString"))))))
(statements
(s-type-anno @7.1-7.27 (name "processString")
(ty-fn @7.17-7.27
(ty @7.17-7.20 (name "Str"))
(ty @7.24-7.27 (name "Str"))))))
~~~
# FORMATTED
~~~roc
platform ""
requires {} { processString : Str -> Str }
exposes []
packages {}
provides { processString: "processString" }
processString : Str -> Str
~~~
# CANONICALIZE
~~~clojure
(can-ir (empty true))
~~~
# TYPES
~~~clojure
(inferred-types
(defs)
(expressions))
~~~

View file

@ -5,7 +5,7 @@ type=file
~~~
# SOURCE
~~~roc
app [main] { pf: platform "../basic-cli/platform.roc" }
app [] { pf: platform "../basic-cli/platform.roc" }
# TODO: if you do this whole thing as an expr block, with `composed` at
# the end instead of `answer =`, it triggers a parser bug!
@ -39,7 +39,7 @@ But here it's being used as:
# TOKENS
~~~zig
KwApp(1:1-1:4),OpenSquare(1:5-1:6),LowerIdent(1:6-1:10),CloseSquare(1:10-1:11),OpenCurly(1:12-1:13),LowerIdent(1:14-1:16),OpColon(1:16-1:17),KwPlatform(1:18-1:26),StringStart(1:27-1:28),StringPart(1:28-1:53),StringEnd(1:53-1:54),CloseCurly(1:55-1:56),
KwApp(1:1-1:4),OpenSquare(1:5-1:6),CloseSquare(1:6-1:7),OpenCurly(1:8-1:9),LowerIdent(1:10-1:12),OpColon(1:12-1:13),KwPlatform(1:14-1:22),StringStart(1:23-1:24),StringPart(1:24-1:49),StringEnd(1:49-1:50),CloseCurly(1:51-1:52),
LowerIdent(5:1-5:12),OpColon(5:13-5:14),LowerIdent(5:15-5:16),OpArrow(5:17-5:19),OpenCurly(5:20-5:21),LowerIdent(5:22-5:27),OpColon(5:27-5:28),LowerIdent(5:29-5:30),Comma(5:30-5:31),LowerIdent(5:32-5:35),OpColon(5:35-5:36),UpperIdent(5:37-5:40),CloseCurly(5:41-5:42),
LowerIdent(6:1-6:12),OpAssign(6:13-6:14),OpBar(6:15-6:16),LowerIdent(6:16-6:17),OpBar(6:17-6:18),OpenCurly(6:19-6:20),LowerIdent(6:21-6:26),OpColon(6:26-6:27),LowerIdent(6:28-6:29),Comma(6:29-6:30),LowerIdent(6:31-6:34),OpColon(6:34-6:35),StringStart(6:36-6:37),StringPart(6:37-6:41),StringEnd(6:41-6:42),CloseCurly(6:43-6:44),
LowerIdent(8:1-8:10),OpColon(8:11-8:12),OpenCurly(8:13-8:14),LowerIdent(8:15-8:20),OpColon(8:20-8:21),LowerIdent(8:22-8:23),Comma(8:23-8:24),LowerIdent(8:25-8:28),OpColon(8:28-8:29),UpperIdent(8:30-8:33),CloseCurly(8:34-8:35),OpArrow(8:36-8:38),LowerIdent(8:39-8:40),
@ -52,17 +52,15 @@ EndOfFile(15:1-15:1),
# PARSE
~~~clojure
(file @1.1-14.24
(app @1.1-1.56
(provides @1.5-1.11
(exposed-lower-ident @1.6-1.10
(text "main")))
(record-field @1.14-1.54 (name "pf")
(e-string @1.27-1.54
(e-string-part @1.28-1.53 (raw "../basic-cli/platform.roc"))))
(packages @1.12-1.56
(record-field @1.14-1.54 (name "pf")
(e-string @1.27-1.54
(e-string-part @1.28-1.53 (raw "../basic-cli/platform.roc"))))))
(app @1.1-1.52
(provides @1.5-1.7)
(record-field @1.10-1.50 (name "pf")
(e-string @1.23-1.50
(e-string-part @1.24-1.49 (raw "../basic-cli/platform.roc"))))
(packages @1.8-1.52
(record-field @1.10-1.50 (name "pf")
(e-string @1.23-1.50
(e-string-part @1.24-1.49 (raw "../basic-cli/platform.roc"))))))
(statements
(s-type-anno @5.1-5.42 (name "make_record")
(ty-fn @5.15-5.42
@ -124,7 +122,7 @@ EndOfFile(15:1-15:1),
~~~
# FORMATTED
~~~roc
app [main] { pf: platform "../basic-cli/platform.roc" }
app [] { pf: platform "../basic-cli/platform.roc" }
# TODO: if you do this whole thing as an expr block, with `composed` at
# the end instead of `answer =`, it triggers a parser bug!

View file

@ -6,7 +6,7 @@ This directory contains a primitive test platform for Roc and demonstrates passi
- **Description**: Takes a string from the host and returns a processed string
```bash
zig build -Dllvm
zig build
# Run (ignore cached files)
./zig-out/bin/roc --no-cache test/str/app.roc

View file

@ -1,5 +1,5 @@
app [main] { pf: platform "./platform/main.roc" }
app [processString] { pf: platform "./platform/main.roc" }
main : Str -> Str
main = |input|
processString : Str -> Str
processString = |input|
"Got the following from the host: ${input}\n"

View file

@ -80,14 +80,37 @@ fn rocCrashedFn(roc_crashed: *const RocCrashed, env: *anyopaque) callconv(.c) no
// External symbol provided by the Roc runtime object file
// Follows RocCall ABI: ops, ret_ptr, then argument pointers
extern fn roc_entrypoint(ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
extern fn roc__processString(ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
// Windows __main stub for MinGW-style initialization
pub export fn __main() void {}
// OS-specific entry point handling
comptime {
// Export main for all platforms
@export(&main, .{ .name = "main" });
// Windows MinGW/MSVCRT compatibility: export __main stub
if (@import("builtin").os.tag == .windows) {
@export(&__main, .{ .name = "__main" });
}
}
// Windows MinGW/MSVCRT compatibility stub
// The C runtime on Windows calls __main from main for constructor initialization
fn __main() callconv(.C) void {}
// C compatible main for runtime
fn main(argc: c_int, argv: [*][*:0]u8) callconv(.C) c_int {
_ = argc;
_ = argv;
platform_main() catch |err| {
std.io.getStdErr().writer().print("HOST ERROR: {?}", .{err}) catch unreachable;
return 1;
};
return 0;
}
/// Platform host entrypoint -- this is where the roc application starts and does platform things
/// before the platform calls into Roc to do application-specific things.
pub export fn main() void {
fn platform_main() !void {
var host_env = HostEnv{
.arena = std.heap.ArenaAllocator.init(std.heap.page_allocator),
};
@ -114,32 +137,23 @@ pub export fn main() void {
// Arguments struct for single string parameter - consistent with struct-based approach
// `extern struct` has well-defined in-memory layout matching the C ABI for the target
const Args = extern struct {
str: RocStr,
};
var args = Args{
.str = input_roc_str,
};
const Args = extern struct { str: RocStr };
var args = Args{ .str = input_roc_str };
// Call the Roc entrypoint - pass argument pointer for functions, null for values
var roc_str: RocStr = undefined;
roc_entrypoint(&roc_ops, @as(*anyopaque, @ptrCast(&roc_str)), @as(*anyopaque, @ptrCast(&args)));
roc__processString(&roc_ops, @as(*anyopaque, @ptrCast(&roc_str)), @as(*anyopaque, @ptrCast(&args)));
defer roc_str.decref(&roc_ops);
// Get the string as a slice and print it
const result_slice = roc_str.asSlice();
stdout.print("{s}", .{result_slice}) catch {
std.log.err("Failed to write to stdout\n", .{});
std.process.exit(1);
};
try stdout.print("{s}", .{result_slice});
// Verify the result contains the expected input
const expected_substring = "Got the following from the host: string from host";
if (std.mem.indexOf(u8, result_slice, expected_substring) != null) {
stdout.print("\n\x1b[32mSUCCESS\x1b[0m: Result contains expected substring!\n", .{}) catch {};
try stdout.print("\n\x1b[32mSUCCESS\x1b[0m: Result contains expected substring!\n", .{});
} else {
stdout.print("\n\x1b[31mFAIL\x1b[0m: Result does not contain expected substring!\n", .{}) catch {};
std.process.exit(1);
try stdout.print("\n\x1b[31mFAIL\x1b[0m: Result does not contain expected substring!\n", .{});
}
}

View file

@ -1,8 +1,7 @@
platform ""
requires {} { main : Str -> Str }
requires {} { processString : Str -> Str }
exposes []
packages {}
imports []
provides [main]
provides { processString: "processString" }
main : Str -> Str
processString : Str -> Str