Skip to content
Draft
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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ hash not found". After adding a ref:
and `Darklang.SCM.Branch.mainBranchId` resolve; `SCM.Branch.mainBranchId` doesn't. Impl:
`backend/src/LibParser/NameResolver.fs` and `packages/darklang/languageTools/nameResolver.dark`.

**No `PACKAGE.` source prefix.** `PACKAGE.` is internal runtime/debug notation, not a
Dark namespace. Write `Stdlib.List.map` or `Darklang.Stdlib.List.map`, never
`PACKAGE.Darklang.Stdlib.List.map`; the same rule applies to search queries.

**Enum construction across modules.** Name the DU type, not just the module:
`ProgramTypes.Reference.PackageFn hash` works, `ProgramTypes.PackageFn hash` doesn't. In a
match arm the bare case is fine, since the matched value's type resolves it.
Expand Down
316 changes: 316 additions & 0 deletions Run result.md

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions backend/src/Builtins/Builtins.Language/Libs/Parser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,62 @@ let fns () : List<BuiltInFn> =
sqlSpec = NotQueryable
previewable = Impure
capabilities = LibExecution.Capabilities.noCaps
deprecated = NotDeprecated }

{ name = fn "parserParsePackageToWrittenTypes" 0
typeParams = []
parameters = [ Param.make "sourceCode" TString "" ]
returnType =
TCustomType(
NR.ok (
FQTypeName.fqPackage (LibExecution.PackageRefs.Type.Stdlib.option ())
),
[ TCustomType(NR.ok (FQTypeName.fqPackage (WTRefs.parsedFile ())), []) ]
)
description =
"Parses Darklang package source with declaration semantics at the file root."
fn =
(function
| _, _, _, [| DString sourceCode |] ->
let pfKT = KTCustomType(FQTypeName.fqPackage (WTRefs.parsedFile ()), [])
let result =
match (P.parsePackage sourceCode).parsed with
| Some parsed ->
Dval.optionSome
pfKT
(WrittenTypesToDarkTypes.parsedFileToDT parsed)
| None -> Dval.optionNone pfKT
Ply result
| _ -> incorrectArgs ())
sqlSpec = NotQueryable
previewable = Impure
capabilities = LibExecution.Capabilities.noCaps
deprecated = NotDeprecated }

{ name = fn "parserParsePackageDiagnostics" 0
typeParams = []
parameters = [ Param.make "sourceCode" TString "" ]
returnType =
TList(
TTuple(
TCustomType(NR.ok (FQTypeName.fqPackage (PackageRefs.range ())), []),
TString,
[]
)
)
description =
"Package-source diagnostics using declaration semantics at the file root."
fn =
(function
| _, _, _, [| DString sourceCode |] ->
Ply(
WrittenTypesToDarkTypes.diagnosticsToDT
(P.parsePackage sourceCode).diagnostics
)
| _ -> incorrectArgs ())
sqlSpec = NotQueryable
previewable = Impure
capabilities = LibExecution.Capabilities.noCaps
deprecated = NotDeprecated } ]


Expand Down
18 changes: 14 additions & 4 deletions backend/src/LibDB/ProgramTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,12 @@ let search
let isQualified = query.text.Contains "."

// Multi-token searches require every significant token to match either the
// item name or doc comment. Single unqualified queries of 3+ chars also search docs.
// qualified location or doc comment. This also applies to dotted queries:
// `AltJson.get` should find `AltJson.Helpers.getInt`, even though the parent
// module and operation are not contiguous in the fully-qualified name.
// Single unqualified queries of 3+ chars also search docs.
let tokens = tokenizeQuery query.text
let useTokenSearch =
(not query.exactMatch) && (not isQualified) && (List.length tokens > 1)
let useTokenSearch = (not query.exactMatch) && (List.length tokens > 1)

// Ignore short filler tokens in multi-token searches, unless every token is short.
let matchTokens =
Expand All @@ -254,7 +256,15 @@ let search
if useTokenSearch then
matchTokens
|> List.mapi (fun i _ ->
$"(l.name LIKE '%%' || @tok{i} || '%%' OR c.description LIKE '%%' || @tok{i} || '%%')")
// A conceptual query commonly combines a module with an operation,
// such as `string toList` or `json parse`. Match each token against
// the qualified location as well as the name and docs; otherwise the
// module token can only match when somebody happened to repeat it in
// the function's doc comment.
let qualifiedName = "(l.owner || '.' || l.modules || '.' || l.name)"

$"({qualifiedName} LIKE '%%' || @tok{i} || '%%' "
+ $"OR c.description LIKE '%%' || @tok{i} || '%%')")
|> String.concat " AND "
elif query.exactMatch then
if isQualified then
Expand Down
75 changes: 63 additions & 12 deletions backend/src/LibParser/Parser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1390,7 +1390,16 @@ and parseLet (state : ParserState) (i : int) : WT.Expr * int =
else
errExpected state afterRet "'=' in function binding"
(zeroWidthAtEnd (rng state afterRet), afterRet)
let (fnBody, m1) = parseBlock state m0
// If an `if` starts on the same line as `=`, its `else` is commonly
// aligned with the binding rather than the inline `if` token.
let (fnBody, m1) =
if
tok state m0 = TIf
&& (rng state m0).start.row = symbolEquals.end_.row
then
parseIf state keywordLet.start.column m0
else
parseBlock state m0
let m = if tok state m1 = TIn then m1 + 1 else m1
let (body, p) = parseBlock state m
let z = zeroWidthAtEnd keywordLet // no real `fun`/`->` tokens in this sugar
Expand Down Expand Up @@ -1435,7 +1444,14 @@ and parseLet (state : ParserState) (i : int) : WT.Expr * int =
// the value is an offside block, not a single expr, so a multi-statement
// binding (`let x =\n doThing ()\n result`) sequences instead of gluing
// the following statement onto the first as an application argument.
let (value, m) = parseBlock state k
let (value, m) =
if
tok state k = TIf
&& (rng state k).start.row = symbolEquals.end_.row
then
parseIf state keywordLet.start.column k
else
parseBlock state k
// `in` is optional
let m = if tok state m = TIn then m + 1 else m
let (body, p) = parseBlock state m
Expand Down Expand Up @@ -1770,7 +1786,10 @@ and parseInterpString (state : ParserState) (i : int) : WT.Expr * int =
|> List.map (fun (t : SpannedToken) ->
{ t with range = offRange t.range })
|> List.toArray
let subResult = parseTokensAt (state.interpDepth + 1) offToks
// Interpolation contents are expressions even when the surrounding
// file is being parsed as package declarations.
let subResult =
parseTokensAt (state.interpDepth + 1) false offToks
// surface parse errors from inside the interpolation `{…}` (their
// ranges are already offset to the outer source) rather than dropping them
subResult.diagnostics |> List.iter state.diagnostics.Add
Expand Down Expand Up @@ -3072,9 +3091,9 @@ and parseItemsBody
if k = before then if tok state k = TEOF then go <- false else k <- k + 1
(List.ofSeq decls, List.ofSeq exprs, k)

and parseFile (state : ParserState) : ParseResult =
and parseFile (topLevelIsModule : bool) (state : ParserState) : ParseResult =
validateLiterals state
let (topDecls, topExprs, _) = parseItems state false 0 0
let (topDecls, topExprs, _) = parseItems state topLevelIsModule 0 0
let fileRange =
if state.tokenCount > 1 then
span (rng state 0) (rng state (state.tokenCount - 2))
Expand All @@ -3086,7 +3105,11 @@ and parseFile (state : ParserState) : ParseResult =

/// Parse a pre-tokenized stream. Part of the rec chain so string interpolation
/// can recursively parse the (range-offset) sub-tokens of each `{expr}`.
and parseTokensAt (interpDepth : int) (toks : SpannedToken[]) : ParseResult =
and parseTokensAt
(interpDepth : int)
(topLevelIsModule : bool)
(toks : SpannedToken[])
: ParseResult =
let scopes = System.Collections.Generic.Stack<OffsideScope>()
scopes.Push { stmtCol = -1; stmtExact = false }
let state =
Expand All @@ -3102,11 +3125,18 @@ and parseTokensAt (interpDepth : int) (toks : SpannedToken[]) : ParseResult =
abandoned = false
steps = 0
interpDepth = interpDepth }
parseFile state
parseFile topLevelIsModule state

and parseTokens (toks : SpannedToken[]) : ParseResult =
parseTokensAt 0 false toks

and parseTokens (toks : SpannedToken[]) : ParseResult = parseTokensAt 0 toks
let private parsePackageTokens (toks : SpannedToken[]) : ParseResult =
parseTokensAt 0 true toks

let private parseSyntax (source : string) : ParseResult =
let private parseSyntaxWithMode
(topLevelIsModule : bool)
(source : string)
: ParseResult =
match tokenize source with
| Error e ->
{ parsed = None
Expand All @@ -3121,7 +3151,11 @@ let private parseSyntax (source : string) : ParseResult =
| Ok(toksList, lexDiags) ->
// lexical-recovery diagnostics (malformed lexemes the tokenizer recovered from)
// are surfaced alongside the parser's own diagnostics.
let result = parseTokens (List.toArray toksList)
let result =
if topLevelIsModule then
parsePackageTokens (List.toArray toksList)
else
parseTokens (List.toArray toksList)
let lexDiagnostics =
lexDiags
|> List.map (fun (r, m) ->
Expand All @@ -3136,7 +3170,7 @@ let private parseSyntax (source : string) : ParseResult =
/// Parse for tooling: return a recoverable tree and include mode-independent
/// structural diagnostics after a clean syntax pass.
let parse (source : string) : ParseResult =
let result = parseSyntax source
let result = parseSyntaxWithMode false source
let syntaxDiagnostics = result.diagnostics
// Tree-wide rules have one implementation in Validation. Run them only
// after a clean syntax pass so recovery holes do not create cascaded errors.
Expand All @@ -3149,13 +3183,30 @@ let parse (source : string) : ParseResult =
| _ -> []
{ result with diagnostics = syntaxDiagnostics @ structuralDiagnostics }

/// Parse package/module source where the file root is declaration scope.
/// A root `let value = ...` is therefore diagnosed as a package value that
/// must use `val`, rather than being consumed as a script-local binding whose
/// missing continuation is eventually reported at EOF.
let parsePackage (source : string) : ParseResult =
let result = parseSyntaxWithMode true source
let syntaxDiagnostics = result.diagnostics
let structuralDiagnostics =
match syntaxDiagnostics, result.parsed with
| [], Some(WT.SourceFile sourceFile) ->
sourceFile
|> Validation.validateStructure
|> List.map diagnosticOfValidationIssue
| _ -> []
{ result with diagnostics = syntaxDiagnostics @ structuralDiagnostics }

/// Parse for execution: syntax, structural, and file-purpose validation run
/// once, and only a validated source file can be returned on success.
let parseFor
(mode : Validation.Mode)
(source : string)
: Result<Validation.ValidatedSourceFile, List<Diagnostic>> =
let result = parseSyntax source
let result =
parseSyntaxWithMode (mode = Validation.Package) source
match result.diagnostics, result.parsed with
| [], Some(WT.SourceFile sourceFile) ->
match Validation.validate mode sourceFile with
Expand Down
18 changes: 15 additions & 3 deletions backend/src/LibParser/WrittenTypesToProgramTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ let private enumTypeName
(q.modules |> List.map (fun (m, _) -> m.name)) @ [ q.typ.name ]
|> List.filter (fun s -> s <> "")

// Option and Result are language prelude types: their cases are valid bare in
// both patterns and expressions. Other enum cases still need `Type.Case`, since
// without an expected-type inference pass there is no type name to resolve.
let private enumTypeNameForCase
(q : WT.QualifiedTypeIdentifier)
(caseName : string)
: WT.UnresolvedEnumTypeName =
match enumTypeName q, caseName with
| [], ("Some" | "None") -> [ "Stdlib"; "Option"; "Option" ]
| [], ("Ok" | "Error") -> [ "Stdlib"; "Result"; "Result" ]
| names, _ -> names

module InfixFnName =
let toPT (name : WT.InfixFnName) : PT.InfixFnName =
match name with
Expand Down Expand Up @@ -697,7 +709,7 @@ module Expr =
// EVariable so name resolution disambiguates. Left as an EEnum, a DB or
// variable ref would be forced into enum-case resolution and fail.
| WT.EEnum(_, tn, (_, caseName), fields, _) when
List.isEmpty tn.modules && tn.typ.name = "" && List.isEmpty fields
List.isEmpty (enumTypeNameForCase tn caseName) && List.isEmpty fields
->
return! toPT context (WT.EVariable(WT.synthRange, caseName))
| WT.EEnum(_, tn, (_, caseName), fields, _) ->
Expand All @@ -708,7 +720,7 @@ module Expr =
onMissing
branchId
currentModule
(enumTypeName tn)
(enumTypeNameForCase tn caseName)
caseName
let! exprs = Ply.List.mapSequentially (toPT context) fields
let! typeArgs =
Expand Down Expand Up @@ -885,7 +897,7 @@ module Expr =
onMissing
branchId
currentModule
(enumTypeName tn)
(enumTypeNameForCase tn caseName)
caseName
let! fields = Ply.List.mapSequentially (toPT context) fields
return PT.EPipeEnum(id, typeName, caseName, fields)
Expand Down
2 changes: 1 addition & 1 deletion backend/src/Wasm/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ <h1>Darklang REPL</h1>
"x * 2",
"let double (n: Int) : Int =\n n * 2",
"double 21",
'Stdlib.printLine "hello"',
'Stdlib.println "hello"',
'match 3 with\n| 1 -> "one"\n| _ -> "many"',
];
// #input is a textarea so multi-line examples survive; grow it with content.
Expand Down
6 changes: 3 additions & 3 deletions backend/testfiles/execution/stdlib/list.dark
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ Stdlib.List.map2shortest [ 10L; 20L ] [ 1L; 2L; 3L ] (fun a b -> a - b) = [ 9L;
Stdlib.List.map2shortest [] [ 1L; 2L; 3L ] (fun a b -> a - b) = []
Stdlib.List.map2shortest [ 1L; 2L; 3L ] [] (fun a b -> a - b) = []

Stdlib.List.member [ 1L; 2L; 3L ] 2L = true
Stdlib.List.member [ 1L; 2L; 3L ] 4L = false
Stdlib.List.member [] 1L = false
Stdlib.List.contains [ 1L; 2L; 3L ] 2L = true
Stdlib.List.contains [ 1L; 2L; 3L ] 4L = false
Stdlib.List.contains [] 1L = false

module Partition =
Stdlib.List.partition [ -20L; 5L; 9L ] (fun x -> x > 0L) = ([ 5L; 9L ], [ -20L ])
Expand Down
38 changes: 38 additions & 0 deletions backend/tests/Tests/LibParser.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,31 @@ let private parserStructureTests =
"migration hint"
| other -> failtest $"expected one diagnostic, got {other}")

testCase "package roots diagnose let values without an EOF cascade" (fun _ ->
let source =
"let chars = \"abc\"\n\nlet length (s: String) : Int = Stdlib.String.length s"
let result = P.parsePackage source
match result.diagnostics with
| [ diagnostic ] ->
Expect.equal
diagnostic.message
"Module value declarations must use 'val'; 'let' is reserved for functions and local bindings"
"focused package-value diagnostic"
Expect.isFalse
(diagnostic.message.Contains "end of file")
"the package-mode diagnostic identifies the root cause"
| other -> failtest $"expected one diagnostic, got {other}")

testCase "inline binding if allows else aligned with let" (fun _ ->
let source =
"let choose (condition: Bool) : Int =\n let result = if condition then\n 1\n else\n 2\n result"
let result = P.parsePackage source
Expect.isEmpty result.diagnostics "ordinary binding-aligned else parses"
match result.parsed with
| Some(WT.SourceFile { declarations = [ WT.DFunction _ ]; exprsToEval = [] }) ->
()
| other -> failtest $"function declaration: {other}")

testCase "val cannot declare a function" (fun _ ->
let result = P.parse "val f (x: Int64) : Int64 = x"
Expect.exists
Expand Down Expand Up @@ -669,6 +694,19 @@ let private desugarTests =
match toPT (lowerExpr "XDB") with
| PT.EVariable(_, "XDB") -> ()
| other -> failtest $"expected EVariable XDB, got: {other}")
testCase "bare prelude constructors lower to their enum types" (fun _ ->
match toPT (lowerExpr "Some 5L"), toPT (lowerExpr "None") with
| (PT.EEnum(_, someType, [], "Some", [ PT.EInt64 _ ]),
PT.EEnum(_, noneType, [], "None", [])) ->
Expect.equal
someType.originalName
[ "Stdlib"; "Option"; "Option" ]
"Some type"
Expect.equal
noneType.originalName
[ "Stdlib"; "Option"; "Option" ]
"None type"
| other -> failtest $"expected Option constructors, got: {other}")
testCase "a statement sequence lowers to EStatement" (fun _ ->
match toPT (fnBody "let f () : Unit =\n foo ()\n bar ()") with
| PT.EStatement(_, PT.EApply _, PT.EApply _) -> ()
Expand Down
6 changes: 5 additions & 1 deletion backend/tests/Tests/WrittenTypesLoweringParity.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,11 @@ let tests =
[ "M" ],
[],
[ "x" ],
"x |> Stdlib.Json.serialize<Bool>") ]
"x |> Stdlib.Json.serialize<Bool>")
("bare-some", [ "M" ], [], [], "Some 5")
("bare-none", [ "M" ], [], [], "None")
("bare-ok", [ "M" ], [], [], "Ok 5")
("bare-error", [ "M" ], [], [], "Error \"no\"") ]
let mismatches = ResizeArray<string>()
for (label, cmod, cfn, prms, snip) in cases do
let ctx : WT2PT.Context =
Expand Down
Loading