From 909f9aa6eec4b4e4835062cd753d9d641ab0bcad Mon Sep 17 00:00:00 2001 From: LunaStev Date: Fri, 16 Oct 2026 14:52:37 +0900 Subject: [PATCH 1/3] std: remove unused std::libc bindings Remove the ten unused binding modules and their policy-checker exemption. Document the removed import paths and external-binding migration while preserving the existing OS providers and generated C I/O behavior. Refs: wavefnd/Wave#923 Signed-off-by: LunaStev --- std/POLICY.md | 14 +++++--- std/README.md | 10 ++++-- std/libc/arpa.wave | 25 -------------- std/libc/errno.wave | 22 ------------- std/libc/netinet.wave | 37 --------------------- std/libc/poll.wave | 39 ---------------------- std/libc/socket.wave | 66 ------------------------------------- std/libc/stdio.wave | 26 --------------- std/libc/stdlib.wave | 35 -------------------- std/libc/string.wave | 27 --------------- std/libc/time.wave | 36 -------------------- std/libc/unistd.wave | 37 --------------------- tools/check_std_policy.py | 8 +++-- tools/test_release_gates.py | 15 ++++++++- 14 files changed, 36 insertions(+), 361 deletions(-) delete mode 100644 std/libc/arpa.wave delete mode 100644 std/libc/errno.wave delete mode 100644 std/libc/netinet.wave delete mode 100644 std/libc/poll.wave delete mode 100644 std/libc/socket.wave delete mode 100644 std/libc/stdio.wave delete mode 100644 std/libc/stdlib.wave delete mode 100644 std/libc/string.wave delete mode 100644 std/libc/time.wave delete mode 100644 std/libc/unistd.wave diff --git a/std/POLICY.md b/std/POLICY.md index 21b1d6c2..76eb8d50 100644 --- a/std/POLICY.md +++ b/std/POLICY.md @@ -4,9 +4,13 @@ This document defines non-negotiable rules for the Wave standard library (`std/` ## 1) Layering Rules -- `std/libc/**` is the only directory allowed to declare `extern(c)`. -- Outside `std/libc/**`, modules must use Wave code and/or `std/sys/**` raw syscall wrappers. -- High-level modules (`std/io`, `std/fs`, `std/net`, `std/process`, `std/mem`, etc.) must not import `std::libc::*`. +- General-purpose C library bindings belong in external libraries, not `std/`. +- `extern(c)` declarations are limited to the target-specific C ABI providers + explicitly approved by `tools/check_std_policy.py`. This includes hosted OS + APIs and WASI imports; it does not require replacing existing OS bindings. +- Portable modules use Wave code and the target-selected `std/sys/**` provider + surface. No standard library module may import the removed `std::libc::*` + binding modules. ## 2) Syntax Rules @@ -37,6 +41,6 @@ Run: The checker validates: -- `extern(c)` appears only under `std/libc/**` -- no `std::libc::*` import outside `std/libc/**` +- `extern(c)` appears only in explicitly approved target C ABI providers +- no `std::libc::*` import in standard library modules - no retired `let` or `let mut` declarations in Wave sources diff --git a/std/README.md b/std/README.md index 02b9dc1c..6eb99ff0 100644 --- a/std/README.md +++ b/std/README.md @@ -16,7 +16,7 @@ licensed under the repository's [Mozilla Public License 2.0](../LICENSE). ## Dependency Policy -- General-purpose `extern(c)` bindings belong under `std/libc/*`. +- General-purpose C library bindings are not bundled with the standard library. - Target providers under `std/sys/*` may use the small set of hosted C ABI bindings explicitly approved by `tools/check_std_policy.sh` when the OS does not expose an equivalent stable raw syscall contract. @@ -25,9 +25,15 @@ licensed under the repository's [Mozilla Public License 2.0](../LICENSE). environment access, process exit, and linear-memory allocation. The allocator is also available to bare `wasm32-unknown-unknown` modules. A separate Memory64 allocator backs `wasm64-unknown-unknown` without narrowing pointers. -- Modules outside `std/libc/*` must not import `std::libc::*`; portable modules +- Standard library modules must not import `std::libc::*`; portable modules depend on the target-independent `std::sys::*` provider surface instead. +The former `std::libc::*` binding modules have been removed during alpha +development. Existing imports of these modules no longer resolve. Use the +relevant Wave standard library APIs, or provide C interoperability bindings in +an external library for the required target ABI. The target providers under +`std::sys::*` retain their existing OS API bindings. + ## Core Modules - `std::time`: portable clocks and sleep, normalized durations, UTC calendar diff --git a/std/libc/arpa.wave b/std/libc/arpa.wave deleted file mode 100644 index 1f6c4e27..00000000 --- a/std/libc/arpa.wave +++ /dev/null @@ -1,25 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C networking declarations for explicit interoperability. Portable -// std modules use std::sys and std::net instead of importing this module. - -extern(c) { - fun inet_addr(cp: ptr) -> i32; - fun inet_ntoa(addr: i32) -> ptr; -} diff --git a/std/libc/errno.wave b/std/libc/errno.wave deleted file mode 100644 index 6536ecd1..00000000 --- a/std/libc/errno.wave +++ /dev/null @@ -1,22 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C errno declarations for explicit interoperability. Portable std -// modules preserve negative errors through std::sys instead of using this API. - -extern(c) fun __errno_location() -> ptr; diff --git a/std/libc/netinet.wave b/std/libc/netinet.wave deleted file mode 100644 index 8a99dc14..00000000 --- a/std/libc/netinet.wave +++ /dev/null @@ -1,37 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C internet-address declarations for explicit interoperability. -// Portable std modules use the address types provided by std::net. - -// sockaddr_in (IPv4) -pub struct SockAddrIn { - family: i16; // AF_INET - port: i16; // network byte order - addr: i32; // in_addr - zero: array; -} - - -// byte order helpers -extern(c) { - fun htons(x: i16) -> i16; - fun ntohs(x: i16) -> i16; - fun htonl(x: i32) -> i32; - fun ntohl(x: i32) -> i32; -} diff --git a/std/libc/poll.wave b/std/libc/poll.wave deleted file mode 100644 index b1d162ce..00000000 --- a/std/libc/poll.wave +++ /dev/null @@ -1,39 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C polling declarations for explicit interoperability. Portable std -// modules use the target-selected polling operations under std::sys. - -// poll events -pub const POLLIN: i16 = 0x001; -pub const POLLOUT: i16 = 0x004; - -// pollfd -pub struct PollFd { - fd: i32; - events: i16; - revents: i16; -} - -extern(c) { - fun poll( - fds: ptr, - nfds: i64, - timeout: i32 - ) -> i32; -} diff --git a/std/libc/socket.wave b/std/libc/socket.wave deleted file mode 100644 index 0f7951d3..00000000 --- a/std/libc/socket.wave +++ /dev/null @@ -1,66 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C socket declarations for explicit interoperability. Portable std -// modules use std::sys and std::net instead of importing this module. - -// address families -pub const AF_INET: i32 = 2; -pub const AF_INET6: i32 = 10; - -// socket types -pub const SOCK_STREAM: i32 = 1; -pub const SOCK_DGRAM: i32 = 2; - -// protocols -pub const IPPROTO_IP: i32 = 0; -pub const IPPROTO_TCP: i32 = 6; -pub const IPPROTO_UDP: i32 = 17; - -// shutdown how -pub const SHUT_RD: i32 = 0; -pub const SHUT_WR: i32 = 1; -pub const SHUT_RDWR: i32 = 2; - -// socket options -pub const SOL_SOCKET: i32 = 1; -pub const SO_REUSEADDR: i32 = 2; - - -// ====================== -// functions (C ABI) -// ====================== -extern(c) { - fun socket(domain: i32, ty: i32, protocol: i32) -> i32; - fun bind(fd: i32, addr: ptr, len: i32) -> i32; - fun listen(fd: i32, backlog: i32) -> i32; - fun accept(fd: i32, addr: ptr, len: ptr) -> i32; - fun connect(fd: i32, addr: ptr, len: i32) -> i32; - fun shutdown(fd: i32, how: i32) -> i32; - - fun send(fd: i32, buf: ptr, len: i64, flags: i32) -> i64; - fun recv(fd: i32, buf: ptr, len: i64, flags: i32) -> i64; - - fun setsockopt( - fd: i32, - level: i32, - optname: i32, - optval: ptr, - optlen: i32 - ) -> i32; -} diff --git a/std/libc/stdio.wave b/std/libc/stdio.wave deleted file mode 100644 index 79d16b99..00000000 --- a/std/libc/stdio.wave +++ /dev/null @@ -1,26 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C stdio declarations for explicit interoperability. Portable Wave -// I/O is implemented by std::io rather than this module. - -extern(c) { - fun puts(s: ptr) -> i32; - fun getchar() -> i32; - fun putchar(c: i32) -> i32; -} diff --git a/std/libc/stdlib.wave b/std/libc/stdlib.wave deleted file mode 100644 index 144c39c4..00000000 --- a/std/libc/stdlib.wave +++ /dev/null @@ -1,35 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C runtime declarations for explicit interoperability. Portable std -// modules use Wave implementations and std::sys instead of this module. - -extern(c) fun malloc(size: i64) -> ptr; -extern(c) fun calloc(count: i64, size: i64) -> ptr; -extern(c) fun realloc(ptr: ptr, size: i64) -> ptr; -extern(c) fun free(ptr: ptr); - -extern(c) fun exit(code: i32) -> void; -extern(c) fun abort() -> void; - -extern(c) fun atoi(s: ptr) -> i32; -extern(c) fun atol(s: ptr) -> i64; - -extern(c) fun getenv(name: ptr) -> ptr; -extern(c) fun setenv(name: ptr, value: ptr, overwrite: i32) -> i32; -extern(c) fun unsetenv(name: ptr) -> i32; diff --git a/std/libc/string.wave b/std/libc/string.wave deleted file mode 100644 index af482473..00000000 --- a/std/libc/string.wave +++ /dev/null @@ -1,27 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C memory and string declarations for explicit interoperability. -// Portable std modules use Wave implementations under std::mem and std::string. - -extern(c) fun strlen(s: ptr) -> i64; -extern(c) fun strcmp(a: ptr, b: ptr) -> i32; -extern(c) fun strcpy(dst: ptr, src: ptr) -> ptr; - -extern(c) fun memcpy(dst: ptr, src: ptr, n: i64) -> ptr; -extern(c) fun memset(dst: ptr, c: i32, n: i64) -> ptr; diff --git a/std/libc/time.wave b/std/libc/time.wave deleted file mode 100644 index 8868fdf1..00000000 --- a/std/libc/time.wave +++ /dev/null @@ -1,36 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C time declarations for explicit interoperability. Portable clock -// and sleep helpers are provided by std::time. - -pub struct TimeSpec { - sec: i64; - nsec: i64; -} - -pub const CLOCK_REALTIME: i32 = 0; -pub const CLOCK_MONOTONIC: i32 = 1; - -extern(c) { - fun nanosleep(ptr, ptr) -> i32; - fun clock_gettime(i32, ptr) -> i32; - fun c_time(ptr) -> i64 "time"; - fun sleep(i32) -> i32; - fun usleep(i32) -> i32; -} diff --git a/std/libc/unistd.wave b/std/libc/unistd.wave deleted file mode 100644 index fd937d06..00000000 --- a/std/libc/unistd.wave +++ /dev/null @@ -1,37 +0,0 @@ -// This file is part of the Wave language project. -// Copyright (c) 2024-2026 Wave Foundation -// Copyright (c) 2024-2026 LunaStev and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -// Direct C POSIX declarations for explicit interoperability. Portable std -// modules use target-selected std::sys operations instead of this module. - -extern(c) fun read(fd: i32, buf: ptr, count: i64) -> i64; -extern(c) fun write(fd: i32, buf: ptr, count: i64) -> i64; -extern(c) fun close(fd: i32) -> i32; - -extern(c) fun getcwd(buf: ptr, size: i64) -> ptr; -extern(c) fun chdir(path: ptr) -> i32; -extern(c) fun access(path: ptr, mode: i32) -> i32; -extern(c) fun isatty(fd: i32) -> i32; - -extern(c) fun fork() -> i32; - -extern(c) fun execve( - path: ptr, - argv: ptr>, - envp: ptr> -) -> i32; diff --git a/tools/check_std_policy.py b/tools/check_std_policy.py index f134b0f3..02761000 100644 --- a/tools/check_std_policy.py +++ b/tools/check_std_policy.py @@ -72,7 +72,7 @@ def tokens(source): def violations(path, source): items = list(tokens(source)) values = [value for _, value, _ in items] - std = path.startswith('std/') and not path.startswith('std/libc/') + std = path.startswith('std/') for i, (kind, value, line) in enumerate(items): if kind != 'word': continue @@ -81,8 +81,10 @@ def violations(path, source): yield line, 'extern(c) found outside approved C ABI providers' if std and value == 'import' and values[i + 1:i + 2] == ['('] and i + 2 < len(items): module = items[i + 2] - if module[0] == 'string' and module[1].startswith('std::libc::'): - yield line, 'std::libc import found outside std/libc' + if module[0] == 'string' and ( + module[1] == 'std::libc' or module[1].startswith('std::libc::') + ): + yield line, 'removed std::libc import found in std' if value == 'let' and (i == 0 or items[i - 1][2] < line or values[i - 1] in ('(', '{', ';', '}')): j = i + 1 + (values[i + 1:i + 2] == ['mut']) if j + 1 < len(items) and items[j][0] == 'word' and values[j + 1] == ':': diff --git a/tools/test_release_gates.py b/tools/test_release_gates.py index b7692771..27aefb23 100644 --- a/tools/test_release_gates.py +++ b/tools/test_release_gates.py @@ -226,10 +226,23 @@ def test_layout_and_nested_comments_do_not_bypass_binding_rules(self): self.assertFalse(list(policy.violations('std/math/test.wave', '// extern(c)\n/* import("std::libc::io") */\nvar x: str = "extern(c)";'))) self.assertFalse(list(policy.violations('std/sys/linux/event.wave', 'extern /* x */ (c) fun f();'))) - self.assertFalse(list(policy.violations('std/libc/io.wave', 'import ("std::libc::io"); extern(c) fun f();'))) self.assertTrue(list(policy.violations('tests/case.wave', 'fun main() { let /* x */ mut a: i32; }'))) self.assertTrue(list(policy.violations('tests/case.wave', '#[target(os="linux")]\nlet a: i32;'))) + def test_removed_binding_directory_does_not_bypass_policy(self): + for source in ('extern(c) fun f();', 'import("std::libc::stdio")::{puts};', + 'import("std::libc");'): + with self.subTest(source=source): + self.assertTrue(list(policy.violations('std/libc/io.wave', source))) + + def test_os_c_abi_providers_retain_their_binding_permissions(self): + for path in ('std/sys/linux/amd64/memory.wave', 'std/sys/linux/vector_io.wave', + 'std/sys/macos/arm64/memory.wave', 'std/sys/freebsd/event.wave', + 'std/sys/wasm/io.wave', 'std/sys/wasm64/memory.wave'): + with self.subTest(path=path): + self.assertFalse(list(policy.violations(path, 'extern(c) fun os_api();'))) + self.assertTrue(list(policy.violations(path, 'import("std::libc::stdio");'))) + def test_search_failures_are_distinct_from_no_matches(self): for result, expected in [(FileNotFoundError('rg missing'), 1), (subprocess.CompletedProcess([], 2, '', 'search failed'), 1), From d786655844e1d7571841b20edb318c353b8d1ff0 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Fri, 16 Oct 2026 14:52:49 +0900 Subject: [PATCH 2/3] feat(whale): expand scalar lowering and bounded call validation Resolve the official master dependency to 9eed1958 and adapt format-5 round trips and explicit interpreter resource budgets. Consume canonical scalar aliases from Typed HIR and lower eager bool bitwise operations with verified select and comparison instructions. Exercise direct integer/void calls, recursion, evaluation order, traps and shared execution budgets through the bounded upstream interpreter. Refs: wavefnd/Wave#875 Refs: wavefnd/Wave#915 Refs: wavefnd/Wave#916 Refs: wavefnd/Wave#917 Signed-off-by: LunaStev --- Cargo.lock | 4 +- tests/whale_backend.rs | 72 +++++- whale/src/codegen/expression.rs | 19 ++ whale/src/codegen/mod.rs | 14 +- whale/src/codegen/tests.rs | 1 + whale/src/codegen/tests/float_negation.rs | 3 + whale/src/codegen/tests/oracle.rs | 261 +++++++++++++++++++++- whale/src/codegen/tests/scalar_support.rs | 135 +++++++++++ 8 files changed, 499 insertions(+), 10 deletions(-) create mode 100644 whale/src/codegen/tests/scalar_support.rs diff --git a/Cargo.lock b/Cargo.lock index ccade7c6..2cfe9e89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ dependencies = [ [[package]] name = "ir" version = "0.1.0" -source = "git+https://github.com/wavefnd/Whale.git?branch=master#06577d657a242ffdd60f07db5bd788d110fc66ab" +source = "git+https://github.com/wavefnd/Whale.git?branch=master#9eed1958d37a02000c440a5efcbf87beeb60aa7a" dependencies = [ "whale-target", ] @@ -238,4 +238,4 @@ dependencies = [ [[package]] name = "whale-target" version = "0.1.0" -source = "git+https://github.com/wavefnd/Whale.git?branch=master#06577d657a242ffdd60f07db5bd788d110fc66ab" +source = "git+https://github.com/wavefnd/Whale.git?branch=master#9eed1958d37a02000c440a5efcbf87beeb60aa7a" diff --git a/tests/whale_backend.rs b/tests/whale_backend.rs index b85d0b6f..88d99cf6 100644 --- a/tests/whale_backend.rs +++ b/tests/whale_backend.rs @@ -61,7 +61,7 @@ fun main() -> i32 { "#, ); let ir = case.ir(); - assert!(ir.contains("format_version 4"), "{ir}"); + assert!(ir.contains("format_version 5"), "{ir}"); assert!(ir.contains("x86_64-whale-linux"), "{ir}"); assert!(ir.contains("phi"), "{ir}"); assert!(ir.contains("call"), "{ir}"); @@ -140,6 +140,76 @@ fun main() -> i32 { } } +#[test] +fn scalar_aliases_and_eager_bool_bitwise_operations_reach_cli_ir() { + let case = Case::new( + "scalar-aliases", + r#" +type Number = i32; +type Count = Number; +type Flag = bool; +fun combine(a: Flag, b: Flag) -> Flag { return (a & b) | (a ^ b); } +type ByteAlias = byte; +type UnusedFloat = f64; +fun narrow(n: Count) -> Number { return (n as ByteAlias) as Number; } +fun main() -> Number { + if (combine(true, false)) { return narrow(256); } + return 1; +} +"#, + ); + let ir = case.ir(); + assert!(ir.contains("select bool"), "{ir}"); + assert!(ir.contains("icmp ne bool"), "{ir}"); + assert!(ir.contains("trunc i32"), "{ir}"); + assert!(ir.contains("zext u8"), "{ir}"); + for args in [["--whale", "check", "main.wave"], ["check", "main.wave", "--error-format=json"]] { + let output = case.run(&args); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + } +} + +#[test] +fn unsupported_alias_targets_and_declarations_keep_source_diagnostics_and_previous_ir() { + let case = Case::new("unsupported-alias", ""); + for (source, expected) in [ + ("type Values = array;\nfun main() -> i32 { return 0; }\n", "type Array"), + ("type Pointer = ptr;\nfun main() -> i32 { return 0; }\n", "type Pointer"), + ("type Wide = u256;\nfun main() -> i32 { return 0; }\n", "type Uint(256)"), + ("const LIMIT: i32 = 3;\nfun main() -> i32 { return LIMIT; }\n", "non-function top-level"), + ] { + fs::write(case.root.join("main.wave"), source).unwrap(); + fs::write(case.root.join("main.wir"), "previous verified IR").unwrap(); + for args in [ + vec!["--whale", "check", "main.wave", "--error-format=json"], + vec!["--whale", "build", "main.wave", "--emit=ir", "--error-format=json"], + ] { + let output = case.run(&args); + assert!(!output.status.success(), "{source}"); + let stderr = String::from_utf8_lossy(&output.stderr); + let diagnostic = wson::parse_json(stderr.trim()).unwrap(); + let span = diagnostic.get("error").unwrap().get("span").unwrap(); + let name = source.split_whitespace().nth(1).unwrap().trim_end_matches(':'); + let start = source.find(name).unwrap() as f64; + assert_eq!(span.get_num("start"), Some(start), "{stderr}"); + assert_eq!(span.get_num("line"), Some(1.0), "{stderr}"); + assert!(span.get_num("end").unwrap() > start, "{stderr}"); + for text in [expected, "main.wave", "E4001"] { + assert!(stderr.contains(text), "{stderr}"); + } + assert!(!stderr.contains("internal compiler error"), "{stderr}"); + assert_eq!( + fs::read_to_string(case.root.join("main.wir")).unwrap(), + "previous verified IR" + ); + } + fs::remove_file(case.root.join("main.wir")).unwrap(); + let output = case.run(&["--whale", "build", "main.wave", "--emit=ir"]); + assert!(!output.status.success(), "{source}"); + assert!(!case.root.join("main.wir").exists()); + } +} + #[test] fn untyped_integer_condition_literals_retain_their_values_in_whale_ir() { let case = Case::new( diff --git a/whale/src/codegen/expression.rs b/whale/src/codegen/expression.rs index 3a107b3e..c688a03f 100644 --- a/whale/src/codegen/expression.rs +++ b/whale/src/codegen/expression.rs @@ -241,6 +241,25 @@ impl Lowerer<'_> { if lhs.ty != ty || rhs.ty != ty { return Err(ice("binary operands disagree with HIR computation type")); } + if ty == Type::Bool && matches!(op, BitwiseAnd | BitwiseOr | BitwiseXor) { + // The expression walker has evaluated both operands once, in + // source order. Bool is not an upstream integer Bin type; select + // the already-computed values rather than introducing lazy flow. + if matches!(op, BitwiseXor) { + return self.icmp(ICmpPred::Ne, lhs, rhs); + } + let value = self.value(Type::Bool); + let (on_true, on_false) = + if matches!(op, BitwiseAnd) { (rhs.id, lhs.id) } else { (lhs.id, rhs.id) }; + self.emit(Instruction::Select { + dst: value.id, + ty: Type::Bool, + cond: lhs.id, + on_true, + on_false, + }); + return Ok(value); + } let float = matches!(ty, Type::F32 | Type::F64); if *result == Type::Bool { if float { diff --git a/whale/src/codegen/mod.rs b/whale/src/codegen/mod.rs index 37c32344..bb15445c 100644 --- a/whale/src/codegen/mod.rs +++ b/whale/src/codegen/mod.rs @@ -63,8 +63,16 @@ pub(crate) fn lower(program: &TypedProgram) -> Result { let mut functions = HashMap::new(); for node in program.syntax() { let result = (|| { - let ASTNode::Function(f) = node.unspanned() else { - return Err(unsupported("non-function top-level declarations")); + let f = match node.unspanned() { + ASTNode::Function(function) => function, + ASTNode::TypeAlias(alias) => { + // Typed HIR already resolved aliases, including chains. + // Validate this backend's type subset without rebuilding + // an alias environment or emitting a runtime declaration. + scalar(&alias.target)?; + return Ok(()); + }, + _ => return Err(unsupported("non-function top-level declarations")), }; if f.is_async || f.export.is_some() { return Err(unsupported("async or exported functions")); @@ -94,7 +102,7 @@ pub(crate) fn lower(program: &TypedProgram) -> Result { })?; } for node in program.syntax() { - let ASTNode::Function(f) = node.unspanned() else { unreachable!() }; + let ASTNode::Function(f) = node.unspanned() else { continue }; let function = Lowerer::function(program, &functions, f).map_err(|mut e| { if e.span.is_none() { e.span = f.span.clone().or_else(|| node.span().cloned()); diff --git a/whale/src/codegen/tests.rs b/whale/src/codegen/tests.rs index 3542cf35..632a53a1 100644 --- a/whale/src/codegen/tests.rs +++ b/whale/src/codegen/tests.rs @@ -4,6 +4,7 @@ use utils::const_int::ConstInt; mod float_negation; mod oracle; +mod scalar_support; fn module(source: &str) -> Module { let tokens = lexer::Lexer::new_with_file(source, "numeric.wave").tokenize().unwrap(); diff --git a/whale/src/codegen/tests/float_negation.rs b/whale/src/codegen/tests/float_negation.rs index 90738322..dfbe0798 100644 --- a/whale/src/codegen/tests/float_negation.rs +++ b/whale/src/codegen/tests/float_negation.rs @@ -88,6 +88,9 @@ fn floating_negation_flips_only_the_sign_bit_and_double_negation_restores_payloa for pattern in &patterns { let options = InterpreterOptions { max_steps: 100, + max_call_depth: 1, + max_frames: 1, + max_live_values: 128, ir_limits: IrLimits { max_nodes: 1000, ..IrLimits::default() }, memory_limits: MemoryLimits { max_bytes: 64, diff --git a/whale/src/codegen/tests/oracle.rs b/whale/src/codegen/tests/oracle.rs index 6fbecb1c..2d49faac 100644 --- a/whale/src/codegen/tests/oracle.rs +++ b/whale/src/codegen/tests/oracle.rs @@ -1,12 +1,16 @@ // SPDX-License-Identifier: MPL-2.0 //! Execute the supported integer subset with the upstream bounded interpreter. -//! Float arithmetic and calls are explicit unsupported outcomes, not successes. +//! Direct integer/void calls share execution budgets. Floating arithmetic and +//! external calls remain explicit unsupported outcomes, not successes. use super::{mask, module}; use whale_ir::*; fn options() -> InterpreterOptions { InterpreterOptions { max_steps: 10_000, + max_call_depth: 32, + max_frames: 256, + max_live_values: 8192, ir_limits: IrLimits { max_nodes: 20_000, ..IrLimits::default() }, memory_limits: MemoryLimits { max_bytes: 4096, @@ -249,14 +253,263 @@ fn budgets_unsupported_operations_and_traps_have_distinct_outcomes() { interpret_with_options(&ir, ir.functions[0].id, &[ConstValue::I(0)], limited), Err(InterpreterError::MemoryLimit { .. }) )); - let ir = module("fun probe(n: i32) -> i32 { return helper(n); } fun helper(n: i32) -> i32 { return n + 1; }"); + let ir = module("fun probe(n: i32) -> i32 { var f: f64 = n as f64; return f as i32; }"); assert!(matches!( run(&ir, &[ConstValue::I(0)]), Err(InterpreterError::UnsupportedInstruction { .. }) )); - let ir = module("fun probe(n: i32) -> i32 { var f: f64 = n as f64; return f as i32; }"); +} + +#[test] +fn nested_direct_calls_preserve_integer_widths_and_argument_order() { + for bits in [8, 16, 32, 64, 128] { + for signed in [false, true] { + let ty = format!("{}{bits}", if signed { "i" } else { "u" }); + let ir = module(&format!( + "fun probe(n: {ty}) -> {ty} {{ return outer(n, 3); }} + fun outer(a: {ty}, b: {ty}) -> {ty} {{ return leaf(a - b); }} + fun leaf(n: {ty}) -> {ty} {{ return n + 1; }}" + )); + for raw in [0, 1, mask(bits), 1 << (bits - 1)] { + let expected = raw.wrapping_sub(3).wrapping_add(1); + assert_eq!( + run(&ir, &[integer(raw, bits, signed)]).unwrap().value, + Some(integer(expected, bits, signed)), + "{ty}({raw})" + ); + } + } + } +} + +#[test] +fn recursive_calls_keep_each_invocations_locals_until_return() { + let ir = module( + r#" +fun probe(n: u32) -> u32 { return sum(n); } +fun sum(n: u32) -> u32 { + var saved: u32 = n; + if (n == 0) { return 0; } + return saved + sum(n - 1); +} +"#, + ); + for n in [0, 1, 2, 6, 12] { + assert_eq!( + run(&ir, &[ConstValue::U(n)]).unwrap().value, + Some(ConstValue::U(n * (n + 1) / 2)) + ); + } +} + +#[test] +fn grouped_and_returned_void_calls_execute_once_even_without_results() { + let ir = module( + r#" +fun probe(n: u64) { + ((grouped(n))); + return ((returned(n))); +} +fun grouped(n: u64) { ((leaf(n))); } +fun returned(n: u64) { return ((leaf(n))); } +fun leaf(n: u64) { var shifted: u8 = (1 as u8) << n; } +"#, + ); + // Entry + two wrappers + two leaves. A skipped call needs fewer frames; + // duplicated evaluation exceeds this budget even when its result is void. + let exact = InterpreterOptions { max_call_depth: 3, max_frames: 5, ..options() }; + assert_eq!( + interpret_with_options(&ir, ir.functions[0].id, &[ConstValue::U(0)], exact).unwrap().value, + None + ); + let returned = ir.functions.iter().find(|f| f.name == "returned").unwrap().id; + assert!(matches!( + interpret_with_options( + &ir, + ir.functions[0].id, + &[ConstValue::U(0)], + InterpreterOptions { max_frames: 4, ..exact } + ), + Err(InterpreterError::CallLimit { site, resource: CallResource::Frames, limit: 4 }) + if site.function == returned + )); + let leaf = ir.functions.iter().find(|f| f.name == "leaf").unwrap().id; + assert!(matches!( + run(&ir, &[ConstValue::U(8)]), + Err(InterpreterError::Trap(InterpreterTrap { site, reason: TrapReason::Explicit(ref reason), .. })) + if site.function == leaf && reason == "shift count out of range" + )); +} + +#[test] +fn short_circuiting_skips_calls_but_reached_unused_results_still_trap() { + for (operator, skipped, entered) in [("&&", false, true), ("||", true, false)] { + let ir = module(&format!( + "fun probe(flag: bool, n: u64) -> bool {{ return flag {operator} helper(n); }} + fun helper(n: u64) -> bool {{ return ((1 as u8) << n) != 0; }}" + )); + let only_entry = InterpreterOptions { max_call_depth: 1, max_frames: 1, ..options() }; + assert_eq!( + interpret_with_options( + &ir, + ir.functions[0].id, + &[ConstValue::Bool(skipped), ConstValue::U(8)], + only_entry + ) + .unwrap() + .value, + Some(ConstValue::Bool(skipped)) + ); + assert!(matches!( + run(&ir, &[ConstValue::Bool(entered), ConstValue::U(8)]), + Err(InterpreterError::Trap(InterpreterTrap { reason: TrapReason::Explicit(ref reason), .. })) + if reason == "shift count out of range" + )); + assert_eq!( + run(&ir, &[ConstValue::Bool(entered), ConstValue::U(0)]).unwrap().value, + Some(ConstValue::Bool(true)) + ); + } + let ir = module( + "fun probe(n: u64) -> i32 { helper(n); return 7; } + fun helper(n: u64) -> u8 { return (1 as u8) << n; }", + ); + assert_eq!(run(&ir, &[ConstValue::U(0)]).unwrap().value, Some(ConstValue::I(7))); + assert!(matches!(run(&ir, &[ConstValue::U(8)]), Err(InterpreterError::Trap(_)))); +} + +#[test] +fn calls_share_fuel_memory_and_distinct_frame_and_live_value_limits() { + let ir = module( + "fun probe(n: u32) -> u32 { var first: u32 = leaf(n); return leaf(first); } + fun leaf(n: u32) -> u32 { var copy: u32 = n; return copy + 1; }", + ); + let entry = ir.functions[0].id; + let arguments = [ConstValue::U(3)]; + let exact = InterpreterOptions { max_call_depth: 2, max_frames: 3, ..options() }; + let complete = interpret_with_options(&ir, entry, &arguments, exact).unwrap(); + assert_eq!(complete.value, Some(ConstValue::U(5))); + for (limited, resource, limit) in [ + (InterpreterOptions { max_call_depth: 1, ..exact }, CallResource::Depth, 1), + (InterpreterOptions { max_frames: 2, ..exact }, CallResource::Frames, 2), + ] { + assert!(matches!( + interpret_with_options(&ir, entry, &arguments, limited), + Err(InterpreterError::CallLimit { site, resource: actual, limit: actual_limit }) + if site.function == entry && actual == resource && actual_limit == limit + )); + } + let required_values: usize = ir.functions.iter().map(|f| f.value_types.len()).sum(); + assert_eq!( + interpret_with_options( + &ir, + entry, + &arguments, + InterpreterOptions { max_live_values: required_values, ..exact } + ) + .unwrap() + .value, + complete.value + ); + assert!(matches!( + interpret_with_options( + &ir, + entry, + &arguments, + InterpreterOptions { max_live_values: required_values - 1, ..exact } + ), + Err(InterpreterError::CallLimit { site, resource: CallResource::Values, limit }) + if site.function == entry && limit == (required_values - 1) as u64 + )); + let fuel = complete.steps - 1; + assert!(matches!( + interpret_with_options( + &ir, + entry, + &arguments, + InterpreterOptions { max_steps: fuel, ..exact } + ), + Err(InterpreterError::StepLimit { limit, .. }) if limit == fuel + )); + // Each individual function fits, but their allocations share one arena + // and retired invocation allocations remain charged to its total budget. + let memory = InterpreterOptions { + memory_limits: MemoryLimits { max_bytes: 16, ..exact.memory_limits }, + ..exact + }; + let leaf = ir.functions.iter().find(|f| f.name == "leaf").unwrap().id; + assert_eq!( + interpret_with_options(&ir, leaf, &arguments, memory).unwrap().value, + Some(ConstValue::U(4)) + ); + assert!(matches!( + interpret_with_options(&ir, entry, &arguments, memory), + Err(InterpreterError::MemoryLimit { + site, + resource: MemoryResource::Bytes, + limit: 16, + }) if site.function == leaf + )); +} + +#[test] +fn recursive_calls_exhaust_shared_depth_and_total_frame_budgets() { + let ir = module( + "fun probe(n: u32) -> u32 { + if (n == 0) { return 0; } + return n + probe(n - 1); + }", + ); + let entry = ir.functions[0].id; + for (limited, resource) in [ + (InterpreterOptions { max_call_depth: 4, ..options() }, CallResource::Depth), + (InterpreterOptions { max_frames: 4, ..options() }, CallResource::Frames), + ] { + assert!(matches!( + interpret_with_options(&ir, entry, &[ConstValue::U(5)], limited), + Err(InterpreterError::CallLimit { site, resource: actual, limit: 4 }) + if site.function == entry && actual == resource + )); + } + let exact = InterpreterOptions { max_call_depth: 6, max_frames: 6, ..options() }; + assert_eq!( + interpret_with_options(&ir, entry, &[ConstValue::U(5)], exact).unwrap().value, + Some(ConstValue::U(15)) + ); +} + +#[test] +fn unsupported_callees_are_rejected_before_runtime_effects() { + let ir = module( + "fun probe(n: u64) -> i32 { + var shifted: u8 = (1 as u8) << n; + return helper(); + } + fun helper() -> i32 { var f: f64 = 1.0; return f as i32; }", + ); + let helper = ir.functions.iter().find(|f| f.name == "helper").unwrap().id; + assert!(matches!( + run(&ir, &[ConstValue::U(8)]), + Err(InterpreterError::UnsupportedInstruction { site, .. }) if site.function == helper + )); + + let mut ir = module( + "fun probe(n: i32) -> i32 { return helper(n); } + fun helper(n: i32) -> i32 { return n + 1; }", + ); + let helper = ir.functions.iter().find(|f| f.name == "helper").unwrap().id; + // The Wave adapter still rejects external definitions. Turn a generated + // callee into a verifier-valid declaration to test the oracle boundary. + ir.functions.retain(|f| f.id != helper); + let declaration = ir.declarations.iter_mut().find(|d| d.id == helper).unwrap(); + declaration.linkage = Linkage::External; + declaration.link_name = Some("external_helper".into()); + verify_module(&ir).unwrap(); + let text = print_module(&ir); + let ir = parse_module(&text).unwrap(); + assert_eq!(print_module(&ir), text); assert!(matches!( run(&ir, &[ConstValue::I(0)]), - Err(InterpreterError::UnsupportedInstruction { .. }) + Err(InterpreterError::UnsupportedInstruction { operation: "call", .. }) )); } diff --git a/whale/src/codegen/tests/scalar_support.rs b/whale/src/codegen/tests/scalar_support.rs new file mode 100644 index 00000000..8a552726 --- /dev/null +++ b/whale/src/codegen/tests/scalar_support.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Preserve typed scalar aliases and eager boolean bitwise evaluation. +use super::module; +use whale_ir::*; + +fn options() -> InterpreterOptions { + InterpreterOptions { + max_steps: 2000, + max_call_depth: 16, + max_frames: 64, + max_live_values: 4096, + ir_limits: IrLimits { max_nodes: 10_000, ..IrLimits::default() }, + memory_limits: MemoryLimits { + max_bytes: 4096, + max_allocations: 128, + max_pointer_fragments: 128, + max_work: 100_000, + }, + } +} + +fn run(module: &Module, args: &[ConstValue]) -> Option { + interpret_with_options(module, module.functions[0].id, args, options()).unwrap().value +} + +#[test] +fn bool_bitwise_truth_tables_execute_without_invalid_bool_bin_instructions() { + for operator in ["&", "|", "^"] { + let ir = + module(&format!("fun probe(a: bool, b: bool) -> bool {{ return a {operator} b; }}")); + for left in [false, true] { + for right in [false, true] { + let expected = match operator { + "&" => left & right, + "|" => left | right, + "^" => left ^ right, + _ => unreachable!(), + }; + assert_eq!( + run(&ir, &[ConstValue::Bool(left), ConstValue::Bool(right)]), + Some(ConstValue::Bool(expected)), + "{left} {operator} {right}" + ); + } + } + assert!(!ir.functions[0] + .blocks + .iter() + .flat_map(|b| &b.instructions) + .any(|instruction| { matches!(instruction, Instruction::Bin { ty: Type::Bool, .. }) })); + } +} + +#[test] +fn bitwise_operands_execute_once_in_order_and_remain_distinct_from_logical_operators() { + for (operator, left, expected) in + [("&", false, 2), ("|", true, 3), ("^", false, 3), ("&&", false, 1), ("||", true, 2)] + { + let ir = module(&format!( + r#" +fun probe() -> i32 {{ + var count: i32 = 0; + var result: bool = ((count = count + 1) > 0 && {left}) + {operator} ((count = count * 2) > 0); + return count + result as i32; +}} +"# + )); + assert_eq!(run(&ir, &[]), Some(ConstValue::I(expected)), "{operator}"); + } +} + +#[test] +fn eager_bitwise_evaluates_a_trapping_rhs_that_logical_operators_skip() { + for operator in ["&", "|", "^", "&&", "||"] { + let left = operator == "|" || operator == "||"; + let ir = module(&format!( + "fun probe(n: i32) -> bool {{ return {left} {operator} (((1 as i32) << n) > 0); }}" + )); + let result = + interpret_with_options(&ir, ir.functions[0].id, &[ConstValue::I(32)], options()); + if matches!(operator, "&&" | "||") { + assert_eq!(result.unwrap().value, Some(ConstValue::Bool(left))); + } else { + assert!(matches!(result, Err(InterpreterError::Trap(_))), "{operator}: {result:?}"); + } + } +} + +#[test] +fn scalar_aliases_and_chains_leave_canonical_ir_and_execution_unchanged() { + let aliases = module( + r#" +type Number = i32; +type NumberAgain = Number; +type Flag = bool; +type Unused = f64; +fun probe(n: NumberAgain) -> Number { + var flag: Flag = n > 0; + var narrowed: NumberAgain = (n as u8) as Number; + if (flag) { return narrowed + 1; } + return narrowed; +} +"#, + ); + let canonical = module( + r#" +fun probe(n: i32) -> i32 { + var flag: bool = n > 0; + var narrowed: i32 = (n as u8) as i32; + if (flag) { return narrowed + 1; } + return narrowed; +} +"#, + ); + assert_eq!(print_module(&aliases), print_module(&canonical)); + for (input, expected) in [(-1, 255), (0, 0), (256, 1), (257, 2)] { + assert_eq!(run(&aliases, &[ConstValue::I(input)]), Some(ConstValue::I(expected))); + } +} + +#[test] +fn every_supported_value_scalar_alias_retains_its_canonical_signature() { + for ty in [ + "i8", "i16", "i32", "i64", "i128", "u8", "u16", "u32", "u64", "u128", "byte", "bool", + "f32", "f64", + ] { + let aliases = module(&format!( + "type Scalar = {ty}; type Chained = Scalar;\n\ + fun probe(n: Chained) -> Scalar {{ return n; }}" + )); + let canonical = module(&format!("fun probe(n: {ty}) -> {ty} {{ return n; }}")); + assert_eq!(print_module(&aliases), print_module(&canonical), "{ty}"); + } +} From d2742633af4fcc70cccb0a38a3a0edc3a8151574 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Fri, 16 Oct 2026 16:35:44 +0900 Subject: [PATCH 3/3] ci: reuse stable macOS LLVM provisioning for packages Use the same LLVM prerequisite as the successful macOS build/test lanes instead of refreshing Homebrew before packaging. Disable automatic metadata updates and upgrades of installed requested formulae while retaining installation, version checks and existing timeout budgets. Cover both macOS architectures and all shared lanes, failure propagation, wrong LLVM majors and non-provisioning behavior in tooling regressions. Refs: wavefnd/Wave#925 Signed-off-by: LunaStev --- tools/ci/build.py | 4 +++ tools/ci/plans.json | 2 +- tools/ci/procedures.json | 6 ---- tools/test_ci.py | 77 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/tools/ci/build.py b/tools/ci/build.py index 9c4eddb5..060a659a 100644 --- a/tools/ci/build.py +++ b/tools/ci/build.py @@ -56,6 +56,10 @@ def llvm_setup(r, operation): ) return if r.target.host_os == "macos": + # Keep the runner's formula metadata and valid LLVM 21 installation. + # Refreshing Homebrew can replace available Intel bottles with source builds. + r.setenv("HOMEBREW_NO_AUTO_UPDATE", "1") + r.setenv("HOMEBREW_NO_INSTALL_UPGRADE", "1") if operation.get("macos_update"): r.run(["brew", "update"]) r.run(["brew", "install", "llvm@" + LLVM_MAJOR, "lld@" + LLVM_MAJOR]) diff --git a/tools/ci/plans.json b/tools/ci/plans.json index 0f6e4526..d7169bf2 100644 --- a/tools/ci/plans.json +++ b/tools/ci/plans.json @@ -1741,7 +1741,7 @@ "id": "stage-3", "name": "Install LLVM 21 and LLD", "condition": "", - "procedure": "install_llvm_21_and_lld_2", + "procedure": "install_llvm_21_and_lld", "phase": "prerequisites", "timeout_seconds": 1800 }, diff --git a/tools/ci/procedures.json b/tools/ci/procedures.json index be3e7de4..e2f50dca 100644 --- a/tools/ci/procedures.json +++ b/tools/ci/procedures.json @@ -1715,12 +1715,6 @@ "env": {}, "operation": "riscv_package" }, - "install_llvm_21_and_lld_2": { - "env": {}, - "operation": "llvm_setup", - "packages": [], - "macos_update": true - }, "execute_complete_windows_case_selection": { "env": {}, "operation": "commands", diff --git a/tools/test_ci.py b/tools/test_ci.py index 5d9a334b..5f5964dd 100644 --- a/tools/test_ci.py +++ b/tools/test_ci.py @@ -19,6 +19,83 @@ from tools.ci import common, targets, build, package, release, test as ci_test +class MacOSProvisioningTests(unittest.TestCase): + def runner(self, target_id, *, provision=True): + runner = SimpleNamespace( + target=targets.resolve(target_id), + provision=provision, + env={"PATH": os.defpath}, + run=Mock(return_value=""), + addpath=Mock(), + ) + runner.setenv = Mock(side_effect=lambda name, value: runner.env.update({name: value})) + return runner + + def test_macos_lanes_install_without_refreshing_or_upgrading_homebrew(self): + plans, procedures = common.inventory() + for lane in ( + "rust/build-macos-amd64", "rust/build-macos-arm64", + "cases/cases-macos", "release/package-macos", + ): + setups = [ + procedures[stage["procedure"]] + for stage in plans[lane]["stages"] + if procedures[stage["procedure"]]["operation"] == "llvm_setup" + ] + self.assertEqual(len(setups), 1, lane) + self.assertFalse(setups[0].get("macos_update"), lane) + for target_id in ("macos-amd64", "macos-arm64"): + with self.subTest(lane=lane, target=target_id): + runner = self.runner(target_id) + observed = [] + + def run(command): + observed.append(( + command, runner.env["HOMEBREW_NO_AUTO_UPDATE"], + runner.env["HOMEBREW_NO_INSTALL_UPGRADE"], + )) + return "" + + runner.run.side_effect = run + with patch.object(build, "llvm_config") as configure: + build.llvm_setup(runner, setups[0]) + self.assertEqual(observed, [( + ["brew", "install", "llvm@21", "lld@21"], "1", "1", + )]) + configure.assert_called_once_with(runner, setups[0]) + + def test_brew_failure_or_timeout_stops_before_configuration(self): + for error in (RuntimeError("brew failed"), subprocess.TimeoutExpired(["brew"], 30)): + with self.subTest(error=type(error).__name__): + runner = self.runner("macos-amd64") + runner.run.side_effect = error + with patch.object(build, "llvm_config") as configure: + with self.assertRaises(type(error)): + build.llvm_setup(runner, {}) + configure.assert_not_called() + + def test_macos_provisioning_still_rejects_the_wrong_llvm_major(self): + for target_id in ("macos-amd64", "macos-arm64"): + with self.subTest(target=target_id): + runner = self.runner(target_id) + runner.run.side_effect = ["", "/opt/llvm", "/opt/lld", "20.1.8"] + with self.assertRaisesRegex(ValueError, "expected LLVM 21, found 20.1.8"): + build.llvm_setup(runner, {}) + self.assertEqual( + runner.run.call_args.args[0], + [Path("/opt/llvm/bin/llvm-config"), "--version"], + ) + + def test_without_provision_only_checks_existing_tools(self): + runner = self.runner("macos-amd64", provision=False) + with patch.object(build, "llvm_config") as configure, \ + patch.object(build.shutil, "which", return_value="/installed/tool"): + build.llvm_setup(runner, {}) + runner.run.assert_not_called() + runner.setenv.assert_not_called() + configure.assert_called_once_with(runner, {}) + + class PlatformSmokeTests(unittest.TestCase): def test_windows_object_reports_compiler_failure_even_with_existing_artifact(self): with tempfile.TemporaryDirectory() as directory: