Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions src/antlr/GroovyParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ referenceType
;

matchingType // see: instanceof
: standardType identifier?
: recordPattern
| standardType identifier?
;

standardType // see: returnType
Expand Down Expand Up @@ -789,11 +790,85 @@ switchBlockStatementExpressionGroup
;

switchExpressionLabel
: ( CASE expressionList[true]
: ( CASE (casePattern | expressionList[true])
| DEFAULT
) ac=(ARROW | COLON)
;

// GEP-19 structural pattern matching: pattern forms usable in case labels
casePattern
: (typePattern | recordPattern | listPattern | mapPattern) (nls caseGuard)?
;

typePattern
: standardType identifier
;

// components are mandatory and must be pattern-shaped so that legacy method
// call labels such as `case foo()` and `case foo(bar)` keep isCase semantics
recordPattern
: standardType LPAREN nls recordPatternComponents nls RPAREN
;

recordPatternComponents
: recordPatternComponent (COMMA nls recordPatternComponent)*
;

recordPatternComponent
: recordPattern
| (DEF | VAR) identifier
| typePattern
| { "_".equals(_input.LT(1).getText()) }? identifier
;

// a `[...]` label parses as a list pattern whenever its elements fit; whether it
// is treated as a pattern or keeps legacy isCase semantics is decided in the
// AST builder (a non-empty literal with no binding form, rest or nested pattern
// among its elements is rebuilt as a legacy list expression label)
listPattern
: LBRACK nls (listPatternElements nls)? RBRACK
;

listPatternElements
: listPatternElement (COMMA nls listPatternElement)*
;

listPatternElement
: listPatternRest
| recordPattern
| listPattern
| mapPattern
| (DEF | VAR) identifier
| typePattern
| expression
;

// at most one rest binding per list pattern (validated in the AST builder);
// `...` and `... t` are shortcuts for `var... _` and `var... t`
listPatternRest
: (DEF | VAR | standardType)? ELLIPSIS identifier?
;

// like listPattern, this rule over-matches: whether a `[k: v, ...]` literal is a
// pattern or keeps legacy isCase semantics is decided in the AST builder
mapPattern
: LBRACK
( mapPatternEntry (COMMA mapPatternEntry)*
| COLON
)
RBRACK
;

// `... rest` binds the entries not named by the pattern; `...` discards them
mapPatternEntry
: mapEntryLabel COLON nls listPatternElement
| ELLIPSIS identifier?
;

caseGuard
: { "when".equals(_input.LT(1).getText()) }? identifier nls expression
;

expression
// must come before postfixExpression to resolve the ambiguities between casting and call on parentheses expression, e.g. (int)(1 / 2)
: castParExpression castOperandExpression #castExprAlt
Expand Down
1,019 changes: 1,003 additions & 16 deletions src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions src/main/java/org/apache/groovy/runtime/ListPatternSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.runtime;

import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* Runtime destructuring support for list patterns (GEP-19) — the emit
* target of the pattern switch lowering in the parser.
* <p>
* List patterns destructure {@code List}, array and {@code Iterable} values
* structurally; other values (and {@code null}) do not match. An
* {@code Iterable} is materialised as a list for matching, so it must be
* traversable non-destructively.
*
* @since 6.0.0
*/
@Incubating
public final class ListPatternSupport {

private ListPatternSupport() {
}

/**
* Returns the given value as a list of its elements, or {@code null} when the
* value is not destructurable by a list pattern: lists are returned as is,
* arrays (including primitive arrays) and other iterables as a copy.
*/
@SuppressWarnings("unchecked")
public static List<Object> elementsOrNull(final Object value) {
if (value instanceof List) {
return (List<Object>) value;
}
if (value instanceof Object[]) {
return Arrays.asList((Object[]) value);
}
if (value != null && value.getClass().isArray()) {
return DefaultTypeTransformation.primitiveArrayToList(value);
}
if (value instanceof Iterable) {
List<Object> result = new ArrayList<>();
for (Object element : (Iterable<Object>) value) {
result.add(element);
}
return result;
}
return null;
}

/**
* Returns a copy of the elements bound by a rest binding: those from
* {@code from} up to (but not including) the last {@code dropRight} elements.
*/
public static List<Object> rest(final List<Object> elements, final int from, final int dropRight) {
return new ArrayList<>(elements.subList(from, elements.size() - dropRight));
}

/**
* Whether every element is an instance of the given type; used for the type
* check of a typed rest binding such as {@code Integer... t}.
*/
public static boolean allInstanceOf(final List<Object> elements, final Class<?> type) {
for (Object element : elements) {
if (!type.isInstance(element)) return false;
}
return true;
}
}
64 changes: 64 additions & 0 deletions src/main/java/org/apache/groovy/runtime/MapPatternSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.runtime;

import org.apache.groovy.lang.annotation.Incubating;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Runtime destructuring support for map patterns (GEP-19) &mdash; the emit
* target of the pattern switch lowering in the parser.
* <p>
* Map patterns destructure {@code Map} values with open semantics: a pattern
* matches if all named keys are present and their value patterns match; extra
* entries are ignored unless captured by a rest binding. Other values (and
* {@code null}) do not match.
*
* @since 6.0.0
*/
@Incubating
public final class MapPatternSupport {

private MapPatternSupport() {
}

/**
* Returns the given value as a map of its entries, or {@code null} when the
* value is not destructurable by a map pattern.
*/
@SuppressWarnings("unchecked")
public static Map<Object, Object> entriesOrNull(final Object value) {
return value instanceof Map ? (Map<Object, Object>) value : null;
}

/**
* Returns a copy of the entries bound by a rest binding: those whose keys are
* not named by the map pattern.
*/
public static Map<Object, Object> rest(final Map<Object, Object> entries, final List<Object> namedKeys) {
Map<Object, Object> result = new LinkedHashMap<>(entries);
for (Object key : namedKeys) {
result.remove(key);
}
return result;
}
}
97 changes: 97 additions & 0 deletions src/main/java/org/apache/groovy/runtime/RecordPatternSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.runtime;

import groovy.lang.GroovyRuntimeException;
import groovy.lang.MissingMethodException;
import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.runtime.InvokerHelper;

import java.lang.reflect.RecordComponent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Runtime deconstruction support for record patterns (GEP-19) &mdash; the emit
* target of the pattern switch lowering in the parser.
* <p>
* Record patterns are positional, but component names are not known at parse
* time, so the lowering cannot emit direct accessor calls. Instead it emits
* {@code RecordPatternSupport.components(value)} calls; this class resolves the
* components at runtime: native records are deconstructed through their record
* components (invoked via the meta-object protocol, so Groovy and Java records
* behave identically), and other values are deconstructed through a
* {@code toList()} method if one exists (which covers emulated Groovy records).
*
* @since 6.0.0
*/
@Incubating
public final class RecordPatternSupport {

private RecordPatternSupport() {
}

/**
* Returns the components of the given value, in declaration order.
*
* @throws GroovyRuntimeException if the value is neither a record nor deconstructable via {@code toList()}
*/
public static List<Object> components(final Object value) {
if (value == null) {
throw new GroovyRuntimeException("Cannot deconstruct null");
}
RecordComponent[] recordComponents = value.getClass().getRecordComponents();
if (recordComponents != null) {
List<Object> result = new ArrayList<>(recordComponents.length);
for (RecordComponent recordComponent : recordComponents) {
result.add(InvokerHelper.invokeMethod(value, recordComponent.getName(), InvokerHelper.EMPTY_ARGS));
}
return result;
}
try {
Object components = InvokerHelper.invokeMethod(value, "toList", InvokerHelper.EMPTY_ARGS);
if (components instanceof List) {
return (List<Object>) components;
}
} catch (MissingMethodException ignore) {
}
throw new GroovyRuntimeException("Cannot deconstruct an instance of " + value.getClass().getName() + ": it is neither a record nor does it provide a toList() method");
}

/**
* Like {@link #components(Object)} but yields an empty list for {@code null}.
* Used by the {@code instanceof} record pattern lowering, whose conjuncts are
* combined without short-circuiting and so may deconstruct the (null) default
* value of a pattern variable whose type check failed.
*/
public static List<Object> componentsOrEmpty(final Object value) {
return value == null ? Collections.emptyList() : components(value);
}

/**
* Bounds-safe component access, yielding {@code null} when the index is out of
* range. Used by the {@code instanceof} record pattern lowering (see
* {@link #componentsOrEmpty}) where a failed arity check does not stop the
* remaining component conjuncts from being evaluated.
*/
public static Object component(final List<Object> components, final int index) {
return index < components.size() ? components.get(index) : null;
}
}
Loading
Loading