Skip to content

Commit

Permalink
Prepare API generator for automatic check (#12175)
Browse files Browse the repository at this point in the history
* Add ApiModificationTest

* Fix docs

* Add DocsGenerateOrderTest

* No signature is generated for empty modules.

Or for a module that contains only imports.

* Move some methods to DumpTestUtils

* Implement BindingSorter

* BindingSorter takes care of extension methods

* DocsGenerate respects order of bindings

* DocsEmitSignatures emits correct markdown format

* No signatures for synthetic modules

* Conversion methods are sorted after instance methods

* Ensure generated docs dir has same structure as src dir

* Update Signatures_Spec

* Use ScalaConversions
  • Loading branch information
Akirathan authored Jan 30, 2025
1 parent 2ca2880 commit b0d05a3
Show file tree
Hide file tree
Showing 12 changed files with 981 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package org.enso.compiler.docs;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.enso.compiler.core.ir.Module;
import org.enso.compiler.core.ir.module.scope.Definition;
import org.enso.compiler.core.ir.module.scope.Definition.Data;
import org.enso.compiler.core.ir.module.scope.Definition.Type;
import org.enso.compiler.core.ir.module.scope.definition.Method;
import scala.jdk.javaapi.CollectionConverters;

/**
* Bindings are sorted to categories. Every category is sorted alphabetically.
* Categories are roughly:
* <ul>
* <li>Types</li>
* <li>Instance and static methods on types</li>
* <li>Module methods</li>
* <li>Extension and conversion methods</li>
* </ul>
*/
public final class BindingSorter {
private BindingSorter() {}

/**
* Returns sorted list of bindings defined on the given {@code moduleIr}.
*/
public static List<Definition> sortedBindings(Module moduleIr) {
var bindings = CollectionConverters.asJava(moduleIr.bindings());
var comparator = new BindingComparator(moduleIr);
return bindings.stream().sorted(comparator).toList();
}

public static List<Definition.Data> sortConstructors(List<Definition.Data> constructors) {
var comparator = new ConstructorComparator();
return constructors.stream().sorted(comparator).toList();
}


private static int compareTypes(Type type1, Type type2) {
return type1.name().name().compareTo(type2.name().name());
}


private static final class BindingComparator implements java.util.Comparator<Definition> {
private final Module moduleIr;
private Set<String> typeNames;

private BindingComparator(Module moduleIr) {
this.moduleIr = moduleIr;
}

@Override
public int compare(Definition def1, Definition def2) {
return switch (def1) {
case Method method1 when def2 instanceof Method methods ->
compareMethods(method1, methods);
case Type type1 when def2 instanceof Type type2 ->
compareTypes(type1, type2);
case Type type1 when def2 instanceof Method method2 -> compareTypeAndMethod(type1, method2);
case Method method1 when def2 instanceof Type type2 ->
-compareTypeAndMethod(type2, method1);
default -> throw new AssertionError("unexpected type " + def1.getClass());
};
}

private int compareTypeAndMethod(Type type, Method method) {
if (method.typeName().isDefined()) {
if (isExtensionMethod(method)) {
return -1;
}
var typeName = type.name().name();
var methodTypeName = method.typeName().get().name();
if (typeName.equals(methodTypeName)) {
return -1;
} else {
return typeName.compareTo(methodTypeName);
}
}
return -1;
}


private int compareMethods(Method method1, Method method2) {
return switch (method1) {
case
Method.Explicit explicitMethod1 when method2 instanceof Method.Explicit explicitMethod2 -> {
if (explicitMethod1.isPrivate() != explicitMethod2.isPrivate()) {
if (explicitMethod1.isPrivate()) {
yield 1;
} else {
yield -1;
}
}
if (isExtensionMethod(explicitMethod1) != isExtensionMethod(explicitMethod2)) {
if (isExtensionMethod(explicitMethod1)) {
yield 1;
} else {
yield -1;
}
}
var type1 = explicitMethod1.methodReference().typePointer();
var type2 = explicitMethod2.methodReference().typePointer();
if (type1.isDefined() && type2.isDefined()) {
// Both methods are instance or static methods - compare by type name
var typeName1 = type1.get().name();
var typeName2 = type2.get().name();
if (typeName1.equals(typeName2)) {
// Methods are defined on the same type
yield explicitMethod1.methodName().name()
.compareTo(explicitMethod2.methodName().name());
} else {
yield type1.get().name().compareTo(type2.get().name());
}
} else if (type1.isDefined() && !type2.isDefined()) {
// Instance or static methods on types have precedence over module methods
yield -1;
} else if (!type1.isDefined() && type2.isDefined()) {
yield 1;
}
assert !type1.isDefined() && !type2.isDefined();
yield explicitMethod1.methodName().name()
.compareTo(explicitMethod2.methodName().name());
}
// Comparison of conversion methods is not supported.
case Method.Conversion conversion1 when method2 instanceof Method.Conversion conversion2 ->
0;
case Method.Explicit explicit when method2 instanceof Method.Conversion -> -1;
case Method.Conversion conversion when method2 instanceof Method.Explicit -> 1;
default -> throw new AssertionError(
"Unexpected type: method1=%s, method2=%s".formatted(method1.getClass(),
method2.getClass()));
};
}

/**
* An extension method is a method that is defined on a type that is defined outside the
* current module.
*/
private boolean isExtensionMethod(Method method) {
if (method.typeName().isDefined()) {
var typeName = method.typeName().get().name();
return !typeNamesInModule().contains(typeName);
}
return false;
}

private Set<String> typeNamesInModule() {
if (typeNames == null) {
typeNames = new HashSet<>();
moduleIr.bindings().foreach(binding -> {
if (binding instanceof Definition.Type type) {
typeNames.add(type.name().name());
}
return null;
});
}
return typeNames;
}
}

private static final class ConstructorComparator implements java.util.Comparator<Definition.Data> {

@Override
public int compare(Data cons1, Data cons2) {
return cons1.name().name().compareTo(cons2.name().name());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@ public boolean visitUnknown(IR ir, PrintWriter w) throws IOException {

@Override
public boolean visitModule(QualifiedName name, Module module, PrintWriter w) throws IOException {
w.println("## Enso Signatures 1.0");
w.println("## module " + name);
return true;
if (isEmpty(module)) {
return false;
} else {
w.println("## Enso Signatures 1.0");
w.println("## module " + name);
return true;
}
}

@Override
public void visitMethod(Definition.Type t, Method.Explicit m, PrintWriter w) throws IOException {
if (t != null) {
w.append(" - ");
} else {
w.append("- ");
if (m.typeName().isDefined()) {
var fqn = DocsUtils.toFqnOrSimpleName(m.typeName().get());
w.append(fqn + ".");
Expand All @@ -43,6 +48,7 @@ public void visitMethod(Definition.Type t, Method.Explicit m, PrintWriter w) thr
public void visitConversion(Method.Conversion c, PrintWriter w) throws IOException {
assert c.typeName().isDefined() : "Conversions need type name: " + c;
var fqn = DocsUtils.toFqnOrSimpleName(c.typeName().get());
w.append("- ");
w.append(fqn + ".");
w.append(DocsVisit.toSignature(c));
w.append(" -> ").println(fqn);
Expand All @@ -64,4 +70,8 @@ public void visitConstructor(Definition.Type t, Definition.Data d, PrintWriter w
throws IOException {
w.println(" - " + DocsVisit.toSignature(d));
}

private static boolean isEmpty(Module mod) {
return mod.bindings().isEmpty() && mod.exports().isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.enso.compiler.docs;

import static org.enso.scala.wrapper.ScalaConversions.asJava;
import static org.enso.scala.wrapper.ScalaConversions.asScala;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.IdentityHashMap;
Expand All @@ -10,8 +13,6 @@
import org.enso.compiler.core.ir.module.scope.definition.Method;
import org.enso.filesystem.FileSystem;
import org.enso.pkg.QualifiedName;
import scala.collection.immutable.Seq;
import scala.jdk.CollectionConverters;

/** Generator of documentation for an Enso project. */
public final class DocsGenerate {
Expand All @@ -32,28 +33,53 @@ public static <File> File write(
DocsVisit visitor, org.enso.pkg.Package<File> pkg, Iterable<CompilerContext.Module> modules)
throws IOException {
var fs = pkg.fileSystem();
var docs = fs.getChild(pkg.root(), "docs");
var api = fs.getChild(docs, "api");
fs.createDirectories(api);
var apiDir = defaultOutputDir(pkg);
fs.createDirectories(apiDir);

for (var module : modules) {
if (module.isSynthetic()) {
continue;
}
var ir = module.getIr();
assert ir != null : "need IR for " + module;
if (ir.isPrivate()) {
continue;
}
var moduleName = module.getName();
var dir = createPkg(fs, api, moduleName);
var dir = createDirs(fs, apiDir, stripNamespace(moduleName));
var md = fs.getChild(dir, moduleName.item() + ".md");
try (var mdWriter = fs.newBufferedWriter(md);
var pw = new PrintWriter(mdWriter)) {
visitModule(visitor, moduleName, ir, pw);
}
}
return apiDir;
}

public static <File> File defaultOutputDir(org.enso.pkg.Package<File> pkg) {
var fs = pkg.fileSystem();
var docs = fs.getChild(pkg.root(), "docs");
var api = fs.getChild(docs, "api");
return api;
}

private static <File> File createPkg(FileSystem<File> fs, File root, QualifiedName pkg)
/**
* Strips namespace part from the given qualified {@code name}.
*
* @param name
*/
private static QualifiedName stripNamespace(QualifiedName name) {
if (!name.isSimple()) {
var path = name.pathAsJava();
assert path.size() >= 2;
var dropped = path.subList(2, path.size());
return new QualifiedName(asScala(dropped), name.item());
} else {
return name;
}
}

private static <File> File createDirs(FileSystem<File> fs, File root, QualifiedName pkg)
throws IOException {
var dir = root;
for (var item : pkg.pathAsJava()) {
Expand All @@ -68,7 +94,7 @@ public static void visitModule(
var dispatch = DocsDispatch.create(visitor, w);

if (dispatch.dispatchModule(moduleName, ir)) {
var moduleBindings = asJava(ir.bindings());
var moduleBindings = BindingSorter.sortedBindings(ir);
var alreadyDispatched = new IdentityHashMap<IR, IR>();
for (var b : moduleBindings) {
if (alreadyDispatched.containsKey(b)) {
Expand All @@ -77,7 +103,7 @@ public static void visitModule(
switch (b) {
case Definition.Type t -> {
if (dispatch.dispatchType(t)) {
for (var d : asJava(t.members())) {
for (var d : BindingSorter.sortConstructors(asJava(t.members()))) {
if (!d.isPrivate()) {
dispatch.dispatchConstructor(t, d);
}
Expand Down Expand Up @@ -111,8 +137,4 @@ public static void visitModule(
}
}
}

private static <T> Iterable<T> asJava(Seq<T> seq) {
return CollectionConverters.IterableHasAsJava(seq).asJava();
}
}
Loading

0 comments on commit b0d05a3

Please sign in to comment.