Skip to content

Commit

Permalink
feat: unknown default value of stub parameters (#952)
Browse files Browse the repository at this point in the history
Closes #951

### Summary of Changes

Add a new literal `unknown` that can be used to mark parameters as
optional if the exact default value of a class/enum variant/function
parameter is unknown. This literal can be used nowhere else.
  • Loading branch information
lars-reimann authored Mar 29, 2024
1 parent 155b1c0 commit 78103e3
Show file tree
Hide file tree
Showing 19 changed files with 132 additions and 9 deletions.
1 change: 1 addition & 0 deletions docs/lexer/safe_ds_lexer/_safe_ds_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"false",
"null",
"true",
"unknown",
)

keywords_namespace = (
Expand Down
7 changes: 7 additions & 0 deletions packages/safe-ds-lang/src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ SdsLiteral returns SdsLiteral:
| SdsMap
| SdsNull
| SdsString
| SdsUnknown
;

interface SdsBoolean extends SdsLiteral {
Expand Down Expand Up @@ -824,6 +825,12 @@ SdsString returns SdsString:
value=STRING
;

interface SdsUnknown extends SdsLiteral {}

SdsUnknown returns SdsUnknown:
{SdsUnknown} 'unknown'
;

interface SdsParenthesizedExpression extends SdsExpression {
expression: SdsExpression
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
isSdsTemplateStringInner,
isSdsTemplateStringStart,
isSdsTypeCast,
isSdsUnknown,
type SdsArgument,
type SdsAssignee,
type SdsCall,
Expand Down Expand Up @@ -212,6 +213,8 @@ export class SafeDsPartialEvaluator {
return new StringConstant(node.value);
} else if (isSdsTemplateStringEnd(node)) {
return new StringConstant(node.value);
} else if (isSdsUnknown(node)) {
return UnknownEvaluatedNode;
} else if (isSdsBlockLambda(node)) {
return new BlockLambdaClosure(node, substitutions);
} else if (isSdsExpressionLambda(node)) {
Expand Down Expand Up @@ -678,7 +681,14 @@ export class SafeDsPartialEvaluator {
* Returns whether the given expression can be the value of a constant parameter.
*/
canBeValueOfConstantParameter = (node: SdsExpression): boolean => {
if (isSdsBoolean(node) || isSdsFloat(node) || isSdsInt(node) || isSdsNull(node) || isSdsString(node)) {
if (
isSdsBoolean(node) ||
isSdsFloat(node) ||
isSdsInt(node) ||
isSdsNull(node) ||
isSdsString(node) ||
isSdsUnknown(node)
) {
return true;
} else if (isSdsCall(node)) {
// If some arguments are not provided, we already show an error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
isSdsTypeCast,
isSdsTypeParameter,
isSdsUnionType,
isSdsUnknown,
isSdsYield,
SdsAbstractResult,
SdsAssignee,
Expand Down Expand Up @@ -345,6 +346,8 @@ export class SafeDsTypeComputer {
return this.coreTypes.Map(keyType, valueType);
} else if (isSdsTemplateString(node)) {
return this.coreTypes.String;
} else if (isSdsUnknown(node)) {
return this.coreTypes.Nothing;
}

// Recursive cases
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
isSdsCallable,
isSdsCallableType,
isSdsClass,
isSdsEnumVariant,
isSdsFunction,
isSdsParameter,
SdsUnknown,
} from '../../../generated/ast.js';
import { AstUtils, ValidationAcceptor } from 'langium';

export const CODE_LITERALS_UNKNOWN = 'literals/unknown';

export const unknownMustOnlyBeUsedAsDefaultValueOfStub = (node: SdsUnknown, accept: ValidationAcceptor): void => {
if (!unknownIsUsedCorrectly(node)) {
accept(
'error',
'unknown is only allowed as the default value of a parameter of a class, enum variant, or function.',
{
node,
code: CODE_LITERALS_UNKNOWN,
},
);
}
};

const unknownIsUsedCorrectly = (node: SdsUnknown): boolean => {
if (!isSdsParameter(node.$container) || node.$containerProperty !== 'defaultValue') {
return false;
}

const containingCallable = AstUtils.getContainerOfType(node.$container, isSdsCallable);
return (
isSdsCallableType(containingCallable) || // Callable types must not have default values in general
isSdsClass(containingCallable) ||
isSdsEnumVariant(containingCallable) ||
isSdsFunction(containingCallable)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ import {
parameterBoundRightOperandMustEvaluateToFloatConstantOrIntConstant,
parameterDefaultValueMustRespectParameterBounds,
} from './other/declarations/parameterBounds.js';
import { unknownMustOnlyBeUsedAsDefaultValueOfStub } from './other/expressions/literals.js';

/**
* Register custom validation checks.
Expand Down Expand Up @@ -379,6 +380,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
unionTypeShouldNotHaveDuplicateTypes(services),
unionTypeShouldNotHaveASingularTypeArgument(services),
],
SdsUnknown: [unknownMustOnlyBeUsedAsDefaultValueOfStub],
SdsYield: [yieldMustNotBeUsedInPipeline, yieldTypeMustMatchResultType(services)],
};
registry.register(checks);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ describe('SafeDsTypeChecker', async () => {
code: '"text"',
expected: true,
},
{
code: 'unknown',
expected: true,
},
{
code: 'unresolved()',
expected: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const code = `
LegalDirectBounds sub Number,
LegalIndirectBounds sub LegalDirectBounds,
UnnamedBounds sub literal<2>,
UnresolvedBounds sub unknown,
UnresolvedBounds sub Unresolved,
>
`;
const module = await getNodeOfType(services, code, isSdsModule);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pipeline myPipeline {
unknown;
}

// -----------------------------------------------------------------------------

pipeline myPipeline {
unknown;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ no_syntax_error

pipeline myPipeline {
unknown;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// $TEST$ syntax_error

class unknown
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class `segment`
class `sub`
class `true`
class `union`
class `unknown`
class `val`
class `where`
class `yield`
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tests.partialValidation.baseCases.unknownLiterals

pipeline test {
// $TEST$ serialization ?
»unknown«;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ pipeline myPipeline {

// $TEST$ serialization literal<"myString">
val stringLiteral = »"myString"«;

// $TEST$ serialization Nothing
val unknownLiteral = »unknown«;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ class Contravariant<in T>
segment mySegment(
one: Contravariant<literal<1>>,
nullable: Contravariant<literal<null>>,
unknown: Contravariant<Unresolved>,
unresolved: Contravariant<Unresolved>,
) {
// $TEST$ serialization List<Contravariant<Nothing>>
»[one, unknown]«;
»[one, unresolved]«;

// $TEST$ serialization List<Contravariant<Nothing>>
»[nullable, unknown]«;
»[nullable, unresolved]«;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ segment mySegment(
»[C]«;

// $TEST$ serialization List<$unknown>
»[unknown]«;
»[unresolved]«;
}

// $TEST$ serialization List<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package tests.typing.lowestCommonSupertype.unknownType

segment mySegment() {
// $TEST$ serialization List<Any?>
»[1, unknown]«;
»[1, unresolved]«;

// $TEST$ serialization List<Any?>
»[null, unknown]«;
»[null, unresolved]«;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tests.validation.other.expressions.literals.unknownMustOnlyBeUsedAsDefaultValueOfStub

// $TEST$ error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
annotation MyAnnotation(p: Int = »unknown«)

// $TEST$ no error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
class MyClass(p: Int = »unknown«)

enum MyEnum {
// $TEST$ no error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
MyVariant(p: Int = »unknown«)
}

// $TEST$ no error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
fun myFunction(p: Int = »unknown«)

segment mySegment(
// $TEST$ no error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
f: (p: Int = »unknown«) -> (),
// $TEST$ error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
p: Int = »unknown«,
) {
// $TEST$ error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
(p: Int = »unknown«) {};
// $TEST$ error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
(p: Int = »unknown«) -> 1;

// $TEST$ error "unknown is only allowed as the default value of a parameter of a class, enum variant, or function."
»unknown«;
}
2 changes: 1 addition & 1 deletion packages/safe-ds-vscode/syntaxes/safe-ds.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
{
"name": "constant.language.safe-ds",
"match": "\\b(false|null|true)\\b"
"match": "\\b(false|null|true|unknown)\\b"
},
{
"name": "storage.type.safe-ds",
Expand Down

0 comments on commit 78103e3

Please sign in to comment.