diff --git a/.gitignore b/.gitignore index 3d5006540..a6fd283c9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ coverage/ dist/ dist-ssr/ out/ +DSL/syntaxes/safe-ds.tmLanguage.json # Node .npm/ diff --git a/DSL/src/language-server/formatting/safe-ds-formatter.ts b/DSL/src/language-server/formatting/safe-ds-formatter.ts index e65c26d68..bc5ada637 100644 --- a/DSL/src/language-server/formatting/safe-ds-formatter.ts +++ b/DSL/src/language-server/formatting/safe-ds-formatter.ts @@ -1,7 +1,7 @@ import { AbstractFormatter, AstNode, CstNode, findCommentNode, Formatting, isAstNode } from 'langium'; import * as ast from '../generated/ast'; import { SdsImport, SdsImportAlias, SdsModule } from '../generated/ast'; -import { annotationCallsOrEmpty, typeArgumentsOrEmpty } from '../helpers/astShortcuts'; +import { annotationCallsOrEmpty, literalsOrEmpty, typeArgumentsOrEmpty } from '../helpers/astShortcuts'; import { FormattingAction, FormattingActionOptions } from 'langium/src/lsp/formatter'; import noSpace = Formatting.noSpace; import newLine = Formatting.newLine; @@ -149,6 +149,10 @@ export class SafeDSFormatter extends AbstractFormatter { this.formatSdsMemberType(node); } else if (ast.isSdsCallableType(node)) { this.formatSdsCallableType(node); + } else if (ast.isSdsLiteralType(node)) { + this.formatSdsLiteralType(node); + } else if (ast.isSdsLiteralList(node)) { + this.formatSdsLiteralList(node); } else if (ast.isSdsNamedType(node)) { this.formatSdsNamedType(node); } else if (ast.isSdsUnionType(node)) { @@ -761,6 +765,25 @@ export class SafeDSFormatter extends AbstractFormatter { formatter.keyword('->').surround(oneSpace()); } + private formatSdsLiteralType(node: ast.SdsLiteralType): void { + const formatter = this.getNodeFormatter(node); + + formatter.keyword('literal').append(noSpace()); + } + + private formatSdsLiteralList(node: ast.SdsLiteralList): void { + const formatter = this.getNodeFormatter(node); + const literals = node.literals ?? []; + + if (literals.length > 0) { + formatter.node(literals[0]).prepend(noSpace()); + formatter.nodes(...literals.slice(1)).prepend(oneSpace()); + } + + formatter.keywords(',').prepend(noSpace()); + formatter.keyword('>').prepend(noSpace()); + } + private formatSdsNamedType(node: ast.SdsNamedType) { const formatter = this.getNodeFormatter(node); @@ -847,10 +870,12 @@ export class SafeDSFormatter extends AbstractFormatter { if (ast.isSdsCallableType(node) || ast.isSdsMemberType(node)) { return true; - } else if (ast.isSdsUnionType(node)) { - return typeArgumentsOrEmpty(node.typeArgumentList).length > 0; + } else if (ast.isSdsLiteralType(node)) { + return literalsOrEmpty(node).length > 0; } else if (ast.isSdsNamedType(node)) { return typeArgumentsOrEmpty(node.typeArgumentList).length > 0; + } else if (ast.isSdsUnionType(node)) { + return typeArgumentsOrEmpty(node.typeArgumentList).length > 0; } else { return false; } diff --git a/DSL/src/language-server/grammar/safe-ds.langium b/DSL/src/language-server/grammar/safe-ds.langium index 61dbfe921..25fd0721a 100644 --- a/DSL/src/language-server/grammar/safe-ds.langium +++ b/DSL/src/language-server/grammar/safe-ds.langium @@ -841,6 +841,7 @@ SdsType returns SdsType: SdsPrimaryType returns SdsType: SdsCallableType + | SdsLiteralType | SdsNamedType | SdsUnionType ; @@ -854,6 +855,29 @@ SdsCallableType returns SdsCallableType: resultList=SdsResultList ; +interface SdsLiteralType extends SdsType { + literalList: SdsLiteralList +} + +SdsLiteralType returns SdsLiteralType: + 'literal' literalList=SdsLiteralList +; + +interface SdsLiteralList extends SdsObject { + literals: SdsLiteral[] +} + +SdsLiteralList returns SdsLiteralList: + {SdsLiteralList} + (LESS_THAN | CALL_TYPE_ARGUMENT_LIST_START) + ( + literals+=SdsLiteral + (',' literals+=SdsLiteral)* + ','? + )? + '>' +; + interface SdsNamedType extends SdsType { declaration: @SdsNamedTypeDeclaration typeArgumentList?: SdsTypeArgumentList @@ -1038,6 +1062,7 @@ terminal CALL_TYPE_ARGUMENT_LIST_START: ( '*' // Star projection as positional type argument | 'in' // Contravariant type projection as positional type argument | 'out' // Covariant type projection as positional type argument + | 'literal' // Invariant literal type as positional type argument | 'union' // Invariant union type as positional type argument | '>' // Empty type argument list | ID /\s*/ diff --git a/DSL/src/language-server/helpers/astShortcuts.ts b/DSL/src/language-server/helpers/astShortcuts.ts index 6301f9a46..3bede7ce5 100644 --- a/DSL/src/language-server/helpers/astShortcuts.ts +++ b/DSL/src/language-server/helpers/astShortcuts.ts @@ -3,8 +3,8 @@ import { isSdsDeclaration, SdsAnnotatedObject, SdsAnnotationCall, - SdsClass, - SdsObject, + SdsLiteral, + SdsLiteralType, SdsTypeArgument, SdsTypeArgumentList, } from '../generated/ast'; @@ -17,8 +17,8 @@ export const annotationCallsOrEmpty = function (node: SdsAnnotatedObject): SdsAn } }; -export const classMembersOrEmpty = function (node: SdsClass): SdsObject[] { - return node.body?.members ?? []; +export const literalsOrEmpty = function (node: SdsLiteralType | undefined): SdsLiteral[] { + return node?.literalList?.literals ?? []; }; export const typeArgumentsOrEmpty = function (node: SdsTypeArgumentList | undefined): SdsTypeArgument[] { diff --git a/DSL/syntaxes/safe-ds.tmLanguage.json b/DSL/syntaxes/safe-ds.tmLanguage.json deleted file mode 100644 index 44789e2b4..000000000 --- a/DSL/syntaxes/safe-ds.tmLanguage.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "name": "safe-ds", - "scopeName": "source.safe-ds", - "fileTypes": [ - ".sdspipe", - ".sdsstub", - ".sdstest" - ], - "patterns": [ - { - "include": "#comments" - }, - { - "name": "keyword.control.safe-ds", - "match": "\\b(and|annotation|as|attr|class|enum|false|fun|import|in|internal|not|null|or|out|package|pipeline|private|schema|segment|static|sub|super|true|union|val|vararg|where|yield)\\b" - }, - { - "name": "string.quoted.double.safe-ds", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "include": "#string-character-escape" - } - ] - } - ], - "repository": { - "comments": { - "patterns": [ - { - "name": "comment.block.safe-ds", - "begin": "/\\*", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.safe-ds" - } - }, - "end": "\\*/", - "endCaptures": { - "0": { - "name": "punctuation.definition.comment.safe-ds" - } - } - }, - { - "begin": "//", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.safe-ds" - } - }, - "end": "(?=$)", - "name": "comment.line.safe-ds" - }, - { - "name": "comment.block.safe-ds", - "begin": "[»«]", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.safe-ds" - } - }, - "end": "[»«]", - "endCaptures": { - "0": { - "name": "punctuation.definition.comment.safe-ds" - } - } - } - ] - }, - "string-character-escape": { - "name": "constant.character.escape.safe-ds", - "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" - } - } -} diff --git a/DSL/tests/resources/formatting/trailing commas/literal list of literal type.sdstest b/DSL/tests/resources/formatting/trailing commas/literal list of literal type.sdstest new file mode 100644 index 000000000..ccdfc1aae --- /dev/null +++ b/DSL/tests/resources/formatting/trailing commas/literal list of literal type.sdstest @@ -0,0 +1,9 @@ +segment s( + f: literal<1.0 , null , > +) {} + +// ----------------------------------------------------------------------------- + +segment s( + f: literal<1.0, null,> +) {} diff --git a/DSL/tests/resources/formatting/types/literal types/empty.sdstest b/DSL/tests/resources/formatting/types/literal types/empty.sdstest new file mode 100644 index 000000000..0451a59e3 --- /dev/null +++ b/DSL/tests/resources/formatting/types/literal types/empty.sdstest @@ -0,0 +1,5 @@ +segment mySegment(x: literal < >) {} + +// ----------------------------------------------------------------------------- + +segment mySegment(x: literal<>) {} diff --git a/DSL/tests/resources/formatting/types/literal types/with literals.sdstest b/DSL/tests/resources/formatting/types/literal types/with literals.sdstest new file mode 100644 index 000000000..ea0e92057 --- /dev/null +++ b/DSL/tests/resources/formatting/types/literal types/with literals.sdstest @@ -0,0 +1,9 @@ +segment mySegment( + x: literal < 1.0 , null > +) {} + +// ----------------------------------------------------------------------------- + +segment mySegment( + x: literal<1.0, null> +) {} diff --git a/DSL/tests/resources/formatting/types/member types/receiver (literal) and member (not nullable).sdstest b/DSL/tests/resources/formatting/types/member types/receiver (literal) and member (not nullable).sdstest new file mode 100644 index 000000000..d484c6e06 --- /dev/null +++ b/DSL/tests/resources/formatting/types/member types/receiver (literal) and member (not nullable).sdstest @@ -0,0 +1,9 @@ +segment mySegment( + x: literal < > . InnerClass +) {} + +// ----------------------------------------------------------------------------- + +segment mySegment( + x: literal<>.InnerClass +) {} diff --git a/DSL/tests/resources/formatting/types/member types/receiver (literal) and member (nullable).sdstest b/DSL/tests/resources/formatting/types/member types/receiver (literal) and member (nullable).sdstest new file mode 100644 index 000000000..dcbe565db --- /dev/null +++ b/DSL/tests/resources/formatting/types/member types/receiver (literal) and member (nullable).sdstest @@ -0,0 +1,9 @@ +segment mySegment( + x: literal < > . InnerClass ? +) {} + +// ----------------------------------------------------------------------------- + +segment mySegment( + x: literal<>.InnerClass? +) {} diff --git a/DSL/tests/resources/grammar/keywords as names/bad-unescaped literal.sdstest b/DSL/tests/resources/grammar/keywords as names/bad-unescaped literal.sdstest new file mode 100644 index 000000000..8578433b6 --- /dev/null +++ b/DSL/tests/resources/grammar/keywords as names/bad-unescaped literal.sdstest @@ -0,0 +1,3 @@ +// $TEST$ syntax_error + +class literal diff --git a/DSL/tests/resources/grammar/trailing commas/good-literal list of literal type.sdstest b/DSL/tests/resources/grammar/trailing commas/good-literal list of literal type.sdstest new file mode 100644 index 000000000..0253c0984 --- /dev/null +++ b/DSL/tests/resources/grammar/trailing commas/good-literal list of literal type.sdstest @@ -0,0 +1,5 @@ +// $TEST$ no_syntax_error + +segment s( + f: literal +) {} diff --git a/DSL/tests/resources/grammar/types/literal types/bad-nested.sdstest b/DSL/tests/resources/grammar/types/literal types/bad-nested.sdstest new file mode 100644 index 000000000..ce0e69ae0 --- /dev/null +++ b/DSL/tests/resources/grammar/types/literal types/bad-nested.sdstest @@ -0,0 +1,5 @@ +// $TEST$ syntax_error + +segment mySegment( + x: literal> +) {} diff --git a/DSL/tests/resources/grammar/types/literal types/bad-unclosed angle bracket.sdstest b/DSL/tests/resources/grammar/types/literal types/bad-unclosed angle bracket.sdstest new file mode 100644 index 000000000..abf93699d --- /dev/null +++ b/DSL/tests/resources/grammar/types/literal types/bad-unclosed angle bracket.sdstest @@ -0,0 +1,5 @@ +// $TEST$ syntax_error + +segment mySegment( + x: literal< +) {} diff --git a/DSL/tests/resources/grammar/types/literal types/good-empty.sdstest b/DSL/tests/resources/grammar/types/literal types/good-empty.sdstest new file mode 100644 index 000000000..66a936d1b --- /dev/null +++ b/DSL/tests/resources/grammar/types/literal types/good-empty.sdstest @@ -0,0 +1,5 @@ +// $TEST$ no_syntax_error + +segment mySegment( + x: literal<> +) {} diff --git a/DSL/tests/resources/grammar/types/literal types/good-with literals.sdstest b/DSL/tests/resources/grammar/types/literal types/good-with literals.sdstest new file mode 100644 index 000000000..3fbda1b55 --- /dev/null +++ b/DSL/tests/resources/grammar/types/literal types/good-with literals.sdstest @@ -0,0 +1,5 @@ +// $TEST$ no_syntax_error + +segment mySegment( + x: literal +) {} diff --git a/DSL/tests/resources/grammar/types/member types/bad-literal type as member.sdstest b/DSL/tests/resources/grammar/types/member types/bad-literal type as member.sdstest new file mode 100644 index 000000000..701706f10 --- /dev/null +++ b/DSL/tests/resources/grammar/types/member types/bad-literal type as member.sdstest @@ -0,0 +1,5 @@ +// $TEST$ syntax_error + +segment mySegment( + x: OuterClass.literal<> +) {} diff --git a/DSL/tests/resources/grammar/types/member types/good-receiver (literal) and member (not nullable).sdstest b/DSL/tests/resources/grammar/types/member types/good-receiver (literal) and member (not nullable).sdstest new file mode 100644 index 000000000..6be0c881d --- /dev/null +++ b/DSL/tests/resources/grammar/types/member types/good-receiver (literal) and member (not nullable).sdstest @@ -0,0 +1,5 @@ +// $TEST$ no_syntax_error + +segment mySegment( + x: literal<>.InnerClass +) {} diff --git a/DSL/tests/resources/grammar/types/member types/good-receiver (literal) and member (nullable).sdstest b/DSL/tests/resources/grammar/types/member types/good-receiver (literal) and member (nullable).sdstest new file mode 100644 index 000000000..2c8e93a96 --- /dev/null +++ b/DSL/tests/resources/grammar/types/member types/good-receiver (literal) and member (nullable).sdstest @@ -0,0 +1,5 @@ +// $TEST$ no_syntax_error + +segment mySegment( + x: literal<>.InnerClass? +) {} diff --git a/DSL/vitest.config.ts b/DSL/vitest.config.ts index 8a51f9224..201219586 100644 --- a/DSL/vitest.config.ts +++ b/DSL/vitest.config.ts @@ -10,5 +10,6 @@ export default defineConfig({ include: ['src'], exclude: ['**/generated'], }, + exclude: ['out'], }, });