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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-waves-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@plutolang/pyright-deducer": patch
---

feat(deducer): support basic unary operation
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
StringNode,
TupleNode,
TypeAnnotationNode,
UnaryOperationNode,
} from "pyright-internal/dist/parser/parseNodes";
import { KeywordType } from "pyright-internal/dist/parser/tokenizerTypes";
import { TypeEvaluator } from "pyright-internal/dist/analyzer/typeEvaluatorTypes";
Expand All @@ -32,6 +33,7 @@ import {
StringTreeNode,
TreeNode,
TupleTreeNode,
UnaryOperationTreeNode,
} from "./value-tree-types";
import { getNodeText } from "./utils";

Expand All @@ -50,7 +52,7 @@ export class TreeBuilder {
private readonly createNodeFunctions: { [NodeType in ParseNodeType]?: CreateNodeFunction<any> } =
{
[ParseNodeType.Error]: this.unimplementedNode,
[ParseNodeType.UnaryOperation]: this.unimplementedNode,
[ParseNodeType.UnaryOperation]: this.createNodeForUnaryOperation,
[ParseNodeType.BinaryOperation]: this.createNodeForBinaryOperation,
[ParseNodeType.Assignment]: this.unimplementedNode,
[ParseNodeType.TypeAnnotation]: this.createNodeForTypeAnnotation,
Expand Down Expand Up @@ -217,6 +219,11 @@ export class TreeBuilder {
return DictionaryTreeNode.create(node, items);
}

private createNodeForUnaryOperation(node: UnaryOperationNode): UnaryOperationTreeNode {
const expression = this.createNode(node.expression);
return UnaryOperationTreeNode.create(node, expression);
}

private createNodeForBinaryOperation(node: BinaryOperationNode): BinaryOperationTreeNode {
const leftNode = this.createNode(node.leftExpression);
const rightNode = this.createNode(node.rightExpression);
Expand Down Expand Up @@ -245,7 +252,9 @@ export class TreeBuilder {

private unimplementedNode(node: ParseNode): never {
throw new Error(
`The creation of node type '${node.nodeType}' is not implemented yet. If you need this feature, please submit an issue.`
`${getNodeText(node)}: The creation of node type '${
node.nodeType
}' is not implemented yet. If you need this feature, please submit an issue.`
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TreeNodeBase,
TreeNodeFlags,
TupleTreeNode,
UnaryOperationTreeNode,
} from "./value-tree-types";
import { getNodeText } from "./utils";
import {
Expand All @@ -43,7 +44,7 @@ export class TreeEvaluator {

private readonly createNodeFunctions: { [NodeType in ParseNodeType]?: EvaluateFunction<any> } = {
[ParseNodeType.Error]: this.unimplementedNode,
[ParseNodeType.UnaryOperation]: this.unimplementedNode,
[ParseNodeType.UnaryOperation]: this.evaluateForUnaryOperation,
[ParseNodeType.BinaryOperation]: this.evaluateForBinaryOperation,
[ParseNodeType.Assignment]: this.unimplementedNode,
[ParseNodeType.TypeAnnotation]: this.unimplementedNode,
Expand Down Expand Up @@ -113,7 +114,7 @@ export class TreeEvaluator {
// Currently, we only support the environment variable access using the index node.
if (node.flags && node.flags & TreeNodeFlags.AccessEnvVar) {
const value = this.evaluate(node.items[0], fillings);
if (!Value.isStringLiteral(value)) {
if (!Value.isLiteral(value) || !Value.isStringLiteral(value)) {
throw new Error(`${getNodeText(node.node)}: Only support string literal access as index.`);
}

Expand All @@ -140,7 +141,7 @@ export class TreeEvaluator {
const result = node.strings
.map((str) => {
const part = this.evaluate(str, fillings);
if (!Value.isStringLiteral(part)) {
if (!Value.isLiteral(part) || !Value.isStringLiteral(part)) {
throw new Error(`${getNodeText(str.node)}: Only support string literal in string list.`);
}
return part.value;
Expand Down Expand Up @@ -169,16 +170,22 @@ export class TreeEvaluator {
(middleIdx >= node.node.middleTokens.length ||
node.node.fieldExpressions[fieldIdx].start < node.node.middleTokens[middleIdx].start);

if (isField && !Value.isStringLiteral(fieldValues[fieldIdx])) {
// prettier-ignore
throw new Error(
`${getNodeText(node.node)}: Only support string literal, not including the string returned by a function, in the format string.`
);
// Check if the field is a string literal; if not, throw an error.
if (isField) {
const fieldValue = fieldValues[fieldIdx];
if (!Value.isLiteral(fieldValue) || !Value.isStringLiteral(fieldValue)) {
// prettier-ignore
throw new Error(
`${getNodeText(node.node)}: Only support string literal, not including the string returned by a function, in the format string.`
);
}
// Append the string literal value to the result.
result += fieldValue.value;
fieldIdx++; // Move to the next field index.
} else {
// If not a field, append the middle token's escaped value to the result.
result += node.node.middleTokens[middleIdx++].escapedValue;
}

result += isField
? (fieldValues[fieldIdx++] as LiteralValue).value
: node.node.middleTokens[middleIdx++].escapedValue;
}

return LiteralValue.create(result);
Expand All @@ -188,12 +195,15 @@ export class TreeEvaluator {
if (node.flags && node.flags & TreeNodeFlags.AccessEnvVar) {
// This expression is an environment variable access.
const envVarName = this.evaluate(node.args[0], fillings);
if (!Value.isStringLiteral(envVarName)) {
if (!Value.isLiteral(envVarName) || !Value.isStringLiteral(envVarName)) {
throw new Error(`${getNodeText(node.node)}: Only support string literal access as index.`);
}

const defaultValue = node.args[1] ? this.evaluate(node.args[1], fillings) : undefined;
if (defaultValue && !Value.isStringLiteral(defaultValue)) {
if (
defaultValue &&
(!Value.isLiteral(defaultValue) || !Value.isStringLiteral(defaultValue))
) {
throw new Error(
`${getNodeText(
node.node
Expand Down Expand Up @@ -238,6 +248,52 @@ export class TreeEvaluator {
return LiteralValue.create(node.node.value);
}

private evaluateForUnaryOperation(node: UnaryOperationTreeNode, fillings: Fillings) {
const value = this.evaluate(node.expression, fillings);

if (!Value.isLiteral(value)) {
throw new Error(`${getNodeText(node.node)}: Only support literal values in unary operation.`);
}

if (Value.isNumberLiteral(value)) {
const numberValue = value.value as number;
switch (node.node.operator) {
case OperatorType.Add:
return LiteralValue.create(numberValue);
case OperatorType.Subtract:
return LiteralValue.create(-numberValue);
default:
throw new Error(
`The operator '${node.node.operator}' is not supported yet. If you need this feature, please submit an issue.`
);
}
}

if (Value.isStringLiteral(value)) {
if (node.node.operator === OperatorType.Add) {
return value;
}

throw new Error(
`${getNodeText(node.node)}: Only support the unary operation '+' for string literal.`
);
}

if (Value.isBooleanLiteral(value)) {
if (node.node.operator === OperatorType.Not) {
return LiteralValue.create(!value);
}

throw new Error(
`${getNodeText(node.node)}: Only support the unary operation 'not' for boolean literal.`
);
}

throw new Error(
`${getNodeText(node.node)}: The unary operation is not supported for the value type.`
);
}

private evaluateForBinaryOperation(
node: BinaryOperationTreeNode,
fillings: Fillings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
StringListNode,
StringNode,
TupleNode,
UnaryOperationNode,
} from "pyright-internal/dist/parser/parseNodes";
import * as AnalyzerNodeInfo from "pyright-internal/dist/analyzer/analyzerNodeInfo";
import { convertOffsetToPosition } from "pyright-internal/dist/common/positionUtils";
Expand Down Expand Up @@ -149,6 +150,26 @@ export namespace CallTreeNode {
}
}

export interface UnaryOperationTreeNode extends TreeNodeBase {
readonly nodeType: ParseNodeType.UnaryOperation;
readonly node: UnaryOperationNode;
readonly expression: TreeNode;
}

export namespace UnaryOperationTreeNode {
export function create(parseNode: UnaryOperationNode, expression: TreeNode) {
const node: UnaryOperationTreeNode = {
nodeType: ParseNodeType.UnaryOperation,
node: parseNode,
expression: expression,
print: () =>
`${getNodeText(parseNode, `UnaryOperation#${parseNode.operator}`)}(${expression.node.id})`,
};

return node;
}
}

export interface BinaryOperationTreeNode extends TreeNodeBase {
readonly nodeType: ParseNodeType.BinaryOperation;
readonly node: BinaryOperationNode;
Expand Down Expand Up @@ -270,6 +291,7 @@ export type TreeNode =
| TupleTreeNode
| DictionaryTreeNode
| CallTreeNode
| UnaryOperationTreeNode
| BinaryOperationTreeNode
| StringListTreeNode
| StringTreeNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,20 @@ export type Value =
export namespace Value {
// export function toJson(value: Value): string {}

export function isStringLiteral(value: Value): value is LiteralValue {
return value.valueType === ValueType.Literal && typeof value.value === "string";
export function isLiteral(value: Value): value is LiteralValue {
return value.valueType === ValueType.Literal;
}

export function isNumberLiteral(value: Value): value is LiteralValue {
return value.valueType === ValueType.Literal && typeof value.value === "number";
export function isStringLiteral(value: LiteralValue) {
return typeof value.value === "string";
}

export function isNumberLiteral(value: LiteralValue) {
return typeof value.value === "number";
}

export function isBooleanLiteral(value: LiteralValue) {
return typeof value.value === "boolean";
}

interface ToStringOptions {
Expand Down