From 647ac708b6b81e4012395c55529f65569a21a985 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 14 Aug 2026 11:46:22 +0200 Subject: [PATCH 01/37] unified: Add string interpolation expression --- unified/extractor/ast_types.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index 854829a17cb4..cbd947e84147 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -6,6 +6,7 @@ supertypes: - boolean_literal - string_literal - regex_literal + - string_interpolation_expr - builtin_expr - binary_expr - unary_expr @@ -130,6 +131,11 @@ named: # A regex literal regex_literal: + # A string interpolation expression. Constant parts are stored as string literals. + string_interpolation_expr: + modifier*: modifier + element*: expr + # Application of a binary operator, such as `a + b` binary_expr: left: expr From e06d66777e4e9a98b14451d8949e487baad87f03 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 14 Aug 2026 11:46:33 +0200 Subject: [PATCH 02/37] unified: Regenerate QL --- .../ql/lib/codeql/unified/internal/Ast.qll | 32 +++++++++++++++++++ unified/ql/lib/unified.dbscheme | 22 +++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index d5ffa0218523..5f1bb6ae8f19 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -1272,6 +1272,32 @@ module Unified { class Stmt extends @unified_stmt, F::AstNode { } + /** A class representing `string_interpolation_expr` nodes. */ + class StringInterpolationExpr extends @unified_string_interpolation_expr, F::Expr { + /** Gets the name of the primary QL class for this element. */ + final override string getAPrimaryQlClass() { result = "StringInterpolationExpr" } + + /** Gets the node corresponding to the field `element`. */ + final F::Expr getElement(int i) { unified_string_interpolation_expr_element(this, i, result) } + + /** Gets the node corresponding to the field `element`. */ + final F::Expr getAnElement() { result = this.getElement(_) } + + /** Gets the node corresponding to the field `modifier`. */ + final F::Modifier getModifier(int i) { + unified_string_interpolation_expr_modifier(this, i, result) + } + + /** Gets the node corresponding to the field `modifier`. */ + final F::Modifier getAModifier() { result = this.getModifier(_) } + + /** Gets a field or child node of this node. */ + final override F::AstNode getAFieldOrChild() { + unified_string_interpolation_expr_element(this, _, result) or + unified_string_interpolation_expr_modifier(this, _, result) + } + } + /** A class representing `string_literal` tokens. */ class StringLiteral extends @unified_token_string_literal, F::Expr, F::Token { /** Gets the name of the primary QL class for this element. */ @@ -1942,6 +1968,10 @@ module Unified { or result = node.(ReturnExpr).getValue() and i = -1 and name = "getValue" or + result = node.(StringInterpolationExpr).getElement(i) and name = "getElement" + or + result = node.(StringInterpolationExpr).getModifier(i) and name = "getModifier" + or result = node.(SwitchCase).getBody() and i = -1 and name = "getBody" or result = node.(SwitchCase).getModifier(i) and name = "getModifier" @@ -2177,6 +2207,8 @@ module UnifiedFinal { final class Stmt = F::Stmt; + final class StringInterpolationExpr = F::StringInterpolationExpr; + final class StringLiteral = F::StringLiteral; final class SuperExpr = F::SuperExpr; diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index c36721515d5c..3bdaebd0f659 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -460,7 +460,7 @@ unified_equality_type_constraint_def( int right: @unified_type_expr ref ); -@unified_expr = @unified_array_literal | @unified_assign_expr | @unified_binary_expr | @unified_block | @unified_break_expr | @unified_call_expr | @unified_compound_assign_expr | @unified_continue_expr | @unified_function_expr | @unified_if_expr | @unified_key_value_pair | @unified_map_literal | @unified_member_access_expr | @unified_name_expr | @unified_pattern_guard_expr | @unified_return_expr | @unified_switch_expr | @unified_throw_expr | @unified_token_boolean_literal | @unified_token_builtin_expr | @unified_token_empty_expr | @unified_token_float_literal | @unified_token_int_literal | @unified_token_regex_literal | @unified_token_string_literal | @unified_token_super_expr | @unified_token_unsupported_node | @unified_try_expr | @unified_tuple_expr | @unified_type_cast_expr | @unified_type_test_expr | @unified_unary_expr | @unified_unresolved_operator_sequence +@unified_expr = @unified_array_literal | @unified_assign_expr | @unified_binary_expr | @unified_block | @unified_break_expr | @unified_call_expr | @unified_compound_assign_expr | @unified_continue_expr | @unified_function_expr | @unified_if_expr | @unified_key_value_pair | @unified_map_literal | @unified_member_access_expr | @unified_name_expr | @unified_pattern_guard_expr | @unified_return_expr | @unified_string_interpolation_expr | @unified_switch_expr | @unified_throw_expr | @unified_token_boolean_literal | @unified_token_builtin_expr | @unified_token_empty_expr | @unified_token_float_literal | @unified_token_int_literal | @unified_token_regex_literal | @unified_token_string_literal | @unified_token_super_expr | @unified_token_unsupported_node | @unified_try_expr | @unified_tuple_expr | @unified_type_cast_expr | @unified_type_test_expr | @unified_unary_expr | @unified_unresolved_operator_sequence unified_expr_equality_pattern_def( unique int id: @unified_expr_equality_pattern, @@ -816,6 +816,24 @@ unified_return_expr_def( @unified_stmt = @unified_accessor_declaration | @unified_class_like_declaration | @unified_constructor_declaration | @unified_destructor_declaration | @unified_do_while_stmt | @unified_expr | @unified_for_each_stmt | @unified_function_declaration | @unified_guard_if_stmt | @unified_import_declaration | @unified_labeled_stmt | @unified_operator_syntax_declaration | @unified_type_alias_declaration | @unified_variable_declaration | @unified_while_stmt +#keyset[unified_string_interpolation_expr, index] +unified_string_interpolation_expr_element( + int unified_string_interpolation_expr: @unified_string_interpolation_expr ref, + int index: int ref, + unique int element: @unified_expr ref +); + +#keyset[unified_string_interpolation_expr, index] +unified_string_interpolation_expr_modifier( + int unified_string_interpolation_expr: @unified_string_interpolation_expr ref, + int index: int ref, + unique int modifier: @unified_token_modifier ref +); + +unified_string_interpolation_expr_def( + unique int id: @unified_string_interpolation_expr +); + #keyset[unified_switch_case, index] unified_switch_case_modifier( int unified_switch_case: @unified_switch_case ref, @@ -1093,7 +1111,7 @@ unified_trivia_tokeninfo( string value: string ref ); -@unified_ast_node = @unified_accessor_declaration | @unified_argument | @unified_array_literal | @unified_assign_expr | @unified_associated_type_declaration | @unified_base_type | @unified_binary_expr | @unified_block | @unified_bound_type_constraint | @unified_break_expr | @unified_bulk_importing_pattern | @unified_call_expr | @unified_catch_clause | @unified_class_like_declaration | @unified_compound_assign_expr | @unified_conditional_pattern | @unified_constructor_declaration | @unified_constructor_pattern | @unified_continue_expr | @unified_destructor_declaration | @unified_do_while_stmt | @unified_equality_type_constraint | @unified_expr_equality_pattern | @unified_for_each_stmt | @unified_function_declaration | @unified_function_expr | @unified_function_type_expr | @unified_generic_type_expr | @unified_guard_if_stmt | @unified_if_expr | @unified_import_declaration | @unified_initializer_declaration | @unified_key_value_pair | @unified_labeled_stmt | @unified_map_literal | @unified_member_access_expr | @unified_name_expr | @unified_name_pattern | @unified_named_type_expr | @unified_operator_syntax_declaration | @unified_or_pattern | @unified_parameter | @unified_pattern_element | @unified_pattern_guard_expr | @unified_return_expr | @unified_switch_case | @unified_switch_expr | @unified_throw_expr | @unified_token | @unified_top_level | @unified_trivia_token | @unified_try_expr | @unified_tuple_expr | @unified_tuple_pattern | @unified_tuple_type_element | @unified_tuple_type_expr | @unified_type_alias_declaration | @unified_type_cast_expr | @unified_type_parameter | @unified_type_test_expr | @unified_type_test_pattern | @unified_unary_expr | @unified_unresolved_operator_sequence | @unified_variable_declaration | @unified_while_stmt +@unified_ast_node = @unified_accessor_declaration | @unified_argument | @unified_array_literal | @unified_assign_expr | @unified_associated_type_declaration | @unified_base_type | @unified_binary_expr | @unified_block | @unified_bound_type_constraint | @unified_break_expr | @unified_bulk_importing_pattern | @unified_call_expr | @unified_catch_clause | @unified_class_like_declaration | @unified_compound_assign_expr | @unified_conditional_pattern | @unified_constructor_declaration | @unified_constructor_pattern | @unified_continue_expr | @unified_destructor_declaration | @unified_do_while_stmt | @unified_equality_type_constraint | @unified_expr_equality_pattern | @unified_for_each_stmt | @unified_function_declaration | @unified_function_expr | @unified_function_type_expr | @unified_generic_type_expr | @unified_guard_if_stmt | @unified_if_expr | @unified_import_declaration | @unified_initializer_declaration | @unified_key_value_pair | @unified_labeled_stmt | @unified_map_literal | @unified_member_access_expr | @unified_name_expr | @unified_name_pattern | @unified_named_type_expr | @unified_operator_syntax_declaration | @unified_or_pattern | @unified_parameter | @unified_pattern_element | @unified_pattern_guard_expr | @unified_return_expr | @unified_string_interpolation_expr | @unified_switch_case | @unified_switch_expr | @unified_throw_expr | @unified_token | @unified_top_level | @unified_trivia_token | @unified_try_expr | @unified_tuple_expr | @unified_tuple_pattern | @unified_tuple_type_element | @unified_tuple_type_expr | @unified_type_alias_declaration | @unified_type_cast_expr | @unified_type_parameter | @unified_type_test_expr | @unified_type_test_pattern | @unified_unary_expr | @unified_unresolved_operator_sequence | @unified_variable_declaration | @unified_while_stmt unified_ast_node_location( unique int node: @unified_ast_node ref, From 41ae049e84e4c65e2a2061e2b2f7593b479cd072 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 14 Aug 2026 12:54:51 +0200 Subject: [PATCH 03/37] unified: Add string interpolation corpus test --- .../literals/string-with-interpolation.output | 121 +++++++++++++++++- .../literals/string-with-interpolation.swift | 10 ++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output index 5207085d174c..273991dc3db6 100644 --- a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output +++ b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output @@ -1,5 +1,15 @@ +// Simple interpolation "hello \(name)" +// Multiple interpolations +"hello \(first) \(last)" + +// Interpolation with expression +"result: \(x + y)" + +// Plain string before and after interpolation +"prefix \(value) suffix" + --- sourceFile @@ -24,10 +34,119 @@ sourceFile baseName: identifier "name" stringSegment content: stringSegment + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "hello " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "first" + stringSegment + content: stringSegment " " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "last" + stringSegment + content: stringSegment + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "result: " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "+" + leftOperand: + declReferenceExpr + baseName: identifier "x" + rightOperand: + declReferenceExpr + baseName: identifier "y" + stringSegment + content: stringSegment + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "prefix " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "value" + stringSegment + content: stringSegment " suffix" --- top_level body: block - stmt: string_literal "\"hello \\(name)\"" + stmt: + string_interpolation_expr + element: + string_literal "hello " + name_expr + identifier: identifier "name" + string_literal + string_interpolation_expr + element: + string_literal "hello " + name_expr + identifier: identifier "first" + string_literal " " + name_expr + identifier: identifier "last" + string_literal + string_interpolation_expr + element: + string_literal "result: " + binary_expr + left: + name_expr + identifier: identifier "x" + operator: infix_operator "+" + right: + name_expr + identifier: identifier "y" + string_literal + string_interpolation_expr + element: + string_literal "prefix " + name_expr + identifier: identifier "value" + string_literal " suffix" diff --git a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift index 4c58b37b89e7..a4cbd681a367 100644 --- a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift +++ b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift @@ -1 +1,11 @@ +// Simple interpolation "hello \(name)" + +// Multiple interpolations +"hello \(first) \(last)" + +// Interpolation with expression +"result: \(x + y)" + +// Plain string before and after interpolation +"prefix \(value) suffix" From 5ae4fca4e34c65e10c69d92eb27c8f6b44a39ca3 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 14 Aug 2026 13:30:45 +0200 Subject: [PATCH 04/37] unified: Naive rules for string interpolations --- .../extractor/src/languages/swift/swift.rs | 18 +- .../collections/dictionary-subscript.output | 4 +- ...g-modifier-does-not-leak-to-sibling.output | 4 +- .../swift/control-flow/defer-statement.output | 4 +- .../switch-case-item-where-clauses.output | 12 +- .../control-flow/switch-statement.output | 12 +- ...with-labeled-case-pattern-arguments.output | 4 +- ...nction-call-with-labelled-arguments.output | 4 +- ...nction-with-default-parameter-value.output | 4 +- .../function-with-no-parameters.output | 4 +- .../swift/literals/string-literal.output | 4 +- .../literals/string-with-interpolation.output | 228 ++++++++++++++++-- .../literals/string-with-interpolation.swift | 6 + .../catch-where-clauses.output | 8 +- .../throwing-function.output | 4 +- 15 files changed, 286 insertions(+), 34 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 6396d4addd75..9288e2a19fab 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -174,10 +174,26 @@ fn translation_rules() -> Vec> { let value = tree!((builtin_expr #{node})); if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } }), - rule!((stringLiteralExpr) @@node => expr { + // Plain string literals (no interpolation) + rule!((simpleStringLiteralExpr) @@node => expr { let value = tree!((string_literal #{node})); if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } }), + // String literals with interpolation + rule!( + (stringLiteralExpr segments: _* @segs) + => + (string_interpolation_expr element: {segs}) + ), + // Map stringSegment to a string_literal for use in string_interpolation_expr + rule!((stringSegment content: @@content) => (string_literal #{content})), + // In the general case, an expressionSegment results in a call to `appendInterpolation()` which + // can take an arbitrary list of arguments. We model it as a call to a built-in called `interpolation`. + rule!( + (expressionSegment expressions: _* @exprs) + => + (call_expr callee: (builtin_expr "interpolation") argument: {exprs}) + ), rule!((regexLiteralExpr) @@node => expr { let value = tree!((regex_literal #{node})); if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } diff --git a/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output b/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output index e2f939e0683c..5b594ceb964d 100644 --- a/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output +++ b/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output @@ -57,4 +57,6 @@ top_level identifier: identifier "d" argument: argument - value: string_literal "\"key\"" + value: + string_interpolation_expr + element: string_literal "key" diff --git a/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output b/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output index 800647eb7474..df1255ac17fd 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output +++ b/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output @@ -114,7 +114,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"matched\"" + value: + string_interpolation_expr + element: string_literal "matched" switch_case body: block diff --git a/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output b/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output index 6e8ec9ae3b9a..ce5b406f7637 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output +++ b/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output @@ -91,4 +91,6 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"work\"" + value: + string_interpolation_expr + element: string_literal "work" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output b/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output index 9d2041aa2ad1..cc6710b16360 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output @@ -181,7 +181,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"positive\"" + value: + string_interpolation_expr + element: string_literal "positive" switch_case pattern: or_pattern @@ -208,7 +210,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"non-positive\"" + value: + string_interpolation_expr + element: string_literal "non-positive" switch_case body: block @@ -219,4 +223,6 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"other\"" + value: + string_interpolation_expr + element: string_literal "other" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output b/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output index a88579852947..703f2200a404 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output @@ -142,7 +142,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"one\"" + value: + string_interpolation_expr + element: string_literal "one" switch_case pattern: or_pattern @@ -160,7 +162,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"two or three\"" + value: + string_interpolation_expr + element: string_literal "two or three" switch_case body: block @@ -171,4 +175,6 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"other\"" + value: + string_interpolation_expr + element: string_literal "other" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output index 20b17d160e44..56e690a452aa 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output @@ -154,7 +154,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"yes\"" + value: + string_interpolation_expr + element: string_literal "yes" switch_case pattern: constructor_pattern diff --git a/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output b/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output index f885d5762f2d..351caa592970 100644 --- a/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output +++ b/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output @@ -39,4 +39,6 @@ top_level argument: argument name: identifier "person" - value: string_literal "\"Bob\"" + value: + string_interpolation_expr + element: string_literal "Bob" diff --git a/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output b/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output index 3495a13106fb..18f9d5f04201 100644 --- a/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output +++ b/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output @@ -75,7 +75,9 @@ top_level pattern: name_pattern identifier: identifier "name" - default: string_literal "\"world\"" + default: + string_interpolation_expr + element: string_literal "world" body: block stmt: diff --git a/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output b/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output index 4ddc26ae94d6..16caecd4f6e8 100644 --- a/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output +++ b/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output @@ -62,4 +62,6 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"hello\"" + value: + string_interpolation_expr + element: string_literal "hello" diff --git a/unified/extractor/tests/corpus/swift/literals/string-literal.output b/unified/extractor/tests/corpus/swift/literals/string-literal.output index 8d3ea8e796c0..7d3c269c4cc4 100644 --- a/unified/extractor/tests/corpus/swift/literals/string-literal.output +++ b/unified/extractor/tests/corpus/swift/literals/string-literal.output @@ -19,4 +19,6 @@ sourceFile top_level body: block - stmt: string_literal "\"hello\"" + stmt: + string_interpolation_expr + element: string_literal "hello" diff --git a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output index 273991dc3db6..c06e04aff67c 100644 --- a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output +++ b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.output @@ -10,6 +10,12 @@ // Plain string before and after interpolation "prefix \(value) suffix" +// Calls to custom DefaultStringInterpolation.appendInterpolation impls +"foo \(x, y)" +"foo \(x, y, z)" +"foo \(arg: x)" +"foo \(arg: x, arg2: y)" + --- sourceFile @@ -110,6 +116,108 @@ sourceFile baseName: identifier "value" stringSegment content: stringSegment " suffix" + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "x" + trailingComma: , + labeledExpr + expression: + declReferenceExpr + baseName: identifier "y" + stringSegment + content: stringSegment + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "x" + trailingComma: , + labeledExpr + expression: + declReferenceExpr + baseName: identifier "y" + trailingComma: , + labeledExpr + expression: + declReferenceExpr + baseName: identifier "z" + stringSegment + content: stringSegment + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + colon: : + label: identifier "arg" + expression: + declReferenceExpr + baseName: identifier "x" + stringSegment + content: stringSegment + codeBlockItem + item: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + colon: : + label: identifier "arg" + expression: + declReferenceExpr + baseName: identifier "x" + trailingComma: , + labeledExpr + colon: : + label: identifier "arg2" + expression: + declReferenceExpr + baseName: identifier "y" + stringSegment + content: stringSegment --- @@ -120,33 +228,121 @@ top_level string_interpolation_expr element: string_literal "hello " - name_expr - identifier: identifier "name" + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "name" string_literal string_interpolation_expr element: string_literal "hello " - name_expr - identifier: identifier "first" + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "first" string_literal " " - name_expr - identifier: identifier "last" + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "last" string_literal string_interpolation_expr element: string_literal "result: " - binary_expr - left: - name_expr - identifier: identifier "x" - operator: infix_operator "+" - right: - name_expr - identifier: identifier "y" + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + binary_expr + left: + name_expr + identifier: identifier "x" + operator: infix_operator "+" + right: + name_expr + identifier: identifier "y" string_literal string_interpolation_expr element: string_literal "prefix " - name_expr - identifier: identifier "value" + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "value" string_literal " suffix" + string_interpolation_expr + element: + string_literal "foo " + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "x" + argument + value: + name_expr + identifier: identifier "y" + string_literal + string_interpolation_expr + element: + string_literal "foo " + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "x" + argument + value: + name_expr + identifier: identifier "y" + argument + value: + name_expr + identifier: identifier "z" + string_literal + string_interpolation_expr + element: + string_literal "foo " + call_expr + callee: builtin_expr "interpolation" + argument: + argument + name: identifier "arg" + value: + name_expr + identifier: identifier "x" + string_literal + string_interpolation_expr + element: + string_literal "foo " + call_expr + callee: builtin_expr "interpolation" + argument: + argument + name: identifier "arg" + value: + name_expr + identifier: identifier "x" + argument + name: identifier "arg2" + value: + name_expr + identifier: identifier "y" + string_literal diff --git a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift index a4cbd681a367..b72a94faa00c 100644 --- a/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift +++ b/unified/extractor/tests/corpus/swift/literals/string-with-interpolation.swift @@ -9,3 +9,9 @@ // Plain string before and after interpolation "prefix \(value) suffix" + +// Calls to custom DefaultStringInterpolation.appendInterpolation impls +"foo \(x, y)" +"foo \(x, y, z)" +"foo \(arg: x)" +"foo \(arg: x, arg2: y)" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output index 79b7879d35fa..800f85511d30 100644 --- a/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output @@ -193,7 +193,9 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"retry\"" + value: + string_interpolation_expr + element: string_literal "retry" catch_clause body: block @@ -204,4 +206,6 @@ top_level identifier: identifier "print" argument: argument - value: string_literal "\"fallback\"" + value: + string_interpolation_expr + element: string_literal "fallback" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output b/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output index e68b4c3c57ac..4980a7b4fba3 100644 --- a/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output @@ -64,4 +64,6 @@ top_level block stmt: return_expr - value: string_literal "\"\"" + value: + string_interpolation_expr + element: string_literal From 9ff9d650627fc24bcf7559d23d51a92d196bb523 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 15:33:42 +0200 Subject: [PATCH 05/37] unified: Set in_pattern=true when translating patterns --- .../extractor/src/languages/swift/swift.rs | 42 ++++-- .../optionals-and-errors/catch-pattern.output | 128 ++++++++++++++++++ .../optionals-and-errors/catch-pattern.swift | 5 + 3 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.output create mode 100644 unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.swift diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 9288e2a19fab..fea301414e47 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -107,6 +107,17 @@ fn make_or_pattern( } } +/// Translate `node` in pattern context (`ctx.in_pattern = true`). +fn translate_pattern( + ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, + node: yeast::Id, +) -> Result, String> { + ctx.scoped(|ctx| { + ctx.in_pattern = true; + ctx.translate(node) + }) +} + /// Translate a multi-part identifier (for example `Foo.Bar.Baz`) into a /// `member_access_expr` chain rooted at a `name_expr` over the first /// part. Panics on an empty input because the grammar's `_+` quantifier @@ -438,14 +449,14 @@ fn translation_rules() -> Vec> { // as a binding. rule!( (patternBinding - pattern: @pattern + pattern: @@pattern typeAnnotation: (typeAnnotation type: @ty)? initializer: (initializerClause value: @@val)?) => (variable_declaration modifier: {ctx.outer_modifiers.clone()} modifier: {chained_modifier(&mut ctx)} - pattern: {pattern} + pattern: {translate_pattern(&mut ctx, pattern)?} type: {ty} value: {ctx.reset(); ctx.translate(val)?}) ), @@ -570,8 +581,7 @@ fn translation_rules() -> Vec> { // An expression pattern only establishes pattern context; its child // determines the concrete pattern shape. rule!((expressionPattern expression: @@e) => expr { - ctx.in_pattern = true; - ctx.translate(e)?.into_iter().next().ok_or("expression pattern has no child")? + translate_pattern(&mut ctx, e)? }), // ---- Functions ---- // A function declaration (parameters/return type/body optional). The @@ -877,15 +887,19 @@ fn translation_rules() -> Vec> { (switch_case body: (block stmt: {body})) ), // A single case item unwraps to its pattern, possibly boxed in conditional_pattern - rule!((switchCaseItem pattern: @p whereClause: (whereClause condition: @cond)) => (conditional_pattern pattern: { p } condition: {cond})), - rule!((switchCaseItem pattern: @p) => pattern { p }), + rule!( + (switchCaseItem pattern: @@p whereClause: (whereClause condition: @cond)) + => + (conditional_pattern pattern: {translate_pattern(&mut ctx, p)?} condition: {cond}) + ), + rule!((switchCaseItem pattern: @@p) => pattern { translate_pattern(&mut ctx, p)? }), // A pattern-matching condition (`if case let x = e`, `if case .foo(let x) // = e`) becomes a `pattern_guard_expr`: the matched pattern and the // scrutinee value are translated recursively. rule!( - (matchingPatternCondition pattern: @pat initializer: (initializerClause value: @val)) + (matchingPatternCondition pattern: @@pat initializer: (initializerClause value: @val)) => - (pattern_guard_expr pattern: {pat} value: {val}) + (pattern_guard_expr pattern: {translate_pattern(&mut ctx, pat)?} value: {val}) ), // Optional binding (`if let x = foo`, or shorthand `if let x`) desugars // to a `pattern_guard_expr` matching `Optional.some(x)`. The initialized @@ -921,13 +935,13 @@ fn translation_rules() -> Vec> { // A `for`-`in` loop. The optional `where` clause becomes the `guard`. rule!( (forStmt - pattern: @pat + pattern: @@pat sequence: @iter whereClause: (whereClause condition: @guard)? body: @body) => (for_each_stmt - pattern: {pat} + pattern: {translate_pattern(&mut ctx, pat)?} iterable: {iter} guard: {guard} body: {body}) @@ -1007,14 +1021,14 @@ fn translation_rules() -> Vec> { catch_clause: {catches}) ), rule!( - (catchItem pattern: @pattern whereClause: (whereClause condition: @guard)) + (catchItem pattern: @@pattern whereClause: (whereClause condition: @guard)) => - (conditional_pattern pattern: {pattern} condition: {guard}) + (conditional_pattern pattern: {translate_pattern(&mut ctx, pattern)?} condition: {guard}) ), rule!( - (catchItem pattern: @pattern) + (catchItem pattern: @@pattern) => - pattern {pattern} + pattern { translate_pattern(&mut ctx, pattern)? } ), // Catch block with one or more patterns (which have been translated by the catchItem rules) rule!( diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.output b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.output new file mode 100644 index 000000000000..1f7a56f03fba --- /dev/null +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.output @@ -0,0 +1,128 @@ +do { + try foo() +} catch MyError.someError(let msg) { + print(msg) +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + doStmt + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + tryExpr + expression: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "foo" + tryKeyword: try + catchClauses: + catchClause + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "msg" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + catchItems: + catchItem + pattern: + expressionPattern + expression: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + patternExpr + pattern: + valueBindingPattern + pattern: + identifierPattern + identifier: identifier "msg" + bindingSpecifier: let + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "someError" + base: + declReferenceExpr + baseName: identifier "MyError" + catchKeyword: catch + doKeyword: do + +--- + +top_level + body: + block + stmt: + try_expr + body: + block + stmt: + unary_expr + operand: + call_expr + callee: + name_expr + identifier: identifier "foo" + operator: prefix_operator "try" + catch_clause: + catch_clause + pattern: + constructor_pattern + constructor: + member_access_expr + base: + name_expr + identifier: identifier "MyError" + member: identifier "someError" + element: + pattern_element + pattern: + name_pattern + identifier: identifier "msg" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + name_expr + identifier: identifier "msg" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.swift b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.swift new file mode 100644 index 000000000000..4160a67efed9 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-pattern.swift @@ -0,0 +1,5 @@ +do { + try foo() +} catch MyError.someError(let msg) { + print(msg) +} From 3dbf730619c0ec328031cba681708bc993775062 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 15:35:19 +0200 Subject: [PATCH 06/37] unified: Include type_test_pattern in pattern type --- unified/extractor/ast_types.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index cbd947e84147..032587452548 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -51,6 +51,7 @@ supertypes: - constructor_pattern - or_pattern - conditional_pattern + - type_test_pattern - ignore_pattern - expr_equality_pattern - bulk_importing_pattern From 0c05a19907b75b686d41be083b5d23488d79fff1 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 15:39:17 +0200 Subject: [PATCH 07/37] unified: Add corpus with with 'case .foo, T.foo, [T].foo' --- .../switch-with-binding-pattern.output | 183 ++++++++++++++++++ .../switch-with-binding-pattern.swift | 8 + 2 files changed, 191 insertions(+) diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output index 90bb6b44e6e5..d185e057a2f2 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output @@ -3,6 +3,14 @@ case .circle(let r): print(r) case .square(let s): print(s) +case .foo: + print("foo") +case T.foo: + print("foo") +// Ambiguous reference to a static property on the Array type, or an instance property on Array where S is the type of the expression T. +// The parser can't tell the difference. +case [T].foo: + print("foo") } --- @@ -108,6 +116,120 @@ sourceFile calledExpression: declReferenceExpr baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "foo" + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "foo" + base: + declReferenceExpr + baseName: identifier "T" + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "foo" + base: + arrayExpr + elements: + arrayElement + expression: + declReferenceExpr + baseName: identifier "T" + leftSquare: [ + rightSquare: ] + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" subject: declReferenceExpr baseName: identifier "shape" @@ -172,3 +294,64 @@ top_level value: name_expr identifier: identifier "s" + switch_case + pattern: + member_access_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got member_access_expr + base: inferred_type_expr "." + member: identifier "foo" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "foo" + switch_case + pattern: + member_access_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got member_access_expr + base: + expr_equality_pattern <-- ERROR: The field member_access_expr.base should contain expr_or_type, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "T" + member: identifier "foo" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "foo" + switch_case + pattern: + member_access_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got member_access_expr + base: + array_literal + element: + expr_equality_pattern <-- ERROR: The field array_literal.element should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "T" + member: identifier "foo" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "foo" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift index e57a4e4ad061..9f430ca21f37 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift @@ -3,4 +3,12 @@ case .circle(let r): print(r) case .square(let s): print(s) +case .foo: + print("foo") +case T.foo: + print("foo") +// Ambiguous reference to a static property on the Array type, or an instance property on Array where S is the type of the expression T. +// The parser can't tell the difference. +case [T].foo: + print("foo") } From 4173b029eb14e19ce4052b34bea89ed3645ae1ad Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 16:17:46 +0200 Subject: [PATCH 08/37] unified: Set and check ctx.in_pattern in more places --- .../extractor/src/languages/swift/swift.rs | 78 +++++++++++++------ .../switch-with-binding-pattern.output | 32 ++++---- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index fea301414e47..d9c498b6bbe4 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -118,6 +118,29 @@ fn translate_pattern( }) } +/// Translate `node` in non-pattern context (`ctx.in_pattern = false`). +fn translate_non_pattern( + ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, + node: yeast::Id, +) -> Result, String> { + ctx.scoped(|ctx| { + ctx.in_pattern = false; + ctx.translate(node) + }) +} + +/// Wrap an expression in an `expr_equality_pattern` if `ctx.in_pattern` is true. +fn wrap_pattern_expr( + ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, + expr: yeast::Id, +) -> yeast::Id { + if ctx.in_pattern { + tree!((expr_equality_pattern expr: {expr})) + } else { + expr + } +} + /// Translate a multi-part identifier (for example `Foo.Bar.Baz`) into a /// `member_access_expr` chain rooted at a `name_expr` over the first /// part. Panics on an empty input because the grammar's `_+` quantifier @@ -171,24 +194,24 @@ fn translation_rules() -> Vec> { // `*LiteralExpr` kind, so one rule per literal type suffices. rule!((integerLiteralExpr) @@node => expr { let value = tree!((int_literal #{node})); - if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + wrap_pattern_expr(&mut ctx, value) }), rule!((floatLiteralExpr) @@node => expr { let value = tree!((float_literal #{node})); - if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + wrap_pattern_expr(&mut ctx, value) }), rule!((booleanLiteralExpr) @@node => expr { let value = tree!((boolean_literal #{node})); - if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + wrap_pattern_expr(&mut ctx, value) }), rule!((nilLiteralExpr) @@node => expr { let value = tree!((builtin_expr #{node})); - if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + wrap_pattern_expr(&mut ctx, value) }), // Plain string literals (no interpolation) rule!((simpleStringLiteralExpr) @@node => expr { let value = tree!((string_literal #{node})); - if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + wrap_pattern_expr(&mut ctx, value) }), // String literals with interpolation rule!( @@ -207,7 +230,7 @@ fn translation_rules() -> Vec> { ), rule!((regexLiteralExpr) @@node => expr { let value = tree!((regex_literal #{node})); - if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + wrap_pattern_expr(&mut ctx, value) }), // ---- Names ---- // A function reference spelled with argument labels (`f(x:y:z:)`) is a @@ -223,11 +246,7 @@ fn translation_rules() -> Vec> { ), rule!((declReferenceExpr baseName: (identifier) @name) => expr { let name = tree!((name_expr identifier: (identifier #{name}))); - if ctx.in_pattern { - tree!((expr_equality_pattern expr: {name})) - } else { - name - } + wrap_pattern_expr(&mut ctx, name) }), // A bare name reference (`x`), and an operator used as a value (`+` in // `reduce(0, +)`), are both `declReferenceExpr`; its `baseName` is the @@ -685,10 +704,7 @@ fn translation_rules() -> Vec> { => expr { // Always translate the callee in non-pattern context. - let callee = ctx.scoped(|ctx| { - ctx.in_pattern = false; - ctx.translate(rawCallee) - })?; + let callee = translate_non_pattern(&mut ctx, rawCallee)?; if ctx.in_pattern { tree!((constructor_pattern constructor: {callee} element: {args})) } else { @@ -709,10 +725,11 @@ fn translation_rules() -> Vec> { (labeledExpr label: _? @@lbl expression: (functionCallExpr - calledExpression: @constructor + calledExpression: @@constructor arguments: _* @elements)) => argument { + let constructor = translate_non_pattern(&mut ctx, constructor)?; if ctx.in_pattern { tree!((pattern_element key: (identifier #{lbl})? @@ -758,24 +775,35 @@ fn translation_rules() -> Vec> { // meaning as `Array` rather than an array literal. rule!( (memberAccessExpr - base: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element)) + base: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @@element)) declName: (declReferenceExpr baseName: @member)) => - (member_access_expr - base: (generic_type_expr - base: (named_type_expr name: (identifier "Array")) - type_argument: {element}) - member: (identifier #{member})) + expr { + let element = translate_non_pattern(&mut ctx, element)?; + let node = tree!((member_access_expr + base: (generic_type_expr + base: (named_type_expr name: (identifier "Array")) + type_argument: {element}) + member: (identifier #{member}))); + wrap_pattern_expr(&mut ctx, node) + } ), rule!( - (memberAccessExpr base: @base declName: (declReferenceExpr baseName: @member)) + (memberAccessExpr base: @@base declName: (declReferenceExpr baseName: @member)) => - (member_access_expr base: {base} member: (identifier #{member})) + expr { + let base = translate_non_pattern(&mut ctx, base)?; + let node = tree!((member_access_expr base: {base} member: (identifier #{member}))); + wrap_pattern_expr(&mut ctx, node) + } ), rule!( (memberAccessExpr period: @dot declName: (declReferenceExpr baseName: @member)) => - (member_access_expr base: (inferred_type_expr #{dot}) member: (identifier #{member})) + expr { + let node = tree!((member_access_expr base: (inferred_type_expr #{dot}) member: (identifier #{member}))); + wrap_pattern_expr(&mut ctx, node) + } ), // Control transfer, one rule per keyword. `return` carries an optional // value; `break` / `continue` an optional target label; `throw` its diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output index d185e057a2f2..a0b19430d7b0 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output @@ -296,9 +296,11 @@ top_level identifier: identifier "s" switch_case pattern: - member_access_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got member_access_expr - base: inferred_type_expr "." - member: identifier "foo" + expr_equality_pattern + expr: + member_access_expr + base: inferred_type_expr "." + member: identifier "foo" body: block stmt: @@ -313,13 +315,13 @@ top_level element: string_literal "foo" switch_case pattern: - member_access_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got member_access_expr - base: - expr_equality_pattern <-- ERROR: The field member_access_expr.base should contain expr_or_type, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + member_access_expr + base: name_expr identifier: identifier "T" - member: identifier "foo" + member: identifier "foo" body: block stmt: @@ -334,15 +336,15 @@ top_level element: string_literal "foo" switch_case pattern: - member_access_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got member_access_expr - base: - array_literal - element: - expr_equality_pattern <-- ERROR: The field array_literal.element should contain expr, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + member_access_expr + base: + array_literal + element: name_expr identifier: identifier "T" - member: identifier "foo" + member: identifier "foo" body: block stmt: From b80626646865e708144581983c143ce8abdecd96 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 16:57:30 +0200 Subject: [PATCH 09/37] unified: Change tuple.element to Argument These can have labels --- unified/extractor/ast_types.yml | 2 +- unified/ql/lib/codeql/unified/internal/Ast.qll | 6 +++--- unified/ql/lib/unified.dbscheme | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index 032587452548..e1a1963d4803 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -231,7 +231,7 @@ named: # A tuple expression, such as `(a, b, c)`. tuple_expr: - element*: expr + element*: argument # A parameter. # diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 5f1bb6ae8f19..dbe14326b618 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -1421,10 +1421,10 @@ module Unified { final override string getAPrimaryQlClass() { result = "TupleExpr" } /** Gets the node corresponding to the field `element`. */ - final F::Expr getElement(int i) { unified_tuple_expr_element(this, i, result) } + final F::Argument getElement(int i) { unified_tuple_expr_element(this, i, result) } /** Gets the node corresponding to the field `element`. */ - final F::Expr getAnElement() { result = this.getElement(_) } + final F::Argument getAnElement() { result = this.getElement(_) } /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { unified_tuple_expr_element(this, _, result) } @@ -1607,7 +1607,7 @@ module Unified { } /** A class representing `type_test_pattern` nodes. */ - class TypeTestPattern extends @unified_type_test_pattern, F::AstNode { + class TypeTestPattern extends @unified_type_test_pattern, F::Pattern { /** Gets the name of the primary QL class for this element. */ final override string getAPrimaryQlClass() { result = "TypeTestPattern" } diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index 3bdaebd0f659..83545d2ac181 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -780,7 +780,7 @@ unified_parameter_def( unique int id: @unified_parameter ); -@unified_pattern = @unified_bulk_importing_pattern | @unified_conditional_pattern | @unified_constructor_pattern | @unified_expr_equality_pattern | @unified_name_pattern | @unified_or_pattern | @unified_token_ignore_pattern | @unified_token_unsupported_node | @unified_tuple_pattern +@unified_pattern = @unified_bulk_importing_pattern | @unified_conditional_pattern | @unified_constructor_pattern | @unified_expr_equality_pattern | @unified_name_pattern | @unified_or_pattern | @unified_token_ignore_pattern | @unified_token_unsupported_node | @unified_tuple_pattern | @unified_type_test_pattern unified_pattern_element_key( unique int unified_pattern_element: @unified_pattern_element ref, @@ -907,7 +907,7 @@ unified_try_expr_def( unified_tuple_expr_element( int unified_tuple_expr: @unified_tuple_expr ref, int index: int ref, - unique int element: @unified_expr ref + unique int element: @unified_argument ref ); unified_tuple_expr_def( From 0ca3c9445e1adfd32e37afba1bb497fb379bf64f Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 17:00:34 +0200 Subject: [PATCH 10/37] unified: Fix mapping of tuple patterns --- .../extractor/src/languages/swift/swift.rs | 8 +- .../swift/collections/tuple-literal.output | 12 +- .../control-flow/switch-tuple-pattern.output | 183 ++++++++++++++++++ .../control-flow/switch-tuple-pattern.swift | 6 + .../operators/parenthesised-expression.output | 14 +- 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output create mode 100644 unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.swift diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index d9c498b6bbe4..a6bc1e730408 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -341,7 +341,13 @@ fn translation_rules() -> Vec> { // TODO: a parenthesised single-element `tupleExpr` is really a grouping // expression and should be elided (unwrapped to its inner expression) // rather than modelled as a tuple. - rule!((tupleExpr) => (tuple_expr)), + rule!((tupleExpr elements: _* @els) => expr { + if ctx.in_pattern { + tree!((tuple_pattern element: {els})) + } else { + tree!((tuple_expr element: {els})) + } + }), // A code block contains its statements directly. rule!((codeBlock statements: _* @stmts) => (block stmt: {stmts})), // ---- Properties with accessors ---- diff --git a/unified/extractor/tests/corpus/swift/collections/tuple-literal.output b/unified/extractor/tests/corpus/swift/collections/tuple-literal.output index facbc2fcbb45..bfeaaf2c3e0e 100644 --- a/unified/extractor/tests/corpus/swift/collections/tuple-literal.output +++ b/unified/extractor/tests/corpus/swift/collections/tuple-literal.output @@ -54,4 +54,14 @@ top_level pattern: name_pattern identifier: identifier "t" - value: tuple_expr "(1, \"two\", 3.0)" + value: + tuple_expr + element: + argument + value: int_literal "1" + argument + value: + string_interpolation_expr + element: string_literal "two" + argument + value: float_literal "3.0" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output new file mode 100644 index 000000000000..8d6aca7532ff --- /dev/null +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output @@ -0,0 +1,183 @@ +switch point { +case (let x, let y): + print(x, y) +case (0, 0): + print("origin") +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + expressionStmt + expression: + switchExpr + leftBrace: { + rightBrace: } + cases: + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + patternExpr + pattern: + valueBindingPattern + pattern: + identifierPattern + identifier: identifier "x" + bindingSpecifier: let + trailingComma: , + labeledExpr + expression: + patternExpr + pattern: + valueBindingPattern + pattern: + identifierPattern + identifier: identifier "y" + bindingSpecifier: let + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "x" + trailingComma: , + labeledExpr + expression: + declReferenceExpr + baseName: identifier "y" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "0" + trailingComma: , + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "0" + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "origin" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + subject: + declReferenceExpr + baseName: identifier "point" + switchKeyword: switch + +--- + +top_level + body: + block + stmt: + switch_expr + value: + name_expr + identifier: identifier "point" + case: + switch_case + pattern: + tuple_pattern + element: + pattern_element + pattern: + name_pattern + identifier: identifier "x" + pattern_element + pattern: + name_pattern + identifier: identifier "y" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + name_expr + identifier: identifier "x" + argument + value: + name_expr + identifier: identifier "y" + switch_case + pattern: + tuple_pattern + element: + pattern_element + pattern: + expr_equality_pattern + expr: int_literal "0" + pattern_element + pattern: + expr_equality_pattern + expr: int_literal "0" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "origin" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.swift new file mode 100644 index 000000000000..1f3a1ab7cdbd --- /dev/null +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.swift @@ -0,0 +1,6 @@ +switch point { +case (let x, let y): + print(x, y) +case (0, 0): + print("origin") +} diff --git a/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output b/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output index 0324476ce994..828ade26919f 100644 --- a/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output +++ b/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output @@ -39,7 +39,19 @@ top_level block stmt: binary_expr - left: tuple_expr "(a + b)" + left: + tuple_expr + element: + argument + value: + binary_expr + left: + name_expr + identifier: identifier "a" + operator: infix_operator "+" + right: + name_expr + identifier: identifier "b" operator: infix_operator "*" right: name_expr From 3a04adbf515076e79adffb97f4bab16ba9339663 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 26 Aug 2026 17:10:26 +0200 Subject: [PATCH 11/37] unified: Collapse single-element tuples --- .../extractor/src/languages/swift/swift.rs | 49 +++++++++++++++---- .../operators/parenthesised-expression.output | 20 +++----- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index a6bc1e730408..d65212b26e1f 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -107,6 +107,31 @@ fn make_or_pattern( } } +/// If `els` contains a single `labeledExpr` node with no label, returns the +/// ID of its inner expression node. +fn get_single_unlabeled_tuple_expr( + ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, + els: &[yeast::Id], +) -> Option { + if els.len() != 1 { + return None; + } + let single = els[0]; + let label_field = ctx.ast.field_id_for_name("label")?; + let expr_field = ctx.ast.field_id_for_name("expression")?; + let node = ctx.ast.get_node(single)?; + + if node.kind_name() != "labeledExpr" { + return None; + } + + if !node.field_children(label_field).is_empty() { + return None; + } + + node.field_children(expr_field).first().copied() +} + /// Translate `node` in pattern context (`ctx.in_pattern = true`). fn translate_pattern( ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, @@ -335,17 +360,21 @@ fn translation_rules() -> Vec> { // Prefix unary operators (`!a`, `-x`). rule!((prefixOperatorExpr operator: @op expression: @operand) => (unary_expr operator: (prefix_operator #{op}) operand: {operand})), // A `tupleExpr` is a tuple literal (`(a, b)`) or a parenthesised - // expression (`(x)`). For now it is kept as an opaque `tuple_expr` leaf - // (its source text); its elements are not descended into. - // - // TODO: a parenthesised single-element `tupleExpr` is really a grouping - // expression and should be elided (unwrapped to its inner expression) - // rather than modelled as a tuple. - rule!((tupleExpr elements: _* @els) => expr { - if ctx.in_pattern { - tree!((tuple_pattern element: {els})) + // expression (`(x)`). A single-element unlabeled tuple expression is + // a grouping expression and unwraps to its inner expression. + rule!((tupleExpr elements: _* @@els) => expr { + if let Some(inner_e) = get_single_unlabeled_tuple_expr(&mut ctx, &els) { + ctx.translate(inner_e)?.into_iter().next().ok_or("tupleExpr element has no child")? } else { - tree!((tuple_expr element: {els})) + let mut out = Vec::new(); + for elem in els { + out.extend(ctx.translate(elem)?); + } + if ctx.in_pattern { + tree!((tuple_pattern element: {out})) + } else { + tree!((tuple_expr element: {out})) + } } }), // A code block contains its statements directly. diff --git a/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output b/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output index 828ade26919f..da23fdf9430b 100644 --- a/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output +++ b/unified/extractor/tests/corpus/swift/operators/parenthesised-expression.output @@ -40,18 +40,14 @@ top_level stmt: binary_expr left: - tuple_expr - element: - argument - value: - binary_expr - left: - name_expr - identifier: identifier "a" - operator: infix_operator "+" - right: - name_expr - identifier: identifier "b" + binary_expr + left: + name_expr + identifier: identifier "a" + operator: infix_operator "+" + right: + name_expr + identifier: identifier "b" operator: infix_operator "*" right: name_expr From acb1a355c7bc74e2b6fe5721288127d5e8e713f8 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 09:47:45 +0200 Subject: [PATCH 12/37] unified: Permit patterns in unresolved operator sequences Also hide the generated union type from the AST classes --- unified/extractor/ast_types.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index e1a1963d4803..b4b7a4760274 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -40,11 +40,6 @@ supertypes: expr_or_type: - expr - type_expr - # An element of an `unresolved_operator_sequence`: either an operand (`expr`) - # or one of the infix operators separating the operands. - expr_or_operator: - - expr - - infix_operator pattern: - name_pattern - tuple_pattern @@ -158,11 +153,13 @@ named: # determine (e.g. one imported from another module), it leaves that chain # unresolved and emits it here rather than guessing a (possibly wrong) # structure. The `element`s alternate operands (`expr`) and infix operators. + # Note that these can contain assignment operators, and the LHS of an assignment operator can be a pattern unresolved_operator_sequence: - element*: expr_or_operator + # An inline union is used here to avoid generating a somewhat Swift-specific and not-that-useful union type in the public AST + element*: [expr, pattern, infix_operator] - # Plain assignment assign_expr: + # Plain assignment target: expr_or_pattern value: expr From bc0a4bff360907b7a6e0acb2bc89e932d3491f32 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 09:48:29 +0200 Subject: [PATCH 13/37] unified: Regenerate QL --- unified/ql/lib/codeql/unified/internal/Ast.qll | 14 ++++---------- unified/ql/lib/unified.dbscheme | 6 +++--- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index dbe14326b618..81bb35c285f3 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -652,7 +652,7 @@ module Unified { } } - class Expr extends @unified_expr, F::ExprOrOperator, F::ExprOrPattern, F::ExprOrType, F::Stmt { } + class Expr extends @unified_expr, F::ExprOrPattern, F::ExprOrType, F::Stmt { } /** A class representing `expr_equality_pattern` nodes. */ class ExprEqualityPattern extends @unified_expr_equality_pattern, F::Pattern { @@ -666,8 +666,6 @@ module Unified { final override F::AstNode getAFieldOrChild() { unified_expr_equality_pattern_def(this, result) } } - class ExprOrOperator extends @unified_expr_or_operator, F::AstNode { } - class ExprOrPattern extends @unified_expr_or_pattern, F::AstNode { } class ExprOrType extends @unified_expr_or_type, F::AstNode { } @@ -941,9 +939,7 @@ module Unified { } /** A class representing `infix_operator` tokens. */ - class InfixOperator extends @unified_token_infix_operator, F::ExprOrOperator, F::Operator, - F::Token - { + class InfixOperator extends @unified_token_infix_operator, F::Operator, F::Token { /** Gets the name of the primary QL class for this element. */ final override string getAPrimaryQlClass() { result = "InfixOperator" } } @@ -1647,12 +1643,12 @@ module Unified { final override string getAPrimaryQlClass() { result = "UnresolvedOperatorSequence" } /** Gets the node corresponding to the field `element`. */ - final F::ExprOrOperator getElement(int i) { + final F::AstNode getElement(int i) { unified_unresolved_operator_sequence_element(this, i, result) } /** Gets the node corresponding to the field `element`. */ - final F::ExprOrOperator getAnElement() { result = this.getElement(_) } + final F::AstNode getAnElement() { result = this.getElement(_) } /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { @@ -2127,8 +2123,6 @@ module UnifiedFinal { final class ExprEqualityPattern = F::ExprEqualityPattern; - final class ExprOrOperator = F::ExprOrOperator; - final class ExprOrPattern = F::ExprOrPattern; final class ExprOrType = F::ExprOrType; diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index 83545d2ac181..f7dcd167c396 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -467,8 +467,6 @@ unified_expr_equality_pattern_def( int expr: @unified_expr ref ); -@unified_expr_or_operator = @unified_expr | @unified_token_infix_operator - @unified_expr_or_pattern = @unified_expr | @unified_pattern @unified_expr_or_type = @unified_expr | @unified_type_expr @@ -1027,11 +1025,13 @@ unified_unary_expr_def( int operator: @unified_operator ref ); +@unified_unresolved_operator_sequence_element_type = @unified_expr | @unified_pattern | @unified_token_infix_operator + #keyset[unified_unresolved_operator_sequence, index] unified_unresolved_operator_sequence_element( int unified_unresolved_operator_sequence: @unified_unresolved_operator_sequence ref, int index: int ref, - unique int element: @unified_expr_or_operator ref + unique int element: @unified_unresolved_operator_sequence_element_type ref ); unified_unresolved_operator_sequence_def( From a707d4ba175f3a8cbfd51d5d50c70f0d9cb5afa4 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 09:54:04 +0200 Subject: [PATCH 14/37] unified: Use top-level rules for patternExpr and wildcard Previously these appeared as nested patterns, meaning they only worked in certain contexts. Decompose the nested patterns into two top-level rules. --- .../extractor/src/languages/swift/swift.rs | 20 +++++-------------- ...solved-operator-sequence-with-casts.output | 6 ++---- .../unresolved-operator-sequence.output | 3 +-- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index d65212b26e1f..df212d2df5b9 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -277,11 +277,8 @@ fn translation_rules() -> Vec> { // `reduce(0, +)`), are both `declReferenceExpr`; its `baseName` is the // referenced identifier / operator symbol. rule!((declReferenceExpr baseName: @name) => (name_expr identifier: (identifier #{name}))), - // A discard `_` used as an expression — e.g. the target of a discarding - // assignment `_ = x`. swift-syntax models it as a `discardAssignmentExpr`; - // the target AST has no expression-level discard (only `ignore_pattern`, - // which is a pattern), so it becomes a `name_expr` over the `_` token. - rule!((discardAssignmentExpr wildcard: @@w) => (name_expr identifier: (identifier #{w}))), + // A discard `_` used as the target of an assignment becomes an `ignore_pattern`. + rule!((discardAssignmentExpr wildcard: @@w) => (ignore_pattern #{w})), // A generic specialization in expression position (`C`, // `Array`) is represented by swift-syntax as a // `genericSpecializationExpr`. When used as a call target @@ -628,9 +625,7 @@ fn translation_rules() -> Vec> { // handling in the future. (Redundant with the catch-all fallback, but // kept as a signpost.) rule!((isTypePattern) => (unsupported_node)), - // A wildcard *binding* pattern (`let _ = x`, `for _ in xs`). swift-syntax - // models this as a `wildcardPattern`, distinct from the `_` match form - // handled by the context-aware `discardAssignmentExpr` rule. + // A wildcard *binding* pattern (`let _ = x`, `for _ in xs`). rule!((wildcardPattern) => (ignore_pattern)), // An expression pattern only establishes pattern context; its child // determines the concrete pattern shape. @@ -779,14 +774,9 @@ fn translation_rules() -> Vec> { } ), rule!( - (labeledExpr label: _? @@lbl expression: (patternExpr pattern: @p)) + (patternExpr pattern: @@p) => - (pattern_element key: (identifier #{lbl})? pattern: {p}) - ), - rule!( - (labeledExpr label: _? @@lbl expression: (discardAssignmentExpr) @@wildcard) - => - (pattern_element key: (identifier #{lbl})? pattern: (ignore_pattern #{wildcard})) + pattern { translate_pattern(&mut ctx, p)? } ), rule!( (labeledExpr label: _? @@lbl expression: @val) diff --git a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output index a0563c7bc88b..dffe2b0829e9 100644 --- a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output +++ b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output @@ -117,8 +117,7 @@ top_level stmt: unresolved_operator_sequence element: - name_expr - identifier: identifier "_" + ignore_pattern "_" infix_operator "=" name_expr identifier: identifier "a" @@ -129,8 +128,7 @@ top_level identifier: identifier "b" unresolved_operator_sequence element: - name_expr - identifier: identifier "_" + ignore_pattern "_" infix_operator "=" name_expr identifier: identifier "a" diff --git a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output index 46dc8ca6ae89..c46180a2dc0b 100644 --- a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output +++ b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output @@ -90,8 +90,7 @@ top_level stmt: unresolved_operator_sequence element: - name_expr - identifier: identifier "_" + ignore_pattern "_" infix_operator "=" name_expr identifier: identifier "a" From c19b61bbe5f2e6e4fb99fdb742119fe8bf9267d7 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 09:54:26 +0200 Subject: [PATCH 15/37] unified: Handle 'asExpr' in pattern context --- .../extractor/src/languages/swift/swift.rs | 8 +- .../switch-with-binding-pattern.output | 86 +++++++++++++++++++ .../switch-with-binding-pattern.swift | 2 + 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index df212d2df5b9..d40f37db9632 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -1097,8 +1097,12 @@ fn translation_rules() -> Vec> { rule!((catchClause body: @body) => (catch_clause body: {body})), // As expression (type cast) — as?, as! rule!((asExpr expression: @val questionOrExclamationMark: _? @@mark type: @ty) => type_cast_expr { - let op = format!("as{}", mark.map(|m| ctx.source_text(m)).unwrap_or_default()); - tree!((type_cast_expr expr: {val} operator: (infix_operator #{op}) type: {ty})) + if ctx.in_pattern { + tree!((type_test_pattern pattern: {val} type: {ty})) + } else { + let op = format!("as{}", mark.map(|m| ctx.source_text(m)).unwrap_or_default()); + tree!((type_cast_expr expr: {val} operator: (infix_operator #{op}) type: {ty})) + } }), // Check expression (`x is T`) → type_test_expr rule!((isExpr expression: @val type: @ty) => (type_test_expr expr: {val} operator: (infix_operator "is") type: {ty})), diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output index a0b19430d7b0..2e97b8b07222 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output @@ -11,6 +11,8 @@ case T.foo: // The parser can't tell the difference. case [T].foo: print("foo") +case .foo(let x as T): + print(x) } --- @@ -230,6 +232,61 @@ sourceFile calledExpression: declReferenceExpr baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + patternExpr + pattern: + valueBindingPattern + pattern: + expressionPattern + expression: + asExpr + expression: + patternExpr + pattern: + identifierPattern + identifier: identifier "x" + asKeyword: as + type: + identifierType + name: identifier "T" + bindingSpecifier: let + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "foo" + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "x" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" subject: declReferenceExpr baseName: identifier "shape" @@ -357,3 +414,32 @@ top_level value: string_interpolation_expr element: string_literal "foo" + switch_case + pattern: + constructor_pattern + constructor: + member_access_expr + base: inferred_type_expr "." + member: identifier "foo" + element: + pattern_element + pattern: + type_test_pattern + pattern: + name_pattern + identifier: identifier "x" + type: + named_type_expr + name: identifier "T" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + name_expr + identifier: identifier "x" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift index 9f430ca21f37..f05bc310b30e 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift @@ -11,4 +11,6 @@ case T.foo: // The parser can't tell the difference. case [T].foo: print("foo") +case .foo(let x as T): + print(x) } From d4aacba24c2e5563b55aff1879c160d39dc2e4e5 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 09:58:13 +0200 Subject: [PATCH 16/37] unified: Make type_test_pattern.pattern optional --- unified/extractor/ast_types.yml | 2 +- unified/ql/lib/codeql/unified/internal/Ast.qll | 7 +++---- unified/ql/lib/unified.dbscheme | 6 +++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index b4b7a4760274..e2f3dd933a85 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -466,7 +466,7 @@ named: # In Swift: `if let y = x as? Foo` is a pattern_guard_expr containing a type_test_pattern # In Java: `x instanceof Foo y` is a type_test_pattern wrapping a name_pattern type_test_pattern: - pattern: pattern + pattern?: pattern type: type_expr # A '*' pattern that imports all members of the incoming value into the local scope diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 81bb35c285f3..19e6aa806cbb 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -1608,15 +1608,14 @@ module Unified { final override string getAPrimaryQlClass() { result = "TypeTestPattern" } /** Gets the node corresponding to the field `pattern`. */ - final F::Pattern getPattern() { unified_type_test_pattern_def(this, result, _) } + final F::Pattern getPattern() { unified_type_test_pattern_pattern(this, result) } /** Gets the node corresponding to the field `type`. */ - final F::TypeExpr getType() { unified_type_test_pattern_def(this, _, result) } + final F::TypeExpr getType() { unified_type_test_pattern_def(this, result) } /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { - unified_type_test_pattern_def(this, result, _) or - unified_type_test_pattern_def(this, _, result) + unified_type_test_pattern_pattern(this, result) or unified_type_test_pattern_def(this, result) } } diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index f7dcd167c396..4a1a45323226 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -1013,9 +1013,13 @@ unified_type_test_expr_def( int type__: @unified_type_expr ref ); +unified_type_test_pattern_pattern( + unique int unified_type_test_pattern: @unified_type_test_pattern ref, + unique int pattern: @unified_pattern ref +); + unified_type_test_pattern_def( unique int id: @unified_type_test_pattern, - int pattern: @unified_pattern ref, int type__: @unified_type_expr ref ); From 9f0a12498f88774bc9aa595453e994c9ee581744 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 09:58:26 +0200 Subject: [PATCH 17/37] unified: support 'case is T' --- .../extractor/src/languages/swift/swift.rs | 7 +-- .../switch-with-binding-pattern.output | 52 +++++++++++++++++++ .../switch-with-binding-pattern.swift | 2 + 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index d40f37db9632..59d155e29b3e 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -620,11 +620,8 @@ fn translation_rules() -> Vec> { => (pattern_element key: (identifier #{label})? pattern: {p}) ), - // A type-casting pattern (`case is T`). Not yet supported, so it is - // mapped to `unsupported_node` — an explicit reminder that this needs - // handling in the future. (Redundant with the catch-all fallback, but - // kept as a signpost.) - rule!((isTypePattern) => (unsupported_node)), + // A type-casting pattern (`case is T`). + rule!((isTypePattern type: @@ty) => (type_test_pattern type: { translate_non_pattern(&mut ctx, ty)? } )), // A wildcard *binding* pattern (`let _ = x`, `for _ in xs`). rule!((wildcardPattern) => (ignore_pattern)), // An expression pattern only establishes pattern context; its child diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output index 2e97b8b07222..309e1dd0ce43 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output @@ -13,6 +13,8 @@ case [T].foo: print("foo") case .foo(let x as T): print(x) +case is T: + print("is T") } --- @@ -287,6 +289,38 @@ sourceFile calledExpression: declReferenceExpr baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + isTypePattern + type: + identifierType + name: identifier "T" + isKeyword: is + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "is T" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" subject: declReferenceExpr baseName: identifier "shape" @@ -443,3 +477,21 @@ top_level value: name_expr identifier: identifier "x" + switch_case + pattern: + type_test_pattern + type: + named_type_expr + name: identifier "T" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "is T" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift index f05bc310b30e..0dc5aa04d61e 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.swift @@ -13,4 +13,6 @@ case [T].foo: print("foo") case .foo(let x as T): print(x) +case is T: + print("is T") } From 77c3018f28de47d148de11b2083d3c3c7b5f8e0e Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 13:44:36 +0200 Subject: [PATCH 18/37] unified: Use the new 'where' syntax for tuples --- .../extractor/src/languages/swift/swift.rs | 67 +++++++------------ 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 59d155e29b3e..49ff970205fd 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -107,31 +107,6 @@ fn make_or_pattern( } } -/// If `els` contains a single `labeledExpr` node with no label, returns the -/// ID of its inner expression node. -fn get_single_unlabeled_tuple_expr( - ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, - els: &[yeast::Id], -) -> Option { - if els.len() != 1 { - return None; - } - let single = els[0]; - let label_field = ctx.ast.field_id_for_name("label")?; - let expr_field = ctx.ast.field_id_for_name("expression")?; - let node = ctx.ast.get_node(single)?; - - if node.kind_name() != "labeledExpr" { - return None; - } - - if !node.field_children(label_field).is_empty() { - return None; - } - - node.field_children(expr_field).first().copied() -} - /// Translate `node` in pattern context (`ctx.in_pattern = true`). fn translate_pattern( ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, @@ -356,24 +331,30 @@ fn translation_rules() -> Vec> { rule!((sequenceExpr elements: _* @els) => (unresolved_operator_sequence element: {els})), // Prefix unary operators (`!a`, `-x`). rule!((prefixOperatorExpr operator: @op expression: @operand) => (unary_expr operator: (prefix_operator #{op}) operand: {operand})), - // A `tupleExpr` is a tuple literal (`(a, b)`) or a parenthesised - // expression (`(x)`). A single-element unlabeled tuple expression is - // a grouping expression and unwraps to its inner expression. - rule!((tupleExpr elements: _* @@els) => expr { - if let Some(inner_e) = get_single_unlabeled_tuple_expr(&mut ctx, &els) { - ctx.translate(inner_e)?.into_iter().next().ok_or("tupleExpr element has no child")? - } else { - let mut out = Vec::new(); - for elem in els { - out.extend(ctx.translate(elem)?); - } - if ctx.in_pattern { - tree!((tuple_pattern element: {out})) - } else { - tree!((tuple_expr element: {out})) - } - } - }), + // A tuple with a single unlabeled element is a grouping expression. + rule!( + (tupleExpr + elements: (labeledExpr + label: _? @label + expression: @inner) + elements: _* @rest) + where label.is_none() && rest.is_empty() + => + expr { inner } + ), + // Other tuple expressions in pattern context become tuple patterns. + rule!( + (tupleExpr elements: _* @elements) + where ctx.in_pattern + => + (tuple_pattern element: {elements}) + ), + // Otherwise, preserve the tuple expression. + rule!( + (tupleExpr elements: _* @elements) + => + (tuple_expr element: {elements}) + ), // A code block contains its statements directly. rule!((codeBlock statements: _* @stmts) => (block stmt: {stmts})), // ---- Properties with accessors ---- From a036680c65bf0ede924c99b26943c746a9b3b6cd Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 13:48:57 +0200 Subject: [PATCH 19/37] unified: Use 'where' syntax for constructor_pattern --- unified/extractor/src/languages/swift/swift.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 49ff970205fd..84cea48cf47c 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -709,17 +709,18 @@ fn translation_rules() -> Vec> { ), rule!( (functionCallExpr calledExpression: @@rawCallee arguments: _* @args) + where ctx.in_pattern => - expr { - // Always translate the callee in non-pattern context. + pattern { let callee = translate_non_pattern(&mut ctx, rawCallee)?; - if ctx.in_pattern { - tree!((constructor_pattern constructor: {callee} element: {args})) - } else { - tree!((call_expr callee: {callee} argument: {args})) - } + tree!((constructor_pattern constructor: {callee} element: {args})) } ), + rule!( + (functionCallExpr calledExpression: @callee arguments: _* @args) + => + (call_expr callee: {callee} argument: {args}) + ), // A call argument or an enum-case pattern argument. When translating an // enum-case `constructor_pattern`'s arguments (`ctx.in_pattern`), a // `patternExpr` argument (`let x`) becomes a bound `name_pattern`, a From 389e064f435abf6e86cfdeef9a1c13a641ad50e5 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 13:51:38 +0200 Subject: [PATCH 20/37] unified: Remove superfluous rule This rule matches a labelled function-call, but the general rules for labelled exprs and function calls result in the same AST. --- .../extractor/src/languages/swift/swift.rs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 84cea48cf47c..4627f6d3acdb 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -721,37 +721,6 @@ fn translation_rules() -> Vec> { => (call_expr callee: {callee} argument: {args}) ), - // A call argument or an enum-case pattern argument. When translating an - // enum-case `constructor_pattern`'s arguments (`ctx.in_pattern`), a - // `patternExpr` argument (`let x`) becomes a bound `name_pattern`, a - // wildcard (`_`) becomes an `ignore_pattern`, and any other expression - // becomes an `expr_equality_pattern`; each is wrapped in a - // `pattern_element` carrying the optional argument label as its `key`. - // Otherwise the argument keeps its label as the `name` and its value. - // The pattern-only shapes (`patternExpr`, `discardAssignmentExpr`) are - // matched first; they never occur as ordinary call arguments. - rule!( - (labeledExpr - label: _? @@lbl - expression: (functionCallExpr - calledExpression: @@constructor - arguments: _* @elements)) - => - argument { - let constructor = translate_non_pattern(&mut ctx, constructor)?; - if ctx.in_pattern { - tree!((pattern_element - key: (identifier #{lbl})? - pattern: (constructor_pattern - constructor: {constructor} - element: {elements}))) - } else { - tree!((argument - name: (identifier #{lbl})? - value: (call_expr callee: {constructor} argument: {elements}))) - } - } - ), rule!( (patternExpr pattern: @@p) => From 2b6f6d897d81af7df050a809a01849eaafab3050 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 14:25:53 +0200 Subject: [PATCH 21/37] unified: Use 'where' clause in labeledExpr rule --- unified/extractor/src/languages/swift/swift.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 4627f6d3acdb..79e267670b40 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -728,16 +728,16 @@ fn translation_rules() -> Vec> { ), rule!( (labeledExpr label: _? @@lbl expression: @val) + where ctx.in_pattern => - argument { - if ctx.in_pattern { - tree!((pattern_element - key: (identifier #{lbl})? - pattern: {val})) - } else { - tree!((argument name: (identifier #{lbl})? value: {val})) - } - } + (pattern_element + key: (identifier #{lbl})? + pattern: {val}) + ), + rule!( + (labeledExpr label: _? @@lbl expression: @val) + => + (argument name: (identifier #{lbl})? value: {val}) ), // Member access (`list.append`). The `declName` is itself a // `declReferenceExpr`; pull its `baseName` out as the member identifier. From c8d55e5c0ff285f01a51bd1cb59940e1cbb4f715 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 15:01:28 +0200 Subject: [PATCH 22/37] unified: Use where in optionalChainingExpr --- .../extractor/src/languages/swift/swift.rs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 79e267670b40..000f4efb184c 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -991,19 +991,17 @@ fn translation_rules() -> Vec> { (call_expr callee: {callee} argument: {args}) ), // ---- Optionals and errors ---- - // Optional chaining — unwrap the marker - rule!((optionalChainingExpr expression: @@inner) => expr { - let inner = ctx.translate(inner)?.into_iter().next().ok_or("optional chaining expression has no child")?; - if ctx.in_pattern { - tree!((constructor_pattern - constructor: (member_access_expr - base: (named_type_expr name: (identifier "Optional")) - member: (identifier "some")) - element: (pattern_element pattern: {inner}))) - } else { - inner - } - }), + // Postfix `?` in a pattern means `Optional.some`. E.g: `(x,y)?` -> `Optional.some((x,y))` + rule!((optionalChainingExpr expression: @inner) + where ctx.in_pattern => + (constructor_pattern + constructor: (member_access_expr + base: (named_type_expr name: (identifier "Optional")) + member: (identifier "some")) + element: (pattern_element pattern: {inner})) + ), + // TODO: handle suffix "?" in expr and type contexts + rule!((optionalChainingExpr expression: @inner) => expr { inner }), // try/try?/try! expr → unary_expr with operator "try", "try?" or "try!" rule!( (tryExpr questionOrExclamationMark: _? @@m expression: @e) From 63976db57ee888c54f013265e4077e68d7353abf Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 27 Aug 2026 15:07:13 +0200 Subject: [PATCH 23/37] unified: 'where' syntaxin asExpr --- unified/extractor/src/languages/swift/swift.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 000f4efb184c..158429f460a2 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -1042,14 +1042,20 @@ fn translation_rules() -> Vec> { // Catch block without error binding rule!((catchClause body: @body) => (catch_clause body: {body})), // As expression (type cast) — as?, as! - rule!((asExpr expression: @val questionOrExclamationMark: _? @@mark type: @ty) => type_cast_expr { - if ctx.in_pattern { - tree!((type_test_pattern pattern: {val} type: {ty})) - } else { + rule!( + (asExpr expression: @val type: @ty) + where ctx.in_pattern + => + (type_test_pattern pattern: {val} type: {ty}) + ), + rule!( + (asExpr expression: @val questionOrExclamationMark: _? @@mark type: @ty) + => + type_cast_expr { let op = format!("as{}", mark.map(|m| ctx.source_text(m)).unwrap_or_default()); tree!((type_cast_expr expr: {val} operator: (infix_operator #{op}) type: {ty})) } - }), + ), // Check expression (`x is T`) → type_test_expr rule!((isExpr expression: @val type: @ty) => (type_test_expr expr: {val} operator: (infix_operator "is") type: {ty})), // Await expression → unary_expr with operator "await" From 08ee7df8cf005927ad486db31df473788a6a8795 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 09:39:19 +0200 Subject: [PATCH 24/37] unified: Make update_corpus.sh run fewer tests --- unified/scripts/update-corpus.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/scripts/update-corpus.sh b/unified/scripts/update-corpus.sh index 2f3ebade8cb3..5c3c95cd0740 100755 --- a/unified/scripts/update-corpus.sh +++ b/unified/scripts/update-corpus.sh @@ -5,4 +5,4 @@ IFS=$'\n\t' cd "$(dirname "$0")/.." cd extractor -UNIFIED_UPDATE_CORPUS=1 cargo test +UNIFIED_UPDATE_CORPUS=1 cargo test test_corpus From 8358fd1973eedb4694dcb31ea6ecdf3a9f4f4f38 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 09:53:09 +0200 Subject: [PATCH 25/37] unified: Add test with arbitrary expressions in pattern context Currently many type errors in the output --- .../switch-expression-pattern.output | 420 ++++++++++++++++++ .../switch-expression-pattern.swift | 9 + 2 files changed, 429 insertions(+) create mode 100644 unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output create mode 100644 unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output new file mode 100644 index 000000000000..2f511f6119e7 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -0,0 +1,420 @@ +// Arbitrary expressions may appear in pattern context +switch x { +case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y: + print("expr") +case true ? y : z: + print("ternary is also valid") +default: + break +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + expressionStmt + expression: + switchExpr + leftBrace: { + rightBrace: } + cases: + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + declReferenceExpr + baseName: identifier "y" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "+" + leftOperand: + declReferenceExpr + baseName: identifier "y" + rightOperand: + integerLiteralExpr + literal: integerLiteral "1" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + prefixOperatorExpr + expression: + declReferenceExpr + baseName: identifier "y" + operator: prefixOperator "-" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "..." + leftOperand: + declReferenceExpr + baseName: identifier "y" + rightOperand: + declReferenceExpr + baseName: identifier "z" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "foo" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + integerLiteralExpr + literal: integerLiteral "4" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "bar" + base: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "foo" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "baz" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + arrayExpr + elements: + arrayElement + expression: + integerLiteralExpr + literal: integerLiteral "42" + leftSquare: [ + rightSquare: ] + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + dictionaryExpr + leftSquare: [ + rightSquare: ] + content: + dictionaryElement + colon: : + value: + integerLiteralExpr + literal: integerLiteral "1" + key: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "a" + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + tryExpr + expression: + declReferenceExpr + baseName: identifier "y" + tryKeyword: try + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + forceUnwrapExpr + expression: + declReferenceExpr + baseName: identifier "y" + exclamationMark: ! + switchCaseItem + trailingComma: , + pattern: + expressionPattern + expression: + isExpr + expression: + declReferenceExpr + baseName: identifier "y" + type: + identifierType + name: identifier "T" + isKeyword: is + switchCaseItem + pattern: + expressionPattern + expression: + awaitExpr + expression: + declReferenceExpr + baseName: identifier "y" + awaitKeyword: await + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "expr" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + ternaryExpr + colon: : + condition: + booleanLiteralExpr + literal: true + questionMark: ? + elseExpression: + declReferenceExpr + baseName: identifier "z" + thenExpression: + declReferenceExpr + baseName: identifier "y" + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "ternary is also valid" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + switchCase + label: + switchDefaultLabel + colon: : + defaultKeyword: default + statements: + codeBlockItem + item: + breakStmt + breakKeyword: break + subject: + declReferenceExpr + baseName: identifier "x" + switchKeyword: switch + +--- + +top_level + body: + block + stmt: + switch_expr + value: + name_expr + identifier: identifier "x" + case: + switch_case + pattern: + or_pattern + pattern: + expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + binary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got binary_expr + left: + expr_equality_pattern <-- ERROR: The field binary_expr.left should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: infix_operator "+" + right: + expr_equality_pattern <-- ERROR: The field binary_expr.right should contain expr, but got expr_equality_pattern + expr: int_literal "1" + unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr + operand: + expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: prefix_operator "-" + binary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got binary_expr + left: + expr_equality_pattern <-- ERROR: The field binary_expr.left should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: infix_operator "..." + right: + expr_equality_pattern <-- ERROR: The field binary_expr.right should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "z" + string_interpolation_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got string_interpolation_expr + element: string_literal "foo" + expr_equality_pattern + expr: int_literal "4" + expr_equality_pattern + expr: + member_access_expr + base: + call_expr + callee: + name_expr + identifier: identifier "foo" + member: identifier "bar" + expr_equality_pattern + expr: + member_access_expr + base: inferred_type_expr "." + member: identifier "baz" + array_literal <-- ERROR: The field or_pattern.pattern should contain pattern, but got array_literal + element: + expr_equality_pattern <-- ERROR: The field array_literal.element should contain expr, but got expr_equality_pattern + expr: int_literal "42" + map_literal "[\"a\": 1]" <-- ERROR: The field or_pattern.pattern should contain pattern, but got map_literal + unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr + operand: + expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: prefix_operator "try" + unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr + operand: + expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: postfix_operator "!" + type_test_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got type_test_expr + expr: + expr_equality_pattern <-- ERROR: The field type_test_expr.expr should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: infix_operator "is" + type: + named_type_expr + name: identifier "T" + unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr + operand: + expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + operator: prefix_operator "await" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "expr" + switch_case + pattern: + if_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got if_expr + condition: + expr_equality_pattern <-- ERROR: The field if_expr.condition should contain expr, but got expr_equality_pattern + expr: boolean_literal "true" + then: + expr_equality_pattern <-- ERROR: The field if_expr.then should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + else: + expr_equality_pattern <-- ERROR: The field if_expr.else should contain expr, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "z" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + string_interpolation_expr + element: string_literal "ternary is also valid" + switch_case + body: + block + stmt: break_expr "break" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift new file mode 100644 index 000000000000..369d84ca0da5 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift @@ -0,0 +1,9 @@ +// Arbitrary expressions may appear in pattern context +switch x { +case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y: + print("expr") +case true ? y : z: + print("ternary is also valid") +default: + break +} From d95f6c6506ee2e71c6321539e6af59e4f69e5565 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 09:59:08 +0200 Subject: [PATCH 26/37] unified: "Coerce" expr to pattern This commits demonstrates the idea we're going to use in later commits, deliberately only done for one expr type so far. --- .../extractor/src/languages/swift/swift.rs | 6 ++++ .../switch-expression-pattern.output | 28 ++++++++----------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 158429f460a2..58f280e0613b 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -283,6 +283,12 @@ fn translation_rules() -> Vec> { // can't match on token text, so a small Rust block reads the spelling // and routes to `compound_assign_expr` or `binary_expr`. The operator // is captured raw (`@@op`) to read its spelling. + rule!((infixOperatorExpr) @@expr where ctx.in_pattern => + pattern { + ctx.in_pattern = false; + tree!((expr_equality_pattern expr: {ctx.translate(expr)?})) + } + ), rule!( (infixOperatorExpr leftOperand: @l operator: (binaryOperatorExpr) @@op rightOperand: @r) => diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index 2f511f6119e7..1cf92fcba475 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -291,16 +291,14 @@ top_level expr: name_expr identifier: identifier "y" - binary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got binary_expr - left: - expr_equality_pattern <-- ERROR: The field binary_expr.left should contain expr, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + binary_expr + left: name_expr identifier: identifier "y" - operator: infix_operator "+" - right: - expr_equality_pattern <-- ERROR: The field binary_expr.right should contain expr, but got expr_equality_pattern - expr: int_literal "1" + operator: infix_operator "+" + right: int_literal "1" unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr operand: expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern @@ -308,16 +306,14 @@ top_level name_expr identifier: identifier "y" operator: prefix_operator "-" - binary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got binary_expr - left: - expr_equality_pattern <-- ERROR: The field binary_expr.left should contain expr, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + binary_expr + left: name_expr identifier: identifier "y" - operator: infix_operator "..." - right: - expr_equality_pattern <-- ERROR: The field binary_expr.right should contain expr, but got expr_equality_pattern - expr: + operator: infix_operator "..." + right: name_expr identifier: identifier "z" string_interpolation_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got string_interpolation_expr From a0666928988affba76610e683c466cef723e37b1 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:01:44 +0200 Subject: [PATCH 27/37] unified: Factor the pattern into a macro --- .../extractor/src/languages/swift/swift.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 58f280e0613b..f65198ca132c 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -167,6 +167,20 @@ const COMPOUND_ASSIGN_OPS: &[&str] = &[ "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "|=", "^=", "&+=", "&-=", "&*=", ]; +/// `coerce_to_pattern!(exprKind)` generates a rule to ensure that if the given node kind appears in a pattern context, +/// it is translated as an expression and wrapped in an `expr_equality_pattern`. +/// Must appear before the other rules in order to apply first. +macro_rules! coerce_to_pattern { + ($kind:ident) => { + rule!(($kind) @@expr where ctx.in_pattern => + pattern { + ctx.in_pattern = false; + tree!((expr_equality_pattern expr: {ctx.translate(expr)?})) + } + ) + }; +} + fn translation_rules() -> Vec> { vec![ // ---- Top-level ---- @@ -283,12 +297,7 @@ fn translation_rules() -> Vec> { // can't match on token text, so a small Rust block reads the spelling // and routes to `compound_assign_expr` or `binary_expr`. The operator // is captured raw (`@@op`) to read its spelling. - rule!((infixOperatorExpr) @@expr where ctx.in_pattern => - pattern { - ctx.in_pattern = false; - tree!((expr_equality_pattern expr: {ctx.translate(expr)?})) - } - ), + coerce_to_pattern!(infixOperatorExpr), rule!( (infixOperatorExpr leftOperand: @l operator: (binaryOperatorExpr) @@op rightOperand: @r) => From 1f8de18c555c15d8d2dea0bd2b800ba0849b74ab Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:07:19 +0200 Subject: [PATCH 28/37] unified: Apply coercion macro to other exprs --- .../extractor/src/languages/swift/swift.rs | 9 ++ .../switch-expression-pattern.output | 85 +++++++++---------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index f65198ca132c..8cd37c86ed38 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -228,6 +228,7 @@ fn translation_rules() -> Vec> { wrap_pattern_expr(&mut ctx, value) }), // String literals with interpolation + coerce_to_pattern!(stringLiteralExpr), rule!( (stringLiteralExpr segments: _* @segs) => @@ -247,6 +248,7 @@ fn translation_rules() -> Vec> { wrap_pattern_expr(&mut ctx, value) }), // ---- Names ---- + coerce_to_pattern!(prefixOperatorExpr), // A function reference spelled with argument labels (`f(x:y:z:)`) is a // `declReferenceExpr` carrying `argumentNames`. Mark it unsupported for // now (rather than let the bare-name rule below treat it as a plain @@ -875,6 +877,7 @@ fn translation_rules() -> Vec> { else: {else_stmts}) ), // Ternary (`c ? a : b`) desugars to an `if_expr`. + coerce_to_pattern!(ternaryExpr), rule!( (ternaryExpr condition: @cond thenExpression: @then_val elseExpression: @else_val) => @@ -987,6 +990,7 @@ fn translation_rules() -> Vec> { // ---- Collections ---- // An array literal (`[1, 2, 3]`). Each `arrayElement` unwraps to its // contained expression. + coerce_to_pattern!(arrayExpr), rule!( (arrayExpr elements: _* @els) => @@ -995,6 +999,7 @@ fn translation_rules() -> Vec> { rule!((arrayElement expression: @e) => expr { e }), // A dictionary literal (`["a": 1]`) is kept as an opaque `map_literal` // leaf (its source span). + coerce_to_pattern!(dictionaryExpr), rule!((dictionaryExpr) => (map_literal)), // A subscript access (`xs[0]`) is modelled as a call. swift-syntax does // report a distinct `subscriptCallExpr`, so giving @@ -1018,6 +1023,7 @@ fn translation_rules() -> Vec> { // TODO: handle suffix "?" in expr and type contexts rule!((optionalChainingExpr expression: @inner) => expr { inner }), // try/try?/try! expr → unary_expr with operator "try", "try?" or "try!" + coerce_to_pattern!(tryExpr), rule!( (tryExpr questionOrExclamationMark: _? @@m expression: @e) => @@ -1072,11 +1078,14 @@ fn translation_rules() -> Vec> { } ), // Check expression (`x is T`) → type_test_expr + coerce_to_pattern!(isExpr), rule!((isExpr expression: @val type: @ty) => (type_test_expr expr: {val} operator: (infix_operator "is") type: {ty})), // Await expression → unary_expr with operator "await" + coerce_to_pattern!(awaitExpr), rule!((awaitExpr expression: @val) => (unary_expr operator: (prefix_operator "await") operand: {val})), // Force-unwrap (`x!`) → postfix unary_expr, via swift-syntax's dedicated // `forceUnwrapExpr` node. + coerce_to_pattern!(forceUnwrapExpr), rule!((forceUnwrapExpr expression: @e) => (unary_expr operator: (postfix_operator "!") operand: {e})), // ---- Imports ---- // An import declaration. The dotted path (a list of diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index 1cf92fcba475..7b49c7009241 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -299,13 +299,13 @@ top_level identifier: identifier "y" operator: infix_operator "+" right: int_literal "1" - unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr - operand: - expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + unary_expr + operand: name_expr identifier: identifier "y" - operator: prefix_operator "-" + operator: prefix_operator "-" expr_equality_pattern expr: binary_expr @@ -316,8 +316,10 @@ top_level right: name_expr identifier: identifier "z" - string_interpolation_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got string_interpolation_expr - element: string_literal "foo" + expr_equality_pattern + expr: + string_interpolation_expr + element: string_literal "foo" expr_equality_pattern expr: int_literal "4" expr_equality_pattern @@ -334,42 +336,43 @@ top_level member_access_expr base: inferred_type_expr "." member: identifier "baz" - array_literal <-- ERROR: The field or_pattern.pattern should contain pattern, but got array_literal - element: - expr_equality_pattern <-- ERROR: The field array_literal.element should contain expr, but got expr_equality_pattern - expr: int_literal "42" - map_literal "[\"a\": 1]" <-- ERROR: The field or_pattern.pattern should contain pattern, but got map_literal - unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr - operand: - expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + array_literal + element: int_literal "42" + expr_equality_pattern + expr: map_literal "[\"a\": 1]" + expr_equality_pattern + expr: + unary_expr + operand: name_expr identifier: identifier "y" - operator: prefix_operator "try" - unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr - operand: - expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern - expr: + operator: prefix_operator "try" + expr_equality_pattern + expr: + unary_expr + operand: name_expr identifier: identifier "y" - operator: postfix_operator "!" - type_test_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got type_test_expr + operator: postfix_operator "!" + expr_equality_pattern expr: - expr_equality_pattern <-- ERROR: The field type_test_expr.expr should contain expr, but got expr_equality_pattern + type_test_expr expr: name_expr identifier: identifier "y" - operator: infix_operator "is" - type: - named_type_expr - name: identifier "T" - unary_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got unary_expr - operand: - expr_equality_pattern <-- ERROR: The field unary_expr.operand should contain expr, but got expr_equality_pattern - expr: + operator: infix_operator "is" + type: + named_type_expr + name: identifier "T" + expr_equality_pattern + expr: + unary_expr + operand: name_expr identifier: identifier "y" - operator: prefix_operator "await" + operator: prefix_operator "await" body: block stmt: @@ -384,18 +387,14 @@ top_level element: string_literal "expr" switch_case pattern: - if_expr <-- ERROR: The field switch_case.pattern should contain pattern, but got if_expr - condition: - expr_equality_pattern <-- ERROR: The field if_expr.condition should contain expr, but got expr_equality_pattern - expr: boolean_literal "true" - then: - expr_equality_pattern <-- ERROR: The field if_expr.then should contain expr, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + if_expr + condition: boolean_literal "true" + then: name_expr identifier: identifier "y" - else: - expr_equality_pattern <-- ERROR: The field if_expr.else should contain expr, but got expr_equality_pattern - expr: + else: name_expr identifier: identifier "z" body: From 8323982db42d4052d5b49c7d8243594d1c6d32de Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:09:35 +0200 Subject: [PATCH 29/37] unified: Also test for string with interpolation This worked, but it's nice to have a test for it --- .../switch-expression-pattern.output | 37 ++++++++++++++++++- .../switch-expression-pattern.swift | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index 7b49c7009241..6de1c0c88d54 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -1,6 +1,6 @@ // Arbitrary expressions may appear in pattern context switch x { -case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y: +case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y, "interpolate \(y)": print("expr") case true ? y : z: print("ternary is also valid") @@ -189,6 +189,7 @@ sourceFile name: identifier "T" isKeyword: is switchCaseItem + trailingComma: , pattern: expressionPattern expression: @@ -197,6 +198,27 @@ sourceFile declReferenceExpr baseName: identifier "y" awaitKeyword: await + switchCaseItem + pattern: + expressionPattern + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "interpolate " + expressionSegment + leftParen: ( + rightParen: ) + backslash: \ + expressions: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "y" + stringSegment + content: stringSegment statements: codeBlockItem item: @@ -373,6 +395,19 @@ top_level name_expr identifier: identifier "y" operator: prefix_operator "await" + expr_equality_pattern + expr: + string_interpolation_expr + element: + string_literal "interpolate " + call_expr + callee: builtin_expr "interpolation" + argument: + argument + value: + name_expr + identifier: identifier "y" + string_literal body: block stmt: diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift index 369d84ca0da5..5df13210f7f6 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift @@ -1,6 +1,6 @@ // Arbitrary expressions may appear in pattern context switch x { -case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y: +case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y, "interpolate \(y)": print("expr") case true ? y : z: print("ternary is also valid") From 54dd65e343d571c15c9225fb8f2f327f74d3ad36 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:22:14 +0200 Subject: [PATCH 30/37] unified: Move prefixOperatorExpr to a more meaningful place --- unified/extractor/src/languages/swift/swift.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 8cd37c86ed38..96dcca906af4 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -248,7 +248,6 @@ fn translation_rules() -> Vec> { wrap_pattern_expr(&mut ctx, value) }), // ---- Names ---- - coerce_to_pattern!(prefixOperatorExpr), // A function reference spelled with argument labels (`f(x:y:z:)`) is a // `declReferenceExpr` carrying `argumentNames`. Mark it unsupported for // now (rather than let the bare-name rule below treat it as a plain @@ -347,6 +346,7 @@ fn translation_rules() -> Vec> { // infix operators, rather than guessing a structure. rule!((sequenceExpr elements: _* @els) => (unresolved_operator_sequence element: {els})), // Prefix unary operators (`!a`, `-x`). + coerce_to_pattern!(prefixOperatorExpr), rule!((prefixOperatorExpr operator: @op expression: @operand) => (unary_expr operator: (prefix_operator #{op}) operand: {operand})), // A tuple with a single unlabeled element is a grouping expression. rule!( From 6215cbb7ba74848872931217f764a7a9f279f3bf Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:25:06 +0200 Subject: [PATCH 31/37] unified: Add subscriptExpr test --- .../switch-expression-pattern.output | 30 ++++++++++++++++++- .../switch-expression-pattern.swift | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index 6de1c0c88d54..51091898b18d 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -1,6 +1,6 @@ // Arbitrary expressions may appear in pattern context switch x { -case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y, "interpolate \(y)": +case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y, "interpolate \(y)", y[4]: print("expr") case true ? y : z: print("ternary is also valid") @@ -199,6 +199,7 @@ sourceFile baseName: identifier "y" awaitKeyword: await switchCaseItem + trailingComma: , pattern: expressionPattern expression: @@ -219,6 +220,22 @@ sourceFile baseName: identifier "y" stringSegment content: stringSegment + switchCaseItem + pattern: + expressionPattern + expression: + subscriptCallExpr + leftSquare: [ + rightSquare: ] + arguments: + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "4" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "y" statements: codeBlockItem item: @@ -408,6 +425,17 @@ top_level name_expr identifier: identifier "y" string_literal + call_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got call_expr + callee: + expr_equality_pattern <-- ERROR: The field call_expr.callee should contain expr_or_type, but got expr_equality_pattern + expr: + name_expr + identifier: identifier "y" + argument: + pattern_element <-- ERROR: The field call_expr.argument should contain argument, but got pattern_element + pattern: + expr_equality_pattern + expr: int_literal "4" body: block stmt: diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift index 5df13210f7f6..55d58af58131 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.swift @@ -1,6 +1,6 @@ // Arbitrary expressions may appear in pattern context switch x { -case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y, "interpolate \(y)": +case y, y + 1, -y, y...z, "foo", 4, foo().bar, .baz, [42], ["a": 1], try y, y!, y is T, await y, "interpolate \(y)", y[4]: print("expr") case true ? y : z: print("ternary is also valid") From a22033fc2a712e0138e8b18f1fe49160dd09649e Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:43:56 +0200 Subject: [PATCH 32/37] unified: Fix subscriptExpr error --- unified/extractor/src/languages/swift/swift.rs | 1 + .../switch-expression-pattern.output | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 96dcca906af4..904c950e2356 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -1005,6 +1005,7 @@ fn translation_rules() -> Vec> { // report a distinct `subscriptCallExpr`, so giving // subscripts their own shape needs only a `subscript_expr` node in // ast_types.yml and a remap here. + coerce_to_pattern!(subscriptCallExpr), rule!( (subscriptCallExpr calledExpression: @callee arguments: _* @args) => diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index 51091898b18d..e57f769bd4d8 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -425,17 +425,15 @@ top_level name_expr identifier: identifier "y" string_literal - call_expr <-- ERROR: The field or_pattern.pattern should contain pattern, but got call_expr - callee: - expr_equality_pattern <-- ERROR: The field call_expr.callee should contain expr_or_type, but got expr_equality_pattern - expr: + expr_equality_pattern + expr: + call_expr + callee: name_expr identifier: identifier "y" - argument: - pattern_element <-- ERROR: The field call_expr.argument should contain argument, but got pattern_element - pattern: - expr_equality_pattern - expr: int_literal "4" + argument: + argument + value: int_literal "4" body: block stmt: From c1ef3f14bac09ec74f150d9d2ceb0730a72cb219 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 10:45:00 +0200 Subject: [PATCH 33/37] unified: Drive-by fix: Explicitly unsupport subscriptCallExpr Subscript support is deliberately post-poned to a point where we can design an end-to-end solution for it. --- unified/extractor/src/languages/swift/swift.rs | 2 +- .../swift/collections/dictionary-subscript.output | 11 +---------- .../corpus/swift/collections/subscript-access.output | 9 +-------- .../control-flow/switch-expression-pattern.output | 9 +-------- 4 files changed, 4 insertions(+), 27 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 904c950e2356..8d3d8b085ce5 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -1009,7 +1009,7 @@ fn translation_rules() -> Vec> { rule!( (subscriptCallExpr calledExpression: @callee arguments: _* @args) => - (call_expr callee: {callee} argument: {args}) + (unsupported_node) ), // ---- Optionals and errors ---- // Postfix `?` in a pattern means `Optional.some`. E.g: `(x,y)?` -> `Optional.some((x,y))` diff --git a/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output b/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output index 5b594ceb964d..90cdc25313d0 100644 --- a/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output +++ b/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output @@ -50,13 +50,4 @@ top_level pattern: name_pattern identifier: identifier "v" - value: - call_expr - callee: - name_expr - identifier: identifier "d" - argument: - argument - value: - string_interpolation_expr - element: string_literal "key" + value: unsupported_node "d[\"key\"]" diff --git a/unified/extractor/tests/corpus/swift/collections/subscript-access.output b/unified/extractor/tests/corpus/swift/collections/subscript-access.output index ec24f0c1f1df..465148e4f69d 100644 --- a/unified/extractor/tests/corpus/swift/collections/subscript-access.output +++ b/unified/extractor/tests/corpus/swift/collections/subscript-access.output @@ -47,11 +47,4 @@ top_level pattern: name_pattern identifier: identifier "first" - value: - call_expr - callee: - name_expr - identifier: identifier "xs" - argument: - argument - value: int_literal "0" + value: unsupported_node "xs[0]" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index e57f769bd4d8..ebfc69eb1727 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -426,14 +426,7 @@ top_level identifier: identifier "y" string_literal expr_equality_pattern - expr: - call_expr - callee: - name_expr - identifier: identifier "y" - argument: - argument - value: int_literal "4" + expr: unsupported_node "y[4]" body: block stmt: From 3b56e522fb7e8bd26f589dfa66587f166f0b03a3 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 28 Aug 2026 21:03:46 +0200 Subject: [PATCH 34/37] unified: Fix issue with '_' as an assignment target --- .../extractor/src/languages/swift/swift.rs | 7 +- ...solved-operator-sequence-with-casts.output | 4 +- .../unresolved-operator-sequence.output | 2 +- .../swift/variables/tuple-assignment.output | 126 ++++++++++++++++++ .../swift/variables/tuple-assignment.swift | 2 + 5 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 unified/extractor/tests/corpus/swift/variables/tuple-assignment.output create mode 100644 unified/extractor/tests/corpus/swift/variables/tuple-assignment.swift diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 8d3d8b085ce5..743df02d6790 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -267,8 +267,11 @@ fn translation_rules() -> Vec> { // `reduce(0, +)`), are both `declReferenceExpr`; its `baseName` is the // referenced identifier / operator symbol. rule!((declReferenceExpr baseName: @name) => (name_expr identifier: (identifier #{name}))), - // A discard `_` used as the target of an assignment becomes an `ignore_pattern`. - rule!((discardAssignmentExpr wildcard: @@w) => (ignore_pattern #{w})), + // A discard `_` used as the target of an assignment becomes a `builtin_expr` because the LHS of a + // tuple-assignment such as `(foo.x, _) = ...` can't contain a mix of patterns and exprs. So we consistently + // use exprs. + rule!((discardAssignmentExpr wildcard: @@w) where ctx.in_pattern => (ignore_pattern #{w})), + rule!((discardAssignmentExpr wildcard: @@w) => (builtin_expr #{w})), // A generic specialization in expression position (`C`, // `Array`) is represented by swift-syntax as a // `genericSpecializationExpr`. When used as a call target diff --git a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output index dffe2b0829e9..fadba6e43a87 100644 --- a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output +++ b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence-with-casts.output @@ -117,7 +117,7 @@ top_level stmt: unresolved_operator_sequence element: - ignore_pattern "_" + builtin_expr "_" infix_operator "=" name_expr identifier: identifier "a" @@ -128,7 +128,7 @@ top_level identifier: identifier "b" unresolved_operator_sequence element: - ignore_pattern "_" + builtin_expr "_" infix_operator "=" name_expr identifier: identifier "a" diff --git a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output index c46180a2dc0b..6fc46fc64167 100644 --- a/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output +++ b/unified/extractor/tests/corpus/swift/operators/unresolved-operator-sequence.output @@ -90,7 +90,7 @@ top_level stmt: unresolved_operator_sequence element: - ignore_pattern "_" + builtin_expr "_" infix_operator "=" name_expr identifier: identifier "a" diff --git a/unified/extractor/tests/corpus/swift/variables/tuple-assignment.output b/unified/extractor/tests/corpus/swift/variables/tuple-assignment.output new file mode 100644 index 000000000000..f46d7aa508af --- /dev/null +++ b/unified/extractor/tests/corpus/swift/variables/tuple-assignment.output @@ -0,0 +1,126 @@ +(x, _) = (1, 2) +(foo.x, _) = (1, 2) + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + infixOperatorExpr + operator: + assignmentExpr + equal: = + leftOperand: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "x" + trailingComma: , + labeledExpr + expression: + discardAssignmentExpr + wildcard: _ + rightOperand: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "1" + trailingComma: , + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "2" + codeBlockItem + item: + infixOperatorExpr + operator: + assignmentExpr + equal: = + leftOperand: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "x" + base: + declReferenceExpr + baseName: identifier "foo" + trailingComma: , + labeledExpr + expression: + discardAssignmentExpr + wildcard: _ + rightOperand: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "1" + trailingComma: , + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "2" + +--- + +top_level + body: + block + stmt: + assign_expr + target: + tuple_expr + element: + argument + value: + name_expr + identifier: identifier "x" + argument + value: builtin_expr "_" + value: + tuple_expr + element: + argument + value: int_literal "1" + argument + value: int_literal "2" + assign_expr + target: + tuple_expr + element: + argument + value: + member_access_expr + base: + name_expr + identifier: identifier "foo" + member: identifier "x" + argument + value: builtin_expr "_" + value: + tuple_expr + element: + argument + value: int_literal "1" + argument + value: int_literal "2" diff --git a/unified/extractor/tests/corpus/swift/variables/tuple-assignment.swift b/unified/extractor/tests/corpus/swift/variables/tuple-assignment.swift new file mode 100644 index 000000000000..dcb9e8c8c205 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/variables/tuple-assignment.swift @@ -0,0 +1,2 @@ +(x, _) = (1, 2) +(foo.x, _) = (1, 2) From 6490cce3d17e10a4fb2f84d1bf4ce31ca7e880b0 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 1 Sep 2026 08:36:07 +0200 Subject: [PATCH 35/37] unified: Use the coercion macro for literals --- .../extractor/src/languages/swift/swift.rs | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 743df02d6790..640ec63f49dc 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -206,27 +206,17 @@ fn translation_rules() -> Vec> { // swift-syntax does not distinguish the lexical integer/string forms // (hex/binary/octal, single- vs multi-line, raw): each is a single // `*LiteralExpr` kind, so one rule per literal type suffices. - rule!((integerLiteralExpr) @@node => expr { - let value = tree!((int_literal #{node})); - wrap_pattern_expr(&mut ctx, value) - }), - rule!((floatLiteralExpr) @@node => expr { - let value = tree!((float_literal #{node})); - wrap_pattern_expr(&mut ctx, value) - }), - rule!((booleanLiteralExpr) @@node => expr { - let value = tree!((boolean_literal #{node})); - wrap_pattern_expr(&mut ctx, value) - }), - rule!((nilLiteralExpr) @@node => expr { - let value = tree!((builtin_expr #{node})); - wrap_pattern_expr(&mut ctx, value) - }), + coerce_to_pattern!(integerLiteralExpr), + rule!((integerLiteralExpr) @@node => (int_literal #{node})), + coerce_to_pattern!(floatLiteralExpr), + rule!((floatLiteralExpr) @@node => (float_literal #{node})), + coerce_to_pattern!(booleanLiteralExpr), + rule!((booleanLiteralExpr) @@node => (boolean_literal #{node})), + coerce_to_pattern!(nilLiteralExpr), + rule!((nilLiteralExpr) @@node => (builtin_expr #{node})), // Plain string literals (no interpolation) - rule!((simpleStringLiteralExpr) @@node => expr { - let value = tree!((string_literal #{node})); - wrap_pattern_expr(&mut ctx, value) - }), + coerce_to_pattern!(simpleStringLiteralExpr), + rule!((simpleStringLiteralExpr) @@node => (string_literal #{node})), // String literals with interpolation coerce_to_pattern!(stringLiteralExpr), rule!( @@ -243,10 +233,8 @@ fn translation_rules() -> Vec> { => (call_expr callee: (builtin_expr "interpolation") argument: {exprs}) ), - rule!((regexLiteralExpr) @@node => expr { - let value = tree!((regex_literal #{node})); - wrap_pattern_expr(&mut ctx, value) - }), + coerce_to_pattern!(regexLiteralExpr), + rule!((regexLiteralExpr) @@node => (regex_literal #{node})), // ---- Names ---- // A function reference spelled with argument labels (`f(x:y:z:)`) is a // `declReferenceExpr` carrying `argumentNames`. Mark it unsupported for From 1a899629e91837ee24dc74eeb5c1b10fa3721167 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 1 Sep 2026 14:07:02 +0200 Subject: [PATCH 36/37] unified: Do not emit plain strings as interpolations --- unified/extractor/src/languages/swift/swift.rs | 8 +++++++- .../swift/collections/tuple-literal.output | 4 +--- ...ding-modifier-does-not-leak-to-sibling.output | 4 +--- .../swift/control-flow/defer-statement.output | 4 +--- .../switch-case-item-where-clauses.output | 12 +++--------- .../switch-expression-pattern.output | 12 +++--------- .../swift/control-flow/switch-statement.output | 12 +++--------- .../control-flow/switch-tuple-pattern.output | 4 +--- .../switch-with-binding-pattern.output | 16 ++++------------ ...ch-with-labeled-case-pattern-arguments.output | 4 +--- .../function-call-with-labelled-arguments.output | 4 +--- .../function-with-default-parameter-value.output | 4 +--- .../functions/function-with-no-parameters.output | 4 +--- .../corpus/swift/literals/string-literal.output | 4 +--- .../catch-where-clauses.output | 8 ++------ .../throwing-function.output | 4 +--- 16 files changed, 32 insertions(+), 76 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 640ec63f49dc..38fb493b558b 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -217,8 +217,14 @@ fn translation_rules() -> Vec> { // Plain string literals (no interpolation) coerce_to_pattern!(simpleStringLiteralExpr), rule!((simpleStringLiteralExpr) @@node => (string_literal #{node})), - // String literals with interpolation + // String literals, possibly with interpolation. Flatten to string_literal if no interpolation. coerce_to_pattern!(stringLiteralExpr), + rule!( + (stringLiteralExpr segments: (stringSegment) segments: _* @@rest) @@node + where rest.is_empty() + => + (string_literal #{node}) // Note: capture entire 'stringLiteralExpr' to preserve quotation marks + ), rule!( (stringLiteralExpr segments: _* @segs) => diff --git a/unified/extractor/tests/corpus/swift/collections/tuple-literal.output b/unified/extractor/tests/corpus/swift/collections/tuple-literal.output index bfeaaf2c3e0e..a8be2d3fb5f8 100644 --- a/unified/extractor/tests/corpus/swift/collections/tuple-literal.output +++ b/unified/extractor/tests/corpus/swift/collections/tuple-literal.output @@ -60,8 +60,6 @@ top_level argument value: int_literal "1" argument - value: - string_interpolation_expr - element: string_literal "two" + value: string_literal "\"two\"" argument value: float_literal "3.0" diff --git a/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output b/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output index df1255ac17fd..800647eb7474 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output +++ b/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output @@ -114,9 +114,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "matched" + value: string_literal "\"matched\"" switch_case body: block diff --git a/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output b/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output index ce5b406f7637..6e8ec9ae3b9a 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output +++ b/unified/extractor/tests/corpus/swift/control-flow/defer-statement.output @@ -91,6 +91,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "work" + value: string_literal "\"work\"" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output b/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output index cc6710b16360..9d2041aa2ad1 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output @@ -181,9 +181,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "positive" + value: string_literal "\"positive\"" switch_case pattern: or_pattern @@ -210,9 +208,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "non-positive" + value: string_literal "\"non-positive\"" switch_case body: block @@ -223,6 +219,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "other" + value: string_literal "\"other\"" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index ebfc69eb1727..62d80afe02bf 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -356,9 +356,7 @@ top_level name_expr identifier: identifier "z" expr_equality_pattern - expr: - string_interpolation_expr - element: string_literal "foo" + expr: string_literal "\"foo\"" expr_equality_pattern expr: int_literal "4" expr_equality_pattern @@ -436,9 +434,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "expr" + value: string_literal "\"expr\"" switch_case pattern: expr_equality_pattern @@ -460,9 +456,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "ternary is also valid" + value: string_literal "\"ternary is also valid\"" switch_case body: block diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output b/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output index 703f2200a404..a88579852947 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output @@ -142,9 +142,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "one" + value: string_literal "\"one\"" switch_case pattern: or_pattern @@ -162,9 +160,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "two or three" + value: string_literal "\"two or three\"" switch_case body: block @@ -175,6 +171,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "other" + value: string_literal "\"other\"" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output index 8d6aca7532ff..60910d2fefee 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-tuple-pattern.output @@ -178,6 +178,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "origin" + value: string_literal "\"origin\"" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output index 309e1dd0ce43..a72ddf32af4f 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-binding-pattern.output @@ -401,9 +401,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "foo" + value: string_literal "\"foo\"" switch_case pattern: expr_equality_pattern @@ -422,9 +420,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "foo" + value: string_literal "\"foo\"" switch_case pattern: expr_equality_pattern @@ -445,9 +441,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "foo" + value: string_literal "\"foo\"" switch_case pattern: constructor_pattern @@ -492,6 +486,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "is T" + value: string_literal "\"is T\"" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output b/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output index 56e690a452aa..20b17d160e44 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output @@ -154,9 +154,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "yes" + value: string_literal "\"yes\"" switch_case pattern: constructor_pattern diff --git a/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output b/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output index 351caa592970..f885d5762f2d 100644 --- a/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output +++ b/unified/extractor/tests/corpus/swift/functions/function-call-with-labelled-arguments.output @@ -39,6 +39,4 @@ top_level argument: argument name: identifier "person" - value: - string_interpolation_expr - element: string_literal "Bob" + value: string_literal "\"Bob\"" diff --git a/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output b/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output index 18f9d5f04201..3495a13106fb 100644 --- a/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output +++ b/unified/extractor/tests/corpus/swift/functions/function-with-default-parameter-value.output @@ -75,9 +75,7 @@ top_level pattern: name_pattern identifier: identifier "name" - default: - string_interpolation_expr - element: string_literal "world" + default: string_literal "\"world\"" body: block stmt: diff --git a/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output b/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output index 16caecd4f6e8..4ddc26ae94d6 100644 --- a/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output +++ b/unified/extractor/tests/corpus/swift/functions/function-with-no-parameters.output @@ -62,6 +62,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "hello" + value: string_literal "\"hello\"" diff --git a/unified/extractor/tests/corpus/swift/literals/string-literal.output b/unified/extractor/tests/corpus/swift/literals/string-literal.output index 7d3c269c4cc4..8d3ea8e796c0 100644 --- a/unified/extractor/tests/corpus/swift/literals/string-literal.output +++ b/unified/extractor/tests/corpus/swift/literals/string-literal.output @@ -19,6 +19,4 @@ sourceFile top_level body: block - stmt: - string_interpolation_expr - element: string_literal "hello" + stmt: string_literal "\"hello\"" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output index 800f85511d30..79b7879d35fa 100644 --- a/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/catch-where-clauses.output @@ -193,9 +193,7 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "retry" + value: string_literal "\"retry\"" catch_clause body: block @@ -206,6 +204,4 @@ top_level identifier: identifier "print" argument: argument - value: - string_interpolation_expr - element: string_literal "fallback" + value: string_literal "\"fallback\"" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output b/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output index 4980a7b4fba3..e68b4c3c57ac 100644 --- a/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/throwing-function.output @@ -64,6 +64,4 @@ top_level block stmt: return_expr - value: - string_interpolation_expr - element: string_literal + value: string_literal "\"\"" From dfc9d60c4d9fa0e84d3cdeccbf8f2442eefc51c4 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 1 Sep 2026 14:24:04 +0200 Subject: [PATCH 37/37] unified: Update BasicTest output This is a result of mapping subscriptCallExpr -> unsupportedNode --- .../ql/test/library-tests/BasicTest/test.expected | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/unified/ql/test/library-tests/BasicTest/test.expected b/unified/ql/test/library-tests/BasicTest/test.expected index 301cd90ca2a5..fe7d4f50fb39 100644 --- a/unified/ql/test/library-tests/BasicTest/test.expected +++ b/unified/ql/test/library-tests/BasicTest/test.expected @@ -18,22 +18,20 @@ nameExpr | test.swift:48:15:48:19 | NameExpr | index | | test.swift:48:29:48:33 | NameExpr | index | | test.swift:48:37:48:40 | NameExpr | data | -| test.swift:49:16:49:19 | NameExpr | data | -| test.swift:49:21:49:25 | NameExpr | index | | test.swift:53:9:53:12 | NameExpr | data | | test.swift:53:21:53:24 | NameExpr | item | | test.swift:63:16:63:19 | NameExpr | self | | test.swift:65:29:65:37 | NameExpr | transform | | test.swift:65:39:65:43 | NameExpr | value | | test.swift:67:29:67:33 | NameExpr | error | -| test.swift:76:16:76:19 | NameExpr | self | -| test.swift:76:21:76:21 | NameExpr | i | -| test.swift:76:26:76:29 | NameExpr | self | -| test.swift:76:31:76:31 | NameExpr | i | +| test.swift:75:23:75:27 | NameExpr | count | | test.swift:86:12:86:17 | NameExpr | values | | test.swift:87:12:87:17 | NameExpr | values | -| test.swift:87:38:87:43 | NameExpr | values | | test.swift:87:49:87:57 | NameExpr | transform | unsupported +| test.swift:49:16:49:26 | | | +| test.swift:76:16:76:22 | | | +| test.swift:76:26:76:36 | | | +| test.swift:87:38:87:46 | | | stringValue | strings.swift:1:9:1:15 | "hello" | "hello" |