diff --git a/etc/config/lua.amazon.properties b/etc/config/lua.amazon.properties new file mode 100644 index 000000000..e40ba3945 --- /dev/null +++ b/etc/config/lua.amazon.properties @@ -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 diff --git a/etc/config/lua.defaults.properties b/etc/config/lua.defaults.properties new file mode 100644 index 000000000..3c0633201 --- /dev/null +++ b/etc/config/lua.defaults.properties @@ -0,0 +1,6 @@ +compilers=/usr/bin/luac +supportsBinary=false +supportsExecute=true +interpreted=true +interpreter=/usr/bin/lua +compilerType=lua diff --git a/examples/lua/default.lua b/examples/lua/default.lua new file mode 100644 index 000000000..09ef7da18 --- /dev/null +++ b/examples/lua/default.lua @@ -0,0 +1,5 @@ +local function square(num) + return num * num +end + +print(square(5)) diff --git a/lib/compilers/_all.ts b/lib/compilers/_all.ts index 1dbbce419..0a15973a6 100644 --- a/lib/compilers/_all.ts +++ b/lib/compilers/_all.ts @@ -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'; diff --git a/lib/compilers/lua.ts b/lib/compilers/lua.ts new file mode 100644 index 000000000..f49425255 --- /dev/null +++ b/lib/compilers/lua.ts @@ -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(`compiler.${this.compiler.id}.interpreter`, '') || + this.compilerProps(`group.${this.compiler.group}.interpreter`, '') || + this.compilerProps('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 { + 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 { + 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 (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]; + } +} diff --git a/lib/languages.ts b/lib/languages.ts index 459724cc5..5ab940854 100644 --- a/lib/languages.ts +++ b/lib/languages.ts @@ -629,6 +629,17 @@ const definitions: Record = { 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', diff --git a/public/logos/lua.png b/public/logos/lua.png new file mode 100644 index 000000000..dd98c93d8 Binary files /dev/null and b/public/logos/lua.png differ diff --git a/test/lua-tests.ts b/test/lua-tests.ts new file mode 100644 index 000000000..909ac08b6 --- /dev/null +++ b/test/lua-tests.ts @@ -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 (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 (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]); + }); +}); diff --git a/types/languages.interfaces.ts b/types/languages.interfaces.ts index 97b8f7255..04a8be8a5 100644 --- a/types/languages.interfaces.ts +++ b/types/languages.interfaces.ts @@ -76,6 +76,7 @@ export type LanguageKey = | 'lean' | 'llvm' | 'llvm_mir' + | 'lua' | 'mlir' | 'modula2' | 'mojo'