Implement SKIP/FREEZE in parser/TreeToIr (#3942)

See: https://www.pivotaltracker.com/story/show/183919788

# Important Notes
`SKIP` would be simpler if implemented in the parser, but there is some work needed before the Rust AST and Java IR are able to represent the results of macro-expansion: https://www.pivotaltracker.com/story/show/184004555
This commit is contained in:
Kaz Wesley 2022-12-20 09:32:59 -08:00 committed by GitHub
parent 579d3fc397
commit d24019aa57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 171 additions and 6 deletions

View File

@ -1,6 +1,7 @@
package org.enso.compiler;
import java.util.ArrayList;
import java.util.Objects;
import java.util.UUID;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.IR$Application$Literal$Sequence;
@ -77,6 +78,8 @@ import scala.jdk.javaapi.CollectionConverters;
final class TreeToIr {
static final TreeToIr MODULE = new TreeToIr();
static final String SKIP_MACRO_IDENTIFIER = "SKIP";
static final String FREEZE_MACRO_IDENTIFIER = "FREEZE";
private TreeToIr() {
}
@ -727,7 +730,15 @@ final class TreeToIr {
sep = "_";
}
var fn = new IR$Name$Literal(fnName.toString(), true, Option.empty(), meta(), diag());
var fullName = fnName.toString();
if (fullName.equals(FREEZE_MACRO_IDENTIFIER)) {
yield translateExpression(app.getSegments().get(0).getBody(), false);
} else if (fullName.equals(SKIP_MACRO_IDENTIFIER)) {
var body = app.getSegments().get(0).getBody();
var subexpression = Objects.requireNonNullElse(applySkip(body), body);
yield translateExpression(subexpression, false);
}
var fn = new IR$Name$Literal(fullName, true, Option.empty(), meta(), diag());
checkArgs(args);
yield new IR$Application$Prefix(fn, args.reverse(), false, getIdentifiedLocation(tree), meta(), diag());
}
@ -916,6 +927,71 @@ final class TreeToIr {
};
}
Tree applySkip(Tree tree) {
// Termination:
// Every iteration either breaks, or reduces [`tree`] to a substructure of [`tree`].
var done = false;
while (!done && tree != null) {
tree = switch (tree) {
case Tree.MultiSegmentApp app
when FREEZE_MACRO_IDENTIFIER.equals(app.getSegments().get(0).getHeader().codeRepr()) ->
app.getSegments().get(0).getBody();
case Tree.Invalid ignored -> null;
case Tree.BodyBlock ignored -> null;
case Tree.Number ignored -> null;
case Tree.Wildcard ignored -> null;
case Tree.AutoScope ignored -> null;
case Tree.ForeignFunction ignored -> null;
case Tree.Import ignored -> null;
case Tree.Export ignored -> null;
case Tree.TypeDef ignored -> null;
case Tree.TypeSignature ignored -> null;
case Tree.ArgumentBlockApplication app -> app.getLhs();
case Tree.OperatorBlockApplication app -> app.getLhs();
case Tree.OprApp app -> app.getLhs();
case Tree.Ident ident when ident.getToken().isTypeOrConstructor() -> null;
case Tree.Ident ignored -> {
done = true;
yield tree;
}
case Tree.Group ignored -> {
done = true;
yield tree;
}
case Tree.UnaryOprApp app -> app.getRhs();
case Tree.OprSectionBoundary section -> section.getAst();
case Tree.TemplateFunction function -> function.getAst();
case Tree.Annotated annotated -> annotated.getExpression();
case Tree.Documented documented -> documented.getExpression();
case Tree.Assignment assignment -> assignment.getExpr();
case Tree.TypeAnnotated annotated -> annotated.getExpression();
case Tree.DefaultApp app -> app.getFunc();
case Tree.App app when isApplication(app.getFunc()) -> app.getFunc();
case Tree.NamedApp app when isApplication(app.getFunc()) -> app.getFunc();
case Tree.App app -> Objects.requireNonNullElse(applySkip(app.getFunc()), app.getArg());
case Tree.NamedApp app -> Objects.requireNonNullElse(applySkip(app.getFunc()), app.getArg());
case Tree.MultiSegmentApp ignored -> null;
case Tree.TextLiteral ignored -> null;
case Tree.Function ignored -> null;
case Tree.Lambda ignored -> null;
case Tree.CaseOf ignored -> null;
case Tree.Array ignored -> null;
case Tree.Tuple ignored -> null;
default -> null;
};
}
return tree;
}
boolean isApplication(Tree tree) {
return switch (tree) {
case Tree.App ignored -> true;
case Tree.NamedApp ignored -> true;
case Tree.DefaultApp ignored -> true;
default -> false;
};
}
// The `insideTypeAscription` argument replicates an AstToIr quirk. Once the parser
// transition is complete, we should eliminate it, keeping only the `false` branches.
IR.Expression translateType(Tree tree, boolean insideTypeAscription) {

View File

@ -1205,6 +1205,33 @@ public class EnsoCompilerTest {
""");
}
@Test
public void testFreeze() throws Exception {
equivalenceTest("a = x", "a = FREEZE x");
equivalenceTest("a = x+1", "a = FREEZE x+1");
equivalenceTest("a = x + 1", "a = FREEZE x + 1");
equivalenceTest("a = x.f 1", "a = FREEZE x.f 1");
}
@Test
public void testSkip() throws Exception {
equivalenceTest("a = x", "a = SKIP x");
equivalenceTest("a = x", "a = SKIP x+1");
equivalenceTest("a = x", "a = SKIP x + 1");
equivalenceTest("a = x", "a = SKIP x");
equivalenceTest("a = x", "a = SKIP x+y");
equivalenceTest("a = x", "a = SKIP x + y");
equivalenceTest("a = x", "a = SKIP x.f y");
equivalenceTest("a = x", "a = SKIP Std.foo x");
equivalenceTest("a = x", "a = SKIP Std.foo x.f");
equivalenceTest("a = (Std.bar x)", "a = SKIP Std.foo (Std.bar x)");
equivalenceTest("a = x", "a = SKIP FREEZE x");
equivalenceTest("a = x", "a = SKIP FREEZE x + y");
equivalenceTest("a = x", "a = SKIP FREEZE x.f");
equivalenceTest("a = x", "a = SKIP FREEZE x.f y");
}
static String simplifyIR(IR i, boolean noIds, boolean noLocations, boolean lessDocs) {
var txt = i.pretty();
if (noIds) {
@ -1267,15 +1294,12 @@ public class EnsoCompilerTest {
@SuppressWarnings("unchecked")
private static void parseTest(String code, boolean noIds, boolean noLocations, boolean lessDocs) throws IOException {
var src = Source.newBuilder("enso", code, "test-" + Integer.toHexString(code.hashCode()) + ".enso").build();
var ir = ensoCompiler.compile(src);
assertNotNull("IR was generated", ir);
var ir = compile(code);
var oldAst = new Parser().runWithIds(src.getCharacters().toString());
var oldAst = new Parser().runWithIds(code);
var oldIr = AstToIr.translate((ASTOf<Shape>)(Object)oldAst);
Function<IR, String> filter = (f) -> simplifyIR(f, noIds, noLocations, lessDocs);
var old = filter.apply(oldIr);
var now = filter.apply(ir);
if (!old.equals(now)) {
@ -1287,6 +1311,26 @@ public class EnsoCompilerTest {
}
}
private static void equivalenceTest(String code1, String code2) throws IOException {
Function<IR, String> filter = (f) -> simplifyIR(f, true, true, false);
var ir1 = filter.apply(compile(code1));
var ir2 = filter.apply(compile(code2));
if (!ir1.equals(ir2)) {
var name = findTestMethodName();
var home = new File(System.getProperty("user.home")).toPath();
Files.writeString(home.resolve(name + ".1") , ir1, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
Files.writeString(home.resolve(name + ".2") , ir2, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
assertEquals("IR for " + code1 + " shall be equal to IR for " + code2, ir1, ir2);
}
}
private static IR.Module compile(String code) {
var src = Source.newBuilder("enso", code, "test-" + Integer.toHexString(code.hashCode()) + ".enso").build();
var ir = ensoCompiler.compile(src);
assertNotNull("IR was generated", ir);
return ir;
}
private static String findTestMethodName() {
for (var e : new Exception().getStackTrace()) {
if (e.getMethodName().startsWith("test")) {

View File

@ -1240,6 +1240,29 @@ fn multiline_annotations() {
}
// === SKIP and FREEZE ===
#[test]
fn freeze() {
test!("FREEZE x", (MultiSegmentApp #(((Ident FREEZE) (Ident x)))));
test!("FREEZE x + y", (MultiSegmentApp
#(((Ident FREEZE) (OprApp (Ident x) (Ok "+") (Ident y))))));
test!("FREEZE x.f", (MultiSegmentApp
#(((Ident FREEZE) (OprApp (Ident x) (Ok ".") (Ident f))))));
test!("FREEZE x.f y", (MultiSegmentApp #(((Ident FREEZE)
(App (OprApp (Ident x) (Ok ".") (Ident f)) (Ident y))))));
}
#[test]
fn skip() {
test!("SKIP x", (MultiSegmentApp #(((Ident SKIP) (Ident x)))));
test!("SKIP x + y", (MultiSegmentApp #(((Ident SKIP) (OprApp (Ident x) (Ok "+") (Ident y))))));
test!("SKIP x.f", (MultiSegmentApp #(((Ident SKIP) (OprApp (Ident x) (Ok ".") (Ident f))))));
test!("SKIP x.f y", (MultiSegmentApp #(((Ident SKIP)
(App (OprApp (Ident x) (Ok ".") (Ident f)) (Ident y))))));
}
// ==========================
// === Syntax Error Tests ===

View File

@ -27,6 +27,8 @@ fn expression() -> resolver::SegmentMap<'static> {
macro_map.register(array());
macro_map.register(tuple());
macro_map.register(splice());
macro_map.register(skip());
macro_map.register(freeze());
macro_map
}
@ -695,6 +697,26 @@ fn foreign<'s>() -> Definition<'s> {
crate::macro_definition! {("foreign", everything()) foreign_body}
}
fn skip<'s>() -> Definition<'s> {
crate::macro_definition! {("SKIP", everything()) capture_expressions}
}
fn freeze<'s>() -> Definition<'s> {
crate::macro_definition! {("FREEZE", everything()) capture_expressions}
}
/// Macro body builder that just parses the tokens of each segment as expressions, and places them
/// in a [`MultiSegmentApp`].
fn capture_expressions(segments: NonEmptyVec<MatchedSegment>) -> syntax::Tree {
use syntax::tree::*;
Tree::multi_segment_app(segments.mapped(|s| {
let header = s.header;
let body = s.result.tokens();
let body = operator::resolve_operator_precedence_if_non_empty(body);
MultiSegmentAppSegment { header, body }
}))
}
fn foreign_body(segments: NonEmptyVec<MatchedSegment>) -> syntax::Tree {
let segment = segments.pop().0;
let keyword = into_ident(segment.header);