blob: f6687372e85bbb1ffd340808f060d7b1c52f3ddf [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// This transformations outputs a Coq definitions to resemble the Dart syntax
// tree.
//
// Classes that are supposed to be converted are marked with the `@coq`
// annotation. Some fields within these classes will be converted according to
// the following rules:
//
// 1. A field labeled `@nocoq` will not be converted.
//
// 2. A field labeled `@coq` or `@coqopt` will be converted unless it's type is
// unuspported, in which case an exception will be raised.
//
// 3. An unannotated field will be converted if it has type `T` or `List<T>`,
// where `T` is a class marked for conversion.
//
// Classes marked `@coqref` are referenced by natural numbered IDs wherever
// fields of their type appear, and we have a finite map resolving these IDs.
// This breaks cycles in the AST graph and provides a way to identify nodes for
// substitution.
//
// All classes with data members are given an induction type definition with a
// single constructor, holding the data members of that class. Classes with
// subclasses are also given an inductive definition enumerating all (converted)
// subclasses. Their data-member type definition will appear inline in the data
// member definitions for their leaf subclasses. Due to this representation,
// converted classes with subclasses must be abstract.
//
// Since the whole syntax tree is mutually recursive, all the types are dumped
// into one big "Inductive ... with ... with ... " definition.
library kernel.transformations.coq;
import 'dart:io';
import '../ast.dart';
import '../coq_annot.dart' as coq_annot;
import '../core_types.dart' show CoreTypes;
enum RefStyle { direct, identified }
enum FieldStyle { list, optional, normal }
class CoqFieldInfo {
// Only one of these two may be non-null.
final CoqClassInfo type;
final String primitiveCoqType;
final String dartName;
final FieldStyle style;
bool definitional = false;
String get innerRefType => type == null ? primitiveCoqType : type.refType;
String get refType {
if (type != null) {
var rt = definitional ? type.coqType : type.refType;
if (style == FieldStyle.list) {
return "list $rt";
} else if (style == FieldStyle.optional) {
return "option $rt";
} else {
return rt;
}
} else {
if (style == FieldStyle.list) {
return "list " + primitiveCoqType;
} else if (style == FieldStyle.optional) {
return "option " + primitiveCoqType;
} else {
return primitiveCoqType;
}
}
}
CoqFieldInfo(this.dartName, this.type, this.primitiveCoqType, this.style);
}
class CoqClassInfo {
final Class cls;
final RefStyle refStyle;
List<CoqClassInfo> subs = <CoqClassInfo>[];
List<CoqFieldInfo> fields = <CoqFieldInfo>[];
bool needsOption = false;
bool needsList = false;
String get coqType => coqifyName(cls.name);
String get coqTypeCaps => coqifyName(cls.name, capitalize: true);
String get abbrevName => abbrev(cls.name);
String get abbrevNameCaps => abbrev(cls.name, capitalize: true);
String get refType => refStyle == RefStyle.direct ? coqType : "nat";
CoqClassInfo(this.cls, this.refStyle);
Iterable<CoqClassInfo> supersWithData(CoqLibInfo info) sync* {
for (Supertype st = cls.supertype;
st != null;
st = st.classNode.supertype) {
Class spr = st.classNode;
var sprInfo = info.classes[spr];
if (sprInfo == null) break;
if (sprInfo.fields.length == 0) continue;
yield sprInfo;
}
}
}
class CoqLibInfo {
final Map<Class, CoqClassInfo> classes = <Class, CoqClassInfo>{};
CoqLibInfo();
}
// Get the number associated with the annotation from `coq_annot.dart` on a
// [Field] or [Class] if one exists, and 0 otherwise. Throws an exception if
// invalid or multiple annotations are discoverted.
int getCoqAnnot(NamedNode N, List<Expression> annotations) {
if (coq_annot.coqEnums.contains("$N")) {
return coq_annot.coq;
}
int annot = 0;
for (var A in annotations) {
if (A is StaticGet) {
var target = A.targetReference.node;
var parent = target.parent;
if (parent is Library) {
if (parent is NamedNode && parent.name == "kernel.coq_annot") {
if (target is Field) {
if (annot != 0) {
throw new Exception("ERROR: Multiple Coq annotations on ${N}!");
}
switch ("${target.name}") {
case "coq":
annot = coq_annot.coq;
break;
case "coqref":
annot = coq_annot.coqref;
break;
case "nocoq":
annot = coq_annot.nocoq;
break;
case "coqopt":
annot = coq_annot.coqopt;
break;
case "coqsingle":
annot = coq_annot.coqsingle;
break;
case "coqdef":
annot = coq_annot.coqdef;
break;
case "coqsingledef":
annot = coq_annot.coqsingledef;
break;
default:
throw new Exception("ERROR: Invalid Coq annotation on ${N}!");
}
} else {
throw new Exception("ERROR: Invalid Coq annotation on ${N}!");
}
}
}
}
}
return annot;
}
// Determine which classes we're going to convert.
class CoqPass1 extends RecursiveVisitor {
CoqLibInfo info;
CoqPass1(this.info);
visitClass(Class C) {
int annot = getCoqAnnot(C, C.annotations);
if (annot == 0) return;
if (annot != coq_annot.coq && annot != coq_annot.coqref) {
throw new Exception("ERROR: Invalid Coq annotation on ${C.name}!");
}
info.classes[C] = new CoqClassInfo(
C, annot == coq_annot.coq ? RefStyle.direct : RefStyle.identified);
}
}
// Determine which fields we're going to convert and which classes have
// converted subclasses.
class CoqPass2 extends RecursiveVisitor {
CoqLibInfo info;
CoreTypes coreTypes;
CoqPass2(this.info, this.coreTypes);
CoqClassInfo currentClass = null;
String getCoqPrimitiveType(Class cls) {
if (cls == coreTypes.stringClass) {
return "string";
} else if (cls == coreTypes.boolClass) {
return "bool";
} else if (cls == coreTypes.intClass) {
return "nat";
} else {
return null;
}
}
visitClass(Class C) {
var classInfo = info.classes[C];
if (classInfo == null) return;
if (C.supertype != null) {
Class spr = C.supertype.classNode;
var sprInfo = info.classes[spr];
if (sprInfo != null) {
sprInfo.subs.add(classInfo);
}
}
currentClass = classInfo;
C.visitChildren(this);
currentClass = null;
}
visitField(Field F) {
if (currentClass == null) return;
int annot = getCoqAnnot(F, F.annotations);
if (annot == coq_annot.nocoq) return;
var type = F.type;
if (type is! InterfaceType) return;
var interfaceType = type as InterfaceType;
var cls = null;
bool isList = false;
if (interfaceType.classNode == coreTypes.listClass) {
isList = annot != coq_annot.coqsingle && annot != coq_annot.coqsingledef;
if (interfaceType.typeArguments.length != 1) return;
var elemType = interfaceType.typeArguments[0];
if (elemType is InterfaceType) {
cls = elemType.classNode;
} else if (annot == coq_annot.coqopt) {
throw new Exception("ERROR: Field $F may not be optional.");
}
} else {
cls = interfaceType.classNode;
}
FieldStyle style = isList
? FieldStyle.list
: (annot == coq_annot.coqopt ? FieldStyle.optional : FieldStyle.normal);
CoqFieldInfo fieldInfo = null;
var primitive = getCoqPrimitiveType(cls);
var fieldName = F.name.name;
if (primitive != null) {
if (annot == 0) return;
fieldInfo = new CoqFieldInfo(fieldName, null, primitive, style);
} else {
var fieldClassInfo = info.classes[cls];
if (fieldClassInfo == null) {
return;
}
fieldInfo = new CoqFieldInfo(fieldName, fieldClassInfo, null, style);
if (style == FieldStyle.optional) {
fieldClassInfo.needsOption = true;
} else if (style == FieldStyle.list) {
fieldClassInfo.needsList = true;
}
if (annot == coq_annot.coqdef || annot == coq_annot.coqsingledef) {
fieldInfo.definitional = true;
}
}
currentClass.fields.add(fieldInfo);
}
}
// Conventional Coq code uses underscores instead of camelCase as Dart code
// does. This function converts the Dart convention to the Coq convention.
final coqReserved = <String>["let"];
String coqifyName(String S, {bool capitalize: false}) {
List<int> codes = <int>[];
bool skipUnderscore = false;
for (int i = 0; i < S.length; ++i) {
var c = S.codeUnitAt(i);
if (c >= "A".codeUnits[0] && c <= "Z".codeUnits[0]) {
if (i > 0 && !skipUnderscore) {
codes.add("_".codeUnitAt(0));
}
if (!capitalize) c += ("a".codeUnits[0] - "A".codeUnits[0]);
codes.add(c);
} else {
codes.add(c);
}
skipUnderscore = c == "_".codeUnits[0];
}
var name = new String.fromCharCodes(codes);
if (coqReserved.contains(name)) {
name = "dart_" + name;
}
return name;
}
// Give an abbreviation of a identifying by combining the capital letters of the
// identifier. For example, "ProcedureKind" becomes "pk" or "PK".
String abbrev(String S, {bool capitalize: false}) {
List<int> codes = <int>[];
for (var c in S.codeUnits)
if (c >= 65 && c <= 90) codes.add(capitalize ? c : c + 32);
return new String.fromCharCodes(codes);
}
void outputCoqImports() {
print("""
Require Import Common.
""");
}
void outputCoqSyntax(CoqLibInfo info) {
int defN = 0;
defkw() => defN++ > 0 ? "with" : "Inductive";
for (var classInfo in info.classes.values) {
bool isAbstract = classInfo.subs.length > 0;
Class cls = classInfo.cls;
var coqName = classInfo.coqType;
if (classInfo.cls.isEnum) {
var enums = [];
for (var fld in classInfo.fields) {
if (fld.dartName == "values") continue;
enums.add(coqifyName(fld.dartName, capitalize: true));
}
print("${defkw()} $coqName : Set := ${enums.join(" | ")}\n");
continue;
}
if (!isAbstract || classInfo.fields.length > 0) {
var suffix = isAbstract ? "_data" : "";
var dataTypeName = coqName + suffix;
var dataCtorName = coqifyName(cls.name, capitalize: true);
print("${defkw()} ${dataTypeName} : Set :=");
print(" | ${dataCtorName} : ");
// Insert fields for superclasses.
int arw = 0;
arrow() => arw++ == 0 ? "" : "-> ";
if (classInfo.refStyle == RefStyle.identified) {
print(" ${arrow()}nat");
}
for (var sprInfo in classInfo.supersWithData(info)) {
print(" ${arrow()}${sprInfo.coqType}_data");
}
for (CoqFieldInfo fld in classInfo.fields) {
print(" ${arrow()}${fld.refType} (* ${fld.dartName} *)");
}
print(" ${arrow()}$dataTypeName\n");
}
if (classInfo.subs.length > 0) {
print("${defkw()} $coqName : Set :=");
var abbrevName = abbrev(cls.name, capitalize: true);
for (var sub in classInfo.subs) {
var subTypeName = coqifyName(sub.cls.name);
var ctorName = coqifyName(sub.cls.name, capitalize: true);
print(" | ${abbrevName}_${ctorName} : ${subTypeName} -> $coqName");
}
print("\n");
}
}
print(".\n");
}
void outputCoqStore(info) {
print("Record ast_store : Type := Ast_Store {");
for (var classInfo in info.classes.values) {
if (classInfo.refStyle != RefStyle.identified) continue;
print(" ${classInfo.abbrevName}_refs : NatMap.t ${classInfo.coqType};");
}
print("}.\n");
}
void outputCoqSyntaxValidity(CoqLibInfo info) {
int defN = 0;
defkw() => defN++ > 0 ? "with" : "Fixpoint";
validityPredicate(CoqClassInfo CI) {
if (CI.refStyle == RefStyle.identified) {
var mapName = "${CI.abbrevName}_refs";
return (X) => "NatMap.In $X ($mapName ast)";
} else {
return (X) => "${CI.coqType}_validity ast $X";
}
}
for (var CI in info.classes.values) {
stdout.write(
"${defkw()} ${CI.coqType}_validity (ast : ast_store) (T : ${CI.coqType}) {struct T} : Prop :=");
if (CI.cls.isEnum) {
stdout.write(" True\n");
continue;
} else {
stdout.write("\n");
}
print(" match T with");
for (var sub in CI.subs) {
print(
" | ${CI.abbrevNameCaps}_${sub.coqTypeCaps} ST => ${sub.coqType}_validity ast ST");
}
if (CI.subs.length > 0) {
print("end");
if (CI.fields.length == 0) continue;
print(
"${defkw()} ${CI.coqType}_data_validity (ast : ast_store) (T : ${CI.coqType}_data) {struct T}: Prop :=");
print(" match T with");
}
int i = 0;
var fieldNames = [];
var validityClauses = [];
for (var SI in CI.supersWithData(info)) {
var f = "f${i++}";
fieldNames.add(f);
validityClauses.add("${SI.coqType}_data_validity ast $f");
}
for (var fld in CI.fields) {
if (fld.type == null) {
fieldNames.add("_");
continue;
}
var f = "f${i++}";
fieldNames.add(f);
var pred;
if (fld.style == FieldStyle.normal) {
pred = validityPredicate(fld.type)(f);
} else if (fld.style == FieldStyle.list) {
pred = "${fld.type.coqType}_list_validity ast $f";
} else if (fld.style == FieldStyle.optional) {
pred = "${fld.type.coqType}_option_validity ast $f";
}
validityClauses.add(pred);
}
var clause = "True";
if (validityClauses.length > 0) {
clause = validityClauses.join(" /\\\n ");
}
print(
" | ${CI.coqTypeCaps} ${fieldNames.join(" ")} =>\n $clause");
print(" end");
}
for (var CI in info.classes.values) {
var pred = validityPredicate(CI)("X");
if (CI.needsList) {
var def = """
with ${CI.coqType}_list_validity (ast : ast_store) (L : ${CI.coqType}_list) {struct L} : Prop :=
match L with
| ${CI.coqType}_nil => True
| ${CI.coqType}_cons X XS => $pred /\\ ${CI.coqType}_list_validity ast XS
end""";
print(def);
}
if (CI.needsOption) {
var def = """
with ${CI.coqType}_option_validity (ast : ast_store) (O : ${CI.coqType}_option) {struct O} : Prop :=
match O with
| ${CI.coqType}_none => True
| ${CI.coqType}_some X => $pred
end""";
print(def);
}
}
print(".\n");
}
void outputCoqStoreValidity(CoqLibInfo info) {
var clauses = [];
for (var CI in info.classes.values) {
if (CI.refStyle != RefStyle.identified) continue;
var mapName = "${CI.abbrevName}_refs";
clauses.add(
" forall (n : nat), forall (X : ${CI.coqType}), NatMap.MapsTo n X ($mapName ast) -> ${CI.coqType}_validity ast X");
}
var clause = clauses.join(" /\\\n");
print(
"Definition ast_store_validity (ast : ast_store) : Prop := \n$clause\n.");
}
Component transformComponent(CoreTypes coreTypes, Component component) {
for (Library lib in component.libraries) {
// TODO(30610): Ideally we'd output to the file in the coq annotation on the
// library name, but currently fasta throws away annotations on libraries.
// Instead, we just special case "kernel.ast" and output to stdout.
if ("$lib" != "kernel.ast") continue;
var info = new CoqLibInfo();
(new CoqPass1(info)).visitLibrary(lib);
(new CoqPass2(info, coreTypes)).visitLibrary(lib);
outputCoqImports();
outputCoqSyntax(info);
}
return component;
}