diff --git a/packages/hardhat-slang/README.md b/packages/hardhat-slang/README.md index 3985e4d5267..441d91401bf 100644 --- a/packages/hardhat-slang/README.md +++ b/packages/hardhat-slang/README.md @@ -30,7 +30,7 @@ export default defineConfig({ }, }, slang: { - version: "0.1.0-pre.2026-10-01", + version: "0.1.0-pre.2026-10-14", }, }); ``` @@ -82,7 +82,7 @@ export default defineConfig({ }, }, slang: { - version: "0.1.0-pre.2026-10-01", + version: "0.1.0-pre.2026-10-14", }, }); ``` @@ -108,7 +108,7 @@ export default defineConfig({ }, }, slang: { - version: "0.1.0-pre.2026-10-01", + version: "0.1.0-pre.2026-10-14", dangerouslyAllowSlangInProduction: true, }, }); @@ -159,18 +159,18 @@ export default defineConfig({ }, }, slang: { - version: "0.1.0-pre.2026-10-01", + version: "0.1.0-pre.2026-10-14", }, }); ``` ### Supported Solidity versions -Each slang release supports a range of Solidity versions: +Each slang release supports a range of Solidity versions. A compiler entry's `version` picks the Solidity version within that range, and the plugin passes it to slang as `settings.solidityVersion`. | slang release | Solidity versions | Notes | | --- | --- | --- | -| `0.1.0-pre.2026-10-01` | `0.8.0` to `0.8.37` | Internal prerelease (solx tag `b74af542`, 2026-10-01) downloaded from GitHub. | +| `0.1.0-pre.2026-10-14` | `0.8.0` to `0.8.37` | Internal prerelease (solx tag `5e6ec5e1`, 2026-10-14) downloaded from GitHub. | ### EVM version support diff --git a/packages/hardhat-slang/src/internal/constants.ts b/packages/hardhat-slang/src/internal/constants.ts index 53c896779f9..bdd315ce1a6 100644 --- a/packages/hardhat-slang/src/internal/constants.ts +++ b/packages/hardhat-slang/src/internal/constants.ts @@ -48,11 +48,6 @@ export interface SlangRelease { maxSolidity: string; /** CLI arguments passed on every compile, after `--standard-json`. */ extraArgs: readonly string[]; - /** - * The CLI flag that receives the compilation job's Solidity version, e.g. - * `--solidity-version`. Absent when the release doesn't accept one. - */ - targetVersionFlag?: string; /** * Where to download the release from, when it isn't served by the mirror * under the standard `v${version}` asset naming. @@ -68,14 +63,14 @@ export interface SlangRelease { */ // TODO: revisit before release to finalize. export const SLANG_RELEASES: Record = { - "0.1.0-pre.2026-10-01": { + "0.1.0-pre.2026-10-14": { minSolidity: "0.8.0", maxSolidity: "0.8.37", extraArgs: ["--no-import-callback"], assetOverride: { baseUrl: - "https://github.com/NomicFoundation/solx/releases/download/b74af542", - assetSuffix: "slang-2026-10-01", + "https://github.com/NomicFoundation/solx/releases/download/5e6ec5e1", + assetSuffix: "debug-fast", }, }, }; diff --git a/packages/hardhat-slang/src/internal/downloader.ts b/packages/hardhat-slang/src/internal/downloader.ts index 4d57de28d99..2509af05f0e 100644 --- a/packages/hardhat-slang/src/internal/downloader.ts +++ b/packages/hardhat-slang/src/internal/downloader.ts @@ -77,7 +77,7 @@ export interface DownloadSlangOptions { * Downloads the binary of the given slang release if not already cached. * Returns the path to the binary on disk. * - * @param slangVersion - The slang version to download (e.g. "0.1.0-pre.2026-10-01") + * @param slangVersion - The slang version to download (e.g. "0.1.0-pre.2026-10-14") * @param release - The release table row for that version * @param onBinaryDownloadStart - A callback invoked once the compiler download is about to start * @param options - See {@link DownloadSlangOptions}. diff --git a/packages/hardhat-slang/src/internal/hook-handlers/config.ts b/packages/hardhat-slang/src/internal/hook-handlers/config.ts index 3e520320549..6b26fee442f 100644 --- a/packages/hardhat-slang/src/internal/hook-handlers/config.ts +++ b/packages/hardhat-slang/src/internal/hook-handlers/config.ts @@ -33,7 +33,7 @@ import { SUPPORTED_SLANG_EVM_VERSIONS, SUPPORTED_SLANG_OPTIMIZER_MODES, } from "../constants.js"; -import { addSlangDebugInfoSelectors } from "../slang-compiler.js"; +import { addSlangStackTraceSelectors } from "../slang-compiler.js"; const log = createDebug("hardhat:slang:hook-handlers:config"); @@ -176,7 +176,18 @@ const slangUserConfigType = z .optional(), }) .superRefine((config, ctx) => { - const entries = collectSlangEntriesToDownload(config.solidity); + const slangEntries = collectSlangEntries(config.solidity); + for (const { version, solidityVersion, path } of slangEntries) { + if (solidityVersion !== undefined && solidityVersion !== version) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "settings", "solidityVersion"], + message: `settings.solidityVersion (${String(solidityVersion)}) must match the compiler entry's version (${version}). slang compiles the entry's version; leave settings.solidityVersion unset.`, + }); + } + } + + const entries = slangEntries.filter((entry) => !entry.hasCustomPath); if (entries.length === 0) { return; } @@ -208,30 +219,34 @@ const slangUserConfigType = z } }); -interface SlangEntryToDownload { +interface SlangEntry { version: string; + solidityVersion: unknown; + /** Whether the entry brings its own binary instead of the pinned release. */ + hasCustomPath: boolean; path: Array; } /** - * Collects every compiler entry with `type: "slang"` and no custom `path` - * from any of the shapes `solidity` can take, along with the config path of - * the entry, so that errors point at the right place. + * Collects every compiler entry with `type: "slang"` from any of the shapes + * `solidity` can take, along with the config path of the entry, so that + * errors point at the right place. */ -function collectSlangEntriesToDownload( - solidity: unknown, -): SlangEntryToDownload[] { - const entries: SlangEntryToDownload[] = []; +function collectSlangEntries(solidity: unknown): SlangEntry[] { + const entries: SlangEntry[] = []; function visit(entry: unknown, path: Array): void { - if ( - !isObject(entry) || - entry.type !== SLANG_COMPILER_TYPE || - (typeof entry.path === "string" && entry.path.length > 0) - ) { + if (!isObject(entry) || entry.type !== SLANG_COMPILER_TYPE) { return; } - entries.push({ version: String(entry.version), path }); + entries.push({ + version: String(entry.version), + solidityVersion: isObject(entry.settings) + ? entry.settings.solidityVersion + : undefined, + hasCustomPath: typeof entry.path === "string" && entry.path.length > 0, + path, + }); } function visitVersionsConfig( @@ -310,8 +325,8 @@ export async function resolveUserConfig( ): Promise { const resolvedConfig = await next(userConfig, resolveConfigurationVariable); - // Add slang debugInfo selectors so the cached solcInput, build-info, - // and build-ID hash all include them. + // Add the slang debugInfo and debugSymbols selectors so the cached + // solcInput, build-info, and build-ID hash all include them. const profiles = await augmentSlangOutputSelectionInProfiles( resolvedConfig.solidity.profiles, ); @@ -337,7 +352,8 @@ export async function resolveUserConfig( /** * For each compiler entry whose `type === "slang"`, augments - * `settings.outputSelection` with the slang debugInfo selectors. + * `settings.outputSelection` with the slang debugInfo and debugSymbols + * selectors. * Non-slang entries pass through unchanged. */ async function augmentSlangOutputSelectionInProfiles( @@ -368,7 +384,11 @@ async function augmentSlangOutputSelectionInProfiles( // `CommonSolidityCompilerConfig`), so we use the same here — narrowing // would require type assertions that the repo's eslint config forbids. async function augmentIfSlang< - T extends { type?: string; settings?: Record }, + T extends { + type?: string; + version: string; + settings?: Record; + }, >(entry: T): Promise { if (entry.type !== SLANG_COMPILER_TYPE) { return entry; @@ -382,12 +402,14 @@ async function augmentIfSlang< settings: { ...settings, // Defaults added here instead of in SlangCompiler.compile so this reaches - // the solcInput and hence the build-id hash. + // the solcInput and hence the build-id hash. One slang binary compiles a + // range of Solidity versions, so the entry's version is part of it too. + solidityVersion: entry.version, optimizer: { ...optimizer, mode: optimizer.mode ?? DEFAULT_SLANG_OPTIMIZER_MODE, }, - outputSelection: await addSlangDebugInfoSelectors( + outputSelection: await addSlangStackTraceSelectors( entry.settings?.outputSelection, ), }, diff --git a/packages/hardhat-slang/src/internal/hook-handlers/solidity.ts b/packages/hardhat-slang/src/internal/hook-handlers/solidity.ts index cedc9d3bf7a..cd69ece6031 100644 --- a/packages/hardhat-slang/src/internal/hook-handlers/solidity.ts +++ b/packages/hardhat-slang/src/internal/hook-handlers/solidity.ts @@ -148,7 +148,6 @@ export default async (): Promise> => ({ return new SlangCompiler(customSlangVersion, compilerConfig.path, { release: getReleaseForCustomPath(context.config), - targetSolidityVersion: compilerConfig.version, }); } @@ -165,9 +164,6 @@ export default async (): Promise> => ({ `Creating SlangCompiler for Solidity ${compilerConfig.version} (slang ${slangVersion}) at ${binaryPath}`, ); - return new SlangCompiler(slangVersion, binaryPath, { - release, - targetSolidityVersion: compilerConfig.version, - }); + return new SlangCompiler(slangVersion, binaryPath, { release }); }, }); diff --git a/packages/hardhat-slang/src/internal/platform.ts b/packages/hardhat-slang/src/internal/platform.ts index fdc4e36502a..89c7f680e04 100644 --- a/packages/hardhat-slang/src/internal/platform.ts +++ b/packages/hardhat-slang/src/internal/platform.ts @@ -9,22 +9,20 @@ import { HardhatError } from "@nomicfoundation/hardhat-errors"; * The full asset name is `${baseName}-v${version}` (or `.exe` on Windows), * unless the release overrides the suffix. * - * The slang pipeline ships as the `solx` binary built with its Slang front - * end, so the assets keep the `solx-` prefix for now: - * solx-linux-amd64-gnu-v0.1.4 - * solx-linux-arm64-gnu-v0.1.4 - * solx-macosx-v0.1.4 (universal binary) - * solx-windows-amd64-gnu-v0.1.4.exe + * slang-linux-amd64-gnu-v0.2.0 + * slang-linux-arm64-gnu-v0.2.0 + * slang-macosx-v0.2.0 (universal binary) + * slang-windows-amd64-gnu-v0.2.0.exe */ export function getSlangBinaryBaseName(): string { // TODO: revisit before release to finalize. const platform = os.platform(); const arch = os.arch(); - if (platform === "linux" && arch === "x64") return "solx-linux-amd64-gnu"; - if (platform === "linux" && arch === "arm64") return "solx-linux-arm64-gnu"; - if (platform === "darwin") return "solx-macosx"; - if (platform === "win32" && arch === "x64") return "solx-windows-amd64-gnu"; + if (platform === "linux" && arch === "x64") return "slang-linux-amd64-gnu"; + if (platform === "linux" && arch === "arm64") return "slang-linux-arm64-gnu"; + if (platform === "darwin") return "slang-macosx"; + if (platform === "win32" && arch === "x64") return "slang-windows-amd64-gnu"; throw new HardhatError( HardhatError.ERRORS.HARDHAT_SLANG.GENERAL.UNSUPPORTED_PLATFORM, diff --git a/packages/hardhat-slang/src/internal/slang-compiler.ts b/packages/hardhat-slang/src/internal/slang-compiler.ts index 66aaa9c8504..389c10d50e8 100644 --- a/packages/hardhat-slang/src/internal/slang-compiler.ts +++ b/packages/hardhat-slang/src/internal/slang-compiler.ts @@ -14,15 +14,15 @@ export const SLANG_DEBUG_INFO_SELECTORS: readonly string[] = [ "evm.deployedBytecode.debugInfo", ] as const; +// File-level selectors for the per-source symbol table EDR reads in place of +// the solc AST +export const SLANG_DEBUG_SYMBOLS_SELECTORS: readonly string[] = [ + "debugSymbols", +] as const; + export interface SlangCompilerOptions { /** The release table row of the slang binary being driven. */ release: SlangRelease; - /** - * The Solidity version Hardhat selected for the compilation job. slang - * compiles a range of Solidity versions, so the job's version is passed to - * the binary through the release's `targetVersionFlag`, when it has one. - */ - targetSolidityVersion: string; /** Intended for tests. */ spawnCompile?: typeof defaultSpawnCompile; } @@ -43,36 +43,52 @@ export class SlangCompiler implements Compiler { compilerPath: string, options: SlangCompilerOptions, ) { - const { - release, - targetSolidityVersion, - spawnCompile = defaultSpawnCompile, - } = options; + const { release, spawnCompile = defaultSpawnCompile } = options; this.version = slangVersion; this.longVersion = `${slangVersion}+slang`; this.compilerPath = compilerPath; - this.args = [ - "--standard-json", - ...release.extraArgs, - ...(release.targetVersionFlag !== undefined - ? [release.targetVersionFlag, targetSolidityVersion] - : []), - ]; + this.args = ["--standard-json", ...release.extraArgs]; this.#spawnCompile = spawnCompile; } public async compile(input: CompilerInput): Promise { - return await this.#spawnCompile(this.compilerPath, [...this.args], input); + return await this.#spawnCompile( + this.compilerPath, + [...this.args], + withoutAst(input), + ); } } +/** + * `input` without the `ast` output Hardhat requests from every compiler. + * slang's AST is not the solc AST that Hardhat's consumers read (EDR builds + * slang stack traces from debugSymbols), and on a large project it is most + * of the output Hardhat parses and writes into the build info. + */ +function withoutAst(input: CompilerInput): CompilerInput { + const outputSelection = Object.fromEntries( + Object.entries(input.settings.outputSelection).map(([file, contracts]) => [ + file, + Object.fromEntries( + Object.entries(contracts).map(([contract, selectors]) => [ + contract, + selectors.filter((selector) => selector !== "ast"), + ]), + ), + ]), + ); + return { ...input, settings: { ...input.settings, outputSelection } }; +} + /** * Returns a new outputSelection with the slang debugInfo selectors at - * `["*"]["*"]`. Existing user selectors are preserved; downstream - * `#dedupeAndSortOutputSelection` removes duplicates. + * `["*"]["*"]` and the debugSymbols selectors at `["*"][""]`, the two outputs + * EDR builds slang stack traces from. Existing user selectors are preserved; + * downstream `#dedupeAndSortOutputSelection` removes duplicates. */ -export async function addSlangDebugInfoSelectors( +export async function addSlangStackTraceSelectors( outputSelection: unknown, ): Promise["outputSelection"]> { const seed: Record< @@ -87,11 +103,13 @@ export async function addSlangDebugInfoSelectors( ); // Hardhat normalizes outputSelection to populate `["*"]["*"]` upstream, but - // unit tests construct the input directly with `{}`, so make sure the slot - // exists before we push. + // unit tests construct the input directly with `{}`, so make sure the slots + // exist before we push. cloned["*"] ??= {}; cloned["*"]["*"] ??= []; cloned["*"]["*"].push(...SLANG_DEBUG_INFO_SELECTORS); + cloned["*"][""] ??= []; + cloned["*"][""].push(...SLANG_DEBUG_SYMBOLS_SELECTORS); return cloned; } diff --git a/packages/hardhat-slang/test/config.ts b/packages/hardhat-slang/test/config.ts index 45b1064a5f6..b342724a013 100644 --- a/packages/hardhat-slang/test/config.ts +++ b/packages/hardhat-slang/test/config.ts @@ -11,9 +11,12 @@ import { validateResolvedConfig, validateUserConfig, } from "../src/internal/hook-handlers/config.js"; -import { SLANG_DEBUG_INFO_SELECTORS } from "../src/internal/slang-compiler.js"; +import { + SLANG_DEBUG_INFO_SELECTORS, + SLANG_DEBUG_SYMBOLS_SELECTORS, +} from "../src/internal/slang-compiler.js"; -const PINNED_VERSION = "0.1.0-pre.2026-10-01"; +const PINNED_VERSION = "0.1.0-pre.2026-10-14"; const { minSolidity: PINNED_MIN, maxSolidity: PINNED_MAX } = SLANG_RELEASES[PINNED_VERSION]; @@ -224,7 +227,7 @@ describe("hardhat-slang plugin config resolution", () => { assert.deepEqual(profileNames, ["default"]); }); - it("adds slang debugInfo selectors to slang-typed compilers in resolved config", async () => { + it("adds slang stack-trace selectors to slang-typed compilers in resolved config", async () => { const resolvedConfig = await resolveUserConfig( {}, undefined as any, @@ -258,6 +261,15 @@ describe("hardhat-slang plugin config resolution", () => { wildcardSelectors.includes("abi"), "user-provided selectors must be preserved alongside the augmentation", ); + const fileSelectors = slangCompiler.settings.outputSelection["*"][ + "" + ] as string[]; + for (const selector of SLANG_DEBUG_SYMBOLS_SELECTORS) { + assert.ok( + fileSelectors.includes(selector), + `expected resolved slang compiler config to include file-level "${selector}", got: ${fileSelectors.join(", ")}`, + ); + } }); it("does NOT add slang selectors to non-slang compilers", async () => { @@ -289,6 +301,14 @@ describe("hardhat-slang plugin config resolution", () => { `solc-typed compiler must NOT receive slang selector "${selector}"; got: ${wildcardSelectors.join(", ")}`, ); } + const fileSelectors: string[] = + solcCompiler.settings.outputSelection["*"][""] ?? []; + for (const selector of SLANG_DEBUG_SYMBOLS_SELECTORS) { + assert.ok( + !fileSelectors.includes(selector), + `solc-typed compiler must NOT receive slang selector "${selector}"; got: ${fileSelectors.join(", ")}`, + ); + } }); it("augments slang-typed override entries too", async () => { @@ -324,6 +344,34 @@ describe("hardhat-slang plugin config resolution", () => { } }); + it("gives a slang override the Solidity version of its own entry", async () => { + const resolvedConfig = await resolveUserConfig( + {}, + undefined as any, + makeNext({ + slang: { + isolated: false, + preferWasm: false, + compilers: [{ version: "0.8.34", type: "slang", settings: {} }], + overrides: { + "contracts/Old.sol": { + version: "0.8.20", + type: "slang", + settings: {}, + }, + }, + }, + }), + ); + + const { compilers, overrides } = resolvedConfig.solidity.profiles.slang; + assert.equal(compilers[0].settings.solidityVersion, "0.8.34"); + assert.equal( + overrides["contracts/Old.sol"].settings.solidityVersion, + "0.8.20", + ); + }); + it("augments every slang-typed compiler when several Solidity versions use slang", async () => { const resolvedConfig = await resolveUserConfig( { slang: { version: PINNED_VERSION } }, @@ -346,6 +394,7 @@ describe("hardhat-slang plugin config resolution", () => { resolvedConfig.solidity.profiles.slang.compilers; for (const compiler of [first, second]) { assert.equal(compiler.settings.optimizer.mode, "1"); + assert.equal(compiler.settings.solidityVersion, compiler.version); const wildcardSelectors = compiler.settings.outputSelection["*"][ "*" ] as string[]; @@ -783,6 +832,74 @@ describe("hardhat-slang Solidity version validation", () => { ); assert.equal(rangeErrors(errors).length, 1); }); + + it("rejects settings.solidityVersion that differs from the entry's version", async () => { + const errors = await validateUserConfig( + slangProfileConfig([ + { + version: "0.8.34", + type: "slang", + settings: { solidityVersion: "0.8.20" }, + }, + ]), + ); + assert.deepEqual( + errors.map((e) => e.path), + [ + [ + "solidity", + "profiles", + "slang", + "compilers", + 0, + "settings", + "solidityVersion", + ], + ], + ); + }); + + it("rejects a differing settings.solidityVersion on an override with its own binary", async () => { + const errors = await validateUserConfig( + slangProfileConfig([{ version: "0.8.34", type: "slang" }], { + overrides: { + "contracts/Old.sol": { + version: "0.8.20", + type: "slang", + path: "/tmp/slang-nightly", + settings: { solidityVersion: "0.8.34" }, + }, + }, + }), + ); + assert.deepEqual( + errors.map((e) => e.path), + [ + [ + "solidity", + "profiles", + "slang", + "overrides", + "contracts/Old.sol", + "settings", + "solidityVersion", + ], + ], + ); + }); + + it("accepts settings.solidityVersion equal to the entry's version", async () => { + const errors = await validateUserConfig( + slangProfileConfig([ + { + version: "0.8.34", + type: "slang", + settings: { solidityVersion: "0.8.34" }, + }, + ]), + ); + assert.deepEqual(errors, []); + }); }); describe("hardhat-slang resolved config validation", () => { diff --git a/packages/hardhat-slang/test/downloader.ts b/packages/hardhat-slang/test/downloader.ts index ed260c4a71d..ccfae0a6eff 100644 --- a/packages/hardhat-slang/test/downloader.ts +++ b/packages/hardhat-slang/test/downloader.ts @@ -43,7 +43,7 @@ const TEST_RELEASE: SlangRelease = { extraArgs: [], }; -const PINNED_VERSION = "0.1.0-pre.2026-10-01"; +const PINNED_VERSION = "0.1.0-pre.2026-10-14"; const PINNED_RELEASE = SLANG_RELEASES[PINNED_VERSION]; const BINARY_CONTENTS = "#!/bin/sh\necho slang 0.0.1-test\n"; diff --git a/packages/hardhat-slang/test/fixture-projects/simple/hardhat.config.ts b/packages/hardhat-slang/test/fixture-projects/simple/hardhat.config.ts index 03592ca55a0..84620a60d75 100644 --- a/packages/hardhat-slang/test/fixture-projects/simple/hardhat.config.ts +++ b/packages/hardhat-slang/test/fixture-projects/simple/hardhat.config.ts @@ -15,7 +15,7 @@ const config: HardhatUserConfig = { }, }, slang: { - version: "0.1.0-pre.2026-10-01", + version: "0.1.0-pre.2026-10-14", }, plugins: [HardhatSlangPlugin], }; diff --git a/packages/hardhat-slang/test/fixture-projects/with-debug-info/hardhat.config.ts b/packages/hardhat-slang/test/fixture-projects/with-debug-info/hardhat.config.ts index 03592ca55a0..84620a60d75 100644 --- a/packages/hardhat-slang/test/fixture-projects/with-debug-info/hardhat.config.ts +++ b/packages/hardhat-slang/test/fixture-projects/with-debug-info/hardhat.config.ts @@ -15,7 +15,7 @@ const config: HardhatUserConfig = { }, }, slang: { - version: "0.1.0-pre.2026-10-01", + version: "0.1.0-pre.2026-10-14", }, plugins: [HardhatSlangPlugin], }; diff --git a/packages/hardhat-slang/test/hook-handlers/solidity.ts b/packages/hardhat-slang/test/hook-handlers/solidity.ts index cef3500b672..7e02e3a1cba 100644 --- a/packages/hardhat-slang/test/hook-handlers/solidity.ts +++ b/packages/hardhat-slang/test/hook-handlers/solidity.ts @@ -26,7 +26,7 @@ import solidityHookHandlers, { } from "../../src/internal/hook-handlers/solidity.js"; import { SlangCompiler } from "../../src/internal/slang-compiler.js"; -const PINNED_VERSION = "0.1.0-pre.2026-10-01"; +const PINNED_VERSION = "0.1.0-pre.2026-10-14"; const PINNED_RELEASE = SLANG_RELEASES[PINNED_VERSION]; // Helper to create a compiler config @@ -99,9 +99,9 @@ describe("parseSlangVersion", () => { it("parses the prerelease build's banner, which names the Slang front end", () => { assert.equal( parseSlangVersion( - "solx v0.1.8, LLVM-based Solidity compiler for the EVM, Front end: Slang, LLVM build: e7c67b95\nVersion: 0.8.37", + "slang v0.2.0, LLVM-based Solidity compiler for the EVM, Front end: Slang, LLVM build: 716613b7\nVersion: 0.8.37", ), - "0.1.8", + "0.2.0", ); }); diff --git a/packages/hardhat-slang/test/integration.ts b/packages/hardhat-slang/test/integration.ts index 8e41ca94a47..427f18b9185 100644 --- a/packages/hardhat-slang/test/integration.ts +++ b/packages/hardhat-slang/test/integration.ts @@ -20,7 +20,7 @@ describe("hardhat-slang integration", () => { it("resolves plugin config through the HRE", async () => { const hre = await createHre(); assert.equal(hre.config.slang.dangerouslyAllowSlangInProduction, false); - assert.equal(hre.config.slang.version, "0.1.0-pre.2026-10-01"); + assert.equal(hre.config.slang.version, "0.1.0-pre.2026-10-14"); }); it("resolves plugin config with defaults when not specified", async () => { @@ -36,7 +36,7 @@ describe("hardhat-slang integration", () => { }, }, }, - slang: { version: "0.1.0-pre.2026-10-01" }, + slang: { version: "0.1.0-pre.2026-10-14" }, plugins: [(await import("../src/index.js")).default], }); @@ -60,7 +60,7 @@ describe("hardhat-slang integration", () => { }, }, }, - slang: { version: "0.1.0-pre.2026-10-01" }, + slang: { version: "0.1.0-pre.2026-10-14" }, plugins: [ (await import("../src/index.js")).default, (await import("@nomicfoundation/hardhat-slang-solx")).default, @@ -72,7 +72,7 @@ describe("hardhat-slang integration", () => { ["slang", "slang-solx", "solc"], "both plugins register their compiler type", ); - assert.equal(hre.config.slang.version, "0.1.0-pre.2026-10-01"); + assert.equal(hre.config.slang.version, "0.1.0-pre.2026-10-14"); assert.equal( hre.config["slang-solx"].dangerouslyAllowSlangSolxInProduction, false, diff --git a/packages/hardhat-slang/test/mirror-availability.ts b/packages/hardhat-slang/test/mirror-availability.ts index 65d06214a72..af147216234 100644 --- a/packages/hardhat-slang/test/mirror-availability.ts +++ b/packages/hardhat-slang/test/mirror-availability.ts @@ -17,10 +17,10 @@ import { function assetNames(version: string, release: SlangRelease): string[] { const suffix = release.assetOverride?.assetSuffix ?? `v${version}`; return [ - `solx-linux-amd64-gnu-${suffix}`, - `solx-linux-arm64-gnu-${suffix}`, - `solx-macosx-${suffix}`, - `solx-windows-amd64-gnu-${suffix}.exe`, + `slang-linux-amd64-gnu-${suffix}`, + `slang-linux-arm64-gnu-${suffix}`, + `slang-macosx-${suffix}`, + `slang-windows-amd64-gnu-${suffix}.exe`, ]; } diff --git a/packages/hardhat-slang/test/output-augmentation.ts b/packages/hardhat-slang/test/output-augmentation.ts index c0c11123d0b..c80b27c78ce 100644 --- a/packages/hardhat-slang/test/output-augmentation.ts +++ b/packages/hardhat-slang/test/output-augmentation.ts @@ -22,13 +22,7 @@ const ELF_MAGIC_HEX = "7f454c46"; describe( "hardhat-slang output augmentation", - { - // The prerelease binary doesn't emit debugInfo yet. Opt in once the pinned - // release does, so this turns on without a code change. - skip: - process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" || - process.env.HARDHAT_SLANG_EXPECT_DEBUG_INFO !== "true", - }, + { skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" }, () => { useFixtureProject("with-debug-info"); diff --git a/packages/hardhat-slang/test/platform.ts b/packages/hardhat-slang/test/platform.ts index 6d77a69eedf..cf154e35d93 100644 --- a/packages/hardhat-slang/test/platform.ts +++ b/packages/hardhat-slang/test/platform.ts @@ -9,7 +9,7 @@ import { getSlangBinaryBaseName, } from "../src/internal/platform.js"; -const PINNED_RELEASE = SLANG_RELEASES["0.1.0-pre.2026-10-01"]; +const PINNED_RELEASE = SLANG_RELEASES["0.1.0-pre.2026-10-14"]; // A release served by the mirror under the standard `v${version}` naming. const MIRROR_RELEASE: SlangRelease = { @@ -25,8 +25,8 @@ describe("hardhat-slang platform detection", () => { const baseName = getSlangBinaryBaseName(); assert.ok(typeof baseName === "string", "base name should be a string"); assert.ok( - baseName.startsWith("solx-"), - "base name should start with 'solx-'", + baseName.startsWith("slang-"), + "base name should start with 'slang-'", ); assert.ok(baseName.length > 5, "base name should have meaningful length"); }); @@ -37,13 +37,13 @@ describe("hardhat-slang platform detection", () => { const arch = process.arch; if (platform === "linux" && arch === "x64") { - assert.equal(baseName, "solx-linux-amd64-gnu"); + assert.equal(baseName, "slang-linux-amd64-gnu"); } else if (platform === "linux" && arch === "arm64") { - assert.equal(baseName, "solx-linux-arm64-gnu"); + assert.equal(baseName, "slang-linux-arm64-gnu"); } else if (platform === "darwin") { - assert.equal(baseName, "solx-macosx"); + assert.equal(baseName, "slang-macosx"); } else if (platform === "win32" && arch === "x64") { - assert.equal(baseName, "solx-windows-amd64-gnu"); + assert.equal(baseName, "slang-windows-amd64-gnu"); } }); @@ -56,8 +56,8 @@ describe("hardhat-slang platform detection", () => { it("names the pinned prerelease's asset with its override suffix instead of the version", () => { assert.equal( - getSlangAssetName("0.1.0-pre.2026-10-01", PINNED_RELEASE), - `${getSlangBinaryBaseName()}-slang-2026-10-01${EXE}`, + getSlangAssetName("0.1.0-pre.2026-10-14", PINNED_RELEASE), + `${getSlangBinaryBaseName()}-debug-fast${EXE}`, ); }); }); diff --git a/packages/hardhat-slang/test/slang-compiler.ts b/packages/hardhat-slang/test/slang-compiler.ts index 38d4cce7daa..290d06d2dfc 100644 --- a/packages/hardhat-slang/test/slang-compiler.ts +++ b/packages/hardhat-slang/test/slang-compiler.ts @@ -1,4 +1,3 @@ -import type { SlangRelease } from "../src/internal/constants.js"; import type { CompilerInput, CompilerOutput } from "hardhat/types/solidity"; import assert from "node:assert/strict"; @@ -7,22 +6,14 @@ import { beforeEach, describe, it } from "node:test"; import { SLANG_RELEASES } from "../src/internal/constants.js"; import { SLANG_DEBUG_INFO_SELECTORS, + SLANG_DEBUG_SYMBOLS_SELECTORS, SlangCompiler, - addSlangDebugInfoSelectors, + addSlangStackTraceSelectors, } from "../src/internal/slang-compiler.js"; -const PINNED_VERSION = "0.1.0-pre.2026-10-01"; +const PINNED_VERSION = "0.1.0-pre.2026-10-14"; const PINNED_RELEASE = SLANG_RELEASES[PINNED_VERSION]; -// What a real release row is expected to look like once the compiler accepts -// the target Solidity version: no import-callback flag, a version flag. -const RELEASE_WITH_VERSION_FLAG: SlangRelease = { - minSolidity: "0.8.0", - maxSolidity: "0.8.36", - extraArgs: [], - targetVersionFlag: "--solidity-version", -}; - // Track calls to the fake spawnCompile let spawnCompileCalls: Array<{ command: string; @@ -54,7 +45,6 @@ describe("SlangCompiler", () => { it("implements the Compiler interface", async () => { const compiler = new SlangCompiler(PINNED_VERSION, "/path/to/slang", { release: PINNED_RELEASE, - targetSolidityVersion: "0.8.34", }); assert.equal(compiler.version, PINNED_VERSION); @@ -63,10 +53,9 @@ describe("SlangCompiler", () => { assert.equal(compiler.isSolcJs, false); }); - it("forwards binary path, the release's extra args, and the input unchanged to spawnCompile", async () => { + it("forwards binary path, the release's extra args, and the input to spawnCompile", async () => { const compiler = new SlangCompiler(PINNED_VERSION, "/path/to/slang", { release: PINNED_RELEASE, - targetSolidityVersion: "0.8.34", spawnCompile: fakeSpawnCompile, }); @@ -75,92 +64,59 @@ describe("SlangCompiler", () => { assert.equal(spawnCompileCalls.length, 1); const call = spawnCompileCalls[0]; assert.equal(call.command, "/path/to/slang"); - // The prerelease row keeps --no-import-callback and has no version flag, so - // the prerelease binary is never given an argument it rejects. + // The prerelease row keeps --no-import-callback. assert.deepEqual(call.args, ["--standard-json", "--no-import-callback"]); - // compile() transforms nothing — the plugin's defaults (optimizer mode, + // compile() only drops `ast` — the plugin's defaults (optimizer mode, // debugInfo) are applied at config resolution, so the input reaches the - // compiler exactly as the build system constructed it. - assert.equal(call.input, sampleInput); + // compiler otherwise exactly as the build system constructed it. + assert.deepEqual(call.input, sampleInput); }); - it("passes only --standard-json when the release has no extra args or version flag", async () => { - const compiler = new SlangCompiler("0.2.0", "/path/to/slang", { - release: { minSolidity: "0.8.0", maxSolidity: "0.8.36", extraArgs: [] }, - targetSolidityVersion: "0.8.34", + it("drops the ast output and keeps every other selector", async () => { + const compiler = new SlangCompiler(PINNED_VERSION, "/path/to/slang", { + release: PINNED_RELEASE, spawnCompile: fakeSpawnCompile, }); + const input: CompilerInput = { + ...sampleInput, + settings: { + ...sampleInput.settings, + outputSelection: { + "*": { + "": ["ast", "debugSymbols"], + "*": ["abi", "evm.bytecode.object"], + }, + }, + }, + }; - await compiler.compile(sampleInput); + await compiler.compile(input); - assert.deepEqual(spawnCompileCalls[0].args, ["--standard-json"]); - }); - - it("appends the release's version flag and the target Solidity version", async () => { - const compiler = new SlangCompiler("0.2.0", "/path/to/slang", { - release: RELEASE_WITH_VERSION_FLAG, - targetSolidityVersion: "0.8.20", - spawnCompile: fakeSpawnCompile, + assert.deepEqual(spawnCompileCalls[0].input.settings.outputSelection, { + "*": { "": ["debugSymbols"], "*": ["abi", "evm.bytecode.object"] }, }); - - await compiler.compile(sampleInput); - - assert.deepEqual(spawnCompileCalls[0].args, [ - "--standard-json", - "--solidity-version", - "0.8.20", - ]); + assert.deepEqual( + input.settings.outputSelection["*"][""], + ["ast", "debugSymbols"], + "the build system's input must not be modified", + ); + assert.deepEqual(spawnCompileCalls[0].input.sources, input.sources); }); - it("puts the release's extra args before the version flag", async () => { + it("passes only --standard-json when the release has no extra args", async () => { const compiler = new SlangCompiler("0.2.0", "/path/to/slang", { - release: { - ...RELEASE_WITH_VERSION_FLAG, - extraArgs: ["--no-import-callback"], - }, - targetSolidityVersion: "0.8.20", + release: { minSolidity: "0.8.0", maxSolidity: "0.8.36", extraArgs: [] }, spawnCompile: fakeSpawnCompile, }); await compiler.compile(sampleInput); - assert.deepEqual(spawnCompileCalls[0].args, [ - "--standard-json", - "--no-import-callback", - "--solidity-version", - "0.8.20", - ]); - }); - - it("gives two Solidity versions the same binary and long version, differing only in the target", async () => { - const forNewest = new SlangCompiler("0.2.0", "/path/to/slang", { - release: RELEASE_WITH_VERSION_FLAG, - targetSolidityVersion: "0.8.34", - spawnCompile: fakeSpawnCompile, - }); - const forOlder = new SlangCompiler("0.2.0", "/path/to/slang", { - release: RELEASE_WITH_VERSION_FLAG, - targetSolidityVersion: "0.8.20", - spawnCompile: fakeSpawnCompile, - }); - - // Core requires longVersion to be deterministic per compiler version. - assert.equal(forNewest.compilerPath, forOlder.compilerPath); - assert.equal(forNewest.longVersion, forOlder.longVersion); - - await forNewest.compile(sampleInput); - await forOlder.compile(sampleInput); - - assert.deepEqual( - spawnCompileCalls.map((c) => c.args.at(-1)), - ["0.8.34", "0.8.20"], - ); + assert.deepEqual(spawnCompileCalls[0].args, ["--standard-json"]); }); it("returns the output from spawnCompile", async () => { const compiler = new SlangCompiler(PINNED_VERSION, "/path/to/slang", { release: PINNED_RELEASE, - targetSolidityVersion: "0.8.34", spawnCompile: fakeSpawnCompile, }); @@ -169,44 +125,54 @@ describe("SlangCompiler", () => { }); }); -describe("addSlangDebugInfoSelectors", () => { - it("populates the wildcard slot when the input is empty", async () => { - const result = await addSlangDebugInfoSelectors({}); +describe("addSlangStackTraceSelectors", () => { + it("populates the wildcard slots when the input is empty", async () => { + const result = await addSlangStackTraceSelectors({}); assert.deepEqual(result, { - "*": { "*": [...SLANG_DEBUG_INFO_SELECTORS] }, + "*": { + "*": [...SLANG_DEBUG_INFO_SELECTORS], + "": [...SLANG_DEBUG_SYMBOLS_SELECTORS], + }, }); }); it("appends to an existing wildcard selector list without removing user entries", async () => { - const result = await addSlangDebugInfoSelectors({ + const result = await addSlangStackTraceSelectors({ "*": { "*": ["abi", "metadata"] }, }); assert.deepEqual(result, { - "*": { "*": ["abi", "metadata", ...SLANG_DEBUG_INFO_SELECTORS] }, + "*": { + "*": ["abi", "metadata", ...SLANG_DEBUG_INFO_SELECTORS], + "": [...SLANG_DEBUG_SYMBOLS_SELECTORS], + }, }); }); - it('preserves the file-level `[*][""]` slot for outputs like ast', async () => { - const result = await addSlangDebugInfoSelectors({ + it('appends debugSymbols to the file-level `[*][""]` slot, keeping outputs like ast', async () => { + const result = await addSlangStackTraceSelectors({ "*": { "": ["ast"] }, }); - // The file-level slot must round-trip unchanged. Selectors are added at - // the per-contract slot `["*"]["*"]` instead. assert.ok(result !== undefined, "result should not be undefined"); - assert.deepEqual(result["*"][""], ["ast"]); + assert.deepEqual(result["*"][""], [ + "ast", + ...SLANG_DEBUG_SYMBOLS_SELECTORS, + ]); }); it("does not mutate the input object", async () => { const input2 = { "*": { "*": ["abi"] } }; const before = JSON.stringify(input2); - await addSlangDebugInfoSelectors(input2); + await addSlangStackTraceSelectors(input2); assert.equal(JSON.stringify(input2), before); }); - it("accepts undefined input (sets up the wildcard slot)", async () => { - const result = await addSlangDebugInfoSelectors(undefined); + it("accepts undefined input (sets up the wildcard slots)", async () => { + const result = await addSlangStackTraceSelectors(undefined); assert.deepEqual(result, { - "*": { "*": [...SLANG_DEBUG_INFO_SELECTORS] }, + "*": { + "*": [...SLANG_DEBUG_INFO_SELECTORS], + "": [...SLANG_DEBUG_SYMBOLS_SELECTORS], + }, }); }); }); diff --git a/packages/hardhat-slang/test/solidity-version.ts b/packages/hardhat-slang/test/solidity-version.ts new file mode 100644 index 00000000000..c5fdfcb5b8b --- /dev/null +++ b/packages/hardhat-slang/test/solidity-version.ts @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import { describe, it } from "node:test"; + +import { useFixtureProject } from "@nomicfoundation/hardhat-test-utils"; +import { + createHardhatRuntimeEnvironment, + importUserConfig, + resolveHardhatConfigPath, +} from "hardhat/hre"; + +import { SLANG_RELEASES } from "../src/internal/constants.js"; +import { getSlangBinaryPath } from "../src/internal/downloader.js"; +import { SlangCompiler } from "../src/internal/slang-compiler.js"; + +describe( + "hardhat-slang Solidity version", + { skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" }, + () => { + useFixtureProject("with-debug-info"); + + it("compiles at the compiler entry's version, which the binary reads from settings.solidityVersion", async () => { + const configPath = await resolveHardhatConfigPath(); + const userConfig = await importUserConfig(configPath); + const hre = await createHardhatRuntimeEnvironment(userConfig); + + const rootFilePaths = await hre.solidity.getRootFilePaths({ + scope: "contracts", + }); + const counterPath = rootFilePaths.find( + (p) => path.basename(p) === "Counter.sol", + ); + assert.ok( + counterPath !== undefined, + "Counter.sol should be a build root", + ); + + const jobsResult = await hre.solidity.getCompilationJobs([counterPath], { + force: true, + quiet: true, + buildProfile: "slang", + }); + assert.ok(jobsResult.success, "getCompilationJobs should succeed"); + const job = jobsResult.compilationJobsPerFile.get(counterPath); + assert.ok(job !== undefined, "Counter.sol should have a job"); + + const input = await job.getSolcInput(); + const settings: Record = input.settings; + assert.equal(settings.solidityVersion, "0.8.34"); + + const { output } = await hre.solidity.runCompilationJob(job, { + quiet: true, + buildProfile: "slang", + }); + const errors = (output.errors ?? []).filter( + (e) => e.severity === "error", + ); + assert.deepEqual( + errors.map((e) => e.message), + [], + "Counter.sol (pragma ^0.8.34) should compile at 0.8.34", + ); + + // The same input at a version below the pragma must be refused, which + // only happens if the binary compiles at settings.solidityVersion. + const slangVersion = hre.config.slang.version; + assert.ok(slangVersion !== undefined, "the fixture pins slang.version"); + const release = SLANG_RELEASES[slangVersion]; + const compiler = new SlangCompiler( + slangVersion, + await getSlangBinaryPath(slangVersion, release), + { release }, + ); + const mismatchedSettings = { + ...input.settings, + solidityVersion: "0.8.20", + }; + const mismatched = await compiler.compile({ + ...input, + settings: mismatchedSettings, + }); + const mismatchErrors = (mismatched.errors ?? []).filter( + (e) => e.severity === "error", + ); + assert.ok( + mismatchErrors.some((e) => e.message.includes("'0.8.20'")), + `expected slang to reject pragma ^0.8.34 at 0.8.20, got: ${JSON.stringify(mismatchErrors.map((e) => e.message))}`, + ); + }); + }, +); diff --git a/packages/hardhat-slang/test/stack-traces.ts b/packages/hardhat-slang/test/stack-traces.ts new file mode 100644 index 00000000000..56861c038a1 --- /dev/null +++ b/packages/hardhat-slang/test/stack-traces.ts @@ -0,0 +1,72 @@ +import { describe, it } from "node:test"; + +import { + assertRejects, + useFixtureProject, +} from "@nomicfoundation/hardhat-test-utils"; +import { + createHardhatRuntimeEnvironment, + importUserConfig, + resolveHardhatConfigPath, +} from "hardhat/hre"; + +// `set(uint256)` with `v = 0`, which fails `Counter._checkPositive`. +const SET_ZERO_CALLDATA = `0x60fe47b1${"0".repeat(64)}`; + +describe( + "hardhat-slang stack traces", + { + // Needs an EDR that builds stack traces from debugSymbols. Opt in once + // Hardhat's does, so this turns on without a code change. + skip: + process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" || + process.env.HARDHAT_SLANG_EXPECT_STACK_TRACES !== "true", + }, + () => { + useFixtureProject("with-debug-info"); + + it("reports Solidity frames for a revert in a slang-compiled contract", async () => { + const configPath = await resolveHardhatConfigPath(); + const userConfig = await importUserConfig(configPath); + const hre = await createHardhatRuntimeEnvironment(userConfig, { + buildProfile: "slang", + }); + + await hre.tasks + .getTask("build") + .run({ force: true, quiet: true, noTests: true }); + + const { bytecode } = await hre.artifacts.readArtifact("Counter"); + const connection = await hre.network.connect(); + try { + const { provider } = connection; + const [from] = await provider.request({ method: "eth_accounts" }); + const deployHash = await provider.request({ + method: "eth_sendTransaction", + params: [{ from, data: bytecode }], + }); + const receipt = await provider.request({ + method: "eth_getTransactionReceipt", + params: [deployHash], + }); + + await assertRejects( + provider.request({ + method: "eth_sendTransaction", + params: [ + { from, to: receipt.contractAddress, data: SET_ZERO_CALLDATA }, + ], + }), + (error) => + error.stack?.includes( + "at Counter._checkPositive (contracts/Counter.sol:13)", + ) === true && + error.stack.includes("at Counter.set (contracts/Counter.sol:8)"), + "expected Solidity frames for Counter._checkPositive and Counter.set in the error stack", + ); + } finally { + await connection.close(); + } + }); + }, +);