mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 16:18:23 +03:00
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:
parent
579d3fc397
commit
d24019aa57
@ -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) {
|
||||
|
@ -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")) {
|
||||
|
@ -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 ===
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user