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/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/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/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/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: 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), 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}"); + } +}