From d984a6c6ae0f23a4434f87748bac2e11085ab3c1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 2 Nov 2023 18:30:09 +0000 Subject: [PATCH] Cover 100% of core spec tests, support WIT codegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Here is a more detailed breakdown for specific changes: 1. WebAssembly binary parser change to make it compliant with the spec. Files changed: ./Sources/WasmKit/Parser/ ./Tests/WasmKitTests/Parser/ 2. WebAssembly interpreter changes to make it compliant with the spec. No JIT or assembly code generation done in the interpreter. Files changed: ./Sources/WasmKit/Execution/ ./Sources/WasmKit/Types/ ./Tests/WasmKitTests/Execution/ 3. WASI system interface changes to make it compliant with the spec. ./Sources/WASI/ ./Sources/SystemExtras/ ./Tests/WASITests/ ./IntegrationTests/WASI/ 4 .WIT parser change to make it compliant with the Component Model specification. Files changed: ./Sources/WIT/ ./Tests/WITTests/ 5. WIT code generation change. We should mention that this only generates Swift code statically and no binary code is generated. It’s similar to protoc in Protocol Buffers or Swift OpenAPI Generator projects that are already open-source. No “hot reloading” or dynamic code generation is done (i.e. when application is running). Files changed: ./Plugins/ ./Sources/WITExtractor/ ./Sources/WITOverlayGenerator/ ./Sources/WasmKit/Component/ ./Tests/WITExtractorTests/ ./Tests/WITOverlayGeneratorTests/ ./Tests/WITExtractorPluginTests/ Co-authored-by: Yuta Saito --- .gitignore | 10 +- .gitmodules | 12 +- .swift-format | 7 + .swift-version | 1 - CI/Sources/os-check.sh | 19 + CI/check-spectest.sh | 37 + CI/check-wasi-testsuite.sh | 29 + Documentation/ComponentModel/CanonicalABI.md | 79 + .../ComponentModel/SemanticsNotes.md | 84 + Examples/wasm/factorial.wat | 11 + Examples/wasm/fib.wat | 46 + Examples/wasm/host.wat | 18 + IntegrationTests/WASI/adapter.py | 33 + IntegrationTests/WASI/run-tests.sh | 15 + IntegrationTests/WASI/skip.json | 8 + Makefile | 57 +- Mintfile | 2 - NOTICE.txt | 15 + Package.resolved | 125 +- Package.swift | 110 +- .../GenerateOverlayForTesting/Plugin.swift | 38 + Plugins/WITExtractorPlugin/Plugin.swift | 75 + Plugins/WITOverlayPlugin/Plugin.swift | 36 + README.md | 31 +- Sources/CLI/{main.swift => CLI.swift} | 10 +- .../Generated/AutoEquatable.generated.swift | 28 - Sources/CLI/Run/Run.swift | 133 +- Sources/CLI/SpecTest/Spectest.swift | 55 - Sources/CLI/SpecTest/TestCase.swift | 339 --- Sources/Spectest/Spectest.swift | 120 ++ Sources/Spectest/TestCase.swift | 568 +++++ Sources/SystemExtras/Clock.swift | 112 + Sources/SystemExtras/Constants.swift | 128 ++ Sources/SystemExtras/FileAtOperations.swift | 290 +++ Sources/SystemExtras/FileOperations.swift | 391 ++++ Sources/SystemExtras/Syscalls.swift | 131 ++ Sources/SystemExtras/Vendor/Exports.swift | 169 ++ Sources/SystemExtras/Vendor/Utils.swift | 150 ++ .../Execution/Instructions/Control.swift | 162 -- .../Instructions/InstructionFactory.swift | 11 - .../WAKit/Execution/Instructions/Memory.swift | 64 - .../Execution/Instructions/Numeric.swift | 68 - .../Execution/Instructions/Parametric.swift | 27 - .../Execution/Instructions/Variable.swift | 47 - Sources/WAKit/Execution/Runtime/Runtime.swift | 220 -- Sources/WAKit/Execution/Runtime/Stack.swift | 170 -- Sources/WAKit/Execution/Runtime/Store.swift | 122 -- Sources/WAKit/Execution/Types/Instances.swift | 105 - Sources/WAKit/Execution/Types/Trap.swift | 52 - Sources/WAKit/Execution/Types/Value.swift | 544 ----- .../Generated/AutoEquatable.generated.swift | 23 - .../Parser/Stream/FileHandleStream.swift | 56 - Sources/WAKit/Parser/Stream/Stream.swift | 30 - Sources/WAKit/Parser/Wasm/WasmParser.swift | 982 --------- .../Instructions/NumericInstruction.swift | 497 ----- Sources/WAKit/Types/Module.swift | 135 -- Sources/WAKit/Types/Types.swift | 54 - Sources/WAKit/Validation/Validation.swift | 89 - Sources/WASI/FileSystem.swift | 120 ++ Sources/WASI/GuestMemorySupport.swift | 16 + Sources/WASI/Platform/Directory.swift | 169 ++ Sources/WASI/Platform/Entry.swift | 100 + Sources/WASI/Platform/File.swift | 116 + Sources/WASI/Platform/PlatformTypes.swift | 221 ++ Sources/WASI/Platform/SandboxPrimitives.swift | 1 + .../Platform/SandboxPrimitives/Open.swift | 101 + .../SandboxPrimitives/OpenParent.swift | 51 + Sources/WASI/WASI.swift | 1871 +++++++++++++++++ Sources/WIT/AST.swift | 326 +++ Sources/WIT/ASTVisitor.swift | 214 ++ Sources/WIT/CanonicalABI/CanonicalABI.swift | 385 ++++ .../CanonicalABI/CanonicalDeallocation.swift | 117 ++ .../WIT/CanonicalABI/CanonicalLifting.swift | 192 ++ .../WIT/CanonicalABI/CanonicalLoading.swift | 144 ++ .../WIT/CanonicalABI/CanonicalLowering.swift | 258 +++ .../WIT/CanonicalABI/CanonicalStoring.swift | 146 ++ Sources/WIT/Diagnostics.swift | 66 + Sources/WIT/Lexer.swift | 348 +++ Sources/WIT/PackageResolver.swift | 246 +++ Sources/WIT/Semantics/NameLookup.swift | 339 +++ Sources/WIT/Semantics/PackageBuilder.swift | 32 + Sources/WIT/Semantics/RequestEvaluator.swift | 78 + Sources/WIT/Semantics/SemanticsContext.swift | 11 + Sources/WIT/Semantics/Type.swift | 213 ++ Sources/WIT/Semantics/Validation.swift | 254 +++ Sources/WIT/SyntaxNode.swift | 32 + .../WIT/TextParser/ParseFunctionDecl.swift | 100 + Sources/WIT/TextParser/ParseInterface.swift | 50 + Sources/WIT/TextParser/ParseTop.swift | 224 ++ Sources/WIT/TextParser/ParseTypes.swift | 244 +++ Sources/WIT/TextParser/ParseWorld.swift | 109 + Sources/WIT/TextParser/Parser.swift | 31 + Sources/WITExtractor/Diagnostic.swift | 31 + Sources/WITExtractor/ModuleTranslation.swift | 170 ++ Sources/WITExtractor/Naming/ConvertCase.swift | 157 ++ Sources/WITExtractor/SourcePrinter.swift | 37 + Sources/WITExtractor/SourceSummary.swift | 238 +++ Sources/WITExtractor/SwiftAPIDigester.swift | 143 ++ Sources/WITExtractor/TypeMapping.swift | 160 ++ Sources/WITExtractor/WITBuilder.swift | 137 ++ Sources/WITExtractor/WITExtractor.swift | 76 + .../CanonicalABI/CanonicalName.swift | 33 + .../CanonicalABI/CanonicalOperation.swift | 67 + .../CanonicalABI/SignatureTranslation.swift | 88 + .../StaticCanonicalDeallocation.swift | 42 + .../CanonicalABI/StaticCanonicalLifting.swift | 160 ++ .../CanonicalABI/StaticCanonicalLoading.swift | 59 + .../StaticCanonicalLowering.swift | 205 ++ .../CanonicalABI/StaticCanonicalStoring.swift | 140 ++ .../DefinitionMapping.swift | 119 ++ .../GuestGenerators/GuestExportFunction.swift | 304 +++ .../GuestExportInterface.swift | 64 + .../GuestGenerators/GuestPrelude.swift | 137 ++ .../GuestGenerators/GuestWorldGenerator.swift | 91 + .../HostGenerators/HostExportFunction.swift | 284 +++ .../HostGenerators/HostExportInterface.swift | 53 + .../HostGenerators/HostWorldGenerator.swift | 77 + .../HostGenerators/WasmKitSourcePrinter.swift | 21 + .../Naming/ConvertCase.swift | 34 + .../Naming/SwiftName.swift | 235 +++ .../WITOverlayGenerator/Naming/TypeName.swift | 43 + .../WITOverlayGenerator/SourcePrinter.swift | 50 + .../InterfaceTypeGenerator.swift | 31 + .../TypeGenerators/TypeAccessory.swift | 44 + .../TypeGenerators/TypeDefinition.swift | 221 ++ .../TypeGenerators/TypeGenerator.swift | 19 + .../WITOverlayGenerator/WITOverlayGen.swift | 111 + Sources/WITTool/Utilities.swift | 9 + Sources/WITTool/WITTool.swift | 182 ++ Sources/WasmKit/Component/CanonicalCall.swift | 47 + .../WasmKit/Component/CanonicalLifting.swift | 26 + .../WasmKit/Component/CanonicalLowering.swift | 44 + .../WasmKit/Component/CanonicalOptions.swift | 47 + .../WasmKit/Component/ComponentTypes.swift | 10 + Sources/WasmKit/Docs.docc/Docs.md | 100 + .../Execution/Instructions/Control.swift | 163 ++ .../Execution/Instructions/Expression.swift | 31 + .../Execution/Instructions/Instruction.swift | 33 + .../Execution/Instructions/Memory.swift | 178 ++ .../Execution/Instructions/Numeric.swift | 887 ++++++++ .../Execution/Instructions/Parametric.swift | 27 + .../Execution/Instructions/Reference.swift | 36 + .../Execution/Instructions/Table.swift | 177 ++ .../Execution/Instructions/Variable.swift | 68 + .../Execution/Runtime/GuestMemory.swift | 347 +++ .../Execution/Runtime/NameRegistry.swift | 39 + .../WasmKit/Execution/Runtime/Profiler.swift | 85 + .../WasmKit/Execution/Runtime/Runtime.swift | 372 ++++ Sources/WasmKit/Execution/Runtime/Stack.swift | 218 ++ Sources/WasmKit/Execution/Runtime/Store.swift | 345 +++ Sources/WasmKit/Execution/Types/Errors.swift | 98 + .../WasmKit/Execution/Types/Instances.swift | 198 ++ Sources/WasmKit/Execution/Types/Value.swift | 753 +++++++ .../Parser/Stream/ByteStream.swift | 28 +- .../Parser/Stream/FileHandleStream.swift | 86 + Sources/WasmKit/Parser/Stream/Stream.swift | 26 + .../Parser/Wasm/InstructionCode.swift} | 120 +- Sources/WasmKit/Parser/Wasm/LEB.swift | 83 + Sources/WasmKit/Parser/Wasm/WasmParser.swift | 1330 ++++++++++++ .../Parser/Wasm/WasmTypes.swift | 4 +- Sources/WasmKit/Types/Module.swift | 187 ++ Sources/WasmKit/Types/Types.swift | 83 + Sources/_CabiShims/include/module.modulemap | 3 + Sources/_CabiShims/include/shims.h | 4 + Sources/_CabiShims/shims.c | 1 + Templates/AutoEquatable.stencil | 64 - .../ControlInstructionTests.swift | 276 --- Tests/WAKitTests/Execution/SignedTests.swift | 15 - Tests/WAKitTests/Execution/StackTests.swift | 53 - Tests/WAKitTests/Parser/ByteStreamTests.swift | 46 - Tests/WAKitTests/Parser/WASMParserTests.swift | 628 ------ .../SandboxPrimitives/OpenParentTests.swift | 39 + .../Fixtures/PluginSmokePackage/.gitignore | 8 + .../Fixtures/PluginSmokePackage/Package.swift | 13 + .../PluginSmokeModule/PluginSmoke.swift | 8 + .../PluginSmokeTests.swift | 28 + .../WITExtractorPluginTests/TestSupport.swift | 81 + .../WITExtractorTests/ConvertCaseTests.swift | 15 + .../ExportFunctionTests.swift | 61 + Tests/WITExtractorTests/TestSupport.swift | 128 ++ .../TypeTranslationTests.swift | 270 +++ .../Fixtures/Char/Char.swift | 3 + .../Fixtures/Char/wit/world.wit | 5 + .../Fixtures/Enum/Enum.swift | 13 + .../Fixtures/Enum/wit/world.wit | 47 + .../Fixtures/Flags/Flags.swift | 10 + .../Fixtures/Flags/wit/world.wit | 39 + .../Fixtures/Interface/Interface.swift | 7 + .../Fixtures/Interface/wit/world.wit | 17 + .../Fixtures/List/List.swift | 17 + .../Fixtures/List/wit/world.wit | 8 + .../Fixtures/Naming/Naming.swift | 6 + .../Fixtures/Naming/wit/world.wit | 26 + .../Fixtures/Number/Number.swift | 22 + .../Fixtures/Number/wit/world.wit | 28 + .../Fixtures/Option/Option.swift | 20 + .../Fixtures/Option/wit/world.wit | 14 + .../Fixtures/Record/Record.swift | 18 + .../Fixtures/Record/wit/world.wit | 26 + .../Fixtures/Result/Result.swift | 14 + .../Fixtures/Result/wit/world.wit | 8 + .../Fixtures/Smoke/Smoke.swift | 3 + .../Fixtures/Smoke/wit/world.wit | 5 + .../Fixtures/String/String.swift | 9 + .../Fixtures/String/wit/world.wit | 6 + .../Fixtures/Tuple/Tuple.swift | 3 + .../Fixtures/Tuple/wit/world.wit | 5 + .../Fixtures/Variant/Variant.swift | 25 + .../Fixtures/Variant/wit/world.wit | 48 + .../HostGeneratorTests.swift | 29 + .../Runtime/RuntimeSmokeTests.swift | 12 + .../Runtime/RuntimeTestHarness.swift | 173 ++ .../Runtime/RuntimeTypesTests.swift | 284 +++ Tests/WITTests/LexerTests.swift | 104 + Tests/WITTests/PackageResolverTests.swift | 172 ++ .../WITTests/Semantics/NameLookupTests.swift | 302 +++ .../Semantics/RequestEvaluatorTests.swift | 80 + .../WITTests/Semantics/ValidationTests.swift | 221 ++ .../TextParser/ParseFunctionDeclTests.swift | 62 + .../TextParser/ParseInterfaceTests.swift | 220 ++ Tests/WITTests/TextParser/ParseTopTests.swift | 150 ++ .../WITTests/TextParser/ParseTypesTests.swift | 104 + .../TextParser/ParseVersionTests.swift | 75 + .../WITTests/TextParser/ParseWorldTests.swift | 326 +++ .../Execution/EndiannessTests.swift | 39 + .../ControlInstructionTests.swift | 405 ++++ .../VariableInstructionTests.swift | 91 + Tests/WasmKitTests/Execution/StackTests.swift | 107 + Tests/WasmKitTests/Parser/LEBTests.swift | 114 + Utilities/format.sh | 11 + Vendor/Mint | 1 - Vendor/spec | 1 - Vendor/testsuite | 1 + Vendor/wasi-testsuite | 1 + 234 files changed, 24308 insertions(+), 5223 deletions(-) create mode 100644 .swift-format delete mode 100644 .swift-version create mode 100644 CI/Sources/os-check.sh create mode 100644 CI/check-spectest.sh create mode 100644 CI/check-wasi-testsuite.sh create mode 100644 Documentation/ComponentModel/CanonicalABI.md create mode 100644 Documentation/ComponentModel/SemanticsNotes.md create mode 100644 Examples/wasm/factorial.wat create mode 100644 Examples/wasm/fib.wat create mode 100644 Examples/wasm/host.wat create mode 100644 IntegrationTests/WASI/adapter.py create mode 100644 IntegrationTests/WASI/run-tests.sh create mode 100644 IntegrationTests/WASI/skip.json delete mode 100644 Mintfile create mode 100644 NOTICE.txt create mode 100644 Plugins/GenerateOverlayForTesting/Plugin.swift create mode 100644 Plugins/WITExtractorPlugin/Plugin.swift create mode 100644 Plugins/WITOverlayPlugin/Plugin.swift rename Sources/CLI/{main.swift => CLI.swift} (52%) delete mode 100644 Sources/CLI/Generated/AutoEquatable.generated.swift delete mode 100644 Sources/CLI/SpecTest/Spectest.swift delete mode 100644 Sources/CLI/SpecTest/TestCase.swift create mode 100644 Sources/Spectest/Spectest.swift create mode 100644 Sources/Spectest/TestCase.swift create mode 100644 Sources/SystemExtras/Clock.swift create mode 100644 Sources/SystemExtras/Constants.swift create mode 100644 Sources/SystemExtras/FileAtOperations.swift create mode 100644 Sources/SystemExtras/FileOperations.swift create mode 100644 Sources/SystemExtras/Syscalls.swift create mode 100644 Sources/SystemExtras/Vendor/Exports.swift create mode 100644 Sources/SystemExtras/Vendor/Utils.swift delete mode 100644 Sources/WAKit/Execution/Instructions/Control.swift delete mode 100644 Sources/WAKit/Execution/Instructions/InstructionFactory.swift delete mode 100644 Sources/WAKit/Execution/Instructions/Memory.swift delete mode 100644 Sources/WAKit/Execution/Instructions/Numeric.swift delete mode 100644 Sources/WAKit/Execution/Instructions/Parametric.swift delete mode 100644 Sources/WAKit/Execution/Instructions/Variable.swift delete mode 100644 Sources/WAKit/Execution/Runtime/Runtime.swift delete mode 100644 Sources/WAKit/Execution/Runtime/Stack.swift delete mode 100644 Sources/WAKit/Execution/Runtime/Store.swift delete mode 100644 Sources/WAKit/Execution/Types/Instances.swift delete mode 100644 Sources/WAKit/Execution/Types/Trap.swift delete mode 100644 Sources/WAKit/Execution/Types/Value.swift delete mode 100644 Sources/WAKit/Generated/AutoEquatable.generated.swift delete mode 100644 Sources/WAKit/Parser/Stream/FileHandleStream.swift delete mode 100644 Sources/WAKit/Parser/Stream/Stream.swift delete mode 100644 Sources/WAKit/Parser/Wasm/WasmParser.swift delete mode 100644 Sources/WAKit/Types/Instructions/NumericInstruction.swift delete mode 100644 Sources/WAKit/Types/Module.swift delete mode 100644 Sources/WAKit/Types/Types.swift delete mode 100644 Sources/WAKit/Validation/Validation.swift create mode 100644 Sources/WASI/FileSystem.swift create mode 100644 Sources/WASI/GuestMemorySupport.swift create mode 100644 Sources/WASI/Platform/Directory.swift create mode 100644 Sources/WASI/Platform/Entry.swift create mode 100644 Sources/WASI/Platform/File.swift create mode 100644 Sources/WASI/Platform/PlatformTypes.swift create mode 100644 Sources/WASI/Platform/SandboxPrimitives.swift create mode 100644 Sources/WASI/Platform/SandboxPrimitives/Open.swift create mode 100644 Sources/WASI/Platform/SandboxPrimitives/OpenParent.swift create mode 100644 Sources/WASI/WASI.swift create mode 100644 Sources/WIT/AST.swift create mode 100644 Sources/WIT/ASTVisitor.swift create mode 100644 Sources/WIT/CanonicalABI/CanonicalABI.swift create mode 100644 Sources/WIT/CanonicalABI/CanonicalDeallocation.swift create mode 100644 Sources/WIT/CanonicalABI/CanonicalLifting.swift create mode 100644 Sources/WIT/CanonicalABI/CanonicalLoading.swift create mode 100644 Sources/WIT/CanonicalABI/CanonicalLowering.swift create mode 100644 Sources/WIT/CanonicalABI/CanonicalStoring.swift create mode 100644 Sources/WIT/Diagnostics.swift create mode 100644 Sources/WIT/Lexer.swift create mode 100644 Sources/WIT/PackageResolver.swift create mode 100644 Sources/WIT/Semantics/NameLookup.swift create mode 100644 Sources/WIT/Semantics/PackageBuilder.swift create mode 100644 Sources/WIT/Semantics/RequestEvaluator.swift create mode 100644 Sources/WIT/Semantics/SemanticsContext.swift create mode 100644 Sources/WIT/Semantics/Type.swift create mode 100644 Sources/WIT/Semantics/Validation.swift create mode 100644 Sources/WIT/SyntaxNode.swift create mode 100644 Sources/WIT/TextParser/ParseFunctionDecl.swift create mode 100644 Sources/WIT/TextParser/ParseInterface.swift create mode 100644 Sources/WIT/TextParser/ParseTop.swift create mode 100644 Sources/WIT/TextParser/ParseTypes.swift create mode 100644 Sources/WIT/TextParser/ParseWorld.swift create mode 100644 Sources/WIT/TextParser/Parser.swift create mode 100644 Sources/WITExtractor/Diagnostic.swift create mode 100644 Sources/WITExtractor/ModuleTranslation.swift create mode 100644 Sources/WITExtractor/Naming/ConvertCase.swift create mode 100644 Sources/WITExtractor/SourcePrinter.swift create mode 100644 Sources/WITExtractor/SourceSummary.swift create mode 100644 Sources/WITExtractor/SwiftAPIDigester.swift create mode 100644 Sources/WITExtractor/TypeMapping.swift create mode 100644 Sources/WITExtractor/WITBuilder.swift create mode 100644 Sources/WITExtractor/WITExtractor.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/CanonicalName.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/CanonicalOperation.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/SignatureTranslation.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/StaticCanonicalDeallocation.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/StaticCanonicalLifting.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/StaticCanonicalLoading.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/StaticCanonicalLowering.swift create mode 100644 Sources/WITOverlayGenerator/CanonicalABI/StaticCanonicalStoring.swift create mode 100644 Sources/WITOverlayGenerator/DefinitionMapping.swift create mode 100644 Sources/WITOverlayGenerator/GuestGenerators/GuestExportFunction.swift create mode 100644 Sources/WITOverlayGenerator/GuestGenerators/GuestExportInterface.swift create mode 100644 Sources/WITOverlayGenerator/GuestGenerators/GuestPrelude.swift create mode 100644 Sources/WITOverlayGenerator/GuestGenerators/GuestWorldGenerator.swift create mode 100644 Sources/WITOverlayGenerator/HostGenerators/HostExportFunction.swift create mode 100644 Sources/WITOverlayGenerator/HostGenerators/HostExportInterface.swift create mode 100644 Sources/WITOverlayGenerator/HostGenerators/HostWorldGenerator.swift create mode 100644 Sources/WITOverlayGenerator/HostGenerators/WasmKitSourcePrinter.swift create mode 100644 Sources/WITOverlayGenerator/Naming/ConvertCase.swift create mode 100644 Sources/WITOverlayGenerator/Naming/SwiftName.swift create mode 100644 Sources/WITOverlayGenerator/Naming/TypeName.swift create mode 100644 Sources/WITOverlayGenerator/SourcePrinter.swift create mode 100644 Sources/WITOverlayGenerator/TypeGenerators/InterfaceTypeGenerator.swift create mode 100644 Sources/WITOverlayGenerator/TypeGenerators/TypeAccessory.swift create mode 100644 Sources/WITOverlayGenerator/TypeGenerators/TypeDefinition.swift create mode 100644 Sources/WITOverlayGenerator/TypeGenerators/TypeGenerator.swift create mode 100644 Sources/WITOverlayGenerator/WITOverlayGen.swift create mode 100644 Sources/WITTool/Utilities.swift create mode 100644 Sources/WITTool/WITTool.swift create mode 100644 Sources/WasmKit/Component/CanonicalCall.swift create mode 100644 Sources/WasmKit/Component/CanonicalLifting.swift create mode 100644 Sources/WasmKit/Component/CanonicalLowering.swift create mode 100644 Sources/WasmKit/Component/CanonicalOptions.swift create mode 100644 Sources/WasmKit/Component/ComponentTypes.swift create mode 100644 Sources/WasmKit/Docs.docc/Docs.md create mode 100644 Sources/WasmKit/Execution/Instructions/Control.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Expression.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Instruction.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Memory.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Numeric.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Parametric.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Reference.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Table.swift create mode 100644 Sources/WasmKit/Execution/Instructions/Variable.swift create mode 100644 Sources/WasmKit/Execution/Runtime/GuestMemory.swift create mode 100644 Sources/WasmKit/Execution/Runtime/NameRegistry.swift create mode 100644 Sources/WasmKit/Execution/Runtime/Profiler.swift create mode 100644 Sources/WasmKit/Execution/Runtime/Runtime.swift create mode 100644 Sources/WasmKit/Execution/Runtime/Stack.swift create mode 100644 Sources/WasmKit/Execution/Runtime/Store.swift create mode 100644 Sources/WasmKit/Execution/Types/Errors.swift create mode 100644 Sources/WasmKit/Execution/Types/Instances.swift create mode 100644 Sources/WasmKit/Execution/Types/Value.swift rename Sources/{WAKit => WasmKit}/Parser/Stream/ByteStream.swift (59%) create mode 100644 Sources/WasmKit/Parser/Stream/FileHandleStream.swift create mode 100644 Sources/WasmKit/Parser/Stream/Stream.swift rename Sources/{WAKit/Types/Instructions/Instruction.swift => WasmKit/Parser/Wasm/InstructionCode.swift} (62%) create mode 100644 Sources/WasmKit/Parser/Wasm/LEB.swift create mode 100644 Sources/WasmKit/Parser/Wasm/WasmParser.swift rename Sources/{WAKit => WasmKit}/Parser/Wasm/WasmTypes.swift (85%) create mode 100644 Sources/WasmKit/Types/Module.swift create mode 100644 Sources/WasmKit/Types/Types.swift create mode 100644 Sources/_CabiShims/include/module.modulemap create mode 100644 Sources/_CabiShims/include/shims.h create mode 100644 Sources/_CabiShims/shims.c delete mode 100644 Templates/AutoEquatable.stencil delete mode 100644 Tests/WAKitTests/Execution/Instructions/ControlInstructionTests.swift delete mode 100644 Tests/WAKitTests/Execution/SignedTests.swift delete mode 100644 Tests/WAKitTests/Execution/StackTests.swift delete mode 100644 Tests/WAKitTests/Parser/ByteStreamTests.swift delete mode 100644 Tests/WAKitTests/Parser/WASMParserTests.swift create mode 100644 Tests/WASITests/Platform/SandboxPrimitives/OpenParentTests.swift create mode 100644 Tests/WITExtractorPluginTests/Fixtures/PluginSmokePackage/.gitignore create mode 100644 Tests/WITExtractorPluginTests/Fixtures/PluginSmokePackage/Package.swift create mode 100644 Tests/WITExtractorPluginTests/Fixtures/PluginSmokePackage/Sources/PluginSmokeModule/PluginSmoke.swift create mode 100644 Tests/WITExtractorPluginTests/PluginSmokeTests.swift create mode 100644 Tests/WITExtractorPluginTests/TestSupport.swift create mode 100644 Tests/WITExtractorTests/ConvertCaseTests.swift create mode 100644 Tests/WITExtractorTests/ExportFunctionTests.swift create mode 100644 Tests/WITExtractorTests/TestSupport.swift create mode 100644 Tests/WITExtractorTests/TypeTranslationTests.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Char/Char.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Char/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Enum/Enum.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Enum/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Flags/Flags.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Flags/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Interface/Interface.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Interface/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/List/List.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/List/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Naming/Naming.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Naming/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Number/Number.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Number/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Option/Option.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Option/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Record/Record.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Record/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Result/Result.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Result/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Smoke/Smoke.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Smoke/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/String/String.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/String/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Tuple/Tuple.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Tuple/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Variant/Variant.swift create mode 100644 Tests/WITOverlayGeneratorTests/Fixtures/Variant/wit/world.wit create mode 100644 Tests/WITOverlayGeneratorTests/HostGeneratorTests.swift create mode 100644 Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift create mode 100644 Tests/WITOverlayGeneratorTests/Runtime/RuntimeTestHarness.swift create mode 100644 Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift create mode 100644 Tests/WITTests/LexerTests.swift create mode 100644 Tests/WITTests/PackageResolverTests.swift create mode 100644 Tests/WITTests/Semantics/NameLookupTests.swift create mode 100644 Tests/WITTests/Semantics/RequestEvaluatorTests.swift create mode 100644 Tests/WITTests/Semantics/ValidationTests.swift create mode 100644 Tests/WITTests/TextParser/ParseFunctionDeclTests.swift create mode 100644 Tests/WITTests/TextParser/ParseInterfaceTests.swift create mode 100644 Tests/WITTests/TextParser/ParseTopTests.swift create mode 100644 Tests/WITTests/TextParser/ParseTypesTests.swift create mode 100644 Tests/WITTests/TextParser/ParseVersionTests.swift create mode 100644 Tests/WITTests/TextParser/ParseWorldTests.swift create mode 100644 Tests/WasmKitTests/Execution/EndiannessTests.swift create mode 100644 Tests/WasmKitTests/Execution/Instructions/ControlInstructionTests.swift create mode 100644 Tests/WasmKitTests/Execution/Instructions/VariableInstructionTests.swift create mode 100644 Tests/WasmKitTests/Execution/StackTests.swift create mode 100644 Tests/WasmKitTests/Parser/LEBTests.swift create mode 100644 Utilities/format.sh delete mode 160000 Vendor/Mint delete mode 160000 Vendor/spec create mode 160000 Vendor/testsuite create mode 160000 Vendor/wasi-testsuite diff --git a/.gitignore b/.gitignore index 246c2cc9..d81c094b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ +.DS_Store + .build *.xcodeproj .swiftpm -/Tests/LinuxMain.swift -/Tests/WAKitTests/XCTestManifests.swift - /spectest .vscode + +/Tests/default.json +/Tests/WITOverlayGeneratorTests/Compiled/ +/Tests/WITOverlayGeneratorTests/Generated/ +Tests/WITExtractorPluginTests/Fixtures/*/Package.resolved diff --git a/.gitmodules b/.gitmodules index 3d7706dd..07a15a11 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "Vendor/Mint"] - path = Vendor/Mint - url = https://github.com/yonaskolb/Mint.git -[submodule "Vendor/spec"] - path = Vendor/spec - url = https://github.com/WebAssembly/spec +[submodule "Vendor/testsuite"] + path = Vendor/testsuite + url = https://github.com/WebAssembly/testsuite.git +[submodule "Vendor/wasi-testsuite"] + path = Vendor/wasi-testsuite + url = https://github.com/WebAssembly/wasi-testsuite.git diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..7c0b4d15 --- /dev/null +++ b/.swift-format @@ -0,0 +1,7 @@ +{ + "version": 1, + "lineLength": 200, + "indentation": { + "spaces": 4 + } +} diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 819e07a2..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -5.0 diff --git a/CI/Sources/os-check.sh b/CI/Sources/os-check.sh new file mode 100644 index 00000000..e98f27a9 --- /dev/null +++ b/CI/Sources/os-check.sh @@ -0,0 +1,19 @@ +is_amazonlinux2() { + if [ -f /etc/os-release ]; then + source /etc/os-release + if [ "$ID" == "amzn" ]; then + return 0 + fi + fi + return 1 +} + +is_debian_family() { + if [ -f /etc/os-release ]; then + source /etc/os-release + if [ "$ID_LIKE" == "debian" ]; then + return 0 + fi + fi + return 1 +} diff --git a/CI/check-spectest.sh b/CI/check-spectest.sh new file mode 100644 index 00000000..9e2fc189 --- /dev/null +++ b/CI/check-spectest.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# A CI script to run "make spectest" with wabt installed. +# + +set -eu -o pipefail +source "$(dirname $0)/Sources/os-check.sh" + +install_tools() { + if ! which make curl cmake ninja python3 xz > /dev/null; then + apt update && apt install -y curl build-essential cmake ninja-build python3 xz-utils + fi + + if ! which wat2wasm > /dev/null; then + local build_dir=$(mktemp -d /tmp/WasmKit-wabt.XXXXXX) + mkdir -p $build_dir + curl -L https://github.com/WebAssembly/wabt/releases/download/1.0.33/wabt-1.0.33.tar.xz | tar xJ --strip-components=1 -C $build_dir + cmake -B $build_dir/build -GNinja -DBUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local $build_dir + cmake --build $build_dir/build --target install + fi + + echo "Use wat2wasm $(wat2wasm --version): $(which wat2wasm)" + echo "Use wasm2wat $(wasm2wat --version): $(which wasm2wat)" +} + +# Currently wabt is unavailable in amazonlinux2, so we skip the spectest on it. +if is_amazonlinux2; then + echo "Skip spectest on amazonlinux2" + exit 0 +fi + +set -e + +install_tools + +SOURCE_DIR="$(cd $(dirname $0)/.. && pwd)" +exec make -C $SOURCE_DIR spectest diff --git a/CI/check-wasi-testsuite.sh b/CI/check-wasi-testsuite.sh new file mode 100644 index 00000000..3943374c --- /dev/null +++ b/CI/check-wasi-testsuite.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# +# A CI script to run wasi-testsuite +# + +set -eu -o pipefail +source "$(dirname $0)/Sources/os-check.sh" + +install_tools() { + if is_amazonlinux2; then + amazon-linux-extras install -y python3.8 + ln -s /usr/bin/python3.8 /usr/bin/python3 + elif is_debian_family; then + apt-get update + apt-get install -y python3-pip + else + echo "Unknown OS" + exit 1 + fi +} + +install_tools + +SOURCE_DIR="$(cd $(dirname $0)/.. && pwd)" +( + cd $SOURCE_DIR && \ + python3 -m pip install -r ./Vendor/wasi-testsuite/test-runner/requirements.txt && \ + exec ./IntegrationTests/WASI/run-tests.sh +) diff --git a/Documentation/ComponentModel/CanonicalABI.md b/Documentation/ComponentModel/CanonicalABI.md new file mode 100644 index 00000000..3a86392f --- /dev/null +++ b/Documentation/ComponentModel/CanonicalABI.md @@ -0,0 +1,79 @@ +# Implementation notes on Canonical ABI + +Component model defines a high-level interface between components. The interface defined by the WIT is mapped to the low-level core values and memory operations. The mapping is called the Canonical ABI. +The key idea of the Canonical ABI is to define a set of operations that can be used to translate between the WIT values and the core values. + +Each WIT type has 2 key operations, [lift and lower](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#lifting-and-lowering-values): + +- Lift: Translates core values to a WIT value. +- Lower: Translates a WIT value to core values. + +Considering Component A and B, where A is calling a function exported by B that takes a string and returns an integer, the following diagram shows how the operations are used. + +```mermaid +graph LR; + subgraph CA[Component A] + F1["(string) -> u32"] + + Import["(i32, i32) -> i32"] + F1 --> |"lower"| Import + end + + subgraph CB[Component B] + F2["(string) -> u32"] + Export["(i32, i32) -> i32"] + + Export --> |"lift"| F2 + end + + Import --> |"invoke"| Export +``` + +```mermaid +graph RL; + subgraph CA[Component A] + F1["(string) -> u32"] + + Import["(i32, i32) -> i32"] + Import --> |"lift"| F1 + end + + subgraph CB[Component B] + F2["(string) -> u32"] + Export["(i32, i32) -> i32"] + + F2 --> |"lower"| Export + end + + Export --> |"return"| Import +``` + +## Lifting + +Lifting operation translates core values to a WIT value. It is used when calling a WIT-typed function from a core-typed function, or when returning a WIT value from a core-typed function. The operation can be split into 2 parts: + +1. [Flat Lifting](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flat-lifting): Translates a list of core values to a WIT value. +2. [Loading](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#loading): Reads a WIT value from the memory. + +The loading operation is only used when the value is too large to be passed as a function argument, or too large to be returned from a function. Currently, the number of return value known as [`MAX_FLAT_RESULTS`](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening) is limited to 1 in the core-level signature so that the Canonical ABI can be implemented without the multi-value proposal. + +## Lowering + +Lowering operation translates a WIT value to core values. It is used when calling a core-typed function from a WIT-typed function, or when returning a core value from a WIT-typed function. The operation can be split into 2 parts: + +1. [Flat Lowering](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flat-lowering): Translates a WIT value to a list of core values. +2. [Storing](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#storing): Writes a WIT value to the memory. + +The same as the loading operation, the storing operation is only used when the WIT value is too large to be passed as a function argument, or too large to be returned from a function. + +## Code sharing for static and dynamic operations + +There are 3 places that the Canonical ABI needs to be implemented: + +1. Code Generator: Statically generate the code for lifting and lowering at the Swift level for guest components and host runtime. +2. Host Runtime: Dynamically exchange the WIT values between guest components based on the given WIT definition at runtime. +3. AOT/JIT Compiler: Statically generate the code for lifting and lowering with the given WIT definition at runtime. + +To reduce the code duplication and maintenance cost, WasmKit uses the same ABI modeling code that describes the essential logic of each lifting, lowering, loading, and storing operation in abstract ways. (See [`Sources/WIT/CanonicalABI/`](../../Sources/WIT/CanonicalABI)) + +The ABI modeling code is designed to be used in both static and dynamic contexts. In the static context, each operation is performed at the meta-level, which means the operation is not actually executed but only used to construct the sequence of instructions. In the dynamic context, the operation is actually executed. diff --git a/Documentation/ComponentModel/SemanticsNotes.md b/Documentation/ComponentModel/SemanticsNotes.md new file mode 100644 index 00000000..5a6744a0 --- /dev/null +++ b/Documentation/ComponentModel/SemanticsNotes.md @@ -0,0 +1,84 @@ +# Component Model semantics notes + +## Identifier namespace + +Function and type names in an interface should be unique within the interface. Each interface has its own +namespace. A world has its own namespace for interface and function. An interface defined in a world with +the same name with another interface defined in a package should be distinguished. + +```wit +package ns:pkg +interface iface { + type my-type = u8 +} +world w { + interface ns-pkg-iface { + type my-type = u32 + } +} +``` +In Swift, we can't use kebab-case, `:`, and `/` in identifiers, so we need to transform the identifiers defined in WIT +to PascalCase by replacing `-`, `:`, and `/` and upcase the first letter following those symbols. +Therefore, our overlay code generator cannot accept the above WIT definition, while it conflicts `ns:pkg/iface` +and `ns-pkg-iface`. In the future, we can implement name escaping, but it requires careful transformation, so +we postponed its implementation for now. + +## World + +A World corresponds to a component, in Swift toolchain, a linked WebAssembly binary after wasm-ld. +A component contains only single World. A world can include other worlds, but items in the included Worlds +are flattened into the including World, it doesn't violate single-world rule. + +## Import + +A World can import `interface` and bare functions. +A function imported through `interface` defined in package-level has module name +`my-namespace:my-pkg/my-interface`. The namespace and package names are where the interface +is originally defined. Alias names in top-level use are not used in the import name. +A function imported through `interface` defined in world-level has module name `my-interface`. +A bare function defined directly in world like `import f: func()` has module name `$root`. + +## Resource methods + +A resource method can be defined within a `resource` definition. The Component Model proposal does not +explicitly specifies which component is responsible to provide the resource method definition, but usually a component +that exposes an interface that includes the resource type definition in WIT level is expected to provide the resource +methods. Consider the following example: +``` +package example:http +interface handler { + record header-entry { + key: string, + value: string, + } + resource blob { + constructor(bytes: list) + size: func() -> u32 + } + record message { + body: own, + headers: list, + } + handle: func(request: message) -> message +} +world service { + export handler +} +world middleware { + import handler + export handler +} +``` + +In this case, both `service` and `middleware` components are responsible to provide the following implementations: + +- `example:http/handler#[constructor]blob` +- `example:http/handler#[dtor]blob` +- `example:http/handler#[method]blob.size` +- `example:http/handler#handle` + +A type defined in `handler` interface can be shared between export and import interfaces unless it transitively +uses a `resource` type. In this case, `header-entry` type can be shared, but `message` and `blob` types can't. +This is because each resource type in import and export has its own constructor, destructor, and methods implementations +even though they both have the same raw representation. A `message` passing to or returned from an imported function +should call imported implementations and vice vasa. \ No newline at end of file diff --git a/Examples/wasm/factorial.wat b/Examples/wasm/factorial.wat new file mode 100644 index 00000000..b5139813 --- /dev/null +++ b/Examples/wasm/factorial.wat @@ -0,0 +1,11 @@ + (func $fac (export "fac") (param i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (i64.const 1)) + (else + (i64.mul + (local.get 0) + (call $fac (i64.sub (local.get 0) (i64.const 1))) + ) + ) + ) + ) diff --git a/Examples/wasm/fib.wat b/Examples/wasm/fib.wat new file mode 100644 index 00000000..1746b1b3 --- /dev/null +++ b/Examples/wasm/fib.wat @@ -0,0 +1,46 @@ +(module + (type (;0;) (func)) + (type (;1;) (func (param i32) (result i32))) + (func (;0;) (type 0)) + (func (;1;) (type 1) (param i32) (result i32) + (local i32) + i32.const 1 + local.set 1 + block ;; label = @1 + local.get 0 + i32.const 2 + i32.lt_s + br_if 0 (;@1;) + local.get 0 + i32.const 2 + i32.add + local.set 0 + i32.const 1 + local.set 1 + loop ;; label = @2 + local.get 0 + i32.const -3 + i32.add + call 1 + local.get 1 + i32.add + local.set 1 + local.get 0 + i32.const -2 + i32.add + local.tee 0 + i32.const 3 + i32.gt_s + br_if 0 (;@2;) + end + end + local.get 1) + (table (;0;) 1 1 funcref) + (memory (;0;) 2) + (global (;0;) (mut i32) (i32.const 66560)) + (global (;1;) i32 (i32.const 66560)) + (global (;2;) i32 (i32.const 1024)) + (export "memory" (memory 0)) + (export "__heap_base" (global 1)) + (export "__data_end" (global 2)) + (export "fib" (func 1))) diff --git a/Examples/wasm/host.wat b/Examples/wasm/host.wat new file mode 100644 index 00000000..1a64fd12 --- /dev/null +++ b/Examples/wasm/host.wat @@ -0,0 +1,18 @@ +(module + (global (export "global_i32") i32 (i32.const 666)) + (global (export "global_i64") i64 (i64.const 666)) + (global (export "global_f32") f32 (f32.const 666)) + (global (export "global_f64") f64 (f64.const 666)) + + (table (export "table") 10 20 funcref) + + (memory (export "memory") 1 2) + + (func (export "print")) + (func (export "print_i32") (param i32)) + (func (export "print_i64") (param i64)) + (func (export "print_f32") (param f32)) + (func (export "print_f64") (param f64)) + (func (export "print_i32_f32") (param i32 f32)) + (func (export "print_f64_f64") (param f64 f64)) +) diff --git a/IntegrationTests/WASI/adapter.py b/IntegrationTests/WASI/adapter.py new file mode 100644 index 00000000..70c98f2b --- /dev/null +++ b/IntegrationTests/WASI/adapter.py @@ -0,0 +1,33 @@ +import argparse +import subprocess +import sys +import os +import shlex + +# shlex.split() splits according to shell quoting rules +WASMKIT_CLI = shlex.split(os.getenv("TEST_RUNTIME_EXE", "wasmkit-cli")) + +parser = argparse.ArgumentParser() +parser.add_argument("--version", action="store_true") +parser.add_argument("--test-file", action="store") +parser.add_argument("--arg", action="append", default=[]) +parser.add_argument("--env", action="append", default=[]) +parser.add_argument("--dir", action="append", default=[]) + +args = parser.parse_args() + +if args.version: + print("wasmkit 0.1.0") + sys.exit(0) + +TEST_FILE = args.test_file +PROG_ARGS = args.arg +ENV_ARGS = [j for i in args.env for j in ["--env", i]] +DIR_ARGS = [j for i in args.dir for j in ["--dir", i]] + +# HACK: WasmKit intentionally does not support fd_allocate +if TEST_FILE.endswith("fd_advise.wasm"): + ENV_ARGS += ["--env", "NO_FD_ALLOCATE=1"] + +r = subprocess.run(WASMKIT_CLI + ["run"] + DIR_ARGS + ENV_ARGS + [TEST_FILE] + ["--"] + PROG_ARGS) +sys.exit(r.returncode) diff --git a/IntegrationTests/WASI/run-tests.sh b/IntegrationTests/WASI/run-tests.sh new file mode 100644 index 00000000..4d6a74ff --- /dev/null +++ b/IntegrationTests/WASI/run-tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +SOURCE_DIR="$(cd $(dirname $0)/../.. && pwd)" +DEFAULT_RUNTIME_EXE="$(swift build --show-bin-path)/wasmkit-cli" + +# If custom TEST_RUNTIME_EXE is not set and DEFAULT_RUNTIME_EXE does not exist, build it +if [ -z "$TEST_RUNTIME_EXE" ] && [ ! -f "$DEFAULT_RUNTIME_EXE" ]; then + swift build --product wasmkit-cli +fi + +env TEST_RUNTIME_EXE="${TEST_RUNTIME_EXE:-$DEFAULT_RUNTIME_EXE}" \ + python3 ./Vendor/wasi-testsuite/test-runner/wasi_test_runner.py \ + --test-suite ./Vendor/wasi-testsuite/tests/assemblyscript/testsuite/ Vendor/wasi-testsuite/tests/c/testsuite/ Vendor/wasi-testsuite/tests/rust/testsuite/ \ + --runtime-adapter IntegrationTests/WASI/adapter.py \ + --exclude-filter ./IntegrationTests/WASI/skip.json $@ diff --git a/IntegrationTests/WASI/skip.json b/IntegrationTests/WASI/skip.json new file mode 100644 index 00000000..7e691c68 --- /dev/null +++ b/IntegrationTests/WASI/skip.json @@ -0,0 +1,8 @@ +{ + "WASI Assemblyscript tests": { + }, + "WASI C tests": { + }, + "WASI Rust tests": { + } +} diff --git a/Makefile b/Makefile index 9be8b8f3..d84f74a8 100644 --- a/Makefile +++ b/Makefile @@ -1,62 +1,44 @@ -NAME := WAKit - -MINT = swift run --package-path Vendor/Mint mint -SWIFTFORMAT = $(MINT) run swiftformat swiftformat -SOURCERY = $(MINT) run sourcery sourcery +NAME := WasmKit MODULES = $(notdir $(wildcard Sources/*)) -TEMPLATES = $(wildcard Templates/*.stencil) -GENERATED_DIRS = $(foreach MODULE, $(MODULES), Sources/$(MODULE)/Generated) - -SWIFT_VERSION = $(shell cat .swift-version) .PHONY: all -all: bootstrap project build - -.PHONY: bootstrap -bootstrap: - $(MINT) bootstrap - -.PHONY: project -project: update generate $(NAME).xcodeproj - -$(NAME).xcodeproj: Package.swift FORCE - @swift package generate-xcodeproj \ - --enable-code-coverage +all: build .PHONY: build build: @swift build .PHONY: test -test: linuxmain +test: @swift test -WAST_ROOT = Vendor/spec/test/core +.PHONY: docs +docs: + swift package generate-documentation --target WasmKit + +WAST_ROOT = Vendor/testsuite SPECTEST_ROOT = ./spectest -WAST_FILES = $(wildcard $(WAST_ROOT)/*.wast) +WAST_FILES = $(wildcard $(WAST_ROOT)/*.wast) $(wildcard $(WAST_ROOT)/proposals/memory64/*.wast) JSON_FILES = $(WAST_FILES:$(WAST_ROOT)/%.wast=$(SPECTEST_ROOT)/%.json) .PHONY: spec -spec: $(JSON_FILES) +spec: $(JSON_FILES) $(SPECTEST_ROOT)/host.wasm + +$(SPECTEST_ROOT)/host.wasm: ./Examples/wasm/host.wat + wat2wasm ./Examples/wasm/host.wat -o $(SPECTEST_ROOT)/host.wasm $(SPECTEST_ROOT)/%.json: $(WAST_ROOT)/%.wast - @mkdir -p $(SPECTEST_ROOT) - @echo $^ -> $@ + @mkdir -p $(@D) wast2json $^ -o $@ .PHONY: spectest spectest: spec - swift run wakit spectest $(SPECTEST_ROOT) --exclude fac,forward - -.PHONY: format -format: - $(SWIFTFORMAT) --swiftversion $(SWIFT_VERSION) Sources Tests --exclude **/Generated + swift run Spectest $(SPECTEST_ROOT) .PHONY: clean clean: @swift package clean - @$(RM) -r ./$(NAME).xcodeproj .PHONY: update update: @@ -65,15 +47,6 @@ update: .PHONY: generate generate: $(GENERATED_DIRS) -Sources/%/Generated: FORCE - $(SOURCERY) \ - --sources $(dir $@) \ - --templates $(TEMPLATES) \ - --output $@ - -linuxmain: FORCE - @swift test --generate-linuxmain - GIT_STATUS = $(shell git status --porcelain) ensure_clean: @[ -z "$(GIT_STATUS)" ] \ diff --git a/Mintfile b/Mintfile deleted file mode 100644 index 12cab579..00000000 --- a/Mintfile +++ /dev/null @@ -1,2 +0,0 @@ -nicklockwood/SwiftFormat@0.39.4 -krzysztofzablocki/Sourcery@0.16.0 diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..125af121 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,15 @@ +------------------------------------------------------------------------------- +This product contains a derivation of the utility code from Swift System. + + * LICENSE (Apache License 2.0): + * https://www.apache.org/licenses/LICENSE-2.0 + * HOMEPAGE: + * https://github.com/apple/swift-system + +------------------------------------------------------------------------------- +This product contains a derivation of the list of Swift keywords from Swift Syntax. + + * LICENSE (Apache License 2.0): + * https://www.apache.org/licenses/LICENSE-2.0 + * HOMEPAGE: + * https://github.com/apple/swift-syntax diff --git a/Package.resolved b/Package.resolved index c6303948..73200ce3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,52 +1,77 @@ { - "object": { - "pins": [ - { - "package": "Rainbow", - "repositoryURL": "https://github.com/onevcat/Rainbow", - "state": { - "branch": null, - "revision": "797a68d0a642609424b08f11eb56974a54d5f6e2", - "version": "3.1.4" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser", - "state": { - "branch": null, - "revision": "e1465042f195f374b94f915ba8ca49de24300a0d", - "version": "1.0.2" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", - "version": "1.2.0" - } - }, - { - "package": "swift-system", - "repositoryURL": "https://github.com/apple/swift-system", - "state": { - "branch": null, - "revision": "025bcb1165deab2e20d4eaba79967ce73013f496", - "version": "1.2.1" - } - }, - { - "package": "SwiftLEB", - "repositoryURL": "https://github.com/akkyie/SwiftLEB", - "state": { - "branch": null, - "revision": "200c21588c0126c2d4cab89f2776ee5296a3b90a", - "version": "0.1.1" - } - } - ] - }, - "version": 1 + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", + "version" : "1.2.2" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-format", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-format.git", + "state" : { + "revision" : "fbfe1869527923dd9f9b2edac148baccfce0dce7", + "version" : "508.0.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", + "version" : "1.5.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "2c49d66d34dfd6f8130afdba889de77504b58ec0", + "version" : "508.0.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system", + "state" : { + "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core.git", + "state" : { + "revision" : "3b13e439a341bbbfe0f710c7d1be37221745ef1a", + "version" : "0.6.1" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index e2acd16f..17ffe20c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,45 +1,119 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.8 import PackageDescription let package = Package( - name: "WAKit", + name: "WasmKit", + platforms: [.macOS(.v12)], products: [ .library( - name: "WAKit", - targets: ["WAKit"] + name: "WasmKit", + targets: ["WasmKit"] + ), + .library( + name: "WASI", + targets: ["WASI"] + ), + .library( + name: "WIT", targets: ["WIT"] ), .executable( - name: "wakit", + name: "wasmkit-cli", targets: ["CLI"] ), + .library(name: "_CabiShims", targets: ["_CabiShims"]), + .plugin(name: "WITOverlayPlugin", targets: ["WITOverlayPlugin"]), + .plugin(name: "WITExtractorPlugin", targets: ["WITExtractorPlugin"]), ], dependencies: [ - .package(url: "https://github.com/akkyie/SwiftLEB", from: "0.1.0"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.2"), - .package(url: "https://github.com/onevcat/Rainbow", from: "3.1.4"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-system", from: "1.2.1"), + .package(url: "https://github.com/apple/swift-system", .upToNextMinor(from: "1.1.1")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-format.git", from: "508.0.1"), ], targets: [ + .executableTarget( + name: "CLI", + dependencies: [ + "WasmKit", + "WASI", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log"), + .product(name: "SystemPackage", package: "swift-system"), + ] + ), .target( - name: "WAKit", - dependencies: [.product(name: "LEB", package: "SwiftLEB")] + name: "WasmKit", + dependencies: [ + "SystemExtras", + .product(name: "SystemPackage", package: "swift-system"), + ] ), - .testTarget( - name: "WAKitTests", - dependencies: ["WAKit"] + .target( + name: "WASI", + dependencies: ["WasmKit", "SystemExtras"] ), .target( - name: "CLI", + name: "SystemExtras", + dependencies: [ + .product(name: "SystemPackage", package: "swift-system") + ]), + .executableTarget( + name: "Spectest", dependencies: [ - "WAKit", + "WasmKit", .product(name: "ArgumentParser", package: "swift-argument-parser"), - "Rainbow", .product(name: "Logging", package: "swift-log"), - .product(name: "SystemPackage", package: "swift-system") + .product(name: "SystemPackage", package: "swift-system"), + ] + ), + .target(name: "WIT"), + .testTarget(name: "WITTests", dependencies: ["WIT"]), + .target(name: "WITOverlayGenerator", dependencies: ["WIT"]), + .target(name: "_CabiShims"), + .plugin(name: "WITOverlayPlugin", capability: .buildTool(), dependencies: ["WITTool"]), + .plugin(name: "GenerateOverlayForTesting", capability: .buildTool(), dependencies: ["WITTool"]), + .testTarget( + name: "WITOverlayGeneratorTests", + dependencies: ["WITOverlayGenerator", "WasmKit", "WASI"], + exclude: ["Fixtures", "Compiled", "Generated"], + plugins: [.plugin(name: "GenerateOverlayForTesting")] + ), + .target(name: "WITExtractor"), + .testTarget( + name: "WITExtractorTests", + dependencies: ["WITExtractor", "WIT"] + ), + .plugin( + name: "WITExtractorPlugin", + capability: .command( + intent: .custom(verb: "extract-wit", description: "Extract WIT definition from Swift module"), + permissions: [] + ), + dependencies: ["WITTool"] + ), + .testTarget( + name: "WITExtractorPluginTests", + exclude: ["Fixtures"] + ), + .executableTarget( + name: "WITTool", + dependencies: [ + "WIT", + "WITOverlayGenerator", + "WITExtractor", + .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), + .testTarget( + name: "WasmKitTests", + dependencies: ["WasmKit"] + ), + .testTarget( + name: "WASITests", + dependencies: ["WASI"] + ), ], swiftLanguageVersions: [.v5] ) diff --git a/Plugins/GenerateOverlayForTesting/Plugin.swift b/Plugins/GenerateOverlayForTesting/Plugin.swift new file mode 100644 index 00000000..1551672a --- /dev/null +++ b/Plugins/GenerateOverlayForTesting/Plugin.swift @@ -0,0 +1,38 @@ +import PackagePlugin +import Foundation + +@main +struct Plugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let witTool = try context.tool(named: "WITTool").path + let fixturesDir = target.directory.appending("Fixtures") + let hostOverlayDir = context.pluginWorkDirectory.appending("GeneratedHostOverlay") + return try FileManager.default.contentsOfDirectory(atPath: fixturesDir.string).compactMap { singleFixture in + let outputFile = hostOverlayDir.appending(singleFixture + "HostOverlay.swift") + let inputFileDir = fixturesDir.appending(singleFixture, "wit") + guard FileManager.default.isDirectory(filePath: inputFileDir.string) else { return nil } + + let inputFiles = try FileManager.default.subpathsOfDirectory(atPath: inputFileDir.string).map { + inputFileDir.appending(subpath: $0) + } + return Command.buildCommand( + displayName: "Generating host overlay for \(singleFixture)", + executable: witTool, + arguments: [ + "generate-overlay", "--target", "host", + inputFileDir, "-o", outputFile + ], + inputFiles: inputFiles, + outputFiles: [outputFile] + ) + } + } +} + +extension FileManager { + internal func isDirectory(filePath: String) -> Bool { + var isDirectory: ObjCBool = false + let exists = self.fileExists(atPath: filePath, isDirectory: &isDirectory) + return exists && isDirectory.boolValue + } +} diff --git a/Plugins/WITExtractorPlugin/Plugin.swift b/Plugins/WITExtractorPlugin/Plugin.swift new file mode 100644 index 00000000..69dcb840 --- /dev/null +++ b/Plugins/WITExtractorPlugin/Plugin.swift @@ -0,0 +1,75 @@ +import PackagePlugin +import Foundation + +@main +struct Plugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + var argumentExtractor = ArgumentExtractor(arguments) + let targets = argumentExtractor.extractOption(named: "target") + let sdk = argumentExtractor.extractOption(named: "sdk").last + let parameters = PackageManager.BuildParameters() + for target in targets { + try extractFromTarget(target: target, sdk: sdk, parameters: parameters, context: context) + } + } + + func extractFromTarget( + target: String, + sdk: String?, + parameters: PackageManager.BuildParameters, + context: PluginContext + ) throws { + let buildResult = try packageManager.build(.target(target), parameters: parameters) + guard buildResult.succeeded else { + throw PluginError(description: "Failed to build \(target): \(buildResult.logText)") + } + // FIXME: Don't assume build directory to be ".build" because it can be configured --scratch-path + let dataPath = context.package.directory.appending([".build"]) + let buildPath = dataPath.appending([parameters.configuration.rawValue]) + let llbuildManifest = dataPath.appending([parameters.configuration.rawValue + ".yaml"]) + guard let swiftcExecutable = ProcessInfo.processInfo.environment["WIT_EXTRACTOR_SWIFTC_PATH"] + ?? inferSwiftcExecutablePath(llbuildManifest: llbuildManifest) else { + throw PluginError(description: "Cloudn't infer `swiftc` command path from build directory. Please specify WIT_EXTRACTOR_SWIFTC_PATH") + } + let digesterExecutable = Path(swiftcExecutable).removingLastComponent().appending(["swift-api-digester"]) + let tool = try context.tool(named: "WITTool") + var arguments = [ + "extract-wit", + "--swift-api-digester", digesterExecutable.string, + "--module-name", target, + "--package-name", context.package.displayName, + "-I", buildPath.string + ] + if let sdk { + arguments += ["-sdk", sdk] + } + let process = try Process.run(URL(fileURLWithPath: tool.path.string), arguments: arguments) + process.waitUntilExit() + guard process.terminationStatus == 0 else { + throw PluginError( + description: "Failed to run \(([tool.path.string] + arguments).joined(separator: " "))" + ) + } + } + + func inferSwiftcExecutablePath(llbuildManifest: Path) -> String? { + // FIXME: This is completely not the right way but there is no right way for now... + guard let contents = try? String(contentsOfFile: llbuildManifest.string, encoding: .utf8) else { + return nil + } + for line in contents.split(separator: "\n") { + let prefix = " executable: \"" + if line.hasPrefix(prefix), line.hasSuffix("/swiftc\"") { + let pathStart = line.index(line.startIndex, offsetBy: prefix.count) + let pathEnd = line.index(before: line.endIndex) + let executablePath = line[pathStart.. [Command] { + if !target.recursiveTargetDependencies.contains(where: { $0.name == "_CabiShims" }) { + Diagnostics.emit(.error, "\"_CabiShims\" must be included as a dependency") + } + let witTool = try context.tool(named: "WITTool").path + let witDir = target.directory.appending("wit") + let inputFiles = try FileManager.default.subpathsOfDirectory(atPath: witDir.string).map { + witDir.appending(subpath: $0) + } + let outputFile = context.pluginWorkDirectory.appending("GeneratedOverlay", "\(target.name)Overlay.swift") + let command = Command.buildCommand( + displayName: "Generating WIT overlay for \(target.name)", + executable: witTool, + arguments: [ + "generate-overlay", "--target", "guest", + witDir, "-o", outputFile + ], + inputFiles: inputFiles, + outputFiles: [outputFile] + ) + return [command] + } +} + +extension FileManager { + internal func isDirectory(filePath: String) -> Bool { + var isDirectory: ObjCBool = false + let exists = self.fileExists(atPath: filePath, isDirectory: &isDirectory) + return exists && isDirectory.boolValue + } +} diff --git a/README.md b/README.md index 131fdb30..6bfa7af9 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,20 @@ - WAKit Icon - -# WAKit +# WasmKit -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/akkyie/WAKit/Build%20and%20test) +A WebAssembly runtime written in Swift. A fork of [an open-source project licensed under MIT](https://github.com/swiftwasm/WAKit). Originally developed and maintained by [@akkyie](https://github.com/akkyie). -A WebAssembly runtime written in Swift. Originally developed and maintained by [@akkyie](https://github.com/akkyie). +Implements all of WebAssembly 2.0 binary parsing and execution core spec, with an exclusion of SIMD instructions. The validation and text format parts of the spec are not implemented yet. -🚧 Highly experimental. Do not expect to work. +It also has rudimentary support for [WASI](https://wasi.dev) with only a few WASI imports implemented currently, with a goal of eventual full support for `wasi_snapshot_preview1`. See `WASI` module for more details. ## Usage ### Command Line Tool ```sh -$ swift build # or prefix `swift run` before the command below -$ # Usage: wakit run [] ... -$ wakit run Examples/wasm/fib.wasm fib i32:10 +$ # Usage: wasmkit-cli run [] ... +$ swift run wasmkit-cli run Examples/wasm/fib.wasm fib i32:10 [I32(89)] ``` @@ -25,22 +22,12 @@ $ wakit run Examples/wasm/fib.wasm fib i32:10 #### Swift Package Manager -```swift -dependencies: [ - .package(url: "https://github.com/swiftwasm/WAKit", .branch("main")), -], -``` - -## Development +Add the URL of this repository to your `Package.swift` manifest. Then add the `WasmKit` library product as dependency to the target you'd like to use it with. -```sh -$ make bootstrap # Install tools through Mint -$ make generate # Run Sourcery to generate source code from templates -$ make build # or `swift build` -``` +## Testing To run the core spec test suite run this: ```sh -$ make spectest # Prepare core spec tests and check their assertions with WAKit +$ make spectest # Prepare core spec tests and check their assertions with WasmKit ``` diff --git a/Sources/CLI/main.swift b/Sources/CLI/CLI.swift similarity index 52% rename from Sources/CLI/main.swift rename to Sources/CLI/CLI.swift index 68594fe4..1729fa0c 100644 --- a/Sources/CLI/main.swift +++ b/Sources/CLI/CLI.swift @@ -1,11 +1,11 @@ import ArgumentParser -struct CLI: ParsableCommand { +@main +struct CLI: AsyncParsableCommand { static let configuration = CommandConfiguration( - commandName: "wakit", + commandName: "wasmkit", abstract: "WebAssembly Runtime written in Swift.", - subcommands: [Run.self, Spectest.self] + version: "0.0.1", + subcommands: [Run.self] ) } - -CLI.main() diff --git a/Sources/CLI/Generated/AutoEquatable.generated.swift b/Sources/CLI/Generated/AutoEquatable.generated.swift deleted file mode 100644 index ffd7e76b..00000000 --- a/Sources/CLI/Generated/AutoEquatable.generated.swift +++ /dev/null @@ -1,28 +0,0 @@ -// Generated using Sourcery 0.16.0 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT - -// swiftlint:disable file_length -fileprivate func compareOptionals(lhs: T?, rhs: T?, compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool { - switch (lhs, rhs) { - case let (lValue?, rValue?): - return compare(lValue, rValue) - case (nil, nil): - return true - default: - return false - } -} - -fileprivate func compareArrays(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool { - guard lhs.count == rhs.count else { return false } - for (idx, lhsItem) in lhs.enumerated() { - guard compare(lhsItem, rhs[idx]) else { return false } - } - - return true -} - - -// MARK: - AutoEquatable for classes, protocols, structs - -// MARK: - AutoEquatable for Enums diff --git a/Sources/CLI/Run/Run.swift b/Sources/CLI/Run/Run.swift index 87a56140..a0e3fa45 100644 --- a/Sources/CLI/Run/Run.swift +++ b/Sources/CLI/Run/Run.swift @@ -1,22 +1,46 @@ import ArgumentParser import Foundation import Logging -import WAKit +import SystemPackage +import WASI +import WasmKit -private let logger = Logger(label: "com.WAKit.CLI") +private let logger = Logger(label: "com.WasmKit.CLI") struct Run: ParsableCommand { @Flag var verbose = false - @Argument - var path: String + @Option + var profileOutput: String? + + struct EnvOption: ExpressibleByArgument { + let key: String + let value: String + init?(argument: String) { + var parts = argument.split(separator: "=", maxSplits: 2).makeIterator() + guard let key = parts.next(), let value = parts.next() else { return nil } + self.key = String(key) + self.value = String(value) + } + } + + @Option( + name: .customLong("env"), + help: ArgumentHelp( + "Pass an environment variable to the WASI program", + valueName: "key=value" + )) + var environment: [EnvOption] = [] + + @Option(name: .customLong("dir"), help: "Grant access to the given host directory") + var directories: [String] = [] @Argument - var functionName: String + var path: String @Argument - var arguments: [String] + var arguments: [String] = [] func run() throws { LoggingSystem.bootstrap { @@ -25,24 +49,76 @@ struct Run: ParsableCommand { return handler } - guard let fileHandle = FileHandle(forReadingAtPath: path) else { - logger.error("File \"\(path)\" could not be opened") - return + logger.info("Started parsing module") + + let module: Module + if verbose { + let (parsedModule, parseTime) = try measure { + try parseWasm(filePath: FilePath(path)) + } + logger.info("Finished parsing module: \(parseTime)") + module = parsedModule + } else { + module = try parseWasm(filePath: FilePath(path)) + } + + let interceptor = try deriveInterceptor() + defer { interceptor?.finalize() } + + let invoke: () throws -> Void + if module.exports.contains(where: { $0.name == "_start" }) { + invoke = try instantiateWASI(module: module, interceptor: interceptor?.interceptor) + } else { + guard let entry = try instantiateNonWASI(module: module, interceptor: interceptor?.interceptor) else { + return + } + invoke = entry } - defer { fileHandle.closeFile() } - let stream = FileHandleStream(fileHandle: fileHandle) + let (_, invokeTime) = try measure(execution: invoke) - logger.info("Started to parse module") + logger.info("Finished invoking function \"\(path)\": \(invokeTime)") + } - let (module, parseTime) = try measure(if: verbose) { - try WasmParser.parse(stream: stream) + func deriveInterceptor() throws -> (interceptor: GuestTimeProfiler, finalize: () -> Void)? { + guard let outputPath = self.profileOutput else { return nil } + FileManager.default.createFile(atPath: outputPath, contents: nil) + let fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: outputPath)) + let profiler = GuestTimeProfiler { data in + try? fileHandle.write(contentsOf: data) } + return ( + profiler, + { + profiler.finalize() + try! fileHandle.synchronize() + try! fileHandle.close() + + print("\nProfile Completed: \(outputPath) can be viewed using https://ui.perfetto.dev/") + } + ) + } - logger.info("Ended to parse module: \(parseTime)") + func instantiateWASI(module: Module, interceptor: RuntimeInterceptor?) throws -> () throws -> Void { + // Flatten environment variables into a dictionary (Respect the last value if a key is duplicated) + let environment = environment.reduce(into: [String: String]()) { + $0[$1.key] = $1.value + } + let preopens = directories.reduce(into: [String: String]()) { + $0[$1] = $1 + } + let wasi = try WASIBridgeToHost(args: [path] + arguments, environment: environment, preopens: preopens) + let runtime = Runtime(hostModules: wasi.hostModules, interceptor: interceptor) + let moduleInstance = try runtime.instantiate(module: module) + return { + let exitCode = try wasi.start(moduleInstance, runtime: runtime) + throw ExitCode(Int32(exitCode)) + } + } - let runtime = Runtime() - let moduleInstance = try runtime.instantiate(module: module, externalValues: []) + func instantiateNonWASI(module: Module, interceptor: RuntimeInterceptor?) throws -> (() throws -> Void)? { + var arguments = arguments + let functionName = arguments.popLast() var parameters: [Value] = [] for argument in arguments { @@ -52,26 +128,27 @@ struct Run: ParsableCommand { switch type { case "i32": parameter = Value(signed: Int32(value)!) case "i64": parameter = Value(signed: Int64(value)!) - case "f32": parameter = .f32(Float(value)!) - case "f64": parameter = .f64(Double(value)!) + case "f32": parameter = .f32(Float32(value)!.bitPattern) + case "f64": parameter = .f64(Float64(value)!.bitPattern) default: fatalError("unknown type") } parameters.append(parameter) } - - logger.info("Started invoking function \"\(functionName)\" with parameters: \(parameters)") - - let (results, invokeTime) = try measure(if: verbose) { - try runtime.invoke(moduleInstance, function: functionName, with: parameters) + guard let functionName else { + logger.error("No function specified to run in a given module.") + return nil } - logger.info("Ended invoking function \"\(functionName)\": \(invokeTime)") - - print(results.description) + let runtime = Runtime(interceptor: interceptor) + let moduleInstance = try runtime.instantiate(module: module) + return { + logger.info("Started invoking function \"\(functionName)\" with parameters: \(parameters)") + let results = try runtime.invoke(moduleInstance, function: functionName, with: parameters) + print(results.description) + } } func measure( - if _: @autoclosure () -> Bool, execution: () throws -> Result ) rethrows -> (Result, String) { let start = DispatchTime.now() diff --git a/Sources/CLI/SpecTest/Spectest.swift b/Sources/CLI/SpecTest/Spectest.swift deleted file mode 100644 index 94e459e1..00000000 --- a/Sources/CLI/SpecTest/Spectest.swift +++ /dev/null @@ -1,55 +0,0 @@ -import ArgumentParser -import Foundation -import SystemPackage - -struct Spectest: ParsableCommand { - @Argument - var path: String - - @Option - var include: String? - - @Option - var exclude: String? - - @Flag - var verbose = false - - func run() throws { - let include = self.include.flatMap { $0.split(separator: ",").map(String.init) } ?? [] - let exclude = self.exclude.flatMap { $0.split(separator: ",").map(String.init) } ?? [] - - let testCases: [TestCase] - do { - testCases = try TestCase.load(include: include, exclude: exclude, in: path) - } catch { - fatalError("failed to load test: \(error)") - } - - let rootPath: String - let filePath = FilePath(path) - if (try? FileDescriptor.open(filePath, FileDescriptor.AccessMode.readOnly, options: .directory)) != nil { - rootPath = path - } else { - rootPath = URL(fileURLWithPath: path).deletingLastPathComponent().path - } - - var results = [Result]() - for testCase in testCases { - testCase.run(rootPath: rootPath) { testCase, command, result in - switch result { - case let .failed(reason): - print("\(testCase.sourceFilename):\(command.line):", result.banner, reason) - case let .skipped(reason): - if verbose { print("\(testCase.sourceFilename):\(command.line):", result.banner, reason) } - default: - print("\(testCase.sourceFilename):\(command.line):", result.banner) - } - results.append(result) - } - } - - let passingCount = results.filter { if case .passed = $0 { return true } else { return false} }.count - print("\(passingCount)/\(results.count) \(Int(Double(passingCount)/Double(results.count) * 100))% passing") - } -} diff --git a/Sources/CLI/SpecTest/TestCase.swift b/Sources/CLI/SpecTest/TestCase.swift deleted file mode 100644 index df27e4cd..00000000 --- a/Sources/CLI/SpecTest/TestCase.swift +++ /dev/null @@ -1,339 +0,0 @@ -import Foundation -import LEB -import Rainbow -import SystemPackage -import WAKit - -struct TestCase: Decodable { - enum Error: Swift.Error { - case invalidPath - } - struct Command: Decodable { - enum CommandType: String, Decodable { - case module - case action - case register - case assertReturn = "assert_return" - case assertInvalid = "assert_invalid" - case assertTrap = "assert_trap" - case assertMalformed = "assert_malformed" - case assertExhaustion = "assert_exhaustion" - case assertUnlinkable = "assert_unlinkable" - case assertUninstantiable = "assert_uninstantiable" - case assertReturnCanonicalNan = "assert_return_canonical_nan" - case assertReturnArithmeticNan = "assert_return_arithmetic_nan" - } - - enum ModuleType: String, Decodable { - case binary - case text - } - - enum ValueType: String, Decodable { - case i32 - case i64 - case f32 - case f64 - case externref - case funcref - } - - struct Value: Decodable { - let type: ValueType - let value: String - } - - struct Expectation: Decodable { - let type: ValueType - let value: String? - } - - struct Action: Decodable { - enum ActionType: String, Decodable { - case invoke - case get - } - - let type: ActionType - let field: String - let args: [Value]? - } - - let type: CommandType - let line: Int - let filename: String? - let text: String? - let moduleType: ModuleType? - let action: Action? - let expected: [Expectation]? - } - - let sourceFilename: String - let commands: [Command] - - static func load(include: [String], exclude: [String], in path: String) throws -> [TestCase] { - let fileManager = FileManager.default - let filePath = FilePath(path) - let dirPath: String - let filePaths: [String] - if (try? FileDescriptor.open(filePath, FileDescriptor.AccessMode.readOnly, options: .directory)) != nil { - dirPath = path - filePaths = try fileManager.contentsOfDirectory(atPath: path).filter { $0.hasSuffix("json") }.sorted() - } else if fileManager.isReadableFile(atPath: path) { - let url = URL(fileURLWithPath: path) - dirPath = url.deletingLastPathComponent().path - filePaths = [url.lastPathComponent] - } else { - throw Error.invalidPath - } - - guard !filePaths.isEmpty else { - return [] - } - - let matchesPattern: (String) throws -> Bool = { filePath in - let filePath = filePath.hasSuffix(".json") ? String(filePath.dropLast(".json".count)) : filePath - guard !exclude.contains(filePath) else { return false } - guard !include.isEmpty else { return true } - return include.contains(filePath) - } - - let jsonDecoder = JSONDecoder() - jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase - - var testCases: [TestCase] = [] - for filePath in filePaths where try matchesPattern(filePath) { - print("loading \(filePath)") - guard let data = fileManager.contents(atPath: dirPath + "/" + filePath) else { - assertionFailure("failed to load \(filePath)") - continue - } - - let spec = try jsonDecoder.decode(TestCase.self, from: data) - testCases.append(spec) - } - - return testCases - } -} - -enum Result { - case passed - case failed(String) - case skipped(String) - case `internal`(Swift.Error) - - var banner: String { - switch self { - case .passed: return "[PASSED]".green - case .failed: return "[FAILED]".red - case .skipped: return "[SKIPPED]".blue - case .internal: return "[INTERNAL]".white.onRed - } - } -} - -extension TestCase { - func run(rootPath: String, handler: @escaping (TestCase, TestCase.Command, Result) -> Void) { - let runtime = Runtime() - var currentModuleInstance: ModuleInstance? - let queue = DispatchQueue(label: "sh.aky.WAKit.spectest") - let semaphore = DispatchSemaphore(value: 0) - for command in commands { - queue.async { - command.run(runtime: runtime, module: ¤tModuleInstance, rootPath: rootPath) { command, result in - handler(self, command, result) - semaphore.signal() - } - } - - guard semaphore.wait(timeout: .now() + 5) != .timedOut else { - semaphore.resume() - return handler(self, command, .failed("timed out")) - } - } - } -} - -extension TestCase.Command { - func run(runtime: Runtime, module currentModuleInstance: inout ModuleInstance?, rootPath: String, handler: (TestCase.Command, Result) -> Void) { - guard moduleType != .text else { - return handler(self, .skipped("module type is text")) - } - - switch type { - case .module: - currentModuleInstance = nil - - guard let filename = filename else { - return handler(self, .skipped("type is \(type), but no filename specified")) - } - - let module: Module - do { - module = try parseModule(rootPath: rootPath, filename: filename) - } catch { - return handler(self, .failed("module could not be parsed: \(error)")) - } - - do { - currentModuleInstance = try runtime.instantiate(module: module, externalValues: []) - } catch { - return handler(self, .failed("module could not be instantiated: \(error)")) - } - - return handler(self, .passed) - case .assertMalformed: - currentModuleInstance = nil - - guard let filename = filename else { - return handler(self, .skipped("type is \(type), but no filename specified")) - } - - var error: Error? - do { - _ = try parseModule(rootPath: rootPath, filename: filename) - } catch let e { - error = e - } - - guard let e = error, e.text == text else { - return handler(self, .failed("module should not be parsed: expected \"\(text ?? "null")\"")) - } - return handler(self, .passed) - - case .assertReturn: - guard let action = action else { - return handler(self, .failed("type is \(type), but no action specified")) - } - guard let moduleInstance = currentModuleInstance else { - return handler(self, .failed("type is \(type), but no current module")) - } - guard case .invoke = action.type else { - return handler(self, .failed("action type \(action.type) has been not implemented for \(type.rawValue)")) - } - - let args = parseValues(args: action.args!) - let expected = parseValues(args: self.expected ?? []) - let result: [WAKit.Value] - do { - result = try runtime.invoke(moduleInstance, function: action.field, with: args) - } catch { - return handler(self, .failed("\(error)")) - } - guard result.isTestEquivalent(to: expected) else { - return handler(self, .failed("result mismatch: expected: \(expected), actual: \(result)")) - } - return handler(self, .passed) - - case .assertTrap: - guard let action = action else { - return handler(self, .failed("type is \(type), but no action specified")) - } - guard let moduleInstance = currentModuleInstance else { - return handler(self, .failed("type is \(type), but no current module")) - } - guard case .invoke = action.type else { - return handler(self, .failed("action type \(action.type) has been not implemented for \(type.rawValue)")) - } - - let args = parseValues(args: action.args!) - do { - _ = try runtime.invoke(moduleInstance, function: action.field, with: args) - } catch let trap as Trap { - guard trap.assertionText == text else { - return handler(self, .failed("assertion mismatch: expected: \(text!), actual: \(trap.assertionText)")) - } - } catch { - return handler(self, .failed("\(error)")) - } - return handler(self, .passed) - - default: - return handler(self, .failed("type \(type) has been not implemented")) - } - } - - private func parseModule(rootPath: String, filename: String) throws -> Module { - let url = URL(fileURLWithPath: rootPath).appendingPathComponent(filename) - let fileHandle = try FileHandle(forReadingFrom: url) - defer { fileHandle.closeFile() } - - let stream = FileHandleStream(fileHandle: fileHandle) - - let module = try WasmParser.parse(stream: stream) - return module - } - - private func parseValues(args: [TestCase.Command.Value]) -> [WAKit.Value] { - return args.map { - switch $0.type { - case .i32: return .i32(UInt32($0.value)!) - case .i64: return .i64(UInt64($0.value)!) - case .f32 where $0.value.starts(with: "nan:"): return .f32(Float32.nan) - case .f32: return .f32(Float32(bitPattern: UInt32($0.value)!)) - case .f64 where $0.value.starts(with: "nan:"): return .f64(Float64.nan) - case .f64: return .f64(Float64(bitPattern: UInt64($0.value)!)) - case .externref: - fatalError("externref is not currently supported") - case .funcref: - fatalError("funcref is not currently supported") - } - } - } - - private func parseValues(args: [TestCase.Command.Expectation]) -> [WAKit.Value] { - return args.compactMap { - switch $0.type { - case .i32: return .i32(UInt32($0.value!)!) - case .i64: return .i64(UInt64($0.value!)!) - case .f32 where $0.value!.starts(with: "nan:"): return .f32(Float32.nan) - case .f32: return .f32(Float32(bitPattern: UInt32($0.value!)!)) - case .f64 where $0.value!.starts(with: "nan:"): return .f64(Float64.nan) - case .f64: return .f64(Float64(bitPattern: UInt64($0.value!)!)) - case .externref: - fatalError("externref is not currently supported") - case .funcref: - fatalError("funcref is not currently supported") - } - } - } -} - -extension Swift.Error { - var text: String { - if let error = self as? StreamError { - switch error { - case .unexpectedEnd(expected: _): - return "unexpected end" - default: break - } - } - - if let error = self as? WasmParserError { - switch error { - case .invalidMagicNumber: - return "magic header not detected" - case .unknownVersion: - return "unknown binary version" - case .invalidUTF8: - return "invalid UTF-8 encoding" - case .zeroExpected: - return "zero flag expected" - case .inconsistentFunctionAndCodeLength: - return "function and code section have inconsistent lengths" - default: break - } - } - - if let error = self as? LEBError { - switch error { - case .overflow: - return "integer too large" - default: break - } - } - - return "unknown error" - } -} diff --git a/Sources/Spectest/Spectest.swift b/Sources/Spectest/Spectest.swift new file mode 100644 index 00000000..ad32fcd5 --- /dev/null +++ b/Sources/Spectest/Spectest.swift @@ -0,0 +1,120 @@ +import ArgumentParser +import Foundation +import Logging +import SystemPackage +import WasmKit + +@main +struct Spectest: AsyncParsableCommand { + @Argument + var path: String + + @Option + var include: String? + + @Option + var exclude: String? + + @Flag + var verbose = false + + @Flag(inversion: .prefixedNo) + var parallel: Bool = true + + func run() async throws { + LoggingSystem.bootstrap { + var handler = StreamLogHandler.standardOutput(label: $0) + handler.logLevel = verbose ? .info : .warning + return handler + } + + let logger = Logger(label: "com.WasmKit.CLI.Spectest") + + let include = self.include.flatMap { $0.split(separator: ",").map(String.init) } ?? [] + let exclude = self.exclude.flatMap { $0.split(separator: ",").map(String.init) } ?? [] + + let testCases: [TestCase] + do { + testCases = try TestCase.load(include: include, exclude: exclude, in: path, logger: logger) + } catch { + fatalError("failed to load test: \(error)") + } + + let rootPath: String + let filePath = FilePath(path) + if (try? FileDescriptor.open(filePath, FileDescriptor.AccessMode.readOnly, options: .directory)) != nil { + rootPath = path + } else { + rootPath = URL(fileURLWithPath: path).deletingLastPathComponent().path + } + + // https://github.com/WebAssembly/spec/tree/8a352708cffeb71206ca49a0f743bdc57269fb1a/interpreter#spectest-host-module + let hostModulePath = FilePath(rootPath).appending("host.wasm") + let hostModule = try parseWasm(filePath: hostModulePath) + + @Sendable func runTestCase(testCase: TestCase) throws -> [Result] { + var testCaseResults = [Result]() + try testCase.run(spectestModule: hostModule) { testCase, command, result in + switch result { + case let .failed(reason): + logger.warning("\(testCase.content.sourceFilename):\(command.line): \(result.banner) \(reason)") + case let .skipped(reason): + logger.info("\(testCase.content.sourceFilename):\(command.line): \(result.banner) \(reason)") + case .passed: + logger.info("\(testCase.content.sourceFilename):\(command.line): \(result.banner)") + default: + logger.warning("\(testCase.content.sourceFilename):\(command.line): \(result.banner)") + } + testCaseResults.append(result) + } + + return testCaseResults + } + + let results: [Result] + + if parallel { + results = try await withThrowingTaskGroup(of: [Result].self) { group in + for testCase in testCases { + group.addTask { + try await Task { try runTestCase(testCase: testCase) }.value + } + } + + var results = [Result]() + for try await testCaseResults in group { + results.append(contentsOf: testCaseResults) + } + + return results + } + } else { + results = try testCases.flatMap { try runTestCase(testCase: $0) } + } + + let passingCount = results.filter { if case .passed = $0 { return true } else { return false } }.count + let skippedCount = results.filter { if case .skipped = $0 { return true } else { return false } }.count + let failedCount = results.filter { if case .failed = $0 { return true } else { return false } }.count + + print( + """ + \(passingCount)/\(results.count) (\( + percentage(passingCount, results.count) + ) passing, \( + percentage(skippedCount, results.count) + ) skipped, \( + percentage(failedCount, results.count) + ) failed) + """ + ) + + // Exit with non-zero status when there is any failure + if failedCount > 0 { + throw ArgumentParser.ExitCode(1) + } + } + + private func percentage(_ numerator: Int, _ denominator: Int) -> String { + "\(Int(Double(numerator) / Double(denominator) * 100))%" + } +} diff --git a/Sources/Spectest/TestCase.swift b/Sources/Spectest/TestCase.swift new file mode 100644 index 00000000..f9923c98 --- /dev/null +++ b/Sources/Spectest/TestCase.swift @@ -0,0 +1,568 @@ +import Foundation +import Logging +import SystemPackage +import WasmKit + +struct TestCase { + enum Error: Swift.Error { + case invalidPath + } + + struct Command: Decodable { + enum CommandType: String, Decodable { + case module + case action + case register + case assertReturn = "assert_return" + case assertInvalid = "assert_invalid" + case assertTrap = "assert_trap" + case assertMalformed = "assert_malformed" + case assertExhaustion = "assert_exhaustion" + case assertUnlinkable = "assert_unlinkable" + case assertUninstantiable = "assert_uninstantiable" + case assertReturnCanonicalNan = "assert_return_canonical_nan" + case assertReturnArithmeticNan = "assert_return_arithmetic_nan" + } + + enum ModuleType: String, Decodable { + case binary + case text + } + + enum ValueType: String, Decodable { + case i32 + case i64 + case f32 + case f64 + case externref + case funcref + } + + struct Value: Decodable { + let type: ValueType + let value: String + } + + struct Expectation: Decodable { + let type: ValueType + let value: String? + } + + struct Action: Decodable { + enum ActionType: String, Decodable { + case invoke + case get + } + + let type: ActionType + let module: String? + let field: String + let args: [Value]? + } + + let type: CommandType + let line: Int + let `as`: String? + let name: String? + let filename: String? + let text: String? + let moduleType: ModuleType? + let action: Action? + let expected: [Expectation]? + } + + struct Content: Decodable { + let sourceFilename: String + let commands: [Command] + } + + let content: Content + let path: String + + private static func isDirectory(_ path: FilePath) -> Bool { + let fd = try? FileDescriptor.open(path, FileDescriptor.AccessMode.readOnly, options: .directory) + let isDirectory = fd != nil + try? fd?.close() + return isDirectory + } + + static func load(include: [String], exclude: [String], in path: String, logger: Logger? = nil) throws -> [TestCase] { + let fileManager = FileManager.default + let filePath = FilePath(path) + let dirPath: String + let filePaths: [String] + if isDirectory(filePath) { + dirPath = path + filePaths = try self.computeTestSources(inDirectory: filePath, fileManager: fileManager) + } else if fileManager.isReadableFile(atPath: path) { + let url = URL(fileURLWithPath: path) + dirPath = url.deletingLastPathComponent().path + filePaths = [url.lastPathComponent] + } else { + throw Error.invalidPath + } + + guard !filePaths.isEmpty else { + return [] + } + + let matchesPattern: (String) throws -> Bool = { filePath in + // FIXME: Skip names.wast until we have .wat/.wast parser + // "names.wast" contains BOM in some test cases and they are parsed + // as empty string in JSONDecoder because there is no way to express + // it in UTF-8. + guard filePath != "names.json" else { return false } + // FIXME: Skip SIMD proposal tests for now + guard !filePath.starts(with: "simd_") else { return false } + + let filePath = filePath.hasSuffix(".json") ? String(filePath.dropLast(".json".count)) : filePath + guard !exclude.contains(filePath) else { return false } + guard !include.isEmpty else { return true } + return include.contains(filePath) + } + + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + + var testCases: [TestCase] = [] + for filePath in filePaths where try matchesPattern(filePath) { + logger?.info("loading \(filePath)") + let path = dirPath + "/" + filePath + guard let data = fileManager.contents(atPath: path) else { + assertionFailure("failed to load \(filePath)") + continue + } + + let content = try jsonDecoder.decode(TestCase.Content.self, from: data) + let spec = TestCase(content: content, path: path) + testCases.append(spec) + } + + return testCases + } + + /// Returns list of `.json` paths recursively found under `rootPath`. They are relative to `rootPath`. + static func computeTestSources(inDirectory rootPath: FilePath, fileManager: FileManager) throws -> [String] { + var queue: [String] = [rootPath.string] + var contents: [String] = [] + + while let dirPath = queue.popLast() { + let dirContents = try fileManager.contentsOfDirectory(atPath: dirPath) + contents += dirContents.filter { $0.hasSuffix(".json") }.map { dirPath + "/" + $0 } + queue += dirContents.filter { isDirectory(FilePath(dirPath + "/" + $0)) }.map { dirPath + "/" + $0 } + } + + return contents.map { String($0.dropFirst(rootPath.string.count + 1)) } + } +} + +enum Result { + case passed + case failed(String) + case skipped(String) + case `internal`(Swift.Error) + + var banner: String { + switch self { + case .passed: + return "[PASSED]" + case .failed: + return "[FAILED]" + case .skipped: + return "[SKIPPED]" + case .internal: + return "[INTERNAL]" + } + } +} + +extension TestCase { + func run(spectestModule: Module, handler: @escaping (TestCase, TestCase.Command, Result) -> Void) throws { + let runtime = Runtime() + let hostModuleInstance = try runtime.instantiate(module: spectestModule) + + try runtime.store.register(hostModuleInstance, as: "spectest") + + var currentModuleInstance: ModuleInstance? + let rootPath = FilePath(path).removingLastComponent().string + for command in content.commands { + command.run( + runtime: runtime, + module: ¤tModuleInstance, + rootPath: rootPath + ) { command, result in + handler(self, command, result) + } + assert(runtime.isStackEmpty) + } + } +} + +extension TestCase.Command { + func run( + runtime: Runtime, + module currentModuleInstance: inout ModuleInstance?, + rootPath: String, + handler: (TestCase.Command, Result) -> Void + ) { + guard moduleType != .text else { + return handler(self, .skipped("module type is text")) + } + + switch type { + case .module: + currentModuleInstance = nil + + guard let filename else { + return handler(self, .skipped("type is \(type), but no filename specified")) + } + + let module: Module + do { + module = try parseModule(rootPath: rootPath, filename: filename) + } catch { + return handler(self, .failed("module could not be parsed: \(error)")) + } + + do { + currentModuleInstance = try runtime.instantiate(module: module, name: name) + } catch { + return handler(self, .failed("module could not be instantiated: \(error)")) + } + + return handler(self, .passed) + + case .register: + guard let name = `as`, let currentModuleInstance else { + fatalError("`register` command without a module name") + } + + do { + try runtime.store.register(currentModuleInstance, as: name) + } catch { + return handler(self, .failed("module could not be registered: \(error)")) + } + + case .assertMalformed: + currentModuleInstance = nil + + guard let filename else { + return handler(self, .skipped("type is \(type), but no filename specified")) + } + + do { + var module = try parseModule(rootPath: rootPath, filename: filename) + // Materialize all functions to see all errors in the module + try module.materializeAll() + } catch { + return handler(self, .passed) + } + return handler(self, .failed("module should not be parsed: expected \"\(text ?? "null")\"")) + + case .assertUninstantiable: + currentModuleInstance = nil + + guard let filename else { + return handler(self, .skipped("type is \(type), but no filename specified")) + } + + let module: Module + do { + module = try parseModule(rootPath: rootPath, filename: filename) + } catch { + return handler(self, .failed("module could not be parsed: \(error)")) + } + + do { + _ = try runtime.instantiate(module: module) + } catch let error as InstantiationError { + guard error.assertionText == text else { + return handler(self, .failed("assertion mismatch: expected: \(text!), actual: \(error.assertionText)")) + } + } catch let error as Trap { + guard error.assertionText == text else { + return handler(self, .failed("assertion mismatch: expected: \(text!), actual: \(error.assertionText)")) + } + } catch { + return handler(self, .failed("\(error)")) + } + return handler(self, .passed) + + case .assertReturn: + guard let action else { + return handler(self, .failed("type is \(type), but no action specified")) + } + + let moduleInstance: ModuleInstance? + if let name = action.module { + moduleInstance = runtime.store.namedModuleInstances[name] + } else { + moduleInstance = currentModuleInstance + } + + guard let moduleInstance else { + return handler(self, .failed("type is \(type), but no current module")) + } + + let expected = parseValues(args: self.expected ?? []) + + switch action.type { + case .invoke: + let args = parseValues(args: action.args!) + let result: [WasmKit.Value] + do { + result = try runtime.invoke(moduleInstance, function: action.field, with: args) + } catch { + return handler(self, .failed("\(error)")) + } + guard result.isTestEquivalent(to: expected) else { + return handler(self, .failed("invoke result mismatch: expected: \(expected), actual: \(result)")) + } + return handler(self, .passed) + + case .get: + let result: WasmKit.Value + do { + result = try runtime.getGlobal(moduleInstance, globalName: action.field) + } catch { + return handler(self, .failed("\(error)")) + } + guard result.isTestEquivalent(to: expected[0]) else { + return handler(self, .failed("get result mismatch: expected: \(expected), actual: \(result)")) + } + return handler(self, .passed) + } + + case .assertTrap, .assertExhaustion: + guard let action else { + return handler(self, .failed("type is \(type), but no action specified")) + } + let moduleInstance: ModuleInstance? + if let name = action.module { + moduleInstance = runtime.store.namedModuleInstances[name] + } else { + moduleInstance = currentModuleInstance + } + + guard let moduleInstance else { + return handler(self, .failed("type is \(type), but no current module")) + } + guard case .invoke = action.type else { + return handler(self, .failed("action type \(action.type) has been not implemented for \(type.rawValue)")) + } + + let args = parseValues(args: action.args!) + do { + _ = try runtime.invoke(moduleInstance, function: action.field, with: args) + } catch let trap as Trap { + if let text { + guard trap.assertionText.contains(text) else { + return handler(self, .failed("assertion mismatch: expected: \(text), actual: \(trap.assertionText)")) + } + } + } catch { + return handler(self, .failed("\(error)")) + } + return handler(self, .passed) + + case .assertUnlinkable: + currentModuleInstance = nil + + guard let filename else { + return handler(self, .skipped("type is \(type), but no filename specified")) + } + + let module: Module + do { + module = try parseModule(rootPath: rootPath, filename: filename) + } catch { + return handler(self, .failed("module could not be parsed: \(error)")) + } + + do { + _ = try runtime.instantiate(module: module) + } catch let error as ImportError { + guard error.assertionText == text else { + return handler(self, .failed("assertion mismatch: expected: \(text!), actual: \(error.assertionText)")) + } + } catch { + return handler(self, .failed("\(error)")) + } + return handler(self, .passed) + + case .assertInvalid: + return handler(self, .skipped("validation is no implemented yet")) + + case .action: + guard let action else { + return handler(self, .failed("type is \(type), but no action specified")) + } + + guard let currentModuleInstance else { + return handler(self, .failed("type is \(type), but no current module")) + } + + guard case .invoke = action.type else { + return handler(self, .failed("action type \(action.type) is not implemented for \(type.rawValue)")) + } + + let args = parseValues(args: action.args!) + + do { + _ = try runtime.invoke(currentModuleInstance, function: action.field, with: args) + } catch { + return handler(self, .failed("\(error)")) + } + return handler(self, .passed) + + default: + return handler(self, .failed("type \(type) is not implemented yet")) + } + } + + private func deriveFeatureSet(rootPath: String) -> WasmFeatureSet { + var features = WasmFeatureSet.default + if rootPath.hasSuffix("/proposals/memory64") { + features.insert(.memory64) + // memory64 doesn't expect reference-types proposal + // and it depends on the fact reference-types is disabled + features.remove(.referenceTypes) + } + return features + } + + private func parseModule(rootPath: String, filename: String) throws -> Module { + let url = URL(fileURLWithPath: rootPath).appendingPathComponent(filename) + + let module = try parseWasm(filePath: FilePath(url.path), features: deriveFeatureSet(rootPath: rootPath)) + return module + } + + private func parseValues(args: [TestCase.Command.Value]) -> [WasmKit.Value] { + return args.map { + switch $0.type { + case .i32: return .i32(UInt32($0.value)!) + case .i64: return .i64(UInt64($0.value)!) + case .f32 where $0.value.starts(with: "nan:"): return .f32(Float32.nan.bitPattern) + case .f32: return .f32(UInt32($0.value)!) + case .f64 where $0.value.starts(with: "nan:"): return .f64(Float64.nan.bitPattern) + case .f64: return .f64(UInt64($0.value)!) + case .externref: + return .ref(.extern(ExternAddress($0.value))) + case .funcref: + return .ref(.function(FunctionAddress($0.value))) + } + } + } + + private func parseValues(args: [TestCase.Command.Expectation]) -> [WasmKit.Value] { + return args.compactMap { + switch $0.type { + case .i32: return .i32(UInt32($0.value!)!) + case .i64: return .i64(UInt64($0.value!)!) + case .f32 where $0.value!.starts(with: "nan:"): return .f32(Float32.nan.bitPattern) + case .f32: return .f32(UInt32($0.value!)!) + case .f64 where $0.value!.starts(with: "nan:"): return .f64(Float64.nan.bitPattern) + case .f64: return .f64(UInt64($0.value!)!) + case .externref: + return .ref(.extern(ExternAddress($0.value!))) + case .funcref: + return .ref(.function(FunctionAddress($0.value!))) + } + } + } +} + +extension Value { + func isTestEquivalent(to value: Self) -> Bool { + switch (self, value) { + case let (.i32(lhs), .i32(rhs)): + return lhs == rhs + case let (.i64(lhs), .i64(rhs)): + return lhs == rhs + case let (.f32(lhs), .f32(rhs)): + let lhs = Float32(bitPattern: lhs) + let rhs = Float32(bitPattern: rhs) + return lhs.isNaN && rhs.isNaN || lhs == rhs + case let (.f64(lhs), .f64(rhs)): + let lhs = Float64(bitPattern: lhs) + let rhs = Float64(bitPattern: rhs) + return lhs.isNaN && rhs.isNaN || lhs == rhs + case let (.ref(.extern(lhs)), .ref(.extern(rhs))): + return lhs == rhs + case let (.ref(.function(lhs)), .ref(.function(rhs))): + return lhs == rhs + default: + return false + } + } +} + +extension Array where Element == Value { + func isTestEquivalent(to arrayOfValues: Self) -> Bool { + guard count == arrayOfValues.count else { + return false + } + + for (i, value) in enumerated() { + if !value.isTestEquivalent(to: arrayOfValues[i]) { + return false + } + } + + return true + } +} + +extension Swift.Error { + var text: String { + if let error = self as? WasmParserError { + switch error { + case .invalidMagicNumber: + return "magic header not detected" + case .unknownVersion: + return "unknown binary version" + case .invalidUTF8: + return "malformed UTF-8 encoding" + case .zeroExpected: + return "zero byte expected" + case .inconsistentFunctionAndCodeLength: + return "function and code section have inconsistent lengths" + case .tooManyLocals: + return "too many locals" + case .invalidSectionSize: + // XXX: trailing "unexpected end" is just for making spectest happy + // The reference interpreter raises EOF error when the custom content + // size is negative[^1], and custom.wast contains a test case that depends + // on the behavior[^2]. + // [^1]: https://github.com/WebAssembly/spec/blob/653938a88c6f40eb886d5980ca315136eb861d03/interpreter/binary/decode.ml#L20 + // [^2]: https://github.com/WebAssembly/spec/blob/653938a88c6f40eb886d5980ca315136eb861d03/test/core/custom.wast#L76-L82 + return "invalid section size, unexpected end" + case .malformedSectionID: + return "malformed section id" + case .endOpcodeExpected: + return "END opcode expected" + case .unexpectedEnd: + return "unexpected end of section or function" + case .inconsistentDataCountAndDataSectionLength: + return "data count and data section have inconsistent lengths" + case .expectedRefType: + return "malformed reference type" + case .unexpectedContent: + return "unexpected content after last section" + case .sectionSizeMismatch: + return "section size mismatch" + case .illegalOpcode: + return "illegal opcode" + case .malformedMutability: + return "malformed mutability" + case .integerRepresentationTooLong: + return "integer representation too long" + default: + return String(describing: error) + } + } + + return "unknown error: \(self)" + } +} diff --git a/Sources/SystemExtras/Clock.swift b/Sources/SystemExtras/Clock.swift new file mode 100644 index 00000000..470c451b --- /dev/null +++ b/Sources/SystemExtras/Clock.swift @@ -0,0 +1,112 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import CSystem +import Glibc +#elseif os(Windows) +import CSystem +import ucrt +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +@frozen +public struct Clock: RawRepresentable { + + @_alwaysEmitIntoClient + public var rawValue: CInterop.ClockId + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.ClockId) { self.rawValue = rawValue } +} + +extension Clock { + #if os(Linux) + @_alwaysEmitIntoClient + public static var boottime: Clock { Clock(rawValue: CLOCK_BOOTTIME) } + #endif + + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + @_alwaysEmitIntoClient + public static var rawMonotonic: Clock { Clock(rawValue: _CLOCK_MONOTONIC_RAW) } + #endif + + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(Linux) || os(OpenBSD) || os(FreeBSD) || os(WASI) + @_alwaysEmitIntoClient + public static var monotonic: Clock { Clock(rawValue: _CLOCK_MONOTONIC) } + #endif + + #if os(OpenBSD) || os(FreeBSD) || os(WASI) + @_alwaysEmitIntoClient + public static var uptime: Clock { Clock(rawValue: _CLOCK_UPTIME) } + #endif + + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + @_alwaysEmitIntoClient + public static var rawUptime: Clock { Clock(rawValue: _CLOCK_UPTIME_RAW) } + #endif + + @_alwaysEmitIntoClient + public func currentTime() throws -> Clock.TimeSpec { + try _currentTime().get() + } + + @usableFromInline + internal func _currentTime() -> Result { + var timeSpec = CInterop.TimeSpec() + return nothingOrErrno(retryOnInterrupt: false) { + system_clock_gettime(self.rawValue, &timeSpec) + } + .map { Clock.TimeSpec(rawValue: timeSpec) } + } + + @_alwaysEmitIntoClient + public func resolution() throws -> Clock.TimeSpec { + try _currentTime().get() + } + + @usableFromInline + internal func _resolution() -> Result { + var timeSpec = CInterop.TimeSpec() + return nothingOrErrno(retryOnInterrupt: false) { + system_clock_getres(self.rawValue, &timeSpec) + } + .map { Clock.TimeSpec(rawValue: timeSpec) } + } +} + +extension Clock { + @frozen + public struct TimeSpec: RawRepresentable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.TimeSpec + + @_alwaysEmitIntoClient + public var seconds: Int { rawValue.tv_sec } + + @_alwaysEmitIntoClient + public var nanoseconds: Int { rawValue.tv_nsec } + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.TimeSpec) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public init(seconds: Int, nanoseconds: Int) { + self.init(rawValue: CInterop.TimeSpec(tv_sec: seconds, tv_nsec: nanoseconds)) + } + + @_alwaysEmitIntoClient + public static var now: TimeSpec { + return TimeSpec(rawValue: CInterop.TimeSpec(tv_sec: 0, tv_nsec: Int(_UTIME_NOW))) + } + + @_alwaysEmitIntoClient + public static var omit: TimeSpec { + return TimeSpec(rawValue: CInterop.TimeSpec(tv_sec: 0, tv_nsec: Int(_UTIME_OMIT))) + } + } +} diff --git a/Sources/SystemExtras/Constants.swift b/Sources/SystemExtras/Constants.swift new file mode 100644 index 00000000..c44c5d9b --- /dev/null +++ b/Sources/SystemExtras/Constants.swift @@ -0,0 +1,128 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import CSystem +import Glibc +#elseif os(Windows) +import CSystem +import ucrt +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +@_alwaysEmitIntoClient +internal var _AT_EACCESS: CInt { AT_EACCESS } +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_NOFOLLOW } +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_FOLLOW: CInt { AT_SYMLINK_FOLLOW } +@_alwaysEmitIntoClient +internal var _AT_REMOVEDIR: CInt { AT_REMOVEDIR } +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _AT_REALDEV: CInt { AT_REALDEV } +@_alwaysEmitIntoClient +internal var _AT_FDONLY: CInt { AT_FDONLY } +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } +#endif +/* FIXME: Disabled until CSystem will include "linux/fcntl.h" +#if os(Linux) +@_alwaysEmitIntoClient +internal var _AT_NO_AUTOMOUNT: CInt { AT_NO_AUTOMOUNT } +#endif +*/ + +@_alwaysEmitIntoClient +internal var _F_GETFL: CInt { F_GETFL } +@_alwaysEmitIntoClient +internal var _O_DSYNC: CInt { O_DSYNC } +@_alwaysEmitIntoClient +internal var _O_SYNC: CInt { O_SYNC } +#if os(Linux) +@_alwaysEmitIntoClient +internal var _O_RSYNC: CInt { O_RSYNC } +#endif + +@_alwaysEmitIntoClient +internal var _UTIME_NOW: CInt { + #if os(Linux) + // Hard-code constants because it's defined in glibc in a form that + // ClangImporter cannot interpret as constants. + // https://github.com/torvalds/linux/blob/92901222f83d988617aee37680cb29e1a743b5e4/include/linux/stat.h#L15 + return ((1 << 30) - 1) + #else + return UTIME_NOW + #endif +} +@_alwaysEmitIntoClient +internal var _UTIME_OMIT: CInt { + #if os(Linux) + // Hard-code constants because it's defined in glibc in a form that + // ClangImporter cannot interpret as constants. + // https://github.com/torvalds/linux/blob/92901222f83d988617aee37680cb29e1a743b5e4/include/linux/stat.h#L16 + return ((1 << 30) - 2) + #else + return UTIME_OMIT + #endif +} + +@_alwaysEmitIntoClient +internal var _DT_UNKNOWN: CInt { CInt(DT_UNKNOWN) } +@_alwaysEmitIntoClient +internal var _DT_FIFO: CInt { CInt(DT_FIFO) } +@_alwaysEmitIntoClient +internal var _DT_CHR: CInt { CInt(DT_CHR) } +@_alwaysEmitIntoClient +internal var _DT_DIR: CInt { CInt(DT_DIR) } +@_alwaysEmitIntoClient +internal var _DT_BLK: CInt { CInt(DT_BLK) } +@_alwaysEmitIntoClient +internal var _DT_REG: CInt { CInt(DT_REG) } +@_alwaysEmitIntoClient +internal var _DT_LNK: CInt { CInt(DT_LNK) } +@_alwaysEmitIntoClient +internal var _DT_SOCK: CInt { CInt(DT_SOCK) } +@_alwaysEmitIntoClient +internal var _DT_WHT: CInt { CInt(DT_WHT) } + +@_alwaysEmitIntoClient +internal var _S_IFMT: CInterop.Mode { S_IFMT } +@_alwaysEmitIntoClient +internal var _S_IFIFO: CInterop.Mode { S_IFIFO } +@_alwaysEmitIntoClient +internal var _S_IFCHR: CInterop.Mode { S_IFCHR } +@_alwaysEmitIntoClient +internal var _S_IFDIR: CInterop.Mode { S_IFDIR } +@_alwaysEmitIntoClient +internal var _S_IFBLK: CInterop.Mode { S_IFBLK } +@_alwaysEmitIntoClient +internal var _S_IFREG: CInterop.Mode { S_IFREG } +@_alwaysEmitIntoClient +internal var _S_IFLNK: CInterop.Mode { S_IFLNK } +@_alwaysEmitIntoClient +internal var _S_IFSOCK: CInterop.Mode { S_IFSOCK } + +#if os(Linux) +@_alwaysEmitIntoClient +internal var _CLOCK_BOOTTIME: CInterop.ClockId { CLOCK_BOOTTIME } +#endif +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _CLOCK_MONOTONIC_RAW: CInterop.ClockId { CLOCK_MONOTONIC_RAW } +#endif +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(Linux) || os(OpenBSD) || os(FreeBSD) || os(WASI) +@_alwaysEmitIntoClient +internal var _CLOCK_MONOTONIC: CInterop.ClockId { CLOCK_MONOTONIC } +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _CLOCK_UPTIME_RAW: CInterop.ClockId { CLOCK_UPTIME_RAW } +#endif +#if os(OpenBSD) || os(FreeBSD) || os(WASI) +@_alwaysEmitIntoClient +internal var _CLOCK_UPTIME: CInterop.ClockId { CLOCK_UPTIME } +#endif diff --git a/Sources/SystemExtras/FileAtOperations.swift b/Sources/SystemExtras/FileAtOperations.swift new file mode 100644 index 00000000..d7dbfa3f --- /dev/null +++ b/Sources/SystemExtras/FileAtOperations.swift @@ -0,0 +1,290 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +import ucrt +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +extension FileDescriptor { + /// Options for use with `*at` functions like `openAt` + /// Each function defines which option values are valid with it + @frozen + public struct AtOptions: OptionSet { + /// The raw C options. + @_alwaysEmitIntoClient + public var rawValue: Int32 + + /// Create a strongly-typed options value from raw C options. + @_alwaysEmitIntoClient + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// Indicates the operation does't follow symlinks + /// + /// If you specify this option and the file you pass to + /// + /// is a symbolic link, then it returns information about the link itself. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW`. + @_alwaysEmitIntoClient + public static var noFollow: AtOptions { AtOptions(rawValue: _AT_SYMLINK_NOFOLLOW) } + + /* FIXME: Disabled until CSystem will include "linux/fcntl.h" + #if os(Linux) + /// Indicates the operation does't mount the basename component automatically + /// + /// If you specify this option and the file you pass to + /// + /// is a auto-mount point, it does't mount the directory even if it's an auto-mount point. + /// + /// The corresponding C constant is `AT_NO_AUTOMOUNT`. + @_alwaysEmitIntoClient + public static var noAutomount: AtOptions { AtOptions(rawValue: _AT_NO_AUTOMOUNT)} + #endif + */ + + /// Indicates the operation removes directory + /// + /// If you specify this option and the file path you pass to + /// + /// is not a directory, then that remove operation fails. + /// + /// The corresponding C constant is `AT_REMOVEDIR`. + @_alwaysEmitIntoClient + public static var removeDirectory: AtOptions { AtOptions(rawValue: _AT_REMOVEDIR) } + } + + /// Opens or creates a file relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the file to open. + /// - mode: The read and write access to use. + /// - options: The behavior for opening the file. + /// - permissions: The file permissions to use for created files. + /// - retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A file descriptor for the open file + /// + /// The corresponding C function is `openat`. + @_alwaysEmitIntoClient + public func open( + at path: FilePath, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions? = nil, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + try path.withPlatformString { + try open( + at: $0, mode, options: options, permissions: permissions, retryOnInterrupt: retryOnInterrupt) + } + } + + /// Opens or creates a file relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the file to open. + /// - mode: The read and write access to use. + /// - options: The behavior for opening the file. + /// - permissions: The file permissions to use for created files. + /// - retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A file descriptor for the open file + /// + /// The corresponding C function is `openat`. + @_alwaysEmitIntoClient + public func open( + at path: UnsafePointer, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions? = nil, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + try _open( + at: path, mode, options: options, permissions: permissions, retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _open( + at path: UnsafePointer, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions?, + retryOnInterrupt: Bool + ) -> Result { + let oFlag = mode.rawValue | options.rawValue + let descOrError: Result = valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + if let permissions = permissions { + return system_openat(self.rawValue, path, oFlag, permissions.rawValue) + } + precondition(!options.contains(.create), + "Create must be given permissions") + return system_openat(self.rawValue, path, oFlag) + } + return descOrError.map { FileDescriptor(rawValue: $0) } + } + + /// Returns attributes information about a file relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the file to retrieve attributes. + /// - options: The behavior for retrieving attributes. Available options are: + /// - + /// - Returns: A set of attributes about the specified file. + /// + /// The corresponding C function is `fstatat`. + @_alwaysEmitIntoClient + public func attributes(at path: FilePath, options: AtOptions) throws -> Attributes { + try path.withPlatformString { _attributes(at: $0, options: options) }.get() + } + + /// Returns attributes information about a file relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the file to retrieve attributes. + /// - options: The behavior for retrieving attributes. Available options are: + /// - + /// - Returns: A set of attributes about the specified file. + /// + /// The corresponding C function is `fstatat`. + @_alwaysEmitIntoClient + public func attributes(at path: UnsafePointer, options: AtOptions) throws -> Attributes { + try _attributes(at: path, options: options).get() + } + + @usableFromInline + internal func _attributes(at path: UnsafePointer, options: AtOptions) -> Result { + var stat: stat = stat() + return nothingOrErrno(retryOnInterrupt: false) { + system_fstatat(self.rawValue, path, &stat, options.rawValue) + } + .map { Attributes(rawValue: stat) } + } + + /// Remove a file entry relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the directory to remove. + /// - options: The behavior for removing a file entry. Available options are: + /// - + /// + /// The corresponding C function is `unlinkat`. + @_alwaysEmitIntoClient + public func remove(at path: FilePath, options: AtOptions) throws { + try path.withPlatformString { _remove(at: $0, options: options) }.get() + } + + /// Remove a file entry relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the directory to remove. + /// - options: The behavior for removing a file entry. Available options are: + /// - + /// + /// The corresponding C function is `unlinkat`. + @_alwaysEmitIntoClient + public func remove(at path: UnsafePointer, options: AtOptions) throws { + try _remove(at: path, options: options).get() + } + + @usableFromInline + internal func _remove( + at path: UnsafePointer, options: AtOptions + ) -> Result<(), Errno> { + return nothingOrErrno(retryOnInterrupt: false) { + system_unlinkat(self.rawValue, path, options.rawValue) + } + } + + /// Create a directory relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the directory to create. + /// - permissions: The file permissions to use for the created directory. + /// + /// The corresponding C function is `mkdirat`. + @_alwaysEmitIntoClient + public func createDirectory( + at path: FilePath, permissions: FilePermissions + ) throws { + try path.withPlatformString { + _createDirectory(at: $0, permissions: permissions) + }.get() + } + + /// Create a directory relative to a directory file descriptor + /// + /// - Parameters: + /// - path: The relative location of the directory to create. + /// - permissions: The file permissions to use for the created directory. + /// + /// The corresponding C function is `mkdirat`. + @_alwaysEmitIntoClient + public func createDirectory( + at path: UnsafePointer, permissions: FilePermissions + ) throws { + try _createDirectory(at: path, permissions: permissions).get() + } + + @usableFromInline + internal func _createDirectory( + at path: UnsafePointer, permissions: FilePermissions + ) -> Result<(), Errno> { + return nothingOrErrno(retryOnInterrupt: false) { + system_mkdirat(self.rawValue, path, permissions.rawValue) + } + } + + /// Create a symbolic link relative to a directory file descriptor + /// + /// - Parameters: + /// - original: The path to be refered by the created symbolic link. + /// - link: The relative location of the symbolic link to create + /// + /// The corresponding C function is `symlinkat`. + @_alwaysEmitIntoClient + public func createSymlink(original: FilePath, link: FilePath) throws { + try original.withPlatformString { cOriginal in + try link.withPlatformString { cLink in + try _createSymlink(original: cOriginal, link: cLink).get() + } + } + } + + /// Create a symbolic link relative to a directory file descriptor + /// + /// - Parameters: + /// - original: The path to be refered by the created symbolic link. + /// - link: The relative location of the symbolic link to create + /// + /// The corresponding C function is `symlinkat`. + @_alwaysEmitIntoClient + public func createSymlink( + original: UnsafePointer, + link: UnsafePointer + ) throws { + try _createSymlink(original: original, link: link).get() + } + + @usableFromInline + internal func _createSymlink( + original: UnsafePointer, + link: UnsafePointer + ) -> Result<(), Errno> { + return nothingOrErrno(retryOnInterrupt: false) { + system_symlinkat(original, self.rawValue, link) + } + } +} diff --git a/Sources/SystemExtras/FileOperations.swift b/Sources/SystemExtras/FileOperations.swift new file mode 100644 index 00000000..e09f352d --- /dev/null +++ b/Sources/SystemExtras/FileOperations.swift @@ -0,0 +1,391 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +import ucrt +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +extension FileDescriptor { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + + /// Announces an intention to read specific region of file data. + /// + /// - Parameters: + /// - offset: The offset to the starting point of the region. + /// - length: The length of the region. + /// + /// The corresponding C function is `fcntl` with `F_RDADVISE` command. + @_alwaysEmitIntoClient + public func adviseRead(offset: Int64, length: Int32) throws { + try _adviseRead(offset: offset, length: length).get() + } + + @usableFromInline + internal func _adviseRead(offset: Int64, length: Int32) -> Result { + var radvisory = radvisory(ra_offset: offset, ra_count: length) + return withUnsafeMutablePointer(to: &radvisory) { radvisoryPtr in + nothingOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, F_RDADVISE, radvisoryPtr) + } + } + } + #endif + + /// The advisory for specific access pattern to file data. + @frozen + public struct Advice: RawRepresentable, Hashable, Codable { + public var rawValue: CInt + + /// Creates a strongly-typed advice from a raw C access mode. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + #if os(Linux) + /// Access the specified data in the near future. + /// + /// The corresponding C constant is `POSIX_FADV_WILLNEED`. + @_alwaysEmitIntoClient + public static var willNeed: Advice { Advice(rawValue: POSIX_FADV_WILLNEED) } + #endif + } + + #if os(Linux) + /// Announces an intention to access specific region of file data. + /// + /// - Parameters: + /// - offset: The offset to the starting point of the region. + /// - length: The length of the region. + /// - advice: The advisory for the access pattern. + /// + /// The corresponding C function is `posix_fadvise`. + @_alwaysEmitIntoClient + public func advise(offset: Int, length: Int, advice: Advice) throws { + try _advise(offset: offset, length: length, advice: advice).get() + } + + @usableFromInline + internal func _advise(offset: Int, length: Int, advice: Advice) -> Result { + nothingOrErrno(retryOnInterrupt: false) { + system_posix_fadvise(self.rawValue, offset, length, advice.rawValue) + } + } + #endif + + /// A structure representing type of file. + /// + /// Typically created from `st_mode & S_IFMT`. + @frozen + public struct FileType: RawRepresentable { + /// The raw C file type. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed file type from a raw C file type. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { + self.rawValue = rawValue + } + + public static var directory: FileType { FileType(rawValue: _S_IFDIR) } + + public static var symlink: FileType { FileType(rawValue: _S_IFLNK) } + + public static var file: FileType { FileType(rawValue: _S_IFREG) } + + public static var characterDevice: FileType { FileType(rawValue: _S_IFCHR) } + + public static var blockDevice: FileType { FileType(rawValue: _S_IFBLK) } + + public static var socket: FileType { FileType(rawValue: _S_IFSOCK) } + + public static var unknown: FileType { FileType(rawValue: _S_IFMT) } + + /// A Boolean value indicating whether this file type represents a directory file. + @_alwaysEmitIntoClient + public var isDirectory: Bool { + _is(_S_IFDIR) + } + + /// A Boolean value indicating whether this file type represents a symbolic link. + @_alwaysEmitIntoClient + public var isSymlink: Bool { + _is(_S_IFLNK) + } + + /// A Boolean value indicating whether this file type represents a regular file. + @_alwaysEmitIntoClient + public var isFile: Bool { + _is(_S_IFREG) + } + + /// A Boolean value indicating whether this file type represents a character-oriented device file. + @_alwaysEmitIntoClient + public var isCharacterDevice: Bool { + _is(_S_IFCHR) + } + + /// A Boolean value indicating whether this file type represents a block-oriented device file. + @_alwaysEmitIntoClient + public var isBlockDevice: Bool { + _is(_S_IFBLK) + } + + /// A Boolean value indicating whether this file type represents a socket. + @_alwaysEmitIntoClient + public var isSocket: Bool { + _is(_S_IFSOCK) + } + + @_alwaysEmitIntoClient + internal func _is(_ mode: CInterop.Mode) -> Bool { + rawValue == mode + } + } + + /// A metadata information about a file. + /// + /// The corresponding C struct is `stat`. + @frozen + public struct Attributes: RawRepresentable { + /// The raw C file metadata structure. + @_alwaysEmitIntoClient + public let rawValue: stat + + /// Creates a strongly-typed file type from a raw C file metadata structure. + @_alwaysEmitIntoClient + public init(rawValue: stat) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public var device: UInt64 { + UInt64(rawValue.st_dev) + } + + @_alwaysEmitIntoClient + public var inode: UInt64 { + UInt64(rawValue.st_ino) + } + + /// Returns the file type for this metadata. + @_alwaysEmitIntoClient + public var fileType: FileType { + FileType(rawValue: self.rawValue.st_mode & S_IFMT) + } + + @_alwaysEmitIntoClient + public var linkCount: UInt32 { + UInt32(rawValue.st_nlink) + } + + @_alwaysEmitIntoClient + public var size: Int64 { + Int64(rawValue.st_size) + } + + @_alwaysEmitIntoClient + public var accessTime: Clock.TimeSpec { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + Clock.TimeSpec(rawValue: self.rawValue.st_atimespec) + #else + Clock.TimeSpec(rawValue: self.rawValue.st_atim) + #endif + } + + @_alwaysEmitIntoClient + public var modificationTime: Clock.TimeSpec { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + Clock.TimeSpec(rawValue: self.rawValue.st_mtimespec) + #else + Clock.TimeSpec(rawValue: self.rawValue.st_mtim) + #endif + } + + @_alwaysEmitIntoClient + public var creationTime: Clock.TimeSpec { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + Clock.TimeSpec(rawValue: self.rawValue.st_ctimespec) + #else + Clock.TimeSpec(rawValue: self.rawValue.st_ctim) + #endif + } + } + + /// Queries the metadata about the file + /// + /// - Returns: The attributes of the file + /// + /// The corresponding C function is `fstat`. + @_alwaysEmitIntoClient + public func attributes() throws -> Attributes { + try _attributes().get() + } + + @usableFromInline + internal func _attributes() -> Result { + var stat: stat = stat() + return nothingOrErrno(retryOnInterrupt: false) { + system_fstat(self.rawValue, &stat) + } + .map { Attributes(rawValue: stat) } + } + + /// Queries the current status of the file descriptor. + /// + /// - Returns: The file descriptor's access mode and status. + /// + /// The corresponding C function is `fcntl` with `F_GETFL` command. + @_alwaysEmitIntoClient + public func status() throws -> OpenOptions { + try _status().get() + } + + @usableFromInline + internal func _status() -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, _F_GETFL) + } + .map { OpenOptions(rawValue: $0) } + } + + @_alwaysEmitIntoClient + public func setStatus(_ options: OpenOptions) throws { + try _setStatus(options).get() + } + + @usableFromInline + internal func _setStatus(_ options: OpenOptions) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, F_SETFL, options.rawValue) + } + } + + @_alwaysEmitIntoClient + public func setTimes( + access: Clock.TimeSpec = .omit, modification: Clock.TimeSpec = .omit + ) throws { + try _setTime(access: access, modification: modification).get() + } + + @usableFromInline + internal func _setTime(access: Clock.TimeSpec, modification: Clock.TimeSpec) -> Result<(), Errno> { + let times = ContiguousArray([access.rawValue, modification.rawValue]) + return times.withUnsafeBufferPointer { timesPtr in + nothingOrErrno(retryOnInterrupt: false) { + system_futimens(self.rawValue, timesPtr.baseAddress!) + } + } + } + + @_alwaysEmitIntoClient + public func truncate(size: Int64) throws { + try _truncate(size: size).get() + } + + @usableFromInline + internal func _truncate(size: Int64) -> Result<(), Errno> { + return nothingOrErrno(retryOnInterrupt: false) { + system_ftruncate(self.rawValue, off_t(size)) + } + } + + public struct DirectoryEntry: RawRepresentable { + @_alwaysEmitIntoClient + public var rawValue: UnsafeMutablePointer + + @_alwaysEmitIntoClient + public init(rawValue: UnsafeMutablePointer) { + self.rawValue = rawValue + } + + @_alwaysEmitIntoClient + public var name: String { + withUnsafePointer(to: &rawValue.pointee.d_name) { dName in + dName.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: dName)) { + // String initializer copies the given buffer contents, so it's safe. + return String(cString: $0) + } + } + } + + public var fileType: FileType { + switch CInt(rawValue.pointee.d_type) { + case _DT_REG: return .file + case _DT_BLK: return .blockDevice + case _DT_CHR: return .characterDevice + case _DT_DIR: return .directory + case _DT_LNK: return .symlink + case _DT_SOCK: return .socket + default: return .unknown + } + } + } + + public struct DirectoryStream: RawRepresentable, IteratorProtocol, Sequence { + @_alwaysEmitIntoClient + public let rawValue: CInterop.DirP + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.DirP) { + self.rawValue = rawValue + } + + public func next() -> Result? { + // https://man7.org/linux/man-pages/man3/readdir.3.html#RETURN_VALUE + // > If the end of the directory stream is reached, NULL is returned + // > and errno is not changed. If an error occurs, NULL is returned + // > and errno is set to indicate the error. To distinguish end of + // > stream from an error, set errno to zero before calling readdir() + // > and then check the value of errno if NULL is returned. + system_errno = 0 + if let dirent = system_readdir(rawValue) { + return .success(DirectoryEntry(rawValue: dirent)) + } else { + let currentErrno = system_errno + if currentErrno == 0 { + // We successfully reached the end of the stream. + return nil + } else { + return .failure(Errno(rawValue: currentErrno)) + } + } + } + } + + public func contentsOfDirectory() throws -> DirectoryStream { + return try _contentsOfDirectory().get() + } + + internal func _contentsOfDirectory() -> Result { + guard let dirp = system_fdopendir(self.rawValue) else { + return .failure(Errno(rawValue: system_errno)) + } + return .success(DirectoryStream(rawValue: dirp)) + } +} + +// MARK: - Synchronized Input and Output + +extension FileDescriptor.OpenOptions { + @_alwaysEmitIntoClient + public static var dataSync: FileDescriptor.OpenOptions { + FileDescriptor.OpenOptions(rawValue: _O_DSYNC) + } + + @_alwaysEmitIntoClient + public static var fileSync: FileDescriptor.OpenOptions { + FileDescriptor.OpenOptions(rawValue: _O_SYNC) + } + + #if os(Linux) + @_alwaysEmitIntoClient + public static var readSync: FileDescriptor.OpenOptions { + FileDescriptor.OpenOptions(rawValue: _O_RSYNC) + } + #endif +} diff --git a/Sources/SystemExtras/Syscalls.swift b/Sources/SystemExtras/Syscalls.swift new file mode 100644 index 00000000..87c850a6 --- /dev/null +++ b/Sources/SystemExtras/Syscalls.swift @@ -0,0 +1,131 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +import ucrt +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +// openat +internal func system_openat( + _ fd: Int32, + _ path: UnsafePointer, + _ oflag: Int32 +) -> CInt { + return openat(fd, path, oflag) +} + +internal func system_openat( + _ fd: Int32, + _ path: UnsafePointer, + _ oflag: Int32, _ mode: CInterop.Mode +) -> CInt { + return openat(fd, path, oflag, mode) +} + +// fcntl +internal func system_fcntl(_ fd: Int32, _ cmd: Int32, _ value: UnsafeMutableRawPointer) -> CInt { + return fcntl(fd, cmd, value) +} + +internal func system_fcntl(_ fd: Int32, _ cmd: Int32, _ value: CInt) -> CInt { + return fcntl(fd, cmd, value) +} + +internal func system_fcntl(_ fd: Int32, _ cmd: Int32) -> CInt { + return fcntl(fd, cmd) +} + +#if os(Linux) +// posix_fadvise +internal func system_posix_fadvise( + _ fd: Int32, _ offset: Int, _ length: Int, _ advice: CInt +) -> CInt { + return posix_fadvise(fd, offset, length, advice) +} +#endif + +// fstat +internal func system_fstat(_ fd: Int32, _ stat: UnsafeMutablePointer) -> CInt { + return fstat(fd, stat) +} + +// fstatat +internal func system_fstatat( + _ fd: Int32, _ path: UnsafePointer, + _ stat: UnsafeMutablePointer, + _ flags: Int32 +) -> CInt { + return fstatat(fd, path, stat, flags) +} + +// unlinkat +internal func system_unlinkat( + _ fd: Int32, _ path: UnsafePointer, + _ flags: Int32 +) -> CInt { + return unlinkat(fd, path, flags) +} + +// futimens +internal func system_futimens(_ fd: Int32, _ times: UnsafePointer) -> CInt { + return futimens(fd, times) +} + +// ftruncate +internal func system_ftruncate(_ fd: Int32, _ size: off_t) -> CInt { + return ftruncate(fd, size) +} + +// mkdirat +internal func system_mkdirat( + _ fd: Int32, _ path: UnsafePointer, _ mode: CInterop.Mode +) -> CInt { + return mkdirat(fd, path, mode) +} + +// symlinkat +internal func system_symlinkat( + _ oldPath: UnsafePointer, _ newDirFd: Int32, _ newPath: UnsafePointer +) -> CInt { + return symlinkat(oldPath, newDirFd, newPath) +} + +extension CInterop { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + public typealias DirP = UnsafeMutablePointer + #elseif os(Linux) + public typealias DirP = OpaquePointer + #else + #error("Unsupported Platform") + #endif +} + +// fdopendir +internal func system_fdopendir(_ fd: Int32) -> CInterop.DirP? { + return fdopendir(fd) +} + +// readdir +internal func system_readdir(_ dirp: CInterop.DirP) -> UnsafeMutablePointer? { + return readdir(dirp) +} + +extension CInterop { + public typealias ClockId = clockid_t + public typealias TimeSpec = timespec +} + +// clock_gettime +internal func system_clock_gettime(_ id: CInterop.ClockId, _ tp: UnsafeMutablePointer) -> CInt { + return clock_gettime(id, tp) +} + +// clock_getres +internal func system_clock_getres(_ id: CInterop.ClockId, _ tp: UnsafeMutablePointer) -> CInt { + return clock_getres(id, tp) +} diff --git a/Sources/SystemExtras/Vendor/Exports.swift b/Sources/SystemExtras/Vendor/Exports.swift new file mode 100644 index 00000000..c9d55b0f --- /dev/null +++ b/Sources/SystemExtras/Vendor/Exports.swift @@ -0,0 +1,169 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// Internal wrappers and typedefs which help reduce #if littering in System's +// code base. + +// TODO: Should CSystem just include all the header files we need? + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import CSystem +import Glibc +#elseif os(Windows) +import CSystem +import ucrt +#else +#error("Unsupported Platform") +#endif + +import SystemPackage + +internal typealias _COffT = off_t + +// MARK: syscalls and variables + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +internal var system_errno: CInt { + get { Darwin.errno } + set { Darwin.errno = newValue } +} +#elseif os(Windows) +internal var system_errno: CInt { + get { + var value: CInt = 0 + // TODO(compnerd) handle the error? + _ = ucrt._get_errno(&value) + return value + } + set { + _ = ucrt._set_errno(newValue) + } +} +#else +internal var system_errno: CInt { + get { Glibc.errno } + set { Glibc.errno = newValue } +} +#endif + +// MARK: C stdlib decls + +// Convention: `system_foo` is system's wrapper for `foo`. + +internal func system_strerror(_ __errnum: Int32) -> UnsafeMutablePointer! { + strerror(__errnum) +} + +internal func system_strlen(_ s: UnsafePointer) -> Int { + strlen(s) +} + +// Convention: `system_platform_foo` is a +// platform-representation-abstracted wrapper around `foo`-like functionality. +// Type and layout differences such as the `char` vs `wchar` are abstracted. +// + +// strlen for the platform string +internal func system_platform_strlen(_ s: UnsafePointer) -> Int { + #if os(Windows) + return wcslen(s) + #else + return strlen(s) + #endif +} + +// Interop between String and platfrom string +extension String { + internal func _withPlatformString( + _ body: (UnsafePointer) throws -> Result + ) rethrows -> Result { + // Need to #if because CChar may be signed + #if os(Windows) + return try withCString(encodedAs: CInterop.PlatformUnicodeEncoding.self, body) + #else + return try withCString(body) + #endif + } + + internal init?(_platformString platformString: UnsafePointer) { + // Need to #if because CChar may be signed + #if os(Windows) + guard let strRes = String.decodeCString( + platformString, + as: CInterop.PlatformUnicodeEncoding.self, + repairingInvalidCodeUnits: false + ) else { return nil } + assert(strRes.repairsMade == false) + self = strRes.result + return + + #else + self.init(validatingUTF8: platformString) + #endif + } + + internal init( + _errorCorrectingPlatformString platformString: UnsafePointer + ) { + // Need to #if because CChar may be signed + #if os(Windows) + let strRes = String.decodeCString( + platformString, + as: CInterop.PlatformUnicodeEncoding.self, + repairingInvalidCodeUnits: true) + self = strRes!.result + return + #else + self.init(cString: platformString) + #endif + } +} + +// TLS +#if os(Windows) +internal typealias _PlatformTLSKey = DWORD +#else +internal typealias _PlatformTLSKey = pthread_key_t +#endif + +internal func makeTLSKey() -> _PlatformTLSKey { + #if os(Windows) + let raw: DWORD = FlsAlloc(nil) + if raw == FLS_OUT_OF_INDEXES { + fatalError("Unable to create key") + } + return raw + #else + var raw = pthread_key_t() + guard 0 == pthread_key_create(&raw, nil) else { + fatalError("Unable to create key") + } + return raw + #endif +} +internal func setTLS(_ key: _PlatformTLSKey, _ p: UnsafeMutableRawPointer?) { + #if os(Windows) + guard FlsSetValue(key, p) else { + fatalError("Unable to set TLS") + } + #else + guard 0 == pthread_setspecific(key, p) else { + fatalError("Unable to set TLS") + } + #endif +} +internal func getTLS(_ key: _PlatformTLSKey) -> UnsafeMutableRawPointer? { + #if os(Windows) + return FlsGetValue(key) + #else + return pthread_getspecific(key) + #endif +} diff --git a/Sources/SystemExtras/Vendor/Utils.swift b/Sources/SystemExtras/Vendor/Utils.swift new file mode 100644 index 00000000..f22d551d --- /dev/null +++ b/Sources/SystemExtras/Vendor/Utils.swift @@ -0,0 +1,150 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// NOTICE: This utility definitions are copied from swift-system package +// temporarily until we will upstream extra functions in this module. + +import SystemPackage + +#if canImport(Darwin) +import Darwin +#endif + +#if canImport(Glibc) +import Glibc +#endif + +#if canImport(ucrt) +import ucrt +#endif + +#if canImport(WASILibc) +import WASILibc +#endif + +// Results in errno if i == -1 +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +private func valueOrErrno( + _ i: I +) -> Result { + i == -1 ? .failure(Errno(rawValue: errno)) : .success(i) +} + +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +private func nothingOrErrno( + _ i: I +) -> Result<(), Errno> { + valueOrErrno(i).map { _ in () } +} + +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +internal func valueOrErrno( + retryOnInterrupt: Bool, _ f: () -> I +) -> Result { + repeat { + switch valueOrErrno(f()) { + case .success(let r): return .success(r) + case .failure(let err): + guard retryOnInterrupt && err == .interrupted else { return .failure(err) } + break + } + } while true +} + +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +internal func nothingOrErrno( + retryOnInterrupt: Bool, _ f: () -> I +) -> Result<(), Errno> { + valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } +} + +// Run a precondition for debug client builds +internal func _debugPrecondition( + _ condition: @autoclosure () -> Bool, + _ message: StaticString = StaticString(), + file: StaticString = #file, line: UInt = #line +) { + // Only check in debug mode. + if _slowPath(_isDebugAssertConfiguration()) { + precondition( + condition(), String(describing: message), file: file, line: line) + } +} + +extension OpaquePointer { + internal var _isNULL: Bool { + OpaquePointer(bitPattern: Int(bitPattern: self)) == nil + } +} + +extension Sequence { + // Tries to recast contiguous pointer if available, otherwise allocates memory. + internal func _withRawBufferPointer( + _ body: (UnsafeRawBufferPointer) throws -> R + ) rethrows -> R { + guard let result = try self.withContiguousStorageIfAvailable({ + try body(UnsafeRawBufferPointer($0)) + }) else { + return try Array(self).withUnsafeBytes(body) + } + return result + } +} + +extension OptionSet { + // Helper method for building up a comma-separated list of options + // + // Taking an array of descriptions reduces code size vs + // a series of calls due to avoiding register copies. Make sure + // to pass an array literal and not an array built up from a series of + // append calls, else that will massively bloat code size. This takes + // StaticStrings because otherwise we get a warning about getting evicted + // from the shared cache. + @inline(never) + internal func _buildDescription( + _ descriptions: [(Element, StaticString)] + ) -> String { + var copy = self + var result = "[" + + for (option, name) in descriptions { + if _slowPath(copy.contains(option)) { + result += name.description + copy.remove(option) + if !copy.isEmpty { result += ", " } + } + } + + if _slowPath(!copy.isEmpty) { + result += "\(Self.self)(rawValue: \(copy.rawValue))" + } + result += "]" + return result + } +} + +internal func _dropCommonPrefix( + _ lhs: C, _ rhs: C +) -> (C.SubSequence, C.SubSequence) +where C.Element: Equatable { + var (lhs, rhs) = (lhs[...], rhs[...]) + while lhs.first != nil && lhs.first == rhs.first { + lhs.removeFirst() + rhs.removeFirst() + } + return (lhs, rhs) +} + +extension MutableCollection where Element: Equatable { + mutating func _replaceAll(_ e: Element, with new: Element) { + for idx in self.indices { + if self[idx] == e { self[idx] = new } + } + } +} diff --git a/Sources/WAKit/Execution/Instructions/Control.swift b/Sources/WAKit/Execution/Instructions/Control.swift deleted file mode 100644 index c7d53aff..00000000 --- a/Sources/WAKit/Execution/Instructions/Control.swift +++ /dev/null @@ -1,162 +0,0 @@ -/// - Note: -/// -extension InstructionFactory { - var unreachable: Instruction { - return makeInstruction { _, _, _ in throw Trap.unreachable } - } - - var nop: Instruction { - return makeInstruction { pc, _, _ in .jump(pc + 1) } - } - - func block(type: ResultType, expression: Expression) -> [Instruction] { - let count = expression.instructions.count - let block = makeInstruction { pc, _, stack in - let start = pc + 1 - let end = pc + count - let label = Label(arity: type.count, continuation: start + count, range: start ... end) - stack.push(label) - return .jump(start) - } - return [block] + expression.instructions - } - - func loop(type: ResultType, expression: Expression) -> [Instruction] { - let count = expression.instructions.count - let loop = makeInstruction { pc, _, stack in - let start = pc + 1 - let end = pc + count - let label = Label(arity: type.count, continuation: pc, range: start ... end) - stack.push(label) - return .jump(start) - } - return [loop] + expression.instructions - } - - func `if`(type: ResultType, then: Expression, else: Expression) -> [Instruction] { - let thenCount = then.instructions.count - let elseCount = `else`.instructions.count - let instructions = then.instructions + `else`.instructions - let `if` = makeInstruction { pc, _, stack in - let isTrue = try stack.pop(Value.self).i32 != 0 - - if !isTrue, elseCount == 0 { - return .jump(pc + thenCount + 1) - } - - let start = pc + 1 + (isTrue ? 0 : thenCount) - let end = start + (isTrue ? thenCount - 1 : elseCount - 1) - let label = Label(arity: type.count, continuation: pc + instructions.count + 1, range: start ... end) - stack.push(label) - return .jump(start) - } - return [`if`] + instructions - } - - var `else`: Instruction { - return makeInstruction { _, _, _ in throw Trap.unreachable } - } - - var end: Instruction { - return makeInstruction { _, _, _ in throw Trap.unreachable } - } - - func br(_ labelIndex: LabelIndex) -> Instruction { - return makeInstruction { _, _, stack in - let label = try stack.get(Label.self, index: Int(labelIndex)) - let values = try stack.pop(Value.self, count: label.arity) - for _ in 0 ... labelIndex { - while stack.peek() is Value { - _ = stack.pop() - } - try stack.pop(Label.self) - } - for value in values { - stack.push(value) - } - - return .jump(label.continuation) - } - } - - func brIf(_ labelIndex: LabelIndex) -> Instruction { - return makeInstruction { pc, _, stack in - guard try stack.pop(Value.self).i32 != 0 else { - return .jump(pc + 1) - } - - let label = try stack.get(Label.self, index: Int(labelIndex)) - let values = try stack.pop(Value.self, count: label.arity) - for _ in 0 ... labelIndex { - while stack.peek() is Value { - _ = stack.pop() - } - try stack.pop(Label.self) - } - for value in values { - stack.push(value) - } - - return .jump(label.continuation) - } - } - - func brTable(_ labelIndices: [LabelIndex], default defaultLabelIndex: LabelIndex) -> Instruction { - return makeInstruction { _, _, stack in - let value = try stack.pop(Value.self).i32 - let labelIndex: LabelIndex - if labelIndices.indices.contains(Int(value)) { - labelIndex = labelIndices[Int(value)] - } else { - labelIndex = defaultLabelIndex - } - - let label = try stack.get(Label.self, index: Int(labelIndex)) - let values = try stack.pop(Value.self, count: label.arity) - for _ in 0 ... labelIndex { - while stack.peek() is Value { - _ = stack.pop() - } - try stack.pop(Label.self) - } - for value in values { - stack.push(value) - } - - return .jump(label.continuation) - } - } - - var `return`: Instruction { - return makeInstruction { _, _, _ in - throw Trap.unimplemented(#function) - } - } - - func call(_ functionIndex: UInt32) -> Instruction { - return makeInstruction { _, _, stack in - let frame = try stack.get(current: Frame.self) - let functionAddress = frame.module.functionAddresses[Int(functionIndex)] - return .invoke(functionAddress) - } - } - - func callIndirect(_ typeIndex: UInt32) -> Instruction { - return makeInstruction { _, store, stack in - let frame = try stack.get(current: Frame.self) - let module = frame.module - let tableAddresses = module.tableAddresses[0] - let tableInstance = store.tables[tableAddresses] - let expectedType = module.types[Int(typeIndex)] - let value = try Int(stack.pop(Value.self).i32) - guard let functionAddress = tableInstance.elements[value] else { - throw Trap.tableUninitialized - } - let function = store.functions[functionAddress] - guard function.type == expectedType else { - throw Trap.callIndirectFunctionTypeMismatch(actual: function.type, expected: expectedType) - } - return .invoke(functionAddress) - } - } -} diff --git a/Sources/WAKit/Execution/Instructions/InstructionFactory.swift b/Sources/WAKit/Execution/Instructions/InstructionFactory.swift deleted file mode 100644 index 3aaa4ad6..00000000 --- a/Sources/WAKit/Execution/Instructions/InstructionFactory.swift +++ /dev/null @@ -1,11 +0,0 @@ -final class InstructionFactory { - let code: InstructionCode - - init(code: InstructionCode) { - self.code = code - } - - func makeInstruction(_ implementation: @escaping Instruction.Implementation) -> Instruction { - return Instruction(code, implementation: implementation) - } -} diff --git a/Sources/WAKit/Execution/Instructions/Memory.swift b/Sources/WAKit/Execution/Instructions/Memory.swift deleted file mode 100644 index c565bc2f..00000000 --- a/Sources/WAKit/Execution/Instructions/Memory.swift +++ /dev/null @@ -1,64 +0,0 @@ -/// - Note: -/// -extension InstructionFactory { - func load( - _ type: ValueType, - bitWidth: Int? = nil, - isSigned: Bool = true, - _ offset: UInt32 - ) -> Instruction { - // FIXME: handle `isSigned` - return makeInstruction { pc, store, stack in - let frame = try stack.get(current: Frame.self) - let memoryAddress = frame.module.memoryAddresses[0] - let memoryInstance = store.memories[memoryAddress] - let i = try stack.pop(Value.self).i32 - let (incrementedOffset, isOverflow) = offset.addingReportingOverflow(i) - guard !isOverflow else { - throw Trap.outOfBoundsMemoryAccess - } - let address = Int(incrementedOffset) - let length = (bitWidth ?? type.bitWidth) / 8 - guard memoryInstance.data.indices.contains(address + length) else { - throw Trap.outOfBoundsMemoryAccess - } - - let bytes = memoryInstance.data[address ..< address + length] - let value = Value(bytes, type) - - stack.push(value) - return .jump(pc + 1) - } - } - - func store(_ type: ValueType, _ offset: UInt32) -> Instruction { - return makeInstruction { pc, store, stack in - let value = try stack.pop(Value.self) - - let frame = try stack.get(current: Frame.self) - let memoryAddress = frame.module.memoryAddresses[0] - let memoryInstance = store.memories[memoryAddress] - let i = try stack.pop(Value.self).i32 - let address = Int(offset + i) - let length = type.bitWidth / 8 - guard memoryInstance.data.indices.contains(address + length) else { - throw Trap.outOfBoundsMemoryAccess - } - - memoryInstance.data.replaceSubrange(address ..< address + length, with: value.bytes) - return .jump(pc + 1) - } - } - - var memorySize: Instruction { - return makeInstruction { _, _, _ in - throw Trap.unimplemented() - } - } - - var memoryGrow: Instruction { - return makeInstruction { _, _, _ in - throw Trap.unimplemented() - } - } -} diff --git a/Sources/WAKit/Execution/Instructions/Numeric.swift b/Sources/WAKit/Execution/Instructions/Numeric.swift deleted file mode 100644 index 977f16d9..00000000 --- a/Sources/WAKit/Execution/Instructions/Numeric.swift +++ /dev/null @@ -1,68 +0,0 @@ -/// - Note: -/// - -extension InstructionFactory { - func const(_ value: Value) -> Instruction { - makeInstruction { pc, _, stack in - stack.push(value) - return .jump(pc + 1) - } - } - - func numeric(intUnary instruction: NumericInstruction.IntUnary) -> Instruction { - makeInstruction { pc, _, stack in - let value = try stack.pop(Value.self) - - stack.push(instruction(value)) - return .jump(pc + 1) - } - } - - func numeric(floatUnary instruction: NumericInstruction.FloatUnary) -> Instruction { - makeInstruction { pc, _, stack in - let value = try stack.pop(Value.self) - - stack.push(instruction(value)) - return .jump(pc + 1) - } - } - - func numeric(binary instruction: NumericInstruction.Binary) -> Instruction { - makeInstruction { pc, _, stack in - let value2 = try stack.pop(Value.self) - let value1 = try stack.pop(Value.self) - - stack.push(instruction(value1, value2)) - return .jump(pc + 1) - } - } - - func numeric(intBinary instruction: NumericInstruction.IntBinary) -> Instruction { - makeInstruction { pc, _, stack in - let value2 = try stack.pop(Value.self) - let value1 = try stack.pop(Value.self) - - try stack.push(instruction(value1.type, value1, value2)) - return .jump(pc + 1) - } - } - - func numeric(floatBinary instruction: NumericInstruction.FloatBinary) -> Instruction { - makeInstruction { pc, _, stack in - let value2 = try stack.pop(Value.self) - let value1 = try stack.pop(Value.self) - - try stack.push(instruction(value1, value2)) - return .jump(pc + 1) - } - } - - func numeric(conversion instruction: NumericInstruction.Conversion) -> Instruction { - makeInstruction { pc, _, stack in - let value = try stack.pop(Value.self) - - try stack.push(instruction(value)) - return .jump(pc + 1) - } - } -} diff --git a/Sources/WAKit/Execution/Instructions/Parametric.swift b/Sources/WAKit/Execution/Instructions/Parametric.swift deleted file mode 100644 index 882c73ef..00000000 --- a/Sources/WAKit/Execution/Instructions/Parametric.swift +++ /dev/null @@ -1,27 +0,0 @@ -/// - Note: -/// -extension InstructionFactory { - var drop: Instruction { - return makeInstruction { pc, _, stack in - _ = try stack.pop(Value.self) - return .jump(pc) - } - } - - var select: Instruction { - return makeInstruction { pc, _, stack in - let flagValue = try stack.pop(Value.self) - guard case let .i32(flag) = flagValue else { - throw Trap.stackValueTypesMismatch(expected: .int(.i32), actual: flagValue.type) - } - let value2 = try stack.pop(Value.self) - let value1 = try stack.pop(Value.self) - if flag != 0 { - stack.push(value1) - } else { - stack.push(value2) - } - return .jump(pc) - } - } -} diff --git a/Sources/WAKit/Execution/Instructions/Variable.swift b/Sources/WAKit/Execution/Instructions/Variable.swift deleted file mode 100644 index e277318b..00000000 --- a/Sources/WAKit/Execution/Instructions/Variable.swift +++ /dev/null @@ -1,47 +0,0 @@ -/// - Note: -/// -extension InstructionFactory { - func localGet(_ index: UInt32) -> Instruction { - return makeInstruction { pc, _, stack in - let currentFrame = try stack.get(current: Frame.self) - let value = try currentFrame.localGet(index: index) - stack.push(value) - return .jump(pc + 1) - } - } - - func localSet(_ index: UInt32) -> Instruction { - return makeInstruction { pc, _, stack in - let currentFrame = try stack.get(current: Frame.self) - let value = try stack.pop(Value.self) - try currentFrame.localSet(index: index, value: value) - return .jump(pc + 1) - } - } - - func localTee(_ index: UInt32) -> Instruction { - return makeInstruction { pc, _, stack in - let currentFrame = try stack.get(current: Frame.self) - let value = try stack.peek(Value.self) - try currentFrame.localSet(index: index, value: value) - return .jump(pc + 1) - } - } - - func globalGet(_ index: UInt32) -> Instruction { - return makeInstruction { pc, store, stack in - let value = try store.getGlobal(index: index) - stack.push(value) - return .jump(pc + 1) - } - } - - func globalSet(_ index: UInt32) -> Instruction { - return makeInstruction { pc, _, stack in - let currentFrame = try stack.get(current: Frame.self) - let value = try currentFrame.localGet(index: index) - stack.push(value) - return .jump(pc + 1) - } - } -} diff --git a/Sources/WAKit/Execution/Runtime/Runtime.swift b/Sources/WAKit/Execution/Runtime/Runtime.swift deleted file mode 100644 index 935caf27..00000000 --- a/Sources/WAKit/Execution/Runtime/Runtime.swift +++ /dev/null @@ -1,220 +0,0 @@ -public final class Runtime { - let store: Store - var stack: Stack - - public init() { - stack = Stack() - store = Store() - } -} - -extension Runtime { - /// - Note: - /// - public func instantiate(module: Module, externalValues: [ExternalValue]) throws -> ModuleInstance { - guard module.imports.count == externalValues.count else { - throw Trap.importsAndExternalValuesMismatch - } - - let isValid = zip(module.imports, externalValues).map { (i, e) -> Bool in - switch (i.descripter, e) { - case (.function, .function), - (.table, .table), - (.memory, .memory), - (.global, .global): return true - default: return false - } - }.reduce(true) { $0 && $1 } - - guard isValid else { - throw Trap.importsAndExternalValuesMismatch - } - - let initialGlobals = try evaluateGlobals(module: module, externalValues: externalValues) - - let instance = store.allocate( - module: module, - externalValues: externalValues, - initialGlobals: initialGlobals - ) - - let frame = Frame(arity: 0, module: instance, locals: []) - stack.push(frame) - - for element in module.elements { - let tableInstance = store.tables[Int(element.index)] - let offset = try Int(execute(element.offset, resultType: .int(.i32)).i32) - let end = offset + element.initializer.count - guard - tableInstance.elements.indices.contains(offset), - tableInstance.elements.indices.contains(end) - else { throw Trap.tableOutOfRange } - tableInstance.elements.replaceSubrange(offset ..< end, with: element.initializer.map { instance.functionAddresses[Int($0)] }) - } - - for data in module.data { - let memoryIndex = instance.memoryAddresses[Int(data.index)] - let memoryInstance = store.memories[memoryIndex] - let offset = try Int(execute(data.offset, resultType: .int(.i32)).i32) - let end = Int(offset) + data.initializer.count - guard - memoryInstance.data.indices.contains(offset), - memoryInstance.data.indices.contains(end) - else { throw Trap.outOfBoundsMemoryAccess } - memoryInstance.data.replaceSubrange(offset ..< end, with: data.initializer) - } - - try stack.pop(Frame.self) - - if let startIndex = module.start { - try invoke(functionAddress: instance.functionAddresses[Int(startIndex)]) - } - - return instance - } - - private func evaluateGlobals(module: Module, externalValues: [ExternalValue]) throws -> [Value] { - let globalModuleInstance = ModuleInstance() - globalModuleInstance.globalAddresses = externalValues.compactMap { - guard case let .global(address) = $0 else { return nil } - return address - } - let frame = Frame(arity: 0, module: globalModuleInstance, locals: []) - stack.push(frame) - - let globalInitializers = try module.globals.map { global in - try execute(global.initializer, resultType: global.type.valueType) - } - - try stack.pop(Frame.self) - - return globalInitializers - } - - /// - Note: - /// - func invoke(functionAddress address: FunctionAddress) throws { - let function = store.functions[address] - guard case let .some(parameterType, resultType) = function.type else { - throw Trap._raw("any type is not allowed here") - } - - let locals = function.code.locals.map { $0.defaultValue } - let expression = function.code.body - - let parameters = try stack.pop(Value.self, count: parameterType.count) - - let frame = Frame(arity: resultType.count, module: function.module, locals: parameters + locals) - stack.push(frame) - - let values = try enterBlock(expression, resultType: resultType) - - assert((try? stack.get(current: Frame.self)) == frame) - _ = try stack.pop(Frame.self) - - stack.push(values) - } -} - -extension Runtime { - public func invoke(_ moduleInstance: ModuleInstance, function: String, with parameters: [Value] = []) throws -> [Value] { - guard case let .function(address)? = moduleInstance.exports[function] else { - throw Trap.exportedFunctionNotFound(moduleInstance, name: function) - } - return try invoke(functionAddress: address, with: parameters) - } - - func invoke(functionAddress address: FunctionAddress, with parameters: [Value]) throws -> [Value] { - let function = store.functions[address] - guard case let .some(parameterTypes, _) = function.type else { - throw Trap._raw("any type is not allowed here") - } - - guard parameterTypes.count == parameters.count else { - throw Trap._raw("numbers of parameters don't match") - } - - assert(zip(parameterTypes, parameters).reduce(true) { acc, types in - acc && types.1.type == types.0 - }) - - stack.push(parameters) - - try invoke(functionAddress: address) - - var results: [Value] = [] - while stack.peek() is Value { - let value = try stack.pop(Value.self) - results.append(value) - } - return results - } -} - -extension Runtime { - func enterBlock(_ expression: Expression, resultType: ResultType) throws -> [Value] { - guard !expression.instructions.isEmpty else { - return [] - } - - let label = Label( - arity: resultType.count, - continuation: expression.instructions.indices.upperBound, - range: ClosedRange(expression.instructions.indices) - ) - - stack.push(label) - - var address: Int = 0 - while address <= label.range.upperBound { - while let currentLabel = try? stack.get(current: Label.self), currentLabel.range.upperBound < address { - try exitBlock(label: currentLabel) - } - - let action = try expression.execute(address: address, store: store, stack: &stack) - - switch action { - case let .jump(newAddress): - address = newAddress - - case let .invoke(functionIndex): - let currentFrame = try stack.get(current: Frame.self) - guard currentFrame.module.functionAddresses.indices.contains(functionIndex) else { - throw Trap.invalidFunctionIndex(functionIndex) - } - let functionAddress = currentFrame.module.functionAddresses[functionIndex] - try invoke(functionAddress: functionAddress) - address += 1 - } - } - - let values = try (0 ..< resultType.count).map { _ in try stack.pop(Value.self) } - - let _label = try stack.pop(Label.self) - guard label == _label else { - throw Trap.poppedLabelMismatch - } - - return values - } - - func execute(_ expression: Expression, resultType: ValueType) throws -> Value { - let values = try enterBlock(expression, resultType: [resultType]) - guard let value = values.first, values.count == 1 else { - preconditionFailure() - } - return value - } - - func exitBlock(label: Label) throws { - var values: [Value] = [] - while stack.peek() is Value { - values.append(try stack.pop(Value.self)) - } - - let _label = try stack.pop(Label.self) - assert(label == _label) - - stack.push(values) - } -} diff --git a/Sources/WAKit/Execution/Runtime/Stack.swift b/Sources/WAKit/Execution/Runtime/Stack.swift deleted file mode 100644 index acefadbe..00000000 --- a/Sources/WAKit/Execution/Runtime/Stack.swift +++ /dev/null @@ -1,170 +0,0 @@ -/// - Note: -/// - -protocol Stackable {} - -struct Stack { - fileprivate final class Entry { - var value: Stackable - var next: Entry? - - init(value: Stackable, next: Entry?) { - self.value = value - self.next = next - } - } - - fileprivate var _top: Entry? - - var top: Stackable? { - return _top?.value - } - - mutating func push(_ entry: Stackable) { - _top = Entry(value: entry, next: _top) - } - - func peek() -> Stackable? { - return top - } - - @discardableResult - mutating func pop() -> Stackable? { - let value = _top?.value - _top = _top?.next - return value - } -} - -extension Stack { - func peek(_: T.Type) throws -> T { - guard let value = top as? T else { - throw Trap.stackTypeMismatch(expected: T.self, actual: Swift.type(of: top)) - } - return value - } - - @discardableResult - mutating func pop(_: T.Type) throws -> T { - let popped = pop() - guard let value = popped as? T else { - if let popped = popped { - throw Trap.stackTypeMismatch(expected: T.self, actual: Swift.type(of: popped)) - } else { - throw Trap.stackTypeMismatch(expected: T.self, actual: Void.self) - } - } - return value - } - - mutating func pop(_ type: T.Type, count: Int) throws -> [T] { - var values: [T] = [] - for _ in 0 ..< count { - values.insert(try pop(type), at: 0) - } - return values - } - - mutating func push(_ entries: [Stackable]) { - for entry in entries { - push(entry) - } - } -} - -extension Stack { - func get(current type: T.Type) throws -> T { - return try get(type, index: 0) - } - - func get(_ type: T.Type, index: Int) throws -> T { - var currentIndex: Int = -1 - var entry: Entry? = _top - repeat { - defer { entry = entry?.next } - guard let value = entry?.value as? T else { - continue - } - currentIndex += 1 - if currentIndex == index { - return value - } - } while entry != nil - throw Trap.stackNotFound(type, index: index) - } -} - -extension Stack: CustomDebugStringConvertible { - var debugDescription: String { - var debugDescription = "" - - var entry: Stack.Entry? = _top - guard entry != nil else { - print("(empty)", to: &debugDescription) - return debugDescription - } - - while let value = entry?.value { - defer { entry = entry?.next } - dump(value, to: &debugDescription) - } - - return debugDescription - } -} - -/// - Note: -/// -extension Value: Stackable {} - -/// - Note: -/// -struct Label: Equatable, Stackable { - let arity: Int - let continuation: Int - - let range: ClosedRange -} - -/// - Note: -/// -// sourcery: AutoEquatable -final class Frame: Stackable { - let arity: Int - let module: ModuleInstance - var locals: [Value] - - init(arity: Int, module: ModuleInstance, locals: [Value]) { - self.arity = arity - self.module = module - self.locals = locals - } -} - -extension Frame { - func localGet(index: UInt32) throws -> Value { - guard locals.indices.contains(Int(index)) else { - throw Trap.localIndexOutOfRange(index: index) - } - return locals[Int(index)] - } - - func localSet(index: UInt32, value: Value) throws { - guard locals.indices.contains(Int(index)) else { - throw Trap.localIndexOutOfRange(index: index) - } - locals[Int(index)] = value - } -} - -extension Stack { - internal func entries() -> [Stackable] { - var entries = [Stackable]() - var entry = _top - while let value = entry?.value { - entries.append(value) - entry = entry?.next - } - return entries - } -} diff --git a/Sources/WAKit/Execution/Runtime/Store.swift b/Sources/WAKit/Execution/Runtime/Store.swift deleted file mode 100644 index 761cd9c0..00000000 --- a/Sources/WAKit/Execution/Runtime/Store.swift +++ /dev/null @@ -1,122 +0,0 @@ -/// - Note: -/// -public typealias FunctionAddress = Int -public typealias TableAddress = Int -public typealias MemoryAddress = Int -public typealias GlobalAddress = Int - -/// - Note: -/// -final class Store { - var functions: [FunctionInstance] = [] - var tables: [TableInstance] = [] - var memories: [MemoryInstance] = [] - var globals: [GlobalInstance] = [] -} - -extension Store { - /// - Note: - /// - func allocate(module: Module, externalValues: [ExternalValue], initialGlobals: [Value]) -> ModuleInstance { - let moduleInstance = ModuleInstance() - - moduleInstance.types = module.types - - for function in module.functions { - let address = allocate(function: function, module: moduleInstance) - moduleInstance.functionAddresses.append(address) - } - - for table in module.tables { - let address = allocate(tableType: table.type) - moduleInstance.tableAddresses.append(address) - } - - for memory in module.memories { - let address = allocate(memoryType: memory.type) - moduleInstance.memoryAddresses.append(address) - } - - assert(module.globals.count == initialGlobals.count) - for (global, initialValue) in zip(module.globals, initialGlobals) { - let address = allocate(globalType: global.type, initialValue: initialValue) - moduleInstance.globalAddresses.append(address) - } - - for external in externalValues { - switch external { - case let .function(address): - moduleInstance.functionAddresses.append(address) - case let .table(address): - moduleInstance.tableAddresses.append(address) - case let .memory(address): - moduleInstance.memoryAddresses.append(address) - case let .global(address): - moduleInstance.globalAddresses.append(address) - } - } - - for export in module.exports { - let exportInstance = ExportInstance(export, moduleInstance: moduleInstance) - moduleInstance.exportInstances.append(exportInstance) - } - - return moduleInstance - } - - /// - Note: - /// - func allocate(function: Function, module: ModuleInstance) -> FunctionAddress { - let address = functions.count - let instance = FunctionInstance(function, module: module) - functions.append(instance) - return address - } - - /// - Note: - /// - func allocate(tableType: TableType) -> TableAddress { - let address = tables.count - let instance = TableInstance(tableType) - tables.append(instance) - return address - } - - /// - Note: - /// - func allocate(memoryType: MemoryType) -> MemoryAddress { - let address = memories.count - let instance = MemoryInstance(memoryType) - memories.append(instance) - return address - } - - /// - Note: - /// - func allocate(globalType: GlobalType, initialValue: Value) -> GlobalAddress { - let address = globals.count - let instance = GlobalInstance(globalType: globalType, initialValue: initialValue) - globals.append(instance) - return address - } -} - -extension Store { - func getGlobal(index: UInt32) throws -> Value { - guard globals.indices.contains(Int(index)) else { - throw Trap.globalIndexOutOfRange(index: index) - } - return globals[Int(index)].value - } - - func setGlobal(index: UInt32, value: Value) throws { - guard globals.indices.contains(Int(index)) else { - throw Trap.globalIndexOutOfRange(index: index) - } - let global = globals[Int(index)] - guard global.mutability == .variable else { - throw Trap.globalImmutable(index: index) - } - global.value = value - } -} diff --git a/Sources/WAKit/Execution/Types/Instances.swift b/Sources/WAKit/Execution/Types/Instances.swift deleted file mode 100644 index 5f4dffb6..00000000 --- a/Sources/WAKit/Execution/Types/Instances.swift +++ /dev/null @@ -1,105 +0,0 @@ -/// - Note: -/// -// sourcery: AutoEquatable -public final class ModuleInstance { - var types: [FunctionType] = [] - var functionAddresses: [FunctionAddress] = [] - var tableAddresses: [TableAddress] = [] - var memoryAddresses: [MemoryAddress] = [] - var globalAddresses: [GlobalAddress] = [] - var exportInstances: [ExportInstance] = [] - - public var exports: [String: ExternalValue] { - return exportInstances.reduce(into: [:]) { exports, export in - exports[export.name] = export.value - } - } -} - -/// - Note: -/// -final class FunctionInstance { - let type: FunctionType - let module: ModuleInstance - let code: Function - - init(_ function: Function, module: ModuleInstance) { - type = module.types[Int(function.type)] - self.module = module - code = function - } -} - -/// - Note: -/// -final class TableInstance { - var elements: [FunctionAddress?] - let max: UInt32? - - init(_ tableType: TableType) { - elements = Array(repeating: nil, count: Int(tableType.limits.min)) - max = tableType.limits.max - } -} - -/// - Note: -/// -final class MemoryInstance { - private static let pageSize = 64 * 1024 - - var data: [UInt8] - let max: UInt32? - - init(_ memoryType: MemoryType) { - data = Array(repeating: 0, count: Int(memoryType.min) * MemoryInstance.pageSize) - max = memoryType.max - } -} - -extension MemoryInstance: CustomStringConvertible { - public var description: String { - String(describing: ObjectIdentifier(self)) - } -} - -/// - Note: -/// -final class GlobalInstance { - var value: Value - let mutability: Mutability - - init(globalType: GlobalType, initialValue: Value) { - value = initialValue - mutability = globalType.mutability - } -} - -/// - Note: -/// -public enum ExternalValue: Equatable { - case function(FunctionAddress) - case table(TableAddress) - case memory(MemoryAddress) - case global(GlobalAddress) -} - -/// - Note: -/// -struct ExportInstance: Equatable { - let name: String - let value: ExternalValue - - init(_ export: Export, moduleInstance: ModuleInstance) { - name = export.name - switch export.descriptor { - case let .function(index): - value = ExternalValue.function(moduleInstance.functionAddresses[Int(index)]) - case let .table(index): - value = ExternalValue.table(moduleInstance.tableAddresses[Int(index)]) - case let .memory(index): - value = ExternalValue.memory(moduleInstance.memoryAddresses[Int(index)]) - case let .global(index): - value = ExternalValue.global(moduleInstance.globalAddresses[Int(index)]) - } - } -} diff --git a/Sources/WAKit/Execution/Types/Trap.swift b/Sources/WAKit/Execution/Types/Trap.swift deleted file mode 100644 index 8a297223..00000000 --- a/Sources/WAKit/Execution/Types/Trap.swift +++ /dev/null @@ -1,52 +0,0 @@ -public enum Trap: Error { - // FIXME: for debugging purposes, to be eventually deleted - case _raw(String) - case _unimplemented(description: String, file: StaticString, line: UInt) - - case unreachable - - // Stack - case stackTypeMismatch(expected: Any.Type, actual: Any.Type) - case stackValueTypesMismatch(expected: ValueType, actual: ValueType) - case stackNotFound(Any.Type, index: Int) - case localIndexOutOfRange(index: UInt32) - - // Store - case globalIndexOutOfRange(index: UInt32) - case globalImmutable(index: UInt32) - - // Invocation - case exportedFunctionNotFound(ModuleInstance, name: String) - case invalidTypeForInstruction(Any.Type, Instruction) - case importsAndExternalValuesMismatch - case tableUninitialized - case tableOutOfRange - case callIndirectFunctionTypeMismatch(actual: FunctionType, expected: FunctionType) - case outOfBoundsMemoryAccess - case invalidFunctionIndex(Int) - case poppedLabelMismatch - case labelMismatch - case integerDividedByZero - case integerOverflowed - case invalidConversionToInteger - - static func unimplemented(_ description: String = "", file: StaticString = #file, line: UInt = #line) -> Trap { - return ._unimplemented(description: description, file: file, line: line) - } - - /// Human-readable text representation of the trap that `.wast` text format expects in assertions - public var assertionText: String { - switch self { - case .outOfBoundsMemoryAccess: - return "out of bounds memory access" - case .integerDividedByZero: - return "integer divide by zero" - case .integerOverflowed: - return "integer overflow" - case .invalidConversionToInteger: - return "invalid conversion to integer" - default: - return String(describing: self) - } - } -} diff --git a/Sources/WAKit/Execution/Types/Value.swift b/Sources/WAKit/Execution/Types/Value.swift deleted file mode 100644 index 1e9bc7d8..00000000 --- a/Sources/WAKit/Execution/Types/Value.swift +++ /dev/null @@ -1,544 +0,0 @@ -/// - Note: -/// - -public enum ValueType: Equatable { - case int(IntValueType) - case float(FloatValueType) - - var defaultValue: Value { - switch self { - case .int(.i32): return .i32(0) - case .int(.i64): return .i64(0) - case .float(.f32): return .f32(0) - case .float(.f64): return .f64(0) - } - } - - var bitWidth: Int { - switch self { - case .int(.i32), .float(.f32): return 32 - case .int(.i64), .float(.f64): return 64 - } - } -} - -public enum Value: Equatable, Hashable { - case i32(UInt32) - case i64(UInt64) - case f32(Float32) - case f64(Float64) - - var type: ValueType { - switch self { - case .i32: return .int(.i32) - case .i64: return .int(.i64) - case .f32: return .float(.f32) - case .f64: return .float(.f64) - } - } - - init(_ rawValue: V) { - switch rawValue { - case let value as UInt32: - self = .i32(value) - case let value as UInt64: - self = .i64(value) - default: - fatalError("unknown raw integer type \(Swift.type(of: rawValue)) passed to `Value.init` ") - } - } - - public init(signed value: V) { - if value < 0 { - self.init(V.Unsigned(~value)) - } else { - self.init(V.Unsigned(value)) - } - } - - init(_ rawValue: V) { - switch rawValue { - case let value as Float32: - self = .f32(value) - case let value as Float64: - self = .f64(value) - default: - fatalError("unknown raw float type \(Swift.type(of: rawValue)) passed to `Value.init` ") - } - } - - var i32: UInt32 { - guard case let .i32(result) = self else { fatalError() } - return result - } - - var i64: UInt64 { - guard case let .i64(result) = self else { fatalError() } - return result - } - - public func isTestEquivalent(to value: Self) -> Bool { - switch (self, value) { - case let (.i32(lhs), .i32(rhs)): return lhs == rhs - case let (.i64(lhs), .i64(rhs)): return lhs == rhs - case let (.f32(lhs), .f32(rhs)): return lhs.isNaN && rhs.isNaN || lhs == rhs - case let (.f64(lhs), .f64(rhs)): return lhs.isNaN && rhs.isNaN || lhs == rhs - default: return false - } - } -} - -extension Array where Element == Value { - public func isTestEquivalent(to arrayOfValues: Self) -> Bool { - guard count == arrayOfValues.count else { - return false - } - - for (i, value) in enumerated() { - if !value.isTestEquivalent(to: arrayOfValues[i]) { - return false - } - } - - return true - } -} - -extension Value: Comparable { - public static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return lhs < rhs - case let (.i64(lhs), .i64(rhs)): return lhs < rhs - case let (.f32(lhs), .f32(rhs)): return lhs < rhs - case let (.f64(lhs), .f64(rhs)): return lhs < rhs - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value: Comparable` implementation") - } - } -} - -extension Value: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: BooleanLiteralType) { - if value { - self = .i32(1) - } else { - self = .i32(0) - } - } -} - -extension Value: CustomStringConvertible { - public var description: String { - switch self { - case .i32(let rawValue): return "I32(\(rawValue))" - case .i64(let rawValue): return "I64(\(rawValue))" - case .f32(let rawValue): return "F32(\(rawValue))" - case .f64(let rawValue): return "F64(\(rawValue))" - } - } -} - - -// Integers -/// - Note: -/// - -public enum IntValueType { - case i32 - case i64 -} - -public protocol RawUnsignedInteger: FixedWidthInteger & UnsignedInteger { - associatedtype Signed: RawSignedInteger where Signed.Unsigned == Self -} - -public protocol RawSignedInteger: FixedWidthInteger & SignedInteger { - associatedtype Unsigned: RawUnsignedInteger - init(bitPattern: Unsigned) -} - -extension UInt32: RawUnsignedInteger { - public typealias Signed = Int32 -} - -extension UInt64: RawUnsignedInteger { - public typealias Signed = Int64 -} - -extension Int32: RawSignedInteger {} -extension Int64: RawSignedInteger {} - -extension RawUnsignedInteger { - var signed: Signed { - return self > Signed.max ? -Signed(Self.max - self) - 1 : Signed(self) - } -} - -extension RawSignedInteger { - var unsigned: Unsigned { - return self < 0 ? Unsigned.max - Unsigned(-(self + 1)) : Unsigned(self) - } -} - -// Floating-Point -/// - Note: -/// - -public enum FloatValueType { - case f32 - case f64 -} - -protocol ByteConvertible { - init(_ bytes: T, _ type: ValueType) where T.Element == UInt8 - - var bytes: [UInt8] { get } -} - -extension Value: ByteConvertible { - init(_ bytes: T, _ type: ValueType) where T.Element == UInt8 { - switch type { - case .int(.i32): self = .i32(UInt32(littleEndian: bytes)) - case .int(.i64): self = .i64(UInt64(littleEndian: bytes)) - case .float(.f32): self = .f32(Float32(bitPattern: UInt32(bigEndian: bytes))) - case .float(.f64): self = .f64(Float64(bitPattern: UInt64(bigEndian: bytes))) - } - } - - var bytes: [UInt8] { - switch self { - case let .i32(rawValue): return rawValue.littleEndianBytes - case let .i64(rawValue): return rawValue.littleEndianBytes - case let .f32(rawValue): return rawValue.bitPattern.bigEndianBytes - case let .f64(rawValue): return rawValue.bitPattern.bigEndianBytes - } - } -} - -extension FixedWidthInteger { - init(littleEndian bytes: T) where T.Element == UInt8 { - self.init(bigEndian: bytes.reversed()) - } - - var littleEndianBytes: [UInt8] { - return (0 ..< Self.bitWidth / 8).map { UInt8(truncatingIfNeeded: self >> $0) } - } - - init(bigEndian bytes: T) where T.Element == UInt8 { - self = bytes.reduce(into: Self()) { acc, next in - acc <<= 8 - acc |= Self(next) - } - } - - var bigEndianBytes: [UInt8] { - return littleEndianBytes.reversed() - } -} - -extension Array where Element == ValueType { - static func == (lhs: [ValueType], rhs: [ValueType]) -> Bool { - guard lhs.count == rhs.count else { return false } - return zip(lhs, rhs).reduce(true) { result, zipped in - result && zipped.0 == zipped.1 - } - } - - static func != (lhs: [ValueType], rhs: [ValueType]) -> Bool { - return !(lhs == rhs) - } -} - -// MARK: Arithmetic - -extension Value { - var abs: Value { - switch self { - case let .f32(rawValue): return .f32(Swift.abs(rawValue)) - case let .f64(rawValue): return .f64(Swift.abs(rawValue)) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var isZero: Bool { - switch self { - case let .i32(rawValue): return rawValue == 0 - case let .i64(rawValue): return rawValue == 0 - case let .f32(rawValue): return rawValue.isZero - case let .f64(rawValue): return rawValue.isZero - } - } - - var ceil: Value { - switch self { - case var .f32(rawValue): - rawValue.round(.up) - return .f32(rawValue) - case var .f64(rawValue): - rawValue.round(.up) - return .f64(rawValue) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var floor: Value { - switch self { - case var .f32(rawValue): - rawValue.round(.down) - return .f32(rawValue) - case var .f64(rawValue): - rawValue.round(.down) - return .f64(rawValue) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var truncate: Value { - switch self { - case var .f32(rawValue): - rawValue.round(.towardZero) - return .f32(rawValue) - case var .f64(rawValue): - rawValue.round(.towardZero) - return .f64(rawValue) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var nearest: Value { - switch self { - case var .f32(rawValue): - rawValue.round(.toNearestOrEven) - return .f32(rawValue) - case var .f64(rawValue): - rawValue.round(.toNearestOrEven) - return .f64(rawValue) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var squareRoot: Value { - switch self { - case let .f32(rawValue): return .f32(rawValue.squareRoot()) - case let .f64(rawValue): return .f64(rawValue.squareRoot()) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var leadingZeroBitCount: Value { - switch self { - case let .i32(rawValue): return .i32(UInt32(rawValue.leadingZeroBitCount)) - case let .i64(rawValue): return .i64(UInt64(rawValue.leadingZeroBitCount)) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var trailingZeroBitCount: Value { - switch self { - case let .i32(rawValue): return .i32(UInt32(rawValue.trailingZeroBitCount)) - case let .i64(rawValue): return .i64(UInt64(rawValue.trailingZeroBitCount)) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - var nonzeroBitCount: Value { - switch self { - case let .i32(rawValue): return .i32(UInt32(rawValue.nonzeroBitCount)) - case let .i64(rawValue): return .i64(UInt64(rawValue.nonzeroBitCount)) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - func rotl(_ l: Self) -> Self { - switch (self, l) { - case let (.i32(rawValue), .i32(l)): - let shift = l % UInt32(type.bitWidth) - return .i32(rawValue << shift | rawValue >> (32 - shift)) - case let (.i64(rawValue), .i64(l)): - let shift = l % UInt64(type.bitWidth) - return .i64(rawValue << shift | rawValue >> (64 - shift)) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - func rotr(_ r: Self) -> Self { - switch (self, r) { - case let (.i32(rawValue), .i32(r)): - let shift = r % UInt32(type.bitWidth) - return .i32(rawValue >> shift | rawValue << (32 - shift)) - case let (.i64(rawValue), .i64(r)): - let shift = r % UInt64(type.bitWidth) - return .i64(rawValue >> shift | rawValue << (64 - shift)) - default: fatalError("Invalid type \(type) for `Value.\(#function)` implementation") - } - } - - prefix static func -(_ value: Self) -> Self { - switch value { - case let .f32(rawValue): return .f32(-rawValue) - case let .f64(rawValue): return .f64(-rawValue) - default: fatalError("Invalid type \(value.type) for prefix `Value.-` implementation") - } - } - - static func copySign(_ lhs: Self, _ rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.f32(lhs), .f32(rhs)): return .f32(lhs.sign == rhs.sign ? lhs : -lhs) - case let (.f64(lhs), .f64(rhs)): return .f64(lhs.sign == rhs.sign ? lhs : -lhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func + (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return .i32(lhs &+ rhs) - case let (.i64(lhs), .i64(rhs)): return .i64(lhs &+ rhs) - case let (.f32(lhs), .f32(rhs)): return .f32(lhs + rhs) - case let (.f64(lhs), .f64(rhs)): return .f64(lhs + rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func - (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return .i32(lhs &- rhs) - case let (.i64(lhs), .i64(rhs)): return .i64(lhs &- rhs) - case let (.f32(lhs), .f32(rhs)): return .f32(lhs - rhs) - case let (.f64(lhs), .f64(rhs)): return .f64(lhs - rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func * (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return .i32(lhs &* rhs) - case let (.i64(lhs), .i64(rhs)): return .i64(lhs &* rhs) - case let (.f32(lhs), .f32(rhs)): return .f32(lhs * rhs) - case let (.f64(lhs), .f64(rhs)): return .f64(lhs * rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func / (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.f32(lhs), .f32(rhs)): return .f32(lhs / rhs) - case let (.f64(lhs), .f64(rhs)): return .f64(lhs / rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func & (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return .i32(lhs & rhs) - case let (.i64(lhs), .i64(rhs)): return .i64(lhs & rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func | (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return .i32(lhs | rhs) - case let (.i64(lhs), .i64(rhs)): return .i64(lhs | rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func ^ (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): return .i32(lhs ^ rhs) - case let (.i64(lhs), .i64(rhs)): return .i64(lhs ^ rhs) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func << (lhs: Self, rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let shift = rhs % 32 - return .i32(lhs << shift) - case let (.i64(lhs), .i64(rhs)): - let shift = rhs % 64 - return .i64(lhs << shift) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func rightShiftSigned(_ lhs: Self, _ rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let shift = rhs.signed % 32 - return .i32((lhs.signed >> shift).unsigned) - case let (.i64(lhs), .i64(rhs)): - let shift = rhs.signed % 64 - return .i64((lhs.signed >> shift).unsigned) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func rightShiftUnsigned(_ lhs: Self, _ rhs: Self) -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let shift = rhs % 32 - return .i32(lhs >> shift) - case let (.i64(lhs), .i64(rhs)): - let shift = rhs % 64 - return .i64(lhs >> shift) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func divisionSigned(_ lhs: Self, _ rhs: Self) throws -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let (signed, overflow) = lhs.signed.dividedReportingOverflow(by: rhs.signed) - guard !overflow else { throw Trap.integerOverflowed } - return .i32(signed.unsigned) - case let (.i64(lhs), .i64(rhs)): - let (signed, overflow) = lhs.signed.dividedReportingOverflow(by: rhs.signed) - guard !overflow else { throw Trap.integerOverflowed } - return .i64(signed.unsigned) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func divisionUnsigned(_ lhs: Self, _ rhs: Self) throws -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let (signed, overflow) = lhs.dividedReportingOverflow(by: rhs) - guard !overflow else { throw Trap.integerOverflowed } - return .i32(signed) - case let (.i64(lhs), .i64(rhs)): - let (signed, overflow) = lhs.dividedReportingOverflow(by: rhs) - guard !overflow else { throw Trap.integerOverflowed } - return .i64(signed) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func remainderSigned(_ lhs: Self, _ rhs: Self) throws -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let (signed, overflow) = lhs.signed.remainderReportingOverflow(dividingBy: rhs.signed) - guard !overflow else { throw Trap.integerOverflowed } - return .i32(signed.unsigned) - case let (.i64(lhs), .i64(rhs)): - let (signed, overflow) = lhs.signed.remainderReportingOverflow(dividingBy: rhs.signed) - guard !overflow else { throw Trap.integerOverflowed } - return .i64(signed.unsigned) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } - - static func remainderUnsigned(_ lhs: Self, _ rhs: Self) throws -> Self { - switch (lhs, rhs) { - case let (.i32(lhs), .i32(rhs)): - let (signed, overflow) = lhs.remainderReportingOverflow(dividingBy: rhs) - guard !overflow else { throw Trap.integerOverflowed } - return .i32(signed) - case let (.i64(lhs), .i64(rhs)): - let (signed, overflow) = lhs.remainderReportingOverflow(dividingBy: rhs) - guard !overflow else { throw Trap.integerOverflowed } - return .i64(signed) - default: fatalError("Invalid types \(lhs.type) and \(rhs.type) for `Value.\(#function)` implementation") - } - } -} diff --git a/Sources/WAKit/Generated/AutoEquatable.generated.swift b/Sources/WAKit/Generated/AutoEquatable.generated.swift deleted file mode 100644 index e3607955..00000000 --- a/Sources/WAKit/Generated/AutoEquatable.generated.swift +++ /dev/null @@ -1,23 +0,0 @@ -// Generated using Sourcery 0.16.0 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT - -// MARK: - Frame AutoEquatable -extension Frame: Equatable {} -internal func == (lhs: Frame, rhs: Frame) -> Bool { - guard lhs.arity == rhs.arity else { return false } - guard lhs.module == rhs.module else { return false } - guard lhs.locals == rhs.locals else { return false } - return true -} - -// MARK: - ModuleInstance AutoEquatable -extension ModuleInstance: Equatable {} -public func == (lhs: ModuleInstance, rhs: ModuleInstance) -> Bool { - guard lhs.types == rhs.types else { return false } - guard lhs.functionAddresses == rhs.functionAddresses else { return false } - guard lhs.tableAddresses == rhs.tableAddresses else { return false } - guard lhs.memoryAddresses == rhs.memoryAddresses else { return false } - guard lhs.globalAddresses == rhs.globalAddresses else { return false } - guard lhs.exportInstances == rhs.exportInstances else { return false } - return true -} diff --git a/Sources/WAKit/Parser/Stream/FileHandleStream.swift b/Sources/WAKit/Parser/Stream/FileHandleStream.swift deleted file mode 100644 index 948e497f..00000000 --- a/Sources/WAKit/Parser/Stream/FileHandleStream.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation - -public final class FileHandleStream: ByteStream { - public var currentIndex: Int = 0 - - private let fileHandle: FileHandle - private let bufferLength: Int - - private var endOffset: Int = 0 - private var startOffset: Int = 0 - private var bytes: [UInt8] = [] - - public init(fileHandle: FileHandle, bufferLength: Int = 256) { - self.fileHandle = fileHandle - self.bufferLength = bufferLength - } - - private func readMoreIfNeeded() { - guard Int(endOffset) == currentIndex else { return } - startOffset = currentIndex - bytes = [UInt8](fileHandle.readData(ofLength: bufferLength)) - endOffset = startOffset + bytes.count - } - - @discardableResult - public func consumeAny() throws -> UInt8 { - guard let consumed = peek() else { - throw StreamError.unexpectedEnd(expected: nil) - } - currentIndex = bytes.index(after: currentIndex) - return consumed - } - - @discardableResult - public func consume(_ expected: Set) throws -> UInt8 { - guard let consumed = peek() else { - throw StreamError.unexpectedEnd(expected: Set(expected)) - } - guard expected.contains(consumed) else { - throw StreamError.unexpected(consumed, index: currentIndex, expected: Set(expected)) - } - currentIndex = bytes.index(after: currentIndex) - return consumed - } - - public func peek() -> UInt8? { - readMoreIfNeeded() - - let index = currentIndex - startOffset - guard bytes.indices.contains(index) else { - return nil - } - - return bytes[index] - } -} diff --git a/Sources/WAKit/Parser/Stream/Stream.swift b/Sources/WAKit/Parser/Stream/Stream.swift deleted file mode 100644 index 5d37203b..00000000 --- a/Sources/WAKit/Parser/Stream/Stream.swift +++ /dev/null @@ -1,30 +0,0 @@ -public enum StreamError: Swift.Error, Equatable where Element: Hashable { - case unexpectedEnd(expected: Set?) - case unexpected(Element, index: Int, expected: Set?) -} - -public protocol Stream { - associatedtype Element: Hashable - - var currentIndex: Int { get } - - func consumeAny() throws -> Element - func consume(_ expected: Set) throws -> Element - func consume(count: Int) throws -> [Element] - - func peek() -> Element? -} - -extension Stream { - public func consume(_ expected: Element) throws -> Element { - return try consume(Set([expected])) - } - - public func consume(count: Int) throws -> [Element] { - return try (0 ..< count).map { _ in try consumeAny() } - } - - public func hasReachedEnd() throws -> Bool { - return peek() == nil - } -} diff --git a/Sources/WAKit/Parser/Wasm/WasmParser.swift b/Sources/WAKit/Parser/Wasm/WasmParser.swift deleted file mode 100644 index fdf5e5f4..00000000 --- a/Sources/WAKit/Parser/Wasm/WasmParser.swift +++ /dev/null @@ -1,982 +0,0 @@ -import LEB - -public final class WasmParser { - public let stream: Stream - - public var currentIndex: Int { - return stream.currentIndex - } - - public init(stream: Stream) { - self.stream = stream - } -} - -extension WasmParser { - public static func parse(stream: Stream) throws -> Module { - let parser = WasmParser(stream: stream) - let module = try parser.parseModule() - return module - } -} - -public enum WasmParserError: Swift.Error { - case invalidMagicNumber([UInt8]) - case unknownVersion([UInt8]) - case invalidUTF8([UInt8]) - case invalidSectionSize(UInt32) - case zeroExpected(actual: UInt8, index: Int) - case inconsistentFunctionAndCodeLength(functionCount: Int, codeCount: Int) -} - -/// - Note: -/// -extension WasmParser { - func parseVector(content parser: () throws -> Content) throws -> [Content] { - var contents = [Content]() - let count: UInt32 = try parseUnsigned() - for _ in 0 ..< count { - contents.append(try parser()) - } - return contents - } -} - -/// - Note: -/// -extension WasmParser { - func parseUnsigned() throws -> T { - let sequence = AnySequence { [stream] in - AnyIterator { - try? stream.consumeAny() - } - } - return try T(LEB: sequence) - } - - func parseSigned() throws -> T { - let sequence = AnySequence { [stream] in - AnyIterator { - try? stream.consumeAny() - } - } - return try T(LEB: sequence) - } - - func parseInteger() throws -> T { - let signed: T.Signed = try parseSigned() - return signed.unsigned - } -} - -/// - Note: -/// -extension WasmParser { - func parseFloat() throws -> Float { - let bytes = try stream.consume(count: 4).reduce(UInt32(0)) { acc, byte in acc << 8 + UInt32(byte) } - return Float(bitPattern: bytes) - } - - func parseDouble() throws -> Double { - let bytes = try stream.consume(count: 8).reduce(UInt64(0)) { acc, byte in acc << 8 + UInt64(byte) } - return Double(bitPattern: bytes) - } -} - -/// - Note: -/// -extension WasmParser { - func parseName() throws -> String { - let bytes = try parseVector { () -> UInt8 in - try stream.consumeAny() - } - - var name = "" - - var iterator = bytes.makeIterator() - var decoder = UTF8() - Decode: while true { - switch decoder.decode(&iterator) { - case let .scalarValue(scalar): name.append(Character(scalar)) - case .emptyInput: break Decode - case .error: throw WasmParserError.invalidUTF8(bytes) - } - } - - return name - } -} - -/// - Note: -/// -extension WasmParser { - /// - Note: - /// - func parseValueType() throws -> ValueType { - let b = try stream.consume(Set(0x7C ... 0x7F)) - - switch b { - case 0x7F: - return .int(.i32) - case 0x7E: - return .int(.i64) - case 0x7D: - return .float(.f32) - case 0x7C: - return .float(.f64) - default: - throw StreamError.unexpected(b, index: currentIndex, expected: Set(0x7C ... 0x7F)) - } - } - - /// - Note: - /// - func parseResultType() throws -> ResultType { - switch stream.peek() { - case 0x40?: - _ = try stream.consumeAny() - return [] - default: - return [try parseValueType()] - } - } - - /// - Note: - /// - func parseFunctionType() throws -> FunctionType { - _ = try stream.consume(0x60) - - let parameters = try parseVector { try parseValueType() } - let results = try parseVector { try parseValueType() } - return FunctionType.some(parameters: parameters, results: results) - } - - /// - Note: - /// - func parseLimits() throws -> Limits { - let b = try stream.consume([0x00, 0x01]) - - switch b { - case 0x00: - return try Limits(min: parseUnsigned(), max: nil) - case 0x01: - return try Limits(min: parseUnsigned(), max: parseUnsigned()) - default: - preconditionFailure("should never reach here") - } - } - - /// - Note: - /// - func parseMemoryType() throws -> MemoryType { - return try parseLimits() - } - - /// - Note: - /// - func parseTableType() throws -> TableType { - let elementType: FunctionType - let b = try stream.consume(0x70) - - switch b { - case 0x70: - elementType = .any - default: - preconditionFailure("should never reach here") - } - - let limits = try parseLimits() - return TableType(elementType: elementType, limits: limits) - } - - /// - Note: - /// - func parseGlobalType() throws -> GlobalType { - let valueType = try parseValueType() - let mutability = try parseMutability() - return GlobalType(mutability: mutability, valueType: valueType) - } - - func parseMutability() throws -> Mutability { - let b = try stream.consume([0x00, 0x01]) - switch b { - case 0x00: - return .constant - case 0x01: - return .variable - default: - preconditionFailure("should never reach here") - } - } -} - -/// - Note: -/// -extension WasmParser { - func parseInstruction() throws -> [Instruction] { - let rawCode = try stream.consumeAny() - guard let code = InstructionCode(rawValue: rawCode) else { - throw StreamError.unexpected(rawCode, index: currentIndex, expected: []) - } - - let factory = InstructionFactory(code: code) - - switch code { - case .unreachable: - return [factory.unreachable] - case .nop: - return [factory.nop] - case .block: - let type = try parseResultType() - let (expression, _) = try parseExpression() - return factory.block(type: type, expression: expression) - case .loop: - let type = try parseResultType() - let (expression, _) = try parseExpression() - return factory.loop(type: type, expression: expression) - case .if: - let type = try parseResultType() - let (then, lastInstruction) = try parseExpression() - let `else`: Expression - switch lastInstruction.code { - case .else: - (`else`, _) = try parseExpression() - case .end: - `else` = Expression() - default: preconditionFailure("should never reach here") - } - return factory.if(type: type, then: then, else: `else`) - - case .else: - return [factory.else] - - case .end: - return [factory.end] - case .br: - let label: UInt32 = try parseUnsigned() - return [factory.br(label)] - case .br_if: - let label: UInt32 = try parseUnsigned() - return [factory.brIf(label)] - case .br_table: - let labelIndices: [UInt32] = try parseVector { try parseUnsigned() } - let labelIndex: UInt32 = try parseUnsigned() - return [factory.brTable(labelIndices, default: labelIndex)] - case .return: - return [factory.return] - case .call: - let index: UInt32 = try parseUnsigned() - return [factory.call(index)] - case .call_indirect: - let index: UInt32 = try parseUnsigned() - let zero = try stream.consumeAny() - guard zero == 0x00 else { - throw WasmParserError.zeroExpected(actual: zero, index: currentIndex) - } - return [factory.callIndirect(index)] - - case .drop: - return [factory.drop] - case .select: - return [factory.select] - - case .local_get: - let index: UInt32 = try parseUnsigned() - return [factory.localGet(index)] - case .local_set: - let index: UInt32 = try parseUnsigned() - return [factory.localSet(index)] - case .local_tee: - let index: UInt32 = try parseUnsigned() - return [factory.localTee(index)] - case .global_get: - let index: UInt32 = try parseUnsigned() - return [factory.globalGet(index)] - case .global_set: - let index: UInt32 = try parseUnsigned() - return [factory.globalSet(index)] - - case .i32_load: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i32), offset)] - case .i64_load: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), offset)] - case .f32_load: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.float(.f32), offset)] - case .f64_load: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.float(.f64), offset)] - case .i32_load8_s: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i32), bitWidth: 8, isSigned: true, offset)] - case .i32_load8_u: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i32), bitWidth: 8, isSigned: false, offset)] - case .i32_load16_s: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i32), bitWidth: 16, isSigned: true, offset)] - case .i32_load16_u: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i32), bitWidth: 16, isSigned: false, offset)] - case .i64_load8_s: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), bitWidth: 8, isSigned: true, offset)] - case .i64_load8_u: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), bitWidth: 8, isSigned: false, offset)] - case .i64_load16_s: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), bitWidth: 16, isSigned: true, offset)] - case .i64_load16_u: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), bitWidth: 16, isSigned: false, offset)] - case .i64_load32_s: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), bitWidth: 32, isSigned: true, offset)] - case .i64_load32_u: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.load(.int(.i64), bitWidth: 32, isSigned: false, offset)] - case .i32_store: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i32), offset)] - case .i64_store: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i64), offset)] - case .f32_store: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i32), offset)] - case .f64_store: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i64), offset)] - case .i32_store8: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i32), offset)] - case .i32_store16: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i32), offset)] - case .i64_store8: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i64), offset)] - case .i64_store16: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i64), offset)] - case .i64_store32: - let _: UInt32 = try parseUnsigned() - let offset: UInt32 = try parseUnsigned() - return [factory.store(.int(.i64), offset)] - case .memory_size: - let zero = try stream.consumeAny() - guard zero == 0x00 else { - throw WasmParserError.zeroExpected(actual: zero, index: currentIndex) - } - return [factory.memorySize] - case .memory_grow: - let zero = try stream.consumeAny() - guard zero == 0x00 else { - throw WasmParserError.zeroExpected(actual: zero, index: currentIndex) - } - return [factory.memoryGrow] - - case .i32_const: - let n: UInt32 = try parseInteger() - return [factory.const(.i32(n))] - case .i64_const: - let n: UInt64 = try parseInteger() - return [factory.const(.i64(n))] - case .f32_const: - let n = try parseFloat() - return [factory.const(.f32(n))] - case .f64_const: - let n = try parseDouble() - return [factory.const(.f64(n))] - - case .i32_eqz: - return [factory.numeric(intUnary: .eqz(.i32))] - case .i32_eq: - return [factory.numeric(binary: .eq(.int(.i32)))] - case .i32_ne: - return [factory.numeric(binary: .ne(.int(.i32)))] - case .i32_lt_s: - return [factory.numeric(intBinary: .ltS(.i32))] - case .i32_lt_u: - return [factory.numeric(intBinary: .ltU(.i32))] - case .i32_gt_s: - return [factory.numeric(intBinary: .gtS(.i32))] - case .i32_gt_u: - return [factory.numeric(intBinary: .gtU(.i32))] - case .i32_le_s: - return [factory.numeric(intBinary: .leS(.i32))] - case .i32_le_u: - return [factory.numeric(intBinary: .leU(.i32))] - case .i32_ge_s: - return [factory.numeric(intBinary: .geS(.i32))] - case .i32_ge_u: - return [factory.numeric(intBinary: .geU(.i32))] - - case .i64_eqz: - return [factory.numeric(intUnary: .eqz(.i64))] - case .i64_eq: - return [factory.numeric(binary: .eq(.int(.i64)))] - case .i64_ne: - return [factory.numeric(binary: .ne(.int(.i64)))] - case .i64_lt_s: - return [factory.numeric(intBinary: .ltS(.i64))] - case .i64_lt_u: - return [factory.numeric(intBinary: .ltU(.i64))] - case .i64_gt_s: - return [factory.numeric(intBinary: .gtS(.i64))] - case .i64_gt_u: - return [factory.numeric(intBinary: .gtU(.i64))] - case .i64_le_s: - return [factory.numeric(intBinary: .leS(.i64))] - case .i64_le_u: - return [factory.numeric(intBinary: .leU(.i64))] - case .i64_ge_s: - return [factory.numeric(intBinary: .geS(.i64))] - case .i64_ge_u: - return [factory.numeric(intBinary: .geU(.i64))] - - case .f32_eq: - return [factory.numeric(binary: .eq(.float(.f32)))] - case .f32_ne: - return [factory.numeric(binary: .ne(.float(.f32)))] - case .f32_lt: - return [factory.numeric(floatBinary: .lt(.f32))] - case .f32_gt: - return [factory.numeric(floatBinary: .gt(.f32))] - case .f32_le: - return [factory.numeric(floatBinary: .le(.f32))] - case .f32_ge: - return [factory.numeric(floatBinary: .ge(.f32))] - - case .f64_eq: - return [factory.numeric(binary: .eq(.float(.f64)))] - case .f64_ne: - return [factory.numeric(binary: .ne(.float(.f64)))] - case .f64_lt: - return [factory.numeric(floatBinary: .lt(.f64))] - case .f64_gt: - return [factory.numeric(floatBinary: .gt(.f64))] - case .f64_le: - return [factory.numeric(floatBinary: .le(.f64))] - case .f64_ge: - return [factory.numeric(floatBinary: .ge(.f64))] - - case .i32_clz: - return [factory.numeric(intUnary: .clz(.i32))] - case .i32_ctz: - return [factory.numeric(intUnary: .ctz(.i32))] - case .i32_popcnt: - return [factory.numeric(intUnary: .popcnt(.i32))] - case .i32_add: - return [factory.numeric(binary: .add(.int(.i32)))] - case .i32_sub: - return [factory.numeric(binary: .sub(.int(.i32)))] - case .i32_mul: - return [factory.numeric(binary: .mul(.int(.i32)))] - case .i32_div_s: - return [factory.numeric(intBinary: .divS(.i32))] - case .i32_div_u: - return [factory.numeric(intBinary: .divU(.i32))] - case .i32_rem_s: - return [factory.numeric(intBinary: .remS(.i32))] - case .i32_rem_u: - return [factory.numeric(intBinary: .remU(.i32))] - case .i32_and: - return [factory.numeric(intBinary: .and(.i32))] - case .i32_or: - return [factory.numeric(intBinary: .or(.i32))] - case .i32_xor: - return [factory.numeric(intBinary: .xor(.i32))] - case .i32_shl: - return [factory.numeric(intBinary: .shl(.i32))] - case .i32_shr_s: - return [factory.numeric(intBinary: .shrS(.i32))] - case .i32_shr_u: - return [factory.numeric(intBinary: .shrU(.i32))] - case .i32_rotl: - return [factory.numeric(intBinary: .rotl(.i32))] - case .i32_rotr: - return [factory.numeric(intBinary: .rotr(.i32))] - - case .i64_clz: - return [factory.numeric(intUnary: .clz(.i64))] - case .i64_ctz: - return [factory.numeric(intUnary: .ctz(.i64))] - case .i64_popcnt: - return [factory.numeric(intUnary: .popcnt(.i64))] - case .i64_add: - return [factory.numeric(binary: .add(.int(.i64)))] - case .i64_sub: - return [factory.numeric(binary: .sub(.int(.i64)))] - case .i64_mul: - return [factory.numeric(binary: .mul(.int(.i64)))] - case .i64_div_s: - return [factory.numeric(intBinary: .divS(.i64))] - case .i64_div_u: - return [factory.numeric(intBinary: .divU(.i64))] - case .i64_rem_s: - return [factory.numeric(intBinary: .remS(.i64))] - case .i64_rem_u: - return [factory.numeric(intBinary: .remU(.i64))] - case .i64_and: - return [factory.numeric(intBinary: .and(.i64))] - case .i64_or: - return [factory.numeric(intBinary: .or(.i64))] - case .i64_xor: - return [factory.numeric(intBinary: .xor(.i64))] - case .i64_shl: - return [factory.numeric(intBinary: .shl(.i64))] - case .i64_shr_s: - return [factory.numeric(intBinary: .shrS(.i64))] - case .i64_shr_u: - return [factory.numeric(intBinary: .shrU(.i64))] - case .i64_rotl: - return [factory.numeric(intBinary: .rotl(.i64))] - case .i64_rotr: - return [factory.numeric(intBinary: .rotr(.i64))] - - case .f32_abs: - return [factory.numeric(floatUnary: .abs(.f32))] - case .f32_neg: - return [factory.numeric(floatUnary: .neg(.f32))] - case .f32_ceil: - return [factory.numeric(floatUnary: .ceil(.f32))] - case .f32_floor: - return [factory.numeric(floatUnary: .floor(.f32))] - case .f32_trunc: - return [factory.numeric(floatUnary: .trunc(.f32))] - case .f32_nearest: - return [factory.numeric(floatUnary: .nearest(.f32))] - case .f32_sqrt: - return [factory.numeric(floatUnary: .sqrt(.f32))] - - case .f32_add: - return [factory.numeric(binary: .add(.float(.f32)))] - case .f32_sub: - return [factory.numeric(binary: .sub(.float(.f32)))] - case .f32_mul: - return [factory.numeric(binary: .mul(.float(.f32)))] - case .f32_div: - return [factory.numeric(floatBinary: .div(.f32))] - case .f32_min: - return [factory.numeric(floatBinary: .min(.f32))] - case .f32_max: - return [factory.numeric(floatBinary: .max(.f32))] - case .f32_copysign: - return [factory.numeric(floatBinary: .copysign(.f32))] - - case .f64_abs: - return [factory.numeric(floatUnary: .abs(.f64))] - case .f64_neg: - return [factory.numeric(floatUnary: .neg(.f64))] - case .f64_ceil: - return [factory.numeric(floatUnary: .ceil(.f64))] - case .f64_floor: - return [factory.numeric(floatUnary: .floor(.f64))] - case .f64_trunc: - return [factory.numeric(floatUnary: .trunc(.f64))] - case .f64_nearest: - return [factory.numeric(floatUnary: .nearest(.f64))] - case .f64_sqrt: - return [factory.numeric(floatUnary: .sqrt(.f64))] - - case .f64_add: - return [factory.numeric(binary: .add(.float(.f64)))] - case .f64_sub: - return [factory.numeric(binary: .sub(.float(.f64)))] - case .f64_mul: - return [factory.numeric(binary: .mul(.float(.f64)))] - case .f64_div: - return [factory.numeric(floatBinary: .div(.f64))] - case .f64_min: - return [factory.numeric(floatBinary: .min(.f64))] - case .f64_max: - return [factory.numeric(floatBinary: .max(.f64))] - case .f64_copysign: - return [factory.numeric(floatBinary: .copysign(.f64))] - - case .i32_wrap_i64: - return [factory.numeric(conversion: .wrap)] - case .i32_trunc_f32_s: - return [factory.numeric(conversion: .truncS(.i32, .f32))] - case .i32_trunc_f32_u: - return [factory.numeric(conversion: .truncU(.i32, .f32))] - case .i32_trunc_f64_s: - return [factory.numeric(conversion: .truncS(.i32, .f64))] - case .i32_trunc_f64_u: - return [factory.numeric(conversion: .truncU(.i32, .f64))] - case .i64_extend_i32_s: - return [factory.numeric(conversion: .extendS)] - case .i64_extend_i32_u: - return [factory.numeric(conversion: .extendU)] - case .i64_trunc_f32_s: - return [factory.numeric(conversion: .truncS(.i64, .f32))] - case .i64_trunc_f32_u: - return [factory.numeric(conversion: .truncU(.i64, .f32))] - case .i64_trunc_f64_s: - return [factory.numeric(conversion: .truncS(.i64, .f64))] - case .i64_trunc_f64_u: - return [factory.numeric(conversion: .truncU(.i64, .f64))] - case .f32_convert_i32_s: - return [factory.numeric(conversion: .convertS(.f32, .i32))] - case .f32_convert_i32_u: - return [factory.numeric(conversion: .convertU(.f32, .i32))] - case .f32_convert_i64_s: - return [factory.numeric(conversion: .convertS(.f32, .i64))] - case .f32_convert_i64_u: - return [factory.numeric(conversion: .convertU(.f32, .i64))] - case .f32_demote_f64: - return [factory.numeric(conversion: .demote)] - case .f64_convert_i32_s: - return [factory.numeric(conversion: .convertS(.f64, .i32))] - case .f64_convert_i32_u: - return [factory.numeric(conversion: .convertU(.f64, .i32))] - case .f64_convert_i64_s: - return [factory.numeric(conversion: .convertS(.f64, .i64))] - case .f64_convert_i64_u: - return [factory.numeric(conversion: .convertU(.f64, .i64))] - case .f64_promote_f32: - return [factory.numeric(conversion: .promote)] - case .i32_reinterpret_f32: - return [factory.numeric(conversion: .reinterpret(.int(.i32), .float(.f32)))] - case .i64_reinterpret_f64: - return [factory.numeric(conversion: .reinterpret(.int(.i64), .float(.f64)))] - case .f32_reinterpret_i32: - return [factory.numeric(conversion: .reinterpret(.float(.f32), .int(.i32)))] - case .f64_reinterpret_i64: - return [factory.numeric(conversion: .reinterpret(.float(.f64), .int(.i64)))] - } - } - - func parseExpression() throws -> (expression: Expression, lastInstruction: Instruction) { - var instructions: [Instruction] = [] - - repeat { - instructions.append(contentsOf: try parseInstruction()) - } while instructions.last?.isPseudo != true - let last = instructions.removeLast() - - return (Expression(instructions: instructions), last) - } -} - -/// - Note: -/// -extension WasmParser { - /// - Note: - /// - func parseCustomSection() throws -> Section { - _ = try stream.consume(0) - let size: UInt32 = try parseUnsigned() - - let name = try parseName() - guard size > name.utf8.count else { - throw WasmParserError.invalidSectionSize(size) - } - let contentSize = Int(size) - name.utf8.count - - var bytes = [UInt8]() - for _ in 0 ..< contentSize { - bytes.append(try stream.consumeAny()) - } - - return .custom(name: name, bytes: bytes) - } - - /// - Note: - /// - func parseTypeSection() throws -> Section { - _ = try stream.consume(1) - /* size */ _ = try parseUnsigned() as UInt32 - return .type(try parseVector { try parseFunctionType() }) - } - - /// - Note: - /// - func parseImportSection() throws -> Section { - _ = try stream.consume(2) - /* size */ _ = try parseUnsigned() as UInt32 - - let imports: [Import] = try parseVector { - let module = try parseName() - let name = try parseName() - let descriptor = try parseImportDescriptor() - return Import(module: module, name: name, descripter: descriptor) - } - return .import(imports) - } - - /// - Note: - /// - func parseImportDescriptor() throws -> ImportDescriptor { - let b = try stream.consume(Set(0x00 ... 0x03)) - switch b { - case 0x00: - return try .function(parseUnsigned()) - case 0x01: - return try .table(parseTableType()) - case 0x02: - return try .memory(parseMemoryType()) - case 0x03: - return try .global(parseGlobalType()) - default: - preconditionFailure("should never reach here") - } - } - - /// - Note: - /// - func parseFunctionSection() throws -> Section { - _ = try stream.consume(3) - /* size */ _ = try parseUnsigned() as UInt32 - return .function(try parseVector { try parseUnsigned() }) - } - - /// - Note: - /// - func parseTableSection() throws -> Section { - _ = try stream.consume(4) - /* size */ _ = try parseUnsigned() as UInt32 - - return .table(try parseVector { Table(type: try parseTableType()) }) - } - - /// - Note: - /// - func parseMemorySection() throws -> Section { - _ = try stream.consume(5) - /* size */ _ = try parseUnsigned() as UInt32 - - return .memory(try parseVector { Memory(type: try parseLimits()) }) - } - - /// - Note: - /// - func parseGlobalSection() throws -> Section { - _ = try stream.consume(6) - /* size */ _ = try parseUnsigned() as UInt32 - - return .global(try parseVector { - let type = try parseGlobalType() - let (expression, _) = try parseExpression() - return Global(type: type, initializer: expression) - }) - } - - /// - Note: - /// - func parseExportSection() throws -> Section { - _ = try stream.consume(7) - /* size */ _ = try parseUnsigned() as UInt32 - - return .export(try parseVector { - let name = try parseName() - let descriptor = try parseExportDescriptor() - return Export(name: name, descriptor: descriptor) - }) - } - - /// - Note: - /// - func parseExportDescriptor() throws -> ExportDescriptor { - let b = try stream.consume(Set(0x00 ... 0x03)) - switch b { - case 0x00: - return try .function(parseUnsigned()) - case 0x01: - return try .table(parseUnsigned()) - case 0x02: - return try .memory(parseUnsigned()) - case 0x03: - return try .global(parseUnsigned()) - default: - preconditionFailure("should never reach here") - } - } - - /// - Note: - /// - func parseStartSection() throws -> Section { - _ = try stream.consume(8) - /* size */ _ = try parseUnsigned() as UInt32 - - return .start(try parseUnsigned()) - } - - /// - Note: - /// - func parseElementSection() throws -> Section { - _ = try stream.consume(9) - /* size */ _ = try parseUnsigned() as UInt32 - - return .element(try parseVector { - let index: UInt32 = try parseUnsigned() - let (expression, _) = try parseExpression() - let initializer: [UInt32] = try parseVector { try parseUnsigned() } - return Element(index: index, offset: expression, initializer: initializer) - }) - } - - /// - Note: - /// - func parseCodeSection() throws -> Section { - _ = try stream.consume(10) - /* size */ _ = try parseUnsigned() as UInt32 - - return .code(try parseVector { - _ = try parseUnsigned() as UInt32 - let locals = try parseVector { () -> [ValueType] in - let n: UInt32 = try parseUnsigned() - let t = try parseValueType() - return (0 ..< n).map { _ in t } - } - let (expression, _) = try parseExpression() - return Code(locals: locals.flatMap { $0 }, expression: expression) - }) - } - - /// - Note: - /// - func parseDataSection() throws -> Section { - _ = try stream.consume(11) - /* size */ _ = try parseUnsigned() as UInt32 - - return .data(try parseVector { - let index: UInt32 = try parseUnsigned() - let (offset, _) = try parseExpression() - let initializer = try parseVector { try stream.consumeAny() } - return Data(index: index, offset: offset, initializer: initializer) - }) - } -} - -/// - Note: -/// -extension WasmParser { - /// - Note: - /// - func parseMagicNumber() throws { - let magicNumber = try stream.consume(count: 4) - guard magicNumber == [0x00, 0x61, 0x73, 0x6D] else { - throw WasmParserError.invalidMagicNumber(magicNumber) - } - } - - /// - Note: - /// - func parseVersion() throws { - let version = try stream.consume(count: 4) - guard version == [0x01, 0x00, 0x00, 0x00] else { - throw WasmParserError.unknownVersion(version) - } - } - - /// - Note: - /// - func parseModule() throws -> Module { - try parseMagicNumber() - try parseVersion() - - var module = Module() - - var typeIndices = [TypeIndex]() - var codes = [Code]() - - let ids: ClosedRange = 0 ... 11 - for i in ids { - guard let sectionID = stream.peek(), ids.contains(sectionID) else { - break - } - - switch sectionID { - case 0: - _ = try? parseCustomSection() - case 1 where sectionID == i: - if case let .type(types) = try parseTypeSection() { - module.types = types - } - case 2 where sectionID == i: - if case let .import(imports) = try parseImportSection() { - module.imports = imports - } - case 3 where sectionID == i: - if case let .function(_typeIndices) = try parseFunctionSection() { - typeIndices = _typeIndices - } - case 4 where sectionID == i: - if case let .table(tables) = try parseTableSection() { - module.tables = tables - } - case 5 where sectionID == i: - if case let .memory(memory) = try parseMemorySection() { - module.memories = memory - } - case 6 where sectionID == i: - if case let .global(globals) = try parseGlobalSection() { - module.globals = globals - } - case 7 where sectionID == i: - if case let .export(exports) = try parseExportSection() { - module.exports = exports - } - case 8 where sectionID == i: - if case let .start(start) = try parseStartSection() { - module.start = start - } - case 9 where sectionID == i: - if case let .element(elements) = try parseElementSection() { - module.elements = elements - } - case 10 where sectionID == i: - if case let .code(_codes) = try parseCodeSection() { - codes = _codes - } - case 11 where sectionID == i: - if case let .data(data) = try parseDataSection() { - module.data = data - } - default: - continue - } - } - - guard typeIndices.count == codes.count else { - throw WasmParserError.inconsistentFunctionAndCodeLength( - functionCount: typeIndices.count, - codeCount: codes.count - ) - } - - let functions = codes.enumerated().map { index, code in - Function(type: typeIndices[index], locals: code.locals, body: code.expression) - } - module.functions = functions - - return module - } -} diff --git a/Sources/WAKit/Types/Instructions/NumericInstruction.swift b/Sources/WAKit/Types/Instructions/NumericInstruction.swift deleted file mode 100644 index 699d67fa..00000000 --- a/Sources/WAKit/Types/Instructions/NumericInstruction.swift +++ /dev/null @@ -1,497 +0,0 @@ -/// Numeric Instructions -/// - Note: -/// -enum NumericInstruction { - enum Constant { - case const(Value) - } - - enum IntUnary { - // iunop - case clz(IntValueType) - case ctz(IntValueType) - case popcnt(IntValueType) - - /// itestop - case eqz(IntValueType) - - var type: ValueType { - switch self { - case let .clz(type), - let .ctz(type), - let .popcnt(type), - let .eqz(type): - return .int(type) - } - } - - func callAsFunction(_ value: Value) -> Value { - switch self { - case .clz: - return value.leadingZeroBitCount - case .ctz: - return value.trailingZeroBitCount - case .popcnt: - return value.nonzeroBitCount - - case .eqz: - return value.isZero ? true : false - } - } - } - - enum FloatUnary { - // funop - case abs(FloatValueType) - case neg(FloatValueType) - case ceil(FloatValueType) - case floor(FloatValueType) - case trunc(FloatValueType) - case nearest(FloatValueType) - case sqrt(FloatValueType) - - var type: ValueType { - switch self { - - case let .abs(type), - let .neg(type), - let .ceil(type), - let .floor(type), - let .trunc(type), - let .nearest(type), - let .sqrt(type): - return .float(type) - } - } - - func callAsFunction( - _ value: Value - ) -> Value { - switch self { - case .abs: - return value.abs - case .neg: - return -value - case .ceil: - return value.ceil - case .floor: - return value.floor - case .trunc: - return value.truncate - case .nearest: - return value.nearest - case .sqrt: - return value.squareRoot - } - } - - } - - enum Binary { - // binop - case add(ValueType) - case sub(ValueType) - case mul(ValueType) - - // relop - case eq(ValueType) - case ne(ValueType) - - var type: ValueType { - switch self { - case let .add(type), - let .sub(type), - let .mul(type), - let .eq(type), - let .ne(type): - return type - } - } - - func callAsFunction(_ value1: Value, _ value2: Value) -> Value { - switch self { - case .add: - return value1 + value2 - case .sub: - return value1 - value2 - case .mul: - return value1 * value2 - - case .eq: - return value1 == value2 ? true : false - case .ne: - return value1 == value2 ? false : true - } - } - } - - enum IntBinary { - // ibinop - case divS(IntValueType) - case divU(IntValueType) - case remS(IntValueType) - case remU(IntValueType) - case and(IntValueType) - case or(IntValueType) - case xor(IntValueType) - case shl(IntValueType) - case shrS(IntValueType) - case shrU(IntValueType) - case rotl(IntValueType) - case rotr(IntValueType) - - // irelop - case ltS(IntValueType) - case ltU(IntValueType) - case gtS(IntValueType) - case gtU(IntValueType) - case leS(IntValueType) - case leU(IntValueType) - case geS(IntValueType) - case geU(IntValueType) - - var type: ValueType { - switch self { - case let .divS(type), - let .divU(type), - let .remS(type), - let .remU(type), - let .and(type), - let .or(type), - let .xor(type), - let .shl(type), - let .shrS(type), - let .shrU(type), - let .rotl(type), - let .rotr(type), - let .ltS(type), - let .ltU(type), - let .gtS(type), - let .gtU(type), - let .leS(type), - let .leU(type), - let .geS(type), - let .geU(type): - return .int(type) - } - } - - func callAsFunction( - _ type: ValueType, - _ value1: Value, - _ value2: Value - ) throws -> Value { - switch (self, type) { - case (.divS, _): - guard !value2.isZero else { throw Trap.integerDividedByZero } - return try Value.divisionSigned(value1, value2) - case (.divU, _): - guard !value2.isZero else { throw Trap.integerDividedByZero } - return try Value.divisionUnsigned(value1, value2) - case (.remS, _): - guard !value2.isZero else { throw Trap.integerDividedByZero } - return try Value.remainderSigned(value1, value2) - case (.remU, _): - guard !value2.isZero else { throw Trap.integerDividedByZero } - return try Value.remainderUnsigned(value1, value2) - case (.and, _): - return value1 & value2 - case (.or, _): - return value1 | value2 - case (.xor, _): - return value1 ^ value2 - case (.shl, _): - return value1 << value2 - case (.shrS, _): - return Value.rightShiftSigned(value1, value2) - case (.shrU, _): - return Value.rightShiftUnsigned(value1, value2) - case (.rotl, _): - return value1.rotr(value2) - case (.rotr, _): - return value1.rotr(value2) - - case (.ltS, .int(.i32)): - return value1.i32.signed < value2.i32.signed ? true : false - case (.ltU, .int(.i32)): - return value1.i32 < value2.i32 ? true : false - case (.gtS, .int(.i32)): - return value1.i32.signed > value2.i32.signed ? true : false - case (.gtU, .int(.i32)): - return value1.i32 > value2.i32 ? true : false - case (.leS, .int(.i32)): - return value1.i32.signed <= value2.i32.signed ? true : false - case (.leU, .int(.i32)): - return value1.i32 <= value2.i32 ? true : false - case (.geS, .int(.i32)): - return value1.i32.signed >= value2.i32.signed ? true : false - case (.geU, .int(.i32)): - return value1.i32 >= value2.i32 ? true : false - - case (.ltS, .int(.i64)): - return value1.i64.signed < value2.i64.signed ? true : false - case (.ltU, .int(.i64)): - return value1.i64 < value2.i64 ? true : false - case (.gtS, .int(.i64)): - return value1.i64.signed > value2.i32.signed ? true : false - case (.gtU, .int(.i64)): - return value1.i64 > value2.i64 ? true : false - case (.leS, .int(.i64)): - return value1.i64.signed <= value2.i64.signed ? true : false - case (.leU, .int(.i64)): - return value1.i64 <= value2.i64 ? true : false - case (.geS, .int(.i64)): - return value1.i32.signed >= value2.i32.signed ? true : false - case (.geU, .int(.i64)): - return value1.i64 >= value2.i64 ? true : false - - default: - fatalError("Invalid type \(type) for instruction \(self)") - } - } - } - - enum FloatBinary { - // fbinop - case div(FloatValueType) - case min(FloatValueType) - case max(FloatValueType) - case copysign(FloatValueType) - - // frelop - case lt(FloatValueType) - case gt(FloatValueType) - case le(FloatValueType) - case ge(FloatValueType) - - var type: ValueType { - switch self { - case let .div(type), - let .min(type), - let .max(type), - let .copysign(type), - let .lt(type), - let .gt(type), - let .le(type), - let .ge(type): - return .float(type) - } - } - - func callAsFunction(_ value1: Value, _ value2: Value) throws -> Value { - switch self { - case .div: - guard !value2.isZero else { throw Trap.integerDividedByZero } - return value1 / value2 - case .min: - return Swift.min(value1, value2) - case .max: - return Swift.max(value1, value2) - case .copysign: - return .copySign(value1, value2) - case .lt: - return value1 < value2 ? true : false - case .gt: - return value1 > value2 ? true : false - case .le: - return value1 <= value2 ? true : false - case .ge: - return value1 >= value2 ? true : false - } - } - } -} - -extension Instruction: Equatable { - public static func == (lhs: Instruction, rhs: Instruction) -> Bool { - // TODO: Compare with instruction arguments - return lhs.code == rhs.code - } -} - -extension NumericInstruction { - enum Conversion { - case wrap - case extendS - case extendU - case truncS(IntValueType, FloatValueType) - case truncU(IntValueType, FloatValueType) - case convertS(FloatValueType, IntValueType) - case convertU(FloatValueType, IntValueType) - case demote - case promote - case reinterpret(ValueType, ValueType) - - var types: (ValueType, ValueType) { - switch self { - case .wrap: - return (.int(.i32), .int(.i64)) - case .extendS: - return (.int(.i64), .int(.i32)) - case .extendU: - return (.int(.i64), .int(.i32)) - case let .truncS(type1, type2): - return (.int(type1), .float(type2)) - case let .truncU(type1, type2): - return (.int(type1), .float(type2)) - case let .convertS(type1, type2): - return (.float(type1), .int(type2)) - case let .convertU(type1, type2): - return (.float(type1), .int(type2)) - case .demote: - return (.float(.f32), .float(.f64)) - case .promote: - return (.float(.f64), .float(.f32)) - case let .reinterpret(type1, type2): - return (type1, type2) - } - } - - func callAsFunction( - _ value: Value - ) throws -> Value { - switch self { - case .wrap: - switch value { - case let .i64(rawValue): - return .i32(UInt32(truncatingIfNeeded: rawValue)) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case .extendS: - switch value { - case let .i32(rawValue): - return .i64(UInt64(bitPattern: Int64(rawValue.signed))) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case .extendU: - switch value { - case let .i32(rawValue): - return .i64(UInt64(rawValue)) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case let .truncS(target, _): - switch (target, value) { - case (.i32, let .f32(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(signed: Int32(rawValue)) - - case (.i32, let .f64(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(signed: Int32(rawValue)) - - case (.i64, let .f32(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(signed: Int64(rawValue)) - - case (.i64, let .f64(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(signed: Int64(rawValue)) - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case let .truncU(target, _): - switch (target, value) { - case (.i32, let .f32(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(UInt32(rawValue)) - - case (.i32, let .f64(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(UInt32(rawValue)) - - case (.i64, let .f32(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(UInt64(rawValue)) - - case (.i64, let .f64(rawValue)): - guard !rawValue.isNaN else { throw Trap.invalidConversionToInteger } - return Value(UInt64(rawValue)) - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case let .convertS(target, _): - switch (target, value) { - case (.f32, let .i32(rawValue)): - return .f32(Float32(rawValue.signed)) - - case (.f32, let .i64(rawValue)): - return .f32(Float32(rawValue.signed)) - - case (.f64, let .i32(rawValue)): - return .f64(Float64(rawValue.signed)) - - case (.f64, let .i64(rawValue)): - return .f64(Float64(rawValue.signed)) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case let .convertU(target, _): - switch (target, value) { - case (.f32, let .i32(rawValue)): - return .f32(Float32(rawValue)) - - case (.f32, let .i64(rawValue)): - return .f32(Float32(rawValue)) - - case (.f64, let .i32(rawValue)): - return .f64(Float64(rawValue)) - - case (.f64, let .i64(rawValue)): - return .f64(Float64(rawValue)) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - - case .demote: - switch value { - case let .f64(rawValue): - return .f32(Float32(rawValue)) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case .promote: - switch value { - case let .f32(rawValue): - return .f64(Float64(rawValue)) - - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - - case let .reinterpret(target, _): - switch (target, value) { - case (.int(.i32), let .f32(rawValue)): - return .i32(rawValue.bitPattern) - - case (.int(.i64), let .f64(rawValue)): - return .i64(rawValue.bitPattern) - - case (.float(.f32), let .i32(rawValue)): - return .f32(Float32(bitPattern: rawValue)) - - case (.float(.f64), let .i64(rawValue)): - return .f64(Float64(bitPattern: rawValue)) - default: - fatalError("unsupported operand types passed to instruction \(self)") - } - } - } - } -} diff --git a/Sources/WAKit/Types/Module.swift b/Sources/WAKit/Types/Module.swift deleted file mode 100644 index d9d8ebad..00000000 --- a/Sources/WAKit/Types/Module.swift +++ /dev/null @@ -1,135 +0,0 @@ -/// - Note: -/// -public struct Module: Equatable { - var types: [FunctionType] - var functions: [Function] - var tables: [Table] - var memories: [Memory] - var globals: [Global] - var elements: [Element] - var data: [Data] - var start: FunctionIndex? - var imports: [Import] - var exports: [Export] - - public init( - types: [FunctionType] = [], - functions: [Function] = [], - tables: [Table] = [], - memories: [Memory] = [], - globals: [Global] = [], - elements: [Element] = [], - data: [Data] = [], - start: FunctionIndex? = nil, - imports: [Import] = [], - exports: [Export] = [] - ) { - self.types = types - self.functions = functions - self.tables = tables - self.memories = memories - self.globals = globals - self.elements = elements - self.data = data - self.start = start - self.imports = imports - self.exports = exports - } -} - -public enum Section: Equatable { - case custom(name: String, bytes: [UInt8]) - case type([FunctionType]) - case `import`([Import]) - case function([TypeIndex]) - case table([Table]) - case memory([Memory]) - case global([Global]) - case export([Export]) - case start(FunctionIndex) - case element([Element]) - case code([Code]) - case data([Data]) -} - -/// - Note: -/// -public typealias TypeIndex = UInt32 -public typealias FunctionIndex = UInt32 -public typealias TableIndex = UInt32 -public typealias MemoryIndex = UInt32 -public typealias GlobalIndex = UInt32 -public typealias LocalIndex = UInt32 -public typealias LabelIndex = UInt32 - -/// - Note: -/// -public struct Function: Equatable { - let type: TypeIndex - let locals: [ValueType] - let body: Expression -} - -/// - Note: -/// -public struct Table: Equatable { - let type: TableType -} - -/// - Note: -/// -public struct Memory: Equatable { - let type: MemoryType -} - -/// - Note: -/// -public struct Global: Equatable { - let type: GlobalType - let initializer: Expression -} - -/// - Note: -/// -public struct Element: Equatable { - let index: TableIndex - let offset: Expression - let initializer: [FunctionIndex] -} - -/// - Note: -/// -public struct Data: Equatable { - let index: MemoryIndex - let offset: Expression - let initializer: [UInt8] -} - -/// - Note: -/// -public struct Export: Equatable { - let name: String - let descriptor: ExportDescriptor -} - -public enum ExportDescriptor: Equatable { - case function(FunctionIndex) - case table(TableIndex) - case memory(MemoryIndex) - case global(GlobalIndex) -} - -/// - Note: -/// -public struct Import: Equatable { - let module: String - let name: String - let descripter: ImportDescriptor -} - -public enum ImportDescriptor: Equatable { - case function(TypeIndex) - case table(TableType) - case memory(MemoryType) - case global(GlobalType) -} diff --git a/Sources/WAKit/Types/Types.swift b/Sources/WAKit/Types/Types.swift deleted file mode 100644 index ed655f20..00000000 --- a/Sources/WAKit/Types/Types.swift +++ /dev/null @@ -1,54 +0,0 @@ -/// - Note: -/// -typealias ResultType = [ValueType] - -/// - Note: -/// -public enum FunctionType: Equatable { - case any - case some(parameters: [ValueType], results: [ValueType]) -} - -/// - Note: -/// -public struct Limits { - let min: UInt32 - let max: UInt32? -} - -extension Limits: Equatable {} - -/// - Note: -/// -public typealias MemoryType = Limits - -/// - Note: -/// -public struct TableType: Equatable { - let elementType: FunctionType - let limits: Limits -} - -/// - Note: -/// -public enum Mutability: Equatable { - case constant - case variable -} - -/// - Note: -/// -public struct GlobalType: Equatable { - let mutability: Mutability - let valueType: ValueType -} - -/// - Note: -/// -// sourcery: AutoEquatable -public enum ExternalType { - case function(FunctionType) - case table(TableType) - case memory(MemoryType) - case global(GlobalType) -} diff --git a/Sources/WAKit/Validation/Validation.swift b/Sources/WAKit/Validation/Validation.swift deleted file mode 100644 index 615b6c0d..00000000 --- a/Sources/WAKit/Validation/Validation.swift +++ /dev/null @@ -1,89 +0,0 @@ -enum ValidationError: Error { - case genericError -} - -/// - Note: -/// -final class ValidationContext { - var types: [FunctionType] = [] - var functions: [FunctionType] = [] - var tables: [TableType] = [] - var memories: [MemoryType] = [] - var globals: [GlobalType] = [] - var locals: [ValueType] = [] - var labels: [ResultType] = [] - var `return`: ResultType? -} - -protocol Validatable { - func validate(context: ValidationContext) throws -} - -/// - Note: -/// - -/// - Note: -/// -extension Limits: Validatable { - func validate(context _: ValidationContext) throws { - if let max = max { - guard min < max else { - throw ValidationError.genericError - } - } - } -} - -/// - Note: -/// -extension FunctionType: Validatable { - func validate(context _: ValidationContext) throws { - guard case let .some(_, results) = self else { - return - } - guard results.count <= 1 else { - throw ValidationError.genericError - } - } -} - -/// - Note: -/// -extension TableType: Validatable { - func validate(context: ValidationContext) throws { - try limits.validate(context: context) - } -} - -/// - Note: -/// -extension GlobalType: Validatable { - func validate(context _: ValidationContext) throws {} -} - -/// - Note: -/// - -/// - Note: -/// -extension Expression: Validatable { - func validate(context: ValidationContext) throws { - try validate(instructions: instructions, context: context) - } - - /// - Note: - /// - private func validate(instructions: [Instruction], context: ValidationContext) throws { - guard !instructions.isEmpty else { - return - } - - var instructions = instructions - let instruction = instructions.popLast() - try validate(instructions: instructions, context: context) - guard let i = instruction as? Validatable else { - throw ValidationError.genericError - } - try i.validate(context: context) - } -} diff --git a/Sources/WASI/FileSystem.swift b/Sources/WASI/FileSystem.swift new file mode 100644 index 00000000..97b7b34d --- /dev/null +++ b/Sources/WASI/FileSystem.swift @@ -0,0 +1,120 @@ +import Foundation +import SystemPackage + +struct FileAccessMode: OptionSet { + let rawValue: UInt32 + static let read = FileAccessMode(rawValue: 1) + static let write = FileAccessMode(rawValue: 1 << 1) +} + +protocol WASIEntry { + func attributes() throws -> WASIAbi.Filestat + func fileType() throws -> WASIAbi.FileType + func status() throws -> WASIAbi.Fdflags + func setTimes( + atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws + func advise( + offset: WASIAbi.FileSize, length: WASIAbi.FileSize, advice: WASIAbi.Advice + ) throws + func close() throws +} + +protocol WASIFile: WASIEntry { + func fdStat() throws -> WASIAbi.FdStat + func setFdStatFlags(_ flags: WASIAbi.Fdflags) throws + func setFilestatSize(_ size: WASIAbi.FileSize) throws + + func tell() throws -> WASIAbi.FileSize + func seek(offset: WASIAbi.FileDelta, whence: WASIAbi.Whence) throws -> WASIAbi.FileSize + + func write( + vectored buffer: Buffer + ) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec + func pwrite( + vectored buffer: Buffer, offset: WASIAbi.FileSize + ) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec + func read( + into buffer: Buffer + ) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec + func pread( + into buffer: Buffer, offset: WASIAbi.FileSize + ) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec +} + +protocol WASIDir: WASIEntry { + typealias ReaddirElement = (dirent: WASIAbi.Dirent, name: String) + + var preopenPath: String? { get } + + func openFile( + symlinkFollow: Bool, + path: String, + oflags: WASIAbi.Oflags, + accessMode: FileAccessMode, + fdflags: WASIAbi.Fdflags + ) throws -> FileDescriptor + + func createDirectory(atPath path: String) throws + func removeDirectory(atPath path: String) throws + func removeFile(atPath path: String) throws + func symlink(from sourcePath: String, to destPath: String) throws + func readEntries(cookie: WASIAbi.DirCookie) throws -> AnyIterator> + func attributes(path: String, symlinkFollow: Bool) throws -> WASIAbi.Filestat + func setFilestatTimes( + path: String, + atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags, symlinkFollow: Bool + ) throws +} + +enum FdEntry { + case file(any WASIFile) + case directory(any WASIDir) + + func asEntry() -> any WASIEntry { + switch self { + case .file(let entry): + return entry + case .directory(let directory): + return directory + } + } +} + +/// A table that maps file descriptor to actual resource in host environment +struct FdTable { + private var map: [WASIAbi.Fd: FdEntry] + private var nextFd: WASIAbi.Fd + + init() { + self.map = [:] + // 0, 1 and 2 are reserved for stdio + self.nextFd = 3 + } + + /// Inserts a resource as the given file descriptor + subscript(_ fd: WASIAbi.Fd) -> FdEntry? { + get { self.map[fd] } + set { self.map[fd] = newValue } + } + + /// Inserts an entry and returns the corresponding file descriptor + mutating func push(_ entry: FdEntry) throws -> WASIAbi.Fd { + guard map.count < WASIAbi.Fd.max else { + throw WASIAbi.Errno.ENFILE + } + // Find a free fd + while true { + let fd = self.nextFd + // Wrapping to find fd again from 0 after overflow + self.nextFd &+= 1 + if self.map[fd] != nil { + continue + } + self.map[fd] = entry + return fd + } + } +} diff --git a/Sources/WASI/GuestMemorySupport.swift b/Sources/WASI/GuestMemorySupport.swift new file mode 100644 index 00000000..213aa97f --- /dev/null +++ b/Sources/WASI/GuestMemorySupport.swift @@ -0,0 +1,16 @@ +import WasmKit + +extension GuestPointee { + static func readFromGuest(_ pointer: inout UnsafeGuestRawPointer) -> Self { + pointer = pointer.alignedUp(toMultipleOf: Self.alignInGuest) + let value = readFromGuest(pointer) + pointer = pointer.advanced(by: sizeInGuest) + return value + } + + static func writeToGuest(at pointer: inout UnsafeGuestRawPointer, value: Self) { + pointer = pointer.alignedUp(toMultipleOf: Self.alignInGuest) + writeToGuest(at: pointer, value: value) + pointer = pointer.advanced(by: sizeInGuest) + } +} diff --git a/Sources/WASI/Platform/Directory.swift b/Sources/WASI/Platform/Directory.swift new file mode 100644 index 00000000..015e9e3a --- /dev/null +++ b/Sources/WASI/Platform/Directory.swift @@ -0,0 +1,169 @@ +import SystemPackage + +struct DirEntry { + let preopenPath: String? + let fd: FileDescriptor +} + +extension DirEntry: WASIDir, FdWASIEntry { + func openFile( + symlinkFollow: Bool, + path: String, + oflags: WASIAbi.Oflags, + accessMode: FileAccessMode, + fdflags: WASIAbi.Fdflags + ) throws -> FileDescriptor { + var options: FileDescriptor.OpenOptions = [] + if !symlinkFollow { + options.insert(.noFollow) + } + + if oflags.contains(.DIRECTORY) { + options.insert(.directory) + } else { + // For regular file + if oflags.contains(.CREAT) { + options.insert(.create) + } + if oflags.contains(.EXCL) { + options.insert(.exclusiveCreate) + } + if oflags.contains(.TRUNC) { + options.insert(.truncate) + } + } + + // SystemPackage.FilePath implicitly normalizes the trailing "/", however + // it means the last component is expected to be a directory. Therefore + // check it here before converting path string to FilePath. + if path.hasSuffix("/") { + options.insert(.directory) + } + + if fdflags.contains(.APPEND) { + options.insert(.append) + } + + let mode: FileDescriptor.AccessMode + switch (accessMode.contains(.read), accessMode.contains(.write)) { + case (true, true): mode = .readWrite + case (true, false): mode = .readOnly + case (false, true): mode = .writeOnly + case (false, false): + // If not opened for neither write nor read, set read mode by default + // because underlying `openat` requires mode but WASI's + // `path_open` can omit FD_READ. + // https://man7.org/linux/man-pages/man2/open.2.html + // > The argument flags must include one of the following access + // > modes: O_RDONLY, O_WRONLY, or O_RDWR. These request opening the + // > file read-only, write-only, or read/write, respectively. + mode = .readOnly + } + + let newFd = try SandboxPrimitives.openAt( + start: self.fd, + path: FilePath(path), mode: mode, options: options, + // Use 0o600 open mode as the minimum permission + permissions: .ownerReadWrite + ) + return newFd + } + + func setFilestatTimes( + path: String, + atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags, symlinkFollow: Bool + ) throws { + let fd = try openFile( + symlinkFollow: symlinkFollow, path: path, + oflags: [], accessMode: .write, fdflags: [] + ) + let (access, modification) = try WASIAbi.Timestamp.platformTimeSpec( + atim: atim, mtim: mtim, fstFlags: fstFlags + ) + try WASIAbi.Errno.translatingPlatformErrno { + try fd.setTimes(access: access, modification: modification) + } + } + + func removeFile(atPath path: String) throws { + let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path) + try WASIAbi.Errno.translatingPlatformErrno { + try dir.remove(at: FilePath(basename), options: []) + } + } + + func removeDirectory(atPath path: String) throws { + let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path) + try WASIAbi.Errno.translatingPlatformErrno { + try dir.remove(at: FilePath(basename), options: .removeDirectory) + } + } + + func symlink(from sourcePath: String, to destPath: String) throws { + let (destDir, destBasename) = try SandboxPrimitives.openParent( + start: fd, path: destPath + ) + try WASIAbi.Errno.translatingPlatformErrno { + try destDir.createSymlink(original: FilePath(sourcePath), link: FilePath(destBasename)) + } + } + + func readEntries( + cookie: WASIAbi.DirCookie + ) throws -> AnyIterator> { + // Duplicate fd because readdir takes the ownership of + // the given fd and closedir also close the underlying fd + let newFd = try WASIAbi.Errno.translatingPlatformErrno { + try fd.open(at: ".", .readOnly, options: []) + } + let iterator = try WASIAbi.Errno.translatingPlatformErrno { + try newFd.contentsOfDirectory() + } + .lazy.enumerated() + .map { (entryIndex, entry) in + return Result(catching: { () -> ReaddirElement in + let entry = try entry.get() + let name = entry.name + let stat = try WASIAbi.Errno.translatingPlatformErrno { + try fd.attributes(at: name, options: []) + } + let dirent = WASIAbi.Dirent( + // We can't use telldir and seekdir because the location data + // is valid for only the same dirp but and there is no way to + // share dirp among fd_readdir calls. + dNext: WASIAbi.DirCookie(entryIndex + 1), + dIno: stat.inode, + dirNameLen: WASIAbi.DirNameLen(name.utf8.count), + dType: WASIAbi.FileType(platformFileType: entry.fileType) + ) + return (dirent, name) + }) + } + .dropFirst(Int(cookie)) + .makeIterator() + return AnyIterator(iterator) + } + + func createDirectory(atPath path: String) throws { + let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path) + try WASIAbi.Errno.translatingPlatformErrno { + try dir.createDirectory(at: FilePath(basename), permissions: .ownerReadWriteExecute) + } + } + + func attributes(path: String, symlinkFollow: Bool) throws -> WASIAbi.Filestat { + var options: FileDescriptor.AtOptions = [] + if !symlinkFollow { + options.insert(.noFollow) + } + let (dir, basename) = try SandboxPrimitives.openParent(start: fd, path: path) + let attributes = try basename.withCString { cBasename in + try WASIAbi.Errno.translatingPlatformErrno { + try dir.attributes(at: cBasename, options: options) + } + } + + return WASIAbi.Filestat(stat: attributes) + } +} diff --git a/Sources/WASI/Platform/Entry.swift b/Sources/WASI/Platform/Entry.swift new file mode 100644 index 00000000..3c414fd3 --- /dev/null +++ b/Sources/WASI/Platform/Entry.swift @@ -0,0 +1,100 @@ +import SystemPackage + +extension FdWASIEntry { + /// Returns the metadata for the fd entry + func attributes() throws -> WASIAbi.Filestat { + try WASIAbi.Errno.translatingPlatformErrno { + try WASIAbi.Filestat(stat: self.fd.attributes()) + } + } + + /// Announces the expected access pattern to the system for optimization + func advise( + offset: WASIAbi.FileSize, length: WASIAbi.FileSize, + advice: WASIAbi.Advice + ) throws { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + guard let offset = Int64(exactly: offset), + let length = Int32(exactly: length) + else { + // no-op if offset or length is invalid + return + } + try WASIAbi.Errno.translatingPlatformErrno { + try self.fd.adviseRead(offset: offset, length: length) + } + #elseif os(Linux) + guard let offset = Int(exactly: offset), + let length = Int(exactly: length) + else { + // no-op if offset or length is invalid + return + } + try WASIAbi.Errno.translatingPlatformErrno { + try self.fd.advise(offset: offset, length: length, advice: .willNeed) + } + #endif + } + + /// Closes the file descriptor + func close() throws { + try WASIAbi.Errno.translatingPlatformErrno { try fd.close() } + } + + /// Truncates or extends the file + func setFilestatSize(_ size: WASIAbi.FileSize) throws { + try WASIAbi.Errno.translatingPlatformErrno { + try fd.truncate(size: Int64(size)) + } + } + + /// Seek to the offset + func seek(offset: WASIAbi.FileDelta, whence: WASIAbi.Whence) throws -> WASIAbi.FileSize { + let platformWhence: FileDescriptor.SeekOrigin + switch whence { + case .SET: + platformWhence = .start + case .CUR: + platformWhence = .current + case .END: + platformWhence = .end + } + let newOffset = try WASIAbi.Errno.translatingPlatformErrno { + try fd.seek(offset: offset, from: platformWhence) + } + return WASIAbi.FileSize(newOffset) + } + + /// Returns the current reading/writing offset + func tell() throws -> WASIAbi.FileSize { + WASIAbi.FileSize( + try WASIAbi.Errno.translatingPlatformErrno { + try fd.seek(offset: 0, from: .current) + }) + } + + /// Returns the file type of the file + func fileType() throws -> WASIAbi.FileType { + try WASIAbi.FileType(platformFileType: self.fd.attributes().fileType) + } + + /// Returns the current file desciptor status + func status() throws -> WASIAbi.Fdflags { + return try WASIAbi.Errno.translatingPlatformErrno { + WASIAbi.Fdflags(platformOpenOptions: try self.fd.status()) + } + } + + /// Sets timestamps that belongs to the file + func setTimes( + atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws { + let (access, modification) = try WASIAbi.Timestamp.platformTimeSpec( + atim: atim, mtim: mtim, fstFlags: fstFlags + ) + try WASIAbi.Errno.translatingPlatformErrno { + try self.fd.setTimes(access: access, modification: modification) + } + } +} diff --git a/Sources/WASI/Platform/File.swift b/Sources/WASI/Platform/File.swift new file mode 100644 index 00000000..a278586b --- /dev/null +++ b/Sources/WASI/Platform/File.swift @@ -0,0 +1,116 @@ +import Foundation +import SystemPackage + +protocol FdWASIEntry: WASIEntry { + var fd: FileDescriptor { get } +} + +protocol FdWASIFile: WASIFile, FdWASIEntry { + var accessMode: FileAccessMode { get } +} + +extension FdWASIFile { + func fdStat() throws -> WASIAbi.FdStat { + var fsRightsBase: WASIAbi.Rights = [] + if accessMode.contains(.read) { + fsRightsBase.insert(.FD_READ) + } + if accessMode.contains(.write) { + fsRightsBase.insert(.FD_WRITE) + } + return try WASIAbi.FdStat( + fsFileType: self.fileType(), + fsFlags: self.status(), + fsRightsBase: fsRightsBase, fsRightsInheriting: [] + ) + } + + @inlinable + func write(vectored buffer: Buffer) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec { + guard accessMode.contains(.write) else { + throw WASIAbi.Errno.EBADF + } + // TODO: Use `writev` + let handle = FileHandle(fileDescriptor: fd.rawValue) + var bytesWritten: UInt32 = 0 + for iovec in buffer { + try iovec.withHostBufferPointer { + try handle.write(contentsOf: $0) + } + bytesWritten += iovec.length + } + return bytesWritten + } + + @inlinable + func pwrite(vectored buffer: Buffer, offset: WASIAbi.FileSize) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec { + // TODO: Use `pwritev` + let handle = FileHandle(fileDescriptor: fd.rawValue) + let savedOffset = try handle.offset() + try handle.seek(toOffset: offset) + let nwritten = try write(vectored: buffer) + try handle.seek(toOffset: savedOffset) + return nwritten + } + + @inlinable + func read(into buffer: Buffer) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec { + // TODO: Use `readv` + let handle = FileHandle(fileDescriptor: fd.rawValue) + var nread: UInt32 = 0 + for iovec in buffer { + try iovec.buffer.withHostPointer { rawBufferStart in + var bufferStart = rawBufferStart.bindMemory( + to: UInt8.self, capacity: Int(iovec.length) + ) + let bufferEnd = bufferStart + Int(iovec.length) + while bufferStart < bufferEnd { + let remaining = bufferEnd - bufferStart + guard let bytes = try handle.read(upToCount: remaining) else { + break + } + bytes.copyBytes(to: bufferStart, count: bytes.count) + bufferStart += bytes.count + } + nread += iovec.length - UInt32(bufferEnd - bufferStart) + } + } + return nread + } + + @inlinable + func pread(into buffer: Buffer, offset: WASIAbi.FileSize) throws -> WASIAbi.Size where Buffer.Element == WASIAbi.IOVec { + // TODO: Use `preadv` + let handle = FileHandle(fileDescriptor: fd.rawValue) + let savedOffset = try handle.offset() + try handle.seek(toOffset: offset) + let nread = try read(into: buffer) + try handle.seek(toOffset: savedOffset) + return nread + } +} + +struct RegularFileEntry: FdWASIFile { + let fd: FileDescriptor + let accessMode: FileAccessMode +} + +extension FdWASIFile { + func setFdStatFlags(_ flags: WASIAbi.Fdflags) throws { + try WASIAbi.Errno.translatingPlatformErrno { + try fd.setStatus(flags.platformOpenOptions) + } + } +} + +struct StdioFileEntry: FdWASIFile { + let fd: FileDescriptor + let accessMode: FileAccessMode + + func attributes() throws -> WASIAbi.Filestat { + return WASIAbi.Filestat( + dev: 0, ino: 0, filetype: .CHARACTER_DEVICE, + nlink: 0, size: 0, atim: 0, mtim: 0, ctim: 0 + ) + } +} diff --git a/Sources/WASI/Platform/PlatformTypes.swift b/Sources/WASI/Platform/PlatformTypes.swift new file mode 100644 index 00000000..8361870b --- /dev/null +++ b/Sources/WASI/Platform/PlatformTypes.swift @@ -0,0 +1,221 @@ +import SystemExtras +import SystemPackage + +extension WASIAbi.FileType { + init(platformFileType: FileDescriptor.FileType) { + if platformFileType.isDirectory { + self = .DIRECTORY + } else if platformFileType.isSymlink { + self = .SYMBOLIC_LINK + } else if platformFileType.isFile { + self = .REGULAR_FILE + } else if platformFileType.isCharacterDevice { + self = .CHARACTER_DEVICE + } else if platformFileType.isBlockDevice { + self = .BLOCK_DEVICE + } else if platformFileType.isSocket { + self = .SOCKET_STREAM + } else { + self = .UNKNOWN + } + } +} + +extension WASIAbi.Fdflags { + init(platformOpenOptions: FileDescriptor.OpenOptions) { + var fdFlags: WASIAbi.Fdflags = [] + if platformOpenOptions.contains(.append) { + fdFlags.insert(.APPEND) + } + if platformOpenOptions.contains(.dataSync) { + fdFlags.insert(.DSYNC) + } + if platformOpenOptions.contains(.nonBlocking) { + fdFlags.insert(.NONBLOCK) + } + if platformOpenOptions.contains(.fileSync) { + fdFlags.insert(.SYNC) + } + #if os(Linux) + if platformOpenOptions.contains(.readSync) { + fdFlags.insert(.RSYNC) + } + #endif + self = fdFlags + } + + var platformOpenOptions: FileDescriptor.OpenOptions { + var flags: FileDescriptor.OpenOptions = [] + if self.contains(.APPEND) { + flags.insert(.append) + } + if self.contains(.DSYNC) { + flags.insert(.dataSync) + } + if self.contains(.NONBLOCK) { + flags.insert(.nonBlocking) + } + if self.contains(.SYNC) { + flags.insert(.fileSync) + } + #if os(Linux) + if self.contains(.RSYNC) { + flags.insert(.readSync) + } + #endif + return flags + } +} + +extension WASIAbi.Timestamp { + static func platformTimeSpec( + atim: WASIAbi.Timestamp, + mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws -> (access: Clock.TimeSpec, modification: Clock.TimeSpec) { + return try ( + atim.platformTimeSpec( + set: fstFlags.contains(.ATIM), now: fstFlags.contains(.ATIM_NOW) + ), + mtim.platformTimeSpec( + set: fstFlags.contains(.MTIM), now: fstFlags.contains(.MTIM_NOW) + ) + ) + } + + func platformTimeSpec(set: Bool, now: Bool) throws -> Clock.TimeSpec { + switch (set, now) { + case (true, true): + throw WASIAbi.Errno.EINVAL + case (true, false): + return Clock.TimeSpec( + seconds: Int(self / 1_000_000_000), + nanoseconds: Int(self % 1_000_000_000) + ) + case (false, true): return .now + case (false, false): return .omit + } + } +} + +extension WASIAbi.Filestat { + init(stat: FileDescriptor.Attributes) { + self = WASIAbi.Filestat( + dev: WASIAbi.Device(stat.device), + ino: WASIAbi.Inode(stat.inode), + filetype: WASIAbi.FileType(platformFileType: stat.fileType), + nlink: WASIAbi.LinkCount(stat.linkCount), + size: WASIAbi.FileSize(stat.size), + atim: WASIAbi.Timestamp(platformTimeSpec: stat.accessTime), + mtim: WASIAbi.Timestamp(platformTimeSpec: stat.modificationTime), + ctim: WASIAbi.Timestamp(platformTimeSpec: stat.creationTime) + ) + } +} + +extension WASIAbi.Timestamp { + + fileprivate init(seconds: Int, nanoseconds: Int) { + self = UInt64(nanoseconds + seconds * 1_000_000_000) + } + + init(platformTimeSpec timespec: Clock.TimeSpec) { + self.init(seconds: timespec.rawValue.tv_sec, nanoseconds: timespec.rawValue.tv_nsec) + } +} + +extension WASIAbi.Errno { + + static func translatingPlatformErrno(_ body: () throws -> R) throws -> R { + do { + return try body() + } catch let errno as Errno { + guard let error = WASIAbi.Errno(platformErrno: errno) else { + throw WASIError(description: "Unknown underlying OS error: \(errno)") + } + throw error + } + } + + init?(platformErrno: SystemPackage.Errno) { + switch platformErrno { + case .permissionDenied: self = .EPERM + case .notPermitted: self = .EPERM + case .noSuchFileOrDirectory: self = .ENOENT + case .noSuchProcess: self = .ESRCH + case .interrupted: self = .EINTR + case .ioError: self = .EIO + case .noSuchAddressOrDevice: self = .ENXIO + case .argListTooLong: self = .E2BIG + case .execFormatError: self = .ENOEXEC + case .badFileDescriptor: self = .EBADF + case .noChildProcess: self = .ECHILD + case .deadlock: self = .EDEADLK + case .noMemory: self = .ENOMEM + case .permissionDenied: self = .EACCES + case .badAddress: self = .EFAULT + case .resourceBusy: self = .EBUSY + case .fileExists: self = .EEXIST + case .improperLink: self = .EXDEV + case .operationNotSupportedByDevice: self = .ENODEV + case .notDirectory: self = .ENOTDIR + case .isDirectory: self = .EISDIR + case .invalidArgument: self = .EINVAL + case .tooManyOpenFilesInSystem: self = .ENFILE + case .tooManyOpenFiles: self = .EMFILE + case .inappropriateIOCTLForDevice: self = .ENOTTY + case .textFileBusy: self = .ETXTBSY + case .fileTooLarge: self = .EFBIG + case .noSpace: self = .ENOSPC + case .illegalSeek: self = .ESPIPE + case .readOnlyFileSystem: self = .EROFS + case .tooManyLinks: self = .EMLINK + case .brokenPipe: self = .EPIPE + case .outOfDomain: self = .EDOM + case .outOfRange: self = .ERANGE + case .resourceTemporarilyUnavailable: self = .EAGAIN + case .nowInProgress: self = .EINPROGRESS + case .alreadyInProcess: self = .EALREADY + case .notSocket: self = .ENOTSOCK + case .addressRequired: self = .EDESTADDRREQ + case .messageTooLong: self = .EMSGSIZE + case .protocolWrongTypeForSocket: self = .EPROTOTYPE + case .protocolNotAvailable: self = .ENOPROTOOPT + case .protocolNotSupported: self = .EPROTONOSUPPORT + case .notSupported: self = .ENOTSUP + case .addressFamilyNotSupported: self = .EAFNOSUPPORT + case .addressInUse: self = .EADDRINUSE + case .addressNotAvailable: self = .EADDRNOTAVAIL + case .networkDown: self = .ENETDOWN + case .networkUnreachable: self = .ENETUNREACH + case .networkReset: self = .ENETRESET + case .connectionAbort: self = .ECONNABORTED + case .connectionReset: self = .ECONNRESET + case .noBufferSpace: self = .ENOBUFS + case .socketIsConnected: self = .EISCONN + case .socketNotConnected: self = .ENOTCONN + case .timedOut: self = .ETIMEDOUT + case .connectionRefused: self = .ECONNREFUSED + case .tooManySymbolicLinkLevels: self = .ELOOP + case .fileNameTooLong: self = .ENAMETOOLONG + case .noRouteToHost: self = .EHOSTUNREACH + case .directoryNotEmpty: self = .ENOTEMPTY + case .diskQuotaExceeded: self = .EDQUOT + case .staleNFSFileHandle: self = .ESTALE + case .noLocks: self = .ENOLCK + case .noFunction: self = .ENOSYS + case .overflow: self = .EOVERFLOW + case .canceled: self = .ECANCELED + case .identifierRemoved: self = .EIDRM + case .noMessage: self = .ENOMSG + case .illegalByteSequence: self = .EILSEQ + case .badMessage: self = .EBADMSG + case .multiHop: self = .EMULTIHOP + case .noLink: self = .ENOLINK + case .protocolError: self = .EPROTO + case .notRecoverable: self = .ENOTRECOVERABLE + case .previousOwnerDied: self = .EOWNERDEAD + default: return nil + } + } +} diff --git a/Sources/WASI/Platform/SandboxPrimitives.swift b/Sources/WASI/Platform/SandboxPrimitives.swift new file mode 100644 index 00000000..28ec08ef --- /dev/null +++ b/Sources/WASI/Platform/SandboxPrimitives.swift @@ -0,0 +1 @@ +internal enum SandboxPrimitives {} diff --git a/Sources/WASI/Platform/SandboxPrimitives/Open.swift b/Sources/WASI/Platform/SandboxPrimitives/Open.swift new file mode 100644 index 00000000..9b613cbc --- /dev/null +++ b/Sources/WASI/Platform/SandboxPrimitives/Open.swift @@ -0,0 +1,101 @@ +import SystemExtras +import SystemPackage + +struct PathResolution { + private let mode: FileDescriptor.AccessMode + private let options: FileDescriptor.OpenOptions + private let permissions: FilePermissions + + private var baseFd: FileDescriptor + private let path: FilePath + private var openDirectories: [FileDescriptor] + /// Reverse-ordered remaining path components + private var components: FilePath.ComponentView + + init( + baseDirFd: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions, + path: FilePath + ) { + self.baseFd = baseDirFd + self.mode = mode + self.options = options + self.permissions = permissions + self.path = path + self.openDirectories = [] + self.components = FilePath.ComponentView(path.components.reversed()) + } + + mutating func parentDirectory() throws { + guard let lastDirectory = openDirectories.popLast() else { + // no more parent directory means too many `..` + throw WASIAbi.Errno.EPERM + } + self.baseFd = lastDirectory + } + + mutating func regular(component: FilePath.Component) throws { + let options: FileDescriptor.OpenOptions + let mode: FileDescriptor.AccessMode + if !self.components.isEmpty { + var intermediateOtions: FileDescriptor.OpenOptions = [] + + // When trying to open an intermediate directory, + // we can assume it's directory. + intermediateOtions.insert(.directory) + // FIXME: Resolve symlink in safe way + intermediateOtions.insert(.noFollow) + options = intermediateOtions + mode = .readOnly + } else { + options = self.options + mode = self.mode + } + + try WASIAbi.Errno.translatingPlatformErrno { + let newFd = try self.baseFd.open( + at: FilePath(root: nil, components: component), + mode, options: options, permissions: permissions + ) + self.openDirectories.append(self.baseFd) + self.baseFd = newFd + } + } + + mutating func resolve() throws -> FileDescriptor { + if path.isAbsolute { + // POSIX openat(2) interprets absolute path ignoring base directory fd + // but it leads sandbox-escaping, so reject absolute path here. + throw WASIAbi.Errno.EPERM + } + + while let component = components.popLast() { + switch component.kind { + case .currentDirectory: + break // no-op + case .parentDirectory: + try parentDirectory() + case .regular: try regular(component: component) + } + } + return self.baseFd + } +} + +extension SandboxPrimitives { + static func openAt( + start startFd: FileDescriptor, + path: FilePath, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions, + permissions: FilePermissions + ) throws -> FileDescriptor { + var resolution = PathResolution( + baseDirFd: startFd, mode: mode, options: options, + permissions: permissions, path: path + ) + return try resolution.resolve() + } +} diff --git a/Sources/WASI/Platform/SandboxPrimitives/OpenParent.swift b/Sources/WASI/Platform/SandboxPrimitives/OpenParent.swift new file mode 100644 index 00000000..69ce3656 --- /dev/null +++ b/Sources/WASI/Platform/SandboxPrimitives/OpenParent.swift @@ -0,0 +1,51 @@ +import SystemPackage + +/// Split the given path to the parent and the last component to be passed to openat +/// Note: `SystemPackage.FilePath` strips explicit trailing "/" by normalization at `init`, +/// so this function takes path as a `String`. +internal func splitParent(path: String) -> (FilePath, FilePath.Component)? { + func pathRequiresDirectory(path: String) -> Bool { + return path.hasSuffix("/") || path.hasSuffix("/.") + } + + guard !path.isEmpty else { return nil } + + if pathRequiresDirectory(path: path) { + // Create a link to the directory itself + return (FilePath(path), FilePath.Component(".")) + } + + let filePath = FilePath(path) + var components = filePath.components + if let c = components.popLast() { + switch c.kind { + case .regular, .currentDirectory: + return (FilePath(root: filePath.root, components), c) + case .parentDirectory: + // Create a link to the parent directory itself + return (filePath, FilePath.Component(".")) + } + } else { + fatalError("non-empty path should have at least one component") + } +} + +extension SandboxPrimitives { + static func openParent(start: FileDescriptor, path: String) throws -> (FileDescriptor, String) { + guard let (dirName, basename) = splitParent(path: path) else { + throw WASIAbi.Errno.ENOENT + } + + let dirFd: FileDescriptor + if !dirName.isEmpty { + dirFd = try openAt( + start: start, path: dirName, + mode: .readOnly, options: .directory, + permissions: [] + ) + } else { + dirFd = start + } + return (dirFd, basename.string) + } +} diff --git a/Sources/WASI/WASI.swift b/Sources/WASI/WASI.swift new file mode 100644 index 00000000..be1f8d5c --- /dev/null +++ b/Sources/WASI/WASI.swift @@ -0,0 +1,1871 @@ +import Foundation +import SwiftShims // For swift_stdlib_random +import SystemExtras +import SystemPackage +import WasmKit + +protocol WASI { + /// Reads command-line argument data. + /// - Parameters: + /// - argv: Pointer to an array of argument strings to be written + /// - argvBuffer: Pointer to a buffer of argument strings to be written + func args_get( + argv: UnsafeGuestPointer>, + argvBuffer: UnsafeGuestPointer + ) + + /// Return command-line argument data sizes. + /// - Returns: Tuple of number of arguments and required buffer size + func args_sizes_get() -> (WASIAbi.Size, WASIAbi.Size) + + /// Read environment variable data. + func environ_get( + environ: UnsafeGuestPointer>, + environBuffer: UnsafeGuestPointer + ) + + /// Return environment variable data sizes. + /// - Returns: Tuple of number of environment variables and required buffer size + func environ_sizes_get() -> (WASIAbi.Size, WASIAbi.Size) + + /// Return the resolution of a clock. + func clock_res_get(id: WASIAbi.ClockId) throws -> WASIAbi.Timestamp + + /// Return the time value of a clock. + func clock_time_get( + id: WASIAbi.ClockId, precision: WASIAbi.Timestamp + ) throws -> WASIAbi.Timestamp + + /// Provide file advisory information on a file descriptor. + func fd_advise( + fd: WASIAbi.Fd, offset: WASIAbi.FileSize, + length: WASIAbi.FileSize, advice: WASIAbi.Advice + ) throws + + /// Force the allocation of space in a file. + func fd_allocate(fd: WASIAbi.Fd, offset: WASIAbi.FileSize, length: WASIAbi.FileSize) throws + + /// Close a file descriptor. + func fd_close(fd: WASIAbi.Fd) throws + + /// Synchronize the data of a file to disk. + func fd_datasync(fd: WASIAbi.Fd) throws + + /// Get the attributes of a file descriptor. + /// - Parameter fileDescriptor: File descriptor to get attribute. + func fd_fdstat_get(fileDescriptor: UInt32) throws -> WASIAbi.FdStat + + /// Adjust the flags associated with a file descriptor. + func fd_fdstat_set_flags(fd: WASIAbi.Fd, flags: WASIAbi.Fdflags) throws + + /// Adjust the rights associated with a file descriptor. + func fd_fdstat_set_rights( + fd: WASIAbi.Fd, + fsRightsBase: WASIAbi.Rights, + fsRightsInheriting: WASIAbi.Rights + ) throws + + /// Return the attributes of an open file. + func fd_filestat_get(fd: WASIAbi.Fd) throws -> WASIAbi.Filestat + + /// Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. + func fd_filestat_set_size(fd: WASIAbi.Fd, size: WASIAbi.FileSize) throws + + /// Adjust the timestamps of an open file or directory. + func fd_filestat_set_times( + fd: WASIAbi.Fd, + atim: WASIAbi.Timestamp, + mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws + + /// Read from a file descriptor, without using and updating the file descriptor's offset. + func fd_pread( + fd: WASIAbi.Fd, iovs: UnsafeGuestBufferPointer, + offset: WASIAbi.FileSize + ) throws -> WASIAbi.Size + + /// Return a description of the given preopened file descriptor. + func fd_prestat_get(fd: WASIAbi.Fd) throws -> WASIAbi.Prestat + + /// Return a directory name of the given preopened file descriptor + func fd_prestat_dir_name(fd: WASIAbi.Fd, path: UnsafeGuestPointer, maxPathLength: WASIAbi.Size) throws + + /// Write to a file descriptor, without using and updating the file descriptor's offset. + func fd_pwrite( + fd: WASIAbi.Fd, iovs: UnsafeGuestBufferPointer, + offset: WASIAbi.FileSize + ) throws -> WASIAbi.Size + + /// Read from a file descriptor. + func fd_read( + fd: WASIAbi.Fd, iovs: UnsafeGuestBufferPointer + ) throws -> WASIAbi.Size + + /// Read directory entries from a directory. + func fd_readdir( + fd: WASIAbi.Fd, + buffer: UnsafeGuestBufferPointer, + cookie: WASIAbi.DirCookie + ) throws -> WASIAbi.Size + + /// Atomically replace a file descriptor by renumbering another file descriptor. + func fd_renumber(fd: WASIAbi.Fd, to toFd: WASIAbi.Fd) throws + + /// Move the offset of a file descriptor. + func fd_seek(fd: WASIAbi.Fd, offset: WASIAbi.FileDelta, whence: WASIAbi.Whence) throws -> WASIAbi.FileSize + + /// Synchronize the data and metadata of a file to disk. + func fd_sync(fd: WASIAbi.Fd) throws + + /// Return the current offset of a file descriptor. + func fd_tell(fd: WASIAbi.Fd) throws -> WASIAbi.FileSize + + /// POSIX `writev` equivalent. + /// - Parameters: + /// - fileDescriptor: File descriptor to write to. + /// - ioVectors: Buffer pointer to an array of byte buffers to write. + /// - Returns: Number of bytes written. + func fd_write( + fileDescriptor: WASIAbi.Fd, + ioVectors: UnsafeGuestBufferPointer + ) throws -> UInt32 + + /// Create a directory. + func path_create_directory( + dirFd: WASIAbi.Fd, + path: String + ) throws + + /// Return the attributes of a file or directory. + func path_filestat_get( + dirFd: WASIAbi.Fd, + flags: WASIAbi.LookupFlags, + path: String + ) throws -> WASIAbi.Filestat + + /// Adjust the timestamps of a file or directory. + func path_filestat_set_times( + dirFd: WASIAbi.Fd, + flags: WASIAbi.LookupFlags, + path: String, + atim: WASIAbi.Timestamp, + mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws + + /// Create a hard link. + func path_link( + oldFd: WASIAbi.Fd, oldFlags: WASIAbi.LookupFlags, oldPath: String, + newFd: WASIAbi.Fd, newPath: String + ) throws + + /// Open a file or directory. + func path_open( + dirFd: WASIAbi.Fd, + dirFlags: WASIAbi.LookupFlags, + path: String, + oflags: WASIAbi.Oflags, + fsRightsBase: WASIAbi.Rights, + fsRightsInheriting: WASIAbi.Rights, + fdflags: WASIAbi.Fdflags + ) throws -> WASIAbi.Fd + + /// Read the contents of a symbolic link. + func path_readlink( + fd: WASIAbi.Fd, path: String, + buffer: UnsafeGuestBufferPointer + ) throws -> WASIAbi.Size + + /// Remove a directory. + func path_remove_directory(dirFd: WASIAbi.Fd, path: String) throws + + /// Rename a file or directory. + func path_rename( + oldFd: WASIAbi.Fd, oldPath: String, + newFd: WASIAbi.Fd, newPath: String + ) throws + + /// Create a symbolic link. + func path_symlink( + oldPath: String, dirFd: WASIAbi.Fd, newPath: String + ) throws + + /// Unlink a file. + func path_unlink_file( + dirFd: WASIAbi.Fd, + path: String + ) throws + + /// Concurrently poll for the occurrence of a set of events. + func poll_oneoff( + subscriptions: UnsafeGuestRawPointer, + events: UnsafeGuestRawPointer, + numberOfSubscriptions: WASIAbi.Size + ) throws -> WASIAbi.Size + + /// Write high-quality random data into a buffer. + func random_get(buffer: UnsafeGuestPointer, length: WASIAbi.Size) +} + +enum WASIAbi { + enum Errno: UInt32, Error { + /// No error occurred. System call completed successfully. + case SUCCESS = 0 + /// Argument list too long. + case E2BIG = 1 + /// Permission denied. + case EACCES = 2 + /// Address in use. + case EADDRINUSE = 3 + /// Address not available. + case EADDRNOTAVAIL = 4 + /// Address family not supported. + case EAFNOSUPPORT = 5 + /// Resource unavailable, or operation would block. + case EAGAIN = 6 + /// Connection already in progress. + case EALREADY = 7 + /// Bad file descriptor. + case EBADF = 8 + /// Bad message. + case EBADMSG = 9 + /// Device or resource busy. + case EBUSY = 10 + /// Operation canceled. + case ECANCELED = 11 + /// No child processes. + case ECHILD = 12 + /// Connection aborted. + case ECONNABORTED = 13 + /// Connection refused. + case ECONNREFUSED = 14 + /// Connection reset. + case ECONNRESET = 15 + /// Resource deadlock would occur. + case EDEADLK = 16 + /// Destination address required. + case EDESTADDRREQ = 17 + /// Mathematics argument out of domain of function. + case EDOM = 18 + /// Reserved. + case EDQUOT = 19 + /// File exists. + case EEXIST = 20 + /// Bad address. + case EFAULT = 21 + /// File too large. + case EFBIG = 22 + /// Host is unreachable. + case EHOSTUNREACH = 23 + /// Identifier removed. + case EIDRM = 24 + /// Illegal byte sequence. + case EILSEQ = 25 + /// Operation in progress. + case EINPROGRESS = 26 + /// Interrupted function. + case EINTR = 27 + /// Invalid argument. + case EINVAL = 28 + /// I/O error. + case EIO = 29 + /// Socket is connected. + case EISCONN = 30 + /// Is a directory. + case EISDIR = 31 + /// Too many levels of symbolic links. + case ELOOP = 32 + /// File descriptor value too large. + case EMFILE = 33 + /// Too many links. + case EMLINK = 34 + /// Message too large. + case EMSGSIZE = 35 + /// Reserved. + case EMULTIHOP = 36 + /// Filename too long. + case ENAMETOOLONG = 37 + /// Network is down. + case ENETDOWN = 38 + /// Connection aborted by network. + case ENETRESET = 39 + /// Network unreachable. + case ENETUNREACH = 40 + /// Too many files open in system. + case ENFILE = 41 + /// No buffer space available. + case ENOBUFS = 42 + /// No such device. + case ENODEV = 43 + /// No such file or directory. + case ENOENT = 44 + /// Executable file format error. + case ENOEXEC = 45 + /// No locks available. + case ENOLCK = 46 + /// Reserved. + case ENOLINK = 47 + /// Not enough space. + case ENOMEM = 48 + /// No message of the desired type. + case ENOMSG = 49 + /// Protocol not available. + case ENOPROTOOPT = 50 + /// No space left on device. + case ENOSPC = 51 + /// Function not supported. + case ENOSYS = 52 + /// The socket is not connected. + case ENOTCONN = 53 + /// Not a directory or a symbolic link to a directory. + case ENOTDIR = 54 + /// Directory not empty. + case ENOTEMPTY = 55 + /// State not recoverable. + case ENOTRECOVERABLE = 56 + /// Not a socket. + case ENOTSOCK = 57 + /// Not supported, or operation not supported on socket. + case ENOTSUP = 58 + /// Inappropriate I/O control operation. + case ENOTTY = 59 + /// No such device or address. + case ENXIO = 60 + /// Value too large to be stored in data type. + case EOVERFLOW = 61 + /// Previous owner died. + case EOWNERDEAD = 62 + /// Operation not permitted. + case EPERM = 63 + /// Broken pipe. + case EPIPE = 64 + /// Protocol error. + case EPROTO = 65 + /// Protocol not supported. + case EPROTONOSUPPORT = 66 + /// Protocol wrong type for socket. + case EPROTOTYPE = 67 + /// Result too large. + case ERANGE = 68 + /// Read-only file system. + case EROFS = 69 + /// Invalid seek. + case ESPIPE = 70 + /// No such process. + case ESRCH = 71 + /// Reserved. + case ESTALE = 72 + /// Connection timed out. + case ETIMEDOUT = 73 + /// Text file busy. + case ETXTBSY = 74 + /// Cross-device link. + case EXDEV = 75 + /// Extension: Capabilities insufficient. + case ENOTCAPABLE = 76 + } + + typealias Size = UInt32 + + /// Non-negative file size or length of a region within a file. + typealias FileSize = UInt64 + + typealias Fd = UInt32 + + struct IOVec: GuestPointee { + let buffer: UnsafeGuestRawPointer + let length: WASIAbi.Size + + func withHostBufferPointer(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try buffer.withHostPointer { hostPointer in + try body(UnsafeRawBufferPointer(start: hostPointer, count: Int(length))) + } + } + + static var sizeInGuest: UInt32 { + return UnsafeGuestRawPointer.sizeInGuest + WASIAbi.Size.sizeInGuest + } + + static var alignInGuest: UInt32 { + max(UnsafeGuestRawPointer.alignInGuest, WASIAbi.Size.alignInGuest) + } + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> IOVec { + return IOVec( + buffer: .readFromGuest(pointer), + length: .readFromGuest(pointer.advanced(by: UnsafeGuestRawPointer.sizeInGuest)) + ) + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: IOVec) { + UnsafeGuestRawPointer.writeToGuest(at: pointer, value: value.buffer) + WASIAbi.Size.writeToGuest(at: pointer.advanced(by: UnsafeGuestRawPointer.sizeInGuest), value: value.length) + } + } + + /// Relative offset within a file. + typealias FileDelta = Int64 + + /// The position relative to which to set the offset of the file descriptor. + enum Whence: UInt8 { + /// Seek relative to start-of-file. + case SET = 0 + /// Seek relative to current position. + case CUR = 1 + /// Seek relative to end-of-file. + case END = 2 + } + + enum ClockId: UInt32 { + /// The clock measuring real time. Time value zero corresponds with + /// 1970-01-01T00:00:00Z. + case REALTIME = 0 + /// The store-wide monotonic clock, which is defined as a clock measuring + /// real time, whose value cannot be adjusted and which cannot have negative + /// clock jumps. The epoch of this clock is undefined. The absolute time + /// value of this clock therefore has no meaning. + case MONOTONIC = 1 + /// The CPU-time clock associated with the current process. + case PROCESS_CPUTIME_ID = 2 + /// The CPU-time clock associated with the current thread. + case THREAD_CPUTIME_ID = 3 + } + + typealias Timestamp = UInt64 + + struct Fdflags: OptionSet, GuestPrimitivePointee { + var rawValue: UInt16 + /// Append mode: Data written to the file is always appended to the file's end. + static let APPEND = Fdflags(rawValue: 1 << 0) + /// Write according to synchronized I/O data integrity completion. Only the data stored in the file is synchronized. + static let DSYNC = Fdflags(rawValue: 1 << 1) + /// Non-blocking mode. + static let NONBLOCK = Fdflags(rawValue: 1 << 2) + /// Synchronized read I/O operations. + static let RSYNC = Fdflags(rawValue: 1 << 3) + /// Write according to synchronized I/O file integrity completion. In + /// addition to synchronizing the data stored in the file, the implementation + /// may also synchronously update the file's metadata. + static let SYNC = Fdflags(rawValue: 1 << 4) + } + + struct Rights: OptionSet, GuestPrimitivePointee { + let rawValue: UInt64 + + /// The right to invoke `fd_datasync`. + /// If `path_open` is set, includes the right to invoke + /// `path_open` with `fdflags::dsync`. + static let FD_DATASYNC = Rights(rawValue: 1 << 0) + /// The right to invoke `fd_read` and `sock_recv`. + /// If `rights::fd_seek` is set, includes the right to invoke `fd_pread`. + static let FD_READ = Rights(rawValue: 1 << 1) + /// The right to invoke `fd_seek`. This flag implies `rights::fd_tell`. + static let FD_SEEK = Rights(rawValue: 1 << 2) + /// The right to invoke `fd_fdstat_set_flags`. + static let FD_FDSTAT_SET_FLAGS = Rights(rawValue: 1 << 3) + /// The right to invoke `fd_sync`. + /// If `path_open` is set, includes the right to invoke + /// `path_open` with `fdflags::rsync` and `fdflags::dsync`. + static let FD_SYNC = Rights(rawValue: 1 << 4) + /// The right to invoke `fd_seek` in such a way that the file offset + /// remains unaltered (i.e., `whence::cur` with offset zero), or to + /// invoke `fd_tell`. + static let FD_TELL = Rights(rawValue: 1 << 5) + /// The right to invoke `fd_write` and `sock_send`. + /// If `rights::fd_seek` is set, includes the right to invoke `fd_pwrite`. + static let FD_WRITE = Rights(rawValue: 1 << 6) + /// The right to invoke `fd_advise`. + static let FD_ADVISE = Rights(rawValue: 1 << 7) + /// The right to invoke `fd_allocate`. + static let FD_ALLOCATE = Rights(rawValue: 1 << 8) + /// The right to invoke `path_create_directory`. + static let PATH_CREATE_DIRECTORY = Rights(rawValue: 1 << 9) + /// If `path_open` is set, the right to invoke `path_open` with `oflags::creat`. + static let PATH_CREATE_FILE = Rights(rawValue: 1 << 10) + /// The right to invoke `path_link` with the file descriptor as the + /// source directory. + static let PATH_LINK_SOURCE = Rights(rawValue: 1 << 11) + /// The right to invoke `path_link` with the file descriptor as the + /// target directory. + static let PATH_LINK_TARGET = Rights(rawValue: 1 << 12) + /// The right to invoke `path_open`. + static let PATH_OPEN = Rights(rawValue: 1 << 13) + /// The right to invoke `fd_readdir`. + static let FD_READDIR = Rights(rawValue: 1 << 14) + /// The right to invoke `path_readlink`. + static let PATH_READLINK = Rights(rawValue: 1 << 15) + /// The right to invoke `path_rename` with the file descriptor as the source directory. + static let PATH_RENAME_SOURCE = Rights(rawValue: 1 << 16) + /// The right to invoke `path_rename` with the file descriptor as the target directory. + static let PATH_RENAME_TARGET = Rights(rawValue: 1 << 17) + /// The right to invoke `path_filestat_get`. + static let PATH_FILESTAT_GET = Rights(rawValue: 1 << 18) + /// The right to change a file's size (there is no `path_filestat_set_size`). + /// If `path_open` is set, includes the right to invoke `path_open` with `oflags::trunc`. + static let PATH_FILESTAT_SET_SIZE = Rights(rawValue: 1 << 19) + /// The right to invoke `path_filestat_set_times`. + static let PATH_FILESTAT_SET_TIMES = Rights(rawValue: 1 << 20) + /// The right to invoke `fd_filestat_get`. + static let FD_FILESTAT_GET = Rights(rawValue: 1 << 21) + /// The right to invoke `fd_filestat_set_size`. + static let FD_FILESTAT_SET_SIZE = Rights(rawValue: 1 << 22) + /// The right to invoke `fd_filestat_set_times`. + static let FD_FILESTAT_SET_TIMES = Rights(rawValue: 1 << 23) + /// The right to invoke `path_symlink`. + static let PATH_SYMLINK = Rights(rawValue: 1 << 24) + /// The right to invoke `path_remove_directory`. + static let PATH_REMOVE_DIRECTORY = Rights(rawValue: 1 << 25) + /// The right to invoke `path_unlink_file`. + static let PATH_UNLINK_FILE = Rights(rawValue: 1 << 26) + /// If `rights::fd_read` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_read`. + /// If `rights::fd_write` is set, includes the right to invoke `poll_oneoff` to subscribe to `eventtype::fd_write`. + static let POLL_FD_READWRITE = Rights(rawValue: 1 << 27) + /// The right to invoke `sock_shutdown`. + static let SOCK_SHUTDOWN = Rights(rawValue: 1 << 28) + /// The right to invoke `sock_accept`. + static let SOCK_ACCEPT = Rights(rawValue: 1 << 29) + + static let DIRECTORY_BASE_RIGHTS: Rights = [ + .PATH_CREATE_DIRECTORY, + .PATH_CREATE_FILE, + .PATH_LINK_SOURCE, + .PATH_LINK_TARGET, + .PATH_OPEN, + .FD_READDIR, + .PATH_READLINK, + .PATH_RENAME_SOURCE, + .PATH_RENAME_TARGET, + .PATH_SYMLINK, + .PATH_REMOVE_DIRECTORY, + .PATH_UNLINK_FILE, + .PATH_FILESTAT_GET, + .PATH_FILESTAT_SET_TIMES, + .FD_FILESTAT_GET, + .FD_FILESTAT_SET_TIMES, + ] + + static let DIRECTORY_INHERITING_RIGHTS: Rights = DIRECTORY_BASE_RIGHTS.union([ + .FD_DATASYNC, + .FD_READ, + .FD_SEEK, + .FD_FDSTAT_SET_FLAGS, + .FD_SYNC, + .FD_TELL, + .FD_WRITE, + .FD_ADVISE, + .FD_ALLOCATE, + .FD_FILESTAT_GET, + .FD_FILESTAT_SET_SIZE, + .FD_FILESTAT_SET_TIMES, + .POLL_FD_READWRITE, + ]) + } + + /// A reference to the offset of a directory entry. + /// The value 0 signifies the start of the directory. + typealias DirCookie = UInt64 + /// The type for the `dirent::d_namlen` field of `dirent` struct. + typealias DirNameLen = UInt32 + + /// File serial number that is unique within its file system. + typealias Inode = UInt64 + + /// The type of a file descriptor or file. + enum FileType: UInt8, GuestPrimitivePointee { + /// The type of the file descriptor or file is unknown or is different from any of the other types specified. + case UNKNOWN = 0 + /// The file descriptor or file refers to a block device inode. + case BLOCK_DEVICE = 1 + /// The file descriptor or file refers to a character device inode. + case CHARACTER_DEVICE = 2 + /// The file descriptor or file refers to a directory inode. + case DIRECTORY = 3 + /// The file descriptor or file refers to a regular file inode. + case REGULAR_FILE = 4 + /// The file descriptor or file refers to a datagram socket. + case SOCKET_DGRAM = 5 + /// The file descriptor or file refers to a byte-stream socket. + case SOCKET_STREAM = 6 + /// The file refers to a symbolic link inode. + case SYMBOLIC_LINK = 7 + } + + /// A directory entry. + struct Dirent { + /// The offset of the next directory entry stored in this directory. + let dNext: DirCookie + /// The serial number of the file referred to by this directory entry. + let dIno: Inode + /// The length of the name of the directory entry. + let dirNameLen: DirNameLen + /// The type of the file referred to by this directory entry. + let dType: FileType + + static var sizeInGuest: UInt32 { + // Hard coded because WIT aligns up at last when calculating struct size, but Swift doesn't + // https://github.com/WebAssembly/WASI/blob/4712d490fd7662f689af6faa5d718e042f014931/legacy/tools/witx/src/layout.rs#L117C24-L117C24 + 24 + } + + static func writeToGuest(unalignedAt pointer: UnsafeGuestRawPointer, end: UnsafeGuestRawPointer, value: Dirent) { + var pointer = pointer + guard pointer < end else { return } + DirCookie.writeToGuest(at: pointer, value: value.dNext) + pointer = pointer.advanced(by: DirCookie.sizeInGuest) + + guard pointer < end else { return } + Inode.writeToGuest(at: pointer, value: value.dIno) + pointer = pointer.advanced(by: Inode.sizeInGuest) + + guard pointer < end else { return } + DirNameLen.writeToGuest(at: pointer, value: value.dirNameLen) + pointer = pointer.advanced(by: DirNameLen.sizeInGuest) + + guard pointer < end else { return } + FileType.writeToGuest(at: pointer, value: value.dType) + pointer = pointer.advanced(by: FileType.sizeInGuest) + } + } + + enum Advice: UInt8 { + /// The application has no advice to give on its behavior with respect to the specified data. + case NORMAL = 0 + /// The application expects to access the specified data sequentially from lower offsets to higher offsets. + case SEQUENTIAL = 1 + /// The application expects to access the specified data in a random order. + case RANDOM = 2 + /// The application expects to access the specified data in the near future. + case WILLNEED = 3 + /// The application expects that it will not access the specified data in the near future. + case DONTNEED = 4 + /// The application expects to access the specified data once and then not reuse it thereafter. + case NOREUSE = 5 + } + + struct FdStat: GuestPrimitivePointee { + let fsFileType: FileType + let fsFlags: Fdflags + let fsRightsBase: Rights + let fsRightsInheriting: Rights + + static var sizeInGuest: UInt32 { + FileType.sizeInGuest + Fdflags.sizeInGuest + Rights.sizeInGuest * 2 + } + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> FdStat { + var pointer = pointer + return FdStat( + fsFileType: .readFromGuest(&pointer), + fsFlags: .readFromGuest(&pointer), + fsRightsBase: .readFromGuest(&pointer), + fsRightsInheriting: .readFromGuest(&pointer) + ) + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: FdStat) { + var pointer = pointer + FileType.writeToGuest(at: &pointer, value: value.fsFileType) + Fdflags.writeToGuest(at: &pointer, value: value.fsFlags) + Rights.writeToGuest(at: &pointer, value: value.fsRightsBase) + Rights.writeToGuest(at: &pointer, value: value.fsRightsInheriting) + } + } + + /// Identifier for a device containing a file system. Can be used in combination + /// with `inode` to uniquely identify a file or directory in the filesystem. + typealias Device = UInt64 + + /// Which file time attributes to adjust. + struct FstFlags: OptionSet, GuestPrimitivePointee { + let rawValue: UInt16 + + static let ATIM = FstFlags(rawValue: 1 << 0) + /// Adjust the last data access timestamp to the time of clock `clockid::realtime`. + static let ATIM_NOW = FstFlags(rawValue: 1 << 1) + /// Adjust the last data modification timestamp to the value stored in `filestat::mtim`. + static let MTIM = FstFlags(rawValue: 1 << 2) + /// Adjust the last data modification timestamp to the time of clock `clockid::realtime`. + static let MTIM_NOW = FstFlags(rawValue: 1 << 3) + } + + struct LookupFlags: OptionSet, GuestPrimitivePointee { + let rawValue: UInt32 + + /// As long as the resolved path corresponds to a symbolic link, it is expanded. + static let SYMLINK_FOLLOW = LookupFlags(rawValue: 1 << 0) + } + + struct Oflags: OptionSet, GuestPrimitivePointee { + let rawValue: UInt32 + + /// Create file if it does not exist. + static let CREAT = Oflags(rawValue: 1 << 0) + /// Fail if not a directory. + static let DIRECTORY = Oflags(rawValue: 1 << 1) + /// Fail if file already exists. + static let EXCL = Oflags(rawValue: 1 << 2) + /// Truncate file to size 0. + static let TRUNC = Oflags(rawValue: 1 << 3) + } + + /// Number of hard links to an inode. + typealias LinkCount = UInt64 + + /// File attributes. + struct Filestat: GuestPrimitivePointee { + /// Device ID of device containing the file. + let dev: Device + /// File serial number. + let ino: Inode + /// File type. + let filetype: FileType + /// Number of hard links to the file. + let nlink: LinkCount + /// For regular files, the file size in bytes. For symbolic links, the length in bytes of the pathname contained in the symbolic link. + let size: FileSize + /// Last data access timestamp. + let atim: Timestamp + /// Last data modification timestamp. + let mtim: Timestamp + /// Last file status change timestamp. + let ctim: Timestamp + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> WASIAbi.Filestat { + var pointer = pointer + return Filestat( + dev: .readFromGuest(&pointer), ino: .readFromGuest(&pointer), + filetype: .readFromGuest(&pointer), nlink: .readFromGuest(&pointer), + size: .readFromGuest(&pointer), atim: .readFromGuest(&pointer), + mtim: .readFromGuest(&pointer), ctim: .readFromGuest(&pointer) + ) + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: WASIAbi.Filestat) { + var pointer = pointer + Device.writeToGuest(at: &pointer, value: value.dev) + Inode.writeToGuest(at: &pointer, value: value.ino) + FileType.writeToGuest(at: &pointer, value: value.filetype) + LinkCount.writeToGuest(at: &pointer, value: value.nlink) + FileSize.writeToGuest(at: &pointer, value: value.size) + Timestamp.writeToGuest(at: &pointer, value: value.atim) + Timestamp.writeToGuest(at: &pointer, value: value.mtim) + Timestamp.writeToGuest(at: &pointer, value: value.ctim) + } + } + + typealias PrestatDir = Size + + enum Prestat: GuestPointee { + case dir(PrestatDir) + static var sizeInGuest: UInt32 { 8 } + static var alignInGuest: UInt32 { 4 } + + static func readFromGuest(_ pointer: UnsafeGuestRawPointer) -> WASIAbi.Prestat { + var pointer = pointer + switch UInt8.readFromGuest(&pointer) { + case 0: + return .dir(.readFromGuest(&pointer)) + default: fatalError() + } + } + + static func writeToGuest(at pointer: UnsafeGuestRawPointer, value: WASIAbi.Prestat) { + var pointer = pointer + switch value { + case .dir(let dir): + UInt8.writeToGuest(at: &pointer, value: 0) + PrestatDir.writeToGuest(at: &pointer, value: dir) + } + } + } +} + +struct WASIError: Error, CustomStringConvertible { + let description: String +} + +struct WASIExitCode: Error { + let code: UInt32 +} + +extension WASI { + var _hostModules: [String: HostModule] { + let unimplementedFunctionTypes: [String: FunctionType] = [ + "poll_oneoff": .init(parameters: [.i32, .i32, .i32, .i32], results: [.i32]), + "proc_raise": .init(parameters: [.i32], results: [.i32]), + "sched_yield": .init(parameters: [], results: [.i32]), + "sock_accept": .init(parameters: [.i32, .i32, .i32], results: [.i32]), + "sock_recv": .init(parameters: [.i32, .i32, .i32, .i32, .i32, .i32], results: [.i32]), + "sock_send": .init(parameters: [.i32, .i32, .i32, .i32, .i32], results: [.i32]), + "sock_shutdown": .init(parameters: [.i32, .i32], results: [.i32]), + + ] + + var preview1: [String: HostFunction] = unimplementedFunctionTypes.reduce(into: [:]) { functions, entry in + let (name, type) = entry + functions[name] = HostFunction(type: type) { _, _ in + print("\"\(name)\" not implemented yet") + return [.i32(WASIAbi.Errno.ENOSYS.rawValue)] + } + } + + func withMemoryBuffer( + caller: Caller, + body: (GuestMemory) throws -> T + ) throws -> T { + guard case let .memory(memoryAddr) = caller.instance.exports["memory"] else { + throw WASIError(description: "Missing required \"memory\" export") + } + let memory = GuestMemory(store: caller.store, address: memoryAddr) + return try body(memory) + } + + func readString(pointer: UInt32, length: UInt32, buffer: GuestMemory) throws -> String { + let pointer = UnsafeGuestBufferPointer( + baseAddress: UnsafeGuestPointer(memorySpace: buffer, offset: pointer), + count: length + ) + return try pointer.withHostPointer { hostBuffer in + guard let baseAddress = hostBuffer.baseAddress, + memchr(baseAddress, 0x00, Int(pointer.count)) == nil + else { + // If byte sequence contains null byte in the middle, it's illegal string + // TODO: This restriction should be only applied to strings that can be interpreted as platform-string, which is expected to be null-terminated + throw WASIAbi.Errno.EILSEQ + } + return String(decoding: hostBuffer, as: UTF8.self) + } + } + + func wasiFunction(type: FunctionType, implementation: @escaping (Caller, [Value]) throws -> [Value]) -> HostFunction { + return HostFunction(type: type) { caller, arguments in + do { + return try implementation(caller, arguments) + } catch let errno as WASIAbi.Errno { + return [.i32(errno.rawValue)] + } + } + } + + preview1["args_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + self.args_get( + argv: .init(memorySpace: buffer, offset: arguments[0].i32), + argvBuffer: .init(memorySpace: buffer, offset: arguments[1].i32) + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["args_sizes_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let (argc, bufferSize) = self.args_sizes_get() + let argcPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[0].i32) + argcPointer.pointee = argc + let bufferSizePointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + bufferSizePointer.pointee = bufferSize + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["environ_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + self.environ_get( + environ: .init(memorySpace: buffer, offset: arguments[0].i32), + environBuffer: .init(memorySpace: buffer, offset: arguments[1].i32) + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["environ_sizes_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let (environSize, bufferSize) = self.environ_sizes_get() + let environSizePointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[0].i32) + environSizePointer.pointee = environSize + let bufferSizePointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + bufferSizePointer.pointee = bufferSize + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["clock_res_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + guard let id = WASIAbi.ClockId(rawValue: arguments[0].i32) else { + throw WASIAbi.Errno.EBADF + } + let res = try self.clock_res_get(id: id) + try withMemoryBuffer(caller: caller) { buffer in + let resPointer = UnsafeGuestPointer( + memorySpace: buffer, offset: arguments[1].i32 + ) + resPointer.pointee = res + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["clock_time_get"] = wasiFunction( + type: .init(parameters: [.i32, .i64, .i32], results: [.i32]) + ) { caller, arguments in + guard let id = WASIAbi.ClockId(rawValue: arguments[0].i32) else { + throw WASIAbi.Errno.EBADF + } + let time = try self.clock_time_get(id: id, precision: WASIAbi.Timestamp(arguments[1].i64)) + try withMemoryBuffer(caller: caller) { buffer in + let resPointer = UnsafeGuestPointer( + memorySpace: buffer, offset: arguments[2].i32 + ) + resPointer.pointee = time + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_advise"] = wasiFunction( + type: .init(parameters: [.i32, .i64, .i64, .i32], results: [.i32]) + ) { caller, arguments in + guard let rawAdvice = UInt8(exactly: arguments[3].i32), + let advice = WASIAbi.Advice(rawValue: rawAdvice) + else { + throw WASIAbi.Errno.EINVAL + } + try self.fd_advise( + fd: arguments[0].i32, offset: arguments[1].i64, + length: arguments[2].i64, advice: advice + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_allocate"] = wasiFunction( + type: .init(parameters: [.i32, .i64, .i64], results: [.i32]) + ) { caller, arguments in + try self.fd_allocate( + fd: arguments[0].i32, offset: arguments[1].i64, length: arguments[2].i64 + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_close"] = wasiFunction( + type: .init(parameters: [.i32], results: [.i32]) + ) { caller, arguments in + try self.fd_close(fd: arguments[0].i32) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_datasync"] = wasiFunction( + type: .init(parameters: [.i32], results: [.i32]) + ) { caller, arguments in + try self.fd_datasync(fd: arguments[0].i32) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_fdstat_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let stat = try self.fd_fdstat_get(fileDescriptor: arguments[0].i32) + let statPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + statPointer.pointee = stat + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["fd_fdstat_set_flags"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + guard let rawFdFlags = UInt16(exactly: arguments[1].i32) else { + throw WASIAbi.Errno.EINVAL + } + try self.fd_fdstat_set_flags( + fd: arguments[0].i32, flags: WASIAbi.Fdflags(rawValue: rawFdFlags) + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_fdstat_set_rights"] = wasiFunction( + type: .init(parameters: [.i32, .i64, .i64], results: [.i32]) + ) { caller, arguments in + try self.fd_fdstat_set_rights( + fd: arguments[0].i32, + fsRightsBase: WASIAbi.Rights(rawValue: arguments[1].i64), + fsRightsInheriting: WASIAbi.Rights(rawValue: arguments[2].i64) + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_filestat_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let filestat = try self.fd_filestat_get(fd: arguments[0].i32) + let filestatPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + filestatPointer.pointee = filestat + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_filestat_set_size"] = wasiFunction( + type: .init(parameters: [.i32, .i64], results: [.i32]) + ) { caller, arguments in + try self.fd_filestat_set_size(fd: arguments[0].i32, size: arguments[1].i64) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_filestat_set_times"] = wasiFunction( + type: .init(parameters: [.i32, .i64, .i64, .i32], results: [.i32]) + ) { caller, arguments in + guard let rawFstFlags = UInt16(exactly: arguments[3].i32) else { + throw WASIAbi.Errno.EINVAL + } + try self.fd_filestat_set_times( + fd: arguments[0].i32, + atim: arguments[1].i64, mtim: arguments[2].i64, + fstFlags: WASIAbi.FstFlags(rawValue: rawFstFlags) + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_pread"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i64, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let nread = try self.fd_pread( + fd: arguments[0].i32, + iovs: UnsafeGuestBufferPointer( + baseAddress: .init(memorySpace: buffer, offset: arguments[1].i32), + count: arguments[2].i32 + ), + offset: arguments[3].i64 + ) + let nreadPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) + nreadPointer.pointee = nread + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + preview1["fd_prestat_get"] = wasiFunction(type: .init(parameters: [.i32, .i32], results: [.i32])) { caller, arguments in + let prestat = try self.fd_prestat_get(fd: arguments[0].i32) + try withMemoryBuffer(caller: caller) { buffer in + let prestatPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + prestatPointer.pointee = prestat + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_prestat_dir_name"] = wasiFunction(type: .init(parameters: [.i32, .i32, .i32], results: [.i32])) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.fd_prestat_dir_name( + fd: arguments[0].i32, + path: UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32), + maxPathLength: arguments[2].i32 + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_pwrite"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i64, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let nwritten = try self.fd_pwrite( + fd: arguments[0].i32, + iovs: UnsafeGuestBufferPointer( + baseAddress: .init(memorySpace: buffer, offset: arguments[1].i32), + count: arguments[2].i32 + ), + offset: arguments[3].i64 + ) + let nwrittenPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) + nwrittenPointer.pointee = nwritten + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_read"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let nread = try self.fd_read( + fd: arguments[0].i32, + iovs: UnsafeGuestBufferPointer( + baseAddress: .init(memorySpace: buffer, offset: arguments[1].i32), + count: arguments[2].i32 + ) + ) + let nreadPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[3].i32) + nreadPointer.pointee = nread + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_readdir"] = wasiFunction(type: .init(parameters: [.i32, .i32, .i32, .i64, .i32], results: [.i32])) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let nwritten = try self.fd_readdir( + fd: arguments[0].i32, + buffer: UnsafeGuestBufferPointer( + baseAddress: UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32), + count: arguments[2].i32 + ), + cookie: arguments[3].i64 + ) + let nwrittenPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) + nwrittenPointer.pointee = nwritten + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["fd_renumber"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try self.fd_renumber(fd: arguments[0].i32, to: arguments[1].i32) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_seek"] = wasiFunction( + type: .init(parameters: [.i32, .i64, .i32, .i32], results: [.i32]) + ) { caller, arguments in + guard let whence = WASIAbi.Whence(rawValue: UInt8(arguments[2].i32)) else { + return [.i32(WASIAbi.Errno.EINVAL.rawValue)] + } + let ret = try self.fd_seek( + fd: arguments[0].i32, offset: WASIAbi.FileDelta(bitPattern: arguments[1].i64), whence: whence + ) + try withMemoryBuffer(caller: caller) { buffer in + let retPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[3].i32) + retPointer.pointee = ret + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_sync"] = wasiFunction(type: .init(parameters: [.i32], results: [.i32])) { caller, arguments in + try self.fd_sync(fd: arguments[0].i32) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_tell"] = wasiFunction(type: .init(parameters: [.i32, .i32], results: [.i32])) { caller, arguments in + let ret = try self.fd_tell(fd: arguments[0].i32) + try withMemoryBuffer(caller: caller) { buffer in + let retPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[1].i32) + retPointer.pointee = ret + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["fd_write"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let nwritten = try self.fd_write( + fileDescriptor: arguments[0].i32, + ioVectors: UnsafeGuestBufferPointer( + baseAddress: .init(memorySpace: buffer, offset: arguments[1].i32), + count: arguments[2].i32 + ) + ) + let nwrittenPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[3].i32) + nwrittenPointer.pointee = nwritten + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["path_create_directory"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.path_create_directory( + dirFd: arguments[0].i32, + path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + preview1["path_filestat_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let filestat = try self.path_filestat_get( + dirFd: arguments[0].i32, flags: .init(rawValue: arguments[1].i32), + path: readString(pointer: arguments[2].i32, length: arguments[3].i32, buffer: buffer) + ) + let filestatPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[4].i32) + filestatPointer.pointee = filestat + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_filestat_set_times"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i64, .i64, .i32], results: [.i32]) + ) { caller, arguments in + guard let rawFstFlags = UInt16(exactly: arguments[6].i32) else { + throw WASIAbi.Errno.EINVAL + } + try withMemoryBuffer(caller: caller) { buffer in + try self.path_filestat_set_times( + dirFd: arguments[0].i32, flags: .init(rawValue: arguments[1].i32), + path: readString(pointer: arguments[2].i32, length: arguments[3].i32, buffer: buffer), + atim: arguments[4].i64, mtim: arguments[5].i64, + fstFlags: WASIAbi.FstFlags(rawValue: rawFstFlags) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_link"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.path_link( + oldFd: arguments[0].i32, oldFlags: .init(rawValue: arguments[1].i32), + oldPath: readString(pointer: arguments[2].i32, length: arguments[3].i32, buffer: buffer), + newFd: arguments[4].i32, + newPath: readString(pointer: arguments[5].i32, length: arguments[6].i32, buffer: buffer) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_open"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i32, .i64, .i64, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let newFd = try self.path_open( + dirFd: arguments[0].i32, + dirFlags: .init(rawValue: arguments[1].i32), + path: readString(pointer: arguments[2].i32, length: arguments[3].i32, buffer: buffer), + oflags: .init(rawValue: arguments[4].i32), + fsRightsBase: .init(rawValue: arguments[5].i64), + fsRightsInheriting: .init(rawValue: arguments[6].i64), + fdflags: .init(rawValue: UInt16(arguments[7].i32)) + ) + let newFdPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[8].i32) + newFdPointer.pointee = newFd + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + preview1["path_readlink"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + let ret = try self.path_readlink( + fd: arguments[0].i32, + path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer), + buffer: UnsafeGuestBufferPointer( + baseAddress: .init(memorySpace: buffer, offset: arguments[3].i32), + count: arguments[4].i32 + ) + ) + let retPointer = UnsafeGuestPointer(memorySpace: buffer, offset: arguments[5].i32) + retPointer.pointee = ret + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_remove_directory"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.path_remove_directory( + dirFd: arguments[0].i32, + path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_rename"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.path_rename( + oldFd: arguments[0].i32, + oldPath: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer), + newFd: arguments[3].i32, + newPath: readString(pointer: arguments[4].i32, length: arguments[5].i32, buffer: buffer) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_symlink"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.path_symlink( + oldPath: readString(pointer: arguments[0].i32, length: arguments[1].i32, buffer: buffer), + dirFd: arguments[2].i32, + newPath: readString(pointer: arguments[3].i32, length: arguments[4].i32, buffer: buffer) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["path_unlink_file"] = wasiFunction( + type: .init(parameters: [.i32, .i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + try self.path_unlink_file( + dirFd: arguments[0].i32, + path: readString(pointer: arguments[1].i32, length: arguments[2].i32, buffer: buffer) + ) + } + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + + preview1["proc_exit"] = wasiFunction(type: .init(parameters: [.i32])) { memory, arguments in + let exitCode = arguments[0].i32 + throw WASIExitCode(code: exitCode) + } + + preview1["random_get"] = wasiFunction( + type: .init(parameters: [.i32, .i32], results: [.i32]) + ) { caller, arguments in + try withMemoryBuffer(caller: caller) { buffer in + self.random_get( + buffer: UnsafeGuestPointer(memorySpace: buffer, offset: arguments[0].i32), + length: arguments[1].i32 + ) + return [.i32(WASIAbi.Errno.SUCCESS.rawValue)] + } + } + + return [ + "wasi_snapshot_preview1": HostModule(globals: [:], functions: preview1) + ] + } +} + +public class WASIBridgeToHost: WASI { + private let args: [String] + private let environment: [String: String] + private var fdTable: FdTable + + public init( + args: [String] = [], + environment: [String: String] = [:], + preopens: [String: String] = [:], + stdin: FileDescriptor = .standardInput, + stdout: FileDescriptor = .standardOutput, + stderr: FileDescriptor = .standardError + ) throws { + self.args = args + self.environment = environment + var fdTable = FdTable() + fdTable[0] = .file(StdioFileEntry(fd: stdin, accessMode: .read)) + fdTable[1] = .file(StdioFileEntry(fd: stdout, accessMode: .write)) + fdTable[2] = .file(StdioFileEntry(fd: stderr, accessMode: .write)) + + for (guestPath, hostPath) in preopens { + let fd = try hostPath.withCString { cHostPath in + let fd = open(cHostPath, O_DIRECTORY) + if fd < 0 { + let errno = errno + throw POSIXError(POSIXErrorCode(rawValue: errno)!) + } + return FileDescriptor(rawValue: fd) + } + if try fd.attributes().fileType.isDirectory { + _ = try fdTable.push(.directory(DirEntry(preopenPath: guestPath, fd: fd))) + } + } + self.fdTable = fdTable + } + + public var hostModules: [String: HostModule] { _hostModules } + + public func start(_ instance: ModuleInstance, runtime: Runtime) throws -> UInt32 { + do { + _ = try runtime.invoke(instance, function: "_start") + } catch let code as WASIExitCode { + return code.code + } + return 0 + } + + func args_get( + argv: UnsafeGuestPointer>, + argvBuffer: UnsafeGuestPointer + ) { + var offsets = argv + var buffer = argvBuffer + for arg in args { + offsets.pointee = buffer + offsets += 1 + let count = arg.utf8CString.withUnsafeBytes { bytes in + let count = UInt32(bytes.count) + buffer.raw.withHostPointer { hostRawPointer in + let hostDestBuffer = UnsafeMutableRawBufferPointer(start: hostRawPointer, count: bytes.count) + bytes.copyBytes(to: hostDestBuffer) + } + return count + } + buffer += count + } + } + + func args_sizes_get() -> (WASIAbi.Size, WASIAbi.Size) { + let bufferSize = args.reduce(0) { + // `utf8CString` returns null-terminated bytes and WASI also expect it + $0 + $1.utf8CString.count + } + return (WASIAbi.Size(args.count), WASIAbi.Size(bufferSize)) + } + + func environ_get(environ: UnsafeGuestPointer>, environBuffer: UnsafeGuestPointer) { + var offsets = environ + var buffer = environBuffer + for (key, value) in environment { + offsets.pointee = buffer + offsets += 1 + let count = "\(key)=\(value)".utf8CString.withUnsafeBytes { bytes in + let count = UInt32(bytes.count) + buffer.raw.withHostPointer { hostRawPointer in + let hostDestBuffer = UnsafeMutableRawBufferPointer(start: hostRawPointer, count: bytes.count) + bytes.copyBytes(to: hostDestBuffer) + } + return count + } + buffer += count + } + } + + func environ_sizes_get() -> (WASIAbi.Size, WASIAbi.Size) { + let bufferSize = environment.reduce(0) { + // `utf8CString` returns null-terminated bytes and WASI also expect it + $0 + $1.key.utf8CString.count /* = */ + 1 + $1.value.utf8CString.count + } + return (WASIAbi.Size(environment.count), WASIAbi.Size(bufferSize)) + } + + func clock_res_get(id: WASIAbi.ClockId) throws -> WASIAbi.Timestamp { + let clock: SystemExtras.Clock + switch id { + case .REALTIME: + #if os(Linux) + clock = .boottime + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + clock = .rawMonotonic + #elseif os(OpenBSD) || os(FreeBSD) || os(WASI) + clock = .monotonic + #else + #error("Unsupported platform") + #endif + case .MONOTONIC: + #if os(Linux) + clock = .monotonic + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + clock = .rawUptime + #elseif os(WASI) + clock = .monotonic + #elseif os(OpenBSD) || os(FreeBSD) + clock = .uptime + #else + #error("Unsupported platform") + #endif + case .PROCESS_CPUTIME_ID, .THREAD_CPUTIME_ID: + throw WASIAbi.Errno.EBADF + } + let timeSpec = try WASIAbi.Errno.translatingPlatformErrno { + try clock.resolution() + } + return WASIAbi.Timestamp(platformTimeSpec: timeSpec) + } + + func clock_time_get( + id: WASIAbi.ClockId, precision: WASIAbi.Timestamp + ) throws -> WASIAbi.Timestamp { + let clock: SystemExtras.Clock + switch id { + case .REALTIME: + #if os(Linux) + clock = .boottime + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + clock = .rawMonotonic + #elseif os(OpenBSD) || os(FreeBSD) || os(WASI) + clock = .monotonic + #else + #error("Unsupported platform") + #endif + case .MONOTONIC: + #if os(Linux) + clock = .monotonic + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + clock = .rawUptime + #elseif os(WASI) + clock = .monotonic + #elseif os(OpenBSD) || os(FreeBSD) + clock = .uptime + #else + #error("Unsupported platform") + #endif + case .PROCESS_CPUTIME_ID, .THREAD_CPUTIME_ID: + throw WASIAbi.Errno.EBADF + } + let timeSpec = try WASIAbi.Errno.translatingPlatformErrno { + try clock.currentTime() + } + return WASIAbi.Timestamp(platformTimeSpec: timeSpec) + } + + func fd_advise(fd: WASIAbi.Fd, offset: WASIAbi.FileSize, length: WASIAbi.FileSize, advice: WASIAbi.Advice) throws { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + try fileEntry.advise(offset: offset, length: length, advice: advice) + } + + func fd_allocate(fd: WASIAbi.Fd, offset: WASIAbi.FileSize, length: WASIAbi.FileSize) throws { + guard fdTable[fd] != nil else { + throw WASIAbi.Errno.EBADF + } + // This operation has been removed in preview 2 and is not supported across all linux + // filesystems, and has no support on macos or windows, so just return ENOTSUP now. + throw WASIAbi.Errno.ENOTSUP + } + + func fd_close(fd: WASIAbi.Fd) throws { + guard let entry = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + fdTable[fd] = nil + try entry.asEntry().close() + } + + func fd_datasync(fd: WASIAbi.Fd) throws { + throw WASIAbi.Errno.ENOTSUP + } + + func fd_fdstat_get(fileDescriptor: UInt32) throws -> WASIAbi.FdStat { + let entry = self.fdTable[fileDescriptor] + switch entry { + case let .file(entry): + return try entry.fdStat() + case .directory: + return WASIAbi.FdStat( + fsFileType: .DIRECTORY, + fsFlags: [], + fsRightsBase: .DIRECTORY_BASE_RIGHTS, + fsRightsInheriting: .DIRECTORY_INHERITING_RIGHTS + ) + case .none: + throw WASIAbi.Errno.EBADF + } + } + + func fd_fdstat_set_flags(fd: WASIAbi.Fd, flags: WASIAbi.Fdflags) throws { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + try fileEntry.setFdStatFlags(flags) + } + + func fd_fdstat_set_rights( + fd: WASIAbi.Fd, + fsRightsBase: WASIAbi.Rights, + fsRightsInheriting: WASIAbi.Rights + ) throws { + throw WASIAbi.Errno.ENOTSUP + } + + func fd_filestat_get(fd: WASIAbi.Fd) throws -> WASIAbi.Filestat { + guard let entry = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try entry.asEntry().attributes() + } + + func fd_filestat_set_size(fd: WASIAbi.Fd, size: WASIAbi.FileSize) throws { + guard case let .file(entry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try entry.setFilestatSize(size) + } + + func fd_filestat_set_times( + fd: WASIAbi.Fd, atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws { + guard let entry = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + try entry.asEntry().setTimes(atim: atim, mtim: mtim, fstFlags: fstFlags) + } + + func fd_pread( + fd: WASIAbi.Fd, iovs: UnsafeGuestBufferPointer, + offset: WASIAbi.FileSize + ) throws -> WASIAbi.Size { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try fileEntry.pread(into: iovs, offset: offset) + } + + func fd_prestat_get(fd: WASIAbi.Fd) throws -> WASIAbi.Prestat { + guard case let .directory(entry) = fdTable[fd], + let preopenPath = entry.preopenPath + else { + throw WASIAbi.Errno.EBADF + } + return .dir(WASIAbi.PrestatDir(preopenPath.utf8.count)) + } + + func fd_prestat_dir_name(fd: WASIAbi.Fd, path: UnsafeGuestPointer, maxPathLength: WASIAbi.Size) throws { + guard case let .directory(entry) = fdTable[fd], + var preopenPath = entry.preopenPath + else { + throw WASIAbi.Errno.EBADF + } + + try preopenPath.withUTF8 { bytes in + guard bytes.count <= maxPathLength else { + throw WASIAbi.Errno.ENAMETOOLONG + } + path.withHostPointer { + let buffer = UnsafeMutableRawBufferPointer(start: $0, count: Int(maxPathLength)) + bytes.copyBytes(to: buffer) + } + } + } + + func fd_pwrite( + fd: WASIAbi.Fd, iovs: UnsafeGuestBufferPointer, + offset: WASIAbi.FileSize + ) throws -> WASIAbi.Size { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try fileEntry.pwrite(vectored: iovs, offset: offset) + } + + func fd_read( + fd: WASIAbi.Fd, + iovs: UnsafeGuestBufferPointer + ) throws -> WASIAbi.Size { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try fileEntry.read(into: iovs) + } + + func fd_readdir( + fd: WASIAbi.Fd, + buffer: UnsafeGuestBufferPointer, + cookie: WASIAbi.DirCookie + ) throws -> WASIAbi.Size { + guard case let .directory(dirEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + + let entries = try dirEntry.readEntries(cookie: cookie) + var bufferUsed: WASIAbi.Size = 0 + let totalBufferSize = buffer.count + while let result = entries.next() { + var (entry, name) = try result.get() + do { + // 1. Copy dirent to the buffer + // Copy dirent as much as possible even though the buffer doesn't have enough remaining space + let copyingBytes = min(WASIAbi.Dirent.sizeInGuest, totalBufferSize - bufferUsed) + let rangeStart = buffer.baseAddress.raw.advanced(by: bufferUsed) + let rangeEnd = rangeStart.advanced(by: copyingBytes) + WASIAbi.Dirent.writeToGuest(unalignedAt: rangeStart, end: rangeEnd, value: entry) + bufferUsed += copyingBytes + + // bail out if the remaining buffer space is not enough + if copyingBytes < WASIAbi.Dirent.sizeInGuest { + return totalBufferSize + } + } + + do { + // 2. Copy name string to the buffer + // Same truncation rule applied as above + let copyingBytes = min(entry.dirNameLen, totalBufferSize - bufferUsed) + let rangeStart = buffer.baseAddress.raw.advanced(by: bufferUsed) + name.withUTF8 { bytes in + rangeStart.withHostPointer { rangeStart in + let hostBuffer = UnsafeMutableRawBufferPointer( + start: rangeStart, count: Int(copyingBytes) + ) + bytes.copyBytes(to: hostBuffer, count: Int(copyingBytes)) + } + } + bufferUsed += copyingBytes + + // bail out if the remaining buffer space is not enough + if copyingBytes < entry.dirNameLen { + return totalBufferSize + } + } + } + return bufferUsed + } + + func fd_renumber(fd: WASIAbi.Fd, to toFd: WASIAbi.Fd) throws { + throw WASIAbi.Errno.ENOTSUP + } + + func fd_seek(fd: WASIAbi.Fd, offset: WASIAbi.FileDelta, whence: WASIAbi.Whence) throws -> WASIAbi.FileSize { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try fileEntry.seek(offset: offset, whence: whence) + } + + func fd_sync(fd: WASIAbi.Fd) throws { + throw WASIAbi.Errno.ENOTSUP + } + + func fd_tell(fd: WASIAbi.Fd) throws -> WASIAbi.FileSize { + guard case let .file(fileEntry) = fdTable[fd] else { + throw WASIAbi.Errno.EBADF + } + return try fileEntry.tell() + } + + func fd_write( + fileDescriptor: WASIAbi.Fd, + ioVectors: UnsafeGuestBufferPointer + ) throws -> UInt32 { + guard case let .file(entry) = self.fdTable[fileDescriptor] else { + throw WASIAbi.Errno.EBADF + } + return try entry.write(vectored: ioVectors) + } + + func path_create_directory(dirFd: WASIAbi.Fd, path: String) throws { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + try dirEntry.createDirectory(atPath: path) + } + + func path_filestat_get( + dirFd: WASIAbi.Fd, flags: WASIAbi.LookupFlags, path: String + ) throws -> WASIAbi.Filestat { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + return try dirEntry.attributes( + path: path, symlinkFollow: flags.contains(.SYMLINK_FOLLOW) + ) + } + + func path_filestat_set_times( + dirFd: WASIAbi.Fd, flags: WASIAbi.LookupFlags, + path: String, atim: WASIAbi.Timestamp, mtim: WASIAbi.Timestamp, + fstFlags: WASIAbi.FstFlags + ) throws { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + try dirEntry.setFilestatTimes( + path: path, atim: atim, mtim: mtim, + fstFlags: fstFlags, + symlinkFollow: flags.contains(.SYMLINK_FOLLOW) + ) + } + + func path_link( + oldFd: WASIAbi.Fd, oldFlags: WASIAbi.LookupFlags, oldPath: String, + newFd: WASIAbi.Fd, newPath: String + ) throws { + throw WASIAbi.Errno.ENOTSUP + } + + func path_open( + dirFd: WASIAbi.Fd, + dirFlags: WASIAbi.LookupFlags, + path: String, + oflags: WASIAbi.Oflags, + fsRightsBase: WASIAbi.Rights, + fsRightsInheriting: WASIAbi.Rights, + fdflags: WASIAbi.Fdflags + ) throws -> WASIAbi.Fd { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + var accessMode: FileAccessMode = [] + if fsRightsBase.contains(.FD_READ) { + accessMode.insert(.read) + } + if fsRightsBase.contains(.FD_WRITE) { + accessMode.insert(.write) + } + let hostFd = try dirEntry.openFile( + symlinkFollow: dirFlags.contains(.SYMLINK_FOLLOW), + path: path, oflags: oflags, accessMode: accessMode, + fdflags: fdflags + ) + + let actualFileType = try hostFd.attributes().fileType + if oflags.contains(.DIRECTORY), actualFileType != .directory { + // Check O_DIRECTORY validity just in case when the host system + // doesn't respects O_DIRECTORY. + throw WASIAbi.Errno.ENOTDIR + } + + let newEntry: FdEntry + if actualFileType == .directory { + newEntry = .directory(DirEntry(preopenPath: nil, fd: hostFd)) + } else { + newEntry = .file(RegularFileEntry(fd: hostFd, accessMode: accessMode)) + } + let guestFd = try fdTable.push(newEntry) + return guestFd + } + + func path_readlink(fd: WASIAbi.Fd, path: String, buffer: UnsafeGuestBufferPointer) throws -> WASIAbi.Size { + throw WASIAbi.Errno.ENOTSUP + } + + func path_remove_directory(dirFd: WASIAbi.Fd, path: String) throws { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + try dirEntry.removeDirectory(atPath: path) + } + + func path_rename( + oldFd: WASIAbi.Fd, oldPath: String, + newFd: WASIAbi.Fd, newPath: String + ) throws { + throw WASIAbi.Errno.ENOTSUP + } + + func path_symlink(oldPath: String, dirFd: WASIAbi.Fd, newPath: String) throws { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + try dirEntry.symlink(from: oldPath, to: newPath) + } + + func path_unlink_file(dirFd: WASIAbi.Fd, path: String) throws { + guard case let .directory(dirEntry) = fdTable[dirFd] else { + throw WASIAbi.Errno.ENOTDIR + } + try dirEntry.removeFile(atPath: path) + } + + func poll_oneoff( + subscriptions: UnsafeGuestRawPointer, + events: UnsafeGuestRawPointer, + numberOfSubscriptions: WASIAbi.Size + ) throws -> WASIAbi.Size { + throw WASIAbi.Errno.ENOTSUP + } + + func random_get(buffer: UnsafeGuestPointer, length: WASIAbi.Size) { + buffer.withHostPointer { + swift_stdlib_random(UnsafeMutableRawPointer($0), Int(length)) + } + } +} diff --git a/Sources/WIT/AST.swift b/Sources/WIT/AST.swift new file mode 100644 index 00000000..c6f96eba --- /dev/null +++ b/Sources/WIT/AST.swift @@ -0,0 +1,326 @@ +public struct SourceFileSyntax: Equatable, Hashable, SyntaxNodeProtocol { + public var fileName: String + public var packageId: PackageNameSyntax? + public var items: [ASTItemSyntax] +} + +struct Version: Equatable, Hashable, CustomStringConvertible { + var major: Int + var minor: Int + var patch: Int + var prerelease: String? + var buildMetadata: String? + + var textRange: TextRange + + /// Returns true if the version is the same as the other version. + /// + /// NOTE: This ignores the `textRange` property to allow for + /// comparing versions that were parsed from different places. + func isSameVersion(as other: Version) -> Bool { + major == other.major && minor == other.minor && patch == other.patch && prerelease == other.prerelease && buildMetadata == other.buildMetadata + } + + var description: String { + var text = "\(major).\(minor).\(patch)" + if let prerelease { + text += "-\(prerelease)" + } + if let buildMetadata { + text += "+\(buildMetadata)" + } + return text + } +} + +public enum ASTItemSyntax: Equatable, Hashable { + case interface(SyntaxNode) + case world(SyntaxNode) + case use(SyntaxNode) +} + +public struct PackageNameSyntax: Equatable, Hashable, CustomStringConvertible { + public var namespace: Identifier + public var name: Identifier + var version: Version? + var textRange: TextRange + + /// Returns true if the package name is the same as the other package name. + /// + /// NOTE: This ignores the `textRange` property to allow for + /// comparing package names that were parsed from different places. + func isSamePackage(as other: PackageNameSyntax) -> Bool { + guard namespace.text == other.namespace.text && name.text == other.name.text else { return false } + if let version = version, let otherVersion = other.version { + return version.isSameVersion(as: otherVersion) + } else { + return version == nil && other.version == nil + } + } + + public var description: String { + if let version { + return "\(namespace.text):\(name.text)@\(version)" + } else { + return "\(namespace.text):\(name.text)" + } + } +} + +public struct TopLevelUseSyntax: Equatable, Hashable, SyntaxNodeProtocol { + var item: UsePathSyntax + var asName: Identifier? +} + +public struct WorldSyntax: Equatable, Hashable, SyntaxNodeProtocol { + public typealias Parent = SourceFileSyntax + public var documents: DocumentsSyntax + public var name: Identifier + public var items: [WorldItemSyntax] +} + +public enum WorldItemSyntax: Equatable, Hashable { + case `import`(ImportSyntax) + case export(ExportSyntax) + case use(SyntaxNode) + case type(SyntaxNode) + case include(IncludeSyntax) +} + +public struct ImportSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var kind: ExternKindSyntax +} + +public struct ExportSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var kind: ExternKindSyntax +} + +public enum ExternKindSyntax: Equatable, Hashable { + case interface(Identifier, [InterfaceItemSyntax]) + case path(UsePathSyntax) + case function(Identifier, FunctionSyntax) +} + +public struct InterfaceSyntax: Equatable, Hashable, CustomStringConvertible, SyntaxNodeProtocol { + public var documents: DocumentsSyntax + public var name: Identifier + public var items: [InterfaceItemSyntax] + + public var description: String { + "Interface(\(name), items: \(items))" + } +} + +public enum InterfaceItemSyntax: Equatable, Hashable, SyntaxNodeProtocol { + case typeDef(SyntaxNode) + case function(SyntaxNode) + case use(SyntaxNode) +} + +public struct TypeDefSyntax: Equatable, Hashable, SyntaxNodeProtocol { + public var documents: DocumentsSyntax + public var name: Identifier + public var body: TypeDefBodySyntax +} + +public enum TypeDefBodySyntax: Equatable, Hashable { + case flags(FlagsSyntax) + case resource(ResourceSyntax) + case record(RecordSyntax) + case variant(VariantSyntax) + case union(UnionSyntax) + case `enum`(EnumSyntax) + case alias(TypeAliasSyntax) +} + +public struct TypeAliasSyntax: Equatable, Hashable { + public let typeRepr: TypeReprSyntax +} + +public indirect enum TypeReprSyntax: Equatable, Hashable { + case bool + case u8 + case u16 + case u32 + case u64 + case s8 + case s16 + case s32 + case s64 + case float32 + case float64 + case char + case string + case name(Identifier) + case list(TypeReprSyntax) + case handle(HandleSyntax) + case tuple([TypeReprSyntax]) + case option(TypeReprSyntax) + case result(ResultSyntax) + case future(TypeReprSyntax?) + case stream(StreamSyntax) +} + +public enum HandleSyntax: Equatable, Hashable { + case own(resource: Identifier) + case borrow(resource: Identifier) + + var id: Identifier { + switch self { + case .own(let resource): return resource + case .borrow(let resource): return resource + } + } +} + +public struct ResourceSyntax: Equatable, Hashable { + var functions: [ResourceFunctionSyntax] +} + +public enum ResourceFunctionSyntax: Equatable, Hashable { + case method(SyntaxNode) + case `static`(SyntaxNode) + case constructor(SyntaxNode) +} + +public struct RecordSyntax: Equatable, Hashable { + public var fields: [FieldSyntax] +} + +public struct FieldSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var name: Identifier + public var type: TypeReprSyntax + var textRange: TextRange +} + +public struct FlagsSyntax: Equatable, Hashable { + public var flags: [FlagSyntax] +} + +public struct FlagSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var name: Identifier +} + +public struct VariantSyntax: Equatable, Hashable { + public var cases: [CaseSyntax] + var textRange: TextRange +} + +public struct CaseSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var name: Identifier + public var type: TypeReprSyntax? + var textRange: TextRange +} + +public struct EnumSyntax: Equatable, Hashable { + public var cases: [EnumCaseSyntax] + var textRange: TextRange +} + +public struct EnumCaseSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var name: Identifier + var textRange: TextRange +} + +public struct ResultSyntax: Equatable, Hashable { + public let ok: TypeReprSyntax? + public let error: TypeReprSyntax? +} + +public struct StreamSyntax: Equatable, Hashable { + var element: TypeReprSyntax? + var end: TypeReprSyntax? +} + +public struct NamedFunctionSyntax: Equatable, Hashable, SyntaxNodeProtocol { + public var documents: DocumentsSyntax + public var name: Identifier + public var function: FunctionSyntax +} + +public struct UnionSyntax: Equatable, Hashable, SyntaxNodeProtocol { + public var cases: [UnionCaseSyntax] + var textRange: TextRange +} + +public struct UnionCaseSyntax: Equatable, Hashable { + public var documents: DocumentsSyntax + public var type: TypeReprSyntax + var textRange: TextRange +} + +public struct ParameterSyntax: Equatable, Hashable { + public var name: Identifier + public var type: TypeReprSyntax + var textRange: TextRange +} +public typealias ParameterList = [ParameterSyntax] + +public enum ResultListSyntax: Equatable, Hashable { + case named(ParameterList) + case anon(TypeReprSyntax) + + public var types: [TypeReprSyntax] { + switch self { + case .anon(let type): return [type] + case .named(let named): return named.map(\.type) + } + } +} + +public struct FunctionSyntax: Equatable, Hashable { + public var parameters: ParameterList + public var results: ResultListSyntax + var textRange: TextRange +} + +public struct UseSyntax: Equatable, Hashable, SyntaxNodeProtocol { + public var from: UsePathSyntax + public var names: [UseNameSyntax] +} + +public enum UsePathSyntax: Equatable, Hashable { + case id(Identifier) + case package(id: PackageNameSyntax, name: Identifier) + + var name: Identifier { + switch self { + case .id(let identifier): return identifier + case .package(_, let name): return name + } + } +} + +public struct UseNameSyntax: Equatable, Hashable { + public var name: Identifier + public var asName: Identifier? +} + +public struct IncludeSyntax: Equatable, Hashable { + var from: UsePathSyntax + var names: [IncludeNameSyntax] +} + +public struct IncludeNameSyntax: Equatable, Hashable { + var name: Identifier + var asName: Identifier +} + +public struct Identifier: Equatable, Hashable, CustomStringConvertible { + public var text: String + var textRange: TextRange + + public var description: String { + return "\"\(text)\"" + } +} + +public struct DocumentsSyntax: Equatable, Hashable { + var comments: [String] +} diff --git a/Sources/WIT/ASTVisitor.swift b/Sources/WIT/ASTVisitor.swift new file mode 100644 index 00000000..825c0e21 --- /dev/null +++ b/Sources/WIT/ASTVisitor.swift @@ -0,0 +1,214 @@ +public protocol ASTVisitor { + mutating func visit(_ astItem: ASTItemSyntax) throws + mutating func visit(_ topLevelUse: SyntaxNode) throws + mutating func visit(_ world: SyntaxNode) throws + mutating func visit(_ worldItem: WorldItemSyntax) throws + mutating func visit(_ `import`: ImportSyntax) throws + mutating func visit(_ export: ExportSyntax) throws + mutating func visit(_ interface: SyntaxNode) throws + mutating func visit(_ interfaceItem: InterfaceItemSyntax) throws + mutating func visit(_ typeDef: SyntaxNode) throws + mutating func visit(_ alias: TypeAliasSyntax) throws + mutating func visit(_ handle: HandleSyntax) throws + mutating func visit(_ resource: ResourceSyntax) throws + mutating func visit(_ resourceFunction: ResourceFunctionSyntax) throws + mutating func visit(_ record: RecordSyntax) throws + mutating func visit(_ flags: FlagsSyntax) throws + mutating func visit(_ variant: VariantSyntax) throws + mutating func visit(_ `enum`: EnumSyntax) throws + mutating func visit(_ namedFunction: SyntaxNode) throws + mutating func visit(_ union: UnionSyntax) throws + mutating func visit(_ function: FunctionSyntax) throws + mutating func visit(_ use: SyntaxNode) throws + mutating func visit(_ include: IncludeSyntax) throws + + mutating func visitPost(_ astItem: ASTItemSyntax) throws + mutating func visitPost(_ topLevelUse: SyntaxNode) throws + mutating func visitPost(_ world: SyntaxNode) throws + mutating func visitPost(_ worldItem: WorldItemSyntax) throws + mutating func visitPost(_ `import`: ImportSyntax) throws + mutating func visitPost(_ export: ExportSyntax) throws + mutating func visitPost(_ interface: SyntaxNode) throws + mutating func visitPost(_ interfaceItem: InterfaceItemSyntax) throws + mutating func visitPost(_ typeDef: SyntaxNode) throws + mutating func visitPost(_ alias: TypeAliasSyntax) throws + mutating func visitPost(_ handle: HandleSyntax) throws + mutating func visitPost(_ resource: ResourceSyntax) throws + mutating func visitPost(_ resourceFunction: ResourceFunctionSyntax) throws + mutating func visitPost(_ record: RecordSyntax) throws + mutating func visitPost(_ flags: FlagsSyntax) throws + mutating func visitPost(_ variant: VariantSyntax) throws + mutating func visitPost(_ `enum`: EnumSyntax) throws + mutating func visitPost(_ namedFunction: SyntaxNode) throws + mutating func visitPost(_ union: UnionSyntax) throws + mutating func visitPost(_ function: FunctionSyntax) throws + mutating func visitPost(_ use: SyntaxNode) throws + mutating func visitPost(_ include: IncludeSyntax) throws +} + +extension ASTVisitor { + public mutating func walk(_ sourceFile: SourceFileSyntax) throws { + for item in sourceFile.items { + try walk(item) + } + } + public mutating func walk(_ astItem: ASTItemSyntax) throws { + try visit(astItem) + switch astItem { + case .interface(let interface): try walk(interface) + case .world(let world): try walk(world) + case .use(let topLevelUse): try walk(topLevelUse) + } + try visitPost(astItem) + } + public mutating func walk(_ topLevelUse: SyntaxNode) throws { + try visit(topLevelUse) + try visitPost(topLevelUse) + } + public mutating func walk(_ world: SyntaxNode) throws { + try visit(world) + for item in world.items { + try walk(item) + } + try visitPost(world) + } + public mutating func walk(_ worldItem: WorldItemSyntax) throws { + try visit(worldItem) + switch worldItem { + case .import(let `import`): + try walk(`import`) + case .export(let export): + try walk(export) + case .use(let use): + try walk(use) + case .type(let typeDef): + try walk(typeDef) + case .include(let include): + try walk(include) + } + try visitPost(worldItem) + } + public mutating func walk(_ importItem: ImportSyntax) throws { + try visit(importItem) + switch importItem.kind { + case .function(_, let function): + try walk(function) + case .interface(_, let items): + for item in items { + try walk(item) + } + case .path: break + } + try visitPost(importItem) + } + public mutating func walk(_ export: ExportSyntax) throws { + try visit(export) + switch export.kind { + case .function(_, let function): + try walk(function) + case .interface(_, let items): + for item in items { + try walk(item) + } + case .path: break + } + try visitPost(export) + } + public mutating func walk(_ interface: SyntaxNode) throws { + try visit(interface) + for item in interface.items { + try walk(item) + } + try visitPost(interface) + } + public mutating func walk(_ interfaceItem: InterfaceItemSyntax) throws { + try visit(interfaceItem) + switch interfaceItem { + case .typeDef(let typeDef): + try walk(typeDef) + case .function(let namedFunction): + try walk(namedFunction) + case .use(let use): + try walk(use) + } + try visitPost(interfaceItem) + } + public mutating func walk(_ typeDef: SyntaxNode) throws { + try visit(typeDef) + let body = typeDef.body + switch body { + case .flags(let flags): + try walk(flags) + case .resource(let resource): + try walk(resource) + case .record(let record): + try walk(record) + case .variant(let variant): + try walk(variant) + case .union(let union): + try walk(union) + case .enum(let `enum`): + try walk(`enum`) + case .alias(let alias): + try walk(alias) + } + try visitPost(typeDef) + } + public mutating func walk(_ alias: TypeAliasSyntax) throws { + try visit(alias) + try visitPost(alias) + } + public mutating func walk(_ handle: HandleSyntax) throws { + try visit(handle) + try visitPost(handle) + } + public mutating func walk(_ resource: ResourceSyntax) throws { + try visit(resource) + try visitPost(resource) + } + public mutating func walk(_ resourceFunction: ResourceFunctionSyntax) throws { + try visit(resourceFunction) + switch resourceFunction { + case .method(let namedFunction), .static(let namedFunction), .constructor(let namedFunction): + try walk(namedFunction) + } + try visitPost(resourceFunction) + } + public mutating func walk(_ record: RecordSyntax) throws { + try visit(record) + try visitPost(record) + } + public mutating func walk(_ flags: FlagsSyntax) throws { + try visit(flags) + try visitPost(flags) + } + public mutating func walk(_ variant: VariantSyntax) throws { + try visit(variant) + try visitPost(variant) + } + public mutating func walk(_ `enum`: EnumSyntax) throws { + try visit(`enum`) + try visitPost(`enum`) + } + public mutating func walk(_ namedFunction: SyntaxNode) throws { + try visit(namedFunction) + try walk(namedFunction.function) + try visitPost(namedFunction) + } + public mutating func walk(_ union: UnionSyntax) throws { + try visit(union) + try visitPost(union) + } + public mutating func walk(_ function: FunctionSyntax) throws { + try visit(function) + try visitPost(function) + } + public mutating func walk(_ use: SyntaxNode) throws { + try visit(use) + try visitPost(use) + } + public mutating func walk(_ include: IncludeSyntax) throws { + try visit(include) + try visitPost(include) + } +} diff --git a/Sources/WIT/CanonicalABI/CanonicalABI.swift b/Sources/WIT/CanonicalABI/CanonicalABI.swift new file mode 100644 index 00000000..056be8c0 --- /dev/null +++ b/Sources/WIT/CanonicalABI/CanonicalABI.swift @@ -0,0 +1,385 @@ +import Foundation + +public enum CanonicalABI { + public enum CoreType: Equatable { + case i32, i64, f32, f64 + } + + struct RawArgument { + let name: String + let type: CoreType + } + + /// The direction of cross-component function call + /// A cross-component function call is proxied by host runtime, so there are + /// two call directions, caller component to host and host to callee component. + public enum CallDirection { + /// A component is calling a function imported from another component. + /// Component-level values are being lowered to core-level types. + case lower + /// An exported function defined in a component is called from another component. + /// Lowered core-level values are being lifted to component-level types. + case lift + } + + public typealias LabelPath = [String] + + public struct SignatureSegment { + public var label: LabelPath + public var type: CoreType + + func prepending(label: String) -> SignatureSegment { + return SignatureSegment(label: [label] + self.label, type: type) + } + } + + public struct CoreSignature { + public var parameters: [SignatureSegment] + public var results: [SignatureSegment] + public var isIndirectResult: Bool + } + + /// Flatten the given WIT function signature into core function signature + public static func flattenSignature( + function: FunctionSyntax, + typeResolver: (TypeReprSyntax) throws -> WITType + ) throws -> CoreSignature { + let parameters = try function.parameters.enumerated().map { i, param in + let type = try typeResolver(param.type) + return (param.name.text, type) + } + let results = try { + switch function.results { + case .named(let parameterList): + return try parameterList.enumerated().map { i, result in + let type = try typeResolver(result.type) + return (result.name.text, type) + } + case .anon(let typeReprSyntax): + let type = try typeResolver(typeReprSyntax) + return [("ret", type)] + } + }() + return CanonicalABI.flatten( + parameters: parameters, + results: results, + direction: .lift + ) + } + + /// https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening + public static func flatten( + parameters: some Sequence<(label: String, type: WITType)>, + results: some Sequence<(label: String, type: WITType)>, + direction: CallDirection + ) -> CoreSignature { + var flatParameters = parameters.flatMap { (label, type) in + flatten(type: type).map { $0.prepending(label: label) } + } + let MAX_FLAT_PARAMS = 16 + if flatParameters.count > MAX_FLAT_PARAMS { + flatParameters = [.init(label: ["args"], type: .i32)] + } + + var flatResults = results.flatMap { (label, type) in + flatten(type: type).map { $0.prepending(label: label) } + } + // To be practical, Canonical ABI specifies not to use multi-value returns + var indirectResult: Bool = false + let MAX_FLAT_RESULTS = 1 + if flatResults.count > MAX_FLAT_RESULTS { + indirectResult = true + // To reduce `realloc`/`free` calls, caller side is + // responsible to allocate return value space and tell + // the address to the intermediate host runtime, then + // host runtime performs a copy from the address returned + // by the callee into caller specified return pointer. + // Note that this cross-component copy is required due to + // shared-nothing property. + switch direction { + case .lower: + flatParameters.append(.init(label: ["ret"], type: .i32)) + flatResults = [] + case .lift: + flatResults = [.init(label: ["ret"], type: .i32)] + } + } + return CoreSignature( + parameters: flatParameters, + results: flatResults, + isIndirectResult: indirectResult + ) + } + + public static func flatten(type: WITType) -> [SignatureSegment] { + switch type { + case .bool, .u8, .u16, .u32, .s8, .s16, .s32, .char: + return [.init(label: [], type: .i32)] + case .u64, .s64: return [.init(label: [], type: .i64)] + case .float32: return [.init(label: [], type: .f32)] + case .float64: return [.init(label: [], type: .f64)] + case .string: + return [.init(label: ["ptr"], type: .i32), .init(label: ["len"], type: .i32)] + case .list: + return [.init(label: ["ptr"], type: .i32), .init(label: ["len"], type: .i32)] + case .handleOwn, .handleBorrow: + return [.init(label: [], type: .i32)] + case .tuple(let types): + return types.enumerated().flatMap { i, type in + flatten(type: type).map { $0.prepending(label: i.description) } + } + case .record(let record): + return record.fields.flatMap { field in + flatten(type: field.type).map { $0.prepending(label: field.name) } + } + case .option(let type): + return flatten(variants: [type, nil]) + case .result(let ok, let error): + return flatten(variants: [ok, error]) + case .union(let union): + return flatten(variants: union.cases.map(\.type)) + case .variant(let variant): + return flatten(variants: variant.cases.map(\.type)) + case .enum(let `enum`): + return flatten(variants: `enum`.cases.map { _ in nil }) + case .future: return [.init(label: [], type: .i32)] + case .stream: return [.init(label: [], type: .i32)] + case .flags(let flags): + return Array(repeating: CoreType.i32, count: numberOfInt32(flagsCount: flags.flags.count)).enumerated().map { + SignatureSegment(label: [$0.description], type: $1) + } + case .resource: + fatalError("TODO: resource type is not supported yet") + } + } + + static func flatten(variants: [WITType?]) -> [SignatureSegment] { + let discriminant = flatten(type: discriminantType(numberOfCases: UInt32(variants.count)).asWITType) + return discriminant.map { $0.prepending(label: "disc") } + + flattenVariantPayload(variants: variants).enumerated().map { + SignatureSegment(label: [$0.description], type: $1) + } + } + + /// Flatten the given WIT variant type into core types. + public static func flattenVariantPayload(variants: [WITType?]) -> [CoreType] { + var results: [CoreType] = [] + for variantType in variants { + guard let variantType else { continue } + for (i, flatten) in flatten(type: variantType).enumerated() { + if i < results.count { + results[i] = join(results[i], flatten.type) + } else { + results.append(flatten.type) + } + } + } + return results + /// Return a minimum sized type that fits for two parameter types + func join(_ a: CoreType, _ b: CoreType) -> CoreType { + switch (a, b) { + case (.i32, .f32), (.f32, .i32), + (.f32, .f32), (.i32, .i32): + return .i32 + case (_, .i64), (.i64, _), + (_, .f64), (.f64, _): + return .i64 + } + } + } + + /// A type that represents the discriminant of a variant/enum type. + public enum DiscriminantType { + case u8, u16, u32 + + public var asWITType: WITType { + switch self { + case .u8: return .u8 + case .u16: return .u16 + case .u32: return .u32 + } + } + } + + /// Return the smallest integer type that can represent the given number of cases. + public static func discriminantType(numberOfCases: UInt32) -> DiscriminantType { + switch Int(ceil(log2(Double(numberOfCases)) / 8)) { + case 0: return .u8 + case 1: return .u8 + case 2: return .u16 + case 3: return .u32 + default: fatalError("`ceil(log2(UInt32)) / 8` cannot be greater than 3") + } + } + + static func numberOfInt32(flagsCount: Int) -> Int { + return Int(ceil(Double(flagsCount) / 32)) + } + + static func alignUp(_ offset: Int, to align: Int) -> Int { + let mask = align &- 1 + return (offset &+ mask) & ~mask + } + + static func payloadOffset(cases: [WITType?]) -> Int { + let discriminantType = Self.discriminantType(numberOfCases: UInt32(cases.count)) + let discriminantSize = Self.size(type: discriminantType.asWITType) + let payloadAlign = maxCaseAlignment(cases: cases) + return alignUp(discriminantSize, to: payloadAlign) + } + + public static func fieldOffsets(fields: [WITType]) -> [(WITType, Int)] { + var current = 0 + return fields.map { field in + let aligned = alignUp(current, to: alignment(type: field)) + current = aligned + size(type: field) + return (field, aligned) + } + } + + public static func size(type: WITType) -> Int { + switch type { + case .bool, .u8, .s8: return 1 + case .u16, .s16: return 2 + case .u32, .s32: return 4 + case .u64, .s64: return 8 + case .float32: return 4 + case .float64: return 8 + case .char: return 4 + case .string: return 8 + case .list: return 8 + case .handleOwn, .handleBorrow: + return 4 + case .tuple(let types): + return size(fields: types) + case .option(let type): + return size(cases: [type, nil]) + case .result(let ok, let error): + return size(cases: [ok, error]) + case .future: + return 4 + case .stream: + return 4 + case .record(let record): + return size(fields: record.fields.map(\.type)) + case .flags(let flags): + switch rawType(ofFlags: flags.flags.count) { + case .u8: return 1 + case .u16: return 2 + case .u32: return 4 + } + case .enum(let enumType): + return size(cases: enumType.cases.map { _ in nil }) + case .variant(let variant): + return size(cases: variant.cases.map(\.type)) + case .resource: + fatalError("TODO: resource types are not supported yet") + case .union: + fatalError("FIXME: union types has been removed from the Component Model spec") + } + } + + static func size(fields: [WITType]) -> Int { + var size = 0 + for field in fields { + let fieldSize = Self.size(type: field) + let fieldAlign = alignment(type: field) + size = alignUp(size, to: fieldAlign) + fieldSize + } + return alignUp(size, to: alignment(fields: fields)) + } + + static func size(cases: [WITType?]) -> Int { + var maxSize = 0 + for case .some(let caseType) in cases { + maxSize = max(maxSize, size(type: caseType)) + } + return alignUp(payloadOffset(cases: cases) + maxSize, to: alignment(cases: cases)) + } + + public static func alignment(type: WITType) -> Int { + switch type { + case .bool, .u8, .s8: return 1 + case .u16, .s16: return 2 + case .u32, .s32: return 4 + case .u64, .s64: return 8 + case .float32: return 4 + case .float64: return 8 + case .char: return 4 + case .string: return 4 + case .list: return 4 + case .handleOwn, .handleBorrow: + return 4 + case .tuple(let types): + return alignment(fields: types) + case .option(let type): + return alignment(cases: [type, nil]) + case .result(let ok, let error): + return alignment(cases: [ok, error]) + case .future: + return 4 + case .stream: + return 4 + case .record(let record): + return alignment(fields: record.fields.map(\.type)) + case .flags(let flags): + switch rawType(ofFlags: flags.flags.count) { + case .u8: return 1 + case .u16: return 2 + case .u32: return 4 + } + case .enum(let enumType): + return alignment(cases: enumType.cases.map { _ in nil }) + case .variant(let variant): + return alignment(cases: variant.cases.map(\.type)) + case .resource: + fatalError("TODO: resource type is not supported yet") + case .union: + fatalError("FIXME: union type is already removed from the spec") + } + } + + static func alignment(cases: [WITType?]) -> Int { + max( + alignment(type: discriminantType(numberOfCases: UInt32(cases.count)).asWITType), + maxCaseAlignment(cases: cases) + ) + } + + static func alignment(fields: [WITType]) -> Int { + fields.map(Self.alignment(type:)).max() ?? 1 + } + + static func maxCaseAlignment(cases: [WITType?]) -> Int { + var alignment = 1 + for caseType in cases { + guard let caseType else { continue } + alignment = max(alignment, Self.alignment(type: caseType)) + } + return alignment + } + + public enum FlagsRawRepresentation { + case u8, u16 + case u32(Int) + + public var numberOfInt32: Int { + switch self { + case .u8, .u16: return 1 + case .u32(let v): return v + } + } + } + + public static func rawType(ofFlags: Int) -> FlagsRawRepresentation { + if ofFlags == 0 { + return .u32(0) + } else if ofFlags <= 8 { + return .u8 + } else if ofFlags <= 16 { + return .u16 + } else { + return .u32(CanonicalABI.numberOfInt32(flagsCount: ofFlags)) + } + } +} diff --git a/Sources/WIT/CanonicalABI/CanonicalDeallocation.swift b/Sources/WIT/CanonicalABI/CanonicalDeallocation.swift new file mode 100644 index 00000000..8b664cd7 --- /dev/null +++ b/Sources/WIT/CanonicalABI/CanonicalDeallocation.swift @@ -0,0 +1,117 @@ +/// A type that provides a deallocation operation for WIT values. +public protocol CanonicalDeallocation { + /// A type of a core value and a type of a WIT value. + associatedtype Operand + + /// A type of a pointer representation. + associatedtype Pointer + + /// Deallocates a WIT string value. + /// + /// - Parameters: + /// - pointer: A pointer that contains the byte representation of the string. + /// - length: A number of bytes in the string. + func deallocateString(pointer: Operand, length: Operand) + + /// Deallocates a WIT list value. + /// + /// - Parameters: + /// - pointer: A pointer that contains the byte representation of the list elements. + /// - length: A number of elements in the list. + /// - element: A type of the list elements. + /// - deallocateElement: A closure that deallocates an element from the given pointer. + func deallocateList( + pointer: Operand, length: Operand, element: WITType, + deallocateElement: (Pointer) throws -> Void + ) throws + + /// Deallocates a WIT variant value. + /// + /// - Parameters: + /// - discriminant: An lifted integer value that represents a discriminant of the variant. + /// - cases: A list of types of the variant cases. + /// - deallocatePayload: A closure that deallocates a payload of the variant case at the given index. + func deallocateVariantLike( + discriminant: Operand, cases: [WITType?], + deallocatePayload: (Int) throws -> Void + ) throws +} + +extension CanonicalABI { + /// Performs a deallocation operation for the given WIT value at the given pointer. + /// It recursively deallocates all the nested values. + public static func deallocate( + type: WITType, + pointer: Loading.Pointer, + deallocation: Deallocation, + loading: Loading + ) throws -> Bool where Deallocation.Operand == Loading.Operand, Deallocation.Pointer == Loading.Pointer { + func deallocateRecordLike(fields: [WITType]) throws -> Bool { + var needsDeallocation = false + for (fieldType, offset) in fieldOffsets(fields: fields) { + let result = try deallocate( + type: fieldType, + pointer: pointer.advanced(by: Deallocation.Pointer.Stride(exactly: offset)!), + deallocation: deallocation, loading: loading + ) + needsDeallocation = needsDeallocation || result + } + return needsDeallocation + } + + func deallocateVariantLike(cases: [WITType?]) throws -> Bool { + let discriminant = loadVariantDiscriminant(pointer: pointer, numberOfCases: cases.count, loading: loading) + let payloadOffset = CanonicalABI.payloadOffset(cases: cases) + let payloadPtr = pointer.advanced(by: .init(exactly: payloadOffset)!) + var needsDeallocation = false + try deallocation.deallocateVariantLike( + discriminant: discriminant, cases: cases, + deallocatePayload: { caseIndex in + guard let variantCase = cases[caseIndex] else { return } + let result = try deallocate( + type: variantCase, pointer: payloadPtr, + deallocation: deallocation, loading: loading + ) + needsDeallocation = needsDeallocation || result + } + ) + return needsDeallocation + } + + switch type { + case .bool, .u8, .u16, .u32, .u64, + .s8, .s16, .s32, .s64, + .float32, .float64, .char: + return false + case .string: + let (buffer, length) = loadList(loading: loading, pointer: pointer) + deallocation.deallocateString(pointer: buffer, length: length) + return true + case .list(let element): + let (buffer, length) = loadList(loading: loading, pointer: pointer) + try deallocation.deallocateList( + pointer: buffer, length: length, element: element, + deallocateElement: { pointer in + _ = try deallocate(type: element, pointer: pointer, deallocation: deallocation, loading: loading) + } + ) + return true + case .handleOwn, .handleBorrow: + fatalError("TODO: resource type is not supported yet") + case .tuple(let types): + return try deallocateRecordLike(fields: types) + case .record(let record): + return try deallocateRecordLike(fields: record.fields.map(\.type)) + case .option(let wrapped): + return try deallocateVariantLike(cases: [nil, wrapped]) + case .result(let ok, let error): + return try deallocateVariantLike(cases: [ok, error]) + case .variant(let variant): + return try deallocateVariantLike(cases: variant.cases.map(\.type)) + case .flags: return false + case .enum: return false + default: + fatalError("TODO: deallocation for \"\(type)\" is unimplemented") + } + } +} diff --git a/Sources/WIT/CanonicalABI/CanonicalLifting.swift b/Sources/WIT/CanonicalABI/CanonicalLifting.swift new file mode 100644 index 00000000..57af11ea --- /dev/null +++ b/Sources/WIT/CanonicalABI/CanonicalLifting.swift @@ -0,0 +1,192 @@ +/// A type that provides lifting of a core value to WIT values. +public protocol CanonicalLifting { + /// A type of a core value and a type of a WIT value. + associatedtype Operand + + /// A type of a pointer representation. + associatedtype Pointer: Strideable + + /// Lifts a core i32 value to a WIT bool value. + func liftBool(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT u8 value. + func liftUInt8(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT u16 value. + func liftUInt16(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT u32 value. + func liftUInt32(_ value: Operand) -> Operand + /// Lifts a core i64 value to a WIT u64 value. + func liftUInt64(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT s8 value. + func liftInt8(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT s16 value. + func liftInt16(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT s32 value. + func liftInt32(_ value: Operand) -> Operand + /// Lifts a core i64 value to a WIT s64 value. + func liftInt64(_ value: Operand) -> Operand + /// Lifts a core f32 value to a WIT f32 value. + func liftFloat32(_ value: Operand) -> Operand + /// Lifts a core f64 value to a WIT f64 value. + func liftFloat64(_ value: Operand) -> Operand + /// Lifts a core i32 value to a WIT char value. + func liftChar(_ value: Operand) -> Operand + /// Lifts a pair of a pointer and a length, both of which are core i32 values, to a WIT string value. + func liftString(pointer: Operand, length: Operand, encoding: String) throws -> Operand + /// Lifts a pair of a pointer and a length, both of which are core i32 values, to a WIT list value. + /// + /// - Parameters: + /// - pointer: A pointer that contains the byte representation of the list elements. + /// - length: A number of elements in the list. + /// - element: A type of the list elements. + /// - loadElement: A closure that loads an element from the given pointer. + func liftList( + pointer: Operand, length: Operand, element: WITType, + loadElement: (Pointer) throws -> Operand + ) throws -> Operand + /// Lifts lifted WIT values of the fields of a record to a WIT record value. + func liftRecord(fields: [Operand], type: WITRecord) throws -> Operand + /// Lifts lifted WIT values of the tuple elements to a WIT tuple value + func liftTuple(elements: [Operand], types: [WITType]) throws -> Operand + /// Lifts a core i32 value to a WIT enum value. + func liftEnum(_ value: Operand, type: WITEnum) throws -> Operand + /// Lifts core integer values to a WIT flag value. + func liftFlags(_ value: [Operand], type: WITFlags) throws -> Operand + + /// Lifts a pair of a discriminant and payload core values to a WIT option value. + /// + /// - Parameters: + /// - discriminant: A core i32 value that represents a discriminant. + /// - liftPayload: A closure that lifts a payload core value to a WIT value. + func liftOption( + discriminant: Operand, wrapped: WITType, liftPayload: () throws -> Operand + ) throws -> Operand + + /// Lifts a pair of a discriminant and payload core values to a WIT result value. + /// + /// - Parameters: + /// - discriminant: A core i32 value that represents a discriminant. + /// - liftPayload: A closure that lifts a payload core value to a WIT value. + /// It takes a boolean value that indicates whether the payload is an error or not. + func liftResult( + discriminant: Operand, ok: WITType?, error: WITType?, liftPayload: (_ isError: Bool) throws -> Operand? + ) throws -> Operand + + /// Lifts a pair of a discriminant and payload core values to a WIT variant value. + /// + /// - Parameters: + /// - discriminant: A core i32 value that represents a discriminant. + /// - liftPayload: A closure that lifts a payload core value to a WIT value. + /// It takes a case index of the variant to be lifted. + func liftVariant( + discriminant: Operand, type: WITVariant, liftPayload: (Int) throws -> Operand? + ) throws -> Operand +} + +extension CanonicalABI { + /// Performs ["Flat Lifting"][cabi_flat_lifting] defined in the Canonical ABI. + /// It recursively lifts a core value to a WIT value. + /// + /// [cabi_flat_lifting]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flat-lifting + public static func lift( + type: WITType, + coreValues: inout some IteratorProtocol, + lifting: inout Lifting, + loading: inout Loading + ) throws -> Lifting.Operand where Lifting.Operand == Loading.Operand, Lifting.Pointer == Loading.Pointer { + switch type { + case .bool: return lifting.liftBool(coreValues.next()!) + case .u8: return lifting.liftUInt8(coreValues.next()!) + case .u16: return lifting.liftUInt16(coreValues.next()!) + case .u32: return lifting.liftUInt32(coreValues.next()!) + case .u64: return lifting.liftUInt64(coreValues.next()!) + case .s8: return lifting.liftInt8(coreValues.next()!) + case .s16: return lifting.liftInt16(coreValues.next()!) + case .s32: return lifting.liftInt32(coreValues.next()!) + case .s64: return lifting.liftInt64(coreValues.next()!) + case .float32: return lifting.liftFloat32(coreValues.next()!) + case .float64: return lifting.liftFloat64(coreValues.next()!) + case .char: return lifting.liftChar(coreValues.next()!) + case .string: + return try lifting.liftString( + pointer: coreValues.next()!, + length: coreValues.next()!, + encoding: "utf8" + ) + case .list(let element): + return try liftList( + pointer: coreValues.next()!, length: coreValues.next()!, + element: element, lifting: &lifting, loading: &loading + ) + case .handleOwn, .handleBorrow: + fatalError("TODO: resource type is not supported yet") + case .record(let record): + let fields = try record.fields.map { field in + try lift(type: field.type, coreValues: &coreValues, lifting: &lifting, loading: &loading) + } + return try lifting.liftRecord(fields: fields, type: record) + case .tuple(let types): + let elements = try types.map { type in + try lift(type: type, coreValues: &coreValues, lifting: &lifting, loading: &loading) + } + return try lifting.liftTuple(elements: elements, types: types) + case .enum(let enumType): + return try lifting.liftEnum(coreValues.next()!, type: enumType) + case .flags(let flags): + let numberOfI32 = CanonicalABI.numberOfInt32(flagsCount: flags.flags.count) + let rawValues = (0..( + pointer: Lifting.Operand, length: Lifting.Operand, + element: WITType, + lifting: inout Lifting, loading: inout Loading + ) throws -> Lifting.Operand where Lifting.Operand == Loading.Operand, Lifting.Pointer == Loading.Pointer { + try lifting.liftList( + pointer: pointer, length: length, element: element, + loadElement: { elementPtr in + return try CanonicalABI.load( + loading: &loading, lifting: &lifting, + type: element, pointer: elementPtr + ) + } + ) + } +} diff --git a/Sources/WIT/CanonicalABI/CanonicalLoading.swift b/Sources/WIT/CanonicalABI/CanonicalLoading.swift new file mode 100644 index 00000000..3cc61bff --- /dev/null +++ b/Sources/WIT/CanonicalABI/CanonicalLoading.swift @@ -0,0 +1,144 @@ +public protocol CanonicalLoading { + associatedtype Operand + associatedtype Pointer: Strideable + + func loadUInt8(at pointer: Pointer) -> Operand + func loadUInt16(at pointer: Pointer) -> Operand + func loadUInt32(at pointer: Pointer) -> Operand + func loadUInt64(at pointer: Pointer) -> Operand + func loadInt8(at pointer: Pointer) -> Operand + func loadInt16(at pointer: Pointer) -> Operand + func loadInt32(at pointer: Pointer) -> Operand + func loadInt64(at pointer: Pointer) -> Operand + func loadFloat32(at pointer: Pointer) -> Operand + func loadFloat64(at pointer: Pointer) -> Operand +} + +extension CanonicalABI { + public static func load( + loading: inout Loading, + lifting: inout Lifting, + type: WITType, + pointer: Loading.Pointer + ) throws -> Loading.Operand where Loading.Operand == Lifting.Operand, Lifting.Pointer == Loading.Pointer { + func loadRecordLike(types: [WITType]) throws -> [Loading.Operand] { + var fieldValues: [Loading.Operand] = [] + for field in fieldOffsets(fields: types) { + let (fieldType, offset) = field + let loaded = try load( + loading: &loading, lifting: &lifting, + type: fieldType, pointer: pointer.advanced(by: Loading.Pointer.Stride(exactly: offset)!) + ) + fieldValues.append(loaded) + } + return fieldValues + } + switch type { + case .bool: return lifting.liftBool(loading.loadUInt8(at: pointer)) + case .u8: return loading.loadUInt8(at: pointer) + case .u16: return loading.loadUInt16(at: pointer) + case .u32: return loading.loadUInt32(at: pointer) + case .u64: return loading.loadUInt64(at: pointer) + case .s8: return loading.loadInt8(at: pointer) + case .s16: return loading.loadInt16(at: pointer) + case .s32: return loading.loadInt32(at: pointer) + case .s64: return loading.loadInt64(at: pointer) + case .float32: return loading.loadFloat32(at: pointer) + case .float64: return loading.loadFloat64(at: pointer) + case .char: return lifting.liftChar(loading.loadUInt32(at: pointer)) + case .enum(let enumType): + let discriminant = loadVariantDiscriminant( + pointer: pointer, numberOfCases: enumType.cases.count, loading: loading + ) + return try lifting.liftEnum(discriminant, type: enumType) + case .flags(let flags): + let rawValueType = CanonicalABI.rawType(ofFlags: flags.flags.count) + let rawValues: [Loading.Operand] + switch rawValueType { + case .u8: rawValues = [loading.loadUInt8(at: pointer)] + case .u16: rawValues = [loading.loadUInt16(at: pointer)] + case .u32(let numberOfU32): + rawValues = (0..( + loading: Loading, pointer: Loading.Pointer + ) -> (buffer: Loading.Operand, length: Loading.Operand) { + let buffer = loading.loadUInt32(at: pointer) + let length = loading.loadUInt32(at: pointer.advanced(by: 4)) + return (buffer, length) + } + + static func loadVariantDiscriminant( + pointer: Loading.Pointer, numberOfCases: Int, loading: Loading + ) -> Loading.Operand { + let discriminantType = CanonicalABI.discriminantType(numberOfCases: UInt32(numberOfCases)) + let discriminant: Loading.Operand + switch discriminantType { + case .u8: discriminant = loading.loadUInt8(at: pointer) + case .u16: discriminant = loading.loadUInt16(at: pointer) + case .u32: discriminant = loading.loadUInt32(at: pointer) + } + return discriminant + } +} diff --git a/Sources/WIT/CanonicalABI/CanonicalLowering.swift b/Sources/WIT/CanonicalABI/CanonicalLowering.swift new file mode 100644 index 00000000..93dd80f5 --- /dev/null +++ b/Sources/WIT/CanonicalABI/CanonicalLowering.swift @@ -0,0 +1,258 @@ +/// A type that provides lowering of a WIT value to core values. +public protocol CanonicalLowering { + /// A type of a lowered core value and a type of a WIT value. + associatedtype Operand + + associatedtype Pointer + + /// Lowers a WIT bool value to a core i32 value. + func lowerBool(_ value: Operand) -> Operand + /// Lowers a WIT u8 value to a core i32 value. + func lowerUInt8(_ value: Operand) -> Operand + /// Lowers a WIT u16 value to a core i32 value. + func lowerUInt16(_ value: Operand) -> Operand + /// Lowers a WIT u32 value to a core i32 value. + func lowerUInt32(_ value: Operand) -> Operand + /// Lowers a WIT u64 value to a core i64 value. + func lowerUInt64(_ value: Operand) -> Operand + /// Lowers a WIT s8 value to a core i32 value. + func lowerInt8(_ value: Operand) -> Operand + /// Lowers a WIT s16 value to a core i32 value. + func lowerInt16(_ value: Operand) -> Operand + /// Lowers a WIT s32 value to a core i32 value. + func lowerInt32(_ value: Operand) -> Operand + /// Lowers a WIT s64 value to a core i64 value. + func lowerInt64(_ value: Operand) -> Operand + /// Lowers a WIT f32 value to a core f32 value. + func lowerFloat32(_ value: Operand) -> Operand + /// Lowers a WIT f64 value to a core f64 value. + func lowerFloat64(_ value: Operand) -> Operand + /// Lowers a WIT char value to a core i32 value. + func lowerChar(_ value: Operand) -> Operand + /// Lowers a WIT enum value to a core i32 value. + func lowerEnum(_ value: Operand, type: WITEnum) throws -> Operand + /// Lowers a WIT flags value to core integer values + func lowerFlags(_ value: Operand, type: WITFlags) throws -> [Operand] + /// Lowers a WIT string value to a pair of a pointer and a length, both of which are core i32 values. + func lowerString(_ value: Operand, encoding: String) throws -> (pointer: Operand, length: Operand) + /// Lowers a WIT list value to a pair of a pointer and a length, both of which are core i32 values. + func lowerList( + _ value: Operand, element: WITType, + storeElement: (Pointer, Operand) throws -> Void + ) throws -> (pointer: Operand, length: Operand) + + /// Lowers an option value to a pair of a discriminant and payload values. + /// The implementation of this method should call `lowerPayload` to lower the payload value. + /// + /// - Parameters: + /// - value: The value to be lowered. + /// - wrapped: The wrapped type of the option. + /// - lowerPayload: A closure that lowers the payload value. + /// - Returns: A pair of a discriminant and payload values. + /// The discriminant value should be a boolean value. + /// The payload value should be a lowered core values of the wrapped type. + func lowerOption( + _ value: Operand, wrapped: WITType, + lowerPayload: (Operand) throws -> [Operand] + ) throws -> (discriminant: Operand, payload: [Operand]) + + /// Lowers a result value to a pair of a discriminant and payload values. + /// The implementation of this method should call `lowerPayload` to lower the payload value. + /// + /// - Parameters: + /// - value: The value to be lowered. + /// - ok: The type of the `ok` case. + /// - error: The type of the `error` case. + /// - lowerPayload: A closure that lowers the payload value. It takes a boolean value that + /// indicates whether the payload is an error or not. + /// - Returns: A pair of a discriminant and payload values. + /// The discriminant value should be a core i32 value. + func lowerResult( + _ value: Operand, ok: WITType?, error: WITType?, + lowerPayload: (Bool, Operand) throws -> [Operand] + ) throws -> (discriminant: Operand, payload: [Operand]) + + /// Lowers a record value to a list of WIT values. + func lowerRecord(_ value: Operand, type: WITRecord) -> [Operand] + /// Lowers a tuple value to a list of WIT values. + func lowerTuple(_ value: Operand, types: [WITType]) -> [Operand] + + /// Lowers a variant value to a pair of a discriminant and payload values. + /// The implementation of this method should call `lowerPayload` to lower the payload value. + /// + /// - Parameters: + /// - value: The value to be lowered. + /// - type: The type of the variant. + /// - lowerPayload: A closure that lowers the payload value as the payload type of the given case index. + /// The closure should return a list of lowered core values of the payload type. + /// The returned list should have the same types of elements as the flattened payload + /// types in the variant. If the case doesn't have payload, the closure should return + /// zero values of the flattened payload types in the variant. + /// - Returns: A pair of a discriminant and payload values. Both values should be lowered core values. + func lowerVariant( + _ value: Operand, type: WITVariant, + lowerPayload: (Int, Operand) throws -> [Operand] + ) throws -> (discriminant: Operand, payload: [Operand]) + + /// Makes a zero value of the given core type. + func makeZeroValue(of type: CanonicalABI.CoreType) -> Operand + + /// Casts the given value from the source core type to the destination core type. + /// The `source` type should have smaller or equal size than the `destination` type. + func numericCast( + _ value: Operand, from source: CanonicalABI.CoreType, to destination: CanonicalABI.CoreType + ) -> Operand +} + +extension CanonicalABI { + /// Performs ["Flat Lowering"][cabi_flat_lowering] defined in the Canonical ABI. + /// It recursively lowers a WIT value to a list of core values. + /// + /// [cabi_flat_lowering]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flat-lowering + public static func lower( + type: WITType, + value: Lowering.Operand, + lowering: inout Lowering, storing: inout Storing + ) throws -> [Lowering.Operand] where Lowering.Operand == Storing.Operand, Lowering.Pointer == Storing.Pointer { + switch type { + case .bool: return [lowering.lowerBool(value)] + case .u8: return [lowering.lowerUInt8(value)] + case .u16: return [lowering.lowerUInt16(value)] + case .u32: return [lowering.lowerUInt32(value)] + case .u64: return [lowering.lowerUInt64(value)] + case .s8: return [lowering.lowerInt8(value)] + case .s16: return [lowering.lowerInt16(value)] + case .s32: return [lowering.lowerInt32(value)] + case .s64: return [lowering.lowerInt64(value)] + case .float32: return [lowering.lowerFloat32(value)] + case .float64: return [lowering.lowerFloat64(value)] + case .char: return [lowering.lowerChar(value)] + case .enum(let enumType): + return [try lowering.lowerEnum(value, type: enumType)] + case .flags(let flags): + return try lowering.lowerFlags(value, type: flags) + case .string: + let (pointer, length) = try lowering.lowerString(value, encoding: "utf8") + return [pointer, length] + case .list(let element): + let (pointer, length) = try lowerList(value, element: element, storing: &storing, lowering: &lowering) + return [pointer, length] + case .record(let record): + let fieldValues = lowering.lowerRecord(value, type: record) + return try zip(fieldValues, record.fields).flatMap { value, field in + try lower(type: field.type, value: value, lowering: &lowering, storing: &storing) + } + case .tuple(let types): + let fieldValues = lowering.lowerTuple(value, types: types) + return try zip(fieldValues, types).flatMap { value, type in + try lower(type: type, value: value, lowering: &lowering, storing: &storing) + } + case .option(let wrapped): + return try lowerVariant( + variants: [nil, wrapped], + value: value, lowering: &lowering, storing: &storing, + lowerVariant: { lowering, storing, lowerPayload in + let (discriminant, payload) = try lowering.lowerOption( + value, wrapped: wrapped, + lowerPayload: { payload in + try lowerPayload(&lowering, &storing, 1, payload) + } + ) + return [discriminant] + payload + } + ) + case .result(let ok, let error): + return try lowerVariant( + variants: [ok, error], + value: value, lowering: &lowering, storing: &storing, + lowerVariant: { lowering, storing, lowerPayload in + let (discriminant, payload) = try lowering.lowerResult( + value, ok: ok, error: error, + lowerPayload: { isError, payload in + try lowerPayload(&lowering, &storing, isError ? 1 : 0, payload) + } + ) + return [discriminant] + payload + } + ) + case .variant(let variant): + return try lowerVariant( + variants: variant.cases.map(\.type), + value: value, lowering: &lowering, storing: &storing, + lowerVariant: { lowering, storing, lowerPayload in + let (discriminant, payload) = try lowering.lowerVariant( + value, + type: variant, + lowerPayload: { + try lowerPayload(&lowering, &storing, $0, $1) + } + ) + return [discriminant] + payload + } + ) + default: + fatalError("TODO: lifting \"\(type)\" is unimplemented") + } + } + + typealias LowerVariant = ( + _ lowering: inout Lowering, _ storing: inout Storing, + _ lowerPayloed: LowerVariantPayload + ) throws -> [Lowering.Operand] + + typealias LowerVariantPayload = ( + inout Lowering, inout Storing, Int, Lowering.Operand + ) throws -> [Lowering.Operand] + + static func lowerVariant( + variants: [WITType?], value: Lowering.Operand, + lowering: inout Lowering, storing: inout Storing, + lowerVariant: LowerVariant + ) throws -> [Lowering.Operand] where Lowering.Operand == Storing.Operand, Lowering.Pointer == Storing.Pointer { + let unionPayloadCoreTypes = CanonicalABI.flattenVariantPayload(variants: variants) + let lowerPayload: LowerVariantPayload = { lowering, storing, caseIndex, payload in + guard let payloadType = variants[caseIndex] else { + // If the case doesn't have payload, return zeros + return unionPayloadCoreTypes.map { lowering.makeZeroValue(of: $0) } + } + let singlePayloadCoreTypes = CanonicalABI.flatten(type: payloadType) + let lowered = try lower(type: payloadType, value: payload, lowering: &lowering, storing: &storing) + + var results: [Lowering.Operand] = [] + for (i, unionPayloadCoreType) in unionPayloadCoreTypes.enumerated() { + guard i < singlePayloadCoreTypes.count else { + // Extend the payload core values with zeros + results.append(lowering.makeZeroValue(of: unionPayloadCoreType)) + continue + } + // Unioned payload core type is always larger than or equal to the specific payload core type. + // See ``CanonicalABI/flattenVariantPayload`` for details. + let castedPayloadPiece = lowering.numericCast( + lowered[i], + from: singlePayloadCoreTypes[i].type, + to: unionPayloadCoreType + ) + results.append(castedPayloadPiece) + } + return results + } + return try lowerVariant(&lowering, &storing, lowerPayload) + } + + static func lowerList( + _ value: Lowering.Operand, element: WITType, + storing: inout Storing, lowering: inout Lowering + ) throws -> ( + pointer: Lowering.Operand, length: Lowering.Operand + ) where Lowering.Operand == Storing.Operand, Lowering.Pointer == Storing.Pointer { + return try lowering.lowerList( + value, element: element, + storeElement: { pointer, value in + try CanonicalABI.store( + type: element, value: value, pointer: pointer, + storing: &storing, lowering: &lowering + ) + } + ) + } +} diff --git a/Sources/WIT/CanonicalABI/CanonicalStoring.swift b/Sources/WIT/CanonicalABI/CanonicalStoring.swift new file mode 100644 index 00000000..a68e9210 --- /dev/null +++ b/Sources/WIT/CanonicalABI/CanonicalStoring.swift @@ -0,0 +1,146 @@ +public protocol CanonicalStoring { + associatedtype Operand + associatedtype Pointer: Strideable + + func storeUInt8(at pointer: Pointer, _ value: Operand) + func storeUInt16(at pointer: Pointer, _ value: Operand) + func storeUInt32(at pointer: Pointer, _ value: Operand) + func storeUInt64(at pointer: Pointer, _ value: Operand) + func storeInt8(at pointer: Pointer, _ value: Operand) + func storeInt16(at pointer: Pointer, _ value: Operand) + func storeInt32(at pointer: Pointer, _ value: Operand) + func storeInt64(at pointer: Pointer, _ value: Operand) + func storeFloat32(at pointer: Pointer, _ value: Operand) + func storeFloat64(at pointer: Pointer, _ value: Operand) + func storeFlags(at pointer: Pointer, _ value: Operand, type: WITFlags) throws + func storeOption( + at pointer: Pointer, _ value: Operand, + storeDiscriminant: (Operand) throws -> Void, + storePayload: (Operand) throws -> Void + ) throws + func storeResult( + at pointer: Pointer, _ value: Operand, + ok: WITType?, error: WITType?, + storeDiscriminant: (Operand) throws -> Void, + storePayload: (Bool, Operand) throws -> Void + ) throws + func storeVariant( + at pointer: Pointer, _ value: Operand, type: WITVariant, + storeDiscriminant: (Operand) throws -> Void, + storePayload: (Int, Operand) throws -> Void + ) throws +} + +extension CanonicalABI { + public static func store( + type: WITType, + value: Storing.Operand, + pointer: Storing.Pointer, + storing: inout Storing, + lowering: inout Lowering + ) throws where Storing.Operand == Lowering.Operand, Storing.Pointer == Lowering.Pointer { + func storeList(buffer: Storing.Operand, length: Storing.Operand) { + storing.storeUInt32(at: pointer, buffer) + storing.storeUInt32(at: pointer.advanced(by: 4), length) + } + func storeRecord(values: [Storing.Operand], types: [WITType]) throws { + for (value, field) in zip(values, fieldOffsets(fields: types)) { + let (fieldType, offset) = field + try store( + type: fieldType, value: value, + pointer: pointer.advanced(by: Storing.Pointer.Stride(exactly: offset)!), + storing: &storing, lowering: &lowering + ) + } + } + + switch type { + case .bool: + storing.storeUInt8(at: pointer, lowering.lowerBool(value)) + case .u8: + storing.storeUInt8(at: pointer, value) + case .u16: storing.storeUInt16(at: pointer, value) + case .u32: storing.storeUInt32(at: pointer, value) + case .u64: storing.storeUInt64(at: pointer, value) + case .s8: storing.storeInt8(at: pointer, value) + case .s16: storing.storeInt16(at: pointer, value) + case .s32: storing.storeInt32(at: pointer, value) + case .s64: storing.storeInt64(at: pointer, value) + case .float32: storing.storeFloat32(at: pointer, value) + case .float64: storing.storeFloat64(at: pointer, value) + case .char: storing.storeUInt32(at: pointer, lowering.lowerChar(value)) + case .enum(let enumType): + storing.storeUInt32(at: pointer, try lowering.lowerEnum(value, type: enumType)) + case .flags(let flags): + try storing.storeFlags(at: pointer, value, type: flags) + case .string: + let (buffer, length) = try lowering.lowerString(value, encoding: "utf8") + storeList(buffer: buffer, length: length) + case .option(let wrapped): + try storing.storeOption( + at: pointer, value, + storeDiscriminant: { discriminant in + try store( + type: .u8, value: discriminant, pointer: pointer, + storing: &storing, lowering: &lowering + ) + }, + storePayload: { payload in + let offset = Storing.Pointer.Stride(exactly: payloadOffset(cases: [wrapped, nil]))! + try store( + type: wrapped, value: payload, pointer: pointer.advanced(by: offset), + storing: &storing, lowering: &lowering + ) + } + ) + case .result(let ok, let error): + try storing.storeResult( + at: pointer, value, ok: ok, error: error, + storeDiscriminant: { discriminant in + try store( + type: .u8, value: discriminant, pointer: pointer, + storing: &storing, lowering: &lowering + ) + }, + storePayload: { isError, payload in + let offset = Storing.Pointer.Stride(exactly: payloadOffset(cases: [ok, error]))! + guard let type = isError ? error : ok else { return } + try store( + type: type, value: payload, pointer: pointer.advanced(by: offset), + storing: &storing, lowering: &lowering + ) + } + ) + case .list(let element): + let (buffer, length) = try lowerList(value, element: element, storing: &storing, lowering: &lowering) + storeList(buffer: buffer, length: length) + case .tuple(let types): + let values = lowering.lowerTuple(value, types: types) + try storeRecord(values: values, types: types) + case .record(let record): + let types = record.fields.map(\.type) + let values = lowering.lowerRecord(value, type: record) + try storeRecord(values: values, types: types) + case .variant(let variant): + let discriminantType = CanonicalABI.discriminantType(numberOfCases: UInt32(variant.cases.count)) + try storing.storeVariant( + at: pointer, value, type: variant, + storeDiscriminant: { discriminant in + try store( + type: discriminantType.asWITType, value: discriminant, + pointer: pointer, storing: &storing, lowering: &lowering + ) + }, + storePayload: { i, payload in + guard let payloadType = variant.cases[i].type else { return } + let offset = Storing.Pointer.Stride(exactly: payloadOffset(cases: variant.cases.map(\.type)))! + try store( + type: payloadType, value: payload, pointer: pointer.advanced(by: offset), + storing: &storing, lowering: &lowering + ) + }) + default: + fatalError("TODO: storing \"\(type)\" is unimplemented") + } + } +} diff --git a/Sources/WIT/Diagnostics.swift b/Sources/WIT/Diagnostics.swift new file mode 100644 index 00000000..03aae5b5 --- /dev/null +++ b/Sources/WIT/Diagnostics.swift @@ -0,0 +1,66 @@ +struct ParseError: Error, CustomStringConvertible { + let description: String +} + +struct DiagnosticError: Error { + let diagnostic: Diagnostic +} + +public struct Diagnostic { + + public let message: String + var textRange: TextRange? + + public func location(_ sourceText: String) -> (line: Int, column: Int)? { + guard let textRange else { return nil } + let position = textRange.lowerBound + let linesBeforePos = sourceText[.. Diagnostic { + return Diagnostic( + message: "Invalid redeclaration of '\(identifier)'", + textRange: textRange + ) + } + + static func expectedIdentifier(textRange: TextRange) -> Diagnostic { + return Diagnostic(message: "Expected identifier", textRange: textRange) + } + + static func cannotFindType(of identifier: String, textRange: TextRange?) -> Diagnostic { + return Diagnostic(message: "Cannot find type '\(identifier)' in scope", textRange: textRange) + } + + static func cannotFindInterface(of identifier: String, textRange: TextRange?) -> Diagnostic { + return Diagnostic(message: "Cannot find interface '\(identifier)' in scope", textRange: textRange) + } + + static func expectedResourceType(_ type: WITType, textRange: TextRange?) -> Diagnostic { + return Diagnostic(message: "Non-resource type \(type)", textRange: textRange) + } + + static func noSuchPackage(_ packageName: PackageNameSyntax, textRange: TextRange?) -> Diagnostic { + return Diagnostic(message: "No such package '\(packageName)'", textRange: textRange) + } + + static func inconsistentPackageName( + _ packageName: PackageNameSyntax, + existingName: PackageNameSyntax, + textRange: TextRange? + ) -> Diagnostic { + return Diagnostic( + message: "package identifier `\(packageName)` does not match previous package name of `\(existingName)`", + textRange: textRange + ) + } + + static func noPackageHeader() -> Diagnostic { + return Diagnostic(message: "no `package` header was found in any WIT file for this package", textRange: nil) + } +} diff --git a/Sources/WIT/Lexer.swift b/Sources/WIT/Lexer.swift new file mode 100644 index 00000000..88d54a39 --- /dev/null +++ b/Sources/WIT/Lexer.swift @@ -0,0 +1,348 @@ +typealias TextRange = Range + +struct Lexer { + struct Cursor { + let input: String.UnicodeScalarView + var nextIndex: String.UnicodeScalarView.Index + + init(input: String) { + self.input = input.unicodeScalars + self.nextIndex = self.input.startIndex + } + + func peek(at offset: Int = 0) -> Unicode.Scalar? { + precondition(offset >= 0) + guard self.input.index(self.nextIndex, offsetBy: offset) < self.input.endIndex else { + return nil + } + let index = self.input.index(self.nextIndex, offsetBy: offset) + return self.input[index] + } + + mutating func next() -> Unicode.Scalar? { + guard self.nextIndex < self.input.endIndex else { return nil } + defer { self.nextIndex = self.input.index(after: self.nextIndex) } + return self.input[self.nextIndex] + } + + mutating func eat(_ expected: UnicodeScalar) -> Bool { + if peek() == expected { + _ = next() + return true + } + return false + } + } + + struct Lexeme: Equatable { + var kind: TokenKind + var textRange: TextRange + } + + struct Diagnostic: CustomStringConvertible { + let description: String + } + + var cursor: Cursor + + mutating func advanceToEndOfBlockComment() -> Diagnostic? { + var depth = 1 + while true { + switch self.cursor.next() { + case "*": + // Check end of block comment + if cursor.eat("/") { + depth -= 1 + if depth == 0 { + break + } + } + case "/": + // Check nested "/*" + if cursor.eat("*") { + depth += 1 + } + case nil: + return Diagnostic(description: "unterminated block comment") + case .some: + continue + } + } + } + + struct LexKindResult { + var kind: TokenKind + var diagnostic: Diagnostic? + } + + mutating func lexKind() -> LexKindResult? { + + func isKeyLikeStart(_ ch: UnicodeScalar) -> Bool { + // This check allows invalid identifier but we'll diagnose + // that after we've lexed the full string. + return ch.properties.isXIDStart || ch == "_" || ch == "-" + } + func isKeyLikeContinue(_ ch: UnicodeScalar) -> Bool { + // XID continue includes '_' + return ch.properties.isXIDContinue || ch == "-" + } + func isASCIIDigit(_ ch: UnicodeScalar) -> Bool { + return "0" <= ch && ch <= "9" + } + + let startIndex = cursor.nextIndex + while let nextChar = cursor.next() { + switch nextChar { + case "\n", "\t", " ": + while cursor.eat("\n") || cursor.eat("\t") || cursor.eat(" ") {} + return LexKindResult(kind: .whitespace) + case "/": + // Eat a line comment if it starts with "//" + if cursor.eat("/") { + while let commentChar = cursor.next(), commentChar != "\n" {} + return LexKindResult(kind: .comment) + } + // Eat a block comment if it starts with "/*" + if cursor.eat("*") { + let diag = advanceToEndOfBlockComment() + return LexKindResult(kind: .comment, diagnostic: diag) + } + return LexKindResult(kind: .slash) + case "=": return LexKindResult(kind: .equals) + case ",": return LexKindResult(kind: .comma) + case ":": return LexKindResult(kind: .colon) + case ".": return LexKindResult(kind: .period) + case ";": return LexKindResult(kind: .semicolon) + case "(": return LexKindResult(kind: .leftParen) + case ")": return LexKindResult(kind: .rightParen) + case "{": return LexKindResult(kind: .leftBrace) + case "}": return LexKindResult(kind: .rightBrace) + case "<": return LexKindResult(kind: .lessThan) + case ">": return LexKindResult(kind: .greaterThan) + case "*": return LexKindResult(kind: .star) + case "@": return LexKindResult(kind: .at) + case "-": + if cursor.eat(">") { + return LexKindResult(kind: .rArrow) + } else { + return LexKindResult(kind: .minus) + } + case "+": return LexKindResult(kind: .plus) + case "%": + var tmp = self.cursor + if let ch = tmp.next(), isKeyLikeStart(ch) { + self.cursor = tmp + while let ch = tmp.next() { + if !isKeyLikeContinue(ch) { + break + } + self.cursor = tmp + } + } + return LexKindResult(kind: .explicitId) + case let ch where isKeyLikeStart(ch): + var tmp = self.cursor + while let ch = tmp.next() { + if !isKeyLikeContinue(ch) { + break + } + self.cursor = tmp + } + + switch String(self.cursor.input[startIndex.. Lexeme? { + let start = self.cursor.nextIndex + guard let kind = self.lexKind() else { + return nil + } + let end = self.cursor.nextIndex + return Lexeme(kind: kind.kind, textRange: start.. Lexeme? { + while let token = self.rawLex() { + switch token.kind { + case .comment, .whitespace: continue + default: return token + } + } + return nil + } + + func peek() -> Lexeme? { + var copy = self + return copy.lex() + } + + @discardableResult + mutating func expect(_ expected: TokenKind) throws -> Lexer.Lexeme { + guard let actual = self.lex() else { + throw ParseError(description: "\(expected) expected but got nothing") + } + guard actual.kind == expected else { + throw ParseError(description: "\(expected) expected but got \(actual.kind)") + } + return actual + } + + @discardableResult + mutating func eat(_ expected: TokenKind) -> Bool { + var other = self + guard let token = other.lex(), token.kind == expected else { + return false + } + self = other + return true + } + + var isEOF: Bool { self.peek() == nil } + + func parseText(in range: TextRange) -> String { + String(self.cursor.input[range]) + } + + func parseExplicitIdentifier(in range: TextRange) -> String { + let firstIndex = range.lowerBound + let nextIndex = self.cursor.input.index(after: firstIndex) + assert(self.cursor.input[firstIndex] == "%") + return String(self.cursor.input[nextIndex.. Lexeme? { + return self.lex() + } +} + +enum TokenKind: Equatable { + case whitespace + case comment + + case equals + case comma + case colon + case period + case semicolon + case leftParen + case rightParen + case leftBrace + case rightBrace + case lessThan + case greaterThan + case rArrow + case star + case at + case slash + case plus + case minus + + case use + case type + case `func` + case u8 + case u16 + case u32 + case u64 + case s8 + case s16 + case s32 + case s64 + case float32 + case float64 + case char + case record + case resource + case own + case borrow + case flags + case variant + case `enum` + case union + case bool + case string_ + case option_ + case result_ + case future + case stream + case list + case underscore + case `as` + case from_ + case `static` + case interface + case tuple + case `import` + case export + case world + case package + case constructor + + case id + case explicitId + + case integer + + case include + case with +} diff --git a/Sources/WIT/PackageResolver.swift b/Sources/WIT/PackageResolver.swift new file mode 100644 index 00000000..f54f60cc --- /dev/null +++ b/Sources/WIT/PackageResolver.swift @@ -0,0 +1,246 @@ +import Foundation + +/// A unit of WIT package managing a collection of WIT source files +public final class PackageUnit: Hashable, CustomStringConvertible { + public let packageName: PackageNameSyntax + public let sourceFiles: [SyntaxNode] + + init(packageName: PackageNameSyntax, sourceFiles: [SyntaxNode]) { + self.packageName = packageName + self.sourceFiles = sourceFiles + } + + public var description: String { + "PackageUnit(\(packageName))" + } + + public static func == (lhs: PackageUnit, rhs: PackageUnit) -> Bool { + lhs === rhs + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(packageName.name.text) + } +} + +/// A collection of WIT packages. +/// +/// Responsible to find a package that satisfies the given requirement. +public final class PackageResolver: Hashable { + private(set) var packages: [PackageUnit] = [] + + /// Create a new package resolver. + public init() {} + + /// Register a package to this resolver, creating a new package from the given source files. + /// + /// - Returns: A newly created package from the given source files. + public func register(packageSources: [SyntaxNode]) throws -> PackageUnit { + var packageBuilder = PackageBuilder() + for sourceFile in packageSources { + try packageBuilder.append(sourceFile) + } + let package = try packageBuilder.build() + register(packageUnit: package) + return package + } + + /// Register the given package to this resolver. + public func register(packageUnit: PackageUnit) { + packages.append(packageUnit) + } + + func findPackage( + namespace: String, + package: String, + version: Version? + ) -> PackageUnit? { + for pkg in self.packages { + let found = Self.satisfyRequirement( + pkg: pkg, + namespace: namespace, + packageName: package, + version: version + ) + if found { return pkg } + } + return nil + } + + private static func satisfyRequirement( + pkg: PackageUnit, + namespace: String, + packageName: String, + version: Version? + ) -> Bool { + guard pkg.packageName.namespace.text == namespace, + pkg.packageName.name.text == packageName + else { return false } + // If package user specify version, check package version + if let version { + if let candidateVersion = pkg.packageName.version { + return candidateVersion.isCompatible(with: version) + } + // If candidate does not have a version specification, reject. + return false + } + return true + } + + public static func == (lhs: PackageResolver, rhs: PackageResolver) -> Bool { + lhs === rhs + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +extension Version { + /// Whether this version satisfies the given requirement. + fileprivate func isCompatible(with requirement: Version) -> Bool { + // Currently the same pre-release and build metadata are required + // for compatibility with other WIT tools. + return major == requirement.major && minor == requirement.minor && patch == requirement.patch && prerelease == requirement.prerelease && buildMetadata == requirement.buildMetadata + } +} + +// - MARK: Directory structure convention + +/// A type to interact with files and directories required to load packages. +public protocol PackageFileLoader { + /// A type that represents a file path in this loader. + associatedtype FilePath: CustomStringConvertible + + /// Returns a list of WIT file paths contained in the given package directory. + func packageFiles(in packageDirectory: FilePath) throws -> [FilePath] + + /// Returns text contents of a file at the given file path. + func contentsOfWITFile(at filePath: FilePath) throws -> String + + /// Returns a list of directory paths contained in the given package directory. + /// Typically, returns directory entries in `deps` directory under the package directory. + func dependencyDirectories(from packageDirectory: FilePath) throws -> [FilePath] +} + +extension PackageResolver { + /// Parses a WIT package at the given directory path and its dependency packages. + /// + /// - Parameters: + /// - directory: A WIT package directory containing `*.wit` files and optionally `deps` directory. + /// - loader: A file loader used to load package contents. + /// - Returns: A pair of the main package parsed from the given directory directly and package + /// resolver containing a set of packages including dependencies. + public static func parse( + directory: Loader.FilePath, loader: Loader + ) throws -> (mainPackage: PackageUnit, packageResolver: PackageResolver) { + let packageResolver = PackageResolver() + let mainPackage = try PackageUnit.parse(directory: directory, loader: loader) + packageResolver.register(packageUnit: mainPackage) + + for dependency in try loader.dependencyDirectories(from: directory) { + let depPackage = try PackageUnit.parse(directory: dependency, loader: loader) + packageResolver.register(packageUnit: depPackage) + } + return (mainPackage, packageResolver) + } +} + +extension PackageUnit { + /// Parses a WIT package at the given directory path. + /// + /// - Parameters: + /// - directory: A WIT package directory containing `*.wit` files. + /// - loader: A file loader used to load package contents. + /// - Returns: A package parsed from the given directory. + public static func parse( + directory: Loader.FilePath, loader: Loader + ) throws -> PackageUnit { + var packageBuilder = PackageBuilder() + for filePath in try loader.packageFiles(in: directory) { + try packageBuilder.append( + SourceFileSyntax.parse( + filePath: filePath, + loader: loader + ) + ) + } + return try packageBuilder.build() + } +} + +extension SourceFileSyntax { + /// Parses a WIT file at the given file path. + /// + /// - Parameters: + /// - filePath: A WIT file path. + /// - loader: A file loader used to load package contents. + /// - Returns: A parsed WIT source file representation. + public static func parse( + filePath: Loader.FilePath, loader: Loader + ) throws -> SyntaxNode { + let contents = try loader.contentsOfWITFile(at: filePath) + return try SourceFileSyntax.parse(contents, fileName: filePath.description) + } + + /// Parses the given WIT source + /// + /// - Parameters: + /// - contents: A WIT source contents + /// - fileName: A file name used for diagnostics + /// - Returns: A parsed WIT source file representation. + public static func parse(_ contents: String, fileName: String) throws -> SyntaxNode { + var lexer = Lexer(cursor: Lexer.Cursor(input: contents)) + return try SourceFileSyntax.parse(lexer: &lexer, fileName: fileName) + } +} + +#if !os(WASI) + /// A ``PackageFileLoader`` adapter for local file system. + public struct LocalFileLoader: PackageFileLoader { + public typealias FilePath = String + + let fileManager: FileManager + + public init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } + + enum Error: Swift.Error { + case failedToLoadFile(FilePath) + } + + private func isDirectory(filePath: String) -> Bool { + var isDirectory: ObjCBool = false + let exists = fileManager.fileExists(atPath: filePath, isDirectory: &isDirectory) + return exists && isDirectory.boolValue + } + + public func contentsOfWITFile(at filePath: String) throws -> String { + guard let bytes = fileManager.contents(atPath: filePath) else { + throw Error.failedToLoadFile(filePath) + } + return String(decoding: bytes, as: UTF8.self) + } + + public func packageFiles(in packageDirectory: String) throws -> [String] { + let dirURL = URL(fileURLWithPath: packageDirectory) + return try fileManager.contentsOfDirectory(atPath: packageDirectory).filter { fileName in + return fileName.hasSuffix(".wit") + && { + let filePath = dirURL.appendingPathComponent(fileName) + return !isDirectory(filePath: filePath.path) + }() + } + .map { dirURL.appendingPathComponent($0).path } + } + + public func dependencyDirectories(from packageDirectory: String) throws -> [String] { + let dirURL = URL(fileURLWithPath: packageDirectory) + let depsDir = dirURL.appendingPathComponent("deps") + guard isDirectory(filePath: depsDir.path) else { return [] } + return try fileManager.contentsOfDirectory(atPath: depsDir.path) + } + } + +#endif diff --git a/Sources/WIT/Semantics/NameLookup.swift b/Sources/WIT/Semantics/NameLookup.swift new file mode 100644 index 00000000..b48a1a2e --- /dev/null +++ b/Sources/WIT/Semantics/NameLookup.swift @@ -0,0 +1,339 @@ +struct DeclContext: Equatable, Hashable { + enum Kind: Equatable, Hashable { + case interface(SyntaxNode, sourceFile: SyntaxNode, context: InterfaceDefinitionContext) + case inlineInterface( + name: Identifier, + items: [InterfaceItemSyntax], + sourceFile: SyntaxNode, + parentWorld: Identifier + ) + case world(SyntaxNode, sourceFile: SyntaxNode) + } + + let kind: Kind + let packageUnit: PackageUnit + let packageResolver: PackageResolver + + var parentSourceFile: SyntaxNode? { + switch kind { + case .inlineInterface(_, _, let sourceFile, _), .world(_, let sourceFile), .interface(_, let sourceFile, _): + return sourceFile + } + } +} + +/// Lookup a type with the given name from the given declaration context +struct TypeNameLookup { + let context: DeclContext + let name: String + let evaluator: Evaluator + + func lookup() throws -> WITType { + switch context.kind { + case .interface(let interface, _, _): + return try lookupInterface(interface.items) + case .inlineInterface(_, let items, _, _): + return try lookupInterface(items) + case .world(let world, _): + return try lookupWorld(world) + } + } + + func lookupInterface(_ interfaceItems: [InterfaceItemSyntax]) throws -> WITType { + for item in interfaceItems { + switch item { + case .function: break + case .typeDef(let typeDef): + if typeDef.name.text == name { + return try typeDef.syntax.asWITType(evaluator: evaluator, context: context) + } + case .use(let use): + if let resolved = try lookupUse(use) { + return resolved + } + } + } + throw DiagnosticError(diagnostic: .cannotFindType(of: name, textRange: nil)) + } + + func lookupWorld(_ world: SyntaxNode) throws -> WITType { + for item in world.items { + switch item { + case .import, .export, .include: break + case .use(let use): + if let resolved = try lookupUse(use) { + return resolved + } + case .type(let typeDef): + if typeDef.name.text == name { + return try typeDef.syntax.asWITType(evaluator: evaluator, context: context) + } + } + } + throw DiagnosticError(diagnostic: .cannotFindType(of: name, textRange: nil)) + } + + func lookupUse(_ use: SyntaxNode) throws -> WITType? { + for useName in use.names { + let found: Bool + if let asName = useName.asName { + found = name == asName.text + } else { + found = name == useName.name.text + } + + guard found else { continue } + + // If a `use` found, it must be a valid reference + let (interface, sourceFile, packageUnit) = try evaluator.evaluate( + request: LookupInterfaceForUsePathRequest( + use: use.from, + packageResolver: context.packageResolver, + packageUnit: context.packageUnit, + sourceFile: context.parentSourceFile + ) + ) + // Lookup within the found interface again. + return try evaluator.evaluate( + request: TypeNameLookupRequest( + context: .init( + kind: .interface( + interface, + sourceFile: sourceFile, + context: .package(packageUnit.packageName) + ), + packageUnit: packageUnit, + packageResolver: context.packageResolver + ), + name: useName.name.text + ) + ) + } + return nil + } +} + +struct TypeNameLookupRequest: EvaluationRequest { + let context: DeclContext + let name: String + + func evaluate(evaluator: Evaluator) throws -> WITType { + let lookup = TypeNameLookup(context: context, name: name, evaluator: evaluator) + return try lookup.lookup() + } +} + +struct LookupPackageRequest: EvaluationRequest { + let packageResolver: PackageResolver + let packageName: PackageNameSyntax + + func evaluate(evaluator: Evaluator) throws -> PackageUnit { + guard + let pkgUnit = packageResolver.findPackage( + namespace: packageName.namespace.text, + package: packageName.name.text, + version: packageName.version + ) + else { + throw DiagnosticError(diagnostic: .noSuchPackage(packageName, textRange: packageName.textRange)) + } + return pkgUnit + } +} + +struct LookupInterfaceInPackageRequest: EvaluationRequest { + let packageUnit: PackageUnit + let name: String + + func evaluate(evaluator: Evaluator) throws -> ( + interface: SyntaxNode, + sourceFile: SyntaxNode + ) { + for sourceFile in packageUnit.sourceFiles { + for case let .interface(iface) in sourceFile.items { + if iface.name.text == name { return (iface, sourceFile) } + } + } + throw DiagnosticError(diagnostic: .cannotFindInterface(of: name, textRange: nil)) + } +} + +struct LookupInterfaceForUsePathRequest: EvaluationRequest { + let use: UsePathSyntax + let packageResolver: PackageResolver + let packageUnit: PackageUnit + let sourceFile: SyntaxNode? + + func evaluate(evaluator: Evaluator) throws -> ( + interface: SyntaxNode, + sourceFile: SyntaxNode, + packageUnit: PackageUnit + ) { + let packageUnit: PackageUnit + let interface: SyntaxNode + let sourceFile: SyntaxNode + switch use { + case .id(let id): + // Bare form `iface.{type}` refers to an interface defined in the same package. + packageUnit = self.packageUnit + (interface, sourceFile) = try evaluator.evaluate( + request: LookupLocalInterfaceRequest( + packageResolver: packageResolver, + packageUnit: packageUnit, + sourceFile: self.sourceFile, name: id.text + ) + ) + case .package(let packageName, let id): + // Fully-qualified type reference `use namespace.pkg.{type}` + packageUnit = try evaluator.evaluate( + request: LookupPackageRequest( + packageResolver: self.packageResolver, + packageName: packageName + ) + ) + (interface, sourceFile) = try evaluator.evaluate( + request: LookupInterfaceInPackageRequest(packageUnit: packageUnit, name: id.text) + ) + } + return (interface, sourceFile, packageUnit) + } +} + +struct LookupLocalInterfaceRequest: EvaluationRequest { + let packageResolver: PackageResolver + let packageUnit: PackageUnit + let sourceFile: SyntaxNode? + let name: String + + func evaluate(evaluator: Evaluator) throws -> ( + interface: SyntaxNode, + sourceFile: SyntaxNode + ) { + if let sourceFile { + for case let .use(use) in sourceFile.items { + let found: Bool + if let asName = use.asName { + found = name == asName.text + } else { + found = name == use.item.name.text + } + guard found else { continue } + + let (interface, sourceFile, _) = try evaluator.evaluate( + request: LookupInterfaceForUsePathRequest( + use: use.item, + packageResolver: packageResolver, + packageUnit: packageUnit, + sourceFile: sourceFile + ) + ) + return (interface, sourceFile) + } + } + for sourceFile in packageUnit.sourceFiles { + for case let .interface(iface) in sourceFile.items { + if iface.name.text == name { return (iface, sourceFile) } + } + } + throw DiagnosticError(diagnostic: .cannotFindInterface(of: name, textRange: nil)) + } +} + +extension DeclContext { + var definitionContext: TypeDefinitionContext { + switch self.kind { + case .interface(let interfaceSyntax, _, let context): + return .interface(id: interfaceSyntax.name, parent: context) + case .inlineInterface(let name, _, _, let parentWorld): + return .interface(id: name, parent: .world(parentWorld)) + case .world(let worldSyntax, _): + return .world(worldSyntax.name) + } + } +} + +extension TypeDefSyntax { + fileprivate func asWITType(evaluator: Evaluator, context: DeclContext) throws -> WITType { + switch body { + case .flags(let flags): + return .flags( + WITFlags( + name: self.name.text, + flags: flags.flags.map { + WITFlags.Flag(name: $0.name.text, syntax: $0) + }, + parent: context.definitionContext + ) + ) + case .resource(let resource): return .resource(resource) + case .record(let record): + return try .record( + WITRecord( + name: self.name.text, + fields: record.fields.map { + try WITRecord.Field( + name: $0.name.text, + type: $0.type.resolve(evaluator: evaluator, in: context), + syntax: $0 + ) + }, + parent: context.definitionContext + ) + ) + case .variant(let variant): + return try .variant( + WITVariant( + name: self.name.text, + cases: variant.cases.map { + try WITVariant.Case( + name: $0.name.text, + type: $0.type?.resolve(evaluator: evaluator, in: context), + syntax: $0 + ) + }, + parent: context.definitionContext + ) + ) + case .union(let union): + return try .union( + WITUnion( + name: self.name.text, + cases: union.cases.map { + try WITUnion.Case( + type: $0.type.resolve(evaluator: evaluator, in: context), + syntax: $0 + ) + }, + parent: context.definitionContext + ) + ) + case .enum(let `enum`): + return .enum( + WITEnum( + name: self.name.text, + cases: `enum`.cases.map { + WITEnum.Case(name: $0.name.text, syntax: $0) + }, + parent: context.definitionContext + ) + ) + case .alias(let alias): + return try evaluator.evaluate(request: TypeResolutionRequest(context: context, typeRepr: alias.typeRepr)) + } + } +} + +extension SemanticsContext { + public func lookupInterface( + name: String, + contextPackage: PackageUnit, + sourceFile: SyntaxNode? = nil + ) throws -> (interface: SyntaxNode, sourceFile: SyntaxNode) { + try evaluator.evaluate( + request: LookupLocalInterfaceRequest( + packageResolver: packageResolver, + packageUnit: contextPackage, sourceFile: sourceFile, name: name + ) + ) + } +} diff --git a/Sources/WIT/Semantics/PackageBuilder.swift b/Sources/WIT/Semantics/PackageBuilder.swift new file mode 100644 index 00000000..763c5653 --- /dev/null +++ b/Sources/WIT/Semantics/PackageBuilder.swift @@ -0,0 +1,32 @@ +/// A type responsible to build a package from parsed `.wit` ASTs +struct PackageBuilder { + var packageName: PackageNameSyntax? + var sourceFiles: [SyntaxNode] = [] + + mutating func append(_ ast: SyntaxNode) throws { + // Check package name consistency + switch (self.packageName, ast.packageId) { + case (_, nil): break + case (nil, let name?): + self.packageName = name + case (let existingName?, let newName?): + guard existingName.isSamePackage(as: newName) else { + throw DiagnosticError( + diagnostic: .inconsistentPackageName( + newName, + existingName: existingName, + textRange: newName.textRange + ) + ) + } + } + self.sourceFiles.append(ast) + } + + func build() throws -> PackageUnit { + guard let packageName = self.packageName else { + throw DiagnosticError(diagnostic: .noPackageHeader()) + } + return PackageUnit(packageName: packageName, sourceFiles: sourceFiles) + } +} diff --git a/Sources/WIT/Semantics/RequestEvaluator.swift b/Sources/WIT/Semantics/RequestEvaluator.swift new file mode 100644 index 00000000..ad27e9b6 --- /dev/null +++ b/Sources/WIT/Semantics/RequestEvaluator.swift @@ -0,0 +1,78 @@ +protocol EvaluationRequest: Hashable { + associatedtype Output + + func evaluate(evaluator: Evaluator) throws -> Output +} + +/// A central gate of computation that evaluates "requests" and caches their output, tracking dependencies graph. +/// This "Request-Evaluator" architecture allows to eliminate mutable state from AST, enable lazy-resolution, and +/// extremely simplifies cyclic-reference-detection. +/// +/// This technique is heavily inspired by https://github.com/apple/swift/blob/main/docs/RequestEvaluator.md +internal class Evaluator { + /// A cache that stores the result by request as a key + private var cache: [AnyHashable: Result] = [:] + /// A stack of current evaluating requests used to diagnostic report. + /// The last element is the most recent request. + private var activeRequests: [any EvaluationRequest] = [] + /// A set of current evaluating requests used for cyclic dependencies detection. + private var activeRequestsSet: Set = [] + + /// Create a new evaluator + internal init() {} + + /// The entrypoint of the gate way, which evaluates the given request. + /// - Parameter request: A request to be evaluated + /// - Returns: Returns freshly-evaluated result if the request has never been evaluated yet. + /// Otherwise, returns the cached result. + /// - Throws: Whatever is thrown by the `evaluate` method of the given request + /// and cyclic dependencies error if found. + func evaluate(request: R) throws -> R.Output { + let requestAsHashable = AnyHashable(request) + if let cached = cache[requestAsHashable] { + return try cached.get() as! R.Output + } + + // Check cyclical request + if activeRequestsSet.contains(requestAsHashable) { + throw CyclicalRequestError(activeRequests: activeRequests + [request]) + } + + // Push the given request as an active request + activeRequests.append(request) + activeRequestsSet.insert(requestAsHashable) + + let result: Result + defer { + // Pop the request from active requests + activeRequests.removeLast() + activeRequestsSet.remove(requestAsHashable) + + // Cache the result by request as a key + cache[requestAsHashable] = result + } + do { + let output = try request.evaluate(evaluator: self) + result = .success(output) + return output + } catch { + result = .failure(error) + throw error + } + } +} + +extension Evaluator { + struct CyclicalRequestError: Error, CustomStringConvertible { + let activeRequests: [any EvaluationRequest] + + var description: String { + var description = "==== Cycle detected! ====\n" + for (index, request) in activeRequests.enumerated() { + let indent = String(repeating: " ", count: index) + description += "\(indent)\\- \(request)\n" + } + return description + } + } +} diff --git a/Sources/WIT/Semantics/SemanticsContext.swift b/Sources/WIT/Semantics/SemanticsContext.swift new file mode 100644 index 00000000..bfd6d0e5 --- /dev/null +++ b/Sources/WIT/Semantics/SemanticsContext.swift @@ -0,0 +1,11 @@ +public struct SemanticsContext { + let evaluator: Evaluator + public let rootPackage: PackageUnit + public let packageResolver: PackageResolver + + public init(rootPackage: PackageUnit, packageResolver: PackageResolver) { + self.evaluator = Evaluator() + self.rootPackage = rootPackage + self.packageResolver = packageResolver + } +} diff --git a/Sources/WIT/Semantics/Type.swift b/Sources/WIT/Semantics/Type.swift new file mode 100644 index 00000000..922e254b --- /dev/null +++ b/Sources/WIT/Semantics/Type.swift @@ -0,0 +1,213 @@ +public indirect enum WITType: Equatable, Hashable { + case bool + case u8 + case u16 + case u32 + case u64 + case s8 + case s16 + case s32 + case s64 + case float32 + case float64 + case char + case string + case list(WITType) + case handleOwn(ResourceSyntax) + case handleBorrow(ResourceSyntax) + case tuple([WITType]) + case option(WITType) + case result(ok: WITType?, error: WITType?) + case future(WITType?) + case stream(element: WITType?, end: WITType?) + + case record(WITRecord) + case flags(WITFlags) + case `enum`(WITEnum) + case variant(WITVariant) + case resource(ResourceSyntax) + case union(WITUnion) +} + +public enum InterfaceDefinitionContext: Equatable, Hashable { + case world(Identifier) + case package(PackageNameSyntax) +} +public enum TypeDefinitionContext: Equatable, Hashable { + case world(Identifier) + case interface(id: Identifier, parent: InterfaceDefinitionContext) +} + +public struct WITRecord: Equatable, Hashable { + public struct Field: Equatable, Hashable { + public var name: String + public var type: WITType + public var syntax: FieldSyntax + } + public var name: String + public var fields: [Field] + public var parent: TypeDefinitionContext +} + +public struct WITUnion: Equatable, Hashable { + public struct Case: Equatable, Hashable { + public var type: WITType + public var syntax: UnionCaseSyntax + } + public var name: String + public var cases: [Case] + public var parent: TypeDefinitionContext +} + +public struct WITEnum: Equatable, Hashable { + public struct Case: Equatable, Hashable { + public var name: String + public var syntax: EnumCaseSyntax + } + public var name: String + public var cases: [Case] + public var parent: TypeDefinitionContext +} + +public struct WITVariant: Equatable, Hashable { + public struct Case: Equatable, Hashable { + public var name: String + public var type: WITType? + public var syntax: CaseSyntax + } + public var name: String + public var cases: [Case] + public var parent: TypeDefinitionContext +} + +public struct WITFlags: Equatable, Hashable { + public struct Flag: Equatable, Hashable { + public var name: String + public var syntax: FlagSyntax + } + public var name: String + public var flags: [Flag] + public var parent: TypeDefinitionContext +} + +struct TypeResolutionRequest: EvaluationRequest { + let context: DeclContext + let typeRepr: TypeReprSyntax + + func evaluate(evaluator: Evaluator) throws -> WITType { + switch typeRepr { + case .bool: return .bool + case .u8: return .u8 + case .u16: return .u16 + case .u32: return .u32 + case .u64: return .u64 + case .s8: return .s8 + case .s16: return .s16 + case .s32: return .s32 + case .s64: return .s64 + case .float32: return .float32 + case .float64: return .float64 + case .char: return .char + case .string: return .string + case .name(let id): + let name = id.text + return try evaluator.evaluate(request: TypeNameLookupRequest(context: context, name: name)) + case .list(let elementRepr): + let elementTy = try evaluator.evaluate(request: self.copy(with: elementRepr)) + return .list(elementTy) + case .handle(let handle): + let name = handle.id.text + let resource = try evaluator.evaluate( + request: ResourceTypeNameLookupRequest( + context: context, + name: name + ) + ) + switch handle { + case .own: return .handleOwn(resource) + case .borrow: return .handleBorrow(resource) + } + case .tuple(let typeReprs): + let types = try typeReprs.map { typeRepr in + try evaluator.evaluate(request: self.copy(with: typeRepr)) + } + return .tuple(types) + case .option(let elementRepr): + let elementTy = try evaluator.evaluate(request: self.copy(with: elementRepr)) + return .option(elementTy) + case .result(let result): + let okTy = try result.ok.map { try evaluator.evaluate(request: self.copy(with: $0)) } + let errorTy = try result.error.map { try evaluator.evaluate(request: self.copy(with: $0)) } + return .result(ok: okTy, error: errorTy) + case .future(let elementRepr): + let elementTy = try elementRepr.map { try evaluator.evaluate(request: self.copy(with: $0)) } + return .future(elementTy) + case .stream(let stream): + let elementTy = try stream.element.map { try evaluator.evaluate(request: self.copy(with: $0)) } + let endTy = try stream.end.map { try evaluator.evaluate(request: self.copy(with: $0)) } + return .stream(element: elementTy, end: endTy) + } + } + + private func copy(with typeRepr: TypeReprSyntax) -> TypeResolutionRequest { + return TypeResolutionRequest(context: context, typeRepr: typeRepr) + } +} + +struct ResourceTypeNameLookupRequest: EvaluationRequest { + let context: DeclContext + let name: String + + func evaluate(evaluator: Evaluator) throws -> ResourceSyntax { + let type = try evaluator.evaluate(request: TypeNameLookupRequest(context: context, name: name)) + guard case .resource(let resource) = type else { + throw DiagnosticError(diagnostic: .expectedResourceType(type, textRange: nil)) + } + return resource + } +} + +public struct TypeResolutionContext { + let evaluator: Evaluator + let packageUnit: PackageUnit + let packageResolver: PackageResolver +} + +extension TypeReprSyntax { + func resolve(evaluator: Evaluator, in context: DeclContext) throws -> WITType { + try evaluator.evaluate( + request: TypeResolutionRequest(context: context, typeRepr: self) + ) + } +} + +extension SemanticsContext { + public func resolveType( + _ typeRepr: TypeReprSyntax, + in interface: SyntaxNode, + sourceFile: SyntaxNode, + contextPackage: PackageUnit + ) throws -> WITType { + try resolve( + typeRepr, kind: .interface(interface, sourceFile: sourceFile, context: .package(contextPackage.packageName)), + contextPackage: contextPackage) + } + + public func resolveType( + _ typeRepr: TypeReprSyntax, + in world: SyntaxNode, + sourceFile: SyntaxNode, + contextPackage: PackageUnit + ) throws -> WITType { + try resolve(typeRepr, kind: .world(world, sourceFile: sourceFile), contextPackage: contextPackage) + } + + func resolve(_ typeRepr: TypeReprSyntax, kind: DeclContext.Kind, contextPackage: PackageUnit) throws -> WITType { + try evaluator.evaluate( + request: TypeResolutionRequest( + context: .init(kind: kind, packageUnit: contextPackage, packageResolver: packageResolver), + typeRepr: typeRepr + ) + ) + } +} diff --git a/Sources/WIT/Semantics/Validation.swift b/Sources/WIT/Semantics/Validation.swift new file mode 100644 index 00000000..e597b36f --- /dev/null +++ b/Sources/WIT/Semantics/Validation.swift @@ -0,0 +1,254 @@ +private struct ValidationRequest: EvaluationRequest { + let unit: PackageUnit + let packageResolver: PackageResolver + + func evaluate(evaluator: Evaluator) throws -> [String: [Diagnostic]] { + var diagnostics: [String: [Diagnostic]] = [:] + for sourceFile in unit.sourceFiles { + var validator = PackageValidator( + packageUnit: unit, + packageResolver: packageResolver, + evaluator: evaluator, + sourceFile: sourceFile + ) + try validator.walk(sourceFile.syntax) + diagnostics[sourceFile.fileName] = validator.diagnostics + } + return diagnostics + } +} + +private struct PackageValidator: ASTVisitor { + let packageUnit: PackageUnit + let packageResolver: PackageResolver + let evaluator: Evaluator + let sourceFile: SyntaxNode + var diagnostics: [Diagnostic] = [] + var contextStack: [DeclContext] = [] + var declContext: DeclContext? { contextStack.last } + + init( + packageUnit: PackageUnit, + packageResolver: PackageResolver, + evaluator: Evaluator, + sourceFile: SyntaxNode + ) { + self.packageUnit = packageUnit + self.packageResolver = packageResolver + self.evaluator = evaluator + self.sourceFile = sourceFile + } + + mutating func addDiagnostic(_ diagnostic: Diagnostic) { + self.diagnostics.append(diagnostic) + } + + mutating func pushContext(_ context: DeclContext) { + self.contextStack.append(context) + } + mutating func popContext() { + _ = self.contextStack.popLast() + } + + // No-op for unhandled nodes + func visit(_: T) throws {} + func visitPost(_: T) throws {} + + mutating func visit(_ topLevelUse: SyntaxNode) throws { + _ = try validate(usePath: topLevelUse.item) + } + + mutating func visit(_ world: SyntaxNode) throws { + // Enter world context + pushContext(.init(kind: .world(world, sourceFile: sourceFile), packageUnit: packageUnit, packageResolver: packageResolver)) + } + mutating func visitPost(_ world: SyntaxNode) throws { + popContext() // Leave world context + } + + mutating func visit(_ interface: SyntaxNode) throws { + // Enter interface context + let context: InterfaceDefinitionContext + switch declContext?.kind { + case .interface, .inlineInterface: + fatalError("Interface cannot be defined under other interface") + case .world(let world, _): context = .world(world.name) + case nil: context = .package(packageUnit.packageName) + } + pushContext( + .init( + kind: .interface(interface, sourceFile: sourceFile, context: context), + packageUnit: packageUnit, packageResolver: packageResolver + )) + } + mutating func visitPost(_ interface: SyntaxNode) throws { + popContext() // Leave interface context + } + + mutating func visit(_ importItem: ImportSyntax) throws { + try visitExternKind(importItem.kind) + } + mutating func visitPost(_ importItem: ImportSyntax) throws { + try visitPostExternKind(importItem.kind) + } + mutating func visit(_ export: ExportSyntax) throws { + try visitExternKind(export.kind) + } + mutating func visitPost(_ export: ExportSyntax) throws { + try visitPostExternKind(export.kind) + } + + private mutating func visitExternKind(_ externKind: ExternKindSyntax) throws { + guard case .world(let world, _) = declContext?.kind else { + fatalError("WorldItem should not be appeared in non-world context") + } + switch externKind { + case .interface(let name, let items): + // Just set context. validation for inner items are handled by each visit methods + pushContext( + .init( + kind: .inlineInterface( + name: name, + items: items, + sourceFile: sourceFile, + parentWorld: world.name + ), + packageUnit: packageUnit, + packageResolver: packageResolver + )) + case .path(let path): + _ = try validate(usePath: path) + case .function: break // Handled by visit(_: FunctionSyntax) + } + } + private mutating func visitPostExternKind(_ externKind: ExternKindSyntax) throws { + switch externKind { + case .interface: self.popContext() // Leave inline interface context + default: break + } + } + + // MARK: Validate types + + mutating func visit(_ alias: TypeAliasSyntax) throws { + _ = try validate(typeRepr: alias.typeRepr, textRange: nil) + } + + mutating func visitPost(_ function: FunctionSyntax) throws { + for param in function.parameters { + _ = try validate(typeRepr: param.type, textRange: param.textRange) + } + switch function.results { + case .named(let parameterList): + for result in parameterList { + _ = try validate(typeRepr: result.type, textRange: function.textRange) + } + case .anon(let typeRepr): + _ = try validate(typeRepr: typeRepr, textRange: function.textRange) + } + } + + mutating func visit(_ record: RecordSyntax) throws { + var fieldNames: Set = [] + for field in record.fields { + let name = field.name.text + guard fieldNames.insert(name).inserted else { + addDiagnostic(.invalidRedeclaration(of: name, textRange: field.name.textRange)) + continue + } + _ = try validate(typeRepr: field.type, textRange: field.textRange) + } + } + + mutating func visit(_ variant: VariantSyntax) throws { + var caseNames: Set = [] + for variantCase in variant.cases { + let name = variantCase.name + guard caseNames.insert(name.text).inserted else { + addDiagnostic(.invalidRedeclaration(of: name.text, textRange: name.textRange)) + continue + } + guard let payloadType = variantCase.type else { continue } + _ = try validate(typeRepr: payloadType, textRange: variantCase.textRange) + } + } + + mutating func visit(_ union: UnionSyntax) throws { + for unionCase in union.cases { + _ = try validate(typeRepr: unionCase.type, textRange: unionCase.textRange) + } + } + + mutating func visit(_ use: SyntaxNode) throws { + guard let (interface, sourceFile, packageUnit) = try validate(usePath: use.from) else { + return // Skip rest of checks if interface reference is invalid + } + // Lookup within the found interface again. + for useName in use.names { + let request = TypeNameLookupRequest( + context: .init( + kind: .interface(interface, sourceFile: sourceFile, context: .package(packageUnit.packageName)), + packageUnit: packageUnit, + packageResolver: packageResolver + ), + name: useName.name.text + ) + try catchingDiagnostic { [evaluator] in + _ = try evaluator.evaluate(request: request) + } + } + } + + mutating func catchingDiagnostic(textRange: TextRange? = nil, _ body: () throws -> R) throws -> R? { + do { + return try body() + } catch let error as DiagnosticError { + var diagnostic = error.diagnostic + if diagnostic.textRange == nil { + diagnostic.textRange = textRange + } + addDiagnostic(diagnostic) + return nil + } + } + + mutating func validate(typeRepr: TypeReprSyntax, textRange: TextRange?) throws -> WITType? { + guard let declContext else { + fatalError("TypeRepr outside of declaration context!?") + } + let request = TypeResolutionRequest(context: declContext, typeRepr: typeRepr) + return try self.catchingDiagnostic(textRange: textRange) { [evaluator] in + try evaluator.evaluate(request: request) + } + } + + mutating func validate(usePath: UsePathSyntax) throws -> ( + interface: SyntaxNode, + sourceFile: SyntaxNode, + packageUnit: PackageUnit + )? { + // Check top-level use refers a valid interface + let request = LookupInterfaceForUsePathRequest( + use: usePath, + packageResolver: packageResolver, + packageUnit: packageUnit, + sourceFile: declContext?.parentSourceFile + ) + return try self.catchingDiagnostic { [evaluator] in + try evaluator.evaluate(request: request) + } + } +} + +extension PackageUnit { + func validate(evaluator: Evaluator, packageResolver: PackageResolver) throws -> [String: [Diagnostic]] { + try evaluator.evaluate(request: ValidationRequest(unit: self, packageResolver: packageResolver)) + } +} + +extension SemanticsContext { + /// Semantically validate this package. + public func validate(package: PackageUnit) throws -> [String: [Diagnostic]] { + try package.validate(evaluator: evaluator, packageResolver: packageResolver) + } +} diff --git a/Sources/WIT/SyntaxNode.swift b/Sources/WIT/SyntaxNode.swift new file mode 100644 index 00000000..f2593b11 --- /dev/null +++ b/Sources/WIT/SyntaxNode.swift @@ -0,0 +1,32 @@ +/// An AST node that can be wrapped by ``SyntaxNode`` as a reference +public protocol SyntaxNodeProtocol {} + +/// An AST node with reference semantics to provide identity of a node. +@dynamicMemberLookup +public struct SyntaxNode: Equatable, Hashable { + class Ref { + fileprivate let syntax: Syntax + init(syntax: Syntax) { + self.syntax = syntax + } + } + + private let ref: Ref + public var syntax: Syntax { ref.syntax } + + internal init(syntax: Syntax) { + self.ref = Ref(syntax: syntax) + } + + public subscript(dynamicMember keyPath: KeyPath) -> R { + self.ref.syntax[keyPath: keyPath] + } + + public static func == (lhs: SyntaxNode, rhs: SyntaxNode) -> Bool { + return lhs.ref === rhs.ref + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(ref)) + } +} diff --git a/Sources/WIT/TextParser/ParseFunctionDecl.swift b/Sources/WIT/TextParser/ParseFunctionDecl.swift new file mode 100644 index 00000000..dff1ed74 --- /dev/null +++ b/Sources/WIT/TextParser/ParseFunctionDecl.swift @@ -0,0 +1,100 @@ +extension ResourceFunctionSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> ResourceFunctionSyntax { + guard let token = lexer.peek() else { + throw ParseError(description: "`constructor` or identifier expected but got nothing") + } + switch token.kind { + case .constructor: + try lexer.expect(.constructor) + try lexer.expect(.leftParen) + + let funcStart = lexer.cursor.nextIndex + let params = try Parser.parseListTrailer( + lexer: &lexer, end: .rightParen + ) { docs, lexer in + let start = lexer.cursor.nextIndex + let name = try Identifier.parse(lexer: &lexer) + try lexer.expect(.colon) + let type = try TypeReprSyntax.parse(lexer: &lexer) + return ParameterSyntax(name: name, type: type, textRange: start..) -> ResourceFunctionSyntax + if lexer.eat(.static) { + ctor = ResourceFunctionSyntax.static + } else { + ctor = ResourceFunctionSyntax.method + } + let function = try FunctionSyntax.parse(lexer: &lexer) + return ctor( + .init( + syntax: + NamedFunctionSyntax( + documents: documents, + name: name, + function: function + ) + ) + ) + default: + throw ParseError(description: "`constructor` or identifier expected but got \(token.kind)") + } + } +} + +extension FunctionSyntax { + static func parse(lexer: inout Lexer) throws -> FunctionSyntax { + func parseParameters(lexer: inout Lexer, leftParen: Bool) throws -> ParameterList { + if leftParen { + try lexer.expect(.leftParen) + } + return try Parser.parseListTrailer(lexer: &lexer, end: .rightParen) { docs, lexer in + let start = lexer.cursor.nextIndex + let name = try Identifier.parse(lexer: &lexer) + try lexer.expect(.colon) + let type = try TypeReprSyntax.parse(lexer: &lexer) + return ParameterSyntax(name: name, type: type, textRange: start.. SyntaxNode { + let name = try Identifier.parse(lexer: &lexer) + try lexer.expect(.colon) + let function = try FunctionSyntax.parse(lexer: &lexer) + return .init(syntax: NamedFunctionSyntax(documents: documents, name: name, function: function)) + } +} diff --git a/Sources/WIT/TextParser/ParseInterface.swift b/Sources/WIT/TextParser/ParseInterface.swift new file mode 100644 index 00000000..4d2ce420 --- /dev/null +++ b/Sources/WIT/TextParser/ParseInterface.swift @@ -0,0 +1,50 @@ +extension InterfaceSyntax { + static func parse( + lexer: inout Lexer, documents: DocumentsSyntax + ) throws -> SyntaxNode { + try lexer.expect(.interface) + let name = try Identifier.parse(lexer: &lexer) + let items = try parseItems(lexer: &lexer) + return .init(syntax: InterfaceSyntax(documents: documents, name: name, items: items)) + } + + static func parseItems(lexer: inout Lexer) throws -> [InterfaceItemSyntax] { + try lexer.expect(.leftBrace) + var items: [InterfaceItemSyntax] = [] + while true { + let docs = try DocumentsSyntax.parse(lexer: &lexer) + if lexer.eat(.rightBrace) { + break + } + items.append(try InterfaceItemSyntax.parse(lexer: &lexer, documents: docs)) + } + return items + } +} + +extension InterfaceItemSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> InterfaceItemSyntax { + switch lexer.peek()?.kind { + case .type: + return try .typeDef(.init(syntax: .parse(lexer: &lexer, documents: documents))) + case .flags: + return try .typeDef(.init(syntax: .parseFlags(lexer: &lexer, documents: documents))) + case .enum: + return try .typeDef(.init(syntax: .parseEnum(lexer: &lexer, documents: documents))) + case .variant: + return try .typeDef(.init(syntax: .parseVariant(lexer: &lexer, documents: documents))) + case .resource: + return try .typeDef(TypeDefSyntax.parseResource(lexer: &lexer, documents: documents)) + case .record: + return try .typeDef(.init(syntax: .parseRecord(lexer: &lexer, documents: documents))) + case .union: + return try .typeDef(.init(syntax: .parseUnion(lexer: &lexer, documents: documents))) + case .id, .explicitId: + return try .function(NamedFunctionSyntax.parse(lexer: &lexer, documents: documents)) + case .use: + return try .use(UseSyntax.parse(lexer: &lexer)) + default: + throw ParseError(description: "`import`, `export`, `include`, `use`, or type definition") + } + } +} diff --git a/Sources/WIT/TextParser/ParseTop.swift b/Sources/WIT/TextParser/ParseTop.swift new file mode 100644 index 00000000..a3674ba3 --- /dev/null +++ b/Sources/WIT/TextParser/ParseTop.swift @@ -0,0 +1,224 @@ +extension SourceFileSyntax { + static func parse(lexer: inout Lexer, fileName: String) throws -> SyntaxNode { + var packageId: PackageNameSyntax? + if lexer.peek()?.kind == .package { + packageId = try PackageNameSyntax.parse(lexer: &lexer) + } + + var items: [ASTItemSyntax] = [] + while !lexer.isEOF { + let docs = try DocumentsSyntax.parse(lexer: &lexer) + let item = try ASTItemSyntax.parse(lexer: &lexer, documents: docs) + items.append(item) + } + + return .init(syntax: SourceFileSyntax(fileName: fileName, packageId: packageId, items: items)) + } +} + +extension PackageNameSyntax { + static func parse(lexer: inout Lexer) throws -> PackageNameSyntax { + try lexer.expect(.package) + let namespace = try Identifier.parse(lexer: &lexer) + try lexer.expect(.colon) + let name = try Identifier.parse(lexer: &lexer) + let version = try lexer.eat(.at) ? Version.parse(lexer: &lexer) : nil + let rangeStart = namespace.textRange.lowerBound + let rangeEnd = (version?.textRange ?? name.textRange).upperBound + return PackageNameSyntax(namespace: namespace, name: name, version: version, textRange: rangeStart.. Identifier { + guard let token = lexer.lex() else { + throw ParseError(description: "an identifier expected but got nothing") + } + let text: String + switch token.kind { + case .id: + text = lexer.parseText(in: token.textRange) + case .explicitId: + text = lexer.parseExplicitIdentifier(in: token.textRange) + default: + throw ParseError(description: "an identifier expected but got \(token.kind)") + } + + return Identifier(text: text, textRange: token.textRange) + } +} + +extension Version { + static func parse(lexer: inout Lexer) throws -> Version { + + // Parse semantic version: https://semver.org + let (major, start) = try parseNumericIdentifier(lexer: &lexer) + try lexer.expect(.period) + let (minor, _) = try parseNumericIdentifier(lexer: &lexer) + try lexer.expect(.period) + let (patch, _) = try parseNumericIdentifier(lexer: &lexer) + + let prerelease = try parseMetaIdentifier(lexer: &lexer, prefix: .minus, acceptLeadingZero: false) + let buildMetadata = try parseMetaIdentifier(lexer: &lexer, prefix: .plus, acceptLeadingZero: true) + + return Version( + major: major, minor: minor, patch: patch, + prerelease: prerelease, buildMetadata: buildMetadata, + textRange: start.lowerBound.. (Int, TextRange) { + let token = try lexer.expect(.integer) + let text = lexer.parseText(in: token.textRange) + if text.hasPrefix("0"), text.count > 1 { + throw ParseError(description: "leading zero not accepted") + } + // integer token contains only digits and it's guaranteed to be parsable by `Int.init` + let value = Int(text)! + return (value, token.textRange) + } + + func parseAlphanumericIdentifier(lexer: inout Lexer) throws { + while lexer.eat(.integer) || lexer.eat(.id) || lexer.eat(.minus) {} + } + + func parseDigits(lexer: inout Lexer) throws { + try lexer.expect(.integer) + } + + func parseIdentifier(lexer: inout Lexer, acceptLeadingZero: Bool) throws { + guard let firstToken = lexer.peek() else { + throw ParseError(description: "expected an identifier token") + } + + switch firstToken.kind { + case .integer: + if acceptLeadingZero { + // or + try parseDigits(lexer: &lexer) + } else { + // + _ = try parseNumericIdentifier(lexer: &lexer) + } + // Consume rest of alphanumeric tokens for the case when + // it starts with integer + fallthrough + case .id, .minus: // + try parseAlphanumericIdentifier(lexer: &lexer) + default: + throw ParseError(description: "an id or integer for pre-release id expected") + } + } + + func parseMetaIdentifier(lexer: inout Lexer, prefix: TokenKind, acceptLeadingZero: Bool) throws -> String? { + guard lexer.eat(prefix) else { return nil } + let start = lexer.cursor.nextIndex + func buildResultText(_ lexer: inout Lexer) -> String { + return lexer.parseText(in: start.. ASTItemSyntax { + switch lexer.peek()?.kind { + case .interface: + return try .interface(InterfaceSyntax.parse(lexer: &lexer, documents: documents)) + case .world: + return try .world(WorldSyntax.parse(lexer: &lexer, documents: documents)) + case .use: return try .use(.init(syntax: .parse(lexer: &lexer, documents: documents))) + default: + throw ParseError(description: "`world`, `interface` or `use` expected") + } + } +} + +extension TopLevelUseSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> TopLevelUseSyntax { + try lexer.expect(.use) + let item = try UsePathSyntax.parse(lexer: &lexer) + var asName: Identifier? + if lexer.eat(.as) { + asName = try .parse(lexer: &lexer) + } + return TopLevelUseSyntax(item: item, asName: asName) + } +} + +extension UseSyntax { + static func parse(lexer: inout Lexer) throws -> SyntaxNode { + try lexer.expect(.use) + let from = try UsePathSyntax.parse(lexer: &lexer) + try lexer.expect(.period) + try lexer.expect(.leftBrace) + + var names: [UseNameSyntax] = [] + while !lexer.eat(.rightBrace) { + var name = try UseNameSyntax(name: .parse(lexer: &lexer)) + if lexer.eat(.as) { + name.asName = try .parse(lexer: &lexer) + } + names.append(name) + if !lexer.eat(.comma) { + try lexer.expect(.rightBrace) + break + } + } + return .init(syntax: UseSyntax(from: from, names: names)) + } +} + +extension UsePathSyntax { + static func parse(lexer: inout Lexer) throws -> UsePathSyntax { + let id = try Identifier.parse(lexer: &lexer) + if lexer.eat(.colon) { + let namespace = id + let pkgName = try Identifier.parse(lexer: &lexer) + try lexer.expect(.slash) + let name = try Identifier.parse(lexer: &lexer) + let version = lexer.eat(.at) ? try Version.parse(lexer: &lexer) : nil + return .package( + id: PackageNameSyntax( + namespace: namespace, name: pkgName, version: version, + textRange: namespace.textRange.lowerBound.. DocumentsSyntax { + var comments: [String] = [] + var copy = lexer + while let token = copy.rawLex() { + switch token.kind { + case .whitespace: continue + case .comment: + comments.append(lexer.parseText(in: token.textRange)) + default: + return DocumentsSyntax(comments: comments) + } + lexer = copy // consume comments for real + } + return DocumentsSyntax(comments: comments) + } +} diff --git a/Sources/WIT/TextParser/ParseTypes.swift b/Sources/WIT/TextParser/ParseTypes.swift new file mode 100644 index 00000000..a12e2059 --- /dev/null +++ b/Sources/WIT/TextParser/ParseTypes.swift @@ -0,0 +1,244 @@ +extension TypeReprSyntax { + static func parse(lexer: inout Lexer) throws -> TypeReprSyntax { + guard let token = lexer.next() else { + throw ParseError(description: "a type expected") + } + switch token.kind { + case .bool: return .bool + case .u8: return .u8 + case .u16: return .u16 + case .u32: return .u32 + case .u64: return .u64 + case .s8: return .s8 + case .s16: return .s16 + case .s32: return .s32 + case .s64: return .s64 + case .float32: return .float32 + case .float64: return .float64 + case .char: return .char + case .string_: return .string + + // tuple + case .tuple: + let types = try Parser.parseList( + lexer: &lexer, start: .lessThan, end: .greaterThan + ) { try TypeReprSyntax.parse(lexer: &$1) } + return .tuple(types) + + // list + case .list: + try lexer.expect(.lessThan) + let type = try TypeReprSyntax.parse(lexer: &lexer) + try lexer.expect(.greaterThan) + return .list(type) + + // option + case .option_: + try lexer.expect(.lessThan) + let type = try TypeReprSyntax.parse(lexer: &lexer) + try lexer.expect(.greaterThan) + return .option(type) + + // result + // result<_, E> + // result + // result + case .result_: + var ok: TypeReprSyntax? + var error: TypeReprSyntax? + if lexer.eat(.lessThan) { + if lexer.eat(.underscore) { + try lexer.expect(.comma) + error = try TypeReprSyntax.parse(lexer: &lexer) + } else { + ok = try TypeReprSyntax.parse(lexer: &lexer) + if lexer.eat(.comma) { + error = try TypeReprSyntax.parse(lexer: &lexer) + } + } + try lexer.expect(.greaterThan) + } + return .result(ResultSyntax(ok: ok, error: error)) + + // future + // future + case .future: + var type: TypeReprSyntax? + if lexer.eat(.lessThan) { + type = try TypeReprSyntax.parse(lexer: &lexer) + try lexer.expect(.greaterThan) + } + return .future(type) + + // stream + // stream<_, Z> + // stream + // stream + case .stream: + var element: TypeReprSyntax? + var end: TypeReprSyntax? + if lexer.eat(.lessThan) { + if lexer.eat(.underscore) { + try lexer.expect(.comma) + end = try TypeReprSyntax.parse(lexer: &lexer) + } else { + element = try TypeReprSyntax.parse(lexer: &lexer) + if lexer.eat(.comma) { + end = try TypeReprSyntax.parse(lexer: &lexer) + } + } + try lexer.expect(.greaterThan) + } + return .stream(StreamSyntax(element: element, end: end)) + + // own + case .own: + try lexer.expect(.lessThan) + let resource = try Identifier.parse(lexer: &lexer) + try lexer.expect(.greaterThan) + return .handle(.own(resource: resource)) + + // borrow + case .borrow: + try lexer.expect(.lessThan) + let resource = try Identifier.parse(lexer: &lexer) + try lexer.expect(.greaterThan) + return .handle(.borrow(resource: resource)) + + // `foo` + case .id: + return .name( + Identifier( + text: lexer.parseText(in: token.textRange), + textRange: token.textRange + )) + // `%foo` + case .explicitId: + return .name( + Identifier( + text: lexer.parseExplicitIdentifier(in: token.textRange), + textRange: token.textRange + )) + case let tokenKind: + throw ParseError(description: "unknown type \(String(describing: tokenKind))") + } + } +} + +extension TypeDefSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> TypeDefSyntax { + try lexer.expect(.type) + let name = try Identifier.parse(lexer: &lexer) + try lexer.expect(.equals) + let repr = try TypeReprSyntax.parse(lexer: &lexer) + return TypeDefSyntax(documents: documents, name: name, body: .alias(TypeAliasSyntax(typeRepr: repr))) + } + + static func parseFlags(lexer: inout Lexer, documents: DocumentsSyntax) throws -> TypeDefSyntax { + try lexer.expect(.flags) + let name = try Identifier.parse(lexer: &lexer) + let body = try TypeDefBodySyntax.flags( + FlagsSyntax( + flags: Parser.parseList( + lexer: &lexer, + start: .leftBrace, end: .rightBrace + ) { docs, lexer in + let name = try Identifier.parse(lexer: &lexer) + return FlagSyntax(documents: docs, name: name) + } + )) + return TypeDefSyntax(documents: documents, name: name, body: body) + } + + static func parseResource(lexer: inout Lexer, documents: DocumentsSyntax) throws -> SyntaxNode { + try lexer.expect(.resource) + let name = try Identifier.parse(lexer: &lexer) + var functions: [ResourceFunctionSyntax] = [] + if lexer.eat(.leftBrace) { + while !lexer.eat(.rightBrace) { + let docs = try DocumentsSyntax.parse(lexer: &lexer) + functions.append(try ResourceFunctionSyntax.parse(lexer: &lexer, documents: docs)) + } + } + let body = TypeDefBodySyntax.resource(ResourceSyntax(functions: functions)) + return .init(syntax: TypeDefSyntax(documents: documents, name: name, body: body)) + } + + static func parseRecord(lexer: inout Lexer, documents: DocumentsSyntax) throws -> TypeDefSyntax { + try lexer.expect(.record) + let name = try Identifier.parse(lexer: &lexer) + let body = try TypeDefBodySyntax.record( + RecordSyntax( + fields: Parser.parseList( + lexer: &lexer, + start: .leftBrace, end: .rightBrace + ) { docs, lexer in + let start = lexer.cursor.nextIndex + let name = try Identifier.parse(lexer: &lexer) + try lexer.expect(.colon) + let type = try TypeReprSyntax.parse(lexer: &lexer) + return FieldSyntax(documents: docs, name: name, type: type, textRange: start.. TypeDefSyntax { + try lexer.expect(.variant) + let name = try Identifier.parse(lexer: &lexer) + let body = try TypeDefBodySyntax.variant( + VariantSyntax( + cases: Parser.parseList( + lexer: &lexer, + start: .leftBrace, end: .rightBrace + ) { docs, lexer in + let start = lexer.cursor.nextIndex + let name = try Identifier.parse(lexer: &lexer) + var payloadType: TypeReprSyntax? + if lexer.eat(.leftParen) { + payloadType = try TypeReprSyntax.parse(lexer: &lexer) + try lexer.expect(.rightParen) + } + return CaseSyntax(documents: docs, name: name, type: payloadType, textRange: start.. TypeDefSyntax { + try lexer.expect(.union) + let name = try Identifier.parse(lexer: &lexer) + let body = try TypeDefBodySyntax.union( + UnionSyntax( + cases: Parser.parseList( + lexer: &lexer, + start: .leftBrace, end: .rightBrace + ) { docs, lexer in + let start = lexer.cursor.nextIndex + let type = try TypeReprSyntax.parse(lexer: &lexer) + return UnionCaseSyntax(documents: docs, type: type, textRange: start.. TypeDefSyntax { + try lexer.expect(.enum) + let name = try Identifier.parse(lexer: &lexer) + let body = try TypeDefBodySyntax.enum( + EnumSyntax( + cases: Parser.parseList( + lexer: &lexer, + start: .leftBrace, end: .rightBrace + ) { docs, lexer in + let start = lexer.cursor.nextIndex + let name = try Identifier.parse(lexer: &lexer) + return EnumCaseSyntax(documents: docs, name: name, textRange: start.. SyntaxNode { + try lexer.expect(.world) + let name = try Identifier.parse(lexer: &lexer) + let items = try parseItems(lexer: &lexer) + return .init(syntax: WorldSyntax(documents: documents, name: name, items: items)) + } + + static func parseItems(lexer: inout Lexer) throws -> [WorldItemSyntax] { + try lexer.expect(.leftBrace) + var items: [WorldItemSyntax] = [] + while true { + let docs = try DocumentsSyntax.parse(lexer: &lexer) + if lexer.eat(.rightBrace) { + break + } + items.append(try WorldItemSyntax.parse(lexer: &lexer, documents: docs)) + } + return items + } +} + +extension WorldItemSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> WorldItemSyntax { + switch lexer.peek()?.kind { + case .import: + return try .import(.parse(lexer: &lexer, documents: documents)) + case .export: + return try .export(.parse(lexer: &lexer, documents: documents)) + case .use: + return try .use(UseSyntax.parse(lexer: &lexer)) + case .type: + return try .type(.init(syntax: .parse(lexer: &lexer, documents: documents))) + case .flags: + return try .type(.init(syntax: .parseFlags(lexer: &lexer, documents: documents))) + case .enum: + return try .type(.init(syntax: .parseEnum(lexer: &lexer, documents: documents))) + case .variant: + return try .type(.init(syntax: .parseVariant(lexer: &lexer, documents: documents))) + case .resource: + return try .type(TypeDefSyntax.parseResource(lexer: &lexer, documents: documents)) + case .record: + return try .type(.init(syntax: .parseRecord(lexer: &lexer, documents: documents))) + case .union: + return try .type(.init(syntax: .parseUnion(lexer: &lexer, documents: documents))) + case .include: + return try .include(.parse(lexer: &lexer)) + default: + throw ParseError(description: "`type`, `resource` or `func` expected") + } + } +} + +extension ImportSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> ImportSyntax { + try lexer.expect(.import) + let kind = try ExternKindSyntax.parse(lexer: &lexer) + return ImportSyntax(documents: documents, kind: kind) + } +} + +extension ExportSyntax { + static func parse(lexer: inout Lexer, documents: DocumentsSyntax) throws -> ExportSyntax { + try lexer.expect(.export) + let kind = try ExternKindSyntax.parse(lexer: &lexer) + return ExportSyntax(documents: documents, kind: kind) + } +} + +extension ExternKindSyntax { + static func parse(lexer: inout Lexer) throws -> ExternKindSyntax { + var clone = lexer + let id = try Identifier.parse(lexer: &clone) + if clone.eat(.colon) { + switch clone.peek()?.kind { + case .func: + // import foo: func(...) + lexer = clone + return try .function(id, .parse(lexer: &lexer)) + case .interface: + // import foo: interface { ... } + try clone.expect(.interface) + lexer = clone + return try .interface(id, InterfaceSyntax.parseItems(lexer: &lexer)) + default: break + } + } + // import foo:bar/baz + return try .path(.parse(lexer: &lexer)) + } +} + +extension IncludeSyntax { + static func parse(lexer: inout Lexer) throws -> IncludeSyntax { + try lexer.expect(.include) + let from = try UsePathSyntax.parse(lexer: &lexer) + + var names: [IncludeNameSyntax] = [] + if lexer.eat(.with) { + names = try Parser.parseList(lexer: &lexer, start: .leftBrace, end: .rightBrace) { _, lexer in + let name = try Identifier.parse(lexer: &lexer) + try lexer.expect(.as) + let asName = try Identifier.parse(lexer: &lexer) + return IncludeNameSyntax(name: name, asName: asName) + } + } + return IncludeSyntax(from: from, names: names) + } +} diff --git a/Sources/WIT/TextParser/Parser.swift b/Sources/WIT/TextParser/Parser.swift new file mode 100644 index 00000000..35216ee0 --- /dev/null +++ b/Sources/WIT/TextParser/Parser.swift @@ -0,0 +1,31 @@ +enum Parser { + static func parseList( + lexer: inout Lexer, start: TokenKind, end: TokenKind, + parse: (DocumentsSyntax, inout Lexer) throws -> Item + ) throws -> [Item] { + try lexer.expect(start) + return try parseListTrailer(lexer: &lexer, end: end, parse: parse) + } + + static func parseListTrailer( + lexer: inout Lexer, end: TokenKind, + parse: (DocumentsSyntax, inout Lexer) throws -> Item + ) throws -> [Item] { + var items: [Item] = [] + while true { + let docs = try DocumentsSyntax.parse(lexer: &lexer) + if lexer.eat(end) { + break + } + + let item = try parse(docs, &lexer) + items.append(item) + + guard lexer.eat(.comma) else { + try lexer.expect(end) + break + } + } + return items + } +} diff --git a/Sources/WITExtractor/Diagnostic.swift b/Sources/WITExtractor/Diagnostic.swift new file mode 100644 index 00000000..342a6f58 --- /dev/null +++ b/Sources/WITExtractor/Diagnostic.swift @@ -0,0 +1,31 @@ +public struct Diagnostic: CustomStringConvertible { + public enum Severity { + case warning + case error + } + + public let message: String + public let severity: Severity + + public var description: String { + "\(severity):\(message)" + } + + static func warning(_ messaage: String) -> Diagnostic { + return Diagnostic(message: messaage, severity: .warning) + } +} + +extension Diagnostic { + static func skipField(context: String, field: String, missingType: String) -> Diagnostic { + .warning("Skipping \(context)/\(field) field due to missing corresponding WIT type for \"\(missingType)\"") + } +} + +final class DiagnosticCollection { + private(set) var diagnostics: [Diagnostic] = [] + + func add(_ diagnostic: Diagnostic) { + diagnostics.append(diagnostic) + } +} diff --git a/Sources/WITExtractor/ModuleTranslation.swift b/Sources/WITExtractor/ModuleTranslation.swift new file mode 100644 index 00000000..b08b9249 --- /dev/null +++ b/Sources/WITExtractor/ModuleTranslation.swift @@ -0,0 +1,170 @@ +struct ModuleTranslation { + let diagnostics: DiagnosticCollection + let typeMapping: TypeMapping + var builder: WITBuilder + + mutating func translate(sourceSummary: SwiftSourceSummary) { + for (_, typeSource) in sourceSummary.typesByWITName { + translateType(source: typeSource) + } + + for (_, functionSource) in sourceSummary.functionsByWITName { + translateFunction(source: functionSource) + } + } + + mutating func translateType(source: SwiftTypeSource) { + switch source { + case .structType(let source): + translateStruct(source: source) + case .enumType(let source): + translateEnum(source: source) + } + } + + mutating func translateStruct(source: SwiftStructSource) { + guard let witTypeName = typeMapping.lookupWITType(byUsr: source.usr) else { + return + } + var fields: [WITRecord.Field] = [] + + for field in source.fields { + let fieldName = ConvertCase.witIdentifier(identifier: field.name) + guard let fieldWITType = typeMapping.lookupWITType(byNode: field.type, diagnostics: diagnostics) else { + diagnostics.add( + .skipField( + context: source.node.parent.printedName, + field: field.name, + missingType: field.type.parent.parent.printedName + ) + ) + continue + } + fields.append(WITRecord.Field(name: fieldName, type: fieldWITType)) + } + + builder.define( + record: WITRecord(name: witTypeName, fields: fields) + ) + } + + mutating func translateEnum(source: SwiftEnumSource) { + guard let witTypeName = typeMapping.lookupWITType(byUsr: source.usr) else { + return + } + + var cases: [(name: String, type: String?)] = [] + var hasPayload = false + + for enumCase in source.cases { + let caseName = ConvertCase.witIdentifier(identifier: enumCase.name) + + var payloadWITType: String? + if let payloadTypeNode = enumCase.payloadType { + hasPayload = true + payloadWITType = typeMapping.lookupWITType(byNode: payloadTypeNode, diagnostics: diagnostics) + if payloadWITType == nil { + diagnostics.add( + .skipField( + context: source.node.parent.printedName, + field: enumCase.name, + missingType: payloadTypeNode.parent.parent.printedName + ) + ) + } + } + cases.append((caseName, payloadWITType)) + } + + // If the given Swift enum has at least one case with payload, turn it into variant, + // otherwise optimize to be enum. + if hasPayload { + builder.define(variant: WITVariant(name: witTypeName, cases: cases.map { WITVariant.Case(name: $0, type: $1) })) + } else { + builder.define(enum: WITEnum(name: witTypeName, cases: cases.map { name, _ in name })) + } + } + + mutating func translateFunction(source: SwiftFunctionSource) { + let witName = ConvertCase.witIdentifier(identifier: source.name) + let witResultTypeNames = source.results.compactMap { result -> String? in + guard let singleResult = typeMapping.lookupWITType(byNode: result.type, diagnostics: diagnostics) else { + diagnostics.add( + .skipField( + context: source.name, + field: "result", + missingType: result.type.parent.parent.printedName + ) + ) + return nil + } + return singleResult + } + + guard witResultTypeNames.count == source.results.count else { + // Skip emitting if there is any missing WIT type + return + } + + let witResults: WITFunction.Results + switch source.results.count { + case 0: + witResults = .named([]) + case 1: + witResults = .anon(witResultTypeNames[0]) + default: + var resultNames = AlphabeticalIterator() + witResults = .named( + zip(source.results, witResultTypeNames).map { source, witType in + WITFunction.Parameter(name: source.name ?? resultNames.next(), type: witType) + }) + } + + var parameterNames = AlphabeticalIterator() + let witParameters = source.parameters.compactMap { param -> WITFunction.Parameter? in + let name = parameterNames.next() + guard let type = typeMapping.lookupWITType(byNode: param.type, diagnostics: diagnostics) else { + diagnostics.add( + .skipField( + context: source.name, + field: param.name ?? "parameter", + missingType: param.type.parent.parent.printedName + ) + ) + return nil + } + return WITFunction.Parameter(name: name, type: type) + } + + guard witParameters.count == source.parameters.count else { + // Skip emitting if there is any missing WIT type + return + } + + builder.define( + function: WITFunction( + name: witName, + parameters: witParameters, + results: witResults + ) + ) + } +} + +private struct AlphabeticalIterator { + var index: Int = 0 + + mutating func next() -> String { + let chars = Array("abcdefghijklmnopqrstuvwxyz") + var buffer: [Character] = [] + var tmpIndex = index + while tmpIndex >= chars.count { + buffer.append(chars[tmpIndex % chars.count]) + tmpIndex /= chars.count + tmpIndex -= 1 + } + buffer.append(chars[tmpIndex]) + index += 1 + return String(buffer) + } +} diff --git a/Sources/WITExtractor/Naming/ConvertCase.swift b/Sources/WITExtractor/Naming/ConvertCase.swift new file mode 100644 index 00000000..b19d556c --- /dev/null +++ b/Sources/WITExtractor/Naming/ConvertCase.swift @@ -0,0 +1,157 @@ +enum ConvertCase { + + static func witIdentifier(identifier: [String]) -> String { + return witIdentifier(kebabCase(identifier: identifier)) + } + + static func witIdentifier(identifier: String) -> String { + return witIdentifier(kebabCase(identifier: identifier)) + } + + static func witIdentifier(_ id: String) -> String { + // https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md#keywords + let keywords: Set = [ + "use", + "type", + "resource", + "func", + "record", + "enum", + "flags", + "variant", + "static", + "interface", + "world", + "import", + "export", + "package", + "include", + ] + + if keywords.contains(id) { + return "%\(id)" + } + return id + } + + static func kebabCase(identifier: [String]) -> String { + identifier.map { kebabCase(identifier: $0) }.joined(separator: "-") + } + + /// Convert any Swift-like identifier to WIT identifier + /// + /// The WIT identifier is defined as follows: + /// + /// ``` + /// label ::= + /// |