Skip to content

Commit

Permalink
Merge pull request #1654 from DanielXMoore/let-question
Browse files Browse the repository at this point in the history
`let x?` allows initial `undefined` value + type inference; `let x? = y` for simple `y`
  • Loading branch information
edemaine authored Dec 21, 2024
2 parents ac15515 + a9f2eb0 commit fbc9b46
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 24 deletions.
16 changes: 15 additions & 1 deletion civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2548,14 +2548,28 @@ function f(x?: string)?: string
x
</Playground>
More generally, `T?` allows for `undefined` and
More generally, type `T?` allows for `undefined` and
`T??` additionally allows for `null`:
<Playground>
let i: number?
let x: string??
</Playground>
To allow for type inference and the initial `undefined` value:
<Playground>
let x?
</Playground>
To allow for later assignment of `undefined` while specifying an initial value
(currently limited to a literal or member expression):
<Playground>
let x? = 5
let y? = x
</Playground>
### Non-Null Types
`T!` removes `undefined` and `null` from the type:
Expand Down
21 changes: 13 additions & 8 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -2878,7 +2878,7 @@ LiteralContent
# https://262.ecma-international.org/#prod-NullLiteral
NullLiteral
"null" NonIdContinue ->
return { $loc, token: $1 }
return { type: "NullLiteral", $loc, token: $1 }

# https://262.ecma-international.org/#prod-BooleanLiteral
BooleanLiteral
Expand All @@ -2888,13 +2888,13 @@ BooleanLiteral
_BooleanLiteral
CoffeeBooleansEnabled CoffeeScriptBooleanLiteral -> $2
( "true" / "false" ) NonIdContinue ->
return { $loc, token: $1 }
return { type: "BooleanLiteral", $loc, token: $1 }

CoffeeScriptBooleanLiteral
( "yes" / "on" ) NonIdContinue ->
return { $loc, token: "true" }
return { type: "BooleanLiteral", $loc, token: "true" }
( "no" / "off" ) NonIdContinue ->
return { $loc, token: "false" }
return { type: "BooleanLiteral", $loc, token: "false" }

# NOTE: Added :symbol shorthand for Symbol.symbol or Symbol.for("symbol")
SymbolLiteral
Expand Down Expand Up @@ -6150,8 +6150,12 @@ CoffeeDoubleQuotedStringCharacters
# https://262.ecma-international.org/#prod-RegularExpressionLiteral
RegularExpressionLiteral
HeregexLiteral
$("/" RegularExpressionBody "/" RegularExpressionFlags) ->
return { type: "RegularExpressionLiteral", $loc, token: $1 }
$("/" RegularExpressionBody "/" RegularExpressionFlags):raw ->
return {
type: "RegularExpressionLiteral",
raw,
children: [ { $loc, token: raw } ]
}

RegularExpressionClass
$(OpenBracket RegularExpressionClassCharacters CloseBracket) ->
Expand Down Expand Up @@ -8101,10 +8105,11 @@ TypePrimary
# NOTE: typeof takes a unary expression, as in
# https://github.com/microsoft/TypeScript/blob/ae27e55b027c66bf5b80f596da866f8485ac491d/src/compiler/parser.ts#L5666-L5669
# (binary expression can accidentally grab closing ">" of type arguments)
_? Typeof _? UnaryExpression ->
_? Typeof _? UnaryExpression:expression ->
return {
type: "TypeofType",
type: "TypeTypeof",
children: $0,
expression,
}
_? TypeTuple ->
return prepend($1, $2)
Expand Down
40 changes: 34 additions & 6 deletions source/parser/declaration.civet
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ import {
convertOptionalType
insertTrimmingSpace
isExit
literalType
makeLeftHandSideExpression
makeNode
spliceChild
trimFirstSpace
updateParentPointers
} from ./util.civet
Expand Down Expand Up @@ -109,13 +111,39 @@ function processAssignmentDeclaration(decl: ASTLeaf, pattern: Binding["pattern"]
function processDeclarations(statements: StatementTuple[]): void
for each declaration of gatherRecursiveAll statements, .type is "Declaration"
{ bindings } := declaration
bindings?.forEach (binding) =>
{ typeSuffix } := binding
if typeSuffix and typeSuffix.optional and typeSuffix.t
// Convert `let x?: T` to `let x: undefined | T`
convertOptionalType typeSuffix
continue unless bindings?
for each binding of bindings
{ typeSuffix, initializer } .= binding

if typeSuffix and typeSuffix.optional
// Convert `let x? = y` to `let x: undefined | typeof y = y`
if initializer and not typeSuffix.t
expression := trimFirstSpace initializer.expression!
if expression.type is like "Identifier", "MemberExpression"
typeSuffix.children.push ": ", typeSuffix.t = {}
type: "TypeTypeof"
children: ["typeof ", expression]
expression
else if expression.type is "Literal" or
expression.type is "RegularExpressionLiteral" or
expression.type is "TemplateLiteral"
typeSuffix.children.push ": ", typeSuffix.t = literalType expression
else
spliceChild binding, typeSuffix, 1,
type: "Error"
message: `Optional type can only be inferred from literals or member expressions, not ${expression.type}`
continue
if typeSuffix.t
// Convert `let x?: T` to `let x: undefined | T`
convertOptionalType typeSuffix
else
// Convert `let x?` to `let x = undefined`
spliceChild binding, typeSuffix, 1
binding.children.push initializer = binding.initializer =
type: "Initializer"
expression: "undefined"
children: [" = ", "undefined"]

{ initializer } := binding
if initializer
prependStatementExpressionBlock initializer, declaration

Expand Down
22 changes: 17 additions & 5 deletions source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type ExpressionNode =
| PipelineExpression
| RegularExpressionLiteral
| StatementExpression
| TemplateLiteral
| TypeNode
| UnaryExpression
| UnwrappedExpression
Expand Down Expand Up @@ -766,10 +767,14 @@ export type ConditionFragment =

export type RegularExpressionLiteral =
type: "RegularExpressionLiteral"
$loc: Loc
token: string
children: Children
parent?: Parent
raw: string

export type TemplateLiteral
type: "TemplateLiteral"
children: Children
parent?: Parent
children?: never

export type ArrayBindingPattern =
type: "ArrayBindingPattern"
Expand Down Expand Up @@ -1124,7 +1129,7 @@ export type CoffeeClassPrivate

export type Literal =
type: "Literal"
subtype?: "NumericLiteral" | "StringLiteral"
subtype: "NullLiteral" | "BooleanLiteral" | "NumericLiteral" | "StringLiteral" | "RegularExpressionLiteral"
children: Children & LiteralContentNode[]
parent?: Parent
raw: string
Expand Down Expand Up @@ -1171,6 +1176,7 @@ export type ParseRule = (context: {fail: () => void}, state: {pos: number, input
export type TypeNode =
| TypeIdentifier
| TypeLiteral
| TypeTypeof
| TypeUnary
| TypeTuple
| TypeElement
Expand Down Expand Up @@ -1242,6 +1248,12 @@ export type TypeLiteral
children: Children
parent?: Parent

export type TypeTypeof
type: "TypeTypeof"
children: Children
parent?: Parent
expression: ASTNode

export type TypeAsserts
type: "TypeAsserts"
children: Children
Expand All @@ -1257,7 +1269,7 @@ export type TypePredicate

export type VoidType = ASTLeafWithType "VoidType"

export type TypeLiteralNode = ASTLeaf | VoidType
export type TypeLiteralNode = ASTLeaf | ASTString | VoidType

export type ThisAssignments = ([string, ASTRef] | AssignmentExpression)[]

Expand Down
44 changes: 40 additions & 4 deletions source/parser/util.civet
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
ASTNode
ASTNodeObject
ASTNodeParent
ASTString
Children
FunctionNode
FunctionSignature
Expand All @@ -14,10 +15,13 @@ import type {
Parameter
ParametersNode
Parent
StatementNode
TypeSuffix
RegularExpressionLiteral
ReturnTypeAnnotation
StatementNode
StatementTuple
TemplateLiteral
TypeNode
TypeSuffix
} from ./types.civet

import {
Expand Down Expand Up @@ -397,6 +401,36 @@ function literalValue(literal: Literal)
else
throw new Error("Unrecognized literal " + JSON.stringify(literal))

/** TypeScript type for given literal */
function literalType(literal: Literal | RegularExpressionLiteral | TemplateLiteral): TypeNode
let t: ASTString
switch literal.type
when "RegularExpressionLiteral"
t = "RegExp"
when "TemplateLiteral"
t = "string"
when "Literal"
switch literal.subtype
when "NullLiteral"
t = "null"
when "BooleanLiteral"
t = "boolean"
when "NumericLiteral"
if literal.raw.endsWith 'n'
t = "bigint"
else
t = "number"
when "StringLiteral"
t = "string"
else
throw new Error `unknown literal subtype ${literal.subtype}`
else
throw new Error `unknown literal type ${literal.type}`
{}
type: "TypeLiteral"
t
children: [t]

function makeNumericLiteral(n: number): Literal
s := n.toString()
type: "Literal"
Expand Down Expand Up @@ -680,8 +714,8 @@ function skipIfOnlyWS(target)
* Splice child from children/array, similar to Array.prototype.splice,
* but specifying a child instead of an index. Throw if child not found.
*/
function spliceChild(node: ASTNode, child: ASTNode, del, ...replacements)
children := node?.children ?? node
function spliceChild(node: ASTNodeObject | ASTNode[], child: ASTNode, del: number, ...replacements: ASTNode[])
children := Array.isArray(node) ? node : node.children
unless Array.isArray children
throw new Error "spliceChild: non-array node has no children field"
index := children.indexOf child
Expand Down Expand Up @@ -887,6 +921,7 @@ export {
isToken
isWhitespaceOrEmpty
literalValue
literalType
makeLeftHandSideExpression
makeNode
makeNumericLiteral
Expand All @@ -900,6 +935,7 @@ export {
replaceNode
replaceNodes
skipIfOnlyWS
spliceChild
startsWith
startsWithPredicate
stripTrailingImplicitComma
Expand Down
40 changes: 40 additions & 0 deletions test/types/let-declaration.civet
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,46 @@ describe "[TS] let declaration", ->
ParseError
"""

testCase """
let ? sets to undefined
---
let n?
---
let n = undefined
"""

testCase """
let ? with initializer
---
let a? = null
let b? = true
let c? = 5
let d? = 5n
let e? = "hello"
let f? = `hello`
let g? = /hello/
let h? = x
let i? = x.y[z]
---
let a : undefined | null= null
let b : undefined | boolean= true
let c : undefined | number= 5
let d : undefined | bigint= 5n
let e : undefined | string= "hello"
let f : undefined | string= `hello`
let g : undefined | RegExp= /hello/
let h : undefined | (typeof x)= x
let i : undefined | (typeof x.y[z])= x.y[z]
"""

throws """
let ? with complex initializer
---
let c? = x.y()
---
ParseErrors: unknown:1:6 Optional type can only be inferred from literals or member expressions, not CallExpression
"""

testCase """
let ?: allows for undefined
---
Expand Down

0 comments on commit fbc9b46

Please sign in to comment.