Skip to content

Commit

Permalink
Macros before functions in decompiled source
Browse files Browse the repository at this point in the history
Also section headers
  • Loading branch information
KronicDeth committed Jan 7, 2017
1 parent d66ca54 commit e66a4d8
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 101 deletions.
43 changes: 22 additions & 21 deletions src/org/elixir_lang/beam/Decompiler.java
Expand Up @@ -17,6 +17,8 @@

import java.util.*;

import static org.elixir_lang.psi.call.name.Function.DEF;
import static org.elixir_lang.psi.call.name.Function.DEFMACRO;
import static org.elixir_lang.psi.call.name.Module.ELIXIR_PREFIX;

public class Decompiler implements BinaryFileDecompiler {
Expand Down Expand Up @@ -70,33 +72,32 @@ private static void appendExports(@NotNull StringBuilder decompiled, @NotNull Be
private static void appendExports(@NotNull StringBuilder decompiled,
@NotNull Exports exports,
@NotNull Atoms atoms) {
Pair<SortedMap<String, SortedMap<Integer, Export>>, SortedSet<Export>>
exportByArityByNameNamelessExportSet = exports.exportByArityByName(atoms);
SortedMap<String, SortedMap<Integer, Export>> exportByArityByName =
exportByArityByNameNamelessExportSet.first;

boolean first = true;
for (SortedMap.Entry<String, SortedMap<Integer, Export>> nameExportByArity :
exportByArityByName.entrySet()) {
String name = nameExportByArity.getKey();

SortedMap<Integer, Export> exportByArity = nameExportByArity.getValue();

for (SortedMap.Entry<Integer, Export> arityExport : exportByArity.entrySet()) {
if (!first) {
decompiled.append("\n");
}
SortedSet<MacroNameArity> macroNameAritySortedSet = exports.macroNameAritySortedSet(atoms);
MacroNameArity lastMacroNameArity = null;

for (MacroNameArity macroNameArity : macroNameAritySortedSet) {
if (lastMacroNameArity == null) {
appendHeader(decompiled, "Macros");
} else if (lastMacroNameArity.macro.equals(DEFMACRO) && macroNameArity.macro.equals(DEF)) {
appendHeader(decompiled, "Functions");
}

@Nullable Integer arity = arityExport.getKey();
decompiled.append("\n");

MacroNameArity macroNameArity = new MacroNameArity(name, arity);
appendMacroNameArity(decompiled, macroNameArity);
appendMacroNameArity(decompiled, macroNameArity);

first = false;
}
lastMacroNameArity = macroNameArity;
}
}

private static void appendHeader(@NotNull StringBuilder decompiled, @NotNull String name) {
decompiled
.append("\n")
.append(" # ")
.append(name)
.append("\n");
}

private static void appendMacroNameArity(@NotNull StringBuilder decompiled,
@NotNull MacroNameArity macroNameArity) {
boolean accepted = false;
Expand Down
72 changes: 61 additions & 11 deletions src/org/elixir_lang/beam/MacroNameArity.java
@@ -1,5 +1,6 @@
package org.elixir_lang.beam;

import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand All @@ -9,7 +10,7 @@
/**
* The macro ({@code def} or {@code defmacro}), name of the call definition and arity to define an export
*/
public class MacroNameArity {
public class MacroNameArity implements Comparable<MacroNameArity> {
/*
* CONSTANTS
*/
Expand Down Expand Up @@ -37,30 +38,79 @@ public class MacroNameArity {
* Elixir macros are defined as Erlang functions that take an extra Caller argument, so the exportArity is +1 for
* macros compared to this arity, which is the number of parameters to the call definition head passed to
* {@code defmacro}.
*
* Only {@code null} if {@code exportArity} is {@code null}, which would indicate a malformed {@code .beam}.
*/
@Nullable
@NotNull
public final Integer arity;

/*
* Constructors
*/

public MacroNameArity(@NotNull String exportName, @Nullable Integer exportArity) {
public MacroNameArity(@NotNull String exportName, int exportArity) {
if (exportName.startsWith(MACRO_EXPORT_PREFIX)) {
macro = DEFMACRO;
name = exportName.substring(MACRO_EXPORT_PREFIX.length());

if (exportArity != null) {
arity = exportArity - 1;
} else {
arity = null;
}
arity = exportArity - 1;
} else {
macro = DEF;
name = exportName;
arity = exportArity;
}
}

/*
* Instance Methods
*/

@Override
public int compareTo(@NotNull MacroNameArity other) {
int comparison = compareMacroTo(other.macro);

if (comparison == 0) {
comparison = compareNameTo(other.name);

if (comparison == 0) {
comparison = compareArityTo(other.arity);
}
}

return comparison;
}

@Contract(pure = true)
private int compareArityTo(int otherArity) {
return Double.compare(arity, otherArity);
}

@Contract(pure = true)
private int compareMacroTo(@NotNull String otherMacro) {
int comparison;

if (macro.equals(DEFMACRO)) {
if (otherMacro.equals(DEFMACRO)) {
comparison = 0;
} else if (otherMacro.equals(DEF)) {
comparison = - 1;
} else {
throw new IllegalArgumentException("otherMacro must be \"" + DEFMACRO + "\" or \"" + DEF + "\"");
}
} else if (macro.equals(DEF)) {
if (otherMacro.equals(DEFMACRO)) {
comparison = 1;
} else if (otherMacro.equals(DEF)) {
comparison = 0;
} else {
throw new IllegalArgumentException("otherMacro must be \"" + DEFMACRO + "\" or \"" + DEF + "\"");
}
} else {
throw new IllegalArgumentException("macro must be \"" + DEFMACRO + "\" or \"" + DEF + "\"");
}

return comparison;
}

@Contract(pure = true)
private int compareNameTo(@NotNull String otherName) {
return name.compareTo(otherName);
}
}
43 changes: 26 additions & 17 deletions src/org/elixir_lang/beam/chunk/Exports.java
Expand Up @@ -2,6 +2,7 @@

import com.intellij.openapi.util.Pair;
import gnu.trove.THashSet;
import org.elixir_lang.beam.MacroNameArity;
import org.elixir_lang.beam.chunk.exports.Export;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -67,28 +68,36 @@ public static Exports from(@NotNull Chunk chunk) {
return exports;
}

public Pair<SortedMap<String, SortedMap<Integer, Export>>, SortedSet<Export>> exportByArityByName(@NotNull Atoms atoms) {
SortedMap<String, SortedMap<Integer, Export>> exportByArityByName =
new TreeMap<String, SortedMap<Integer, Export>>();
SortedSet<Export> nameless = new TreeSet<Export>();
/**
* Set of {@link MacroNameArity} sorted as
* 1. {@link MacroNameArity#macro}, so that {@code defmacro} is before {@code def}
* 2. {@link MacroNameArity#name} is sorted alphabetically
* 3. {@link MacroNameArity#arity} is sorted ascending
*
* @param atoms used to look up the names of the {@link Export}s in {@link #exportCollection}.
* @return The sorted set will be the same size as {@link #exportCollection} unless {@link Export#name(Atoms)}
* returns {@code null} for some {@link Export}s.
*/
@NotNull
public SortedSet<MacroNameArity> macroNameAritySortedSet(@NotNull Atoms atoms) {
SortedSet<MacroNameArity> macroNameAritySortedSet = new TreeSet<MacroNameArity>();

for (Export export : exportCollection) {
String name = export.name(atoms);

if (name == null) {
nameless.add(export);
} else {
SortedMap<Integer, Export> exportByArity = exportByArityByName.get(name);

if (exportByArity == null) {
exportByArity = new TreeMap<Integer, Export>();
exportByArityByName.put(name, exportByArity);
}
String exportName = export.name(atoms);

exportByArity.put((int) export.arity(), export);
if (exportName != null) {
MacroNameArity macroNameArity = new MacroNameArity(exportName, (int) export.arity());
macroNameAritySortedSet.add(macroNameArity);
}
}

return pair(exportByArityByName, nameless);
return macroNameAritySortedSet;
}

/**
* The number of exports
*/
public int size() {
return exportCollection.size();
}
}
21 changes: 3 additions & 18 deletions src/org/elixir_lang/beam/psi/BeamFileImpl.java
Expand Up @@ -132,25 +132,10 @@ private static void buildCallDefinitions(@NotNull ModuleStub parentStub, @NotNul
Exports exports = beam.exports();

if (exports != null) {
Pair<SortedMap<String, SortedMap<Integer, Export>>, SortedSet<Export>>
exportByArityByNameNamelessExports = exports.exportByArityByName(atoms);
SortedSet<MacroNameArity> macroNameAritySortedSet = exports.macroNameAritySortedSet(atoms);

SortedMap<String, SortedMap<Integer, Export>> exportByArityByName =
exportByArityByNameNamelessExports.first;

for (SortedMap.Entry<String, SortedMap<Integer, Export>> nameExportByArity :
exportByArityByName.entrySet()) {
String exportName = nameExportByArity.getKey();

SortedMap<Integer, Export> exportByArity = nameExportByArity.getValue();

for (SortedMap.Entry<Integer, Export> arityExport : exportByArity.entrySet()) {
MacroNameArity macroNameArity = new MacroNameArity(exportName, arityExport.getKey());

if (macroNameArity.arity != null) {
buildCallDefinition(parentStub, macroNameArity);
}
}
for (MacroNameArity macroNameArity : macroNameAritySortedSet) {
buildCallDefinition(parentStub, macroNameArity);
}
}
}
Expand Down
68 changes: 34 additions & 34 deletions tests/org/elixir_lang/beam/BeamTest.java
@@ -1,20 +1,16 @@
package org.elixir_lang.beam;

import com.ericsson.otp.erlang.OtpErlangDecodeException;
import com.intellij.openapi.util.Pair;
import gnu.trove.THashSet;
import com.intellij.openapi.util.Condition;
import com.intellij.util.containers.ContainerUtil;
import org.elixir_lang.beam.chunk.Atoms;
import org.elixir_lang.beam.chunk.Exports;
import org.elixir_lang.beam.chunk.exports.Export;
import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Test;

import java.io.*;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.*;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
Expand All @@ -41,27 +37,28 @@ public void elixirModule() throws IOException, OtpErlangDecodeException {

assertNotNull(exports);

Pair<SortedMap<String, SortedMap<Integer, Export>>, SortedSet<Export>> exportByArityByNameNamelessExportSet =
exports.exportByArityByName(atoms);
int exportCount = exports.size();

assertNotNull(exportByArityByNameNamelessExportSet);
assertTrue("There are no exports", exportCount > 0);

Set<Export> namelessExportSet = exportByArityByNameNamelessExportSet.second;
SortedSet<MacroNameArity> macroNameAritySortedSet = exports.macroNameAritySortedSet(atoms);

assertTrue("There are nameless exports", namelessExportSet.isEmpty());
assertEquals("There are nameless exports", exportCount, macroNameAritySortedSet.size());

SortedMap<String, SortedMap<Integer, Export>> exportByArityByName = exportByArityByNameNamelessExportSet.first;

// a name with multiple arities
SortedMap<Integer, Export> exportByArity = exportByArityByName.get("node");

assertNotNull(exportByArity);
List<MacroNameArity> nodes = ContainerUtil.filter(
macroNameAritySortedSet,
new Condition<MacroNameArity>() {
@Override
public boolean value(MacroNameArity macroNameArity) {
return "node".equals(macroNameArity.name);
}
}
);

Set<Integer> expectedAritySet = new THashSet<Integer>();
expectedAritySet.add(0);
expectedAritySet.add(1);
assertEquals(nodes.size(), 2);

assertEquals("node arities do not match", expectedAritySet, exportByArity.keySet());
assertEquals(0, (int) nodes.get(0).arity);
assertEquals(1, (int) nodes.get(1).arity);
}

@Test
Expand All @@ -80,24 +77,27 @@ public void erlangModule() throws IOException, OtpErlangDecodeException {

assertNotNull(exports);

Pair<SortedMap<String, SortedMap<Integer, Export>>, SortedSet<Export>> exportByArityByNameNamelessExportSet =
exports.exportByArityByName(atoms);
int exportCount = exports.size();

assertNotNull(exportByArityByNameNamelessExportSet);
assertTrue("There are no exports", exportCount > 0);

Set<Export> namelessExportSet = exportByArityByNameNamelessExportSet.second;
SortedSet<MacroNameArity> macroNameAritySortedSet = exports.macroNameAritySortedSet(atoms);

assertTrue("There are nameless exports", namelessExportSet.isEmpty());
assertEquals("There are nameless exports", exportCount, macroNameAritySortedSet.size());

SortedMap<String, SortedMap<Integer, Export>> exportByArityByName = exportByArityByNameNamelessExportSet.first;

Map<Integer, Export> exportByArity = exportByArityByName.get("extract");

assertNotNull(exportByArity);
List<MacroNameArity> extracts = ContainerUtil.filter(
macroNameAritySortedSet,
new Condition<MacroNameArity>() {
@Override
public boolean value(MacroNameArity macroNameArity) {
return "extract".equals(macroNameArity.name);
}
}
);

Export export = exportByArity.get(6);
assertEquals(1, extracts.size());

assertNotNull(export);
assertEquals(6, (int) extracts.get(0).arity);
}

private Beam beam(@NotNull String baseName) throws IOException, OtpErlangDecodeException {
Expand Down
5 changes: 5 additions & 0 deletions tests/org/elixir_lang/beam/DecompilerTest.java
Expand Up @@ -32,6 +32,9 @@ public void testIssue575() throws IOException, OtpErlangDecodeException {
assertEquals(
"# Source code recreated from a .beam file by IntelliJ Elixir\n" +
"defmodule Bitwise do\n" +
"\n" +
" # Macros\n" +
"\n" +
" defmacro left &&& right do\n" +
" # body not decompiled\n" +
" end\n" +
Expand Down Expand Up @@ -84,6 +87,8 @@ public void testIssue575() throws IOException, OtpErlangDecodeException {
" # body not decompiled\n" +
" end\n" +
"\n" +
" # Functions\n" +
"\n" +
" def __info__(p0) do\n" +
" # body not decompiled\n" +
" end\n" +
Expand Down

0 comments on commit e66a4d8

Please sign in to comment.