Skip to content

Commit 8a66457

Browse files
committed
Record types [wip]
1 parent f805371 commit 8a66457

13 files changed

Lines changed: 287 additions & 34 deletions

src/main/java/nextflow/lsp/ast/ASTNodeStringUtils.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import nextflow.script.ast.ProcessNode;
2929
import nextflow.script.ast.ProcessNodeV1;
3030
import nextflow.script.ast.ProcessNodeV2;
31+
import nextflow.script.ast.RecordNode;
3132
import nextflow.script.ast.TupleParameter;
3233
import nextflow.script.ast.WorkflowNode;
3334
import nextflow.script.dsl.Constant;
@@ -93,7 +94,9 @@ public static String getLabel(ASTNode node) {
9394

9495
private static String classToLabel(ClassNode node) {
9596
var builder = new StringBuilder();
96-
if( node.isEnum() )
97+
if( node instanceof RecordNode )
98+
builder.append("record ");
99+
else if( node.isEnum() )
97100
builder.append("enum ");
98101
else
99102
builder.append("class ");
@@ -158,11 +161,11 @@ private static void typedOutput(Expression output, Formatter fmt) {
158161
var type = getType(target);
159162
if( fmt.hasType(type) ) {
160163
fmt.append(": ");
161-
fmt.visitTypeAnnotation(type);
164+
fmt.append(TypesEx.getName(type));
162165
}
163166
}
164167
else {
165-
fmt.visitTypeAnnotation(getType(output));
168+
fmt.append(TypesEx.getName(getType(output)));
166169
}
167170
}
168171

@@ -181,13 +184,31 @@ private static String processToLabel(ProcessNodeV2 node) {
181184
for( var input : node.inputs ) {
182185
fmt.appendIndent();
183186
if( input instanceof TupleParameter tp ) {
184-
fmt.append('(');
185-
fmt.append(
186-
Arrays.stream(tp.components)
187+
if( tp.isRecord() ) {
188+
fmt.append('(');
189+
fmt.appendNewLine();
190+
fmt.incIndent();
191+
for( var p : tp.components ) {
192+
fmt.appendIndent();
193+
fmt.append(p.getName());
194+
if( fmt.hasType(p) ) {
195+
fmt.append(": ");
196+
fmt.append(TypesEx.getName(p.getType()));
197+
}
198+
fmt.appendNewLine();
199+
}
200+
fmt.decIndent();
201+
fmt.appendIndent();
202+
fmt.append(')');
203+
}
204+
else {
205+
var components = Arrays.stream(tp.components)
187206
.map(p -> p.getName())
188-
.collect(Collectors.joining(", "))
189-
);
190-
fmt.append(')');
207+
.collect(Collectors.joining(", "));
208+
fmt.append('(');
209+
fmt.append(components);
210+
fmt.append(')');
211+
}
191212
}
192213
else {
193214
fmt.append(input.getName());

src/main/java/nextflow/lsp/services/script/ProcessConverter.java

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Arrays;
2424
import java.util.Collections;
2525
import java.util.HashMap;
26+
import java.util.HashSet;
2627
import java.util.List;
2728
import java.util.Map;
2829
import java.util.Set;
@@ -34,6 +35,7 @@
3435
import nextflow.script.ast.ProcessNodeV1;
3536
import nextflow.script.ast.ProcessNodeV2;
3637
import nextflow.script.ast.TupleParameter;
38+
import nextflow.script.types.Record;
3739
import nextflow.script.types.Tuple;
3840
import org.codehaus.groovy.ast.ClassHelper;
3941
import org.codehaus.groovy.ast.ClassNode;
@@ -171,11 +173,7 @@ private Parameter typedInput(MethodCallExpression call, List<Statement> stagers)
171173
.filter(mce -> mce != null)
172174
.map(mce -> typedInput(mce, stagers))
173175
.toArray(Parameter[]::new);
174-
var type = new ClassNode(Tuple.class);
175-
var genericsTypes = Arrays.stream(components)
176-
.map(p -> new GenericsType(p.getType()))
177-
.toArray(GenericsType[]::new);
178-
type.setGenericsTypes(genericsTypes);
176+
var type = new ClassNode(Record.class);
179177
return new TupleParameter(type, components);
180178
}
181179

@@ -248,8 +246,10 @@ private Statement typedOutputs(Statement outputs, List<Statement> topics) {
248246
.map(call -> typedOutput(call, topics))
249247
.filter(call -> call != null)
250248
.toList();
251-
checkExpressionOutput(statements);
252-
return block(null, statements);
249+
var fatRecord = fatRecord(statements);
250+
if( fatRecord == null )
251+
checkExpressionOutput(statements);
252+
return block(null, fatRecord != null ? List.of(stmt(fatRecord)) : statements);
253253
}
254254

255255
private void checkExpressionOutput(List<Statement> statements) {
@@ -265,6 +265,63 @@ private void checkExpressionOutput(List<Statement> statements) {
265265
}
266266
}
267267

268+
/**
269+
* Convert one or more "skinny tuple" outputs into a
270+
* single "fat record" output.
271+
*
272+
* For example, the following snippet:
273+
*
274+
* output:
275+
* bam = tuple(meta, file('*.bam'))
276+
* bai = tuple(meta, file('*.bai'))
277+
*
278+
* Is converted to:
279+
*
280+
* output:
281+
* record(
282+
* meta: meta,
283+
* bam: file('*.bam'),
284+
* bai: file('*.bai')
285+
* )
286+
*
287+
* If the outputs do not conform to the expected structure,
288+
* the function returns null.
289+
*
290+
* @param statements
291+
*/
292+
private Expression fatRecord(List<Statement> statements) {
293+
if( statements.isEmpty() )
294+
return null;
295+
var entries = new ArrayList<MapEntryExpression>();
296+
var names = new HashSet<String>();
297+
for( var output : statements ) {
298+
if( !isSkinnyTupleOutput(output) )
299+
return null;
300+
var es = (ExpressionStatement) output;
301+
var ae = (AssignmentExpression) es.getExpression();
302+
var target = (VariableExpression) ae.getLeftExpression();
303+
var source = (MethodCallExpression) ae.getRightExpression();
304+
var args = (ArgumentListExpression) source.getArguments();
305+
var first = (VariableExpression) args.getExpression(0);
306+
if( !names.contains(first.getName()) ) {
307+
entries.add(mapEntryX(first.getName(), first));
308+
names.add(first.getName());
309+
}
310+
entries.add(mapEntryX(target.getName(), args.getExpression(1)));
311+
}
312+
return callThisX("record", args(new NamedArgumentListExpression(entries)));
313+
}
314+
315+
private boolean isSkinnyTupleOutput(Statement output) {
316+
return output instanceof ExpressionStatement es
317+
&& es.getExpression() instanceof AssignmentExpression ae
318+
&& ae.getRightExpression() instanceof MethodCallExpression mce
319+
&& "tuple".equals(mce.getMethodAsString())
320+
&& mce.getArguments() instanceof ArgumentListExpression args
321+
&& args.getExpressions().size() == 2
322+
&& args.getExpression(0) instanceof VariableExpression;
323+
}
324+
268325
private static final Token RIGHT_SHIFT = Token.newSymbol(Types.RIGHT_SHIFT, -1, -1);
269326

270327
/**

src/main/java/nextflow/lsp/services/script/ScriptAstCache.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import nextflow.script.ast.FunctionNode;
3535
import nextflow.script.ast.IncludeNode;
3636
import nextflow.script.ast.ProcessNode;
37+
import nextflow.script.ast.RecordNode;
3738
import nextflow.script.ast.ScriptNode;
3839
import nextflow.script.ast.WorkflowNode;
3940
import nextflow.script.control.ModuleResolver;
@@ -45,8 +46,8 @@
4546
import nextflow.script.parser.ScriptParserPluginFactory;
4647
import nextflow.script.types.Types;
4748
import org.codehaus.groovy.ast.ASTNode;
49+
import org.codehaus.groovy.ast.AnnotatedNode;
4850
import org.codehaus.groovy.ast.ClassNode;
49-
import org.codehaus.groovy.ast.MethodNode;
5051
import org.codehaus.groovy.control.CompilerConfiguration;
5152
import org.codehaus.groovy.control.SourceUnit;
5253
import org.codehaus.groovy.control.messages.WarningMessage;
@@ -186,19 +187,21 @@ public List<IncludeNode> getIncludeNodes(URI uri) {
186187
return scriptNode.getIncludes();
187188
}
188189

189-
public List<MethodNode> getDefinitions() {
190-
var result = new ArrayList<MethodNode>();
190+
public List<AnnotatedNode> getDefinitions() {
191+
var result = new ArrayList<AnnotatedNode>();
191192
result.addAll(getFunctionNodes());
192193
result.addAll(getProcessNodes());
193194
result.addAll(getWorkflowNodes());
195+
result.addAll(getTypeNodes());
194196
return result;
195197
}
196198

197-
public List<MethodNode> getDefinitions(URI uri) {
198-
var result = new ArrayList<MethodNode>();
199+
public List<AnnotatedNode> getDefinitions(URI uri) {
200+
var result = new ArrayList<AnnotatedNode>();
199201
result.addAll(getFunctionNodes(uri));
200202
result.addAll(getProcessNodes(uri));
201203
result.addAll(getWorkflowNodes(uri));
204+
result.addAll(getTypeNodes(uri));
202205
return result;
203206
}
204207

src/main/java/nextflow/lsp/services/script/ScriptAstParentVisitor.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
import nextflow.script.ast.ParamBlockNode;
2727
import nextflow.script.ast.ProcessNodeV1;
2828
import nextflow.script.ast.ProcessNodeV2;
29+
import nextflow.script.ast.RecordNode;
2930
import nextflow.script.ast.ScriptNode;
3031
import nextflow.script.ast.ScriptVisitorSupport;
31-
import nextflow.script.ast.TupleParameter;
3232
import nextflow.script.ast.WorkflowNode;
3333
import org.codehaus.groovy.ast.ASTNode;
3434
import org.codehaus.groovy.ast.ClassNode;
@@ -172,6 +172,17 @@ public void visitFunction(FunctionNode node) {
172172
}
173173
}
174174

175+
@Override
176+
public void visitRecord(RecordNode node) {
177+
lookup.push(node);
178+
try {
179+
super.visitRecord(node);
180+
}
181+
finally {
182+
lookup.pop();
183+
}
184+
}
185+
175186
@Override
176187
public void visitEnum(ClassNode node) {
177188
lookup.push(node);

src/main/java/nextflow/lsp/services/script/ScriptCompletionProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ private void addIncludes(String namePrefix) {
169169
for( var includeNode : ast.getIncludeNodes(uri) ) {
170170
for( var entry : includeNode.entries ) {
171171
var node = entry.getTarget();
172-
if( node == null || ast.getURI(node) == null )
172+
if( !(node instanceof MethodNode) || ast.getURI(node) == null )
173173
continue;
174174

175175
var name = entry.getNameOrAlias();

src/main/java/nextflow/lsp/services/script/ScriptReferenceProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ else if( oldName.equals(alias) )
224224
return new TextEdit(range, newText);
225225
}
226226

227+
// TODO: preserve type annotation
227228
if( node instanceof Variable v ) {
228229
if( !oldName.equals(v.getName()) )
229230
return null;

src/main/java/nextflow/lsp/services/script/ScriptSemanticTokensProvider.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,12 @@ public void visitFeatureFlag(FeatureFlagNode node) {
109109
@Override
110110
public void visitInclude(IncludeNode node) {
111111
for( var entry : node.entries ) {
112-
tok.append(entry.getNodeMetaData("_START_NAME"), entry.name, SemanticTokenTypes.Function);
112+
var type = entry.getTarget() instanceof ClassNode
113+
? SemanticTokenTypes.Type
114+
: SemanticTokenTypes.Function;
115+
tok.append(entry.getNodeMetaData("_START_NAME"), entry.name, type);
113116
if( entry.alias != null )
114-
tok.append(entry.getNodeMetaData("_START_ALIAS"), entry.alias, SemanticTokenTypes.Function);
117+
tok.append(entry.getNodeMetaData("_START_ALIAS"), entry.alias, type);
115118
}
116119
}
117120

src/main/java/nextflow/lsp/services/script/ScriptSymbolProvider.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import nextflow.lsp.util.Logger;
2626
import nextflow.script.ast.FunctionNode;
2727
import nextflow.script.ast.ProcessNode;
28+
import nextflow.script.ast.RecordNode;
2829
import nextflow.script.ast.WorkflowNode;
2930
import org.codehaus.groovy.ast.ASTNode;
3031
import org.codehaus.groovy.ast.ClassNode;
@@ -108,6 +109,8 @@ private void addWorkspaceSymbol(ASTNode node, String query, List<WorkspaceSymbol
108109
}
109110

110111
private static String getSymbolName(ASTNode node) {
112+
if( node instanceof RecordNode rn )
113+
return "record " + rn.getName();
111114
if( node instanceof ClassNode cn && cn.isEnum() )
112115
return "enum " + cn.getName();
113116
if( node instanceof FunctionNode fn )
@@ -122,9 +125,11 @@ private static String getSymbolName(ASTNode node) {
122125
}
123126

124127
private static SymbolKind getSymbolKind(ASTNode node) {
128+
if( node instanceof RecordNode )
129+
return SymbolKind.Struct;
125130
if( node instanceof ClassNode cn && cn.isEnum() )
126131
return SymbolKind.Enum;
127-
if( node instanceof MethodNode mn )
132+
if( node instanceof MethodNode )
128133
return SymbolKind.Function;
129134
return null;
130135
}

0 commit comments

Comments
 (0)