mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
Merge branch 'main' of github.com:roc-lang/roc into upgrade-to-zig-0.15.1
This commit is contained in:
commit
d791285e11
79 changed files with 4750 additions and 713 deletions
6
.github/workflows/alwayscheck.yml
vendored
6
.github/workflows/alwayscheck.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/basic_cli_test_arm64.yml
vendored
6
.github/workflows/basic_cli_test_arm64.yml
vendored
|
|
@ -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
140
.github/workflows/ci_cross_compile.yml
vendored
Normal 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
|
||||
16
.github/workflows/ci_zig.yml
vendored
16
.github/workflows/ci_zig.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
25
.github/workflows/nightly_linux_arm64.yml
vendored
25
.github/workflows/nightly_linux_arm64.yml
vendored
|
|
@ -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
1
.gitignore
vendored
|
|
@ -32,6 +32,7 @@ zig-out
|
|||
*.rs.bk
|
||||
*.o
|
||||
*.a
|
||||
*.s
|
||||
*.so
|
||||
*.so.*
|
||||
*.obj
|
||||
|
|
|
|||
214
build.zig
214
build.zig
|
|
@ -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(©_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(©_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(©_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(©_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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
399
ci/test_int_platform.sh
Executable 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
|
||||
|
|
@ -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
86
src/build/glibc_stub.zig
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
273
src/cli/app_stub.zig
Normal 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, ¶ms, .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
265
src/cli/builder.zig
Normal 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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
165
src/cli/cross_compilation.zig
Normal file
165
src/cli/cross_compilation.zig
Normal 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
389
src/cli/libc_finder.zig
Normal 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();
|
||||
}
|
||||
|
|
@ -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" {
|
||||
|
|
|
|||
1496
src/cli/main.zig
1496
src/cli/main.zig
File diff suppressed because it is too large
Load diff
164
src/cli/platform_host_shim.zig
Normal file
164
src/cli/platform_host_shim.zig
Normal 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
288
src/cli/target.zig
Normal 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;
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
238
src/eval/test_runner.zig
Normal 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");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
22
src/roc_src/Span.zig
Normal 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
11
src/roc_src/mod.zig
Normal 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;
|
||||
|
|
@ -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 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
test/int/platform/targets/arm64glibc/Scrt1.o
Executable file
BIN
test/int/platform/targets/arm64glibc/Scrt1.o
Executable file
Binary file not shown.
BIN
test/int/platform/targets/arm64glibc/crti.o
Executable file
BIN
test/int/platform/targets/arm64glibc/crti.o
Executable file
Binary file not shown.
BIN
test/int/platform/targets/arm64glibc/crtn.o
Executable file
BIN
test/int/platform/targets/arm64glibc/crtn.o
Executable file
Binary file not shown.
BIN
test/int/platform/targets/arm64musl/crt1.o
Executable file
BIN
test/int/platform/targets/arm64musl/crt1.o
Executable file
Binary file not shown.
BIN
test/int/platform/targets/arm64musl/libc.a
Executable file
BIN
test/int/platform/targets/arm64musl/libc.a
Executable file
Binary file not shown.
BIN
test/int/platform/targets/x64glibc/Scrt1.o
Normal file
BIN
test/int/platform/targets/x64glibc/Scrt1.o
Normal file
Binary file not shown.
BIN
test/int/platform/targets/x64glibc/crti.o
Normal file
BIN
test/int/platform/targets/x64glibc/crti.o
Normal file
Binary file not shown.
BIN
test/int/platform/targets/x64glibc/crtn.o
Normal file
BIN
test/int/platform/targets/x64glibc/crtn.o
Normal file
Binary file not shown.
BIN
test/int/platform/targets/x64musl/crt1.o
Normal file
BIN
test/int/platform/targets/x64musl/crt1.o
Normal file
Binary file not shown.
BIN
test/int/platform/targets/x64musl/libc.a
Normal file
BIN
test/int/platform/targets/x64musl/libc.a
Normal file
Binary file not shown.
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
73
test/snapshots/platform/platform_int.md
Normal file
73
test/snapshots/platform/platform_int.md
Normal 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))
|
||||
~~~
|
||||
71
test/snapshots/platform/platform_str.md
Normal file
71
test/snapshots/platform/platform_str.md
Normal 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))
|
||||
~~~
|
||||
|
|
@ -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!
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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", .{});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue