Browse Source

Initial commit

AnnulusGames 1 year ago
commit
3dc08599b8
100 changed files with 9615 additions and 0 deletions
  1. 479 0
      .gitignore
  2. 52 0
      Lua.sln
  3. 88 0
      sandbox/Benchmark/AddBenchmark.cs
  4. 20 0
      sandbox/Benchmark/Benchmark.csproj
  5. 35 0
      sandbox/Benchmark/Benchmark.sln
  6. 9 0
      sandbox/Benchmark/FileHelper.cs
  7. 88 0
      sandbox/Benchmark/InterpreterSteps.cs
  8. 19 0
      sandbox/Benchmark/Program.cs
  9. 7 0
      sandbox/Benchmark/add.lua
  10. 14 0
      sandbox/ConsoleApp1/ConsoleApp1.csproj
  11. 97 0
      sandbox/ConsoleApp1/Program.cs
  12. 58 0
      sandbox/ConsoleApp1/vec3.lua
  13. 35 0
      src/Lua/CodeAnalysis/Compilation/Descriptions.cs
  14. 321 0
      src/Lua/CodeAnalysis/Compilation/FunctionCompilationContext.cs
  15. 1062 0
      src/Lua/CodeAnalysis/Compilation/LuaCompiler.cs
  16. 170 0
      src/Lua/CodeAnalysis/Compilation/ScopeCompilationContext.cs
  17. 14 0
      src/Lua/CodeAnalysis/SourcePosition.cs
  18. 500 0
      src/Lua/CodeAnalysis/Syntax/DisplayStringSyntaxVisitor.cs
  19. 38 0
      src/Lua/CodeAnalysis/Syntax/ISyntaxNodeVisitor.cs
  20. 61 0
      src/Lua/CodeAnalysis/Syntax/Keywords.cs
  21. 465 0
      src/Lua/CodeAnalysis/Syntax/Lexer.cs
  22. 30 0
      src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs
  23. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/AssignmentStatementNode.cs
  24. 54 0
      src/Lua/CodeAnalysis/Syntax/Nodes/BinaryExpressionNode.cs
  25. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/BooleanLiteralNode.cs
  26. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/BreakStatementNode.cs
  27. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionExpressionNode.cs
  28. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionStatementNode.cs
  29. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodExpressionNode.cs
  30. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodStatementNode.cs
  31. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/DoStatementNode.cs
  32. 3 0
      src/Lua/CodeAnalysis/Syntax/Nodes/ExpressionNode.cs
  33. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationExpressionNode.cs
  34. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationStatementNode.cs
  35. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/GenericForStatementNode.cs
  36. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/GotoStatementNode.cs
  37. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/IdentifierNode.cs
  38. 15 0
      src/Lua/CodeAnalysis/Syntax/Nodes/IfStatementNode.cs
  39. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/LabelStatementNode.cs
  40. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/LocalAssignmentStatementNode.cs
  41. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/LocalFunctionDeclarationNode.cs
  42. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/NilLiteralNode.cs
  43. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/NumericForStatementNode.cs
  44. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/NumericLiteralNode.cs
  45. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/RepeatStatementNode.cs
  46. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/ReturnStatementNode.cs
  47. 3 0
      src/Lua/CodeAnalysis/Syntax/Nodes/StatementNode.cs
  48. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/StringLiteralNode.cs
  49. 14 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableConstructorExpressionNode.cs
  50. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableIndexerAccessExpressionNode.cs
  51. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableMemberAccessExpressionNode.cs
  52. 30 0
      src/Lua/CodeAnalysis/Syntax/Nodes/UnaryExpressionNode.cs
  53. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/VariableArgumentsExpressionNode.cs
  54. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/WhileStatementNode.cs
  55. 49 0
      src/Lua/CodeAnalysis/Syntax/OperatorPrecedence.cs
  56. 968 0
      src/Lua/CodeAnalysis/Syntax/Parser.cs
  57. 6 0
      src/Lua/CodeAnalysis/Syntax/SyntaxNode.cs
  58. 358 0
      src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs
  59. 60 0
      src/Lua/CodeAnalysis/Syntax/SyntaxTokenEnumerator.cs
  60. 90 0
      src/Lua/Exceptions.cs
  61. 87 0
      src/Lua/Internal/AutoResizeArrayCore.cs
  62. 35 0
      src/Lua/Internal/EnumerableEx.cs
  63. 425 0
      src/Lua/Internal/FarmHash.cs
  64. 109 0
      src/Lua/Internal/FastListCore.cs
  65. 88 0
      src/Lua/Internal/FastStackCore.cs
  66. 51 0
      src/Lua/Internal/MathEx.cs
  67. 47 0
      src/Lua/Internal/PooledArray.cs
  68. 83 0
      src/Lua/Internal/PooledList.cs
  69. 20 0
      src/Lua/Internal/Utf16StringMemoryComparer.cs
  70. 19 0
      src/Lua/Lua.csproj
  71. 29 0
      src/Lua/LuaFunction.Create.cs
  72. 40 0
      src/Lua/LuaFunction.cs
  73. 16 0
      src/Lua/LuaFunctionExecutionContext.cs
  74. 109 0
      src/Lua/LuaState.cs
  75. 66 0
      src/Lua/LuaStateExtensions.cs
  76. 173 0
      src/Lua/LuaTable.cs
  77. 283 0
      src/Lua/LuaValue.cs
  78. 12 0
      src/Lua/Runtime/CallStackFrame.cs
  79. 23 0
      src/Lua/Runtime/Chunk.cs
  80. 46 0
      src/Lua/Runtime/Closure.cs
  81. 671 0
      src/Lua/Runtime/Instruction.cs
  82. 101 0
      src/Lua/Runtime/LuaStack.cs
  83. 39 0
      src/Lua/Runtime/LuaValueRuntimeExtensions.cs
  84. 1043 0
      src/Lua/Runtime/LuaVirtualMachine.cs
  85. 22 0
      src/Lua/Runtime/Metamethods.cs
  86. 61 0
      src/Lua/Runtime/OpCode.cs
  87. 24 0
      src/Lua/Runtime/Tracebacks.cs
  88. 70 0
      src/Lua/Runtime/UpValue.cs
  89. 9 0
      src/Lua/Runtime/UpValueInfo.cs
  90. 23 0
      src/Lua/Standard/Base/AssertFunction.cs
  91. 16 0
      src/Lua/Standard/Base/ErrorFunction.cs
  92. 39 0
      src/Lua/Standard/Base/GetMetatableFunction.cs
  93. 25 0
      src/Lua/Standard/Base/PrintFunction.cs
  94. 13 0
      src/Lua/Standard/Base/RawEqualFunction.cs
  95. 23 0
      src/Lua/Standard/Base/RawGetFunction.cs
  96. 25 0
      src/Lua/Standard/Base/RawSetFunction.cs
  97. 44 0
      src/Lua/Standard/Base/SetMetatableFunction.cs
  98. 39 0
      src/Lua/Standard/Base/ToStringFunction.cs
  99. 20 0
      src/Lua/Standard/OpenLibExtensions.cs
  100. 1 0
      tests/Lua.Tests/GlobalUsings.cs

+ 479 - 0
.gitignore

@@ -0,0 +1,479 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp

+ 52 - 0
Lua.sln

@@ -0,0 +1,52 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{18A64E25-9557-457B-80AE-A6EFE853118D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lua", "src\Lua\Lua.csproj", "{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{79458370-DD8A-48A4-B11E-8DF631520E8C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lua.Tests", "tests\Lua.Tests\Lua.Tests.csproj", "{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{33883F28-679F-48AD-8E64-3515C7BDAF5A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "sandbox\ConsoleApp1\ConsoleApp1.csproj", "{718A361C-AAF3-45A4-84D4-8C4FB6BB374E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "sandbox\Benchmark\Benchmark.csproj", "{FC157C29-8AAE-49C8-9536-208E3F0698DA}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36}.Release|Any CPU.Build.0 = Release|Any CPU
+		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D} = {18A64E25-9557-457B-80AE-A6EFE853118D}
+		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36} = {79458370-DD8A-48A4-B11E-8DF631520E8C}
+		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
+		{FC157C29-8AAE-49C8-9536-208E3F0698DA} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
+	EndGlobalSection
+EndGlobal

+ 88 - 0
sandbox/Benchmark/AddBenchmark.cs

@@ -0,0 +1,88 @@
+using System.Buffers;
+using BenchmarkDotNet.Attributes;
+using Lua;
+using MoonSharp.Interpreter;
+
+[Config(typeof(BenchmarkConfig))]
+public class AddBenchmark
+{
+    NLua.Lua nLuaState = default!;
+    Script moonSharpState = default!;
+    LuaState luaCSharpState = default!;
+
+    string filePath = default!;
+    string sourceText = default!;
+
+    [GlobalSetup]
+    public void GlobalSetup()
+    {
+        // moonsharp
+        moonSharpState = new Script();
+        Script.WarmUp();
+
+        // NLua
+        nLuaState = new();
+
+        // Lua-CSharp
+        luaCSharpState = LuaState.Create();
+
+        filePath = FileHelper.GetAbsolutePath("add.lua");
+        sourceText = File.ReadAllText(filePath);
+    }
+
+    [Benchmark(Description = "MoonSharp (RunString)")]
+    public DynValue Benchmark_MoonSharp_String()
+    {
+        var result = moonSharpState.DoString(sourceText);
+        return result;
+    }
+
+    [Benchmark(Description = "MoonSharp (RunFile)")]
+    public DynValue Benchmark_MoonSharp_File()
+    {
+        var result = moonSharpState.DoFile(filePath);
+        return result;
+    }
+
+    [Benchmark(Description = "NLua (DoString)")]
+    public object[] Benchmark_NLua_String()
+    {
+        return nLuaState.DoString(sourceText);
+    }
+
+    [Benchmark(Description = "NLua (DoFile)")]
+    public object[] Benchmark_NLua_File()
+    {
+        return nLuaState.DoFile(filePath);
+    }
+
+    [Benchmark(Description = "Lua-CSharp (DoString)")]
+    public async Task<LuaValue> Benchmark_LuaCSharp_String()
+    {
+        var buffer = ArrayPool<LuaValue>.Shared.Rent(1);
+        try
+        {
+            await luaCSharpState.DoStringAsync(sourceText, buffer);
+            return buffer[0];
+        }
+        finally
+        {
+            ArrayPool<LuaValue>.Shared.Return(buffer);
+        }
+    }
+
+    [Benchmark(Description = "Lua-CSharp (DoFileAsync)")]
+    public async Task<LuaValue> Benchmark_LuaCSharp_File()
+    {
+        var buffer = ArrayPool<LuaValue>.Shared.Rent(1);
+        try
+        {
+            await luaCSharpState.DoFileAsync(filePath, buffer);
+            return buffer[0];
+        }
+        finally
+        {
+            ArrayPool<LuaValue>.Shared.Return(buffer);
+        }
+    }
+}

+ 20 - 0
sandbox/Benchmark/Benchmark.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+    <PackageReference Include="MoonSharp" Version="2.0.0" />
+    <PackageReference Include="NLua" Version="1.7.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
+  </ItemGroup>
+
+</Project>

+ 35 - 0
sandbox/Benchmark/Benchmark.sln

@@ -0,0 +1,35 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "Benchmark.csproj", "{ECA61AB3-09C9-4990-8201-2E83BB9FD48B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bin", "bin", "{B33416D1-661B-461E-95CD-C3BCC7DDC23F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Release", "Release", "{0A15FAB8-2749-4DF1-B1B3-925E6CD29AB1}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net8.0", "net8.0", "{2308BEED-4D71-4415-B9AC-269272953282}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{ECA61AB3-09C9-4990-8201-2E83BB9FD48B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{ECA61AB3-09C9-4990-8201-2E83BB9FD48B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{ECA61AB3-09C9-4990-8201-2E83BB9FD48B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{ECA61AB3-09C9-4990-8201-2E83BB9FD48B}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{0A15FAB8-2749-4DF1-B1B3-925E6CD29AB1} = {B33416D1-661B-461E-95CD-C3BCC7DDC23F}
+		{2308BEED-4D71-4415-B9AC-269272953282} = {0A15FAB8-2749-4DF1-B1B3-925E6CD29AB1}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {F4ABD181-CD81-474C-93FF-CD4E6F93197A}
+	EndGlobalSection
+EndGlobal

+ 9 - 0
sandbox/Benchmark/FileHelper.cs

@@ -0,0 +1,9 @@
+using System.Runtime.CompilerServices;
+
+public static class FileHelper
+{
+    public static string GetAbsolutePath(string relativePath, [CallerFilePath] string callerFilePath = "")
+    {
+        return Path.Combine(Path.GetDirectoryName(callerFilePath)!, relativePath);
+    }
+}

+ 88 - 0
sandbox/Benchmark/InterpreterSteps.cs

@@ -0,0 +1,88 @@
+using BenchmarkDotNet.Attributes;
+using Lua;
+using Lua.CodeAnalysis.Compilation;
+using Lua.CodeAnalysis.Syntax;
+using Lua.Runtime;
+
+[Config(typeof(BenchmarkConfig))]
+public class InterpreterSteps
+{
+    string sourceText = default!;
+    LuaState state = default!;
+    SyntaxToken[] tokens = [];
+    LuaSyntaxTree ast = default!;
+    Chunk chunk = default!;
+    LuaValue[] results = new LuaValue[1];
+
+    [GlobalSetup]
+    public void GlobalSetup()
+    {
+        var filePath = FileHelper.GetAbsolutePath("add.lua");
+        sourceText = File.ReadAllText(filePath);
+
+        state = LuaState.Create();
+
+        var lexer = new Lexer
+        {
+            Source = sourceText.AsMemory()
+        };
+
+        var buffer = new List<SyntaxToken>();
+        while (lexer.MoveNext())
+        {
+            buffer.Add(lexer.Current);
+        }
+
+        tokens = buffer.ToArray();
+
+        var parser = new Parser();
+        foreach (var token in tokens)
+        {
+            parser.Add(token);
+        }
+
+        ast = parser.Parse();
+        chunk = LuaCompiler.Default.Compile(ast);
+    }
+
+    [Benchmark]
+    public void CreateState()
+    {
+        LuaState.Create();
+    }
+
+    [Benchmark]
+    public void Lexer()
+    {
+        var lexer = new Lexer
+        {
+            Source = sourceText.AsMemory()
+        };
+
+        while (lexer.MoveNext()) { }
+    }
+
+    [Benchmark]
+    public LuaSyntaxTree Parser()
+    {
+        var parser = new Parser();
+        foreach (var token in tokens)
+        {
+            parser.Add(token);
+        }
+
+        return parser.Parse();
+    }
+
+    [Benchmark]
+    public Chunk Compile()
+    {
+        return LuaCompiler.Default.Compile(ast);
+    }
+
+    [Benchmark]
+    public async ValueTask RunAsync()
+    {
+        await state.RunAsync(chunk, results);
+    }
+}

+ 19 - 0
sandbox/Benchmark/Program.cs

@@ -0,0 +1,19 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Running;
+using System.Reflection;
+
+BenchmarkSwitcher.FromAssembly(Assembly.GetEntryAssembly()!).Run(args);
+
+class BenchmarkConfig : ManualConfig
+{
+    public BenchmarkConfig()
+    {
+        AddDiagnoser(MemoryDiagnoser.Default);
+        AddJob(Job.ShortRun
+            .WithWarmupCount(10)
+            .WithIterationCount(10)
+        );
+    }
+}

+ 7 - 0
sandbox/Benchmark/add.lua

@@ -0,0 +1,7 @@
+local x = 0
+
+for i = 0, 10000 do
+    x = x + i;
+end
+
+return x

+ 14 - 0
sandbox/ConsoleApp1/ConsoleApp1.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
+  </ItemGroup>
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+</Project>

+ 97 - 0
sandbox/ConsoleApp1/Program.cs

@@ -0,0 +1,97 @@
+using Lua.CodeAnalysis.Syntax;
+using Lua.CodeAnalysis.Compilation;
+using Lua.Runtime;
+using Lua;
+using Lua.Standard;
+
+var state = LuaState.Create();
+state.OpenBaseLibrary();
+
+try
+{
+    var source =
+@"
+metatable = {
+    __add = function(a, b)
+        local t = { }
+
+        for i = 1, #a do
+            t[i] = a[i] + b[i]
+        end
+
+        return t
+    end
+}
+
+local a = { 1, 2, 3 }
+local b = { 4, 5, 6 }
+
+setmetatable(a, metatable)
+
+return a + b
+";
+
+    var syntaxTree = LuaSyntaxTree.Parse(source, "main.lua");
+
+    Console.WriteLine("Source Code " + new string('-', 50));
+
+    var debugger = new DisplayStringSyntaxVisitor();
+    Console.WriteLine(debugger.GetDisplayString(syntaxTree));
+
+    var chunk = LuaCompiler.Default.Compile(syntaxTree, "main.lua");
+
+    var id = 0;
+    DebugChunk(chunk, ref id);
+
+    Console.WriteLine("Output " + new string('-', 50));
+
+    var results = new LuaValue[64];
+    var resultCount = await state.RunAsync(chunk, results);
+
+    Console.WriteLine("Result " + new string('-', 50));
+
+    for (int i = 0; i < resultCount; i++)
+    {
+        Console.WriteLine(results[i]);
+    }
+
+    Console.WriteLine("End " + new string('-', 50));
+}
+catch (Exception ex)
+{
+    Console.WriteLine(ex);
+}
+
+static void DebugChunk(Chunk chunk, ref int id)
+{
+    Console.WriteLine($"Chunk[{id++}]" + new string('=', 50));
+
+    Console.WriteLine("Instructions " + new string('-', 50));
+    var index = 0;
+    foreach (var inst in chunk.Instructions.ToArray())
+    {
+        Console.WriteLine($"[{index}]\t{chunk.SourcePositions[index]}\t{inst}");
+        index++;
+    }
+
+    Console.WriteLine("Constants " + new string('-', 50)); index = 0;
+    foreach (var constant in chunk.Constants.ToArray())
+    {
+        Console.WriteLine($"[{index}]\t{constant}");
+        index++;
+    }
+
+    Console.WriteLine("UpValues " + new string('-', 50)); index = 0;
+    foreach (var upValue in chunk.UpValues.ToArray())
+    {
+        Console.WriteLine($"[{index}]\t{upValue.Name}\t{(upValue.IsInRegister ? 1 : 0)}\t{upValue.Index}");
+        index++;
+    }
+
+    Console.WriteLine();
+
+    foreach (var localChunk in chunk.Functions)
+    {
+        DebugChunk(localChunk, ref id);
+    }
+}

+ 58 - 0
sandbox/ConsoleApp1/vec3.lua

@@ -0,0 +1,58 @@
+Vec3 = {
+    new = function(x, y, z)
+        local instance = { x, y, z }
+        setmetatable(instance, Vec3)
+        return instance
+    end,
+
+    magnitude = function(self)
+        return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
+    end,
+
+    __index = function(table, index)
+        if index == 'x' then
+            return rawget(table, 1)
+        elseif index == 'y' then
+            return rawget(table, 2)
+        elseif index == 'z' then
+            return rawget(table, 3)
+        else
+            error('vec3 key must be x, y or z')
+        end
+    end,
+
+    __newindex = function(table, index, value)
+        if index == 'x' then
+            return rawset(table, 1, value)
+        elseif index == 'y' then
+            return rawset(table, 2, value)
+        elseif index == 'z' then
+            return rawset(table, 3, value)
+        else
+            error('vec3 key must be x, y or z')
+        end
+    end,
+
+    __add = function(a, b)
+        return Vec3.new(a.x + b.x, a.y + b.y, a.z + b.z)
+    end,
+
+    __sub = function(a, b)
+        return Vec3.new(a.x - b.x, a.y - b.y, a.z - b.z)
+    end,
+
+    __unm = function(a)
+        return Vec3.new(-a.x, -a.y, -a.z)
+    end,
+
+    __eq = function(a, b)
+        return a.x == b.y and a.y == b.y and a.z == b.z
+    end,
+
+    __tostring = function(self)
+        return '(' .. self.x .. ',' .. self.y .. ',' .. self.z .. ')'
+    end
+}
+
+local a = Vec3.new(1, 1, 1)
+local b = Vec3.new(1, 1, 1)

+ 35 - 0
src/Lua/CodeAnalysis/Compilation/Descriptions.cs

@@ -0,0 +1,35 @@
+using Lua.Internal;
+using Lua.Runtime;
+
+namespace Lua.CodeAnalysis.Compilation
+{
+    public readonly record struct LocalVariableDescription
+    {
+        public required byte RegisterIndex { get; init; }
+    }
+
+    public readonly record struct FunctionDescription
+    {
+        public required int Index { get; init; }
+        public required int? ReturnValueCount { get; init; }
+        public required Chunk Chunk { get; init; }
+    }
+
+    public readonly record struct LabelDescription
+    {
+        public required ReadOnlyMemory<char> Name { get; init; }
+        public required int Index { get; init; }
+        public required byte RegisterIndex { get; init; }
+    }
+
+    public readonly record struct GotoDescription
+    {
+        public required ReadOnlyMemory<char> Name { get; init; }
+        public required int JumpInstructionIndex { get; init; }
+    }
+
+    public record struct BreakDescription
+    {
+        public required int Index { get; set; }
+    }
+}

+ 321 - 0
src/Lua/CodeAnalysis/Compilation/FunctionCompilationContext.cs

@@ -0,0 +1,321 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Lua.Runtime;
+using Lua.Internal;
+
+namespace Lua.CodeAnalysis.Compilation;
+
+public class FunctionCompilationContext : IDisposable
+{
+    static class Pool
+    {
+        static readonly ConcurrentStack<FunctionCompilationContext> stack = new();
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static FunctionCompilationContext Rent()
+        {
+            if (!stack.TryPop(out var context))
+            {
+                context = new();
+            }
+
+            return context;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void Return(FunctionCompilationContext context)
+        {
+            context.Reset();
+            stack.Push(context);
+        }
+    }
+
+    internal static FunctionCompilationContext Create(ScopeCompilationContext? parentScope)
+    {
+        var context = Pool.Rent();
+        context.ParentScope = parentScope;
+        return context;
+    }
+
+    FunctionCompilationContext()
+    {
+        Scope = new()
+        {
+            Function = this
+        };
+    }
+
+    // instructions
+    FastListCore<Instruction> instructions;
+    FastListCore<SourcePosition> instructionPositions;
+
+    // constants
+    Dictionary<LuaValue, int> constantIndexMap = new(16);
+    FastListCore<LuaValue> constants;
+
+    // functions
+    Dictionary<ReadOnlyMemory<char>, int> functionMap = new(32, Utf16StringMemoryComparer.Default);
+    FastListCore<Chunk> functions;
+
+    // upvalues
+    FastListCore<UpValueInfo> upvalues;
+
+    // loop
+    FastListCore<BreakDescription> breakQueue;
+    FastListCore<GotoDescription> gotoQueue;
+
+    /// <summary>
+    /// Chunk name (for debug)
+    /// </summary>
+    public string? ChunkName { get; set; }
+
+    /// <summary>
+    /// Level of nesting of while, repeat, and for loops
+    /// </summary>
+    public int LoopLevel { get; set; }
+
+    /// <summary>
+    /// Number of parameters
+    /// </summary>
+    public int ParameterCount { get; set; }
+
+    /// <summary>
+    /// Weather the function has variable arguments
+    /// </summary>
+    public bool HasVariableArguments { get; set; }
+
+    /// <summary>
+    /// Parent scope context
+    /// </summary>
+    public ScopeCompilationContext? ParentScope { get; private set; }
+
+    /// <summary>
+    /// Top-level scope context
+    /// </summary>
+    public ScopeCompilationContext Scope { get; }
+
+    /// <summary>
+    /// Instructions
+    /// </summary>
+    public Span<Instruction> Instructions => instructions.AsSpan();
+
+    /// <summary>
+    /// Push the new instruction.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void PushInstruction(in Instruction instruction, in SourcePosition position)
+    {
+        instructions.Add(instruction);
+        instructionPositions.Add(position);
+    }
+
+    /// <summary>
+    /// Gets the index of the constant from the value, or if the constant is not registered it is added and its index is returned.
+    /// </summary>
+    public uint GetConstantIndex(in LuaValue value)
+    {
+        if (!constantIndexMap.TryGetValue(value, out var index))
+        {
+            index = constants.Length;
+
+            constants.Add(value);
+            constantIndexMap.Add(value, index);
+        }
+
+        return (uint)index;
+    }
+
+    public void AddOrSetFunctionProto(ReadOnlyMemory<char> name, Chunk chunk, out int index)
+    {
+        if (functionMap.TryGetValue(name, out index))
+        {
+            functions.AsSpan()[index] = chunk;
+        }
+        else
+        {
+            index = functions.Length;
+            functionMap.Add(name, functions.Length);
+            functions.Add(chunk);
+        }
+    }
+
+    public void AddFunctionProto(Chunk chunk, out int index)
+    {
+        index = functions.Length;
+        functions.Add(chunk);
+    }
+
+    public bool TryGetFunctionProto(ReadOnlyMemory<char> name, [NotNullWhen(true)] out Chunk? proto)
+    {
+        if (functionMap.TryGetValue(name, out var index))
+        {
+            proto = functions[index];
+            return true;
+        }
+        else
+        {
+            proto = null;
+            return false;
+        }
+    }
+
+    public void AddUpValue(UpValueInfo upValue)
+    {
+        upvalues.Add(upValue);
+    }
+
+    public bool TryGetUpValue(ReadOnlyMemory<char> name, out UpValueInfo description)
+    {
+        var span = upvalues.AsSpan();
+        for (int i = 0; i < span.Length; i++)
+        {
+            var info = span[i];
+            if (info.Name.Span.SequenceEqual(name.Span))
+            {
+                description = info;
+                return true;
+            }
+        }
+
+        if (ParentScope == null)
+        {
+            description = default;
+            return false;
+        }
+
+        if (ParentScope.TryGetLocalVariable(name, out var localVariable))
+        {
+            ParentScope.HasCapturedLocalVariables = true;
+
+            description = new()
+            {
+                Name = name,
+                Index = localVariable.RegisterIndex,
+                Id = upvalues.Length,
+                IsInRegister = true,
+            };
+            upvalues.Add(description);
+
+            return true;
+        }
+        else if (ParentScope.Function.TryGetUpValue(name, out var parentUpValue))
+        {
+            description = new()
+            {
+                Name = name,
+                Index = parentUpValue.Id,
+                Id = upvalues.Length,
+                IsInRegister = false,
+            };
+            upvalues.Add(description);
+
+            return true;
+        }
+
+        description = default;
+        return false;
+    }
+
+    public void AddUnresolvedBreak(BreakDescription description, SourcePosition sourcePosition)
+    {
+        if (LoopLevel == 0)
+        {
+            LuaParseException.BreakNotInsideALoop(ChunkName, sourcePosition);
+        }
+
+        breakQueue.Add(description);
+    }
+
+    public void ResolveAllBreaks(byte startPosition, int endPosition, ScopeCompilationContext loopScope)
+    {
+        foreach (var description in breakQueue.AsSpan())
+        {
+            ref var instruction = ref Instructions[description.Index];
+            if (loopScope.HasCapturedLocalVariables)
+            {
+                instruction.A = startPosition;
+            }
+            instruction.SBx = endPosition - description.Index;
+        }
+
+        breakQueue.Clear();
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void AddUnresolvedGoto(GotoDescription description)
+    {
+        gotoQueue.Add(description);
+    }
+
+    public void ResolveGoto(LabelDescription labelDescription)
+    {
+        for (int i = 0; i < gotoQueue.Length; i++)
+        {
+            var gotoDesc = gotoQueue[i];
+            if (gotoDesc.Name.Span.SequenceEqual(labelDescription.Name.Span))
+            {
+                instructions[gotoDesc.JumpInstructionIndex] = Instruction.Jmp(labelDescription.RegisterIndex, labelDescription.Index - gotoDesc.JumpInstructionIndex - 1);
+                gotoQueue.RemoveAtSwapback(i);
+                i--;
+            }
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Chunk ToChunk()
+    {
+        // add return
+        instructions.Add(Instruction.Return(0, 1));
+        instructionPositions.Add(instructionPositions.Length == 0 ? default : instructionPositions[^1]);
+
+        var chunk = new Chunk()
+        {
+            Name = ChunkName ?? "chunk",
+            Instructions = instructions.AsSpan().ToArray(),
+            SourcePositions = instructionPositions.AsSpan().ToArray(),
+            Constants = constants.AsSpan().ToArray(),
+            UpValues = upvalues.AsSpan().ToArray(),
+            Functions = functions.AsSpan().ToArray(),
+            ParameterCount = ParameterCount,
+            HasVariableArgments = HasVariableArguments,
+        };
+
+        foreach (var function in functions.AsSpan())
+        {
+            function.Parent = chunk;
+        }
+
+        return chunk;
+    }
+
+    /// <summary>
+    /// Resets the values ​​held in the context.
+    /// </summary>
+    public void Reset()
+    {
+        Scope.Reset();
+        instructions.Clear();
+        instructionPositions.Clear();
+        constantIndexMap.Clear();
+        constants.Clear();
+        upvalues.Clear();
+        functionMap.Clear();
+        functions.Clear();
+        breakQueue.Clear();
+        gotoQueue.Clear();
+        ChunkName = null;
+        LoopLevel = 0;
+        ParameterCount = 0;
+        HasVariableArguments = false;
+    }
+
+    /// <summary>
+    /// Returns the context object to the pool.
+    /// </summary>
+    public void Dispose()
+    {
+        ParentScope = null;
+        Pool.Return(this);
+    }
+}

+ 1062 - 0
src/Lua/CodeAnalysis/Compilation/LuaCompiler.cs

@@ -0,0 +1,1062 @@
+using Lua.Internal;
+using Lua.CodeAnalysis.Syntax;
+using Lua.CodeAnalysis.Syntax.Nodes;
+using Lua.Runtime;
+
+namespace Lua.CodeAnalysis.Compilation;
+
+public sealed class LuaCompiler : ISyntaxNodeVisitor<ScopeCompilationContext, bool>
+{
+    enum CallFunctionType
+    {
+        Expression,
+        Statement,
+        TailCall
+    }
+
+    public static readonly LuaCompiler Default = new();
+
+    /// <summary>
+    /// Returns a compiled chunk of the syntax tree.
+    /// </summary>
+    public Chunk Compile(LuaSyntaxTree syntaxTree, string? chunkName = null)
+    {
+        using var context = FunctionCompilationContext.Create(null);
+
+        // set global enviroment upvalue
+        context.AddUpValue(new()
+        {
+            Name = "_ENV".AsMemory(),
+            Id = 0,
+            Index = -1,
+            IsInRegister = false,
+        });
+
+        context.ChunkName = chunkName;
+
+        syntaxTree.Accept(this, context.Scope);
+        return context.ToChunk();
+    }
+
+    // Syntax Tree
+    public bool VisitSyntaxTree(LuaSyntaxTree node, ScopeCompilationContext context)
+    {
+        foreach (var childNode in node.Nodes)
+        {
+            childNode.Accept(this, context);
+        }
+
+        return true;
+    }
+
+    // Literals
+    public bool VisitNilLiteralNode(NilLiteralNode node, ScopeCompilationContext context)
+    {
+        context.PushInstruction(Instruction.LoadNil(context.StackPosition, 1), node.Position, true);
+        return true;
+    }
+
+    public bool VisitBooleanLiteralNode(BooleanLiteralNode node, ScopeCompilationContext context)
+    {
+        context.PushInstruction(Instruction.LoadBool(context.StackPosition, (ushort)(node.Value ? 1 : 0), 0), node.Position, true);
+        return true;
+    }
+
+    public bool VisitNumericLiteralNode(NumericLiteralNode node, ScopeCompilationContext context)
+    {
+        var index = context.Function.GetConstantIndex(node.Value);
+        context.PushInstruction(Instruction.LoadK(context.StackPosition, index), node.Position, true);
+        return true;
+    }
+
+    public bool VisitStringLiteralNode(StringLiteralNode node, ScopeCompilationContext context)
+    {
+        var index = context.Function.GetConstantIndex(node.Text);
+        context.PushInstruction(Instruction.LoadK(context.StackPosition, index), node.Position, true);
+        return true;
+    }
+
+    // identifier
+    public bool VisitIdentifierNode(IdentifierNode node, ScopeCompilationContext context)
+    {
+        LoadIdentifier(node.Name, context, node.Position, false);
+        return true;
+    }
+
+    // vararg
+    public bool VisitVariableArgumentsExpressionNode(VariableArgumentsExpressionNode node, ScopeCompilationContext context)
+    {
+        // TODO: optimize
+        context.PushInstruction(Instruction.VarArg(context.StackPosition, 0), node.Position, true);
+        return true;
+    }
+
+    // Unary/Binary expression
+    public bool VisitUnaryExpressionNode(UnaryExpressionNode node, ScopeCompilationContext context)
+    {
+        var b = context.StackPosition;
+        node.Node.Accept(this, context);
+
+        switch (node.Operator)
+        {
+            case UnaryOperator.Negate:
+                context.PushInstruction(Instruction.Unm(b, b), node.Position);
+                break;
+            case UnaryOperator.Not:
+                context.PushInstruction(Instruction.Not(b, b), node.Position);
+                break;
+            case UnaryOperator.Length:
+                context.PushInstruction(Instruction.Len(b, b), node.Position);
+                break;
+        }
+
+        return true;
+    }
+
+    public bool VisitBinaryExpressionNode(BinaryExpressionNode node, ScopeCompilationContext context)
+    {
+        var r = context.StackPosition;
+        (var b, var c) = GetBAndC(node, context);
+
+        switch (node.OperatorType)
+        {
+            case BinaryOperator.Addition:
+                context.PushInstruction(Instruction.Add(r, b, c), node.Position);
+                break;
+            case BinaryOperator.Subtraction:
+                context.PushInstruction(Instruction.Sub(r, b, c), node.Position);
+                break;
+            case BinaryOperator.Multiplication:
+                context.PushInstruction(Instruction.Mul(r, b, c), node.Position);
+                break;
+            case BinaryOperator.Division:
+                context.PushInstruction(Instruction.Div(r, b, c), node.Position);
+                break;
+            case BinaryOperator.Modulo:
+                context.PushInstruction(Instruction.Mod(r, b, c), node.Position);
+                break;
+            case BinaryOperator.Exponentiation:
+                context.PushInstruction(Instruction.Pow(r, b, c), node.Position);
+                break;
+            case BinaryOperator.Equality:
+                context.PushInstruction(Instruction.Eq(1, b, c), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 1, 1), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 0, 0), node.Position);
+                break;
+            case BinaryOperator.Inequality:
+                context.PushInstruction(Instruction.Eq(0, b, c), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 1, 1), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 0, 0), node.Position);
+                break;
+            case BinaryOperator.GreaterThan:
+                context.PushInstruction(Instruction.Le(0, b, c), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 1, 1), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 0, 0), node.Position);
+                break;
+            case BinaryOperator.GreaterThanOrEqual:
+                context.PushInstruction(Instruction.Lt(0, b, c), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 1, 1), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 0, 0), node.Position);
+                break;
+            case BinaryOperator.LessThan:
+                context.PushInstruction(Instruction.Lt(1, b, c), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 1, 1), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 0, 0), node.Position);
+                break;
+            case BinaryOperator.LessThanOrEqual:
+                context.PushInstruction(Instruction.Le(1, b, c), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 1, 1), node.Position);
+                context.PushInstruction(Instruction.LoadBool(r, 0, 0), node.Position);
+                break;
+            case BinaryOperator.Concat:
+                context.PushInstruction(Instruction.Concat(r, b, c), node.Position);
+                break;
+            case BinaryOperator.And:
+                context.PushInstruction(Instruction.TestSet(r, b, 0), node.Position);
+                context.PushInstruction(Instruction.Jmp(0, 1), node.Position);
+                context.PushInstruction(Instruction.Move(r, c), node.Position);
+                break;
+            case BinaryOperator.Or:
+                context.PushInstruction(Instruction.TestSet(r, b, 1), node.Position);
+                context.PushInstruction(Instruction.Jmp(0, 1), node.Position);
+                context.PushInstruction(Instruction.Move(r, c), node.Position);
+                break;
+        }
+
+        context.StackPosition = (byte)(r + 1);
+
+        return true;
+    }
+
+    // table
+    public bool VisitTableConstructorExpressionNode(TableConstructorExpressionNode node, ScopeCompilationContext context)
+    {
+        var tableRegisterIndex = context.StackPosition;
+        var newTableInstructionIndex = context.Function.Instructions.Length;
+        context.PushInstruction(Instruction.NewTable(tableRegisterIndex, 0, 0), node.Position, true);
+
+        var currentArrayChunkSize = 0;
+        ushort hashMapSize = 0;
+        ushort arrayBlock = 1;
+
+        ListTableConstructorField? lastField = null;
+        if (node.Fields.LastOrDefault() is ListTableConstructorField t)
+        {
+            lastField = t;
+        }
+
+        foreach (var group in node.Fields.GroupConsecutiveBy(x => x.GetType()))
+        {
+            foreach (var field in group)
+            {
+                var p = context.StackPosition;
+
+                switch (field)
+                {
+                    case ListTableConstructorField listItem:
+                        context.StackPosition = (byte)(p + currentArrayChunkSize - 50 * (arrayBlock - 1));
+
+                        listItem.Expression.Accept(this, context);
+
+                        // For the last element, we need to take into account variable arguments and multiple return values.
+                        if (listItem == lastField)
+                        {
+                            context.PushInstruction(Instruction.SetList(tableRegisterIndex, 0, arrayBlock), listItem.Position);
+                            currentArrayChunkSize = 0;
+                        }
+                        else
+                        {
+                            currentArrayChunkSize++;
+
+                            if (currentArrayChunkSize == 50)
+                            {
+                                context.PushInstruction(Instruction.SetList(tableRegisterIndex, 50, arrayBlock), listItem.Position);
+                                currentArrayChunkSize = 0;
+                                arrayBlock++;
+                            }
+                        }
+
+                        break;
+                    case RecordTableConstructorField recordItem:
+                        recordItem.ValueExpression.Accept(this, context);
+                        var keyConstIndex = context.Function.GetConstantIndex(recordItem.Key) + 256;
+
+                        context.PushInstruction(Instruction.SetTable(tableRegisterIndex, (ushort)keyConstIndex, p), recordItem.Position);
+                        hashMapSize++;
+                        break;
+                    case GeneralTableConstructorField generalItem:
+                        var keyIndex = context.StackPosition;
+                        generalItem.KeyExpression.Accept(this, context);
+                        var valueIndex = context.StackPosition;
+                        generalItem.ValueExpression.Accept(this, context);
+
+                        context.PushInstruction(Instruction.SetTable(tableRegisterIndex, keyIndex, valueIndex), generalItem.Position);
+                        hashMapSize++;
+                        break;
+                    default:
+                        throw new NotSupportedException();
+                }
+
+                context.StackPosition = p;
+            }
+        }
+
+        if (currentArrayChunkSize > 0)
+        {
+            context.PushInstruction(Instruction.SetList(tableRegisterIndex, (ushort)currentArrayChunkSize, arrayBlock), node.Position);
+        }
+
+        context.Function.Instructions[newTableInstructionIndex].B = (ushort)(currentArrayChunkSize + (arrayBlock - 1) * 50);
+        context.Function.Instructions[newTableInstructionIndex].C = hashMapSize;
+
+        return true;
+    }
+
+    public bool VisitTableIndexerAccessExpressionNode(TableIndexerAccessExpressionNode node, ScopeCompilationContext context)
+    {
+        // load table
+        var tablePosition = context.StackPosition;
+        node.TableNode.Accept(this, context);
+
+        // load key
+        node.KeyNode.Accept(this, context);
+
+        // push interuction
+        context.PushInstruction(Instruction.GetTable(tablePosition, tablePosition, (ushort)(context.StackPosition - 1)), node.Position);
+        context.StackPosition = (byte)(tablePosition + 1);
+
+        return true;
+    }
+
+    public bool VisitTableMemberAccessExpressionNode(TableMemberAccessExpressionNode node, ScopeCompilationContext context)
+    {
+        // load table
+        var tablePosition = context.StackPosition;
+        node.TableNode.Accept(this, context);
+
+        // load key
+        var keyIndex = context.Function.GetConstantIndex(node.MemberName) + 256;
+
+        // push interuction
+        context.PushInstruction(Instruction.GetTable(tablePosition, tablePosition, (ushort)keyIndex), node.Position);
+        context.StackPosition = (byte)(tablePosition + 1);
+
+        return true;
+    }
+
+    public bool VisitCallTableMethodExpressionNode(CallTableMethodExpressionNode node, ScopeCompilationContext context)
+    {
+        CompileTableMethod(node, context, CallFunctionType.Expression);
+        return true;
+    }
+
+    public bool VisitCallTableMethodStatementNode(CallTableMethodStatementNode node, ScopeCompilationContext context)
+    {
+        CompileTableMethod(node.Expression, context, CallFunctionType.Statement);
+        return true;
+    }
+
+    void CompileTableMethod(CallTableMethodExpressionNode node, ScopeCompilationContext context, CallFunctionType callType)
+    {
+        // load table
+        var tablePosition = context.StackPosition;
+        node.TableNode.Accept(this, context);
+
+        // load key
+        var keyIndex = context.Function.GetConstantIndex(node.MethodName) + 256;
+
+        // get closure
+        context.PushInstruction(Instruction.Self(tablePosition, tablePosition, (ushort)keyIndex), node.Position);
+        context.StackPosition = (byte)(tablePosition + 2);
+
+        // load arguments
+        foreach (var argument in node.ArgumentNodes)
+        {
+            argument.Accept(this, context);
+        }
+
+        var b = node.ArgumentNodes.Length + 2;
+        if (node.ArgumentNodes.Length > 0 && !IsFixedNumberOfReturnValues(node.ArgumentNodes[^1]))
+        {
+            b = 0;
+        }
+
+        // push call interuction
+        switch (callType)
+        {
+            case CallFunctionType.Expression:
+                context.PushInstruction(Instruction.Call(tablePosition, (ushort)b, 0), node.Position);
+                break;
+            case CallFunctionType.Statement:
+                context.PushInstruction(Instruction.Call(tablePosition, (ushort)b, 1), node.Position);
+                break;
+            case CallFunctionType.TailCall:
+                context.PushInstruction(Instruction.TailCall(tablePosition, (ushort)b, 0), node.Position);
+                break;
+        }
+        context.StackPosition = (byte)(tablePosition + 1);
+    }
+
+    // return
+    public bool VisitReturnStatementNode(ReturnStatementNode node, ScopeCompilationContext context)
+    {
+        ushort b;
+
+        // tail call
+        if (node.Nodes.Length == 1)
+        {
+            var lastNode = node.Nodes[^1];
+
+            if (lastNode is CallFunctionExpressionNode call)
+            {
+                CompileCallFunctionExpression(call, context, CallFunctionType.TailCall);
+                return true;
+            }
+            else if (lastNode is CallTableMethodExpressionNode callMethod)
+            {
+                CompileTableMethod(callMethod, context, CallFunctionType.TailCall);
+                return true;
+            }
+        }
+
+        b = node.Nodes.Length > 0 && !IsFixedNumberOfReturnValues(node.Nodes[^1])
+            ? (ushort)0
+            : (ushort)(node.Nodes.Length + 1);
+
+        foreach (var childNode in node.Nodes)
+        {
+            childNode.Accept(this, context);
+        }
+
+        context.PushInstruction(Instruction.Return((byte)(context.StackPosition - node.Nodes.Length), b), node.Position);
+
+        return true;
+    }
+
+    // assignment
+    public bool VisitLocalAssignmentStatementNode(LocalAssignmentStatementNode node, ScopeCompilationContext context)
+    {
+        for (int i = 0; i < node.Identifiers.Length; i++)
+        {
+            if (node.RightNodes.Length > i)
+            {
+                // Load initial values ​​for variables
+                var expression = node.RightNodes[i];
+                expression.Accept(this, context);
+
+                var identifier = node.Identifiers[i];
+
+                if (context.TryGetLocalVariableInThisScope(identifier.Name, out var variable))
+                {
+                    // assign local variable
+                    context.PushInstruction(Instruction.Move(variable.RegisterIndex, (ushort)(context.StackPosition - 1)), node.Position, true);
+                }
+                else
+                {
+                    // register local variable
+                    context.AddLocalVariable(identifier.Name, new()
+                    {
+                        RegisterIndex = (byte)(context.StackPosition - 1),
+                    });
+                }
+            }
+            else
+            {
+                // assigning nil to variables that do not have an initial value
+                var varCount = node.Identifiers.Length - i;
+                context.PushInstruction(Instruction.LoadNil(context.StackPosition, (ushort)varCount), node.Position);
+                context.StackPosition = (byte)(context.StackPosition + varCount);
+
+                // register local variables
+                for (int n = 0; n < varCount; n++)
+                {
+                    context.AddLocalVariable(node.Identifiers[i + n].Name, new()
+                    {
+                        RegisterIndex = (byte)(context.StackPosition + n - 1),
+                    });
+                }
+
+                break;
+            }
+        }
+        return true;
+    }
+
+    public bool VisitAssignmentStatementNode(AssignmentStatementNode node, ScopeCompilationContext context)
+    {
+        var startPosition = context.StackPosition;
+
+        for (int i = 0; i < node.LeftNodes.Length; i++)
+        {
+            var expression = node.RightNodes[i];
+            var leftNode = node.LeftNodes[i];
+
+            expression.Accept(this, context);
+
+            switch (leftNode)
+            {
+                case IdentifierNode identifier:
+                    {
+                        if (context.TryGetLocalVariable(identifier.Name, out var variable))
+                        {
+                            // assign local variable
+                            context.PushInstruction(Instruction.Move(variable.RegisterIndex, (ushort)(context.StackPosition - 1)), node.Position, true);
+                        }
+                        else if (context.Function.TryGetUpValue(identifier.Name, out var upValue))
+                        {
+                            // assign upvalue
+                            context.PushInstruction(Instruction.SetUpVal((byte)(context.StackPosition - 1), (ushort)upValue.Id), node.Position);
+                        }
+                        else if (context.TryGetLocalVariable("_ENV".AsMemory(), out variable))
+                        {
+                            // assign env element
+                            var index = context.Function.GetConstantIndex(identifier.Name.ToString()) + 256;
+                            context.PushInstruction(Instruction.SetTable(variable.RegisterIndex, (ushort)index, (ushort)(context.StackPosition - 1)), node.Position);
+                        }
+                        else
+                        {
+                            // assign global variable
+                            var index = context.Function.GetConstantIndex(identifier.Name.ToString()) + 256;
+                            context.PushInstruction(Instruction.SetTabUp(0, (ushort)index, (ushort)(context.StackPosition - 1)), node.Position);
+                        }
+                    }
+                    break;
+                case TableIndexerAccessExpressionNode tableIndexer:
+                    {
+                        var valueIndex = context.StackPosition - 1;
+                        tableIndexer.TableNode.Accept(this, context);
+                        var tableIndex = context.StackPosition - 1;
+                        tableIndexer.KeyNode.Accept(this, context);
+                        var keyIndex = context.StackPosition - 1;
+                        context.PushInstruction(Instruction.SetTable((byte)tableIndex, (ushort)keyIndex, (ushort)valueIndex), node.Position);
+                    }
+                    break;
+                case TableMemberAccessExpressionNode tableMember:
+                    {
+                        var valueIndex = context.StackPosition - 1;
+                        tableMember.TableNode.Accept(this, context);
+                        var tableIndex = context.StackPosition - 1;
+                        var keyIndex = context.Function.GetConstantIndex(tableMember.MemberName) + 256;
+                        context.PushInstruction(Instruction.SetTable((byte)tableIndex, (ushort)keyIndex, (ushort)valueIndex), node.Position);
+                    }
+                    break;
+                default:
+                    throw new LuaParseException(default, default, "An error occurred while parsing the code"); // TODO: add message
+            }
+        }
+
+        context.StackPosition = startPosition;
+
+        return true;
+    }
+
+    // function call
+    public bool VisitCallFunctionStatementNode(CallFunctionStatementNode node, ScopeCompilationContext context)
+    {
+        CompileCallFunctionExpression(node.Expression, context, CallFunctionType.Statement);
+        return true;
+    }
+
+    public bool VisitCallFunctionExpressionNode(CallFunctionExpressionNode node, ScopeCompilationContext context)
+    {
+        CompileCallFunctionExpression(node, context, CallFunctionType.Expression);
+        return true;
+    }
+
+    void CompileCallFunctionExpression(CallFunctionExpressionNode node, ScopeCompilationContext context, CallFunctionType callType)
+    {
+        // get closure
+        var r = context.StackPosition;
+        node.FunctionNode.Accept(this, context);
+
+        foreach (var argument in node.ArgumentNodes)
+        {
+            argument.Accept(this, context);
+        }
+
+        var b = node.ArgumentNodes.Length + 1;
+        if (node.ArgumentNodes.Length > 0 && !IsFixedNumberOfReturnValues(node.ArgumentNodes[^1]))
+        {
+            b = 0;
+        }
+
+        // push call interuction
+        switch (callType)
+        {
+            case CallFunctionType.Expression:
+                context.PushInstruction(Instruction.Call(r, (ushort)b, 0), node.Position);
+                break;
+            case CallFunctionType.Statement:
+                context.PushInstruction(Instruction.Call(r, (ushort)b, 1), node.Position);
+                break;
+            case CallFunctionType.TailCall:
+                context.PushInstruction(Instruction.TailCall(r, (ushort)b, 0), node.Position);
+                break;
+        }
+        context.StackPosition = (byte)(r + 1);
+    }
+
+    // function declaration
+    public bool VisitFunctionDeclarationExpressionNode(FunctionDeclarationExpressionNode node, ScopeCompilationContext context)
+    {
+        var funcIndex = CompileFunctionProto(ReadOnlyMemory<char>.Empty, context, node.ParameterNodes, node.Nodes, node.HasVariableArguments);
+
+        // push closure instruction
+        context.PushInstruction(Instruction.Closure(context.StackPosition, funcIndex), node.Position, true);
+
+        return true;
+    }
+
+    public bool VisitLocalFunctionDeclarationStatementNode(LocalFunctionDeclarationStatementNode node, ScopeCompilationContext context)
+    {
+        // assign local variable
+        context.AddLocalVariable(node.Name, new()
+        {
+            RegisterIndex = context.StackPosition,
+        });
+
+        // compile function
+        var funcIndex = CompileFunctionProto(node.Name, context, node.ParameterNodes, node.Nodes, node.HasVariableArguments);
+
+        // push closure instruction
+        context.PushInstruction(Instruction.Closure(context.StackPosition, funcIndex), node.Position, true);
+
+        return true;
+    }
+
+    public bool VisitFunctionDeclarationStatementNode(FunctionDeclarationStatementNode node, ScopeCompilationContext context)
+    {
+        var funcIndex = CompileFunctionProto(node.Name, context, node.ParameterNodes, node.Nodes, node.HasVariableArguments);
+
+        // add closure
+        var index = context.Function.GetConstantIndex(node.Name.ToString());
+
+        // push closure instruction
+        context.PushInstruction(Instruction.Closure(context.StackPosition, funcIndex), node.Position, true);
+
+        // assign global variable
+        context.PushInstruction(Instruction.SetTabUp(0, (ushort)(index + 256), (ushort)(context.StackPosition - 1)), node.Position);
+
+        return true;
+    }
+
+    int CompileFunctionProto(ReadOnlyMemory<char> functionName, ScopeCompilationContext context, IdentifierNode[] parameters, SyntaxNode[] statements, bool hasVarArg)
+    {
+        using var funcContext = context.CreateChildFunction();
+        funcContext.ChunkName = functionName.ToString();
+        funcContext.ParameterCount = parameters.Length;
+        funcContext.HasVariableArguments = hasVarArg;
+
+        // add arguments
+        for (int i = 0; i < parameters.Length; i++)
+        {
+            var parameter = parameters[i];
+            funcContext.Scope.AddLocalVariable(parameter.Name, new()
+            {
+                RegisterIndex = (byte)i
+            });
+        }
+
+        funcContext.Scope.StackPosition = (byte)parameters.Length;
+
+        foreach (var statement in statements)
+        {
+            statement.Accept(this, funcContext.Scope);
+        }
+
+        // compile function
+        var chunk = funcContext.ToChunk();
+
+        int index;
+        if (functionName.Length == 0)
+        {
+            // anonymous function
+            context.Function.AddFunctionProto(chunk, out index);
+        }
+        else
+        {
+            context.Function.AddOrSetFunctionProto(functionName, chunk, out index);
+        }
+
+        return index;
+    }
+
+    // control statements
+    public bool VisitDoStatementNode(DoStatementNode node, ScopeCompilationContext context)
+    {
+        using var scopeContext = context.CreateChildScope();
+
+        foreach (var childNode in node.StatementNodes)
+        {
+            childNode.Accept(this, scopeContext);
+        }
+
+        scopeContext.TryPushCloseUpValue(node.Position);
+
+        return true;
+    }
+
+    public bool VisitBreakStatementNode(BreakStatementNode node, ScopeCompilationContext context)
+    {
+        context.Function.AddUnresolvedBreak(new()
+        {
+            Index = context.Function.Instructions.Length
+        }, node.Position);
+        context.PushInstruction(Instruction.Jmp(0, 0), node.Position);
+
+        return true;
+    }
+
+    public bool VisitIfStatementNode(IfStatementNode node, ScopeCompilationContext context)
+    {
+        using var endJumpIndexList = new PooledList<int>(8);
+        var hasElse = node.ElseNodes.Length > 0;
+
+        // if
+        using (var scopeContext = context.CreateChildScope())
+        {
+            CompileConditionNode(node.IfNode.ConditionNode, scopeContext, true);
+
+            var ifPosition = scopeContext.Function.Instructions.Length;
+            scopeContext.PushInstruction(Instruction.Jmp(0, 0), node.Position);
+
+            foreach (var childNode in node.IfNode.ThenNodes)
+            {
+                childNode.Accept(this, scopeContext);
+            }
+
+            if (hasElse)
+            {
+                endJumpIndexList.Add(scopeContext.Function.Instructions.Length);
+                var a = scopeContext.HasCapturedLocalVariables ? scopeContext.StackTopPosition : (byte)0;
+                scopeContext.PushInstruction(Instruction.Jmp(a, 0), node.Position, true);
+            }
+            else
+            {
+                scopeContext.TryPushCloseUpValue(node.Position);
+            }
+
+            scopeContext.Function.Instructions[ifPosition].SBx = scopeContext.Function.Instructions.Length - 1 - ifPosition;
+        }
+
+        // elseif
+        foreach (var elseIf in node.ElseIfNodes)
+        {
+            using var scopeContext = context.CreateChildScope();
+
+            CompileConditionNode(elseIf.ConditionNode, scopeContext, true);
+
+            var elseifPosition = scopeContext.Function.Instructions.Length;
+            scopeContext.PushInstruction(Instruction.Jmp(0, 0), node.Position);
+
+            foreach (var childNode in elseIf.ThenNodes)
+            {
+                childNode.Accept(this, scopeContext);
+            }
+
+            // skip if node doesn't have else statements
+            if (hasElse)
+            {
+                endJumpIndexList.Add(scopeContext.Function.Instructions.Length);
+                var a = scopeContext.HasCapturedLocalVariables ? scopeContext.StackTopPosition : (byte)0;
+                scopeContext.PushInstruction(Instruction.Jmp(a, 0), node.Position);
+            }
+            else
+            {
+                scopeContext.TryPushCloseUpValue(node.Position);
+            }
+
+            scopeContext.Function.Instructions[elseifPosition].SBx = scopeContext.Function.Instructions.Length - 1 - elseifPosition;
+        }
+
+        // else nodes
+        using (var scopeContext = context.CreateChildScope())
+        {
+            foreach (var childNode in node.ElseNodes)
+            {
+                childNode.Accept(this, scopeContext);
+            }
+
+            scopeContext.TryPushCloseUpValue(node.Position);
+        }
+
+        // set JMP sBx
+        foreach (var index in endJumpIndexList.AsSpan())
+        {
+            context.Function.Instructions[index].SBx = context.Function.Instructions.Length - 1 - index;
+        }
+
+        return true;
+    }
+
+    public bool VisitRepeatStatementNode(RepeatStatementNode node, ScopeCompilationContext context)
+    {
+        var startIndex = context.Function.Instructions.Length;
+
+        context.Function.LoopLevel++;
+
+        using var scopeContext = context.CreateChildScope();
+
+        foreach (var childNode in node.Nodes)
+        {
+            childNode.Accept(this, scopeContext);
+        }
+
+        CompileConditionNode(node.ConditionNode, scopeContext, true);
+        var a = scopeContext.HasCapturedLocalVariables ? scopeContext.StackTopPosition : (byte)0;
+        scopeContext.PushInstruction(Instruction.Jmp(a, startIndex - scopeContext.Function.Instructions.Length - 1), node.Position);
+        scopeContext.TryPushCloseUpValue(node.Position);
+
+        context.Function.LoopLevel--;
+
+        // resolve break statements inside repeat block
+        context.Function.ResolveAllBreaks(context.StackPosition, context.Function.Instructions.Length - 1, scopeContext);
+
+        return true;
+    }
+
+    public bool VisitWhileStatementNode(WhileStatementNode node, ScopeCompilationContext context)
+    {
+        var conditionIndex = context.Function.Instructions.Length;
+        context.PushInstruction(Instruction.Jmp(0, 0), node.Position);
+
+        context.Function.LoopLevel++;
+
+        using var scopeContext = context.CreateChildScope();
+
+        foreach (var childNode in node.Nodes)
+        {
+            childNode.Accept(this, scopeContext);
+        }
+
+        context.Function.LoopLevel--;
+
+        // set JMP sBx
+        context.Function.Instructions[conditionIndex].SBx = context.Function.Instructions.Length - 1 - conditionIndex;
+
+        CompileConditionNode(node.ConditionNode, context, false);
+        var a = scopeContext.HasCapturedLocalVariables ? scopeContext.StackTopPosition : (byte)0;
+        context.PushInstruction(Instruction.Jmp(a, conditionIndex - context.Function.Instructions.Length), node.Position);
+
+        // resolve break statements inside while block
+        context.Function.ResolveAllBreaks(context.StackPosition, context.Function.Instructions.Length - 1, scopeContext);
+
+        return true;
+    }
+
+    public bool VisitNumericForStatementNode(NumericForStatementNode node, ScopeCompilationContext context)
+    {
+        var startPosition = context.StackPosition;
+
+        node.InitNode.Accept(this, context);
+        node.LimitNode.Accept(this, context);
+        if (node.StepNode != null)
+        {
+            node.StepNode.Accept(this, context);
+        }
+        else
+        {
+            var index = context.Function.GetConstantIndex(1);
+            context.PushInstruction(Instruction.LoadK(context.StackPosition, index), node.Position, true);
+        }
+
+        var prepIndex = context.Function.Instructions.Length;
+        context.PushInstruction(Instruction.ForPrep(startPosition, 0), node.Position, true);
+
+        // compile statements
+        context.Function.LoopLevel++;
+        using var scopeContext = context.CreateChildScope();
+        {
+            // add local variable
+            scopeContext.AddLocalVariable(node.VariableName, new()
+            {
+                RegisterIndex = startPosition
+            });
+
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, scopeContext);
+            }
+
+            scopeContext.TryPushCloseUpValue(node.Position);
+        }
+        context.Function.LoopLevel--;
+
+        // set ForPrep
+        context.Function.Instructions[prepIndex].SBx = context.Function.Instructions.Length - prepIndex - 1;
+
+        // push ForLoop
+        context.PushInstruction(Instruction.ForLoop(startPosition, prepIndex - context.Function.Instructions.Length), node.Position);
+
+        context.Function.ResolveAllBreaks(startPosition, context.Function.Instructions.Length - 1, scopeContext);
+
+        context.StackPosition = startPosition;
+
+        return true;
+    }
+
+    public bool VisitGenericForStatementNode(GenericForStatementNode node, ScopeCompilationContext context)
+    {
+        // get iterator
+        var startPosition = context.StackPosition;
+        node.ExpressionNode.Accept(this, context);
+
+        // jump to TFORCALL
+        var startJumpIndex = context.Function.Instructions.Length;
+        context.PushInstruction(Instruction.Jmp(0, 0), node.Position);
+
+        // compile statements
+        context.Function.LoopLevel++;
+        using var scopeContext = context.CreateChildScope();
+        {
+            scopeContext.StackPosition = (byte)(startPosition + 3 + node.Names.Length);
+
+            // add local variables
+            for (int i = 0; i < node.Names.Length; i++)
+            {
+                var name = node.Names[i];
+                scopeContext.AddLocalVariable(name.Name, new()
+                {
+                    RegisterIndex = (byte)(startPosition + 3 + i)
+                });
+            }
+
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, scopeContext);
+            }
+
+            scopeContext.TryPushCloseUpValue(node.Position);
+        }
+        context.Function.LoopLevel--;
+
+        // set jump
+        context.Function.Instructions[startJumpIndex].SBx = context.Function.Instructions.Length - startJumpIndex - 1;
+
+        // push OP_TFORCALL and OP_TFORLOOP
+        context.PushInstruction(Instruction.TForCall(startPosition, (ushort)node.Names.Length), node.Position);
+        context.PushInstruction(Instruction.TForLoop((byte)(startPosition + 2), startJumpIndex - context.Function.Instructions.Length), node.Position);
+
+        context.Function.ResolveAllBreaks(startPosition, context.Function.Instructions.Length - 1, scopeContext);
+        context.StackPosition = startPosition;
+
+        return true;
+    }
+
+    public bool VisitLabelStatementNode(LabelStatementNode node, ScopeCompilationContext context)
+    {
+        var desc = new LabelDescription()
+        {
+            Name = node.Name,
+            Index = context.Function.Instructions.Length,
+            RegisterIndex = context.StackPosition
+        };
+
+        context.AddLabel(desc);
+        context.Function.ResolveGoto(desc);
+
+        return true;
+    }
+
+    public bool VisitGotoStatementNode(GotoStatementNode node, ScopeCompilationContext context)
+    {
+        if (context.TryGetLabel(node.Name, out var description))
+        {
+            context.PushInstruction(Instruction.Jmp(description.RegisterIndex, description.Index - context.Function.Instructions.Length - 1), node.Position);
+        }
+        else
+        {
+            context.Function.AddUnresolvedGoto(new()
+            {
+                Name = node.Name,
+                JumpInstructionIndex = context.Function.Instructions.Length
+            });
+
+            // add uninitialized jmp instruction
+            context.PushInstruction(Instruction.Jmp(0, 0), node.Position);
+        }
+
+        return true;
+    }
+
+    static byte LoadIdentifier(ReadOnlyMemory<char> name, ScopeCompilationContext context, SourcePosition sourcePosition, bool dontLoadLocalVariable)
+    {
+        var p = context.StackPosition;
+
+        if (context.TryGetLocalVariable(name, out var variable))
+        {
+            if (dontLoadLocalVariable)
+            {
+                return variable.RegisterIndex;
+            }
+            else
+            {
+                context.PushInstruction(Instruction.Move(p, variable.RegisterIndex), sourcePosition, true);
+                return p;
+            }
+        }
+        else if (context.Function.TryGetUpValue(name, out var upValue))
+        {
+            context.PushInstruction(Instruction.GetUpVal(p, (ushort)upValue.Id), sourcePosition, true);
+            return p;
+        }
+        else if (context.TryGetLocalVariable("_ENV".AsMemory(), out variable))
+        {
+            var keyStringIndex = context.Function.GetConstantIndex(name.ToString()) + 256;
+            context.PushInstruction(Instruction.GetTable(p, variable.RegisterIndex, (ushort)keyStringIndex), sourcePosition, true);
+            return p;
+        }
+        else
+        {
+            context.Function.TryGetUpValue("_ENV".AsMemory(), out upValue);
+            var index = context.Function.GetConstantIndex(name.ToString()) + 256;
+            context.PushInstruction(Instruction.GetTabUp(p, (ushort)upValue.Id, (ushort)index), sourcePosition, true);
+            return p;
+        }
+    }
+
+    static bool IsFixedNumberOfReturnValues(ExpressionNode node)
+    {
+        return node is not (CallFunctionExpressionNode or CallTableMethodExpressionNode or VariableArgumentsExpressionNode);
+    }
+
+    /// <summary>
+    /// Compiles a conditional boolean branch: if true (or false), the next instruction added is skipped.
+    /// </summary>
+    /// <param name="node">Condition node</param>
+    /// <param name="context">Context</param>
+    /// <param name="falseIsSkip">If true, generates an instruction sequence that skips the next instruction if the condition is false.</param>
+    void CompileConditionNode(ExpressionNode node, ScopeCompilationContext context, bool falseIsSkip)
+    {
+        if (node is BinaryExpressionNode binaryExpression)
+        {
+            switch (binaryExpression.OperatorType)
+            {
+                case BinaryOperator.Equality:
+                    {
+                        (var b, var c) = GetBAndC(binaryExpression, context);
+                        context.PushInstruction(Instruction.Eq(falseIsSkip ? (byte)0 : (byte)1, b, c), node.Position);
+                        return;
+                    }
+                case BinaryOperator.Inequality:
+                    {
+                        (var b, var c) = GetBAndC(binaryExpression, context);
+                        context.PushInstruction(Instruction.Eq(falseIsSkip ? (byte)1 : (byte)0, b, c), node.Position);
+                        return;
+                    }
+                case BinaryOperator.LessThan:
+                    {
+                        (var b, var c) = GetBAndC(binaryExpression, context);
+                        context.PushInstruction(Instruction.Lt(falseIsSkip ? (byte)0 : (byte)1, b, c), node.Position);
+                        return;
+                    }
+                case BinaryOperator.LessThanOrEqual:
+                    {
+                        (var b, var c) = GetBAndC(binaryExpression, context);
+                        context.PushInstruction(Instruction.Le(falseIsSkip ? (byte)1 : (byte)0, b, c), node.Position);
+                        return;
+                    }
+                case BinaryOperator.GreaterThan:
+                    {
+                        (var b, var c) = GetBAndC(binaryExpression, context);
+                        context.PushInstruction(Instruction.Le(falseIsSkip ? (byte)1 : (byte)0, b, c), node.Position);
+                        return;
+                    }
+                case BinaryOperator.GreaterThanOrEqual:
+                    {
+                        (var b, var c) = GetBAndC(binaryExpression, context);
+                        context.PushInstruction(Instruction.Lt(falseIsSkip ? (byte)1 : (byte)0, b, c), node.Position);
+                        return;
+                    }
+            }
+        }
+
+        node.Accept(this, context);
+        context.PushInstruction(Instruction.Test((byte)(context.StackPosition - 1), falseIsSkip ? (byte)0 : (byte)1), node.Position);
+    }
+
+    (byte b, byte c) GetBAndC(BinaryExpressionNode node, ScopeCompilationContext context)
+    {
+        byte b, c;
+        if (node.LeftNode is IdentifierNode leftIdentifier)
+        {
+            b = LoadIdentifier(leftIdentifier.Name, context, leftIdentifier.Position, true);
+        }
+        else
+        {
+            node.LeftNode.Accept(this, context);
+            b = (byte)(context.StackPosition - 1);
+        }
+        if (node.RightNode is IdentifierNode rightIdentifier)
+        {
+            c = LoadIdentifier(rightIdentifier.Name, context, rightIdentifier.Position, true);
+        }
+        else
+        {
+            node.RightNode.Accept(this, context);
+            c = (byte)(context.StackPosition - 1);
+        }
+
+        return (b, c);
+    }
+}

+ 170 - 0
src/Lua/CodeAnalysis/Compilation/ScopeCompilationContext.cs

@@ -0,0 +1,170 @@
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using Lua.Internal;
+using Lua.Runtime;
+
+namespace Lua.CodeAnalysis.Compilation;
+
+public class ScopeCompilationContext : IDisposable
+{
+    static class Pool
+    {
+        static ConcurrentStack<ScopeCompilationContext> stack = new();
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static ScopeCompilationContext Rent()
+        {
+            if (!stack.TryPop(out var context))
+            {
+                context = new();
+            }
+
+            return context;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static void Return(ScopeCompilationContext context)
+        {
+            context.Reset();
+            stack.Push(context);
+        }
+    }
+
+    readonly Dictionary<ReadOnlyMemory<char>, LocalVariableDescription> localVariables = new(256, Utf16StringMemoryComparer.Default);
+    readonly Dictionary<ReadOnlyMemory<char>, LabelDescription> labels = new(32, Utf16StringMemoryComparer.Default);
+
+    public byte StackStartPosition { get; private set; }
+    public byte StackPosition { get; set; }
+
+    public byte StackTopPosition
+    {
+        get => (byte)(StackPosition - 1);
+    }
+
+    public bool HasCapturedLocalVariables { get; internal set; }
+
+    /// <summary>
+    /// Function context
+    /// </summary>
+    public FunctionCompilationContext Function { get; internal set; } = default!;
+
+    /// <summary>
+    /// Parent scope context
+    /// </summary>
+    public ScopeCompilationContext? Parent { get; private set; }
+
+    public ScopeCompilationContext CreateChildScope()
+    {
+        var childScope = Pool.Rent();
+        childScope.Parent = this;
+        childScope.Function = Function;
+        childScope.StackStartPosition = StackPosition;
+        childScope.StackPosition = StackPosition;
+        return childScope;
+    }
+
+    public FunctionCompilationContext CreateChildFunction()
+    {
+        var context = FunctionCompilationContext.Create(this);
+        return context;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void PushInstruction(in Instruction instruction, SourcePosition position, bool incrementStackPosition = false)
+    {
+        Function.PushInstruction(instruction, position);
+        if (incrementStackPosition) StackPosition++;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void TryPushCloseUpValue(SourcePosition position)
+    {
+        if (HasCapturedLocalVariables)
+        {
+            Function.PushInstruction(Instruction.Jmp(StackTopPosition, 0), position);
+        }
+    }
+
+    /// <summary>
+    /// Add new local variable.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void AddLocalVariable(ReadOnlyMemory<char> name, LocalVariableDescription description)
+    {
+        localVariables[name] = description;
+    }
+
+    /// <summary>
+    /// Gets the local variable in scope.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool TryGetLocalVariable(ReadOnlyMemory<char> name, out LocalVariableDescription description)
+    {
+        if (localVariables.TryGetValue(name, out description)) return true;
+
+        // Find local variables defined in the same function
+        if (Parent != null)
+        {
+            return Parent.TryGetLocalVariable(name, out description);
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Gets the local variable in this scope.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool TryGetLocalVariableInThisScope(ReadOnlyMemory<char> name, out LocalVariableDescription description)
+    {
+        return localVariables.TryGetValue(name, out description);
+    }
+
+    /// <summary>
+    /// Add new label.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void AddLabel(LabelDescription description)
+    {
+        labels.Add(description.Name, description);
+    }
+
+    /// <summary>
+    /// Gets the label in scope.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool TryGetLabel(ReadOnlyMemory<char> name, out LabelDescription description)
+    {
+        if (labels.TryGetValue(name, out description)) return true;
+
+        // Find labels defined in the same function
+        if (Parent != null)
+        {
+            return Parent.TryGetLabel(name, out description);
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Resets the values ​​held in the context.
+    /// </summary>
+    public void Reset()
+    {
+        Parent = null;
+        StackStartPosition = 0;
+        StackPosition = 0;
+        HasCapturedLocalVariables = false;
+        localVariables.Clear();
+        labels.Clear();
+    }
+
+    /// <summary>
+    /// Returns the context object to the pool.
+    /// </summary>
+    public void Dispose()
+    {
+        Function = null!;
+        Pool.Return(this);
+    }
+}

+ 14 - 0
src/Lua/CodeAnalysis/SourcePosition.cs

@@ -0,0 +1,14 @@
+namespace Lua.CodeAnalysis;
+
+public record struct SourcePosition
+{
+    public SourcePosition(int line, int column)
+    {
+        Line = line;
+        Column = column;
+    }
+
+    public int Line { get; set; }
+    public int Column { get; set; }
+    public override readonly string ToString() => $"({Line},{Column})";
+}

+ 500 - 0
src/Lua/CodeAnalysis/Syntax/DisplayStringSyntaxVisitor.cs

@@ -0,0 +1,500 @@
+using System.Text;
+using Lua.CodeAnalysis.Syntax;
+using Lua.CodeAnalysis.Syntax.Nodes;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public sealed class DisplayStringSyntaxVisitor : ISyntaxNodeVisitor<DisplayStringSyntaxVisitor.Context, bool>
+{
+    public sealed class Context
+    {
+        public readonly ref struct IndentScope
+        {
+            readonly Context source;
+
+            public IndentScope(Context source)
+            {
+                this.source = source;
+                source.IncreaseIndent();
+            }
+
+            public void Dispose()
+            {
+                source.DecreaseIndent();
+            }
+        }
+
+        readonly StringBuilder buffer = new();
+        int indentLevel;
+        bool isNewLine = true;
+
+        public IndentScope BeginIndentScope() => new(this);
+
+        public void Append(string value)
+        {
+            if (isNewLine)
+            {
+                buffer.Append(' ', indentLevel * 4);
+                isNewLine = false;
+            }
+            buffer.Append(value);
+        }
+
+        public void AppendLine(string value)
+        {
+            if (isNewLine)
+            {
+                buffer.Append(' ', indentLevel * 4);
+                isNewLine = false;
+            }
+
+            buffer.AppendLine(value);
+            isNewLine = true;
+        }
+
+        public void AppendLine()
+        {
+            buffer.AppendLine();
+            isNewLine = true;
+        }
+
+        public override string ToString() => buffer.ToString();
+
+        public void IncreaseIndent()
+        {
+            indentLevel++;
+        }
+
+        public void DecreaseIndent()
+        {
+            if (indentLevel > 0)
+                indentLevel--;
+        }
+
+        public void Reset()
+        {
+            buffer.Clear();
+            indentLevel = 0;
+            isNewLine = true;
+        }
+    }
+
+    readonly Context context = new();
+
+    public string GetDisplayString(SyntaxNode node)
+    {
+        context.Reset();
+        node.Accept(this, context);
+        return context.ToString();
+    }
+
+    public bool VisitBinaryExpressionNode(BinaryExpressionNode node, Context context)
+    {
+        context.Append("(");
+        node.LeftNode.Accept(this, context);
+        context.Append($" {node.OperatorType.ToDisplayString()} ");
+        node.RightNode.Accept(this, context);
+        context.Append(")");
+        return true;
+    }
+
+    public bool VisitBooleanLiteralNode(BooleanLiteralNode node, Context context)
+    {
+        context.Append(node.Value ? Keywords.True : Keywords.False);
+        return true;
+    }
+
+    public bool VisitBreakStatementNode(BreakStatementNode node, Context context)
+    {
+        context.Append(Keywords.Break);
+        return true;
+    }
+
+    public bool VisitCallFunctionExpressionNode(CallFunctionExpressionNode node, Context context)
+    {
+        node.FunctionNode.Accept(this, context);
+        context.Append("(");
+        AddStatementList(node.ArgumentNodes, context);
+        context.Append(")");
+        return true;
+    }
+
+    public bool VisitCallFunctionStatementNode(CallFunctionStatementNode node, Context context)
+    {
+        node.Expression.Accept(this, context);
+        return true;
+    }
+
+    public bool VisitDoStatementNode(DoStatementNode node, Context context)
+    {
+        context.AppendLine("do");
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitFunctionDeclarationExpressionNode(FunctionDeclarationExpressionNode node, Context context)
+    {
+        context.Append("function(");
+        AddStatementList(node.ParameterNodes, context);
+        if (node.HasVariableArguments)
+        {
+            if (node.ParameterNodes.Length > 0) context.Append(", ");
+            context.Append("...");
+        }
+        context.AppendLine(")");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitFunctionDeclarationStatementNode(FunctionDeclarationStatementNode node, Context context)
+    {
+        context.Append("function ");
+        context.Append(node.Name.ToString());
+        context.Append("(");
+        AddStatementList(node.ParameterNodes, context);
+        if (node.HasVariableArguments)
+        {
+            if (node.ParameterNodes.Length > 0) context.Append(", ");
+            context.Append("...");
+        }
+        context.AppendLine(")");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitGenericForStatementNode(GenericForStatementNode node, Context context)
+    {
+        context.Append($"for ");
+        AddStatementList(node.Names, context);
+        context.Append(" in ");
+        node.ExpressionNode.Accept(this, context);
+        context.AppendLine(" do");
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitGotoStatementNode(GotoStatementNode node, Context context)
+    {
+        context.Append($"goto {node.Name}");
+        return true;
+    }
+
+    public bool VisitIdentifierNode(IdentifierNode node, Context context)
+    {
+        context.Append(node.Name.ToString());
+        return true;
+    }
+
+    public bool VisitIfStatementNode(IfStatementNode node, Context context)
+    {
+        context.Append("if ");
+        node.IfNode.ConditionNode.Accept(this, context);
+        context.AppendLine(" then");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.IfNode.ThenNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        foreach (var elseif in node.ElseIfNodes)
+        {
+            context.Append("elseif ");
+            elseif.ConditionNode.Accept(this, context);
+            context.AppendLine(" then");
+
+            using (context.BeginIndentScope())
+            {
+                foreach (var childNode in elseif.ThenNodes)
+                {
+                    childNode.Accept(this, context);
+                    context.AppendLine();
+                }
+            }
+        }
+
+        if (node.ElseNodes.Length > 0)
+        {
+            context.AppendLine("else");
+
+            using (context.BeginIndentScope())
+            {
+                foreach (var childNode in node.ElseNodes)
+                {
+                    childNode.Accept(this, context);
+                    context.AppendLine();
+                }
+            }
+        }
+
+        context.Append("end");
+
+        return true;
+    }
+
+    public bool VisitLabelStatementNode(LabelStatementNode node, Context context)
+    {
+        context.Append($"::{node.Name}::");
+        return true;
+    }
+
+    public bool VisitAssignmentStatementNode(AssignmentStatementNode node, Context context)
+    {
+        AddStatementList(node.LeftNodes, context);
+
+        if (node.RightNodes.Length > 0)
+        {
+            context.Append(" = ");
+            AddStatementList(node.RightNodes, context);
+        }
+
+        return true;
+    }
+
+    public bool VisitLocalAssignmentStatementNode(LocalAssignmentStatementNode node, Context context)
+    {
+        context.Append("local ");
+        return VisitAssignmentStatementNode(node, context);
+    }
+
+    public bool VisitLocalFunctionDeclarationStatementNode(LocalFunctionDeclarationStatementNode node, Context context)
+    {
+        context.Append("local ");
+        return VisitFunctionDeclarationStatementNode(node, context);
+    }
+
+    public bool VisitNilLiteralNode(NilLiteralNode node, Context context)
+    {
+        context.Append(Keywords.Nil);
+        return true;
+    }
+
+    public bool VisitNumericForStatementNode(NumericForStatementNode node, Context context)
+    {
+        context.Append($"for {node.VariableName} = ");
+        node.InitNode.Accept(this, context);
+        context.Append(", ");
+        node.LimitNode.Accept(this, context);
+        if (node.StepNode != null)
+        {
+            context.Append(", ");
+            node.StepNode.Accept(this, context);
+        }
+
+        context.AppendLine(" do");
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitNumericLiteralNode(NumericLiteralNode node, Context context)
+    {
+        context.Append(node.Value.ToString());
+        return true;
+    }
+
+    public bool VisitRepeatStatementNode(RepeatStatementNode node, Context context)
+    {
+        context.AppendLine("repeat");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.Append("until ");
+        node.ConditionNode.Accept(this, context);
+        context.AppendLine();
+
+        return true;
+    }
+
+    public bool VisitReturnStatementNode(ReturnStatementNode node, Context context)
+    {
+        context.Append("return ");
+        AddStatementList(node.Nodes, context);
+        return true;
+    }
+
+    public bool VisitStringLiteralNode(StringLiteralNode node, Context context)
+    {
+        context.Append("\"");
+        context.Append(node.Text);
+        context.Append("\"");
+        return true;
+    }
+
+    public bool VisitSyntaxTree(LuaSyntaxTree node, Context context)
+    {
+        foreach (var statement in node.Nodes)
+        {
+            statement.Accept(this, context);
+            context.AppendLine();
+        }
+
+        return true;
+    }
+
+    public bool VisitTableConstructorExpressionNode(TableConstructorExpressionNode node, Context context)
+    {
+        context.AppendLine("{");
+        using (context.BeginIndentScope())
+        {
+            for (int i = 0; i < node.Fields.Length; i++)
+            {
+                var field = node.Fields[i];
+
+                switch (field)
+                {
+                    case GeneralTableConstructorField general:
+                        context.Append("[");
+                        general.KeyExpression.Accept(this, context);
+                        context.Append("] = ");
+                        general.ValueExpression.Accept(this, context);
+                        break;
+                    case RecordTableConstructorField record:
+                        context.Append($"{record.Key} = ");
+                        record.ValueExpression.Accept(this, context);
+                        break;
+                    case ListTableConstructorField list:
+                        list.Expression.Accept(this, context);
+                        break;
+                }
+
+                context.AppendLine(i == node.Fields.Length - 1 ? "" : ",");
+            }
+        }
+        context.AppendLine("}");
+
+        return true;
+    }
+
+    public bool VisitTableIndexerAccessExpressionNode(TableIndexerAccessExpressionNode node, Context context)
+    {
+        node.TableNode.Accept(this, context);
+        context.Append("[");
+        node.KeyNode.Accept(this, context);
+        context.Append("]");
+        return true;
+    }
+
+    public bool VisitTableMemberAccessExpressionNode(TableMemberAccessExpressionNode node, Context context)
+    {
+        node.TableNode.Accept(this, context);
+        context.Append($".{node.MemberName}");
+        return true;
+    }
+
+    public bool VisitCallTableMethodExpressionNode(CallTableMethodExpressionNode node, Context context)
+    {
+        node.TableNode.Accept(this, context);
+        context.Append($":{node.MethodName}(");
+        AddStatementList(node.ArgumentNodes, context);
+        context.Append(")");
+        return true;
+    }
+
+    public bool VisitCallTableMethodStatementNode(CallTableMethodStatementNode node, Context context)
+    {
+        return node.Expression.Accept(this, context);
+    }
+
+    public bool VisitUnaryExpressionNode(UnaryExpressionNode node, Context context)
+    {
+        context.Append(node.Operator.ToDisplayString());
+        if (node.Operator is UnaryOperator.Not) context.Append(" ");
+        node.Node.Accept(this, context);
+
+        return true;
+    }
+
+    public bool VisitWhileStatementNode(WhileStatementNode node, Context context)
+    {
+        context.Append("while ");
+        node.ConditionNode.Accept(this, context);
+        context.AppendLine(" do");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitVariableArgumentsExpressionNode(VariableArgumentsExpressionNode node, Context context)
+    {
+        context.Append("...");
+        return true;
+    }
+
+    void AddStatementList(SyntaxNode[] nodes, Context context)
+    {
+        for (int i = 0; i < nodes.Length; i++)
+        {
+            nodes[i].Accept(this, context);
+            if (i != nodes.Length - 1) context.Append(", ");
+        }
+    }
+}

+ 38 - 0
src/Lua/CodeAnalysis/Syntax/ISyntaxNodeVisitor.cs

@@ -0,0 +1,38 @@
+using Lua.CodeAnalysis.Syntax.Nodes;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public interface ISyntaxNodeVisitor<TContext, TResult>
+{
+    TResult VisitNumericLiteralNode(NumericLiteralNode node, TContext context);
+    TResult VisitBooleanLiteralNode(BooleanLiteralNode node, TContext context);
+    TResult VisitNilLiteralNode(NilLiteralNode node, TContext context);
+    TResult VisitStringLiteralNode(StringLiteralNode node, TContext context);
+    TResult VisitUnaryExpressionNode(UnaryExpressionNode node, TContext context);
+    TResult VisitBinaryExpressionNode(BinaryExpressionNode node, TContext context);
+    TResult VisitIdentifierNode(IdentifierNode node, TContext context);
+    TResult VisitDoStatementNode(DoStatementNode node, TContext context);
+    TResult VisitFunctionDeclarationExpressionNode(FunctionDeclarationExpressionNode node, TContext context);
+    TResult VisitFunctionDeclarationStatementNode(FunctionDeclarationStatementNode node, TContext context);
+    TResult VisitLocalFunctionDeclarationStatementNode(LocalFunctionDeclarationStatementNode node, TContext context);
+    TResult VisitWhileStatementNode(WhileStatementNode node, TContext context);
+    TResult VisitRepeatStatementNode(RepeatStatementNode node, TContext context);
+    TResult VisitIfStatementNode(IfStatementNode node, TContext context);
+    TResult VisitLabelStatementNode(LabelStatementNode node, TContext context);
+    TResult VisitGotoStatementNode(GotoStatementNode node, TContext context);
+    TResult VisitBreakStatementNode(BreakStatementNode node, TContext context);
+    TResult VisitReturnStatementNode(ReturnStatementNode node, TContext context);
+    TResult VisitAssignmentStatementNode(AssignmentStatementNode node, TContext context);
+    TResult VisitLocalAssignmentStatementNode(LocalAssignmentStatementNode node, TContext context);
+    TResult VisitCallFunctionExpressionNode(CallFunctionExpressionNode node, TContext context);
+    TResult VisitCallFunctionStatementNode(CallFunctionStatementNode node, TContext context);
+    TResult VisitNumericForStatementNode(NumericForStatementNode node, TContext context);
+    TResult VisitGenericForStatementNode(GenericForStatementNode node, TContext context);
+    TResult VisitTableConstructorExpressionNode(TableConstructorExpressionNode node, TContext context);
+    TResult VisitTableIndexerAccessExpressionNode(TableIndexerAccessExpressionNode node, TContext context);
+    TResult VisitTableMemberAccessExpressionNode(TableMemberAccessExpressionNode node, TContext context);
+    TResult VisitCallTableMethodExpressionNode(CallTableMethodExpressionNode node, TContext context);
+    TResult VisitCallTableMethodStatementNode(CallTableMethodStatementNode node, TContext context);
+    TResult VisitVariableArgumentsExpressionNode(VariableArgumentsExpressionNode node, TContext context);
+    TResult VisitSyntaxTree(LuaSyntaxTree node, TContext context);
+}

+ 61 - 0
src/Lua/CodeAnalysis/Syntax/Keywords.cs

@@ -0,0 +1,61 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+internal static class Keywords
+{
+    public const string LF = "\n";
+
+    public const string LParen = "(";
+    public const string RParen = ")";
+    public const string LCurly = "{";
+    public const string RCurly = "}";
+    public const string LSquare = "[";
+    public const string RSquare = "]";
+
+    public const string Assignment = "=";
+
+    public const string Nil = "nil";
+    public const string True = "true";
+    public const string False = "false";
+
+    public const string Addition = "+";
+    public const string Subtraction = "-";
+    public const string Multiplication = "*";
+    public const string Division = "/";
+    public const string Modulo = "%";
+    public const string Exponentiation = "^";
+
+    public const string Length = "#";
+    public const string Concat = "..";
+
+    public const string Equality = "==";
+    public const string Inequality = "~=";
+    public const string GreaterThan = ">";
+    public const string GreaterThanOrEqual = ">=";
+    public const string LessThan = "<";
+    public const string LessThanOrEqual = "<=";
+
+    public const string And = "and";
+    public const string Or = "or";
+    public const string Not = "not";
+
+    public const string Do = "do";
+    public const string End = "end";
+    public const string Then = "then";
+
+    public const string If = "if";
+    public const string ElseIf = "elseif";
+    public const string Else = "else";
+
+    public const string Return = "return";
+    public const string Break = "break";
+    public const string Goto = "goto";
+
+    public const string For = "for";
+    public const string In = "in";
+    public const string While = "while";
+    public const string Repeat = "repeat";
+    public const string Until = "until";
+
+    public const string Function = "function";
+    public const string Local = "local";
+}

+ 465 - 0
src/Lua/CodeAnalysis/Syntax/Lexer.cs

@@ -0,0 +1,465 @@
+using System.Runtime.CompilerServices;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public ref struct Lexer
+{
+    public required ReadOnlyMemory<char> Source { get; init; }
+    public string? ChunkName { get; init; }
+    
+    SyntaxToken current;
+    SourcePosition position = new(1, 0);
+    int offset;
+
+    public Lexer()
+    {
+    }
+
+    public readonly SyntaxToken Current => current;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void Advance(int count)
+    {
+        var span = Source.Span;
+        for (int i = 0; i < count; i++)
+        {
+            if (offset >= span.Length)
+            {
+                LuaParseException.SyntaxError(ChunkName, position, null);
+            }
+
+            var c = span[offset];
+            offset++;
+
+            var isLF = c is '\n';
+            var isCR = c is '\r' && (span.Length == offset || span[offset] is not '\n');
+
+            if (isLF || isCR)
+            {
+                position.Column = 0;
+                position.Line++;
+            }
+            else
+            {
+                position.Column++;
+            }
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    bool TryRead(int offset, out char value)
+    {
+        if (Source.Length <= offset)
+        {
+            value = default;
+            return false;
+        }
+
+        value = Source.Span[offset];
+        return true;
+    }
+
+    public bool MoveNext()
+    {
+        if (Source.Length <= offset) return false;
+
+        var span = Source.Span;
+        var startOffset = offset;
+        var position = this.position;
+
+        var c1 = span[offset];
+        Advance(1);
+        var c2 = span.Length == offset ? char.MinValue : span[offset];
+
+        switch (c1)
+        {
+            case ' ':
+            case '\t':
+                return MoveNext();
+            case '\n':
+                current = SyntaxToken.EndOfLine(position);
+                return true;
+            case '\r':
+                if (c2 == '\n') Advance(1);
+                current = SyntaxToken.EndOfLine(position);
+                return true;
+            case '(':
+                current = SyntaxToken.LParen(position);
+                return true;
+            case ')':
+                current = SyntaxToken.RParen(position);
+                return true;
+            case '{':
+                current = SyntaxToken.LCurly(position);
+                return true;
+            case '}':
+                current = SyntaxToken.RCurly(position);
+                return true;
+            case '[':
+                current = SyntaxToken.LSquare(position);
+                return true;
+            case ']':
+                current = SyntaxToken.RSquare(position);
+                return true;
+            case '+':
+                current = SyntaxToken.Addition(position);
+                return true;
+            case '-':
+                // comment
+                if (c2 == '-')
+                {
+                    Advance(1);
+
+                    // block comment
+                    if (TryRead(offset, out var c3) && c3 == '[' &&
+                        TryRead(offset, out var c4) && c4 == '[')
+                    {
+                        Advance(2);
+                        ReadUntilEndOfBlockComment(ref span, ref offset);
+                    }
+                    else // line comment
+                    {
+                        ReadUntilEOL(ref span, ref offset, out _);
+                    }
+
+                    return MoveNext();
+                }
+                else
+                {
+                    current = SyntaxToken.Subtraction(position);
+                    return true;
+                }
+            case '*':
+                current = SyntaxToken.Multiplication(position);
+                return true;
+            case '/':
+                current = SyntaxToken.Division(position);
+                return true;
+            case '%':
+                current = SyntaxToken.Modulo(position);
+                return true;
+            case '^':
+                current = SyntaxToken.Exponentiation(position);
+                return true;
+            case '=':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.Equality(position);
+                    Advance(1);
+                }
+                else
+                {
+                    current = SyntaxToken.Assignment(position);
+                }
+                return true;
+            case '~':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.Inequality(position);
+                    Advance(1);
+                }
+                else
+                {
+                    throw new LuaParseException(ChunkName, position, $"error: Invalid '~' token");
+                }
+                return true;
+            case '>':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.GreaterThanOrEqual(position);
+                    Advance(1);
+                }
+                else
+                {
+                    current = SyntaxToken.GreaterThan(position);
+                }
+                return true;
+            case '<':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.LessThanOrEqual(position);
+                    Advance(1);
+                }
+                else
+                {
+                    current = SyntaxToken.LessThan(position);
+                }
+                return true;
+            case '.':
+                if (c2 == '.')
+                {
+                    var c3 = span.Length == (offset + 1) ? char.MinValue : span[offset + 1];
+
+                    if (c3 == '.')
+                    {
+                        // vararg
+                        current = SyntaxToken.VarArg(position);
+                        Advance(2);
+                    }
+                    else
+                    {
+                        // concat
+                        current = SyntaxToken.Concat(position);
+                        Advance(1);
+                    }
+
+                    return true;
+                }
+
+                if (!IsNumeric(c2))
+                {
+                    current = SyntaxToken.Dot(position);
+                    return true;
+                }
+
+                break;
+            case '#':
+                current = SyntaxToken.Length(position);
+                return true;
+            case ',':
+                current = SyntaxToken.Comma(position);
+                return true;
+            case ';':
+                current = SyntaxToken.SemiColon(position);
+                return true;
+        }
+
+        // numeric literal
+        if (IsNumeric(c1))
+        {
+            if (c1 is '0' && c2 is 'x' or 'X') // hex 0x
+            {
+                Advance(1);
+                ReadDigit(ref span, ref offset, out var readCount);
+
+                if (readCount == 0)
+                {
+                    throw new LuaParseException(ChunkName, this.position, $"error: Illegal hexadecimal number");
+                }
+            }
+            else
+            {
+                ReadNumber(ref span, ref offset, out _);
+
+                if (span.Length > offset)
+                {
+                    var c = span[offset];
+
+                    if (c is '.')
+                    {
+                        Advance(1);
+                        ReadNumber(ref span, ref offset, out _);
+                    }
+                    else if (c is 'e' or 'E')
+                    {
+                        Advance(1);
+                        if (span[offset] is '-' or '+') Advance(1);
+
+                        ReadNumber(ref span, ref offset, out _);
+                    }
+                }
+            }
+
+            current = new(SyntaxTokenType.Number, Source[startOffset..offset], position);
+            return true;
+        }
+
+        // label
+        if (c1 is ':')
+        {
+            if (c2 is ':')
+            {
+                var stringStartOffset = offset + 1;
+                Advance(2);
+
+                var prevC = char.MinValue;
+
+                while (span.Length > offset)
+                {
+                    var c = span[offset];
+                    if (prevC == ':' && c == ':') break;
+
+                    Advance(1);
+                    prevC = c;
+                }
+
+                current = SyntaxToken.Label(Source[stringStartOffset..(offset - 1)], position);
+                Advance(1);
+            }
+            else
+            {
+                current = SyntaxToken.Colon(position);
+            }
+
+            return true;
+        }
+
+        // string
+        if (c1 is '"' or '\'')
+        {
+            var quote = c1;
+            var stringStartOffset = offset;
+            Advance(1);
+
+            while (span.Length > offset)
+            {
+                var c = span[offset];
+                if (c == quote) break;
+
+                if (c is '\n' or '\r')
+                {
+                    throw new LuaParseException(ChunkName, this.position, "error: Unterminated string");
+                }
+
+                // if (c is '\\')
+                // {
+
+                // }
+
+                Advance(1);
+            }
+
+            current = SyntaxToken.String(Source[stringStartOffset..offset], position);
+            Advance(1);
+            return true;
+        }
+
+        // identifier
+        if (IsIdentifier(c1))
+        {
+            while (span.Length > offset && IsIdentifier(span[offset]))
+            {
+                Advance(1);
+            }
+
+            var identifier = Source[startOffset..offset];
+
+            current = identifier.Span switch
+            {
+                Keywords.Nil => SyntaxToken.Nil(position),
+                Keywords.True => SyntaxToken.True(position),
+                Keywords.False => SyntaxToken.False(position),
+                Keywords.And => SyntaxToken.And(position),
+                Keywords.Or => SyntaxToken.Or(position),
+                Keywords.Not => SyntaxToken.Not(position),
+                Keywords.End => SyntaxToken.End(position),
+                Keywords.Then => SyntaxToken.Then(position),
+                Keywords.If => SyntaxToken.If(position),
+                Keywords.ElseIf => SyntaxToken.ElseIf(position),
+                Keywords.Else => SyntaxToken.Else(position),
+                Keywords.Local => SyntaxToken.Local(position),
+                Keywords.Return => SyntaxToken.Return(position),
+                Keywords.Goto => SyntaxToken.Goto(position),
+                Keywords.Do => SyntaxToken.Do(position),
+                Keywords.In => SyntaxToken.In(position),
+                Keywords.While => SyntaxToken.While(position),
+                Keywords.Repeat => SyntaxToken.Repeat(position),
+                Keywords.For => SyntaxToken.For(position),
+                Keywords.Until => SyntaxToken.Until(position),
+                Keywords.Break => SyntaxToken.Break(position),
+                Keywords.Function => SyntaxToken.Function(position),
+                _ => new(SyntaxTokenType.Identifier, identifier, position),
+            };
+
+            return true;
+        }
+
+        throw new LuaParseException(ChunkName, position, $"unexpected symbol near '{c1}'");
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadUntilEOL(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
+    {
+        readCount = 0;
+        var flag = true;
+        while (flag)
+        {
+            if (span.Length <= offset) return;
+
+            var c1 = span[offset];
+
+            if (c1 is '\n')
+            {
+                flag = false;
+            }
+            else if (c1 is '\r')
+            {
+                var c2 = span.Length == offset + 1 ? char.MinValue : span[offset + 1];
+                if (c2 is '\n')
+                {
+                    Advance(1);
+                    readCount++;
+                }
+                flag = false;
+            }
+
+            Advance(1);
+            readCount++;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadUntilEndOfBlockComment(ref ReadOnlySpan<char> span, ref int offset)
+    {
+        var start = position;
+
+        while (span.Length > offset + 1)
+        {
+            if (span[offset] is ']' &&
+                span[offset + 1] is ']')
+            {
+                Advance(2);
+                return;
+            }
+
+            Advance(1);
+        }
+
+        LuaParseException.UnfinishedLongComment(ChunkName, start);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadDigit(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
+    {
+        readCount = 0;
+        while (span.Length > offset && IsDigit(span[offset]))
+        {
+            Advance(1);
+            readCount++;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadNumber(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
+    {
+        readCount = 0;
+        while (span.Length > offset && IsNumeric(span[offset]))
+        {
+            Advance(1);
+            readCount++;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static bool IsDigit(char c)
+    {
+        return IsNumeric(c) ||
+            ('a' <= c && c <= 'f') ||
+            ('A' <= c && c <= 'F');
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static bool IsNumeric(char c)
+    {
+        return '0' <= c && c <= '9';
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static bool IsIdentifier(char c)
+    {
+        return c == '_' ||
+            ('A' <= c && c <= 'Z') ||
+            ('a' <= c && c <= 'z') ||
+            IsNumeric(c);
+    }
+}

+ 30 - 0
src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs

@@ -0,0 +1,30 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+public record LuaSyntaxTree(SyntaxNode[] Nodes) : SyntaxNode(new SourcePosition(0, 0))
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitSyntaxTree(this, context);
+    }
+
+    public static LuaSyntaxTree Parse(string source, string? chunkName = null)
+    {
+        var lexer = new Lexer
+        {
+            Source = source.AsMemory(),
+            ChunkName = chunkName,
+        };
+
+        var parser = new Parser
+        {
+            ChunkName = chunkName
+        };
+
+        while (lexer.MoveNext())
+        {
+            parser.Add(lexer.Current);
+        }
+
+        return parser.Parse();
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/AssignmentStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record AssignmentStatementNode(SyntaxNode[] LeftNodes, ExpressionNode[] RightNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitAssignmentStatementNode(this, context);
+    }
+}

+ 54 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/BinaryExpressionNode.cs

@@ -0,0 +1,54 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public enum BinaryOperator
+{
+    Addition,
+    Subtraction,
+    Multiplication,
+    Division,
+    Modulo,
+    Exponentiation,
+    Equality,
+    Inequality,
+    GreaterThan,
+    GreaterThanOrEqual,
+    LessThan,
+    LessThanOrEqual,
+    And,
+    Or,
+    Concat,
+}
+
+internal static class BinaryOperatorEx
+{
+    public static string ToDisplayString(this BinaryOperator @operator)
+    {
+        return @operator switch
+        {
+            BinaryOperator.Addition => Keywords.Addition,
+            BinaryOperator.Subtraction => Keywords.Subtraction,
+            BinaryOperator.Multiplication => Keywords.Multiplication,
+            BinaryOperator.Division => Keywords.Division,
+            BinaryOperator.Modulo => Keywords.Modulo,
+            BinaryOperator.Exponentiation => Keywords.Exponentiation,
+            BinaryOperator.Equality => Keywords.Equality,
+            BinaryOperator.Inequality => Keywords.Inequality,
+            BinaryOperator.GreaterThan => Keywords.GreaterThan,
+            BinaryOperator.GreaterThanOrEqual => Keywords.GreaterThanOrEqual,
+            BinaryOperator.LessThan => Keywords.LessThan,
+            BinaryOperator.LessThanOrEqual => Keywords.LessThanOrEqual,
+            BinaryOperator.And => Keywords.And,
+            BinaryOperator.Or => Keywords.Or,
+            BinaryOperator.Concat => Keywords.Concat,
+            _ => "",
+        };
+    }
+}
+
+public record BinaryExpressionNode(BinaryOperator OperatorType, ExpressionNode LeftNode, ExpressionNode RightNode, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitBinaryExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/BooleanLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record BooleanLiteralNode(bool Value, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitBooleanLiteralNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/BreakStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record BreakStatementNode(SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitBreakStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallFunctionExpressionNode(ExpressionNode FunctionNode, ExpressionNode[] ArgumentNodes) : ExpressionNode(FunctionNode.Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallFunctionExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallFunctionStatementNode(CallFunctionExpressionNode Expression) : StatementNode(Expression.Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallFunctionStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallTableMethodExpressionNode(ExpressionNode TableNode, string MethodName, ExpressionNode[] ArgumentNodes, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallTableMethodExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallTableMethodStatementNode(CallTableMethodExpressionNode Expression) : StatementNode(Expression.Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallTableMethodStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/DoStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record DoStatementNode(StatementNode[] StatementNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitDoStatementNode(this, context);
+    }
+}

+ 3 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/ExpressionNode.cs

@@ -0,0 +1,3 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public abstract record ExpressionNode(SourcePosition Position) : SyntaxNode(Position);

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record FunctionDeclarationExpressionNode(IdentifierNode[] ParameterNodes, SyntaxNode[] Nodes, bool HasVariableArguments, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitFunctionDeclarationExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record FunctionDeclarationStatementNode(ReadOnlyMemory<char> Name, IdentifierNode[] ParameterNodes, SyntaxNode[] Nodes, bool HasVariableArguments, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitFunctionDeclarationStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/GenericForStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record GenericForStatementNode(IdentifierNode[] Names, ExpressionNode ExpressionNode, StatementNode[] StatementNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitGenericForStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/GotoStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record GotoStatementNode(ReadOnlyMemory<char> Name, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitGotoStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/IdentifierNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record IdentifierNode(ReadOnlyMemory<char> Name, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitIdentifierNode(this, context);
+    }
+}

+ 15 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/IfStatementNode.cs

@@ -0,0 +1,15 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record IfStatementNode(IfStatementNode.ConditionAndThenNodes IfNode, IfStatementNode.ConditionAndThenNodes[] ElseIfNodes, StatementNode[] ElseNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public record ConditionAndThenNodes
+    {
+        public required ExpressionNode ConditionNode;
+        public required StatementNode[] ThenNodes;
+    }
+
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitIfStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/LabelStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record LabelStatementNode(ReadOnlyMemory<char> Name, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitLabelStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/LocalAssignmentStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record LocalAssignmentStatementNode(IdentifierNode[] Identifiers, ExpressionNode[] RightNodes, SourcePosition Position) : AssignmentStatementNode(Identifiers, RightNodes, Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitLocalAssignmentStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/LocalFunctionDeclarationNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record LocalFunctionDeclarationStatementNode(ReadOnlyMemory<char> Name, IdentifierNode[] ParameterNodes, SyntaxNode[] Nodes, bool HasVariableArguments, SourcePosition Position) : FunctionDeclarationStatementNode(Name, ParameterNodes, Nodes, HasVariableArguments, Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitLocalFunctionDeclarationStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/NilLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record NilLiteralNode(SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitNilLiteralNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/NumericForStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record NumericForStatementNode(ReadOnlyMemory<char> VariableName, ExpressionNode InitNode, ExpressionNode LimitNode, ExpressionNode? StepNode, StatementNode[] StatementNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitNumericForStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/NumericLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record NumericLiteralNode(double Value, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitNumericLiteralNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/RepeatStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record RepeatStatementNode(ExpressionNode ConditionNode, SyntaxNode[] Nodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitRepeatStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/ReturnStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record ReturnStatementNode(ExpressionNode[] Nodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitReturnStatementNode(this, context);
+    }
+}

+ 3 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/StatementNode.cs

@@ -0,0 +1,3 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public abstract record StatementNode(SourcePosition Position) : SyntaxNode(Position);

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/StringLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record StringLiteralNode(string Text, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitStringLiteralNode(this, context);
+    }
+}

+ 14 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableConstructorExpressionNode.cs

@@ -0,0 +1,14 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableConstructorExpressionNode(TableConstructorField[] Fields, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableConstructorExpressionNode(this, context);
+    }
+}
+
+public abstract record TableConstructorField(SourcePosition Position);
+public record GeneralTableConstructorField(ExpressionNode KeyExpression, ExpressionNode ValueExpression, SourcePosition Position) : TableConstructorField(Position);
+public record RecordTableConstructorField(string Key, ExpressionNode ValueExpression, SourcePosition Position) : TableConstructorField(Position);
+public record ListTableConstructorField(ExpressionNode Expression, SourcePosition Position) : TableConstructorField(Position);

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableIndexerAccessExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableIndexerAccessExpressionNode(ExpressionNode TableNode, ExpressionNode KeyNode, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableIndexerAccessExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableMemberAccessExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableMemberAccessExpressionNode(ExpressionNode TableNode, string MemberName, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableMemberAccessExpressionNode(this, context);
+    }
+}

+ 30 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/UnaryExpressionNode.cs

@@ -0,0 +1,30 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public enum UnaryOperator
+{
+    Negate,
+    Not,
+    Length,
+}
+
+public record UnaryExpressionNode(UnaryOperator Operator, ExpressionNode Node, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitUnaryExpressionNode(this, context);
+    }
+}
+
+internal static class UnaryOperatorEx
+{
+    public static string ToDisplayString(this UnaryOperator @operator)
+    {
+        return @operator switch
+        {
+            UnaryOperator.Negate => Keywords.Subtraction,
+            UnaryOperator.Not => Keywords.Not,
+            UnaryOperator.Length => Keywords.Length,
+            _ => "",
+        };
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/VariableArgumentsExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record VariableArgumentsExpressionNode(SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitVariableArgumentsExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/WhileStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record WhileStatementNode(ExpressionNode ConditionNode, SyntaxNode[] Nodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitWhileStatementNode(this, context);
+    }
+}

+ 49 - 0
src/Lua/CodeAnalysis/Syntax/OperatorPrecedence.cs

@@ -0,0 +1,49 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+public enum OperatorPrecedence
+{
+    /// <summary>
+    /// Non-operator token precedence
+    /// </summary>
+    NonOperator,
+
+    /// <summary>
+    /// 'or' operator
+    /// </summary>
+    Or,
+
+    /// <summary>
+    /// 'and' operator
+    /// </summary>
+    And,
+
+    /// <summary>
+    /// Relational operators (&lt;, &lt;=, &gt;, &gt;=, ==, ~=)
+    /// </summary>
+    Relational,
+
+    /// <summary>
+    /// Concat operator (..)
+    /// </summary>
+    Concat,
+
+    /// <summary>
+    /// Addition and Subtraction (+, -)
+    /// </summary>
+    Addition,
+
+    /// <summary>
+    /// Multipilcation, Division and Modulo (*, /, %)
+    /// </summary>
+    Multiplication,
+
+    /// <summary>
+    /// Negate, Not, Length (-, 'not', #)
+    /// </summary>
+    Unary,
+
+    /// <summary>
+    /// Exponentiation ()
+    /// </summary>
+    Exponentiation,
+}

+ 968 - 0
src/Lua/CodeAnalysis/Syntax/Parser.cs

@@ -0,0 +1,968 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Lua.Internal;
+using Lua.CodeAnalysis.Syntax.Nodes;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public ref struct Parser
+{
+    public string? ChunkName { get; init; }
+
+    PooledList<SyntaxToken> tokens;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Add(SyntaxToken token) => tokens.Add(token);
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Dispose()
+    {
+        tokens.Dispose();
+    }
+
+    public LuaSyntaxTree Parse()
+    {
+        using var root = new PooledList<SyntaxNode>(64);
+
+        var enumerator = new SyntaxTokenEnumerator(tokens.AsSpan());
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            root.Add(node);
+        }
+
+        var tree = new LuaSyntaxTree(root.AsSpan().ToArray());
+        Dispose();
+
+        return tree;
+    }
+
+    StatementNode ParseStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        switch (enumerator.Current.Type)
+        {
+            case SyntaxTokenType.Identifier:
+                {
+                    var firstExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+
+                    switch (firstExpression)
+                    {
+                        case CallFunctionExpressionNode callFunctionExpression:
+                            return new CallFunctionStatementNode(callFunctionExpression);
+                        case CallTableMethodExpressionNode callTableMethodExpression:
+                            return new CallTableMethodStatementNode(callTableMethodExpression);
+                        default:
+                            if (enumerator.GetNext(true).Type is SyntaxTokenType.Comma or SyntaxTokenType.Assignment)
+                            {
+                                // skip ','
+                                MoveNextWithValidation(ref enumerator);
+                                enumerator.SkipEoL();
+
+                                return ParseAssignmentStatement(firstExpression, ref enumerator);
+                            }
+                            break;
+                    }
+                }
+                break;
+            case SyntaxTokenType.Return:
+                return ParseReturnStatement(ref enumerator);
+            case SyntaxTokenType.Do:
+                return ParseDoStatement(ref enumerator);
+            case SyntaxTokenType.Goto:
+                return ParseGotoStatement(ref enumerator);
+            case SyntaxTokenType.Label:
+                return new LabelStatementNode(enumerator.Current.Text, enumerator.Current.Position);
+            case SyntaxTokenType.If:
+                return ParseIfStatement(ref enumerator);
+            case SyntaxTokenType.While:
+                return ParseWhileStatement(ref enumerator);
+            case SyntaxTokenType.Repeat:
+                return ParseRepeatStatement(ref enumerator);
+            case SyntaxTokenType.For:
+                {
+                    // skip 'for' keyword
+                    var forToken = enumerator.Current;
+                    MoveNextWithValidation(ref enumerator);
+                    enumerator.SkipEoL();
+
+                    if (enumerator.GetNext(true).Type is SyntaxTokenType.Assignment)
+                    {
+                        return ParseNumericForStatement(ref enumerator, forToken);
+                    }
+                    else
+                    {
+                        return ParseGenericForStatement(ref enumerator, forToken);
+                    }
+                }
+            case SyntaxTokenType.Break:
+                return new BreakStatementNode(enumerator.Current.Position);
+            case SyntaxTokenType.Local:
+                {
+                    // skip 'local' keyword
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Local, out var localToken);
+
+                    // local function
+                    if (enumerator.Current.Type is SyntaxTokenType.Function)
+                    {
+                        return ParseLocalFunctionDeclarationStatement(ref enumerator);
+                    }
+
+                    CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+
+                    var nextType = enumerator.GetNext().Type;
+
+                    if (nextType is SyntaxTokenType.Comma or SyntaxTokenType.Assignment)
+                    {
+                        return ParseLocalAssignmentStatement(ref enumerator, localToken);
+                    }
+                    else if (nextType is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon)
+                    {
+                        return new LocalAssignmentStatementNode([new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position)], [], localToken.Position);
+                    }
+                }
+                break;
+            case SyntaxTokenType.Function:
+                return ParseFunctionDeclarationStatement(ref enumerator);
+        }
+
+        LuaParseException.UnexpectedToken(ChunkName, enumerator.Current.Position, enumerator.Current);
+        return default!;
+    }
+
+    ReturnStatementNode ParseReturnStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'return' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Return, out var returnToken);
+
+        // parse parameters
+        var expressions = ParseExpressionList(ref enumerator);
+
+        return new ReturnStatementNode(expressions, returnToken.Position);
+    }
+
+    DoStatementNode ParseDoStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // check 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+        var doToken = enumerator.Current;
+
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.End) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        return new DoStatementNode(statements.AsSpan().ToArray(), doToken.Position);
+    }
+
+    GotoStatementNode ParseGotoStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'goto' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Goto, out var gotoToken);
+
+        CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+        return new GotoStatementNode(enumerator.Current.Text, gotoToken.Position);
+    }
+
+    AssignmentStatementNode ParseAssignmentStatement(ExpressionNode firstExpression, ref SyntaxTokenEnumerator enumerator)
+    {
+        // parse leftNodes
+        using var leftNodes = new PooledList<SyntaxNode>(8);
+        leftNodes.Add(firstExpression);
+
+        while (enumerator.Current.Type == SyntaxTokenType.Comma)
+        {
+            // skip ','
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            leftNodes.Add(ParseExpression(ref enumerator, OperatorPrecedence.NonOperator));
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // skip '='
+        if (enumerator.Current.Type is not SyntaxTokenType.Assignment)
+        {
+            enumerator.MovePrevious();
+            return new AssignmentStatementNode(leftNodes.AsSpan().ToArray(), [], firstExpression.Position);
+        }
+        MoveNextWithValidation(ref enumerator);
+
+        // parse expressions
+        var expressions = ParseExpressionList(ref enumerator);
+
+        return new AssignmentStatementNode(leftNodes.AsSpan().ToArray(), expressions, firstExpression.Position);
+    }
+
+    LocalAssignmentStatementNode ParseLocalAssignmentStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken localToken)
+    {
+        // parse identifiers
+        var identifiers = ParseIdentifierList(ref enumerator);
+
+        // skip '='
+        if (enumerator.Current.Type is not SyntaxTokenType.Assignment)
+        {
+            enumerator.MovePrevious();
+            return new LocalAssignmentStatementNode(identifiers, [], localToken.Position);
+        }
+        MoveNextWithValidation(ref enumerator);
+
+        // parse expressions
+        var expressions = ParseExpressionList(ref enumerator);
+
+        return new LocalAssignmentStatementNode(identifiers, expressions, localToken.Position);
+    }
+
+    IfStatementNode ParseIfStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'if' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.If, out var ifToken);
+        enumerator.SkipEoL();
+
+        // parse condition
+        var condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'then' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Then);
+
+        using var builder = new PooledList<StatementNode>(64);
+        using var elseIfBuilder = new PooledList<IfStatementNode.ConditionAndThenNodes>(64);
+
+        IfStatementNode.ConditionAndThenNodes ifNodes = default!;
+        StatementNode[] elseNodes = [];
+
+        // if = 0, elseif = 1, else = 2
+        var state = 0;
+
+        // parse statements
+        while (true)
+        {
+            if (!enumerator.MoveNext())
+            {
+                LuaParseException.ExpectedToken(ChunkName, enumerator.Current.Position, SyntaxTokenType.End);
+            }
+
+            var tokenType = enumerator.Current.Type;
+
+            if (tokenType is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon)
+            {
+                continue;
+            }
+
+            if (tokenType is SyntaxTokenType.ElseIf or SyntaxTokenType.Else or SyntaxTokenType.End)
+            {
+                switch (state)
+                {
+                    case 0:
+                        ifNodes = new()
+                        {
+                            ConditionNode = condition,
+                            ThenNodes = builder.AsSpan().ToArray(),
+                        };
+                        builder.Clear();
+                        break;
+                    case 1:
+                        elseIfBuilder.Add(new()
+                        {
+                            ConditionNode = condition,
+                            ThenNodes = builder.AsSpan().ToArray(),
+                        });
+                        builder.Clear();
+                        break;
+                    case 2:
+                        elseNodes = builder.AsSpan().ToArray();
+                        break;
+                }
+
+                if (tokenType is SyntaxTokenType.ElseIf)
+                {
+                    // skip 'elseif' keywords
+                    MoveNextWithValidation(ref enumerator);
+                    enumerator.SkipEoL();
+
+                    // parse condition
+                    condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+                    MoveNextWithValidation(ref enumerator);
+                    enumerator.SkipEoL();
+
+                    // skip 'then' keyword
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Then, out _);
+                    enumerator.SkipEoL();
+
+                    // set elseif state
+                    state = 1;
+                }
+                else if (tokenType is SyntaxTokenType.Else)
+                {
+                    // skip 'else' keywords
+                    MoveNextWithValidation(ref enumerator);
+
+                    enumerator.SkipEoL();
+
+                    // set else state
+                    state = 2;
+                }
+                else if (tokenType is SyntaxTokenType.End)
+                {
+                    goto RETURN;
+                }
+            }
+
+            var node = ParseStatement(ref enumerator);
+            builder.Add(node);
+        }
+
+    RETURN:
+        return new IfStatementNode(ifNodes, elseIfBuilder.AsSpan().ToArray(), elseNodes, ifToken.Position);
+    }
+
+    WhileStatementNode ParseWhileStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'while' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.While, out var whileToken);
+        enumerator.SkipEoL();
+
+        // parse condition
+        var condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'do' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Do, out _);
+
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.End) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        return new WhileStatementNode(condition, statements.AsSpan().ToArray(), whileToken.Position);
+    }
+
+    RepeatStatementNode ParseRepeatStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'repeat' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Repeat, out var repeatToken);
+
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.Until) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        // skip 'until keyword'
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Until, out _);
+        enumerator.SkipEoL();
+
+        // parse condition
+        var condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+
+        return new RepeatStatementNode(condition, statements.AsSpan().ToArray(), repeatToken.Position);
+    }
+
+    NumericForStatementNode ParseNumericForStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken forToken)
+    {
+        // parse variable name
+        CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+        var varName = enumerator.Current.Text;
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip '='
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Assignment, out _);
+        enumerator.SkipEoL();
+
+        // parse initial value
+        var initialValueNode = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip ','
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Comma, out _);
+        enumerator.SkipEoL();
+
+        // parse limit
+        var limitNode = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // parse stepNode
+        ExpressionNode? stepNode = null;
+        if (enumerator.Current.Type is SyntaxTokenType.Comma)
+        {
+            // skip ','
+            enumerator.MoveNext();
+
+            // parse step
+            stepNode = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // skip 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.End) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        return new NumericForStatementNode(varName, initialValueNode, limitNode, stepNode, statements.AsSpan().ToArray(), forToken.Position);
+    }
+
+    GenericForStatementNode ParseGenericForStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken forToken)
+    {
+        var identifiers = ParseIdentifierList(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'in' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.In, out _);
+        enumerator.SkipEoL();
+
+        var expression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.End) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        return new GenericForStatementNode(identifiers, expression, statements.AsSpan().ToArray(), forToken.Position);
+    }
+
+    FunctionDeclarationStatementNode ParseFunctionDeclarationStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        var (Name, Identifiers, Statements, HasVariableArgments, FunctionToken) = ParseFunctionDeclarationCore(ref enumerator, false);
+        return new FunctionDeclarationStatementNode(Name, Identifiers, Statements, HasVariableArgments, FunctionToken.Position);
+    }
+
+    LocalFunctionDeclarationStatementNode ParseLocalFunctionDeclarationStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        var (Name, Identifiers, Statements, HasVariableArgments, FunctionToken) = ParseFunctionDeclarationCore(ref enumerator, false);
+        return new LocalFunctionDeclarationStatementNode(Name, Identifiers, Statements, HasVariableArgments, FunctionToken.Position);
+    }
+
+    (ReadOnlyMemory<char> Name, IdentifierNode[] Identifiers, StatementNode[] Statements, bool HasVariableArgments, SyntaxToken FunctionToken) ParseFunctionDeclarationCore(ref SyntaxTokenEnumerator enumerator, bool isAnonymous)
+    {
+        // skip 'function' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Function, out var functionToken);
+        enumerator.SkipEoL();
+
+        ReadOnlyMemory<char> name;
+
+        if (isAnonymous)
+        {
+            name = ReadOnlyMemory<char>.Empty;
+        }
+        else
+        {
+            // parse function name
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            name = enumerator.Current.Text;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // skip '('
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out _);
+        enumerator.SkipEoL();
+
+        // parse parameters
+        var identifiers = enumerator.Current.Type is SyntaxTokenType.Identifier
+            ? ParseIdentifierList(ref enumerator)
+            : [];
+
+        // check variable arguments
+        var hasVarArg = enumerator.Current.Type is SyntaxTokenType.VarArg;
+        if (hasVarArg) enumerator.MoveNext();
+
+        // skip ')'
+        CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.End) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        return (name, identifiers, statements.AsSpan().ToArray(), hasVarArg, functionToken);
+    }
+
+    bool TryParseExpression(ref SyntaxTokenEnumerator enumerator, OperatorPrecedence precedence, [NotNullWhen(true)] out ExpressionNode? result)
+    {
+        result = enumerator.Current.Type switch
+        {
+            SyntaxTokenType.Identifier => enumerator.GetNext(true).Type switch
+            {
+                SyntaxTokenType.LParen => ParseCallFunctionExpression(ref enumerator),
+                SyntaxTokenType.LSquare or SyntaxTokenType.Dot or SyntaxTokenType.Colon => ParseTableAccessExpression(ref enumerator, null),
+                _ => new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position),
+            },
+            SyntaxTokenType.Number => new NumericLiteralNode(double.Parse(enumerator.Current.Text.Span), enumerator.Current.Position),
+            SyntaxTokenType.String => new StringLiteralNode(enumerator.Current.Text.ToString(), enumerator.Current.Position),
+            SyntaxTokenType.True => new BooleanLiteralNode(true, enumerator.Current.Position),
+            SyntaxTokenType.False => new BooleanLiteralNode(false, enumerator.Current.Position),
+            SyntaxTokenType.Nil => new NilLiteralNode(enumerator.Current.Position),
+            SyntaxTokenType.VarArg => new VariableArgumentsExpressionNode(enumerator.Current.Position),
+            SyntaxTokenType.Subtraction => ParseMinusNumber(ref enumerator),
+            SyntaxTokenType.Not or SyntaxTokenType.Length => ParseUnaryExpression(ref enumerator, enumerator.Current),
+            SyntaxTokenType.LParen => ParseGroupedExpression(ref enumerator),
+            SyntaxTokenType.LCurly => ParseTableConstructorExpression(ref enumerator),
+            SyntaxTokenType.Function => ParseFunctionDeclarationExpression(ref enumerator),
+            _ => null,
+        };
+
+        if (result == null) return false;
+
+        while (true)
+        {
+            enumerator.SkipEoL();
+
+            var opPrecedence = GetPrecedence(enumerator.GetNext().Type);
+            if (precedence >= opPrecedence) break;
+
+            MoveNextWithValidation(ref enumerator);
+            result = ParseBinaryExpression(ref enumerator, opPrecedence, result);
+        }
+
+        return true;
+    }
+
+    ExpressionNode ParseExpression(ref SyntaxTokenEnumerator enumerator, OperatorPrecedence precedence)
+    {
+        if (!TryParseExpression(ref enumerator, precedence, out var result))
+        {
+            throw new LuaParseException(ChunkName, enumerator.Current.Position, "Unexpected token <{enumerator.Current.Type}>");
+        }
+
+        return result;
+    }
+
+    ExpressionNode ParseMinusNumber(ref SyntaxTokenEnumerator enumerator)
+    {
+        var token = enumerator.Current;
+        if (enumerator.GetNext(true).Type is SyntaxTokenType.Number)
+        {
+            enumerator.MoveNext();
+            enumerator.SkipEoL();
+
+            return new NumericLiteralNode(-double.Parse(enumerator.Current.Text.Span), token.Position);
+        }
+        else
+        {
+            return ParseUnaryExpression(ref enumerator, token);
+        }
+    }
+
+    UnaryExpressionNode ParseUnaryExpression(ref SyntaxTokenEnumerator enumerator, SyntaxToken operatorToken)
+    {
+        var operatorType = enumerator.Current.Type switch
+        {
+            SyntaxTokenType.Subtraction => UnaryOperator.Negate,
+            SyntaxTokenType.Not => UnaryOperator.Not,
+            SyntaxTokenType.Length => UnaryOperator.Length,
+            _ => throw new LuaParseException(ChunkName, operatorToken.Position, $"unexpected symbol near '{enumerator.Current.Text}'"),
+        };
+
+        MoveNextWithValidation(ref enumerator);
+        var right = ParseExpression(ref enumerator, OperatorPrecedence.Unary);
+
+        return new UnaryExpressionNode(operatorType, right, operatorToken.Position);
+    }
+
+    BinaryExpressionNode ParseBinaryExpression(ref SyntaxTokenEnumerator enumerator, OperatorPrecedence precedence, ExpressionNode left)
+    {
+        var operatorToken = enumerator.Current;
+        var operatorType = operatorToken.Type switch
+        {
+            SyntaxTokenType.Addition => BinaryOperator.Addition,
+            SyntaxTokenType.Subtraction => BinaryOperator.Subtraction,
+            SyntaxTokenType.Multiplication => BinaryOperator.Multiplication,
+            SyntaxTokenType.Division => BinaryOperator.Division,
+            SyntaxTokenType.Modulo => BinaryOperator.Modulo,
+            SyntaxTokenType.Exponentiation => BinaryOperator.Exponentiation,
+            SyntaxTokenType.Equality => BinaryOperator.Equality,
+            SyntaxTokenType.Inequality => BinaryOperator.Inequality,
+            SyntaxTokenType.LessThan => BinaryOperator.LessThan,
+            SyntaxTokenType.LessThanOrEqual => BinaryOperator.LessThanOrEqual,
+            SyntaxTokenType.GreaterThan => BinaryOperator.GreaterThan,
+            SyntaxTokenType.GreaterThanOrEqual => BinaryOperator.GreaterThanOrEqual,
+            SyntaxTokenType.And => BinaryOperator.And,
+            SyntaxTokenType.Or => BinaryOperator.Or,
+            SyntaxTokenType.Concat => BinaryOperator.Concat,
+            _ => throw new LuaParseException(ChunkName, enumerator.Current.Position, $"unexpected symbol near '{enumerator.Current.Text}'"),
+        };
+
+        enumerator.SkipEoL();
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        var right = ParseExpression(ref enumerator, precedence);
+
+        return new BinaryExpressionNode(operatorType, left, right, operatorToken.Position);
+    }
+
+    TableConstructorExpressionNode ParseTableConstructorExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        CheckCurrent(ref enumerator, SyntaxTokenType.LCurly);
+        var startToken = enumerator.Current;
+
+        using var items = new PooledList<TableConstructorField>(16);
+
+        while (enumerator.MoveNext())
+        {
+            var currentToken = enumerator.Current;
+            switch (currentToken.Type)
+            {
+                case SyntaxTokenType.RCurly:
+                    goto RETURN;
+                case SyntaxTokenType.EndOfLine:
+                case SyntaxTokenType.Comma:
+                    continue;
+                case SyntaxTokenType.LSquare:
+                    // general style ([key] = value)
+                    enumerator.MoveNext();
+
+                    var keyExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+                    enumerator.MoveNext();
+
+                    // skip '] ='
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.RSquare, out _);
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Assignment, out _);
+
+                    var valueExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+
+                    items.Add(new GeneralTableConstructorField(keyExpression, valueExpression, currentToken.Position));
+
+                    break;
+                case SyntaxTokenType.Identifier when enumerator.GetNext(true).Type is SyntaxTokenType.Assignment:
+                    // record style (key = value)
+                    var name = enumerator.Current.Text;
+
+                    // skip key and '='
+                    enumerator.MoveNext();
+                    enumerator.MoveNext();
+
+                    var expression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+
+                    items.Add(new RecordTableConstructorField(name.ToString(), expression, currentToken.Position));
+                    break;
+                default:
+                    // list style
+                    items.Add(new ListTableConstructorField(ParseExpression(ref enumerator, OperatorPrecedence.NonOperator), currentToken.Position));
+                    break;
+            }
+        }
+
+    RETURN:
+        return new TableConstructorExpressionNode(items.AsSpan().ToArray(), startToken.Position);
+    }
+
+    ExpressionNode ParseTableAccessExpression(ref SyntaxTokenEnumerator enumerator, ExpressionNode? parentTable)
+    {
+        IdentifierNode? identifier = null;
+        if (parentTable == null)
+        {
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            identifier = new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position);
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        ExpressionNode result;
+        var current = enumerator.Current;
+        if (current.Type is SyntaxTokenType.LSquare)
+        {
+            // indexer access -- table[key]
+
+            // skip '['
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse key expression
+            var keyExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // check ']'
+            CheckCurrent(ref enumerator, SyntaxTokenType.RSquare);
+
+            result = new TableIndexerAccessExpressionNode(identifier ?? parentTable!, keyExpression, current.Position);
+        }
+        else if (current.Type is SyntaxTokenType.Dot)
+        {
+            // member access -- table.key
+
+            // skip '.'
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            var key = enumerator.Current.Text.ToString();
+
+            result = new TableMemberAccessExpressionNode(identifier ?? parentTable!, key, current.Position);
+        }
+        else if (current.Type is SyntaxTokenType.Colon)
+        {
+            // self method call -- table:method(arg0, arg1, ...)
+
+            // skip ':'
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            var methodName = enumerator.Current.Text;
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse arguments
+            var arguments = ParseCallFunctionArguments(ref enumerator);
+            result = new CallTableMethodExpressionNode(identifier ?? parentTable!, methodName.ToString(), arguments, current.Position);
+        }
+        else
+        {
+            LuaParseException.SyntaxError(ChunkName, current.Position, current);
+            return null!; // dummy
+        }
+
+        // parse child table element
+    PARSE_CHILD:
+        var nextType = enumerator.GetNext(true).Type;
+        if (nextType is SyntaxTokenType.Dot or SyntaxTokenType.LSquare or SyntaxTokenType.Colon)
+        {
+            enumerator.SkipEoL();
+            enumerator.MoveNext();
+            enumerator.SkipEoL();
+
+            result = ParseTableAccessExpression(ref enumerator, result);
+        }
+        if (nextType is SyntaxTokenType.LParen)
+        {
+            enumerator.SkipEoL();
+            enumerator.MoveNext();
+            enumerator.SkipEoL();
+
+            var parameters = ParseCallFunctionArguments(ref enumerator);
+            result = new CallFunctionExpressionNode(result, parameters);
+            goto PARSE_CHILD;
+        }
+
+        return result;
+    }
+
+    ExpressionNode ParseGroupedExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip '('
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out _);
+        enumerator.SkipEoL();
+
+        var expression = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+        MoveNextWithValidation(ref enumerator);
+
+        // check ')'
+        CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+
+        return expression;
+    }
+
+    ExpressionNode ParseCallFunctionExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        // parse name
+        CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+        var function = new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position);
+        enumerator.MoveNext();
+        enumerator.SkipEoL();
+
+        // parse parameters
+        var parameters = ParseCallFunctionArguments(ref enumerator);
+
+        var expression = new CallFunctionExpressionNode(function, parameters);
+
+        // parse table access expression
+        if (enumerator.GetNext(true).Type is SyntaxTokenType.LSquare or SyntaxTokenType.Dot or SyntaxTokenType.Colon)
+        {
+            enumerator.SkipEoL();
+            enumerator.MoveNext();
+            enumerator.SkipEoL();
+
+            return ParseTableAccessExpression(ref enumerator, expression);
+        }
+        else
+        {
+            return expression;
+        }
+    }
+
+    FunctionDeclarationExpressionNode ParseFunctionDeclarationExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        var (_, Identifiers, Statements, HasVariableArgments, FunctionToken) = ParseFunctionDeclarationCore(ref enumerator, true);
+        return new FunctionDeclarationExpressionNode(Identifiers, Statements, HasVariableArgments, FunctionToken.Position);
+    }
+
+    ExpressionNode[] ParseCallFunctionArguments(ref SyntaxTokenEnumerator enumerator)
+    {
+        // check and skip '('
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out _);
+
+        ExpressionNode[] arguments;
+        if (enumerator.Current.Type is SyntaxTokenType.RParen)
+        {
+            // parameterless
+            arguments = [];
+        }
+        else
+        {
+            // parse arguments
+            arguments = ParseExpressionList(ref enumerator);
+            enumerator.SkipEoL();
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // check ')'
+        CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+
+        return arguments;
+    }
+
+    ExpressionNode[] ParseExpressionList(ref SyntaxTokenEnumerator enumerator)
+    {
+        using var builder = new PooledList<ExpressionNode>(8);
+
+        while (true)
+        {
+            if (!TryParseExpression(ref enumerator, OperatorPrecedence.NonOperator, out var expression))
+            {
+                enumerator.MovePrevious();
+                break;
+            }
+
+            builder.Add(expression);
+
+            enumerator.SkipEoL();
+            if (enumerator.GetNext().Type != SyntaxTokenType.Comma) break;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            if (!enumerator.MoveNext()) break;
+        }
+
+        return builder.AsSpan().ToArray();
+    }
+
+    IdentifierNode[] ParseIdentifierList(ref SyntaxTokenEnumerator enumerator)
+    {
+        using var buffer = new PooledList<IdentifierNode>(8);
+
+        while (true)
+        {
+            if (enumerator.Current.Type != SyntaxTokenType.Identifier) break;
+            var identifier = new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position);
+            buffer.Add(identifier);
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            if (enumerator.Current.Type != SyntaxTokenType.Comma) break;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        return buffer.AsSpan().ToArray();
+    }
+
+    void CheckCurrentAndSkip(ref SyntaxTokenEnumerator enumerator, SyntaxTokenType expectedToken, out SyntaxToken token)
+    {
+        CheckCurrent(ref enumerator, expectedToken);
+        token = enumerator.Current;
+        MoveNextWithValidation(ref enumerator);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void CheckCurrent(ref SyntaxTokenEnumerator enumerator, SyntaxTokenType expectedToken)
+    {
+        if (enumerator.Current.Type != expectedToken)
+        {
+            LuaParseException.ExpectedToken(ChunkName, enumerator.Current.Position, expectedToken);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void MoveNextWithValidation(ref SyntaxTokenEnumerator enumerator)
+    {
+        if (!enumerator.MoveNext()) LuaParseException.SyntaxError(ChunkName, enumerator.Current.Position, enumerator.Current);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static OperatorPrecedence GetPrecedence(SyntaxTokenType type)
+    {
+        return type switch
+        {
+            SyntaxTokenType.Addition or SyntaxTokenType.Subtraction => OperatorPrecedence.Addition,
+            SyntaxTokenType.Multiplication or SyntaxTokenType.Division or SyntaxTokenType.Modulo => OperatorPrecedence.Multiplication,
+            SyntaxTokenType.Equality or SyntaxTokenType.Inequality or SyntaxTokenType.LessThan or SyntaxTokenType.LessThanOrEqual or SyntaxTokenType.GreaterThan or SyntaxTokenType.GreaterThanOrEqual => OperatorPrecedence.Relational,
+            SyntaxTokenType.Concat => OperatorPrecedence.Concat,
+            SyntaxTokenType.Exponentiation => OperatorPrecedence.Exponentiation,
+            SyntaxTokenType.And => OperatorPrecedence.And,
+            SyntaxTokenType.Or => OperatorPrecedence.Or,
+            _ => OperatorPrecedence.NonOperator,
+        };
+    }
+}

+ 6 - 0
src/Lua/CodeAnalysis/Syntax/SyntaxNode.cs

@@ -0,0 +1,6 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+public abstract record SyntaxNode(SourcePosition Position)
+{
+    public abstract TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context);
+}

+ 358 - 0
src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs

@@ -0,0 +1,358 @@
+using Lua.Internal;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public readonly struct SyntaxToken(SyntaxTokenType type, ReadOnlyMemory<char> text, SourcePosition position) : IEquatable<SyntaxToken>
+{
+    public static SyntaxToken EndOfLine(SourcePosition position) => new(SyntaxTokenType.EndOfLine, Keywords.LF.AsMemory(), position);
+
+    public static SyntaxToken LParen(SourcePosition position) => new(SyntaxTokenType.LParen, Keywords.LParen.AsMemory(), position);
+    public static SyntaxToken RParen(SourcePosition position) => new(SyntaxTokenType.RParen, Keywords.RParen.AsMemory(), position);
+    public static SyntaxToken LCurly(SourcePosition position) => new(SyntaxTokenType.LCurly, Keywords.LCurly.AsMemory(), position);
+    public static SyntaxToken RCurly(SourcePosition position) => new(SyntaxTokenType.RCurly, Keywords.RCurly.AsMemory(), position);
+    public static SyntaxToken LSquare(SourcePosition position) => new(SyntaxTokenType.LSquare, Keywords.LSquare.AsMemory(), position);
+    public static SyntaxToken RSquare(SourcePosition position) => new(SyntaxTokenType.RSquare, Keywords.RSquare.AsMemory(), position);
+
+    public static SyntaxToken Nil(SourcePosition position) => new(SyntaxTokenType.Nil, Keywords.Nil.AsMemory(), position);
+    public static SyntaxToken True(SourcePosition position) => new(SyntaxTokenType.True, Keywords.True.AsMemory(), position);
+    public static SyntaxToken False(SourcePosition position) => new(SyntaxTokenType.False, Keywords.False.AsMemory(), position);
+
+    public static SyntaxToken Addition(SourcePosition position) => new(SyntaxTokenType.Addition, Keywords.Addition.AsMemory(), position);
+    public static SyntaxToken Subtraction(SourcePosition position) => new(SyntaxTokenType.Subtraction, Keywords.Subtraction.AsMemory(), position);
+    public static SyntaxToken Multiplication(SourcePosition position) => new(SyntaxTokenType.Multiplication, Keywords.Multiplication.AsMemory(), position);
+    public static SyntaxToken Division(SourcePosition position) => new(SyntaxTokenType.Division, Keywords.Division.AsMemory(), position);
+    public static SyntaxToken Modulo(SourcePosition position) => new(SyntaxTokenType.Modulo, Keywords.Modulo.AsMemory(), position);
+    public static SyntaxToken Exponentiation(SourcePosition position) => new(SyntaxTokenType.Exponentiation, Keywords.Exponentiation.AsMemory(), position);
+
+    public static SyntaxToken Equality(SourcePosition position) => new(SyntaxTokenType.Equality, Keywords.Equality.AsMemory(), position);
+    public static SyntaxToken Inequality(SourcePosition position) => new(SyntaxTokenType.Inequality, Keywords.Inequality.AsMemory(), position);
+    public static SyntaxToken GreaterThan(SourcePosition position) => new(SyntaxTokenType.GreaterThan, Keywords.GreaterThan.AsMemory(), position);
+    public static SyntaxToken GreaterThanOrEqual(SourcePosition position) => new(SyntaxTokenType.GreaterThanOrEqual, Keywords.GreaterThanOrEqual.AsMemory(), position);
+    public static SyntaxToken LessThan(SourcePosition position) => new(SyntaxTokenType.LessThan, Keywords.LessThan.AsMemory(), position);
+    public static SyntaxToken LessThanOrEqual(SourcePosition position) => new(SyntaxTokenType.LessThanOrEqual, Keywords.LessThanOrEqual.AsMemory(), position);
+
+    public static SyntaxToken Length(SourcePosition position) => new(SyntaxTokenType.Length, Keywords.Length.AsMemory(), position);
+    public static SyntaxToken Concat(SourcePosition position) => new(SyntaxTokenType.Concat, Keywords.Concat.AsMemory(), position);
+    public static SyntaxToken VarArg(SourcePosition position) => new(SyntaxTokenType.VarArg, "...".AsMemory(), position);
+
+    public static SyntaxToken Assignment(SourcePosition position) => new(SyntaxTokenType.Assignment, Keywords.Assignment.AsMemory(), position);
+
+    public static SyntaxToken And(SourcePosition position) => new(SyntaxTokenType.And, Keywords.And.AsMemory(), position);
+    public static SyntaxToken Or(SourcePosition position) => new(SyntaxTokenType.Or, Keywords.Or.AsMemory(), position);
+    public static SyntaxToken Not(SourcePosition position) => new(SyntaxTokenType.Not, Keywords.Not.AsMemory(), position);
+
+    public static SyntaxToken End(SourcePosition position) => new(SyntaxTokenType.End, Keywords.End.AsMemory(), position);
+    public static SyntaxToken Then(SourcePosition position) => new(SyntaxTokenType.Then, Keywords.Then.AsMemory(), position);
+
+    public static SyntaxToken If(SourcePosition position) => new(SyntaxTokenType.If, Keywords.If.AsMemory(), position);
+    public static SyntaxToken ElseIf(SourcePosition position) => new(SyntaxTokenType.ElseIf, Keywords.ElseIf.AsMemory(), position);
+    public static SyntaxToken Else(SourcePosition position) => new(SyntaxTokenType.Else, Keywords.Else.AsMemory(), position);
+
+    public static SyntaxToken Local(SourcePosition position) => new(SyntaxTokenType.Local, Keywords.Local.AsMemory(), position);
+
+    public static SyntaxToken Return(SourcePosition position) => new(SyntaxTokenType.Return, Keywords.Return.AsMemory(), position);
+    public static SyntaxToken Goto(SourcePosition position) => new(SyntaxTokenType.Goto, Keywords.Goto.AsMemory(), position);
+
+    public static SyntaxToken Comma(SourcePosition position) => new(SyntaxTokenType.Comma, ",".AsMemory(), position);
+    public static SyntaxToken Dot(SourcePosition position) => new(SyntaxTokenType.Dot, ".".AsMemory(), position);
+    public static SyntaxToken SemiColon(SourcePosition position) => new(SyntaxTokenType.SemiColon, ";".AsMemory(), position);
+    public static SyntaxToken Colon(SourcePosition position) => new(SyntaxTokenType.Colon, ":".AsMemory(), position);
+
+    public static SyntaxToken Do(SourcePosition position) => new(SyntaxTokenType.Do, Keywords.Do.AsMemory(), position);
+    public static SyntaxToken While(SourcePosition position) => new(SyntaxTokenType.While, Keywords.While.AsMemory(), position);
+    public static SyntaxToken Repeat(SourcePosition position) => new(SyntaxTokenType.Repeat, Keywords.Repeat.AsMemory(), position);
+    public static SyntaxToken Until(SourcePosition position) => new(SyntaxTokenType.Until, Keywords.Until.AsMemory(), position);
+    public static SyntaxToken Break(SourcePosition position) => new(SyntaxTokenType.Break, Keywords.Break.AsMemory(), position);
+    public static SyntaxToken Function(SourcePosition position) => new(SyntaxTokenType.Function, Keywords.Function.AsMemory(), position);
+    public static SyntaxToken For(SourcePosition position) => new(SyntaxTokenType.For, Keywords.For.AsMemory(), position);
+    public static SyntaxToken In(SourcePosition position) => new(SyntaxTokenType.In, Keywords.In.AsMemory(), position);
+
+    public SyntaxTokenType Type { get; } = type;
+    public ReadOnlyMemory<char> Text { get; } = text;
+    public SourcePosition Position { get; } = position;
+
+    public static SyntaxToken Number(string text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Number, text.AsMemory(), position);
+    }
+
+    public static SyntaxToken Number(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Number, text, position);
+    }
+
+    public static SyntaxToken Identifier(string text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Identifier, text.AsMemory(), position);
+    }
+
+    public static SyntaxToken Identifier(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Identifier, text, position);
+    }
+
+    public static SyntaxToken String(string text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.String, text.AsMemory(), position);
+    }
+
+    public static SyntaxToken String(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.String, text, position);
+    }
+
+    public static SyntaxToken Label(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Label, text, position);
+    }
+
+    public override string ToString()
+    {
+        return $"{Position} {Type}:{Text}";
+    }
+
+    public string ToDisplayString()
+    {
+        return Type switch
+        {
+            SyntaxTokenType.EndOfLine => Keywords.LF,
+            SyntaxTokenType.LParen => Keywords.LParen,
+            SyntaxTokenType.RParen => Keywords.RParen,
+            SyntaxTokenType.LCurly => Keywords.LCurly,
+            SyntaxTokenType.RCurly => Keywords.RCurly,
+            SyntaxTokenType.LSquare => Keywords.LSquare,
+            SyntaxTokenType.RSquare => Keywords.RSquare,
+            SyntaxTokenType.SemiColon => ";",
+            SyntaxTokenType.Comma => ",",
+            SyntaxTokenType.Number => Text.ToString(),
+            SyntaxTokenType.String => $"\"{Text}\"",
+            SyntaxTokenType.Nil => Keywords.Nil,
+            SyntaxTokenType.True => Keywords.True,
+            SyntaxTokenType.False => Keywords.False,
+            SyntaxTokenType.Identifier => Text.ToString(),
+            SyntaxTokenType.Addition => Keywords.Addition,
+            SyntaxTokenType.Subtraction => Keywords.Subtraction,
+            SyntaxTokenType.Multiplication => Keywords.Multiplication,
+            SyntaxTokenType.Division => Keywords.Division,
+            SyntaxTokenType.Modulo => Keywords.Modulo,
+            SyntaxTokenType.Exponentiation => Keywords.Exponentiation,
+            SyntaxTokenType.Equality => Keywords.Equality,
+            SyntaxTokenType.Inequality => Keywords.Inequality,
+            SyntaxTokenType.GreaterThan => Keywords.GreaterThan,
+            SyntaxTokenType.LessThan => Keywords.LessThan,
+            SyntaxTokenType.GreaterThanOrEqual => Keywords.GreaterThanOrEqual,
+            SyntaxTokenType.LessThanOrEqual => Keywords.LessThanOrEqual,
+            SyntaxTokenType.And => Keywords.And,
+            SyntaxTokenType.Not => Keywords.Not,
+            SyntaxTokenType.Or => Keywords.Or,
+            SyntaxTokenType.Assignment => Keywords.Assignment,
+            SyntaxTokenType.Concat => Keywords.Concat,
+            SyntaxTokenType.Length => Keywords.Length,
+            SyntaxTokenType.Break => Keywords.Break,
+            SyntaxTokenType.Do => Keywords.Do,
+            SyntaxTokenType.For => Keywords.For,
+            SyntaxTokenType.Goto => Keywords.Goto,
+            SyntaxTokenType.If => Keywords.If,
+            SyntaxTokenType.ElseIf => Keywords.ElseIf,
+            SyntaxTokenType.Else => Keywords.Else,
+            SyntaxTokenType.Function => Keywords.Function,
+            SyntaxTokenType.End => Keywords.End,
+            SyntaxTokenType.Then => Keywords.Then,
+            SyntaxTokenType.In => Keywords.In,
+            SyntaxTokenType.Local => Keywords.Local,
+            SyntaxTokenType.Repeat => Keywords.Repeat,
+            SyntaxTokenType.Return => Keywords.Return,
+            SyntaxTokenType.Until => Keywords.Until,
+            SyntaxTokenType.While => Keywords.While,
+            _ => "",
+        };
+    }
+
+    public bool Equals(SyntaxToken other)
+    {
+        return other.Type == Type &&
+            other.Text.Span.SequenceEqual(Text.Span) &&
+            other.Position == Position;
+    }
+
+    public override bool Equals(object? obj)
+    {
+        if (obj is SyntaxToken token) return Equals(token);
+        return false;
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(Type, Utf16StringMemoryComparer.Default.GetHashCode(Text), Position);
+    }
+
+    public static bool operator ==(SyntaxToken left, SyntaxToken right)
+    {
+        return left.Equals(right);
+    }
+
+    public static bool operator !=(SyntaxToken left, SyntaxToken right)
+    {
+        return !(left == right);
+    }
+}
+
+public enum SyntaxTokenType
+{
+    /// <summary>
+    /// Invalid token
+    /// </summary>
+    Invalid,
+
+    /// <summary>
+    /// End of line
+    /// </summary>
+    EndOfLine,
+
+    /// <summary>
+    /// Left parenthesis '('
+    /// </summary>
+    LParen,
+    /// <summary>
+    /// Right parenthesis ')'
+    /// </summary>
+    RParen,
+
+    /// <summary>
+    /// Left curly bracket '{'
+    /// </summary>
+    LCurly,
+    /// <summary>
+    /// Right curly bracket '}'
+    /// </summary>
+    RCurly,
+
+    /// <summary>
+    /// Left square bracket '['
+    /// </summary>
+    LSquare,
+    /// <summary>
+    /// Right square bracket ']'
+    /// </summary>
+    RSquare,
+
+    /// <summary>
+    /// Semi colon (;)
+    /// </summary>
+    SemiColon,
+
+    /// <summary>
+    /// Colon (:)
+    /// </summary>
+    Colon,
+
+    /// <summary>
+    /// Comma (,)
+    /// </summary>
+    Comma,
+
+    /// <summary>
+    /// Dot (.)
+    /// </summary>
+    Dot,
+
+    /// <summary>
+    /// Numeric literal (e.g. 1, 2, 1.0, 2.0, ...)
+    /// </summary>
+    Number,
+
+    /// <summary>
+    /// String literal (e.g. "foo", "bar", ...)
+    /// </summary>
+    String,
+
+    /// <summary>
+    /// Nil literal (nil)
+    /// </summary>
+    Nil,
+
+    /// <summary>
+    /// Boolean literal (true)
+    /// </summary>
+    True,
+    /// <summary>
+    /// Boolean literal (false)
+    /// </summary>
+    False,
+
+    /// <summary>
+    /// Identifier
+    /// </summary>
+    Identifier,
+
+    /// <summary>
+    /// Label
+    /// </summary>
+    Label,
+
+    /// <summary>
+    /// Addition operator (+)
+    /// </summary>
+    Addition,
+    /// <summary>
+    /// Subtraction operator (-)
+    /// </summary>
+    Subtraction,
+    /// <summary>
+    /// Multiplication operator (*)
+    /// </summary>
+    Multiplication,
+    /// <summary>
+    /// Division operator (/)
+    /// </summary>
+    Division,
+    /// <summary>
+    /// Modulo operator (%)
+    /// </summary>
+    Modulo,
+    /// <summary>
+    /// Exponentiation operator (^)
+    /// </summary>
+    Exponentiation,
+
+    Equality,          // ==
+    Inequality,       // ~=
+    GreaterThan,        // >
+    LessThan,           // <
+    GreaterThanOrEqual, // >=
+    LessThanOrEqual,    // <=
+
+    And,            // and
+    Not,            // not
+    Or,             // or
+
+    /// <summary>
+    /// Assignment operator (=)
+    /// </summary>
+    Assignment,
+
+    Concat,         // ..
+    Length,         // #
+
+    VarArg,         // ...
+
+    Break,          // break
+    Do,             // do
+    For,            // for
+    Goto,           // goto
+
+    If,             // if
+    ElseIf,         // elseif
+    Else,           // else
+    Function,       // function
+
+    End,            // end
+    Then,           // then
+
+    In,             // in
+    Local,          // local
+    Repeat,         // repeat
+    Return,         // return
+    Until,          // until
+    While,          // while
+}

+ 60 - 0
src/Lua/CodeAnalysis/Syntax/SyntaxTokenEnumerator.cs

@@ -0,0 +1,60 @@
+using System.Runtime.CompilerServices;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public ref struct SyntaxTokenEnumerator(ReadOnlySpan<SyntaxToken> source)
+{
+    ReadOnlySpan<SyntaxToken> source = source;
+    SyntaxToken current;
+    int offset;
+
+    public SyntaxToken Current => current;
+    public int Position => offset;
+    public bool IsCompleted => source.Length == offset;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool MoveNext()
+    {
+        if (IsCompleted) return false;
+        current = source[offset];
+        offset++;
+        return true;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool MovePrevious()
+    {
+        if (offset == 0) return false;
+        offset--;
+        current = source[offset - 1];
+        return true;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void SkipEoL()
+    {
+        while (true)
+        {
+            if (current.Type != SyntaxTokenType.EndOfLine) return;
+            if (!MoveNext()) return;
+        }
+    }
+
+    public SyntaxToken GetNext(bool skipEoL = false)
+    {
+        if (!skipEoL)
+        {
+            return IsCompleted ? default : source[offset];
+        }
+
+        var i = offset;
+        while (i < source.Length)
+        {
+            var c = source[i];
+            if (source[i].Type is not SyntaxTokenType.EndOfLine) return c;
+            i++;
+        }
+
+        return default;
+    }
+}

+ 90 - 0
src/Lua/Exceptions.cs

@@ -0,0 +1,90 @@
+using Lua.CodeAnalysis;
+using Lua.CodeAnalysis.Syntax;
+using Lua.Runtime;
+
+namespace Lua;
+
+public class LuaException(string message) : Exception(message);
+
+public class LuaParseException(string? chunkName, SourcePosition position, string message) : LuaException(message)
+{
+    public string? ChunkName { get; } = chunkName;
+    public SourcePosition? Position { get; } = position;
+
+    public static void UnexpectedToken(string? chunkName, SourcePosition position, SyntaxToken token)
+    {
+        throw new LuaParseException(chunkName, position, $"unexpected symbol <{token.Type}> near '{token.Text}'");
+    }
+
+    public static void ExpectedToken(string? chunkName, SourcePosition position, SyntaxTokenType token)
+    {
+        throw new LuaParseException(chunkName, position, $"'{token}' expected");
+    }
+
+    public static void UnfinishedLongComment(string? chunkName, SourcePosition position)
+    {
+        throw new LuaParseException(chunkName, position, $"unfinished long comment (starting at line {position.Line})");
+    }
+
+    public static void SyntaxError(string? chunkName, SourcePosition position, SyntaxToken? token)
+    {
+        throw new LuaParseException(chunkName, position, $"syntax error {(token == null ? "" : $"near '{token.Value.Text}'")}");
+    }
+
+    public static void NoVisibleLabel(string label, string? chunkName, SourcePosition position)
+    {
+        throw new LuaParseException(chunkName, position, $"no visible label '{label}' for <goto>");
+    }
+
+    public static void BreakNotInsideALoop(string? chunkName, SourcePosition position)
+    {
+        throw new LuaParseException(chunkName, position, "<break> not inside a loop");
+    }
+
+    public override string Message => $"{ChunkName ?? "<anonymous.lua>"}:{(Position == null ? "" : $"{Position.Value}:")} {base.Message}";
+}
+
+public class LuaRuntimeException(Tracebacks tracebacks, string message) : LuaException(message)
+{
+    public Tracebacks Tracebacks { get; } = tracebacks;
+
+    public static void AttemptInvalidOperation(Tracebacks tracebacks, string op, LuaValue a, LuaValue b)
+    {
+        throw new LuaRuntimeException(tracebacks, $"attempt to {op} a '{a.Type}' with a '{b.Type}'");
+    }
+
+    public static void AttemptInvalidOperation(Tracebacks tracebacks, string op, LuaValue a)
+    {
+        throw new LuaRuntimeException(tracebacks, $"attempt to {op} a '{a.Type}' value");
+    }
+
+    public static void BadArgument(Tracebacks tracebacks, int argumentId, string functionName)
+    {
+        throw new LuaRuntimeException(tracebacks, $"bad argument #{argumentId} to '{functionName}' (value expected)");
+    }
+
+    public static void BadArgument(Tracebacks tracebacks, int argumentId, string functionName, LuaValueType[] expected)
+    {
+        throw new LuaRuntimeException(tracebacks, $"bad argument #{argumentId} to '{functionName}' ({string.Join(" or ", expected)} expected)");
+    }
+
+    public static void BadArgument(Tracebacks tracebacks, int argumentId, string functionName, LuaValueType expected)
+    {
+        throw new LuaRuntimeException(tracebacks, $"bad argument #{argumentId} to '{functionName}' ({expected} expected, got no value)");
+    }
+
+    public static void BadArgument(Tracebacks tracebacks, int argumentId, string functionName, LuaValueType expected, LuaValueType actual)
+    {
+        throw new LuaRuntimeException(tracebacks, $"bad argument #{argumentId} to '{functionName}' ({expected} expected, got {actual})");
+    }
+
+    public override string Message => $"{Tracebacks.RootChunkName}:{Tracebacks.LastPosition.Line}: {base.Message}{(Tracebacks.StackFrames.Length > 0 ? $"\n{Tracebacks}" : "")}";
+}
+
+public class LuaAssertionException(Tracebacks tracebacks, string message) : LuaRuntimeException(tracebacks, message)
+{
+    public override string ToString()
+    {
+        return $"{Message}\n{StackTrace}";
+    }
+}

+ 87 - 0
src/Lua/Internal/AutoResizeArrayCore.cs

@@ -0,0 +1,87 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Lua.Internal;
+
+internal struct AutoResizeArrayCore<T>
+{
+    T[]? array;
+    int size;
+
+    public int Size => size;
+    public int Capacity => array == null ? 0 : array.Length;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Add(T item)
+    {
+        this[size] = item;
+    }
+
+    public ref T this[int index]
+    {
+        get
+        {
+            EnsureCapacity(index);
+            size = Math.Max(size, index + 1);
+
+#if NET6_0_OR_GREATER
+            ref var reference = ref MemoryMarshal.GetArrayDataReference(array!);
+#else
+            ref var reference = ref MemoryMarshal.GetReference(array.AsSpan());
+#endif
+
+            return ref Unsafe.Add(ref reference, index);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Span<T> AsSpan()
+    {
+        return array.AsSpan(0, size);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public T[] GetInternalArray()
+    {
+        return array ?? [];
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Clear()
+    {
+        array.AsSpan().Clear();
+    }
+
+    public void EnsureCapacity(int newCapacity, bool overrideSize = false)
+    {
+        var capacity = 64;
+        while (capacity <= newCapacity)
+        {
+            capacity = MathEx.NewArrayCapacity(capacity);
+        }
+
+        if (array == null)
+        {
+            array = new T[capacity];
+        }
+        else
+        {
+            Array.Resize(ref array, capacity);
+        }
+
+        if (overrideSize)
+        {
+            size = newCapacity;
+        }
+    }
+
+    public void Shrink(int newSize)
+    {
+        if (array != null && array.Length > newSize)
+        {
+            array.AsSpan(newSize).Clear();
+        }
+
+        size = newSize;
+    }
+}

+ 35 - 0
src/Lua/Internal/EnumerableEx.cs

@@ -0,0 +1,35 @@
+namespace Lua;
+
+internal static class EnumerableEx
+{
+    public static IEnumerable<IEnumerable<T>> GroupConsecutiveBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
+    {
+        if (source == null) throw new ArgumentNullException(nameof(source));
+        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
+
+        using var enumerator = source.GetEnumerator();
+        if (!enumerator.MoveNext())
+        {
+            yield break;
+        }
+
+        var group = new List<T> { enumerator.Current };
+        var previousKey = keySelector(enumerator.Current);
+
+        while (enumerator.MoveNext())
+        {
+            TKey currentKey = keySelector(enumerator.Current);
+
+            if (!EqualityComparer<TKey>.Default.Equals(previousKey, currentKey))
+            {
+                yield return group;
+                group = [];
+            }
+
+            group.Add(enumerator.Current);
+            previousKey = currentKey;
+        }
+
+        yield return group;
+    }
+}

+ 425 - 0
src/Lua/Internal/FarmHash.cs

@@ -0,0 +1,425 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Lua;
+
+internal static class FarmHash
+{
+    // entry point
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static unsafe ulong Hash64(byte[] bytes, int offset, int count)
+    {
+        fixed (byte* p = &bytes[offset])
+        {
+            return Hash64(p, (uint)count);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static unsafe ulong Hash64(ReadOnlySpan<byte> bytes)
+    {
+        fixed (byte* p = &MemoryMarshal.GetReference(bytes))
+        {
+            return Hash64(p, (uint)bytes.Length);
+        }
+    }
+
+    // port from farmhash.cc
+    static unsafe uint Fetch32(byte* p)
+    {
+        return *(uint*)p;
+    }
+
+    struct Pair
+    {
+        public ulong first;
+        public ulong second;
+
+        public Pair(ulong first, ulong second)
+        {
+            this.first = first;
+            this.second = second;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static Pair make_pair(ulong first, ulong second)
+    {
+        return new Pair(first, second);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static void swap(ref ulong x, ref ulong z)
+    {
+        var temp = z;
+        z = x;
+        x = temp;
+    }
+
+    // Some primes between 2^63 and 2^64 for various uses.
+    const ulong k0 = 0xc3a5c85c97cb3127UL;
+    const ulong k1 = 0xb492b66fbe98f273UL;
+    const ulong k2 = 0x9ae16a3b2f90404fUL;
+
+    static unsafe ulong Fetch64(byte* p)
+    {
+        return *(ulong*)p;
+    }
+
+    static ulong Rotate64(ulong val, int shift)
+    {
+        return shift == 0 ? val : (val >> shift) | (val << (64 - shift));
+    }
+
+    // farmhashna.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static ulong ShiftMix(ulong val)
+    {
+        return val ^ (val >> 47);
+    }
+
+    // farmhashna.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static ulong HashLen16(ulong u, ulong v, ulong mul)
+    {
+        unchecked
+        {
+            // Murmur-inspired hashing.
+            ulong a = (u ^ v) * mul;
+            a ^= a >> 47;
+            ulong b = (v ^ a) * mul;
+            b ^= b >> 47;
+            b *= mul;
+            return b;
+        }
+    }
+
+    // farmhashxo.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong Hash64(byte* s, uint len)
+    {
+        if (len <= 16)
+        {
+            // farmhashna::
+            return HashLen0to16(s, len);
+        }
+
+        if (len <= 32)
+        {
+            // farmhashna::
+            return HashLen17to32(s, len);
+        }
+
+        if (len <= 64)
+        {
+            return HashLen33to64(s, len);
+        }
+
+        if (len <= 96)
+        {
+            return HashLen65to96(s, len);
+        }
+
+        if (len <= 256)
+        {
+            // farmhashna::
+            return Hash64NA(s, len);
+        }
+
+        // farmhashuo::
+        return Hash64UO(s, len);
+    }
+
+    // 0-16 farmhashna.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong HashLen0to16(byte* s, uint len)
+    {
+        unchecked
+        {
+            if (len >= 8)
+            {
+                ulong mul = k2 + len * 2;
+                ulong a = Fetch64(s) + k2;
+                ulong b = Fetch64(s + len - 8);
+                ulong c = Rotate64(b, 37) * mul + a;
+                ulong d = (Rotate64(a, 25) + b) * mul;
+                return HashLen16(c, d, mul);
+            }
+            if (len >= 4)
+            {
+                ulong mul = k2 + len * 2;
+                ulong a = Fetch32(s);
+                return HashLen16(len + (a << 3), Fetch32(s + len - 4), mul);
+            }
+            if (len > 0)
+            {
+                ushort a = s[0];
+                ushort b = s[len >> 1];
+                ushort c = s[len - 1];
+                uint y = a + ((uint)b << 8);
+                uint z = len + ((uint)c << 2);
+                return ShiftMix(y * k2 ^ z * k0) * k2;
+            }
+            return k2;
+        }
+    }
+
+    // 17-32 farmhashna.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong HashLen17to32(byte* s, uint len)
+    {
+        unchecked
+        {
+            ulong mul = k2 + len * 2;
+            ulong a = Fetch64(s) * k1;
+            ulong b = Fetch64(s + 8);
+            ulong c = Fetch64(s + len - 8) * mul;
+            ulong d = Fetch64(s + len - 16) * k2;
+            return HashLen16(Rotate64(a + b, 43) + Rotate64(c, 30) + d,
+                             a + Rotate64(b + k2, 18) + c, mul);
+        }
+    }
+
+    // farmhashxo.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong H32(byte* s, uint len, ulong mul, ulong seed0 = 0, ulong seed1 = 0)
+    {
+        unchecked
+        {
+            ulong a = Fetch64(s) * k1;
+            ulong b = Fetch64(s + 8);
+            ulong c = Fetch64(s + len - 8) * mul;
+            ulong d = Fetch64(s + len - 16) * k2;
+            ulong u = Rotate64(a + b, 43) + Rotate64(c, 30) + d + seed0;
+            ulong v = a + Rotate64(b + k2, 18) + c + seed1;
+            a = ShiftMix((u ^ v) * mul);
+            b = ShiftMix((v ^ a) * mul);
+            return b;
+        }
+    }
+
+    // 33-64 farmhashxo.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong HashLen33to64(byte* s, uint len)
+    {
+        const ulong mul0 = k2 - 30;
+
+        unchecked
+        {
+            ulong mul1 = k2 - 30 + 2 * len;
+            ulong h0 = H32(s, 32, mul0);
+            ulong h1 = H32(s + len - 32, 32, mul1);
+            return (h1 * mul1 + h0) * mul1;
+        }
+    }
+
+    // 65-96 farmhashxo.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong HashLen65to96(byte* s, uint len)
+    {
+        const ulong mul0 = k2 - 114;
+
+        unchecked
+        {
+            ulong mul1 = k2 - 114 + 2 * len;
+            ulong h0 = H32(s, 32, mul0);
+            ulong h1 = H32(s + 32, 32, mul1);
+            ulong h2 = H32(s + len - 32, 32, mul1, h0, h1);
+            return (h2 * 9 + (h0 >> 17) + (h1 >> 21)) * mul1;
+        }
+    }
+
+    // farmhashna.cc
+    // Return a 16-byte hash for 48 bytes.  Quick and dirty.
+    // Callers do best to use "random-looking" values for a and b.
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe Pair WeakHashLen32WithSeeds(ulong w, ulong x, ulong y, ulong z, ulong a, ulong b)
+    {
+        unchecked
+        {
+            a += w;
+            b = Rotate64(b + a + z, 21);
+            ulong c = a;
+            a += x;
+            a += y;
+            b += Rotate64(a, 44);
+            return make_pair(a + z, b + c);
+        }
+    }
+
+    // farmhashna.cc
+    // Return a 16-byte hash for s[0] ... s[31], a, and b.  Quick and dirty.
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe Pair WeakHashLen32WithSeeds(byte* s, ulong a, ulong b)
+    {
+        return WeakHashLen32WithSeeds(Fetch64(s),
+                                      Fetch64(s + 8),
+                                      Fetch64(s + 16),
+                                      Fetch64(s + 24),
+                                      a,
+                                      b);
+    }
+
+    // na(97-256) farmhashna.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong Hash64NA(byte* s, uint len)
+    {
+        const ulong seed = 81;
+
+        unchecked
+        {
+            // For strings over 64 bytes we loop.  Internal state consists of
+            // 56 bytes: v, w, x, y, and z.
+            ulong x = seed;
+            ulong y = seed * k1 + 113;
+            ulong z = ShiftMix(y * k2 + 113) * k2;
+            var v = make_pair(0, 0);
+            var w = make_pair(0, 0);
+            x = x * k2 + Fetch64(s);
+
+            // Set end so that after the loop we have 1 to 64 bytes left to process.
+            byte* end = s + ((len - 1) / 64) * 64;
+            byte* last64 = end + ((len - 1) & 63) - 63;
+
+            do
+            {
+                x = Rotate64(x + y + v.first + Fetch64(s + 8), 37) * k1;
+                y = Rotate64(y + v.second + Fetch64(s + 48), 42) * k1;
+                x ^= w.second;
+                y += v.first + Fetch64(s + 40);
+                z = Rotate64(z + w.first, 33) * k1;
+                v = WeakHashLen32WithSeeds(s, v.second * k1, x + w.first);
+                w = WeakHashLen32WithSeeds(s + 32, z + w.second, y + Fetch64(s + 16));
+                swap(ref z, ref x);
+                s += 64;
+            } while (s != end);
+            ulong mul = k1 + ((z & 0xff) << 1);
+            // Make s point to the last 64 bytes of input.
+            s = last64;
+            w.first += ((len - 1) & 63);
+            v.first += w.first;
+            w.first += v.first;
+            x = Rotate64(x + y + v.first + Fetch64(s + 8), 37) * mul;
+            y = Rotate64(y + v.second + Fetch64(s + 48), 42) * mul;
+            x ^= w.second * 9;
+            y += v.first * 9 + Fetch64(s + 40);
+            z = Rotate64(z + w.first, 33) * mul;
+            v = WeakHashLen32WithSeeds(s, v.second * mul, x + w.first);
+            w = WeakHashLen32WithSeeds(s + 32, z + w.second, y + Fetch64(s + 16));
+            swap(ref z, ref x);
+            return HashLen16(HashLen16(v.first, w.first, mul) + ShiftMix(y) * k0 + z,
+                             HashLen16(v.second, w.second, mul) + x,
+                             mul);
+        }
+    }
+
+    // farmhashuo.cc
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static ulong H(ulong x, ulong y, ulong mul, int r)
+    {
+        unchecked
+        {
+            ulong a = (x ^ y) * mul;
+            a ^= (a >> 47);
+            ulong b = (y ^ a) * mul;
+            return Rotate64(b, r) * mul;
+        }
+    }
+
+    // uo(257-) farmhashuo.cc, Hash64WithSeeds
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static unsafe ulong Hash64UO(byte* s, uint len)
+    {
+        const ulong seed0 = 81;
+        const ulong seed1 = 0;
+
+        unchecked
+        {
+            // For strings over 64 bytes we loop.  Internal state consists of
+            // 64 bytes: u, v, w, x, y, and z.
+            ulong x = seed0;
+            ulong y = seed1 * k2 + 113;
+            ulong z = ShiftMix(y * k2) * k2;
+            var v = make_pair(seed0, seed1);
+            var w = make_pair(0, 0);
+            ulong u = x - z;
+            x *= k2;
+            ulong mul = k2 + (u & 0x82);
+
+            // Set end so that after the loop we have 1 to 64 bytes left to process.
+            byte* end = s + ((len - 1) / 64) * 64;
+            byte* last64 = end + ((len - 1) & 63) - 63;
+
+            do
+            {
+                ulong a0 = Fetch64(s);
+                ulong a1 = Fetch64(s + 8);
+                ulong a2 = Fetch64(s + 16);
+                ulong a3 = Fetch64(s + 24);
+                ulong a4 = Fetch64(s + 32);
+                ulong a5 = Fetch64(s + 40);
+                ulong a6 = Fetch64(s + 48);
+                ulong a7 = Fetch64(s + 56);
+                x += a0 + a1;
+                y += a2;
+                z += a3;
+                v.first += a4;
+                v.second += a5 + a1;
+                w.first += a6;
+                w.second += a7;
+
+                x = Rotate64(x, 26);
+                x *= 9;
+                y = Rotate64(y, 29);
+                z *= mul;
+                v.first = Rotate64(v.first, 33);
+                v.second = Rotate64(v.second, 30);
+                w.first ^= x;
+                w.first *= 9;
+                z = Rotate64(z, 32);
+                z += w.second;
+                w.second += z;
+                z *= 9;
+                swap(ref u, ref y);
+
+                z += a0 + a6;
+                v.first += a2;
+                v.second += a3;
+                w.first += a4;
+                w.second += a5 + a6;
+                x += a1;
+                y += a7;
+
+                y += v.first;
+                v.first += x - y;
+                v.second += w.first;
+                w.first += v.second;
+                w.second += x - y;
+                x += w.second;
+                w.second = Rotate64(w.second, 34);
+                swap(ref u, ref z);
+                s += 64;
+            } while (s != end);
+            // Make s point to the last 64 bytes of input.
+            s = last64;
+            u *= 9;
+            v.second = Rotate64(v.second, 28);
+            v.first = Rotate64(v.first, 20);
+            w.first += ((len - 1) & 63);
+            u += y;
+            y += u;
+            x = Rotate64(y - x + v.first + Fetch64(s + 8), 37) * mul;
+            y = Rotate64(y ^ v.second ^ Fetch64(s + 48), 42) * mul;
+            x ^= w.second * 9;
+            y += v.first + Fetch64(s + 40);
+            z = Rotate64(z + w.first, 33) * mul;
+            v = WeakHashLen32WithSeeds(s, v.second * mul, x + w.first);
+            w = WeakHashLen32WithSeeds(s + 32, z + w.second, y + Fetch64(s + 16));
+            return H(HashLen16(v.first + x, w.first ^ y, mul) + z - u,
+                     H(v.second + y, w.second + z, k2, 30) ^ x,
+                     k2,
+                     31);
+        }
+    }
+}

+ 109 - 0
src/Lua/Internal/FastListCore.cs

@@ -0,0 +1,109 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Lua.Internal;
+
+/// <summary>
+/// A list of minimal features. Note that it is NOT thread-safe and must NOT be marked readonly as it is a mutable struct.
+/// </summary>
+/// <typeparam name="T">Element type</typeparam>
+[StructLayout(LayoutKind.Auto)]
+public struct FastListCore<T>
+{
+    const int InitialCapacity = 8;
+
+    public static readonly FastListCore<T> Empty = default;
+
+    T[]? array;
+    int tailIndex;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Add(T element)
+    {
+        if (array == null)
+        {
+            array = new T[InitialCapacity];
+        }
+        else if (array.Length == tailIndex)
+        {
+            Array.Resize(ref array, tailIndex * 2);
+        }
+
+        array[tailIndex] = element;
+        tailIndex++;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void RemoveAtSwapback(int index)
+    {
+        if (array == null) throw new IndexOutOfRangeException();
+        CheckIndex(index);
+
+        array![index] = array[tailIndex - 1];
+        array[tailIndex - 1] = default!;
+        tailIndex--;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Shrink(int newSize)
+    {
+        if (newSize >= tailIndex) return;
+
+        array.AsSpan(newSize).Clear();
+        tailIndex = newSize;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Clear(bool removeArray = false)
+    {
+        if (array == null) return;
+
+        array.AsSpan().Clear();
+        tailIndex = 0;
+        if (removeArray) array = null;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void EnsureCapacity(int capacity)
+    {
+        if (array == null)
+        {
+            array = new T[InitialCapacity];
+        }
+
+        while (array.Length < capacity)
+        {
+            Array.Resize(ref array, array.Length * 2);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void CopyTo(ref FastListCore<T> destination)
+    {
+        destination.EnsureCapacity(tailIndex);
+        destination.tailIndex = tailIndex;
+        AsSpan().CopyTo(destination.AsSpan());
+    }
+
+    public readonly T this[int index]
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        get => array![index];
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        set => array![index] = value;
+    }
+
+    public readonly int Length
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        get => tailIndex;
+    }
+
+    public readonly Span<T> AsSpan() => array == null ? Span<T>.Empty : array.AsSpan(0, tailIndex);
+    public readonly T[]? AsArray() => array;
+
+    readonly void CheckIndex(int index)
+    {
+        if (index < 0 || index > tailIndex) throw new IndexOutOfRangeException();
+    }
+}

+ 88 - 0
src/Lua/Internal/FastStackCore.cs

@@ -0,0 +1,88 @@
+using System.Runtime.InteropServices;
+
+namespace Lua.Internal;
+
+[StructLayout(LayoutKind.Auto)]
+public struct FastStackCore<T>
+{
+    const int InitialCapacity = 8;
+
+    T?[] array;
+    int tail;
+
+    public int Size => tail;
+
+    public readonly ReadOnlySpan<T> AsSpan()
+    {
+        if (array == null) return [];
+        return array.AsSpan(0, tail);
+    }
+
+    public readonly T? this[int index]
+    {
+        get
+        {
+            return array[index];
+        }
+    }
+
+    public void Push(in T item)
+    {
+        array ??= new T[InitialCapacity];
+
+        if (tail == array.Length)
+        {
+            var newArray = new T[tail * 2];
+            Array.Copy(array, newArray, tail);
+            array = newArray;
+        }
+
+        array[tail] = item;
+        tail++;
+    }
+
+    public bool TryPop(out T value)
+    {
+        if (tail == 0)
+        {
+            value = default!;
+            return false;
+        }
+
+        tail--;
+        value = array[tail]!;
+        array[tail] = default;
+
+        return true;
+    }
+
+    public T Pop()
+    {
+        if (!TryPop(out var result)) throw new InvalidOperationException("Empty stack");
+        return result;
+    }
+
+    public bool TryPeek(out T value)
+    {
+        if (tail == 0)
+        {
+            value = default!;
+            return false;
+        }
+
+        value = array[tail - 1]!;
+        return true;
+    }
+
+    public T Peek()
+    {
+        if (!TryPeek(out var result)) throw new InvalidOperationException();
+        return result;
+    }
+
+    public void Clear()
+    {
+        array.AsSpan(0, tail).Clear();
+        tail = 0;
+    }
+}

+ 51 - 0
src/Lua/Internal/MathEx.cs

@@ -0,0 +1,51 @@
+using System.Runtime.CompilerServices;
+
+namespace Lua;
+
+internal static class MathEx
+{
+    const ulong PositiveInfinityBits = 0x7FF0_0000_0000_0000;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static bool IsInteger(double d)
+    {
+#if NET8_0_OR_GREATER
+        return double.IsInteger(d);
+#else
+        return IsFinite(d) && (d == Math.Truncate(d));
+#endif
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static unsafe bool IsFinite(double d)
+    {
+#if NET6_0_OR_GREATER
+        ulong bits = BitConverter.DoubleToUInt64Bits(d);
+#else
+        ulong bits = BitCast<double, ulong>(d);
+#endif
+
+        return (~bits & PositiveInfinityBits) != 0;
+    }
+
+#if !NET6_0_OR_GREATER
+    unsafe static TTo BitCast<TFrom, TTo>(TFrom source)
+    {
+        return Unsafe.ReadUnaligned<TTo>(ref Unsafe.As<TFrom, byte>(ref source));
+    }
+#endif
+
+    public const int ArrayMexLength = 0x7FFFFFC7;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static int NewArrayCapacity(int size)
+    {
+        var newSize = unchecked(size * 2);
+        if ((uint)newSize > ArrayMexLength)
+        {
+            newSize = ArrayMexLength;
+        }
+
+        return newSize;
+    }
+}

+ 47 - 0
src/Lua/Internal/PooledArray.cs

@@ -0,0 +1,47 @@
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
+namespace Lua.Internal;
+
+public struct PooledArray<T>(int sizeHint) : IDisposable
+{
+    T[]? array = ArrayPool<T>.Shared.Rent(sizeHint);
+
+    public ref T this[int index]
+    {
+        get
+        {
+            ThrowIfDisposed();
+            return ref array![index];
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Span<T> AsSpan()
+    {
+        ThrowIfDisposed();
+        return array;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Memory<T> AsMemory()
+    {
+        ThrowIfDisposed();
+        return array;
+    }
+
+    public void Dispose()
+    {
+        ThrowIfDisposed();
+        ArrayPool<T>.Shared.Return(array!);
+        array = null;
+    }
+
+    void ThrowIfDisposed()
+    {
+        if (array == null)
+        {
+            throw new ObjectDisposedException(nameof(PooledArray<T>));
+        }
+    }
+}

+ 83 - 0
src/Lua/Internal/PooledList.cs

@@ -0,0 +1,83 @@
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
+namespace Lua.Internal;
+
+internal ref struct PooledList<T>
+{
+    T[]? buffer;
+    int tail;
+
+    public PooledList(int sizeHint)
+    {
+        buffer = ArrayPool<T>.Shared.Rent(sizeHint);
+    }
+
+    public bool IsDisposed => tail == -1;
+    public int Count => tail;
+
+    public void Add(in T item)
+    {
+        ThrowIfDisposed();
+
+        if (buffer == null)
+        {
+            buffer = ArrayPool<T>.Shared.Rent(32);
+        }
+        else if (buffer.Length == tail)
+        {
+            var newArray = ArrayPool<T>.Shared.Rent(tail * 2);
+            buffer.AsSpan().CopyTo(newArray);
+            ArrayPool<T>.Shared.Return(buffer);
+            buffer = newArray;
+        }
+
+        buffer[tail] = item;
+        tail++;
+    }
+
+    public void Clear()
+    {
+        ThrowIfDisposed();
+
+        if (buffer != null)
+        {
+            new Span<T>(buffer, 0, tail).Clear();
+        }
+
+        tail = 0;
+    }
+
+    public void Dispose()
+    {
+        ThrowIfDisposed();
+
+        if (buffer != null)
+        {
+            ArrayPool<T>.Shared.Return(buffer);
+            buffer = null;
+        }
+
+        tail = -1;
+    }
+
+    public T this[int index]
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        get
+        {
+            return AsSpan()[index];
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public ReadOnlySpan<T> AsSpan()
+    {
+        return new ReadOnlySpan<T>(buffer, 0, tail);
+    }
+
+    void ThrowIfDisposed()
+    {
+        if (tail == -1) throw new ObjectDisposedException(nameof(PooledList<T>));
+    }
+}

+ 20 - 0
src/Lua/Internal/Utf16StringMemoryComparer.cs

@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Lua.Internal;
+
+public sealed class Utf16StringMemoryComparer : IEqualityComparer<ReadOnlyMemory<char>>
+{
+    public static readonly Utf16StringMemoryComparer Default = new();
+
+    public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y)
+    {
+        return x.Span.SequenceEqual(y.Span);
+    }
+
+    public int GetHashCode(ReadOnlyMemory<char> obj)
+    {
+        var span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(obj.Span)), obj.Length * 2);
+        return (int)unchecked(FarmHash.Hash64(span));
+    }
+}

+ 19 - 0
src/Lua/Lua.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>net8.0; net6.0; netstandard2.1</TargetFrameworks>
+    <LangVersion>12</LangVersion>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="PolySharp" Version="1.14.1">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
+  </ItemGroup>
+
+</Project>

+ 29 - 0
src/Lua/LuaFunction.Create.cs

@@ -0,0 +1,29 @@
+namespace Lua;
+
+partial class LuaFunction
+{
+    sealed class AnonymousLuaFunction(Func<LuaValue[], CancellationToken, ValueTask<LuaValue[]>> func) : LuaFunction
+    {
+        protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+        {
+            var args = context.ArgumentCount == 0 ? [] : new LuaValue[context.ArgumentCount];
+            context.Arguments.CopyTo(args);
+
+            var result = await func(args, cancellationToken);
+            if (result != null)
+            {
+                result.AsMemory().CopyTo(buffer);
+                return result.Length;
+            }
+            else
+            {
+                return 0;
+            }
+        }
+    }
+
+    public static LuaFunction Create(Func<LuaValue[], CancellationToken, ValueTask<LuaValue[]>> func)
+    {
+        return new AnonymousLuaFunction(func);
+    }
+}

+ 40 - 0
src/Lua/LuaFunction.cs

@@ -0,0 +1,40 @@
+using Lua.Runtime;
+
+namespace Lua;
+
+public abstract partial class LuaFunction
+{
+    public async ValueTask<int> InvokeAsync(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var state = context.State;
+
+        var frame = new CallStackFrame
+        {
+            Base = context.StackPosition == null ? state.Stack.Count - context.ArgumentCount : context.StackPosition.Value,
+            CallPosition = context.SourcePosition,
+            ChunkName = context.ChunkName ?? LuaState.DefaultChunkName,
+            RootChunkName = context.RootChunkName ?? LuaState.DefaultChunkName,
+            VariableArgumentCount = this is Closure closure ? context.ArgumentCount - closure.Proto.ParameterCount : 0,
+        };
+
+        state.PushCallStackFrame(frame);
+        try
+        {
+            return await InvokeAsyncCore(context, buffer, cancellationToken);
+        }
+        finally
+        {
+            state.PopCallStackFrame();
+        }
+    }
+
+    protected abstract ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken);
+
+    protected static void ThrowIfArgumentNotExists(LuaFunctionExecutionContext context, string chunkName, int index)
+    {
+        if (context.ArgumentCount == index)
+        {
+            LuaRuntimeException.BadArgument(context.State.GetTracebacks(), index + 1, chunkName);
+        }
+    }
+}

+ 16 - 0
src/Lua/LuaFunctionExecutionContext.cs

@@ -0,0 +1,16 @@
+using Lua.CodeAnalysis;
+
+namespace Lua;
+
+public readonly record struct LuaFunctionExecutionContext
+{
+    public required LuaState State { get; init; }
+    public required int ArgumentCount { get; init; }
+    public int? StackPosition { get; init; }
+    public SourcePosition? SourcePosition { get; init; }
+    public string? RootChunkName { get; init; }
+    public string? ChunkName { get; init; }
+
+    public int FrameBase => State.GetCurrentFrame().Base;
+    public ReadOnlySpan<LuaValue> Arguments => State.GetStackValues().Slice(FrameBase, ArgumentCount);
+}

+ 109 - 0
src/Lua/LuaState.cs

@@ -0,0 +1,109 @@
+using Lua.Internal;
+using Lua.Runtime;
+
+namespace Lua;
+
+public sealed class LuaState
+{
+    public const string DefaultChunkName = "chunk";
+
+    LuaStack stack = new();
+    FastStackCore<CallStackFrame> callStack;
+    FastListCore<UpValue> openUpValues;
+
+    LuaTable environment;
+    internal UpValue EnvUpValue { get; }
+
+    internal LuaStack Stack => stack;
+
+    public LuaTable Environment => environment;
+
+    public static LuaState Create()
+    {
+        return new();
+    }
+
+    LuaState()
+    {
+        environment = new();
+        EnvUpValue = UpValue.Closed(environment);
+    }
+
+    public ReadOnlySpan<LuaValue> GetStackValues()
+    {
+        return stack.AsSpan();
+    }
+
+    public void Push(LuaValue value)
+    {
+        stack.Push(value);
+    }
+
+    internal void Reset()
+    {
+        stack.Clear();
+        callStack.Clear();
+    }
+
+    internal void PushCallStackFrame(CallStackFrame frame)
+    {
+        callStack.Push(frame);
+    }
+
+    internal void PopCallStackFrame()
+    {
+        var frame = callStack.Pop();
+        stack.PopUntil(frame.Base);
+    }
+
+    public CallStackFrame GetCurrentFrame()
+    {
+        return callStack.Peek();
+    }
+
+    internal Tracebacks GetTracebacks()
+    {
+        return new()
+        {
+            StackFrames = callStack.AsSpan()[1..].ToArray()
+        };
+    }
+
+    internal UpValue GetOrAddUpValue(int registerIndex)
+    {
+        foreach (var upValue in openUpValues.AsSpan())
+        {
+            if (upValue.RegisterIndex == registerIndex)
+            {
+                return upValue;
+            }
+        }
+
+        var newUpValue = UpValue.Open(registerIndex);
+        openUpValues.Add(newUpValue);
+        return newUpValue;
+    }
+
+    internal void CloseUpValues(int frameBase)
+    {
+        for (int i = 0; i < openUpValues.Length; i++)
+        {
+            var upValue = openUpValues[i];
+            if (upValue.RegisterIndex >= frameBase)
+            {
+                upValue.Close(this);
+                openUpValues.RemoveAtSwapback(i);
+                i--;
+            }
+        }
+    }
+
+    internal void DumpStackValues()
+    {
+        var span = GetStackValues();
+        for (int i = 0; i < span.Length; i++)
+        {
+            Console.WriteLine($"LuaStack [{i}]\t{span[i]}");
+        }
+    }
+}

+ 66 - 0
src/Lua/LuaStateExtensions.cs

@@ -0,0 +1,66 @@
+using System.Buffers;
+using Lua.Runtime;
+using Lua.CodeAnalysis.Compilation;
+using Lua.CodeAnalysis.Syntax;
+
+namespace Lua;
+
+public static class LuaStateExtensions
+{
+    public static ValueTask<int> RunAsync(this LuaState state, Chunk chunk, Memory<LuaValue> buffer, CancellationToken cancellationToken = default)
+    {
+        return new Closure(state, chunk).InvokeAsync(new()
+        {
+            State = state,
+            ArgumentCount = 0,
+            StackPosition = state.Stack.Count,
+            SourcePosition = null,
+            RootChunkName = chunk.Name ?? LuaState.DefaultChunkName,
+            ChunkName = chunk.Name ?? LuaState.DefaultChunkName,
+        }, buffer, cancellationToken);
+    }
+
+    public static ValueTask<int> DoStringAsync(this LuaState state, string source, Memory<LuaValue> buffer, string? chunkName = null, CancellationToken cancellationToken = default)
+    {
+        var syntaxTree = LuaSyntaxTree.Parse(source);
+        var chunk = LuaCompiler.Default.Compile(syntaxTree, chunkName);
+        return RunAsync(state, chunk, buffer, cancellationToken);
+    }
+
+    public static async ValueTask<LuaValue[]> DoStringAsync(this LuaState state, string source, string? chunkName = null, CancellationToken cancellationToken = default)
+    {
+        var buffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+        try
+        {
+            var resultCount = await DoStringAsync(state, source, buffer, chunkName, cancellationToken);
+            return buffer.AsSpan(0, resultCount).ToArray();
+        }
+        finally
+        {
+            ArrayPool<LuaValue>.Shared.Return(buffer);
+        }
+    }
+
+    public static async ValueTask<int> DoFileAsync(this LuaState state, string path, Memory<LuaValue> buffer, CancellationToken cancellationToken = default)
+    {
+        var text = await File.ReadAllTextAsync(path, cancellationToken);
+        var fileName = Path.GetFileName(path);
+        var syntaxTree = LuaSyntaxTree.Parse(text, fileName);
+        var chunk = LuaCompiler.Default.Compile(syntaxTree, fileName);
+        return await RunAsync(state, chunk, buffer, cancellationToken);
+    }
+
+    public static async ValueTask<LuaValue[]> DoFileAsync(this LuaState state, string path, CancellationToken cancellationToken = default)
+    {
+        var buffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+        try
+        {
+            var resultCount = await DoFileAsync(state, path, buffer, cancellationToken);
+            return buffer.AsSpan(0, resultCount).ToArray();
+        }
+        finally
+        {
+            ArrayPool<LuaValue>.Shared.Return(buffer);
+        }
+    }
+}

+ 173 - 0
src/Lua/LuaTable.cs

@@ -0,0 +1,173 @@
+using System.Runtime.CompilerServices;
+using Lua.Runtime;
+
+namespace Lua;
+
+public sealed class LuaTable
+{
+    public LuaTable() : this(8, 8)
+    {
+    }
+
+    public LuaTable(IEnumerable<LuaValue> values)
+    {
+        array = values.ToArray();
+        dictionary = [];
+    }
+
+    public LuaTable(IEnumerable<KeyValuePair<LuaValue, LuaValue>> values)
+    {
+        array = [];
+        dictionary = new Dictionary<LuaValue, LuaValue>(values);
+    }
+
+    public LuaTable(int arrayCapacity, int dictionaryCapacity)
+    {
+        array = new LuaValue[arrayCapacity];
+        dictionary = new(dictionaryCapacity);
+    }
+
+    LuaValue[] array;
+    Dictionary<LuaValue, LuaValue> dictionary;
+    LuaTable? metatable;
+
+    public LuaValue this[LuaValue key]
+    {
+        get
+        {
+            if (key.Type is LuaValueType.Nil)
+            {
+                throw new ArgumentException("table index is nil");
+            }
+
+            if (TryGetInteger(key, out var index))
+            {
+                if (index > 0 && index <= array.Length)
+                {
+                    // Arrays in Lua are 1-origin...
+                    return array[index - 1];
+                }
+            }
+            
+            if (dictionary.TryGetValue(key, out var value)) return value;
+            return LuaValue.Nil;
+        }
+        set
+        {
+            if (TryGetInteger(key, out var index))
+            {
+                if (0 < index && index <= array.Length * 2)
+                {
+                    EnsureArrayCapacity(index);
+                    array[index - 1] = value;
+                    return;
+                }
+            }
+
+            if (value.Type is LuaValueType.Nil)
+            {
+                dictionary.Remove(key);
+            }
+            else
+            {
+                dictionary[key] = value;
+            }
+        }
+    }
+
+    public int HashMapCount
+    {
+        get => dictionary.Count;
+    }
+
+    public int ArrayLength
+    {
+        get
+        {
+            for (int i = 0; i < array.Length; i++)
+            {
+                if (array[i].Type is LuaValueType.Nil) return i;
+            }
+            return array.Length;
+        }
+    }
+
+    public LuaTable? Metatable
+    {
+        get => metatable;
+        set => metatable = value;
+    }
+
+    public bool TryGetValue(LuaValue key, out LuaValue value)
+    {
+        if (key.Type is LuaValueType.Nil)
+        {
+            value = default;
+            return false;
+        }
+
+        if (TryGetInteger(key, out var index))
+        {
+            if (index > 0 && index <= array.Length)
+            {
+                value = array[index - 1];
+                return true;
+            }
+        }
+
+        return dictionary.TryGetValue(key, out value);
+    }
+
+    public bool ContainsKey(LuaValue key)
+    {
+        if (key.Type is LuaValueType.Nil)
+        {
+            return false;
+        }
+
+        if (TryGetInteger(key, out var index))
+        {
+            return index > 0 && index <= array.Length && array[index].Type != LuaValueType.Nil;
+        }
+
+        return dictionary.ContainsKey(key);
+    }
+
+    public void Clear()
+    {
+        dictionary.Clear();
+    }
+
+    public Span<LuaValue> GetArraySpan()
+    {
+        return array.AsSpan();
+    }
+    
+    internal void EnsureArrayCapacity(int newCapacity)
+    {
+        if (array.Length >= newCapacity) return;
+
+        var newSize = array.Length;
+        if (newSize == 0) newSize = 8;
+
+        while (newSize < newCapacity)
+        {
+            newSize *= 2;
+        }
+
+        Array.Resize(ref array, newSize);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static bool TryGetInteger(LuaValue value, out int integer)
+    {
+        if (value.TryRead<double>(out var num) && MathEx.IsInteger(num))
+        {
+            integer = (int)num;
+            return true;
+        }
+
+        integer = default;
+        return false;
+    }
+}

+ 283 - 0
src/Lua/LuaValue.cs

@@ -0,0 +1,283 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Lua;
+
+public enum LuaValueType : byte
+{
+    Nil,
+    Boolean,
+    String,
+    Number,
+    Function,
+    UserData,
+    Table,
+}
+
+[StructLayout(LayoutKind.Auto)]
+public readonly struct LuaValue : IEquatable<LuaValue>
+{
+    public static readonly LuaValue Nil = default;
+
+    readonly LuaValueType type;
+    readonly double value;
+    readonly object? referenceValue;
+
+    public LuaValueType Type => type;
+
+    public bool TryRead<T>(out T result)
+    {
+        var t = typeof(T);
+
+        switch (type)
+        {
+            case LuaValueType.Number:
+                if (t == typeof(int))
+                {
+                    var v = (int)value;
+                    result = Unsafe.As<int, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(long))
+                {
+                    var v = (long)value;
+                    result = Unsafe.As<long, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(float))
+                {
+                    var v = (float)value;
+                    result = Unsafe.As<float, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(double))
+                {
+                    var v = value;
+                    result = Unsafe.As<double, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(object))
+                {
+                    result = (T)(object)value;
+                    return true;
+                }
+                else
+                {
+                    break;
+                }
+            case LuaValueType.Boolean:
+                if (t == typeof(bool))
+                {
+                    var v = value == 1;
+                    result = Unsafe.As<bool, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(object))
+                {
+                    result = (T)(object)value;
+                    return true;
+                }
+                else
+                {
+                    break;
+                }
+            case LuaValueType.String:
+                if (t == typeof(string))
+                {
+                    var v = (string)referenceValue!;
+                    result = Unsafe.As<string, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(object))
+                {
+                    result = (T)referenceValue!;
+                    return true;
+                }
+                else
+                {
+                    break;
+                }
+            case LuaValueType.Function:
+                if (t == typeof(LuaFunction) || t.IsSubclassOf(typeof(LuaFunction)))
+                {
+                    var v = (LuaFunction)referenceValue!;
+                    result = Unsafe.As<LuaFunction, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(object))
+                {
+                    result = (T)referenceValue!;
+                    return true;
+                }
+                else
+                {
+                    break;
+                }
+            case LuaValueType.UserData:
+                if (referenceValue is T userData)
+                {
+                    result = userData;
+                    return true;
+                }
+                break;
+            case LuaValueType.Table:
+                if (t == typeof(LuaTable))
+                {
+                    var v = (LuaTable)referenceValue!;
+                    result = Unsafe.As<LuaTable, T>(ref v);
+                    return true;
+                }
+                else if (t == typeof(object))
+                {
+                    result = (T)referenceValue!;
+                    return true;
+                }
+                else
+                {
+                    break;
+                }
+        }
+
+        result = default!;
+        return false;
+    }
+
+    public T Read<T>()
+    {
+        if (!TryRead<T>(out var result)) throw new InvalidOperationException($"Cannot convert LuaValueType.{Type} to {typeof(T).FullName}.");
+        return result;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool ToBoolean()
+    {
+        if (Type is LuaValueType.Nil) return false;
+        if (TryRead<bool>(out var result)) return result;
+        return true;
+    }
+
+    public LuaValue(bool value)
+    {
+        type = LuaValueType.Boolean;
+        this.value = value ? 1 : 0;
+    }
+
+    public LuaValue(double value)
+    {
+        type = LuaValueType.Number;
+        this.value = value;
+    }
+
+    public LuaValue(string value)
+    {
+        type = LuaValueType.String;
+        referenceValue = value;
+    }
+
+    public LuaValue(LuaFunction value)
+    {
+        type = LuaValueType.Function;
+        referenceValue = value;
+    }
+
+    public LuaValue(LuaTable value)
+    {
+        type = LuaValueType.Table;
+        referenceValue = value;
+    }
+
+    public LuaValue(object? value)
+    {
+        type = LuaValueType.UserData;
+        referenceValue = value;
+    }
+
+    public static implicit operator LuaValue(bool value)
+    {
+        return new(value);
+    }
+
+    public static implicit operator LuaValue(double value)
+    {
+        return new(value);
+    }
+
+    public static implicit operator LuaValue(string value)
+    {
+        return new(value);
+    }
+
+    public static implicit operator LuaValue(LuaTable value)
+    {
+        return new(value);
+    }
+
+    public static implicit operator LuaValue(LuaFunction value)
+    {
+        return new(value);
+    }
+
+    public override int GetHashCode()
+    {
+        var valueHash = type switch
+        {
+            LuaValueType.Nil => 0,
+            LuaValueType.Boolean => Read<bool>().GetHashCode(),
+            LuaValueType.String => Read<string>().GetHashCode(),
+            LuaValueType.Number => Read<double>().GetHashCode(),
+            LuaValueType.Function => Read<LuaFunction>().GetHashCode(),
+            LuaValueType.Table => Read<LuaTable>().GetHashCode(),
+            LuaValueType.UserData => referenceValue == null ? 0 : referenceValue.GetHashCode(),
+            _ => 0,
+        };
+
+        return HashCode.Combine(type, valueHash);
+    }
+
+    public bool Equals(LuaValue other)
+    {
+        if (other.Type != Type) return false;
+
+        return type switch
+        {
+            LuaValueType.Nil => true,
+            LuaValueType.Boolean => Read<bool>().Equals(other.Read<bool>()),
+            LuaValueType.String => Read<string>().Equals(other.Read<string>()),
+            LuaValueType.Number => Read<double>().Equals(other.Read<double>()),
+            LuaValueType.Function => Read<LuaFunction>().Equals(other.Read<LuaFunction>()),
+            LuaValueType.Table => Read<LuaTable>().Equals(other.Read<LuaTable>()),
+            LuaValueType.UserData => referenceValue == other.referenceValue,
+            _ => false,
+        };
+    }
+
+    public override bool Equals(object? obj)
+    {
+        return obj is LuaValue value1 && Equals(value1);
+    }
+
+    public static bool operator ==(LuaValue a, LuaValue b)
+    {
+        return a.Equals(b);
+    }
+
+    public static bool operator !=(LuaValue a, LuaValue b)
+    {
+        return !a.Equals(b);
+    }
+
+    public override string? ToString()
+    {
+        return type switch
+        {
+            LuaValueType.Nil => "Nil",
+            LuaValueType.Boolean => Read<bool>().ToString(),
+            LuaValueType.String => Read<string>().ToString(),
+            LuaValueType.Number => Read<double>().ToString(),
+            LuaValueType.Function => Read<LuaFunction>().ToString(),
+            LuaValueType.Table => Read<LuaTable>().ToString(),
+            LuaValueType.UserData => referenceValue?.ToString(),
+            _ => "",
+        };
+    }
+}

+ 12 - 0
src/Lua/Runtime/CallStackFrame.cs

@@ -0,0 +1,12 @@
+using Lua.CodeAnalysis;
+
+namespace Lua.Runtime;
+
+public record struct CallStackFrame
+{
+    public required int Base;
+    public required string ChunkName;
+    public required string RootChunkName;
+    public required SourcePosition? CallPosition;
+    public required int VariableArgumentCount;
+}

+ 23 - 0
src/Lua/Runtime/Chunk.cs

@@ -0,0 +1,23 @@
+using Lua.CodeAnalysis;
+
+namespace Lua.Runtime;
+
+public sealed class Chunk
+{
+    public Chunk? Parent { get; internal set; }
+
+    public required string Name { get; init; }
+    public required Instruction[] Instructions { get; init; }
+    public required SourcePosition[] SourcePositions { get; init; }
+    public required LuaValue[] Constants { get; init; }
+    public required UpValueInfo[] UpValues { get; init; }
+    public required Chunk[] Functions { get; init; }
+    public required int ParameterCount { get; init; }
+    public required bool HasVariableArgments { get; init; }
+
+    internal Chunk GetRoot()
+    {
+        if (Parent == null) return this;
+        return Parent.GetRoot();
+    }
+}

+ 46 - 0
src/Lua/Runtime/Closure.cs

@@ -0,0 +1,46 @@
+using Lua.Internal;
+
+namespace Lua.Runtime;
+
+public sealed class Closure : LuaFunction
+{
+    Chunk proto;
+    FastListCore<UpValue> upValues;
+
+    public Closure(LuaState state, Chunk proto)
+    {
+        this.proto = proto;
+
+        // add upvalues
+        for (int i = 0; i < proto.UpValues.Length; i++)
+        {
+            var description = proto.UpValues[i];
+            var upValue = GetUpValueFromDescription(state, proto, description);
+            upValues.Add(upValue);
+        }
+    }
+
+    public Chunk Proto => proto;
+    public ReadOnlySpan<UpValue> UpValues => upValues.AsSpan();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        return LuaVirtualMachine.ExecuteClosureAsync(context.State, this, context.State.GetCurrentFrame(), buffer, cancellationToken);
+    }
+
+    static UpValue GetUpValueFromDescription(LuaState state, Chunk proto, UpValueInfo description)
+    {
+        if (description.IsInRegister)
+        {
+            return state.GetOrAddUpValue(state.GetCurrentFrame().Base + description.Index);
+        }
+        else if (description.Index == -1) // -1 is global environment
+        {
+            return state.EnvUpValue;
+        }
+        else
+        {
+            return GetUpValueFromDescription(state, proto.Parent!, proto.Parent!.UpValues[description.Index]);
+        }
+    }
+}

+ 671 - 0
src/Lua/Runtime/Instruction.cs

@@ -0,0 +1,671 @@
+namespace Lua.Runtime;
+
+public struct Instruction : IEquatable<Instruction>
+{
+    uint _value;
+
+    public uint Value
+    {
+        get => _value;
+        set => _value = value;
+    }
+
+    public OpCode OpCode
+    {
+        get => (OpCode)(byte)(_value & 0x3F); // 6 bits
+        set => _value = (_value & 0xFFFFFFC0) | ((uint)value & 0x3F);
+    }
+
+    public byte A
+    {
+        get => (byte)((_value >> 6) & 0xFF); // 8 bits
+        set => _value = (_value & 0xFFFFC03F) | (((uint)value & 0xFF) << 6);
+    }
+
+    public ushort B
+    {
+        get => (ushort)((_value >> 23) & 0x1FF); // 9 bits
+        set => _value = (_value & 0xC07FFFFF) | (((uint)value & 0x1FF) << 23);
+    }
+
+    public ushort C
+    {
+        get => (ushort)((_value >> 14) & 0x1FF); // 9 bits
+        set => _value = (_value & 0xFF803FFF) | (((uint)value & 0x1FF) << 14);
+    }
+
+    public uint Bx
+    {
+        get => (_value >> 14) & 0x3FFFF; // 18 bits (14-31)
+        set => _value = (_value & 0x00003FFF) | ((value & 0x3FFFF) << 14);
+    }
+
+    public int SBx
+    {
+        get => (int)(Bx - 131071); // signed 18 bits
+        set => Bx = (uint)(value + 131071);
+    }
+
+    public uint Ax
+    {
+        get => (_value >> 6) & 0x3FFFFFF; // 26 bits (6-31)
+        set => _value = (_value & 0x0000003F) | ((value & 0x3FFFFFF) << 6);
+    }
+
+    public bool Equals(Instruction other)
+    {
+        return _value == other._value;
+    }
+
+    public override bool Equals(object? obj)
+    {
+        if (obj is Instruction instruction) return Equals(instruction);
+        return false;
+    }
+
+    public override int GetHashCode()
+    {
+        return _value.GetHashCode();
+    }
+
+    public override string ToString()
+    {
+        return OpCode switch
+        {
+            OpCode.Move => $"MOVE      {A} {B}",
+            OpCode.LoadK => $"LOADK     {A} {Bx}",
+            OpCode.LoadKX => $"LOADKX    {A}",
+            OpCode.LoadBool => $"LOADBOOL  {A} {B} {C}",
+            OpCode.LoadNil => $"LOADNIL   {A} {B}",
+            OpCode.GetUpVal => $"GETUPVAL  {A} {B}",
+            OpCode.GetTabUp => $"GETTABUP  {A} {B} {C}",
+            OpCode.GetTable => $"GETTABLE  {A} {B} {C}",
+            OpCode.SetTabUp => $"SETTABUP  {A} {B} {C}",
+            OpCode.SetUpVal => $"SETUPVAL  {A} {B}",
+            OpCode.SetTable => $"SETTABLE  {A} {B} {C}",
+            OpCode.NewTable => $"NEWTABLE  {A} {B} {C}",
+            OpCode.Self => $"SELF      {A} {B} {C}",
+            OpCode.Add => $"ADD       {A} {B} {C}",
+            OpCode.Sub => $"SUB       {A} {B} {C}",
+            OpCode.Mul => $"MUL       {A} {B} {C}",
+            OpCode.Div => $"DIV       {A} {B} {C}",
+            OpCode.Mod => $"MOD       {A} {B} {C}",
+            OpCode.Pow => $"POQ       {A} {B} {C}",
+            OpCode.Unm => $"UNM       {A} {B}",
+            OpCode.Not => $"NOT       {A} {B}",
+            OpCode.Len => $"LEN       {A} {B}",
+            OpCode.Concat => $"CONCAT    {A} {B} {C}",
+            OpCode.Jmp => $"JMP       {A} {SBx}",
+            OpCode.Eq => $"EQ        {A} {B} {C}",
+            OpCode.Lt => $"LT        {A} {B} {C}",
+            OpCode.Le => $"LE        {A} {B} {C}",
+            OpCode.Test => $"TEST      {A} {C}",
+            OpCode.TestSet => $"TESTSET   {A} {B} {C}",
+            OpCode.Call => $"CALL      {A} {B} {C}",
+            OpCode.TailCall => $"TAILCALL  {A} {B} {C}",
+            OpCode.Return => $"RETURN    {A} {B}",
+            OpCode.ForLoop => $"FORLOOP   {A} {SBx}",
+            OpCode.ForPrep => $"FORPREP   {A} {SBx}",
+            OpCode.TForCall => $"TFORCALL  {A} {C}",
+            OpCode.TForLoop => $"TFORLOOP  {A} {SBx}",
+            OpCode.SetList => $"SETLIST   {A} {B} {C}",
+            OpCode.Closure => $"CLOSURE   {A} {SBx}",
+            OpCode.VarArg => $"VARARG    {A} {B}",
+            OpCode.ExtraArg => $"EXTRAARG  {Ax}",
+            _ => "",
+        };
+    }
+
+    public static bool operator ==(Instruction left, Instruction right)
+    {
+        return left.Equals(right);
+    }
+
+    public static bool operator !=(Instruction left, Instruction right)
+    {
+        return !(left == right);
+    }
+
+    /// <summary>
+    /// R(A) := R(B)
+    /// </summary>
+    public static Instruction Move(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.Move,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := Kst(Bx)
+    /// </summary>
+    public static Instruction LoadK(byte a, uint bx)
+    {
+        return new()
+        {
+            OpCode = OpCode.LoadK,
+            A = a,
+            Bx = bx,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := Kst(extra arg)
+    /// </summary>
+    public static Instruction LoadKX(byte a)
+    {
+        return new()
+        {
+            OpCode = OpCode.LoadKX,
+            A = a,
+        };
+    }
+
+    /// <summary>
+    /// <para>R(A) := (Bool)B</para>
+    /// <para>if (C) pc++</para>
+    /// </summary>
+    public static Instruction LoadBool(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.LoadBool,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A), R(A+1), ..., R(A+B) := nil
+    /// </summary>
+    public static Instruction LoadNil(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.LoadNil,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := UpValue[B]
+    /// </summary>
+    public static Instruction GetUpVal(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.GetUpVal,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := UpValue[B][RK(C)]
+    /// </summary>
+    public static Instruction GetTabUp(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.GetTabUp,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := R(B)[RK(C)]
+    /// </summary>
+    public static Instruction GetTable(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.GetTable,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// UpValue[B] := R(A)
+    /// </summary>
+    public static Instruction SetUpVal(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.SetUpVal,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// UpValue[A][RK(B)] := RK(C)
+    /// </summary>
+    public static Instruction SetTabUp(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.SetTabUp,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A)[RK(B)] := RK(C)
+    /// </summary>
+    public static Instruction SetTable(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.SetTable,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := {} (size = B,C)
+    /// </summary>
+    public static Instruction NewTable(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.NewTable,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A+1) := R(B); R(A) := R(B)[RK(C)]
+    /// </summary>
+    public static Instruction Self(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Self,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := RK(B) + RK(C)
+    /// </summary>
+    public static Instruction Add(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Add,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := RK(B) - RK(C)
+    /// </summary>
+    public static Instruction Sub(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Sub,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := RK(B) * RK(C)
+    /// </summary>
+    public static Instruction Mul(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Mul,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := RK(B) / RK(C)
+    /// </summary>
+    public static Instruction Div(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Div,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := RK(B) % RK(C)
+    /// </summary>
+    public static Instruction Mod(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Mod,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := RK(B) ^ RK(C)
+    /// </summary>
+    public static Instruction Pow(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Pow,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := -R(B)
+    /// </summary>
+    public static Instruction Unm(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.Unm,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := not R(B)
+    /// </summary>
+    public static Instruction Not(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.Not,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := length of R(B)
+    /// </summary>
+    public static Instruction Len(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.Len,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := R(B).. ... ..R(C)
+    /// </summary>
+    public static Instruction Concat(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Concat,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// <para>pc += sBx</para>
+    /// <para>if (A) close all upvalues >= R(A - 1)</para>
+    /// </summary>
+    public static Instruction Jmp(byte a, int sBx)
+    {
+        return new()
+        {
+            OpCode = OpCode.Jmp,
+            A = a,
+            SBx = sBx,
+        };
+    }
+
+    /// <summary>
+    /// if ((RK(B) == RK(C)) ~= A) then pc++
+    /// </summary>
+    public static Instruction Eq(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Eq,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// if ((RK(B) &lt; RK(C)) ~= A) then pc++
+    /// </summary>
+    public static Instruction Lt(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Lt,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// if ((RK(B) &lt;= RK(C)) ~= A) then pc++
+    /// </summary>
+    public static Instruction Le(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Le,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// if not (R(A) &lt;=&gt; C) then pc++
+    /// </summary>
+    public static Instruction Test(byte a, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Test,
+            A = a,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// if (R(B) &lt;=&gt; C) then R(A) := R(B) else pc++
+    /// </summary>
+    public static Instruction TestSet(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.TestSet,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))
+    /// </summary>
+    public static Instruction Call(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.Call,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// return R(A)(R(A+1), ... ,R(A+B-1))
+    /// </summary>
+    public static Instruction TailCall(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.TailCall,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// return R(A), ... ,R(A+B-2)
+    /// </summary>
+    public static Instruction Return(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.Return,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// <para>R(A) += R(A+2);</para>
+    /// <para>if R(A) &lt;?= R(A+1) then { pc += sBx; R(A+3) = R(A) }</para>
+    /// </summary>
+    public static Instruction ForLoop(byte a, int sBx)
+    {
+        return new()
+        {
+            OpCode = OpCode.ForLoop,
+            A = a,
+            SBx = sBx,
+        };
+    }
+
+    /// <summary>
+    /// <para>R(A) -= R(A+2)</para>
+    /// <para>pc += sBx</para>
+    /// </summary>
+    public static Instruction ForPrep(byte a, int sBx)
+    {
+        return new()
+        {
+            OpCode = OpCode.ForPrep,
+            A = a,
+            SBx = sBx,
+        };
+    }
+
+    /// <summary>
+    /// R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));
+    /// </summary>
+    public static Instruction TForCall(byte a, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.TForCall,
+            A = a,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// if R(A+1) ~= nil then { R(A) = R(A+1); pc += sBx }
+    /// </summary>
+    public static Instruction TForLoop(byte a, int sBx)
+    {
+        return new()
+        {
+            OpCode = OpCode.TForLoop,
+            A = a,
+            SBx = sBx,
+        };
+    }
+
+    /// <summary>
+    /// R(A)[(C-1) * FPF + i] := R(A+i), 1 &lt;= i &lt;= B
+    /// </summary>
+    public static Instruction SetList(byte a, ushort b, ushort c)
+    {
+        return new()
+        {
+            OpCode = OpCode.SetList,
+            A = a,
+            B = b,
+            C = c,
+        };
+    }
+
+    /// <summary>
+    /// R(A) := closure(KPROTO[Bx])
+    /// </summary>
+    public static Instruction Closure(byte a, int sBx)
+    {
+        return new()
+        {
+            OpCode = OpCode.Closure,
+            A = a,
+            SBx = sBx,
+        };
+    }
+
+    /// <summary>
+    /// R(A), R(A+1), ..., R(A+B-2) = vararg
+    /// </summary>
+    public static Instruction VarArg(byte a, ushort b)
+    {
+        return new()
+        {
+            OpCode = OpCode.VarArg,
+            A = a,
+            B = b,
+        };
+    }
+
+    /// <summary>
+    /// extra (larger) argument for previous opcode
+    /// </summary>
+    public static Instruction ExtraArg(uint ax)
+    {
+        return new()
+        {
+            OpCode = OpCode.ExtraArg,
+            Ax = ax,
+        };
+    }
+}

+ 101 - 0
src/Lua/Runtime/LuaStack.cs

@@ -0,0 +1,101 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Lua.Runtime;
+
+public class LuaStack(int initialSize = 256)
+{
+    LuaValue[] array = new LuaValue[initialSize];
+    int top;
+
+    public int Count => top;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void EnsureCapacity(int newSize)
+    {
+        var size = array.Length;
+        if (size >= newSize) return;
+
+        while (size < newSize)
+        {
+            size *= 2;
+        }
+
+        Array.Resize(ref array, size);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void NotifyTop(int top)
+    {
+        if (this.top < top) this.top = top;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Push(LuaValue value)
+    {
+        EnsureCapacity(top + 1);
+        array[top] = value;
+        top++;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public LuaValue Pop()
+    {
+        if (top == 0) ThrowEmptyStack();
+        top--;
+        var item = array[top];
+        array[top] = default;
+        return item;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void PopUntil(int newSize)
+    {
+        if (newSize >= top) return;
+        array.AsSpan(newSize, top).Clear();
+        top = newSize;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Clear()
+    {
+        array.AsSpan().Clear();
+        top = 0;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Span<LuaValue> AsSpan()
+    {
+        return new Span<LuaValue>(array, 0, top);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Span<LuaValue> GetBuffer()
+    {
+        return array.AsSpan();
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Memory<LuaValue> GetBufferMemory()
+    {
+        return array.AsMemory();
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public ref LuaValue UnsafeGet(int index)
+    {
+        // if (index < 0 || index >= array.Length) throw new IndexOutOfRangeException();
+
+#if NET6_0_OR_GREATER
+        return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
+#else
+        ref var reference = ref MemoryMarshal.GetReference(array.AsSpan());
+        return ref Unsafe.Add(ref reference, index);
+#endif
+    }
+
+    static void ThrowEmptyStack()
+    {
+        throw new InvalidOperationException("Empty stack");
+    }
+}

+ 39 - 0
src/Lua/Runtime/LuaValueRuntimeExtensions.cs

@@ -0,0 +1,39 @@
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
+namespace Lua.Runtime;
+
+internal static class LuaRuntimeExtensions
+{
+    public static bool TryGetMetamethod(this LuaValue value, string methodName, out LuaValue result)
+    {
+        if (value.TryRead<LuaTable>(out var table) &&
+            table.Metatable != null &&
+            table.Metatable.TryGetValue(methodName, out result))
+        {
+            return true;
+        }
+        else
+        {
+            result = default;
+            return false;
+        }
+    }
+
+#if NET6_0_OR_GREATER
+    [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
+#endif
+    public static async ValueTask<int> InvokeAsync(this LuaFunction function, LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    {
+        var buffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+        buffer.AsSpan().Clear();
+        try
+        {
+            return await function.InvokeAsync(context, cancellationToken);
+        }
+        finally
+        {
+            ArrayPool<LuaValue>.Shared.Return(buffer);
+        }
+    }
+}

+ 1043 - 0
src/Lua/Runtime/LuaVirtualMachine.cs

@@ -0,0 +1,1043 @@
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using Lua.Internal;
+
+namespace Lua.Runtime;
+
+public static partial class LuaVirtualMachine
+{
+    internal async static ValueTask<int> ExecuteClosureAsync(LuaState state, Closure closure, CallStackFrame frame, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var stack = state.Stack;
+        var chunk = closure.Proto;
+
+        for (var pc = 0; pc < chunk.Instructions.Length; pc++)
+        {
+            var instruction = chunk.Instructions[pc];
+
+            var RA = instruction.A + frame.Base;
+            var RB = instruction.B + frame.Base;
+
+            switch (instruction.OpCode)
+            {
+                case OpCode.Move:
+                    stack.EnsureCapacity(RA + 1);
+                    stack.UnsafeGet(RA) = stack.UnsafeGet(RB);
+                    stack.NotifyTop(RA + 1);
+                    break;
+                case OpCode.LoadK:
+                    stack.EnsureCapacity(RA + 1);
+                    stack.UnsafeGet(RA) = chunk.Constants[instruction.Bx];
+                    stack.NotifyTop(RA + 1);
+                    break;
+                case OpCode.LoadKX:
+                    throw new NotImplementedException();
+                case OpCode.LoadBool:
+                    stack.EnsureCapacity(RA + 1);
+                    stack.UnsafeGet(RA) = instruction.B != 0;
+                    stack.NotifyTop(RA + 1);
+                    if (instruction.C != 0) pc++;
+                    break;
+                case OpCode.LoadNil:
+                    stack.EnsureCapacity(RA + instruction.B + 1);
+                    stack.GetBuffer().Slice(RA, instruction.B + 1).Clear();
+                    stack.NotifyTop(RA + instruction.B + 1);
+                    break;
+                case OpCode.GetUpVal:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+                        var upValue = closure.UpValues[instruction.B];
+                        stack.UnsafeGet(RA) = upValue.GetValue(state);
+                        stack.NotifyTop(RA + 1);
+                        break;
+                    }
+                case OpCode.GetTabUp:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        var upValue = closure.UpValues[instruction.B];
+                        var table = upValue.GetValue(state);
+                        var value = await GetTableValue(state, chunk, pc, table, vc, cancellationToken);
+                        stack.UnsafeGet(RA) = value;
+                        stack.NotifyTop(RA + 1);
+                        break;
+                    }
+                case OpCode.GetTable:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+                        var table = stack.UnsafeGet(RB);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        var value = await GetTableValue(state, chunk, pc, table, vc, cancellationToken);
+                        stack.UnsafeGet(RA) = value;
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.SetTabUp:
+                    {
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        var upValue = closure.UpValues[instruction.A];
+                        var table = upValue.GetValue(state);
+                        await SetTableValue(state, chunk, pc, table, vb, vc, cancellationToken);
+                        break;
+                    }
+                case OpCode.SetUpVal:
+                    {
+                        var upValue = closure.UpValues[instruction.B];
+                        upValue.SetValue(state, stack.UnsafeGet(RA));
+                        break;
+                    }
+                case OpCode.SetTable:
+                    {
+                        var table = stack.UnsafeGet(RA);
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        await SetTableValue(state, chunk, pc, table, vb, vc, cancellationToken);
+                    }
+                    break;
+                case OpCode.NewTable:
+                    stack.EnsureCapacity(RA + 1);
+                    stack.UnsafeGet(RA) = new LuaTable(instruction.B, instruction.C);
+                    stack.NotifyTop(RA + 1);
+                    break;
+                case OpCode.Self:
+                    {
+                        stack.EnsureCapacity(RA + 2);
+                        var table = stack.UnsafeGet(RB).Read<LuaTable>();
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        stack.UnsafeGet(RA + 1) = table;
+                        stack.UnsafeGet(RA) = table[vc];
+                        stack.NotifyTop(RA + 2);
+                    }
+                    break;
+                case OpCode.Add:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            stack.UnsafeGet(RA) = valueB + valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Add, out var metamethod) || vc.TryGetMetamethod(Metamethods.Add, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "add", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Sub:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            stack.UnsafeGet(RA) = valueB - valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Sub, out var metamethod) || vc.TryGetMetamethod(Metamethods.Sub, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "sub", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Mul:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            stack.UnsafeGet(RA) = valueB * valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Mul, out var metamethod) || vc.TryGetMetamethod(Metamethods.Mul, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "mul", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Div:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            stack.UnsafeGet(RA) = valueB / valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Div, out var metamethod) || vc.TryGetMetamethod(Metamethods.Div, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "div", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Mod:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            stack.UnsafeGet(RA) = valueB % valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Mod, out var metamethod)  || vc.TryGetMetamethod(Metamethods.Mod, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "mod", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Pow:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            stack.UnsafeGet(RA) = Math.Pow(valueB, valueC);
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Pow, out var metamethod) || vc.TryGetMetamethod(Metamethods.Pow, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "pow", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Unm:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = stack.UnsafeGet(RB);
+
+                        if (vb.TryRead<double>(out var valueB))
+                        {
+                            stack.UnsafeGet(RA) = -valueB;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Unm, out var metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 1,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "unm", vb);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Not:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var rb = stack.UnsafeGet(RB);
+
+                        if (rb.TryRead<bool>(out var valueB))
+                        {
+                            stack.UnsafeGet(RA) = !valueB;
+                        }
+                        else
+                        {
+                            stack.UnsafeGet(RA) = false;
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Len:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = stack.UnsafeGet(RB);
+
+                        if (vb.TryRead<string>(out var str))
+                        {
+                            stack.UnsafeGet(RA) = str.Length;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Len, out var metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 1,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else if (vb.TryRead<LuaTable>(out var table))
+                        {
+                            stack.UnsafeGet(RA) = table.ArrayLength;
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "get length of", vb);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Concat:
+                    {
+                        stack.EnsureCapacity(RA + 1);
+
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+
+                        var bIsValid = vb.TryRead<string>(out var strB);
+                        var cIsValid = vc.TryRead<string>(out var strC);
+
+                        if (!bIsValid && vb.TryRead<double>(out var numB))
+                        {
+                            strB = numB.ToString();
+                            bIsValid = true;
+                        }
+                        if (!cIsValid && vc.TryRead<double>(out var numC))
+                        {
+                            strC = numC.ToString();
+                            cIsValid = true;
+                        }
+
+                        if (bIsValid && cIsValid)
+                        {
+                            stack.UnsafeGet(RA) = strB + strC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Concat, out var metamethod) || vc.TryGetMetamethod(Metamethods.Concat, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            using var methodBuffer = new PooledArray<LuaValue>(1024);
+                            await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                SourcePosition = chunk.SourcePositions[pc],
+                            }, methodBuffer.AsMemory(), cancellationToken);
+
+                            stack.UnsafeGet(RA) = methodBuffer[0];
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "concat", vb, vc);
+                        }
+
+                        stack.NotifyTop(RA + 1);
+                    }
+                    break;
+                case OpCode.Jmp:
+                    pc += instruction.SBx;
+                    if (instruction.A != 0)
+                    {
+                        state.CloseUpValues(instruction.A);
+                    }
+                    break;
+                case OpCode.Eq:
+                    {
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        var compareResult = vb == vc;
+
+                        if (!compareResult && (vb.TryGetMetamethod(Metamethods.Eq, out var metamethod) || vc.TryGetMetamethod(Metamethods.Eq, out metamethod)))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            var methodBuffer = ArrayPool<LuaValue>.Shared.Rent(1);
+                            methodBuffer.AsSpan().Clear();
+                            try
+                            {
+                                await func.InvokeAsync(new()
+                                {
+                                    State = state,
+                                    ArgumentCount = 2,
+                                    SourcePosition = chunk.SourcePositions[pc],
+                                }, methodBuffer, cancellationToken);
+
+                                compareResult = methodBuffer[0].ToBoolean();
+                            }
+                            finally
+                            {
+                                ArrayPool<LuaValue>.Shared.Return(methodBuffer);
+                            }
+                        }
+
+                        if (compareResult != (instruction.A == 1))
+                        {
+                            pc++;
+                        }
+                    }
+                    break;
+                case OpCode.Lt:
+                    {
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        var compareResult = false;
+                        
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            compareResult = valueB < valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Lt, out var metamethod) || vc.TryGetMetamethod(Metamethods.Lt, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            var methodBuffer = ArrayPool<LuaValue>.Shared.Rent(1);
+                            methodBuffer.AsSpan().Clear();
+                            try
+                            {
+                                await func.InvokeAsync(new()
+                                {
+                                    State = state,
+                                    ArgumentCount = 2,
+                                    SourcePosition = chunk.SourcePositions[pc],
+                                }, methodBuffer, cancellationToken);
+
+                                compareResult = methodBuffer[0].ToBoolean();
+                            }
+                            finally
+                            {
+                                ArrayPool<LuaValue>.Shared.Return(methodBuffer);
+                            }
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "less than", vb, vc);
+                        }
+
+                        if (compareResult != (instruction.A == 1))
+                        {
+                            pc++;
+                        }
+                    }
+                    break;
+                case OpCode.Le:
+                    {
+                        var vb = RK(stack, chunk, instruction.B, frame.Base);
+                        var vc = RK(stack, chunk, instruction.C, frame.Base);
+                        var compareResult = false;
+
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        {
+                            compareResult = valueB <= valueC;
+                        }
+                        else if (vb.TryGetMetamethod(Metamethods.Le, out var metamethod) || vc.TryGetMetamethod(Metamethods.Le, out metamethod))
+                        {
+                            if (!metamethod.TryRead<LuaFunction>(out var func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+
+                            stack.Push(vb);
+                            stack.Push(vc);
+
+                            var methodBuffer = ArrayPool<LuaValue>.Shared.Rent(1);
+                            methodBuffer.AsSpan().Clear();
+                            try
+                            {
+                                await func.InvokeAsync(new()
+                                {
+                                    State = state,
+                                    ArgumentCount = 2,
+                                    SourcePosition = chunk.SourcePositions[pc],
+                                }, methodBuffer, cancellationToken);
+
+                                compareResult = methodBuffer[0].ToBoolean();
+                            }
+                            finally
+                            {
+                                ArrayPool<LuaValue>.Shared.Return(methodBuffer);
+                            }
+                        }
+                        else
+                        {
+                            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "less than or equals", vb, vc);
+                        }
+
+                        if (compareResult != (instruction.A == 1))
+                        {
+                            pc++;
+                        }
+                    }
+                    break;
+                case OpCode.Test:
+                    {
+                        if (stack.UnsafeGet(RA).ToBoolean() != (instruction.C == 1))
+                        {
+                            pc++;
+                        }
+                    }
+                    break;
+                case OpCode.TestSet:
+                    {
+                        if (stack.UnsafeGet(RB).ToBoolean() != (instruction.C == 1))
+                        {
+                            pc++;
+                        }
+                        else
+                        {
+                            stack.UnsafeGet(RA) = stack.UnsafeGet(RB);
+                            stack.NotifyTop(RA + 1);
+                        }
+                    }
+                    break;
+                case OpCode.Call:
+                    {
+                        var va = stack.UnsafeGet(RA);
+                        if (!va.TryRead<LuaFunction>(out var func))
+                        {
+                            if (!va.TryGetMetamethod(Metamethods.Call, out var metamethod) && !metamethod.TryRead<LuaFunction>(out func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+                        }
+
+                        (var newBase, var argumentCount) = PrepareForFunctionCall(state, func, instruction, RA, false);
+
+                        var resultBuffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+                        resultBuffer.AsSpan().Clear();
+                        try
+                        {
+                            var resultCount = await func.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = argumentCount,
+                                StackPosition = newBase,
+                                SourcePosition = chunk.SourcePositions[pc],
+                                ChunkName = chunk.Name,
+                                RootChunkName = chunk.GetRoot().Name,
+                            }, resultBuffer.AsMemory(), cancellationToken);
+
+                            if (instruction.C != 0)
+                            {
+                                resultCount = instruction.C - 1;
+                            }
+
+                            stack.EnsureCapacity(RA + resultCount);
+                            for (int i = 0; i < resultCount; i++)
+                            {
+                                stack.UnsafeGet(RA + i) = resultBuffer[i];
+                            }
+                            stack.NotifyTop(RA + resultCount);
+                        }
+                        finally
+                        {
+                            ArrayPool<LuaValue>.Shared.Return(resultBuffer);
+                        }
+                    }
+                    break;
+                case OpCode.TailCall:
+                    {
+                        state.CloseUpValues(frame.Base);
+
+                        var va = stack.UnsafeGet(RA);
+                        if (!va.TryRead<LuaFunction>(out var func))
+                        {
+                            if (!va.TryGetMetamethod(Metamethods.Call, out var metamethod) && !metamethod.TryRead<LuaFunction>(out func))
+                            {
+                                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+                            }
+                        }
+
+                        (var newBase, var argumentCount) = PrepareForFunctionCall(state, func, instruction, RA, true);
+
+                        return await func.InvokeAsync(new()
+                        {
+                            State = state,
+                            ArgumentCount = argumentCount,
+                            StackPosition = newBase,
+                            SourcePosition = chunk.SourcePositions[pc],
+                            ChunkName = chunk.Name,
+                            RootChunkName = chunk.GetRoot().Name,
+                        }, buffer, cancellationToken);
+                    }
+                case OpCode.Return:
+                    {
+                        state.CloseUpValues(frame.Base);
+
+                        var retCount = instruction.B - 1;
+
+                        if (retCount == -1)
+                        {
+                            retCount = stack.Count - RA;
+                        }
+
+                        for (int i = 0; i < retCount; i++)
+                        {
+                            buffer.Span[i] = stack.UnsafeGet(RA + i);
+                        }
+
+                        return retCount;
+                    }
+                case OpCode.ForLoop:
+                    {
+                        stack.EnsureCapacity(RA + 4);
+
+                        // TODO: add error message
+                        var variable = stack.UnsafeGet(RA).Read<double>();
+                        var limit = stack.UnsafeGet(RA + 1).Read<double>();
+                        var step = stack.UnsafeGet(RA + 2).Read<double>();
+
+                        var va = variable + step;
+                        stack.UnsafeGet(RA) = va;
+
+                        if (step >= 0 ? va <= limit : va >= limit)
+                        {
+                            pc += instruction.SBx;
+                            stack.UnsafeGet(RA + 3) = va;
+                            stack.NotifyTop(RA + 4);
+                        }
+                        else
+                        {
+                            stack.NotifyTop(RA + 1);
+                        }
+                    }
+                    break;
+                case OpCode.ForPrep:
+                    {
+                        // TODO: add error message
+                        stack.UnsafeGet(RA) = stack.UnsafeGet(RA).Read<double>() - stack.UnsafeGet(RA + 2).Read<double>();
+                        stack.NotifyTop(RA + 1);
+                        pc += instruction.SBx;
+                    }
+                    break;
+                case OpCode.TForCall:
+                    {
+                        var iterator = stack.UnsafeGet(RA).Read<LuaFunction>();
+
+                        var nextBase = RA + 3 + instruction.C;
+                        
+                        var resultBuffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+                        resultBuffer.AsSpan().Clear();
+                        try
+                        {
+                            await iterator.InvokeAsync(new()
+                            {
+                                State = state,
+                                ArgumentCount = 2,
+                                StackPosition = nextBase,
+                                SourcePosition = chunk.SourcePositions[pc],
+                                ChunkName = chunk.Name,
+                                RootChunkName = chunk.GetRoot().Name,
+                            }, resultBuffer.AsMemory(), cancellationToken);
+
+                            stack.EnsureCapacity(RA + instruction.C + 3);
+                            for (int i = 1; i <= instruction.C; i++)
+                            {
+                                stack.UnsafeGet(RA + 2 + i) = resultBuffer[i - 1];
+                            }
+                            stack.NotifyTop(RA + instruction.C + 3);
+                        }
+                        finally
+                        {
+                            ArrayPool<LuaValue>.Shared.Return(resultBuffer);
+                        }
+                    }
+                    break;
+                case OpCode.TForLoop:
+                    {
+                        var forState = stack.UnsafeGet(RA + 1);
+                        if (forState.Type is not LuaValueType.Nil)
+                        {
+                            stack.UnsafeGet(RA) = forState;
+                            pc += instruction.SBx;
+                        }
+                    }
+                    break;
+                case OpCode.SetList:
+                    {
+                        var table = stack.UnsafeGet(RA).Read<LuaTable>();
+
+                        var count = instruction.B == 0
+                            ? stack.Count - (RA + 1)
+                            : instruction.B;
+
+                        table.EnsureArrayCapacity((instruction.C - 1) * 50 + count);
+                        stack.AsSpan().Slice(RA + 1, count)
+                            .CopyTo(table.GetArraySpan()[((instruction.C - 1) * 50)..]);
+                    }
+                    break;
+                case OpCode.Closure:
+                    stack.EnsureCapacity(RA + 1);
+                    stack.UnsafeGet(RA) = new Closure(state, chunk.Functions[instruction.SBx]);
+                    stack.NotifyTop(RA + 1);
+                    break;
+                case OpCode.VarArg:
+                    {
+                        var count = instruction.B == 0
+                            ? frame.VariableArgumentCount
+                            : instruction.B - 1;
+
+                        stack.EnsureCapacity(RA + count);
+                        for (int i = 0; i < count; i++)
+                        {
+                            stack.UnsafeGet(RA + i) = stack.UnsafeGet(frame.Base - (count - i));
+                        }
+                        stack.NotifyTop(RA + count);
+                    }
+                    break;
+                case OpCode.ExtraArg:
+                    throw new NotImplementedException();
+                default:
+                    break;
+            }
+        }
+
+        return 0;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static LuaValue RK(LuaStack stack, Chunk chunk, ushort index, int frameBase)
+    {
+        return index >= 256 ? chunk.Constants[index - 256] : stack.UnsafeGet(index + frameBase);
+    }
+
+#if NET6_0_OR_GREATER
+    [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
+#endif
+    static async ValueTask<LuaValue> GetTableValue(LuaState state, Chunk chunk, int pc, LuaValue table, LuaValue key, CancellationToken cancellationToken)
+    {
+        var stack = state.Stack;
+        var isTable = table.TryRead<LuaTable>(out var t);
+
+        if (isTable && t.TryGetValue(key, out var result))
+        {
+            return result;
+        }
+        else if (table.TryGetMetamethod(Metamethods.Index, out var metamethod))
+        {
+            if (!metamethod.TryRead<LuaFunction>(out var indexTable))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+            }
+
+            stack.Push(table);
+            stack.Push(key);
+
+            var methodBuffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+            methodBuffer.AsSpan().Clear();
+            try
+            {
+                await indexTable.InvokeAsync(new()
+                {
+                    State = state,
+                    ArgumentCount = 2,
+                    SourcePosition = chunk.SourcePositions[pc],
+                }, methodBuffer, cancellationToken);
+
+                return methodBuffer[0];
+            }
+            finally
+            {
+                ArrayPool<LuaValue>.Shared.Return(methodBuffer);
+            }
+        }
+        else if (isTable)
+        {
+            return LuaValue.Nil;
+        }
+        else
+        {
+            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "index", table);
+            return default; // dummy
+        }
+    }
+
+#if NET6_0_OR_GREATER
+    [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
+#endif
+    static async ValueTask SetTableValue(LuaState state, Chunk chunk, int pc, LuaValue table, LuaValue key, LuaValue value, CancellationToken cancellationToken)
+    {
+        var stack = state.Stack;
+        var isTable = table.TryRead<LuaTable>(out var t);
+
+        if (isTable && t.ContainsKey(key))
+        {
+            t[key] = value;
+        }
+        else if (table.TryGetMetamethod(Metamethods.NewIndex, out var metamethod))
+        {
+            if (!metamethod.TryRead<LuaFunction>(out var indexTable))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "call", metamethod);
+            }
+
+            stack.Push(table);
+            stack.Push(key);
+            stack.Push(value);
+
+            var methodBuffer = ArrayPool<LuaValue>.Shared.Rent(1024);
+            methodBuffer.AsSpan().Clear();
+            try
+            {
+                await indexTable.InvokeAsync(new()
+                {
+                    State = state,
+                    ArgumentCount = 3,
+                    SourcePosition = chunk.SourcePositions[pc],
+                }, methodBuffer, cancellationToken);
+            }
+            finally
+            {
+                ArrayPool<LuaValue>.Shared.Return(methodBuffer);
+            }
+        }
+        else if (isTable)
+        {
+            t[key] = value;
+        }
+        else
+        {
+            LuaRuntimeException.AttemptInvalidOperation(GetTracebacks(state, chunk, pc), "index", table);
+        }
+    }
+
+    static (int FrameBase, int ArgumentCount) PrepareForFunctionCall(LuaState state, LuaFunction function, Instruction instruction, int RA, bool isTailCall)
+    {
+        var stack = state.Stack;
+
+        var argumentCount = instruction.B - 1;
+        if (instruction.B == 0)
+        {
+            argumentCount = (ushort)(stack.Count - (RA + 1));
+        }
+
+        var newBase = RA + 1;
+
+        // In the case of tailcall, the local variables of the caller are immediately discarded, so there is no need to retain them.
+        // Therefore, a call can be made without allocating new registers.
+        if (isTailCall)
+        {
+            var currentBase = state.GetCurrentFrame().Base;
+            var stackBuffer = stack.GetBuffer();
+            stackBuffer.Slice(newBase, argumentCount).CopyTo(stackBuffer.Slice(currentBase, argumentCount));
+            newBase = currentBase;
+        }
+
+        var variableArgumentCount = function is Closure luaClosure && luaClosure.Proto.HasVariableArgments
+            ? argumentCount - luaClosure.Proto.ParameterCount
+            : 0;
+
+        // If there are variable arguments, the base of the stack is moved by that number and the values ​​of the variable arguments are placed in front of it.
+        // see: https://wubingzheng.github.io/build-lua-in-rust/en/ch08-02.arguments.html
+        if (variableArgumentCount > 0)
+        {
+            var temp = newBase;
+            newBase += variableArgumentCount;
+
+            var buffer = ArrayPool<LuaValue>.Shared.Rent(argumentCount);
+            try
+            {
+                stack.EnsureCapacity(newBase + argumentCount);
+                stack.NotifyTop(newBase + argumentCount);
+
+                var stackBuffer = stack.GetBuffer();
+                stackBuffer.Slice(temp, argumentCount).CopyTo(buffer);
+                buffer.AsSpan(0, argumentCount).CopyTo(stackBuffer[newBase..]);
+
+                buffer.AsSpan(argumentCount - variableArgumentCount, variableArgumentCount).CopyTo(stackBuffer[temp..]);
+            }
+            finally
+            {
+                ArrayPool<LuaValue>.Shared.Return(buffer);
+            }
+        }
+
+        return (newBase, argumentCount);
+    }
+
+    static Tracebacks GetTracebacks(LuaState state, Chunk chunk, int pc)
+    {
+        var frame = state.GetCurrentFrame();
+        state.PushCallStackFrame(frame with
+        {
+            CallPosition = chunk.SourcePositions[pc],
+            ChunkName = chunk.Name,
+            RootChunkName = chunk.GetRoot().Name,
+        });
+        var tracebacks = state.GetTracebacks();
+        state.PopCallStackFrame();
+
+        return tracebacks;
+    }
+}

+ 22 - 0
src/Lua/Runtime/Metamethods.cs

@@ -0,0 +1,22 @@
+namespace Lua.Runtime;
+
+public static class Metamethods
+{
+    public const string Metatable = "__metatable";
+    public const string Index = "__index";
+    public const string NewIndex = "__newindex";
+    public const string Add = "__add";
+    public const string Sub = "__sub";
+    public const string Mul = "__mul";
+    public const string Div = "__div";
+    public const string Mod = "__mod";
+    public const string Pow = "__pow";
+    public const string Unm = "__unm";
+    public const string Len = "__len";
+    public const string Eq = "__eq";
+    public const string Lt = "__lt";
+    public const string Le = "__le";
+    public const string Call = "__call";
+    public const string Concat = "__concat";
+    public new const string ToString = "__tostring";
+}

+ 61 - 0
src/Lua/Runtime/OpCode.cs

@@ -0,0 +1,61 @@
+namespace Lua.Runtime;
+
+public enum OpCode : byte
+{
+    Move,       // A B     R(A) := R(B)
+    LoadK,      // A Bx    R(A) := Kst(Bx)
+    LoadKX,     // A       R(A) := Kst(extra arg)
+    LoadBool,   // A B C   R(A) := (Bool)B; if (C) pc++
+    LoadNil,    // A B     R(A), R(A+1), ..., R(A+B) := nil
+
+    GetUpVal,   // A B     R(A) := UpValue[B]
+    GetTabUp,   // A B C   R(A) := UpValue[B][RK(C)]
+    GetTable,   // A B C   R(A) := R(B)[RK(C)]
+
+    SetTabUp,   // A B C   UpValue[A][RK(B)] := RK(C)
+    SetUpVal,   // A B     UpValue[B] := R(A)
+    SetTable,   // A B C   R(A)[RK(B)] := RK(C)
+
+    NewTable,   // A B C   R(A) := {} (size = B,C)
+
+    Self,       // A B C   R(A+1) := R(B); R(A) := R(B)[RK(C)]
+
+    Add,        // A B C   R(A) := RK(B) + RK(C)
+    Sub,        // A B C   R(A) := RK(B) - RK(C)
+    Mul,        // A B C   R(A) := RK(B) * RK(C)
+    Div,        // A B C   R(A) := RK(B) / RK(C)
+    Mod,        // A B C   R(A) := RK(B) % RK(C)
+    Pow,        // A B C   R(A) := RK(B) ^ RK(C)
+    Unm,        // A B     R(A) := -R(B)
+    Not,        // A B     R(A) := not R(B)
+    Len,        // A B     R(A) := length of R(B)
+
+    Concat,     // A B C   R(A) := R(B).. ... ..R(C)
+
+    Jmp,        // A sBx   pc+=sBx; if (A) close all upvalues >= R(A - 1)
+    Eq,         // A B C   if ((RK(B) == RK(C)) ~= A) then pc++
+    Lt,         // A B C   if ((RK(B) <  RK(C)) ~= A) then pc++
+    Le,         // A B C   if ((RK(B) <= RK(C)) ~= A) then pc++
+
+    Test,       // A C     if not (R(A) <=> C) then pc++
+    TestSet,    // A B C   if (R(B) <=> C) then R(A) := R(B) else pc++
+
+    Call,       // A B C   R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))
+    TailCall,   // A B C   return R(A)(R(A+1), ... ,R(A+B-1))
+    Return,     // A B     return R(A), ... ,R(A+B-2)      (see note)
+
+    ForLoop,    // A sBx   R(A)+=R(A+2);
+                //         if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }
+    ForPrep,    // A sBx   R(A)-=R(A+2); pc+=sBx
+
+    TForCall,   // A C     R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));
+    TForLoop,   // A sBx   if R(A+1) ~= nil then { R(A)=R(A+1); pc += sBx }
+
+    SetList,    // A B C   R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B
+
+    Closure,    // A Bx    R(A) := closure(KPROTO[Bx])
+
+    VarArg,     // A B     R(A), R(A+1), ..., R(A+B-2) = vararg
+
+    ExtraArg    // Ax      extra (larger) argument for previous opcode
+}

+ 24 - 0
src/Lua/Runtime/Tracebacks.cs

@@ -0,0 +1,24 @@
+using Lua.CodeAnalysis;
+
+namespace Lua.Runtime;
+
+public class Tracebacks
+{
+    public required CallStackFrame[] StackFrames { get; init; }
+
+    internal string RootChunkName => StackFrames[^1].RootChunkName;
+    internal SourcePosition LastPosition => StackFrames[^1].CallPosition!.Value;
+
+    public override string ToString()
+    {
+        var str = string.Join("\n   ", StackFrames
+            .Where(x => x.CallPosition != null)
+            .Select(x =>
+            {
+                return $"{x.RootChunkName}:{x.CallPosition!.Value.Line}: {(string.IsNullOrEmpty(x.ChunkName) ? "" : $"in '{x.ChunkName}'")}";
+            })
+            .Reverse());
+
+        return $"stack traceback:\n   {str}";
+    }
+}

+ 70 - 0
src/Lua/Runtime/UpValue.cs

@@ -0,0 +1,70 @@
+
+using System.Runtime.CompilerServices;
+
+namespace Lua.Runtime;
+
+public sealed class UpValue
+{
+    LuaValue value;
+
+    public bool IsClosed { get; private set; }
+    public int RegisterIndex { get; private set; }
+
+    UpValue()
+    {
+    }
+
+    public static UpValue Open(int registerIndex)
+    {
+        return new()
+        {
+            RegisterIndex = registerIndex
+        };
+    }
+
+    public static UpValue Closed(LuaValue value)
+    {
+        return new()
+        {
+            IsClosed = true,
+            value = value
+        };
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public LuaValue GetValue(LuaState state)
+    {
+        if (IsClosed)
+        {
+            return value;
+        }
+        else
+        {
+            return state.Stack.UnsafeGet(RegisterIndex);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void SetValue(LuaState state, LuaValue value)
+    {
+        if (IsClosed)
+        {
+            this.value = value;
+        }
+        else
+        {
+            state.Stack.UnsafeGet(RegisterIndex) = value;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Close(LuaState state)
+    {
+        if (!IsClosed)
+        {
+            value = state.Stack.UnsafeGet(RegisterIndex);
+        }
+
+        IsClosed = true;
+    }
+}

+ 9 - 0
src/Lua/Runtime/UpValueInfo.cs

@@ -0,0 +1,9 @@
+namespace Lua.Runtime;
+
+public readonly record struct UpValueInfo
+{
+    public required ReadOnlyMemory<char> Name { get; init; }
+    public required int Index { get; init; }
+    public required int Id { get; init; }
+    public required bool IsInRegister { get; init; }
+}

+ 23 - 0
src/Lua/Standard/Base/AssertFunction.cs

@@ -0,0 +1,23 @@
+namespace Lua.Standard.Base;
+
+public sealed class AssertFunction : LuaFunction
+{
+    public const string Name = "assert";
+    public static readonly AssertFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfArgumentNotExists(context, Name, 0);
+
+        if (!context.Arguments[0].ToBoolean())
+        {
+            var message = context.ArgumentCount >= 2
+                ? context.Arguments[1].Read<string>()
+                : $"assertion failed!";
+
+            throw new LuaAssertionException(context.State.GetTracebacks(), message);
+        }
+
+        return new(0);
+    }
+}

+ 16 - 0
src/Lua/Standard/Base/ErrorFunction.cs

@@ -0,0 +1,16 @@
+namespace Lua.Standard.Base;
+
+public sealed class ErrorFunction : LuaFunction
+{
+    public const string Name = "error";
+    public static readonly ErrorFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var obj = context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil
+            ? "(error object is a nil value)"
+            : context.Arguments[0].ToString();
+
+        throw new LuaRuntimeException(context.State.GetTracebacks(), obj!);
+    }
+}

+ 39 - 0
src/Lua/Standard/Base/GetMetatableFunction.cs

@@ -0,0 +1,39 @@
+
+using Lua.Runtime;
+
+namespace Lua.Standard.Base;
+
+public sealed class GetMetatableFunction : LuaFunction
+{
+    public const string Name = "getmetatable";
+    public static readonly GetMetatableFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfArgumentNotExists(context, Name, 0);
+
+        var obj = context.Arguments[0];
+
+        if (obj.TryRead<LuaTable>(out var table))
+        {
+            if (table.Metatable == null)
+            {
+                buffer.Span[0] = LuaValue.Nil;
+            }
+            else if (table.Metatable.TryGetValue(Metamethods.Metatable, out var metatable))
+            {
+                buffer.Span[0] = metatable;
+            }
+            else
+            {
+                buffer.Span[0] = table.Metatable;
+            }
+        }
+        else
+        {
+            buffer.Span[0] = LuaValue.Nil;
+        }
+
+        return new(1);
+    }
+}

+ 25 - 0
src/Lua/Standard/Base/PrintFunction.cs

@@ -0,0 +1,25 @@
+
+using Lua.Runtime;
+
+namespace Lua.Standard.Base;
+
+public sealed class PrintFunction : LuaFunction
+{
+    public const string Name = "print";
+    public static readonly PrintFunction Instance = new();
+
+    LuaValue[] buffer = new LuaValue[1];
+
+    protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        for (int i = 0; i < context.ArgumentCount; i++)
+        {
+            await ToStringFunction.ToStringCore(context, context.Arguments[i], this.buffer, cancellationToken);
+            Console.Write(this.buffer[0]);
+            Console.Write('\t');
+        }
+
+        Console.WriteLine();
+        return 0;
+    }
+}

+ 13 - 0
src/Lua/Standard/Base/RawEqualFunction.cs

@@ -0,0 +1,13 @@
+
+namespace Lua.Standard.Base;
+
+public sealed class RawEqualFunction : LuaFunction
+{
+    public const string Name = "rawequal";
+    public static readonly RawEqualFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 23 - 0
src/Lua/Standard/Base/RawGetFunction.cs

@@ -0,0 +1,23 @@
+
+namespace Lua.Standard.Base;
+
+public sealed class RawGetFunction : LuaFunction
+{
+    public const string Name = "rawget";
+    public static readonly RawGetFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfArgumentNotExists(context, Name, 0);
+        ThrowIfArgumentNotExists(context, Name, 1);
+
+        var arg0 = context.Arguments[0];
+        if (!arg0.TryRead<LuaTable>(out var table))
+        {
+            LuaRuntimeException.BadArgument(context.State.GetTracebacks(), 1, Name, LuaValueType.Table, arg0.Type);
+        }
+
+        buffer.Span[0] = table[context.Arguments[1]];
+        return new(1);
+    }
+}

+ 25 - 0
src/Lua/Standard/Base/RawSetFunction.cs

@@ -0,0 +1,25 @@
+
+namespace Lua.Standard.Base;
+
+public sealed class RawSetFunction : LuaFunction
+{
+    public const string Name = "rawset";
+    public static readonly RawSetFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfArgumentNotExists(context, Name, 0);
+        ThrowIfArgumentNotExists(context, Name, 1);
+        ThrowIfArgumentNotExists(context, Name, 2);
+
+        var arg0 = context.Arguments[0];
+        if (!arg0.TryRead<LuaTable>(out var table))
+        {
+            LuaRuntimeException.BadArgument(context.State.GetTracebacks(), 1, Name, LuaValueType.Table, arg0.Type);
+        }
+
+        table[context.Arguments[1]] = context.Arguments[2];
+
+        return new(0);
+    }
+}

+ 44 - 0
src/Lua/Standard/Base/SetMetatableFunction.cs

@@ -0,0 +1,44 @@
+
+using Lua.Runtime;
+
+namespace Lua.Standard.Base;
+
+public sealed class SetMetatableFunction : LuaFunction
+{
+    public const string Name = "setmetatable";
+    public static readonly SetMetatableFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfArgumentNotExists(context, Name, 0);
+        ThrowIfArgumentNotExists(context, Name, 1);
+
+        var arg0 = context.Arguments[0];
+        if (!arg0.TryRead<LuaTable>(out var table))
+        {
+            LuaRuntimeException.BadArgument(context.State.GetTracebacks(), 1, Name, LuaValueType.Table, arg0.Type);
+        }
+
+        var arg1 = context.Arguments[1];
+        if (arg1.Type is not (LuaValueType.Nil or LuaValueType.Table))
+        {
+            LuaRuntimeException.BadArgument(context.State.GetTracebacks(), 2, Name, [LuaValueType.Nil, LuaValueType.Table]);
+        }
+
+        if (table.Metatable != null && table.Metatable.TryGetValue(Metamethods.Metatable, out _))
+        {
+            throw new LuaRuntimeException(context.State.GetTracebacks(), "cannot change a protected metatable");
+        }
+        else if (arg1.Type is LuaValueType.Nil)
+        {
+            table.Metatable = null;
+        }
+        else
+        {
+            table.Metatable = arg1.Read<LuaTable>();
+        }
+
+        buffer.Span[0] = table;
+        return new(1);
+    }
+}

+ 39 - 0
src/Lua/Standard/Base/ToStringFunction.cs

@@ -0,0 +1,39 @@
+
+using System.Buffers;
+using Lua.Runtime;
+
+namespace Lua.Standard.Base;
+
+public sealed class ToStringFunction : LuaFunction
+{
+    public const string Name = "tostring";
+    public static readonly ToStringFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfArgumentNotExists(context, Name, 0);
+        return ToStringCore(context, context.Arguments[0], buffer, cancellationToken);
+    }
+
+    internal static async ValueTask<int> ToStringCore(LuaFunctionExecutionContext context, LuaValue value, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        if (value.TryGetMetamethod(Metamethods.ToString, out var metamethod))
+        {
+            if (!metamethod.TryRead<LuaFunction>(out var func))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(context.State.GetTracebacks(), "call", metamethod);
+            }
+
+            context.State.Push(value);
+            return await func.InvokeAsync(context with
+            {
+                ArgumentCount = 1,
+            }, buffer, cancellationToken);
+        }
+        else
+        {
+            buffer.Span[0] = value.ToString()!;
+            return 1;
+        }
+    }
+}

+ 20 - 0
src/Lua/Standard/OpenLibExtensions.cs

@@ -0,0 +1,20 @@
+using Lua.Standard.Base;
+
+namespace Lua.Standard;
+
+public static class OpenLibExtensions
+{
+    public static void OpenBaseLibrary(this LuaState state)
+    {
+        state.Environment["_G"] = state.Environment;
+        state.Environment["_VERSION"] = "Lua 5.2";
+        state.Environment[AssertFunction.Name] = AssertFunction.Instance;
+        state.Environment[ErrorFunction.Name] = ErrorFunction.Instance;
+        state.Environment[PrintFunction.Name] = PrintFunction.Instance;
+        state.Environment[RawGetFunction.Name] = RawGetFunction.Instance;
+        state.Environment[RawSetFunction.Name] = RawSetFunction.Instance;
+        state.Environment[GetMetatableFunction.Name] = GetMetatableFunction.Instance;
+        state.Environment[SetMetatableFunction.Name] = SetMetatableFunction.Instance;
+        state.Environment[ToStringFunction.Name] = ToStringFunction.Instance;
+    }
+}

+ 1 - 0
tests/Lua.Tests/GlobalUsings.cs

@@ -0,0 +1 @@
+global using NUnit.Framework;

Some files were not shown because too many files changed in this diff