diff --git a/.github/labeler.yml b/.github/labeler.yml index 54952c83c..fbd1e94ba 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -98,6 +98,12 @@ - 'etc/config/clean.*.properties' - 'static/modes/clean-mode.ts' +'lang-clojure': + - changed-files: + - any-glob-to-any-file: + - 'lib/compilers/clojure.ts' + - 'etc/config/clojure.*.properties' + 'lang-cobol': - changed-files: - any-glob-to-any-file: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 08f0ce1a1..93991daa4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -167,3 +167,4 @@ From oldest to newest contributor, we would like to thank: - [Alex Trotta](https://github.com/Ahajha) - [natinusala](https://github.com/natinusala) - [LJ](https://github.com/elle-j) +- [Frank Leon Rose](https://github.com/frankleonrose) diff --git a/cypress/e2e/frontend.cy.ts b/cypress/e2e/frontend.cy.ts index 72a06577c..da4143ca2 100644 --- a/cypress/e2e/frontend.cy.ts +++ b/cypress/e2e/frontend.cy.ts @@ -15,6 +15,7 @@ const PANE_DATA_MAP = { core: {name: 'Core', selector: 'view-haskellCore'}, stg: {name: 'STG', selector: 'view-haskellStg'}, cmm: {name: 'Cmm', selector: 'view-haskellCmm'}, + clojure_macro: {name: 'Clojure Macro', selector: 'view-clojuremacroexp'}, dump: {name: 'Tree/RTL', selector: 'view-gccdump'}, tree: {name: 'Tree', selector: 'view-gnatdebugtree'}, debug: {name: 'Debug', selector: 'view-gnatdebug'}, diff --git a/etc/config/clojure.amazon.properties b/etc/config/clojure.amazon.properties new file mode 100644 index 000000000..4e2ac65d2 --- /dev/null +++ b/etc/config/clojure.amazon.properties @@ -0,0 +1,60 @@ +compilers=&clojure +compilerType=clojure +versionFlag=--version +objdumper=/opt/compiler-explorer/jdk-21.0.2/bin/javap +instructionSet=java +defaultCompiler=clojure1123 +demangler= +postProcess= +options= +supportsBinary=false +needsMulti=false +supportsExecute=true +interpreted=true + +group.clojure.compilers=clojure1123:clojure1122:clojure1121:clojure1120:clojure1114:clojure1113:clojure1112:clojure1111 +group.clojure.groupName=Clojure +group.clojure.baseName=clojure +group.clojure.isSemVer=true +group.clojure.licenseName=Eclipse Public License 1.0 +group.clojure.licenseLink=https://github.com/clojure/clojure/blob/master/epl-v10.html + +compiler.clojure1123.exe=/opt/compiler-explorer/clojure/1.12.3.1577/bin/clojure +compiler.clojure1123.semver=1.12.3 +compiler.clojure1123.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1123.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1122.exe=/opt/compiler-explorer/clojure/1.12.2.1571/bin/clojure +compiler.clojure1122.semver=1.12.2 +compiler.clojure1122.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1122.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1121.exe=/opt/compiler-explorer/clojure/1.12.1.1550/bin/clojure +compiler.clojure1121.semver=1.12.1 +compiler.clojure1121.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1121.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1120.exe=/opt/compiler-explorer/clojure/1.12.0.1530/bin/clojure +compiler.clojure1120.semver=1.12.0 +compiler.clojure1120.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1120.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1114.exe=/opt/compiler-explorer/clojure/1.11.4.1474/bin/clojure +compiler.clojure1114.semver=1.11.4 +compiler.clojure1114.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1114.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1113.exe=/opt/compiler-explorer/clojure/1.11.3.1463/bin/clojure +compiler.clojure1113.semver=1.11.3 +compiler.clojure1113.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1113.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1112.exe=/opt/compiler-explorer/clojure/1.11.2.1446/bin/clojure +compiler.clojure1112.semver=1.11.2 +compiler.clojure1112.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1112.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java + +compiler.clojure1111.exe=/opt/compiler-explorer/clojure/1.11.1.1435/bin/clojure +compiler.clojure1111.semver=1.11.1 +compiler.clojure1111.java_home=/opt/compiler-explorer/jdk-21.0.2 +compiler.clojure1111.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java diff --git a/etc/config/clojure.defaults.properties b/etc/config/clojure.defaults.properties new file mode 100644 index 000000000..a26b2fa1a --- /dev/null +++ b/etc/config/clojure.defaults.properties @@ -0,0 +1,18 @@ +# Default settings for Clojure/JVM +compilers=&clojure +compilerType=clojure +versionFlag=--version +objdumper=javap +instructionSet=java + +group.clojure.compilers=clojuredefault +compiler.clojuredefault.exe=/usr/local/bin/clojure +compiler.clojuredefault.name=clojure default + +defaultCompiler=clojuredefault +demangler= +postProcess= +options= +supportsBinary=false +needsMulti=false +supportsExecute=false diff --git a/etc/scripts/clojure_wrapper.clj b/etc/scripts/clojure_wrapper.clj new file mode 100644 index 000000000..1bfd29a0b --- /dev/null +++ b/etc/scripts/clojure_wrapper.clj @@ -0,0 +1,141 @@ +(ns clojure-wrapper + (:require [clojure.java.io :as io] + [clojure.pprint :as pp] + [clojure.string :as str] + [clojure.walk :as walk]) + (:import [java.io PushbackReader])) + +(def help-text + "Compiler options supported: + -h --help - Shows this text and flags sent to compiler + -dl --direct-linking - Eliminates var indirection in fn invocation + -dlc --disable-locals-clearing - Eliminates instructions setting locals to null + -em --elide-meta [:doc,:arglists,:added,:file,...] - Drops metadata keys from classfiles + -omm --omit-macro-meta - Omit metadata from macro-expanded output") + +(defn parse-command-line [] + (loop [params {} + positional [] + ignored [] + args *command-line-args*] + (if-let [arg (first args)] + (case arg + ("-h" "--help") + (recur (assoc params :show-help true) + positional ignored (rest args)) + + "--macro-expand" + (recur (assoc params :macro-expand true) + positional ignored (rest args)) + + ("-omm" "--omit-macro-meta") + (recur (assoc params :print-meta false) + positional ignored (rest args)) + + ("-dlc" "--disable-locals-clearing") + (recur (assoc params :disable-locals-clearing true) + positional ignored (rest args)) + + ("-dl" "--direct-linking") + (recur (assoc params :direct-linking true) + positional ignored (rest args)) + + ("-em" "--elide-meta") + (let [elisions (try (some-> args second read-string) (catch Exception _e))] + (when-not (and (sequential? elisions) + (every? keyword? elisions)) + (println (str "Invalid elide-meta parameter: '" (second args) "'\n") + "-em flag must be followed by a vector of keywords, like '-em [:doc,:arglists]'") + (System/exit 1)) + (recur (assoc params :elide-meta elisions) + positional ignored (drop 2 args))) + + (if (or (re-matches #"-.*" arg) + (not (re-matches #".*\.clj" arg))) + (recur params positional (conj ignored arg) (rest args)) + (recur params (conj positional arg) ignored (rest args)))) + [params positional ignored]))) + +(defn forms + ([input-file] + ;; Default is to load all forms while file is open + (forms input-file doall)) + ([input-file extract] + (with-open [rdr (-> input-file io/reader PushbackReader.)] + (->> #(try (read rdr) (catch Exception _e nil)) + (repeatedly) + (take-while some?) + extract)))) + +(defn read-namespace [input-file] + (let [parse-ns-name (fn [forms] + (some->> forms + (filter (fn [form] + (and (= 'ns (first form)) + (symbol? (second form))))) + first ;; ns form + second ;; namespace symbol + name))] + (forms input-file parse-ns-name))) + +(defn ns->filename [namespace] + (-> namespace + (str/replace "." "/") + (str/replace "-" "_") + (str ".clj"))) + +(defn path-of-file [file] + (.getParent file)) + +(defn print-macro-expansion [input-file macro-params] + (binding [clojure.pprint/*print-pprint-dispatch* clojure.pprint/code-dispatch + clojure.pprint/*print-right-margin* 60 + clojure.pprint/*print-miser-width* 20 + *print-meta* (:print-meta macro-params true)] + (doseq [form (forms input-file)] + (pp/pprint (walk/macroexpand-all form)) + (println)))) + +(defn compile-input [input-file {:keys [show-help] :as params}] + (let [working-dir (path-of-file input-file) + namespace (read-namespace input-file) + missing-namespace? (nil? namespace) + namespace (or namespace "sample") + compile-filename (io/file working-dir (ns->filename namespace)) + compile-path (path-of-file compile-filename) + compiler-options (select-keys params + [:disable-locals-clearing + :direct-linking + :elide-meta])] + (.mkdirs (io/file working-dir "classes")) + (when compile-path + (.mkdirs (io/file compile-path))) + (with-open [out (io/writer (io/output-stream compile-filename))] + (when missing-namespace? + (let [ns-form (str "(ns " namespace ")")] + (println "Injecting namespace form on first line:" ns-form) + (.write out ns-form))) + (io/copy input-file out)) + + (when show-help + (when (seq *compiler-options*) + (println "*compiler-options* set via environment:" *compiler-options*)) + (when (seq compiler-options) + (println "*compiler-options* set via flags:" compiler-options))) + (binding [*compiler-options* (merge *compiler-options* compiler-options)] + (compile (symbol namespace))))) + +(let [[params positional ignored] (parse-command-line) + input-file (io/file (first positional))] + (if (:macro-expand params) + (print-macro-expansion input-file params) + (let [count-ignored (count ignored)] + (doseq [param ignored] + (println (format "unrecognized option '%s' ignored" param))) + (when (pos-int? count-ignored) + (println (format "%d warning%s found" count-ignored + (if (= 1 count-ignored) "" "s")))) + (when (or (:show-help params) + (pos-int? count-ignored)) + (println help-text)) + (compile-input input-file params)))) diff --git a/examples/clojure/default.clj b/examples/clojure/default.clj new file mode 100644 index 000000000..b69fba28b --- /dev/null +++ b/examples/clojure/default.clj @@ -0,0 +1,5 @@ +;; Type your code here, or load an example. +(ns example) + +(defn square [num] + (* num num)) diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts index 5e77229b8..1195b7d65 100644 --- a/lib/base-compiler.ts +++ b/lib/base-compiler.ts @@ -1680,6 +1680,10 @@ export class BaseCompiler { return [{text: 'Internal error; unable to open output path'}]; } + async generateClojureMacroExpansion(inputFilename: string, options: string[]): Promise { + return [{text: 'Clojure Macro Expansion not applicable to current compiler.'}]; + } + async processHaskellExtraOutput(outpath: string, output: CompilationResult): Promise { if (output.code !== 0) { return [{text: 'Failed to run compiler to get Haskell Core'}]; @@ -2430,6 +2434,7 @@ export class BaseCompiler { const makeGnatDebugTree = backendOptions.produceGnatDebugTree && this.compiler.supportsGnatDebugViews; const makeIr = backendOptions.produceIr && this.compiler.supportsIrView; const makeClangir = backendOptions.produceClangir && this.compiler.supportsClangirView; + const makeClojureMacroExp = backendOptions.produceClojureMacroExp && this.compiler.supportsClojureMacroExpView; const makeOptPipeline = backendOptions.produceOptPipeline && this.compiler.optPipeline; const makeRustMir = backendOptions.produceRustMir && this.compiler.supportsRustMirView; const makeRustMacroExp = backendOptions.produceRustMacroExp && this.compiler.supportsRustMacroExpView; @@ -2448,6 +2453,7 @@ export class BaseCompiler { optPipelineResult, rustHirResult, rustMacroExpResult, + clojureMacroExpResult, toolsResult, ] = await Promise.all([ this.runCompiler(this.compiler.exe, options, inputFilenameSafe, execOptions, filters), @@ -2468,6 +2474,7 @@ export class BaseCompiler { : undefined, makeRustHir ? this.generateRustHir(inputFilename, options) : undefined, makeRustMacroExp ? this.generateRustMacroExpansion(inputFilename, options) : undefined, + makeClojureMacroExp ? this.generateClojureMacroExpansion(inputFilename, options) : undefined, Promise.all( this.runToolsOfType( tools, @@ -2552,6 +2559,8 @@ export class BaseCompiler { asmResult.haskellStgOutput = haskellStgResult; asmResult.haskellCmmOutput = haskellCmmResult; + asmResult.clojureMacroExpOutput = clojureMacroExpResult; + if (asmResult.code !== 0) { return [{...asmResult, asm: ''}, [], []]; } diff --git a/lib/compilers/_all.ts b/lib/compilers/_all.ts index 714d64e67..d3e04694e 100644 --- a/lib/compilers/_all.ts +++ b/lib/compilers/_all.ts @@ -47,6 +47,7 @@ export { } from './clang.js'; export {ClangCLCompiler} from './clangcl.js'; export {CleanCompiler} from './clean.js'; +export {ClojureCompiler} from './clojure.js'; export {CLSPVCompiler} from './clspv.js'; export {CMakeScriptCompiler} from './cmakescript.js'; export {CoccinelleCCompiler, CoccinelleCPlusPlusCompiler} from './coccinelle.js'; diff --git a/lib/compilers/argument-parsers.ts b/lib/compilers/argument-parsers.ts index 39ba13ebc..a0c1e38b9 100644 --- a/lib/compilers/argument-parsers.ts +++ b/lib/compilers/argument-parsers.ts @@ -701,6 +701,12 @@ export class JavaParser extends BaseParser { } } +export class ClojureParser extends BaseParser { + override async parse() { + return this.compiler; + } +} + export class KotlinParser extends BaseParser { override async parse() { await this.getOptions('-help'); diff --git a/lib/compilers/clojure.ts b/lib/compilers/clojure.ts new file mode 100644 index 000000000..a388cbab5 --- /dev/null +++ b/lib/compilers/clojure.ts @@ -0,0 +1,155 @@ +// Copyright (c) 2025, 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 _ from 'underscore'; +import type {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js'; +import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js'; +import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js'; +import type {ResultLine} from '../../types/resultline/resultline.interfaces.js'; +import {CompilationEnvironment} from '../compilation-env.js'; +import * as utils from '../utils.js'; +import {ClojureParser} from './argument-parsers.js'; +import {JavaCompiler} from './java.js'; + +export class ClojureCompiler extends JavaCompiler { + public compilerWrapperPath: string; + + static override get key() { + return 'clojure'; + } + + javaHome: string; + + constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) { + super(compilerInfo, env); + // Use invalid Clojure filename to avoid clashing with name determined by namespace + this.compileFilename = `example-source${this.lang.extensions[0]}`; + this.javaHome = this.compilerProps(`compiler.${this.compiler.id}.java_home`); + this.compilerWrapperPath = + this.compilerProps('compilerWrapper', '') || + utils.resolvePathFromAppRoot('etc', 'scripts', 'clojure_wrapper.clj'); + this.compiler.supportsClojureMacroExpView = true; + } + + override getDefaultExecOptions() { + const execOptions = super.getDefaultExecOptions(); + if (this.javaHome) { + execOptions.env.JAVA_HOME = this.javaHome; + } + + return execOptions; + } + + override filterUserOptions(userOptions: string[]) { + return userOptions.filter(option => { + // Filter out anything that looks like a Clojure source file + // that would confuse the wrapper. + // Also, don't allow users to specify macro expansion mode used + // internally. + return !option.match(/^.*\.clj$/) && option !== '--macro-expand'; + }); + } + + override optionsForFilter(filters: ParseFiltersAndOutputOptions) { + // Forcibly enable javap + filters.binary = true; + return []; + } + + override getArgumentParserClass() { + return ClojureParser; + } + + override async readdir(dirPath: string): Promise { + // Clojure requires recursive walk to find namespace-pathed class files + return fs.readdir(dirPath, {recursive: true}); + } + + async getClojureClasspathArgument( + dirPath: string, + compiler: string, + execOptions: ExecutionOptionsWithEnv, + ): Promise { + const pathOption = ['-Spath']; + const output = await this.exec(compiler, pathOption, execOptions); + const cp = dirPath + ':' + output.stdout.trim(); + return ['-Scp', cp]; + } + + 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); + } + + // The items in 'options' before the source file are user inputs. + const sourceFileOptionIndex = options.findIndex(option => { + return option.endsWith('.clj'); + }); + const userOptions = options.slice(0, sourceFileOptionIndex); + const classpathArgument = await this.getClojureClasspathArgument(execOptions.customCwd, compiler, execOptions); + const wrapperInvokeArgument = ['-M', this.compilerWrapperPath]; + const clojureOptions = _.compact([ + ...classpathArgument, + ...wrapperInvokeArgument, + ...userOptions, + inputFilename, + ]); + const result = await this.exec(compiler, clojureOptions, execOptions); + return { + ...this.transformToCompilationResult(result, inputFilename), + languageId: this.getCompilerResultLanguageId(filters), + instructionSet: this.getInstructionSetFromCompilerArgs(options), + }; + } + + override async generateClojureMacroExpansion(inputFilename: string, options: string[]): Promise { + // The items in 'options' before the source file are user inputs. + const sourceFileOptionIndex = options.findIndex(option => { + return option.endsWith('.clj'); + }); + const userOptions = options.slice(0, sourceFileOptionIndex); + const clojureOptions = _.compact([...userOptions, '--macro-expand', inputFilename]); + const output = await this.runCompiler( + this.compiler.exe, + clojureOptions, + inputFilename, + this.getDefaultExecOptions(), + ); + if (output.code !== 0) { + return [{text: `Failed to run compiler to get Clojure Macro Expansion`}, ...output.stderr]; + } + return output.stdout; + } +} diff --git a/lib/compilers/java.ts b/lib/compilers/java.ts index 6b6f711ac..9b51a7450 100644 --- a/lib/compilers/java.ts +++ b/lib/compilers/java.ts @@ -103,9 +103,15 @@ export class JavaCompiler extends BaseCompiler implements SimpleOutputFilenameCo }; } + async readdir(dirPath: string): Promise { + // Separate method allows override to find classfiles + // that are not in root of dirPath + return fs.readdir(dirPath); + } + override async objdump(outputFilename: string, result: any, maxSize: number) { const dirPath = path.dirname(outputFilename); - const files = await fs.readdir(dirPath); + const files = await this.readdir(dirPath); logger.verbose('Class files: ', files); const results = await Promise.all( files diff --git a/lib/languages.ts b/lib/languages.ts index e4ddd9661..79c3602f0 100644 --- a/lib/languages.ts +++ b/lib/languages.ts @@ -226,6 +226,17 @@ const definitions: Record = { previewFilter: null, monacoDisassembly: null, }, + clojure: { + name: 'Clojure', + monaco: 'clojure', + extensions: ['.clj'], + alias: [], + logoFilename: 'clojure.svg', + logoFilenameDark: null, + formatter: null, + previewFilter: null, + monacoDisassembly: null, + }, cmake: { name: 'CMake', monaco: 'cmake', diff --git a/public/logos/clojure.svg b/public/logos/clojure.svg new file mode 100644 index 000000000..152078cc7 --- /dev/null +++ b/public/logos/clojure.svg @@ -0,0 +1,50 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/static/components.interfaces.ts b/static/components.interfaces.ts index 339c040c5..10741cd9f 100644 --- a/static/components.interfaces.ts +++ b/static/components.interfaces.ts @@ -75,6 +75,7 @@ export const GNAT_DEBUG_TREE_VIEW_COMPONENT_NAME = 'gnatdebugtree' as const; export const GNAT_DEBUG_VIEW_COMPONENT_NAME = 'gnatdebug' as const; export const RUST_MACRO_EXP_VIEW_COMPONENT_NAME = 'rustmacroexp' as const; export const RUST_HIR_VIEW_COMPONENT_NAME = 'rusthir' as const; +export const CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME = 'clojuremacroexp' as const; export const DEVICE_VIEW_COMPONENT_NAME = 'device' as const; export const EXPLAIN_VIEW_COMPONENT_NAME = 'explain' as const; @@ -330,6 +331,15 @@ export type PopulatedRustHirViewState = StateWithId & { treeid: number; }; +export type EmptyClojureMacroExpViewState = EmptyState; +export type PopulatedClojureMacroExpViewState = StateWithId & { + source: string; + clojureMacroExpOutput: unknown; + compilerName: string; + editorid: number; + treeid: number; +}; + export type EmptyDeviceViewState = EmptyState; export type PopulatedDeviceViewState = StateWithId & { source: string; @@ -379,6 +389,7 @@ export interface ComponentStateMap { [GNAT_DEBUG_VIEW_COMPONENT_NAME]: EmptyGnatDebugViewState | PopulatedGnatDebugViewState; [RUST_MACRO_EXP_VIEW_COMPONENT_NAME]: EmptyRustMacroExpViewState | PopulatedRustMacroExpViewState; [RUST_HIR_VIEW_COMPONENT_NAME]: EmptyRustHirViewState | PopulatedRustHirViewState; + [CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME]: EmptyClojureMacroExpViewState | PopulatedClojureMacroExpViewState; [DEVICE_VIEW_COMPONENT_NAME]: EmptyDeviceViewState | PopulatedDeviceViewState; [EXPLAIN_VIEW_COMPONENT_NAME]: EmptyExplainViewState | PopulatedExplainViewState; } diff --git a/static/components.ts b/static/components.ts index 2c3bf6236..8443ded7e 100644 --- a/static/components.ts +++ b/static/components.ts @@ -32,6 +32,7 @@ import { AST_VIEW_COMPONENT_NAME, CFG_VIEW_COMPONENT_NAME, CLANGIR_VIEW_COMPONENT_NAME, + CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME, COMPILER_COMPONENT_NAME, CONFORMANCE_VIEW_COMPONENT_NAME, ComponentConfig, @@ -909,6 +910,38 @@ export function getRustHirViewWith( }; } +/** Get an empty Clojure macro exp view component. */ +export function getClojureMacroExpView(): ComponentConfig { + return { + type: 'component', + componentName: CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME, + componentState: {}, + }; +} + +/** Get a Clojure macro exp view with the given configuration. */ +export function getClojureMacroExpViewWith( + id: number, + source: string, + clojureMacroExpOutput: unknown, + compilerName: string, + editorid: number, + treeid: number, +): ComponentConfig { + return { + type: 'component', + componentName: CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME, + componentState: { + id, + source, + clojureMacroExpOutput, + compilerName, + editorid, + treeid, + }, + }; +} + /** Get an empty device view component. */ export function getDeviceView(): ComponentConfig { return { @@ -1198,6 +1231,7 @@ function validateComponentState(componentName: string, state: any): boolean { case GNAT_DEBUG_VIEW_COMPONENT_NAME: case RUST_MACRO_EXP_VIEW_COMPONENT_NAME: case RUST_HIR_VIEW_COMPONENT_NAME: + case CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME: case DEVICE_VIEW_COMPONENT_NAME: return true; diff --git a/static/event-map.ts b/static/event-map.ts index cfe13c32a..824d5e606 100644 --- a/static/event-map.ts +++ b/static/event-map.ts @@ -158,6 +158,8 @@ export type EventMap = { rustMacroExpViewOpened: (compilerId: number) => void; rustMirViewClosed: (compilerId: number) => void; rustMirViewOpened: (compilerId: number) => void; + clojureMacroExpViewClosed: (compilerId: number) => void; + clojureMacroExpViewOpened: (compilerId: number) => void; // TODO: There are no emitters for this event selectLine: (editorId: number, lineNumber: number) => void; settingsChange: (newSettings: SiteSettings) => void; diff --git a/static/hub.ts b/static/hub.ts index 72b3b1d43..7485e47ab 100644 --- a/static/hub.ts +++ b/static/hub.ts @@ -30,6 +30,7 @@ import { AST_VIEW_COMPONENT_NAME, CFG_VIEW_COMPONENT_NAME, CLANGIR_VIEW_COMPONENT_NAME, + CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME, COMPILER_COMPONENT_NAME, CONFORMANCE_VIEW_COMPONENT_NAME, DEVICE_VIEW_COMPONENT_NAME, @@ -65,6 +66,7 @@ import {IdentifierSet} from './identifier-set.js'; import {Ast as AstView} from './panes/ast-view.js'; import {Cfg as CfgView} from './panes/cfg-view.js'; import {Clangir as ClangirView} from './panes/clangir-view.js'; +import {ClojureMacroExp as ClojureMacroExpView} from './panes/clojuremacroexp-view.js'; import {Compiler} from './panes/compiler.js'; import {Conformance as ConformanceView} from './panes/conformance-view.js'; import {DeviceAsm as DeviceView} from './panes/device-view.js'; @@ -162,6 +164,9 @@ export class Hub { this.rustMacroExpViewFactory(c, s), ); layout.registerComponent(RUST_HIR_VIEW_COMPONENT_NAME, (c: GLC, s: any) => this.rustHirViewFactory(c, s)); + layout.registerComponent(CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME, (c: GLC, s: any) => + this.clojureMacroExpViewFactory(c, s), + ); layout.registerComponent(GCC_DUMP_VIEW_COMPONENT_NAME, (c: GLC, s: any) => this.gccDumpViewFactory(c, s)); layout.registerComponent(CFG_VIEW_COMPONENT_NAME, (c: GLC, s: any) => this.cfgViewFactory(c, s)); layout.registerComponent(CONFORMANCE_VIEW_COMPONENT_NAME, (c: GLC, s: any) => @@ -574,6 +579,13 @@ export class Hub { return new HaskellCmmView(this, container, state); } + public clojureMacroExpViewFactory( + container: GoldenLayout.Container, + state: InferComponentState, + ): ClojureMacroExpView { + return new ClojureMacroExpView(this, container, state); + } + public gccDumpViewFactory(container: GoldenLayout.Container, state: InferComponentState): GCCDumpView { return new GCCDumpView(this, container, state); } diff --git a/static/panes/clojuremacroexp-view.interfaces.ts b/static/panes/clojuremacroexp-view.interfaces.ts new file mode 100644 index 000000000..828945033 --- /dev/null +++ b/static/panes/clojuremacroexp-view.interfaces.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2025, 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. + +export interface ClojureMacroExpState { + clojureMacroExpOutput: any; +} diff --git a/static/panes/clojuremacroexp-view.ts b/static/panes/clojuremacroexp-view.ts new file mode 100644 index 000000000..ae788845c --- /dev/null +++ b/static/panes/clojuremacroexp-view.ts @@ -0,0 +1,129 @@ +// Copyright (c) 2025, 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 {Container} from 'golden-layout'; +import $ from 'jquery'; +import * as monaco from 'monaco-editor'; +import _ from 'underscore'; +import {CompilationResult} from '../../types/compilation/compilation.interfaces.js'; +import {CompilerInfo} from '../../types/compiler.interfaces.js'; +import {Hub} from '../hub.js'; +import {extendConfig} from '../monaco-config.js'; +import {ClojureMacroExpState} from './clojuremacroexp-view.interfaces.js'; +import {MonacoPaneState} from './pane.interfaces.js'; +import {MonacoPane} from './pane.js'; + +export class ClojureMacroExp extends MonacoPane { + constructor(hub: Hub, container: Container, state: ClojureMacroExpState & MonacoPaneState) { + super(hub, container, state); + if (state.clojureMacroExpOutput) { + this.showClojureMacroExpResults(state.clojureMacroExpOutput); + } + } + + override getInitialHTML(): string { + return $('#clojuremacroexp').html(); + } + + override createEditor(editorRoot: HTMLElement): void { + this.editor = monaco.editor.create( + editorRoot, + extendConfig({ + language: 'clojure', + readOnly: true, + glyphMargin: true, + lineNumbersMinChars: 3, + }), + ); + } + + override getPrintName() { + return 'Clojure Macro Expansion Output'; + } + + override getDefaultPaneName(): string { + return 'Clojure Macro Expansion Viewer'; + } + + override registerCallbacks(): void { + const throttleFunction = _.throttle( + (event: monaco.editor.ICursorSelectionChangedEvent) => this.onDidChangeCursorSelection(event), + 500, + ); + this.editor.onDidChangeCursorSelection(event => throttleFunction(event)); + this.eventHub.emit('clojureMacroExpViewOpened', this.compilerInfo.compilerId); + this.eventHub.emit('requestSettings'); + } + + override onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult): void { + if (this.compilerInfo.compilerId !== compilerId) return; + if (result.clojureMacroExpOutput) { + this.showClojureMacroExpResults(result.clojureMacroExpOutput); + } else if (compiler.supportsClojureMacroExpView) { + this.showClojureMacroExpResults([{text: ''}]); + } + } + + override onCompiler( + compilerId: number, + compiler: CompilerInfo | null, + options: string, + editorId?: number, + treeId?: number, + ): void { + if (this.compilerInfo.compilerId === compilerId) { + this.compilerInfo.compilerName = compiler ? compiler.name : ''; + this.compilerInfo.editorId = editorId; + this.compilerInfo.treeId = treeId; + this.updateTitle(); + if (compiler && !compiler.supportsClojureMacroExpView) { + this.showClojureMacroExpResults([ + { + text: '', + }, + ]); + } + } + } + + showClojureMacroExpResults(result: any[]): void { + this.editor + .getModel() + ?.setValue(result.length ? _.pluck(result, 'text').join('\n') : ''); + + if (!this.isAwaitingInitialResults) { + if (this.selection) { + this.editor.setSelection(this.selection); + this.editor.revealLinesInCenter(this.selection.selectionStartLineNumber, this.selection.endLineNumber); + } + this.isAwaitingInitialResults = true; + } + } + + override close(): void { + this.eventHub.unsubscribe(); + this.eventHub.emit('clojureMacroExpViewClosed', this.compilerInfo.compilerId); + this.editor.dispose(); + } +} diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts index 05978b014..9d09b09c3 100644 --- a/static/panes/compiler.ts +++ b/static/panes/compiler.ts @@ -193,6 +193,7 @@ export class Compiler extends MonacoPane; private haskellStgButton: JQuery; private haskellCmmButton: JQuery; + private clojureMacroExpButton: JQuery; private gccDumpButton: JQuery; private cfgButton: JQuery; private explainButton: JQuery; @@ -269,6 +270,7 @@ export class Compiler extends MonacoPane { + return Components.getClojureMacroExpViewWith( + this.id, + this.source, + this.lastResult?.clojureMacroExpOutput, + this.getCompilerName(), + this.sourceEditorId ?? 0, + this.sourceTreeId ?? 0, + ); + }; + const createGccDumpView = () => { return Components.getGccDumpViewWith( this.id, @@ -896,6 +909,17 @@ export class Compiler extends MonacoPane + createClojureMacroExpView(), + ).on('dragStart', hidePaneAdder); + + this.clojureMacroExpButton.on('click', () => { + const insertPoint = + this.hub.findParentRowOrColumn(this.container.parent) || + this.container.layoutManager.root.contentItems[0]; + insertPoint.addChild(createClojureMacroExpView()); + }); + createDragSource(this.container.layoutManager, this.gccDumpButton, () => createGccDumpView()).on( 'dragStart', hidePaneAdder, @@ -1291,6 +1315,7 @@ export class Compiler extends MonacoPane"}, ]; break; + case DiffType.ClojureMacroExpOutput: + output = this.result.clojureMacroExpOutput || [ + {text: "