Files
leftibot 532b6c2b57 Fix #8695: [LANGUAGE REQUEST]: Add lua as a supported language (#8696)
Automated fix by @leftibot.

### What changed

> Fix #8695: add Lua as a supported language
> Add support for Lua via the reference PUC-Rio interpreter. Disassembly
is
> produced by `luac -l -l -p`, which writes a verbose bytecode listing
to
> stdout that the LuaCompiler captures and writes to the output file.
The
> class exposes overridable hooks (`resolveLuacExe`,
`getDisassemblyArgs`)
> so alternative implementations such as LuaJIT can plug in a different
> bytecode dumper without rewriting the compiler. Production config
ships
> five Lua releases (5.1.5, 5.2.4, 5.3.6, 5.4.7, 5.5.0) covering the
> actively used minor versions.

### Files
```
 etc/config/lua.amazon.properties   |  27 ++++++
 etc/config/lua.defaults.properties |   5 +
 examples/lua/default.lua           |   5 +
 lib/compilers/_all.ts              |   1 +
 lib/compilers/lua.ts               | 182 +++++++++++++++++++++++++++++++++++++
 lib/languages.ts                   |  11 +++
 test/lua-tests.ts                  | 138 ++++++++++++++++++++++++++++
 types/languages.interfaces.ts      |   1 +
 8 files changed, 370 insertions(+)
```

Closes #8695

_Triggered by @lefticus._

---------

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Matt Godbolt <matt@godbolt.org>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 22:45:23 -05:00

171 lines
7.2 KiB
TypeScript

// Copyright (c) 2026, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import fs from 'node:fs/promises';
import path from 'node:path';
import type {AsmResultSource, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
import type {
CacheKey,
CompilationResult,
ExecutionOptionsWithEnv,
} from '../../types/compilation/compilation.interfaces.js';
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
import type {ExecutableExecutionOptions} from '../../types/execution/execution.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {BaseCompiler} from '../base-compiler.js';
import {CompilationEnvironment} from '../compilation-env.js';
import {BaseParser} from './argument-parsers.js';
// Compiler for the reference (PUC-Rio) Lua interpreter.
//
// The configured `compiler.exe` is expected to be `luac`; it is invoked with
// `-l -l -p` to produce a verbose bytecode listing on stdout. Execution uses
// the `lua` interpreter configured via the `interpreter` property.
//
// Alternative Lua implementations (e.g. LuaJIT) can extend this class and
// override `getDisassemblyArgs` to plug in a different bytecode dumper.
export class LuaCompiler extends BaseCompiler {
private readonly interpreterExe: string;
static get key() {
return 'lua';
}
constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
super(compilerInfo, env);
this.compiler.demangler = '';
this.demanglerClass = null;
// Per-compiler properties aren't inherited through the normal cascade
// (TODO(#7150)), so resolve the interpreter explicitly: compiler-specific,
// then group, then the language-level default. Reading plain 'interpreter'
// would only ever see the top-level value, so every version would execute
// with the same Lua.
this.interpreterExe =
this.compilerProps<string>(`compiler.${this.compiler.id}.interpreter`, '') ||
this.compilerProps<string>(`group.${this.compiler.group}.interpreter`, '') ||
this.compilerProps<string>('interpreter', '');
}
override getArgumentParserClass() {
return BaseParser;
}
override optionsForFilter(_filters: ParseFiltersAndOutputOptions, _outputFilename: string): string[] {
// luac's listing always goes to stdout; runCompiler handles capturing it.
return [];
}
override async runCompiler(
compiler: string,
options: string[],
inputFilename: string,
execOptions: ExecutionOptionsWithEnv,
filters?: ParseFiltersAndOutputOptions,
): Promise<CompilationResult> {
if (!execOptions) {
execOptions = this.getDefaultExecOptions();
}
if (!execOptions.customCwd) {
execOptions.customCwd = path.dirname(inputFilename);
}
const luacArgs = this.getDisassemblyArgs(options, inputFilename);
const result = await this.exec(compiler, luacArgs, execOptions);
const outputFilename = this.getOutputFilename(path.dirname(inputFilename), this.outputFilebase);
await fs.writeFile(outputFilename, result.stdout);
result.stdout = '';
return {
...this.transformToCompilationResult(result, inputFilename),
languageId: this.getCompilerResultLanguageId(filters),
instructionSet: this.getInstructionSetFromCompilerArgs(luacArgs),
};
}
override async handleInterpreting(
key: CacheKey,
executeParameters: ExecutableExecutionOptions,
): Promise<CompilationResult> {
if (!this.interpreterExe) {
return super.handleInterpreting(key, executeParameters);
}
// `compiler.exe` points at luac for compilation; swap it to the lua
// interpreter for the duration of the script run.
const originalExe = this.compiler.exe;
this.compiler.exe = this.interpreterExe;
try {
return await super.handleInterpreting(key, executeParameters);
} finally {
this.compiler.exe = originalExe;
}
}
override async processAsm(result: {asm: string}) {
// luac listing format (Lua 5.x):
// "main <name:0,0> (N instructions at 0x...)"
// " 1 [3] VARARGPREP 0"
// The bracketed value is the source line for that bytecode instruction.
const instructionLineRe = /^\s*\d+\s+\[(\d+)\]/;
const functionHeaderRe = /<[^:>]*:(\d+),\d+>/;
const bytecodeLines = result.asm.split('\n');
const bytecodeResult: ParsedAsmResultLine[] = [];
let lastLineNo: number | null = null;
let sourceLoc: AsmResultSource | null = null;
for (const line of bytecodeLines) {
const insnMatch = line.match(instructionLineRe);
const fnMatch = line.match(functionHeaderRe);
if (insnMatch) {
const lineno = Number.parseInt(insnMatch[1], 10);
lastLineNo = lineno;
sourceLoc = {line: lineno, file: null};
} else if (fnMatch) {
const lineno = Number.parseInt(fnMatch[1], 10);
lastLineNo = lineno > 0 ? lineno : null;
sourceLoc = {line: lastLineNo, file: null};
} else if (line) {
sourceLoc = {line: lastLineNo, file: null};
} else {
lastLineNo = null;
sourceLoc = {line: null, file: null};
}
bytecodeResult.push({text: line, source: sourceLoc});
}
return {asm: bytecodeResult};
}
/** Arguments passed to the bytecode dumper. */
protected getDisassemblyArgs(_options: string[], inputFilename: string): string[] {
// -l -l : verbose listing (constants, locals, upvalues)
// -p : parse only, do not write a bytecode file
return ['-l', '-l', '-p', inputFilename];
}
}