Skip to content
12 changes: 6 additions & 6 deletions packages/hardhat-slang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default defineConfig({
},
},
slang: {
version: "0.1.0-pre.2026-10-01",
version: "0.1.0-pre.2026-10-14",
},
});
```
Expand Down Expand Up @@ -82,7 +82,7 @@ export default defineConfig({
},
},
slang: {
version: "0.1.0-pre.2026-10-01",
version: "0.1.0-pre.2026-10-14",
},
});
```
Expand All @@ -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,
},
});
Expand Down Expand Up @@ -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

Expand Down
11 changes: 3 additions & 8 deletions packages/hardhat-slang/src/internal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -68,14 +63,14 @@ export interface SlangRelease {
*/
// TODO: revisit before release to finalize.
export const SLANG_RELEASES: Record<string, SlangRelease> = {
"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",
},
},
};
2 changes: 1 addition & 1 deletion packages/hardhat-slang/src/internal/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
66 changes: 44 additions & 22 deletions packages/hardhat-slang/src/internal/hook-handlers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<string | number>;
}

/**
* 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<string | number>): 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(
Expand Down Expand Up @@ -310,8 +325,8 @@ export async function resolveUserConfig(
): Promise<HardhatConfig> {
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,
);
Expand All @@ -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(
Expand Down Expand Up @@ -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<string, unknown> },
T extends {
type?: string;
version: string;
settings?: Record<string, unknown>;
},
>(entry: T): Promise<T> {
if (entry.type !== SLANG_COMPILER_TYPE) {
return entry;
Expand All @@ -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,
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export default async (): Promise<Partial<SolidityHooks>> => ({

return new SlangCompiler(customSlangVersion, compilerConfig.path, {
release: getReleaseForCustomPath(context.config),
targetSolidityVersion: compilerConfig.version,
});
}

Expand All @@ -165,9 +164,6 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
`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 });
},
});
18 changes: 8 additions & 10 deletions packages/hardhat-slang/src/internal/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
66 changes: 42 additions & 24 deletions packages/hardhat-slang/src/internal/slang-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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<CompilerOutput> {
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<NonNullable<CompilerInput["settings"]>["outputSelection"]> {
const seed: Record<
Expand All @@ -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;
}
Loading
Loading