Skip to content

Commit

Permalink
Support optional properties and optional chaining of those properties (
Browse files Browse the repository at this point in the history
…#29)

* Update the parser to member access, optional chaining for member access, and optional properties on object types

* Add new snapshot files

* Update the type checker to handle optional chaining

* Update test case to verify behavior of optional chaining when it isn't required
  • Loading branch information
kevinbarabash committed Nov 20, 2023
1 parent f535806 commit 9db368f
Show file tree
Hide file tree
Showing 12 changed files with 464 additions and 45 deletions.
8 changes: 7 additions & 1 deletion src/Escalier.Data/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,20 @@ module Syntax =

type Stmt = { Kind: StmtKind; Span: Span }

type Property =
{ Name: string
TypeAnn: TypeAnn
Optional: bool
Readonly: bool }

// TODO: add location information
type ObjTypeAnnElem =
| Callable of Function
| Constructor of Function
| Method of name: string * is_mut: bool * type_: Function
| Getter of name: string * return_type: TypeAnn * throws: TypeAnn
| Setter of name: string * param: FuncParam<TypeAnn> * throws: TypeAnn
| Property of name: string * typeAnn: TypeAnn // TODO: readonly, optional
| Property of Property

type KeywordTypeAnn =
| Boolean
Expand Down
55 changes: 54 additions & 1 deletion src/Escalier.Parser.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ open FParsec
open Escalier.Parser

let lit = run Parser.lit
let expr = run Parser.expr
let expr = run (Parser.expr .>> eof)
let pattern = run Parser.pattern
let stmt = run Parser.stmt
let typeAnn = run Parser.typeAnn
Expand Down Expand Up @@ -37,6 +37,19 @@ let ParseString () =

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseOtherLiterals () =
let src =
"""
let a = undefined
let b = null
"""

let ast = script src
let result = $"input: %s{src}\noutput: %A{ast}"

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseTemplateString () =
let src = """msg = `foo ${`bar ${baz}`}`"""
Expand Down Expand Up @@ -93,6 +106,22 @@ let ParseCallThenIndexer () =

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseObjProperty () =
let src = "obj.a.b"
let expr = expr src
let result = $"input: %s{src}\noutput: %A{expr}"

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseObjPropWithOptChain () =
let src = "obj?.a?.b"
let expr = expr src
let result = $"input: %s{src}\noutput: %A{expr}"

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseFuncDef () =
let src = "fn (x, y) { x }"
Expand Down Expand Up @@ -208,3 +237,27 @@ let ParseObjRestSpread () =
let result = $"input: %s{src}\noutput: %A{ast}"

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseOptionalProps () =
let src =
"""
type Obj = {a?: {b?: {c?: number}}}
"""

let ast = script src
let result = $"input: %s{src}\noutput: %A{ast}"

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask

[<Fact>]
let ParseOptionalParams () =
let src =
"""
let foo = fn(a?: number, b?: string) => a
"""

let ast = script src
let result = $"input: %s{src}\noutput: %A{ast}"

Verifier.Verify(result, settings).ToTask() |> Async.AwaitTask
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ output: Success: [{ Kind =
("Point",
{ Kind =
Object
[Property ("x", { Kind = Keyword Number
Span = { Start = (Ln: 2, Col: 22)
Stop = (Ln: 2, Col: 28) }
InferredType = None });
Property ("y", { Kind = Keyword Number
Span = { Start = (Ln: 2, Col: 33)
Stop = (Ln: 2, Col: 39) }
InferredType = None })]
[Property { Name = "x"
TypeAnn = { Kind = Keyword Number
Span = { Start = (Ln: 2, Col: 22)
Stop = (Ln: 2, Col: 28) }
InferredType = None }
Optional = false
Readonly = false };
Property { Name = "y"
TypeAnn = { Kind = Keyword Number
Span = { Start = (Ln: 2, Col: 33)
Stop = (Ln: 2, Col: 39) }
InferredType = None }
Optional = false
Readonly = false }]
Span = { Start = (Ln: 2, Col: 18)
Stop = (Ln: 3, Col: 5) }
InferredType = None }, None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
input: obj?.a?.b
output: Success: { Kind = Member ({ Kind = Member ({ Kind = Identifier "obj"
Span = { Start = (Ln: 1, Col: 1)
Stop = (Ln: 1, Col: 4) }
InferredType = None }, "a", true)
Span = { Start = (Ln: 1, Col: 1)
Stop = (Ln: 1, Col: 7) }
InferredType = None }, "b", true)
Span = { Start = (Ln: 1, Col: 1)
Stop = (Ln: 1, Col: 10) }
InferredType = None }
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
input: obj.a.b
output: Success: { Kind = Member ({ Kind = Member ({ Kind = Identifier "obj"
Span = { Start = (Ln: 1, Col: 1)
Stop = (Ln: 1, Col: 4) }
InferredType = None }, "a", false)
Span = { Start = (Ln: 1, Col: 1)
Stop = (Ln: 1, Col: 6) }
InferredType = None }, "b", false)
Span = { Start = (Ln: 1, Col: 1)
Stop = (Ln: 1, Col: 8) }
InferredType = None }
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
input:
let foo = fn(a?: number, b?: string) => a

output: Success: [{ Kind =
Decl
{ Kind =
VarDecl
({ Kind = Identifier { Span = { Start = (Ln: 2, Col: 9)
Stop = (Ln: 2, Col: 13) }
Name = "foo"
IsMut = false }
Span = { Start = (Ln: 2, Col: 9)
Stop = (Ln: 2, Col: 13) }
InferredType = None },
{ Kind =
Function
{ Sig =
{ TypeParams = None
ParamList =
[{ Pattern =
{ Kind =
Identifier { Span = { Start = (Ln: 2, Col: 18)
Stop = (Ln: 2, Col: 19) }
Name = "a"
IsMut = false }
Span = { Start = (Ln: 2, Col: 18)
Stop = (Ln: 2, Col: 19) }
InferredType = None }
TypeAnn = Some { Kind = Keyword Number
Span = { Start = (Ln: 2, Col: 22)
Stop = (Ln: 2, Col: 28) }
InferredType = None }
Optional = true };
{ Pattern =
{ Kind =
Identifier { Span = { Start = (Ln: 2, Col: 30)
Stop = (Ln: 2, Col: 31) }
Name = "b"
IsMut = false }
Span = { Start = (Ln: 2, Col: 30)
Stop = (Ln: 2, Col: 31) }
InferredType = None }
TypeAnn = Some { Kind = Keyword String
Span = { Start = (Ln: 2, Col: 34)
Stop = (Ln: 2, Col: 40) }
InferredType = None }
Optional = true }]
ReturnType = None
Throws = None }
Body = Expr { Kind = Identifier "a"
Span = { Start = (Ln: 2, Col: 45)
Stop = (Ln: 3, Col: 3) }
InferredType = None } }
Span = { Start = (Ln: 2, Col: 15)
Stop = (Ln: 3, Col: 3) }
InferredType = None }, None)
Span = { Start = (Ln: 2, Col: 5)
Stop = (Ln: 3, Col: 3) } }
Span = { Start = (Ln: 2, Col: 5)
Stop = (Ln: 3, Col: 3) } }]
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
input:
type Obj = {a?: {b?: {c?: number}}}

output: Success: [{ Kind =
Decl
{ Kind =
TypeDecl
("Obj",
{ Kind =
Object
[Property
{ Name = "a"
TypeAnn =
{ Kind =
Object
[Property
{ Name = "b"
TypeAnn =
{ Kind =
Object
[Property
{ Name = "c"
TypeAnn =
{ Kind = Keyword Number
Span =
{ Start = (Ln: 2, Col: 31)
Stop = (Ln: 2, Col: 37) }
InferredType = None }
Optional = true
Readonly = false }]
Span = { Start = (Ln: 2, Col: 26)
Stop = (Ln: 2, Col: 38) }
InferredType = None }
Optional = true
Readonly = false }]
Span = { Start = (Ln: 2, Col: 21)
Stop = (Ln: 2, Col: 39) }
InferredType = None }
Optional = true
Readonly = false }]
Span = { Start = (Ln: 2, Col: 16)
Stop = (Ln: 3, Col: 3) }
InferredType = None }, None)
Span = { Start = (Ln: 2, Col: 5)
Stop = (Ln: 3, Col: 3) } }
Span = { Start = (Ln: 2, Col: 5)
Stop = (Ln: 3, Col: 3) } }]
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
input:
let a = undefined
let b = null

output: Success: [{ Kind =
Decl
{ Kind =
VarDecl ({ Kind = Identifier { Span = { Start = (Ln: 2, Col: 7)
Stop = (Ln: 2, Col: 9) }
Name = "a"
IsMut = false }
Span = { Start = (Ln: 2, Col: 7)
Stop = (Ln: 2, Col: 9) }
InferredType = None }, { Kind = Literal Undefined
Span = { Start = (Ln: 2, Col: 11)
Stop = (Ln: 2, Col: 20) }
InferredType = None }, None)
Span = { Start = (Ln: 2, Col: 3)
Stop = (Ln: 3, Col: 3) } }
Span = { Start = (Ln: 2, Col: 3)
Stop = (Ln: 3, Col: 3) } };
{ Kind =
Decl
{ Kind =
VarDecl ({ Kind = Identifier { Span = { Start = (Ln: 3, Col: 7)
Stop = (Ln: 3, Col: 9) }
Name = "b"
IsMut = false }
Span = { Start = (Ln: 3, Col: 7)
Stop = (Ln: 3, Col: 9) }
InferredType = None }, { Kind = Literal Null
Span = { Start = (Ln: 3, Col: 11)
Stop = (Ln: 3, Col: 15) }
InferredType = None }, None)
Span = { Start = (Ln: 3, Col: 3)
Stop = (Ln: 4, Col: 3) } }
Span = { Start = (Ln: 3, Col: 3)
Stop = (Ln: 4, Col: 3) } }]
47 changes: 40 additions & 7 deletions src/Escalier.Parser/Parser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ module Parser =
(pstring "true" |>> fun _ -> Literal.Boolean true)
<|> (pstring "false" |>> fun _ -> Literal.Boolean false)

litRef.Value <- number <|> string <|> boolean
let otherLiterals =
(pstring "undefined" |>> fun _ -> Literal.Undefined)
<|> (pstring "null" |>> fun _ -> Literal.Null)

litRef.Value <- number <|> string <|> boolean <|> otherLiterals

let mergeSpans (x: Span) (y: Span) = { Start = x.Start; Stop = y.Stop }

Expand Down Expand Up @@ -239,7 +243,26 @@ module Parser =

let term = (atom .>> ws) <|> between (strWs "(") (strWs ")") expr

opp.TermParser <- term
let memberOp =
fun (optChain: bool) (x: Expr) (y: Expr) ->
match y.Kind with
| Identifier ident ->
{ Expr.Kind = ExprKind.Member(x, ident, optChain)
Span = mergeSpans x.Span y.Span
InferredType = None }
| _ -> failwith "Expected identifier"

let member': Parser<(Expr -> Expr -> Expr), unit> =
(pstring "?." <|> pstring ".") .>> spaces
>>= fun op ->
match op with
| "." -> preturn (memberOp false)
| "?." -> preturn (memberOp true)
| _ -> failwith "Expected '.' or '?.'"

let termWithPropSuffix = chainl1 term member'

opp.TermParser <- termWithPropSuffix

type Assoc = Associativity

Expand All @@ -255,7 +278,7 @@ module Parser =
"[",
(pipe2 getPosition ((ws >>. expr) .>> (strWs "]"))
<| fun p1 expr -> ([ expr ], p1)), // (indices, position)
18,
17,
true,
(),
(fun (indices, stop) target ->
Expand All @@ -272,7 +295,7 @@ module Parser =
"(",
(pipe2 getPosition (sepBy (ws >>. expr) (strWs ",") .>> (strWs ")"))
<| fun p1 args -> (args, p1)), // args
18,
17,
true,
(),
(fun (args, stop) callee ->
Expand Down Expand Up @@ -528,11 +551,21 @@ module Parser =
InferredType = None }

let private objTypeAnnKeyValue: Parser<ObjTypeAnnElem, unit> =
pipe4 getPosition ident (strWs ":" >>. typeAnn) getPosition
<| fun p1 name typeAnn p2 ->
pipe5
getPosition
ident
(opt (strWs "?"))
(strWs ":" >>. typeAnn)
getPosition
<| fun p1 name optional typeAnn p2 ->
// TODO: add location information
let span = { Start = p1; Stop = p2 }
ObjTypeAnnElem.Property(name, typeAnn)

ObjTypeAnnElem.Property
{ Name = name
TypeAnn = typeAnn
Optional = optional.IsSome
Readonly = false }

let private objTypeAnnElem = choice [ objTypeAnnKeyValue ]

Expand Down
Loading

0 comments on commit 9db368f

Please sign in to comment.