@@ -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