Files
compiler-explorer/lib/compilers/odin.ts
Hayden Gray aeaffd6755 [odin] accommodate changes made in dev-2025-02 (#8328)
in `dev-2025-02`, odin changed from single-module debug builds to
multi-module and prevents it from spitting out a single `.s` file. this
adds version checking to make sure that any odin version after
`dev-2025-02` uses `-use-single-module`. additionally, changes were made
around name canonicalization that required changing the matching for
label names to avoid clogging the assembly output (both standard and
binary)
2025-12-15 19:53:32 -06:00

288 lines
9.8 KiB
TypeScript

import path from 'node:path';
import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import type {CompilerOutputOptions, ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import {CompilationEnvironment} from '../compilation-env.js';
import {OdinAsmParser} from '../parsers/asm-parser-odin.js';
import * as utils from '../utils.js';
class OdinVersion {
year: number = 0;
month: number = 0;
constructor(version: string) {
const match = version.match(/(\d{4})-(\d{1,2})[a-zA-Z]*/);
if (!match) {
throw new Error(`Invalid version format: "${version}"`);
}
const year = Number(match[1]);
const month = Number(match[2]);
if (month < 1 || month > 12) {
throw new Error(`Invalid month in version: "${version}"`);
}
this.year = year;
this.month = month;
}
gte(version: OdinVersion) {
if (this.year > version.year) {
return true;
} else if (this.year === version.year) {
return this.month >= version.month;
} else {
return false;
}
}
}
export class OdinCompiler extends BaseCompiler {
private clangPath?: string;
static get key() {
return 'odin';
}
constructor(info: PreliminaryCompilerInfo, env: CompilationEnvironment) {
super(info, env);
this.asm = new OdinAsmParser(this.compilerProps);
this.compiler.supportsIrView = true;
this.compiler.irArg = [];
this.compiler.supportsIntel = false;
this.clangPath = this.compilerProps<string>('clangPath', undefined);
}
override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: string) {
const options: string[] = ['-debug', '-keep-temp-files', `-out:${this.filename(outputFilename)}`];
const version = new OdinVersion(this.compiler.version);
if (version.gte(new OdinVersion('dev-2025-02'))) {
options.push('-use-single-module');
}
if (!(filters.execute || filters.binary)) {
filters.preProcessLines = this.preProcessLines.bind(this);
options.push('-build-mode:asm');
}
return options;
}
override orderArguments(
options: string[],
inputFilename: string,
libIncludes: string[],
libOptions: string[],
libPaths: string[],
libLinks: string[],
userOptions: string[],
staticLibLinks: string[],
) {
return ['build', this.filename(inputFilename), '-file'].concat(options, userOptions);
}
override getDefaultExecOptions() {
const execOptions = super.getDefaultExecOptions();
if (this.clangPath) {
execOptions.env.ODIN_CLANG_PATH = this.clangPath;
}
return execOptions;
}
override async checkOutputFileAndDoPostProcess(
asmResult: CompilationResult,
outputFilename: string,
filters: ParseFiltersAndOutputOptions,
) {
let newOutputFilename = outputFilename;
if (!filters.binary && !filters.execute) newOutputFilename = outputFilename.replace(/.s$/, '.S');
return super.checkOutputFileAndDoPostProcess(asmResult, newOutputFilename, filters);
}
override getIrOutputFilename(inputFilename: string): string {
return this.filename(path.dirname(inputFilename) + '/output.ll');
}
override async postProcessAsm(result, filters?: ParseFiltersAndOutputOptions) {
// we dont need demangling
return result;
}
/**
* Preprocess the source code to '@require' all the functions
* so that their asm is emitted
*/
override preProcess(source: string, filters: CompilerOutputOptions): string {
if (filters.binary && !this.stubRe.test(source)) {
source += `\n${this.stubText}\n`;
}
if (!filters.binary && !filters.execute) {
const sourceLines = utils.splitLines(source);
const outputLines: string[] = [];
const procRE = /^\s*([A-Za-z]\w+)\s*:\s*:\s*proc\s*("\w+")?\s*\(/;
let lastLine = '';
for (const line of sourceLines) {
// skip if the line doesn't contain proc keyword
if (!line.includes('proc')) {
outputLines.push(line);
lastLine = line;
continue;
}
const match = line.match(procRE);
if (!match) {
outputLines.push(line);
lastLine = line;
continue;
}
// last line already has require?
if (lastLine.includes('@require') || lastLine.includes('@(require)')) {
outputLines.push(line);
continue;
}
// @require the function so they dont get inlined
// put @require on the same line so that output source
// has same number of lines so that asm<->source mapping
// in ui works correctly.
outputLines.push('@(require) ' + line);
lastLine = line;
}
let text = outputLines.join('\n');
// append a final trailing newline
if (!text.endsWith('\n')) {
text += '\n';
}
return text;
}
return source;
}
preProcessLines(asmLines: string[]) {
let i = 0;
let funcStart = -1;
while (i < asmLines.length) {
const line = asmLines[i];
// filter out __$ and "__$" builtin functions
const hasBuiltinPrefix = line.startsWith('__$') || line.startsWith('"__$');
if (funcStart === -1 && hasBuiltinPrefix && line.endsWith(':')) {
// ensure there is cfi_startproc
for (let j = i; j < asmLines.length && j < i + 5; j++) {
if (asmLines[j].includes('.cfi_startproc')) {
funcStart = i;
break;
}
}
// make sure this is a globl
if (funcStart !== -1) {
for (let j = i; j >= 0 && j >= i - 3; --j) {
if (asmLines[j].includes('.globl')) {
funcStart = j;
break;
}
}
}
}
if (funcStart !== -1 && line.includes('.cfi_endproc')) {
const len = i - funcStart;
asmLines.splice(funcStart, len + 1);
i = funcStart - 1;
funcStart = -1;
}
i++;
}
return this.removeNonMainSourceFunctions(asmLines);
}
parseFiles(asmLines: string[]) {
const files: Record<number, string> = {};
const fileFind = /^\s*\.(?:cv_)?file\s+(\d+)\s+"([^"]+)"(\s+"([^"]+)")?.*/;
for (const line of asmLines) {
const match = line.match(fileFind);
if (match) {
const lineNum = Number.parseInt(match[1], 10);
if (match[4] && !line.includes('.cv_file')) {
// Clang-style file directive '.file X "dir" "filename"'
if (match[4].startsWith('/')) {
files[lineNum] = match[4];
} else {
files[lineNum] = match[2] + '/' + match[4];
}
} else {
files[lineNum] = match[2];
}
}
}
return files;
}
removeNonMainSourceFunctions(asmLines: string[]): string[] {
const files = this.parseFiles(asmLines);
const sourceTag = /^\s*\.loc\s+(\d+)\s+(\d+)\s+(.*)/;
const stdInLooking = /<stdin>|^-$|example\.[^/]+$|<source>/;
let i = 0;
let funcStart = -1;
while (i < asmLines.length) {
const line = asmLines[i];
// find a label
if (funcStart === -1 && line.endsWith(':')) {
// scan the next 5 lines for a source loc label
for (let j = i; j < i + 5 && j < asmLines.length; j++) {
const fline = asmLines[j];
const match = fline.match(sourceTag);
if (!match) continue;
const file = files[match[1]];
if (!file) continue;
// if the file is not current file we remove this function
if (!stdInLooking.test(file)) {
funcStart = i;
break;
}
}
// ensure there is cfi_startproc
if (funcStart !== -1) {
funcStart = -1;
for (let j = i; j < asmLines.length && j < i + 5; j++) {
if (asmLines[j].includes('.cfi_startproc')) {
funcStart = i;
break;
}
}
}
if (funcStart !== -1) {
for (let j = i; j >= 0 && j >= i - 3; --j) {
if (asmLines[j].includes('.type') && asmLines[j].endsWith('@function')) {
funcStart = j;
break;
}
}
}
}
if (funcStart !== -1 && line.includes('.cfi_endproc')) {
const len = i - funcStart;
asmLines.splice(funcStart, len + 1);
i = funcStart - 1;
funcStart = -1;
}
i++;
}
return asmLines;
}
}