mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-07-16 18:37:07 -04:00
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>
This commit is contained in:
32
etc/config/lua.amazon.properties
Normal file
32
etc/config/lua.amazon.properties
Normal file
@@ -0,0 +1,32 @@
|
||||
compilers=&lua
|
||||
defaultCompiler=lua550
|
||||
|
||||
group.lua.compilers=lua515:lua524:lua536:lua547:lua550
|
||||
group.lua.isSemVer=true
|
||||
group.lua.baseName=Lua
|
||||
group.lua.groupName=Lua (PUC-Rio)
|
||||
group.lua.compilerType=lua
|
||||
group.lua.licenseLink=https://www.lua.org/license.html
|
||||
group.lua.licenseName=MIT License
|
||||
group.lua.licensePreamble=Copyright (C) 1994-2024 Lua.org, PUC-Rio.
|
||||
|
||||
compiler.lua515.semver=5.1.5
|
||||
compiler.lua515.exe=/opt/compiler-explorer/lua-5.1.5/bin/luac
|
||||
compiler.lua515.interpreter=/opt/compiler-explorer/lua-5.1.5/bin/lua
|
||||
compiler.lua524.semver=5.2.4
|
||||
compiler.lua524.exe=/opt/compiler-explorer/lua-5.2.4/bin/luac
|
||||
compiler.lua524.interpreter=/opt/compiler-explorer/lua-5.2.4/bin/lua
|
||||
compiler.lua536.semver=5.3.6
|
||||
compiler.lua536.exe=/opt/compiler-explorer/lua-5.3.6/bin/luac
|
||||
compiler.lua536.interpreter=/opt/compiler-explorer/lua-5.3.6/bin/lua
|
||||
compiler.lua547.semver=5.4.7
|
||||
compiler.lua547.exe=/opt/compiler-explorer/lua-5.4.7/bin/luac
|
||||
compiler.lua547.interpreter=/opt/compiler-explorer/lua-5.4.7/bin/lua
|
||||
compiler.lua550.semver=5.5.0
|
||||
compiler.lua550.exe=/opt/compiler-explorer/lua-5.5.0/bin/luac
|
||||
compiler.lua550.interpreter=/opt/compiler-explorer/lua-5.5.0/bin/lua
|
||||
|
||||
supportsBinary=false
|
||||
supportsExecute=true
|
||||
interpreted=true
|
||||
compilerType=lua
|
||||
6
etc/config/lua.defaults.properties
Normal file
6
etc/config/lua.defaults.properties
Normal file
@@ -0,0 +1,6 @@
|
||||
compilers=/usr/bin/luac
|
||||
supportsBinary=false
|
||||
supportsExecute=true
|
||||
interpreted=true
|
||||
interpreter=/usr/bin/lua
|
||||
compilerType=lua
|
||||
5
examples/lua/default.lua
Normal file
5
examples/lua/default.lua
Normal file
@@ -0,0 +1,5 @@
|
||||
local function square(num)
|
||||
return num * num
|
||||
end
|
||||
|
||||
print(square(5))
|
||||
@@ -109,6 +109,7 @@ export {LFortranCompiler} from './lfortran.js';
|
||||
export {LLCCompiler} from './llc.js';
|
||||
export {LLVMmcaTool} from './llvm-mca.js';
|
||||
export {LLVMMOSCompiler} from './llvm-mos.js';
|
||||
export {LuaCompiler} from './lua.js';
|
||||
export {M68kCompiler} from './m68k.js';
|
||||
export {MadPascalCompiler} from './madpascal.js';
|
||||
export {MicroPythonCompiler} from './micropython.js';
|
||||
|
||||
170
lib/compilers/lua.ts
Normal file
170
lib/compilers/lua.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
@@ -629,6 +629,17 @@ const definitions: Record<LanguageKey, LanguageDefinition> = {
|
||||
previewFilter: null,
|
||||
monacoDisassembly: null,
|
||||
},
|
||||
lua: {
|
||||
name: 'Lua',
|
||||
monaco: 'lua',
|
||||
extensions: ['.lua'],
|
||||
alias: [],
|
||||
logoFilename: 'lua.png',
|
||||
logoFilenameDark: null,
|
||||
formatter: null,
|
||||
previewFilter: null,
|
||||
monacoDisassembly: null,
|
||||
},
|
||||
modula2: {
|
||||
name: 'Modula-2',
|
||||
monaco: 'modula2',
|
||||
|
||||
BIN
public/logos/lua.png
Normal file
BIN
public/logos/lua.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
128
test/lua-tests.ts
Normal file
128
test/lua-tests.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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 {beforeAll, describe, expect, it} from 'vitest';
|
||||
|
||||
import {CompilationEnvironment} from '../lib/compilation-env.js';
|
||||
import {LuaCompiler} from '../lib/compilers/lua.js';
|
||||
import {languages} from '../lib/languages.js';
|
||||
import {LanguageKey} from '../types/languages.interfaces.js';
|
||||
import {makeCompilationEnvironment, makeFakeCompilerInfo} from './utils.js';
|
||||
|
||||
// Subclass that exposes the protected disassembly hook so tests can validate
|
||||
// the extension point alternative Lua implementations would use.
|
||||
class TestLuaCompiler extends LuaCompiler {
|
||||
public callGetDisassemblyArgs(options: string[], inputFilename: string): string[] {
|
||||
return this.getDisassemblyArgs(options, inputFilename);
|
||||
}
|
||||
}
|
||||
|
||||
const testLanguages = {
|
||||
lua: {id: 'lua' as LanguageKey},
|
||||
};
|
||||
|
||||
// Obviously-fake paths so nobody mistakes these for real install locations.
|
||||
const fakeLuacExe = '/fake/test/lua/bin/luac';
|
||||
|
||||
describe('Lua language definition', () => {
|
||||
it('is registered with the expected metadata', () => {
|
||||
expect(languages.lua).toBeDefined();
|
||||
expect(languages.lua.id).toBe('lua');
|
||||
expect(languages.lua.name).toBe('Lua');
|
||||
expect(languages.lua.monaco).toBe('lua');
|
||||
expect(languages.lua.extensions[0]).toBe('.lua');
|
||||
});
|
||||
|
||||
it('has a default example', () => {
|
||||
expect(languages.lua.example).not.toMatch(/something went wrong/i);
|
||||
expect(languages.lua.example.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LuaCompiler', () => {
|
||||
let ce: CompilationEnvironment;
|
||||
const info = {
|
||||
exe: fakeLuacExe,
|
||||
remote: {
|
||||
target: 'foo',
|
||||
path: 'bar',
|
||||
cmakePath: 'cmake',
|
||||
basePath: '/',
|
||||
},
|
||||
lang: testLanguages.lua.id,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
ce = makeCompilationEnvironment({languages: testLanguages});
|
||||
});
|
||||
|
||||
it('uses BaseParser for argument parsing', () => {
|
||||
const compiler = new TestLuaCompiler(makeFakeCompilerInfo(info), ce);
|
||||
const parser = compiler.getArgumentParserClass();
|
||||
expect(parser.name).toBe('BaseParser');
|
||||
});
|
||||
|
||||
it('emits no framework options for the lua filter', () => {
|
||||
const compiler = new TestLuaCompiler(makeFakeCompilerInfo(info), ce);
|
||||
expect(compiler.optionsForFilter({} as any, '/fake/test/output')).toEqual([]);
|
||||
});
|
||||
|
||||
it('builds a luac listing command line for disassembly', () => {
|
||||
const compiler = new TestLuaCompiler(makeFakeCompilerInfo(info), ce);
|
||||
expect(compiler.callGetDisassemblyArgs([], '/fake/test/input.lua')).toEqual([
|
||||
'-l',
|
||||
'-l',
|
||||
'-p',
|
||||
'/fake/test/input.lua',
|
||||
]);
|
||||
});
|
||||
|
||||
it('processAsm parses source line numbers from luac listing format', async () => {
|
||||
const compiler = new TestLuaCompiler(makeFakeCompilerInfo(info), ce);
|
||||
const sample = [
|
||||
'main <example.lua:0,0> (5 instructions at 0x1000)',
|
||||
'0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function',
|
||||
'\t1\t[3]\tVARARGPREP\t0',
|
||||
'\t2\t[1]\tCLOSURE \t0 0\t; 0x1100',
|
||||
'\t3\t[3]\tSETTABUP \t0 0 0\t; _ENV "square"',
|
||||
'\t4\t[3]\tRETURN \t0 1 1\t; 0 out',
|
||||
'',
|
||||
'function <example.lua:1,3> (3 instructions at 0x1100)',
|
||||
'\t1\t[2]\tMUL \t1 0 0',
|
||||
'\t2\t[2]\tRETURN1 \t1',
|
||||
].join('\n');
|
||||
|
||||
const {asm} = await compiler.processAsm({asm: sample});
|
||||
|
||||
const sourceLines = asm.map(line => (line.source ? line.source.line : null));
|
||||
// The instruction lines must report the source line in their `[N]` token.
|
||||
expect(sourceLines).toContain(3);
|
||||
expect(sourceLines).toContain(1);
|
||||
expect(sourceLines).toContain(2);
|
||||
|
||||
// Each input line is preserved as text in order.
|
||||
expect(asm.length).toBe(sample.split('\n').length);
|
||||
expect(asm[0].text).toBe(sample.split('\n')[0]);
|
||||
});
|
||||
});
|
||||
@@ -76,6 +76,7 @@ export type LanguageKey =
|
||||
| 'lean'
|
||||
| 'llvm'
|
||||
| 'llvm_mir'
|
||||
| 'lua'
|
||||
| 'mlir'
|
||||
| 'modula2'
|
||||
| 'mojo'
|
||||
|
||||
Reference in New Issue
Block a user