Skip to content

Commit f0e7f86

Browse files
committed
feat(language)!: switch to import directive and remove unused LSP warning
1 parent b1353e1 commit f0e7f86

25 files changed

Lines changed: 227 additions & 212 deletions

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ All notable changes to FScript are documented in this file.
44

55
## [Unreleased]
66

7-
- _No entries yet._
7+
- Replaced `#include` with `import` and removed script-level `module` declarations.
8+
- Imported files are now exposed through filename-derived modules (for example `shared.fss` -> `shared.*`).
9+
- Updated parser/runtime/LSP/docs/tests for the new import/module semantics.
10+
- Removed `unused` top-level binding diagnostics from LSP.
811

912
## [0.31.0]
1013

docs/guides/fsharp-ocaml-differences.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This document is a quick comparison guide. It highlights intentional differences
66
## Scope of the language
77
- FScript is a small interpreted subset, not a full compiler language.
88
- FScript supports a focused core: `let`, `let rec` (functions), `if/elif/else`, `for`, `match`, lists, tuples, options, records, discriminated unions, pipelines, and extern calls.
9-
- Many F#/OCaml features are intentionally absent today (classes, interfaces, computation expressions, etc.). `module` declarations are supported only in included files.
9+
- Many F#/OCaml features are intentionally absent today (classes, interfaces, computation expressions, etc.). Script composition uses `import`, and imported files are exposed through filename-derived modules.
1010

1111
## Syntax differences
1212

docs/guides/getting-started-tutorial.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ You will learn:
77
- how to run a script,
88
- how to model data with records/lists/maps/options/unions,
99
- how to use pattern matching,
10-
- how to split scripts with `#include`,
10+
- how to split scripts with `import`,
1111
- how to expose host-callable functions with `[<export>]`.
1212

1313
## Get FScript
@@ -33,7 +33,7 @@ make build
3333
Then run scripts with:
3434

3535
```bash
36-
dotnet run --project src/FScript -- your-script.fss
36+
./src/FScript/bin/Debug/net10.0/fscript your-script.fss
3737
```
3838

3939
## 1. Run your first script
@@ -324,28 +324,26 @@ let hasA = m |> Map.containsKey "a"
324324
Full reference:
325325
- [`docs/specs/stdlib-functions.md`](../specs/stdlib-functions.md)
326326

327-
## 10. Includes and modules
328-
You can split scripts using `#include`.
327+
## 10. Imports and file modules
328+
You can split scripts using `import`.
329329

330330
`main.fss`:
331331

332332
```fsharp
333-
#include "shared/math.fss"
334-
print $"{sum 20 22}"
333+
import "shared/math.fss"
334+
print $"{math.sum 20 22}"
335335
```
336336

337337
`shared/math.fss`:
338338

339339
```fsharp
340-
module Math
341-
342340
let sum a b = a + b
343341
```
344342

345343
Notes:
346-
- included files are `.fss`,
347-
- include cycles are fatal,
348-
- modules are supported in included files.
344+
- imported files are `.fss`,
345+
- import cycles are fatal,
346+
- imported files are namespaced by filename stem (`math.sum`, `common.join`, ...).
349347

350348
## 11. Hosting, exports, and sandboxing (advanced)
351349
FScript is designed to be embedded.

docs/specs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ Normative behavior for the language, runtime surface, hosting model, and editor/
2424
- LSP inlay hints: [`lsp-inlay-hints.md`](./lsp-inlay-hints.md)
2525
- LSP uses runtime extern schemes for typing/signatures and resolves navigation to included-file declarations.
2626
- LSP injected stdlib functions show named-argument signatures when available and resolve definition to readonly virtual stdlib sources (`fscript-stdlib:///...`).
27-
- Definition/type-definition from record field labels in function return record literals resolves to the declared return type (including include-provided types).
27+
- Definition/type-definition from record field labels in function return record literals resolves to the declared return type (including import-provided types).

docs/specs/embedding-fscript-language.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ Each error contains:
188188

189189
## Embedding cookbook
190190

191-
### 1. Parse/eval a file with `#include`
192-
Use include-aware parsing when executing scripts from disk.
191+
### 1. Parse/eval a file with `import`
192+
Use import-aware parsing when executing scripts from disk.
193193

194194
```fsharp
195195
open FScript.Language

docs/specs/sandbox-and-security.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ This document defines the security model for running FScript programs with host
1313
- `RootDirectory : string`
1414
- The CLI defaults `RootDirectory` to the script file directory.
1515
- The CLI allows overriding root with `--root <path>` (or `-r <path>`).
16-
- `#include` file resolution is constrained to `RootDirectory`.
17-
- Include paths are resolved relative to the current script file.
18-
- Include cycles are rejected.
16+
- `import` file resolution is constrained to `RootDirectory`.
17+
- Import paths are resolved relative to the current script file.
18+
- Import cycles are rejected.
1919

2020
Filesystem extern behavior:
2121
- `Fs.readText` and `Fs.enumerateFiles` resolve candidate paths through `HostCommon.tryResolvePath`.

docs/specs/syntax-and-indentation.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This document describes the concrete syntax accepted by the interpreter and the
66
## File and program structure
77
- A program is a sequence of top-level statements.
88
- Top-level statements are:
9-
- `#include "relative/path/file.fss"` directives
9+
- `import "relative/path/file.fss"` directives
1010
- `type` / `type rec` declarations
1111
- `let` / `let rec` bindings
1212
- `[<export>] let` / `[<export>] let rec` bindings
@@ -207,21 +207,22 @@ All of the following map literal layouts are valid:
207207
- declared-type-by-shape: `let say_hello (person: { Name: string }) = ...`
208208
- Inline record annotation fields are `;`-separated in single-line form.
209209

210-
## Include directive
211-
- Include uses preprocessor-style syntax:
212-
- `#include "shared/helpers.fss"`
213-
- Includes are top-level only.
214-
- Included files are merged into the same global namespace as the current script.
215-
- Include loading is recursive.
210+
## Import directive
211+
- Import uses preprocessor-style syntax:
212+
- `import "shared/helpers.fss"`
213+
- Imports are top-level only.
214+
- Imported files are wrapped in an implicit module derived from the imported file name.
215+
- `import "shared/helpers.fss"` exposes symbols under `helpers.*`.
216+
- Import loading is recursive.
216217
- Cycles are fatal and reported as parse errors.
217-
- File paths in includes must be `.fss`.
218-
- Include resolution is file-relative:
219-
- `#include "x.fss"` resolves from the directory of the file containing the directive.
218+
- File paths in imports must be `.fss`.
219+
- Import resolution is file-relative:
220+
- `import "x.fss"` resolves from the directory of the file containing the directive.
220221
- Paths are normalized before loading:
221222
- relative segments like `.` and `..` are collapsed.
222223
- Resolved files must stay inside the configured sandbox root (`RootDirectory`).
223224
- escaping the root is a parse error.
224-
- A file already loaded once in the include graph is skipped on subsequent includes.
225-
- includes are effectively deduplicated.
226-
- `#include` must appear before module declarations and executable/type code in a file.
227-
- Parse/type/eval errors include file-aware spans (`file`, `line`, `column`) so failures in included files point to the offending source.
225+
- A file already loaded once in the import graph is skipped on subsequent imports.
226+
- imports are effectively deduplicated.
227+
- `import` must appear before executable/type code in a file.
228+
- Parse/type/eval errors include file-aware spans (`file`, `line`, `column`) so failures in imported files point to the offending source.

samples/includes-and-exports.fss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
#include "includes/common.fss"
1+
import "includes/common.fss"
22

33
[<export>] let extension_name = "demo"
44

5-
[<export>] let summary (project: ProjectInfo) =
6-
let steps = [ "build"; "test"; "publish" ] |> List.map describe_command
7-
$"{describe_project project}: {join_with_comma steps}"
5+
[<export>] let summary (project: common.ProjectInfo) =
6+
let steps = [ "build"; "test"; "publish" ] |> List.map common.describe_command
7+
$"{common.describe_project project}: {common.join_with_comma steps}"
88

99
let main =
1010
let project = { Name = "Terrabuild"; Language = "F#" }

src/FScript.Language/Ast.fs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ and MapEntry =
8282

8383
and Stmt =
8484
| SType of TypeDef
85-
| SInclude of string * Span
86-
| SModuleDecl of string * Span
85+
| SImport of string * Span
8786
| SLet of string * Param list * Expr * bool * bool * Span
8887
| SLetRecGroup of (string * Param list * Expr * Span) list * bool * Span
8988
| SExpr of Expr

src/FScript.Language/IncludeResolver.fs

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,28 @@ module IncludeResolver =
1313

1414
let private ensureFssPath (path: string) (span: Span) =
1515
if not (path.EndsWith(".fss", StringComparison.OrdinalIgnoreCase)) then
16-
raise (ParseException { Message = "Only '.fss' files can be used with '#include'"; Span = span })
16+
raise (ParseException { Message = "Only '.fss' files can be used with 'import'"; Span = span })
1717

1818
let private ensureWithinRoot (rootDirectoryWithSeparator: string) (path: string) (span: Span) =
1919
let fullPath = Path.GetFullPath(path)
2020
let fullRoot = rootDirectoryWithSeparator.TrimEnd(Path.DirectorySeparatorChar)
2121
let isRootItself = String.Equals(fullPath, fullRoot, StringComparison.OrdinalIgnoreCase)
2222
let isUnderRoot = fullPath.StartsWith(rootDirectoryWithSeparator, StringComparison.OrdinalIgnoreCase)
2323
if not (isRootItself || isUnderRoot) then
24-
raise (ParseException { Message = $"Included file '{fullPath}' is outside of sandbox root"; Span = span })
24+
raise (ParseException { Message = $"Imported file '{fullPath}' is outside of sandbox root"; Span = span })
2525
fullPath
2626

27-
let private resolveIncludePath (currentFile: string) (includePath: string) (rootDirectoryWithSeparator: string) (span: Span) =
28-
if String.IsNullOrWhiteSpace(includePath) then
29-
raise (ParseException { Message = "Include path cannot be empty"; Span = span })
27+
let private resolveImportPath (currentFile: string) (importPath: string) (rootDirectoryWithSeparator: string) (span: Span) =
28+
if String.IsNullOrWhiteSpace(importPath) then
29+
raise (ParseException { Message = "Import path cannot be empty"; Span = span })
3030

31-
ensureFssPath includePath span
31+
ensureFssPath importPath span
3232

3333
let currentDirectory = Path.GetDirectoryName(currentFile)
3434
let candidate =
35-
if Path.IsPathRooted(includePath) then includePath
36-
elif String.IsNullOrEmpty(currentDirectory) then includePath
37-
else Path.Combine(currentDirectory, includePath)
35+
if Path.IsPathRooted(importPath) then importPath
36+
elif String.IsNullOrEmpty(currentDirectory) then importPath
37+
else Path.Combine(currentDirectory, importPath)
3838

3939
ensureWithinRoot rootDirectoryWithSeparator candidate span
4040

@@ -66,6 +66,23 @@ module IncludeResolver =
6666

6767
let private qualifyName (moduleName: string) (name: string) = $"{moduleName}.{name}"
6868

69+
let private isValidModuleName (name: string) =
70+
let startsValid c = Char.IsLetter(c) || c = '_'
71+
let partValid c = Char.IsLetterOrDigit(c) || c = '_'
72+
not (String.IsNullOrWhiteSpace(name))
73+
&& startsValid name[0]
74+
&& (name |> Seq.forall partValid)
75+
76+
let private deriveModuleNameFromFilePath (filePath: string) (span: Span) =
77+
let stem = Path.GetFileNameWithoutExtension(filePath)
78+
if isValidModuleName stem then
79+
stem
80+
else
81+
raise (ParseException {
82+
Message = $"Imported filename stem '{stem}' is not a valid module name. Rename the file to a valid identifier."
83+
Span = span
84+
})
85+
6986
let private rewriteModuleScopedStatements (moduleName: string) (statements: Stmt list) : Stmt list =
7087
let topLevelNames =
7188
statements
@@ -185,70 +202,73 @@ module IncludeResolver =
185202
let private expandProgram
186203
(rootDirectoryWithSeparator: string)
187204
(fileSpan: string -> Span)
205+
(moduleToFile: System.Collections.Generic.Dictionary<string, string>)
188206
(loadFileRef: (string list -> bool -> string -> Program) ref)
189207
(stack: string list)
190208
(isMainFile: bool)
191209
(currentFile: string)
192210
(program: Program)
193211
: Program =
194212
let mutable seenCode = false
195-
let mutable moduleDecl: (string * Span) option = None
196-
let includes = ResizeArray<string * Span>()
213+
let imports = ResizeArray<string * Span>()
197214
let localCode = ResizeArray<Stmt>()
198215

199216
for stmt in program do
200217
match stmt with
201-
| SInclude(includePath, span) ->
202-
if seenCode || moduleDecl.IsSome then
203-
raise (ParseException { Message = "'#include' directives must appear before module declaration and code"; Span = span })
204-
includes.Add(includePath, span)
205-
| SModuleDecl(moduleName, span) ->
206-
if isMainFile then
207-
raise (ParseException { Message = "'module' is not allowed in the main script"; Span = span })
218+
| SImport(importPath, span) ->
208219
if seenCode then
209-
raise (ParseException { Message = "'module' declaration must appear before code"; Span = span })
210-
match moduleDecl with
211-
| Some (_, previousSpan) ->
212-
raise (ParseException { Message = "Only one 'module' declaration is allowed per file"; Span = previousSpan })
213-
| None ->
214-
moduleDecl <- Some(moduleName, span)
220+
raise (ParseException { Message = "'import' directives must appear before code"; Span = span })
221+
imports.Add(importPath, span)
215222
| _ ->
216223
seenCode <- true
217224
localCode.Add(stmt)
218225

219-
let includedStatements =
220-
includes
226+
let importedStatements =
227+
imports
221228
|> Seq.toList
222-
|> List.collect (fun (includePath, span) ->
223-
let resolvedPath = resolveIncludePath currentFile includePath rootDirectoryWithSeparator span
229+
|> List.collect (fun (importPath, span) ->
230+
let resolvedPath = resolveImportPath currentFile importPath rootDirectoryWithSeparator span
224231
(!loadFileRef) stack false resolvedPath)
225232

226233
let localStatements = localCode |> Seq.toList
227234
let rewrittenLocalStatements =
228-
match moduleDecl with
229-
| Some (moduleName, _) -> rewriteModuleScopedStatements moduleName localStatements
230-
| None -> localStatements
235+
if isMainFile then
236+
localStatements
237+
else
238+
let span = fileSpan currentFile
239+
let moduleName = deriveModuleNameFromFilePath currentFile span
240+
match moduleToFile.TryGetValue(moduleName) with
241+
| true, existingPath when not (String.Equals(existingPath, currentFile, StringComparison.OrdinalIgnoreCase)) ->
242+
raise (ParseException {
243+
Message = $"Module name collision: '{moduleName}' is derived from both '{existingPath}' and '{currentFile}'"
244+
Span = span
245+
})
246+
| _ ->
247+
moduleToFile[moduleName] <- currentFile
248+
rewriteModuleScopedStatements moduleName localStatements
231249

232-
includedStatements @ rewrittenLocalStatements
250+
importedStatements @ rewrittenLocalStatements
233251

234252
let parseIncludedSource (sourceName: string) (source: string) : Program =
235253
let program = Parser.parseProgramWithSourceName (Some sourceName) source
236254
let dummyRoot = normalizeDirectoryPath "."
237255
let fileSpan path =
238256
let p = Span.posInFile path 1 1
239257
Span.mk p p
258+
let moduleToFile = System.Collections.Generic.Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
240259
let loadRef = ref (fun (_: string list) (_: bool) (_: string) -> ([]: Program))
241-
if program |> List.exists (function | SInclude _ -> true | _ -> false) then
242-
let includeSpan =
260+
if program |> List.exists (function | SImport _ -> true | _ -> false) then
261+
let importSpan =
243262
program
244-
|> List.choose (function | SInclude(_, span) -> Some span | _ -> None)
263+
|> List.choose (function | SImport(_, span) -> Some span | _ -> None)
245264
|> List.head
246-
raise (ParseException { Message = "Embedded stdlib source does not support '#include'"; Span = includeSpan })
247-
expandProgram dummyRoot fileSpan loadRef [] false sourceName program
265+
raise (ParseException { Message = "Embedded stdlib source does not support 'import'"; Span = importSpan })
266+
expandProgram dummyRoot fileSpan moduleToFile loadRef [] false sourceName program
248267

249268
let parseProgramFromFile (rootDirectory: string) (entryFile: string) : Program =
250269
let rootDirectoryWithSeparator = normalizeDirectoryPath rootDirectory
251270
let visited = System.Collections.Generic.HashSet<string>(StringComparer.OrdinalIgnoreCase)
271+
let moduleToFile = System.Collections.Generic.Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
252272
let fileSpan path =
253273
let p = Span.posInFile path 1 1
254274
Span.mk p p
@@ -261,7 +281,7 @@ module IncludeResolver =
261281

262282
if stack |> List.exists (fun p -> String.Equals(p, sandboxedPath, StringComparison.OrdinalIgnoreCase)) then
263283
let cycleChain = (sandboxedPath :: stack |> List.rev) @ [ sandboxedPath ]
264-
let message = sprintf "Include cycle detected: %s" (String.concat " -> " cycleChain)
284+
let message = sprintf "Import cycle detected: %s" (String.concat " -> " cycleChain)
265285
raise (ParseException { Message = message; Span = fileSpan sandboxedPath })
266286

267287
if visited.Contains(sandboxedPath) then
@@ -271,13 +291,14 @@ module IncludeResolver =
271291
let source = File.ReadAllText(sandboxedPath)
272292
let program = Parser.parseProgramWithSourceName (Some sandboxedPath) source
273293
let loadRef = ref loadFile
274-
expandProgram rootDirectoryWithSeparator fileSpan loadRef (sandboxedPath :: stack) isMainFile sandboxedPath program
294+
expandProgram rootDirectoryWithSeparator fileSpan moduleToFile loadRef (sandboxedPath :: stack) isMainFile sandboxedPath program
275295

276296
loadFile [] true entryFile
277297

278298
let parseProgramFromSourceWithIncludes (rootDirectory: string) (entryFile: string) (entrySource: string) : Program =
279299
let rootDirectoryWithSeparator = normalizeDirectoryPath rootDirectory
280300
let visited = System.Collections.Generic.HashSet<string>(StringComparer.OrdinalIgnoreCase)
301+
let moduleToFile = System.Collections.Generic.Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
281302
let fileSpan path =
282303
let p = Span.posInFile path 1 1
283304
Span.mk p p
@@ -295,7 +316,7 @@ module IncludeResolver =
295316

296317
if stack |> List.exists (fun p -> String.Equals(p, sandboxedPath, StringComparison.OrdinalIgnoreCase)) then
297318
let cycleChain = (sandboxedPath :: stack |> List.rev) @ [ sandboxedPath ]
298-
let message = sprintf "Include cycle detected: %s" (String.concat " -> " cycleChain)
319+
let message = sprintf "Import cycle detected: %s" (String.concat " -> " cycleChain)
299320
raise (ParseException { Message = message; Span = fileSpan sandboxedPath })
300321

301322
if visited.Contains(sandboxedPath) then
@@ -309,6 +330,6 @@ module IncludeResolver =
309330
File.ReadAllText(sandboxedPath)
310331
let program = Parser.parseProgramWithSourceName (Some sandboxedPath) source
311332
let loadRef = ref loadFile
312-
expandProgram rootDirectoryWithSeparator fileSpan loadRef (sandboxedPath :: stack) isMainFile sandboxedPath program
333+
expandProgram rootDirectoryWithSeparator fileSpan moduleToFile loadRef (sandboxedPath :: stack) isMainFile sandboxedPath program
313334

314335
loadFile [] true entrySandboxedPath

0 commit comments

Comments
 (0)