Passing exception to logger as it's already localized

refs gh-1

Rem
	bbdoc: Maximus module base.
	about: An abstract type for the similarities between module scopes and modules.
End Rem
Type mxModuleBase Abstract

	Field m_name:String

'#region Field accessors

	Rem
		bbdoc: Set the base's name.
		returns: Nothing.
	End Rem
	Method SetName(name:String)
		Assert name, "(mxModuleBase.SetName) name cannot be Null"
		m_name = name
	End Method

	Rem
		bbdoc: Get the base's name.
		returns: The base's name.
	End Rem
	Method GetName:String()
		Return m_name
	End Method

'#end region Field accessors

	Rem
		bbdoc: Set a common field from the given variable.
		returns: True if the given variable was handled, or False if it was not.
	End Rem
	Method SetCommonFromVariable:Int(variable:dVariable)
		Return False
	End Method

End Type

Rem
	bbdoc: Maximus module scope.
End Rem
Type mxModuleScope Extends mxModuleBase

	Field m_modules:dObjectMap

	Method New()
		m_modules = New dObjectMap
	End Method

	Rem
		bbdoc: Add the given module to the scope.
		returns: True if the module was added, or False if it was not (the module is Null).
	End Rem
	Method AddModule:Int(modul:mxModule)
		If modul
			m_modules._Insert(modul.GetName(), modul)
			modul.SetParent(Self)
			Return True
		End If
		Return False
	End Method

	Rem
		bbdoc: Check if the given module name is found in the scope.
		returns: True if the given module was found in the scope, or False if it was not.
	End Rem
	Method HasModule:Int(modname:String)
		Return m_modules._Contains(modname)
	End Method

	Rem
		bbdoc: Get a module with the given name.
		returns: The module with the given name, or Null if there is no module with the given name.
	End Rem
	Method GetModuleWithName:mxModule(modname:String)
		Return mxModule(m_modules._ObjectWithKey(modname))
	End Method

	Rem
		bbdoc: Load the given dJObject into the scope.
		returns: Itself, or Null if @root is Null.
	End Rem
	Method FromJSON:mxModuleScope(root:dJObject)
		If root
			SetName(root.GetName())
			For Local variable:dVariable = EachIn root
				If dJObject(variable)
					AddModule(New mxModule.FromJSON(dJObject(variable)))
				'Else
				'	SetCommonFromVariable(variable)
				End If
			Next
			Return Self
		End If
		Return Null
	End Method

	Rem
		bbdoc: Get the module enumerator for the scope.
		returns: The module enumerator for the scope.
	End Rem
	Method ModuleEnumerator:TMapEnumerator()
		Return m_modules.ValueEnumerator()
	End Method

End Type

Rem
	bbdoc: Maximus module.
End Rem
Type mxModule Extends mxModuleBase

	Field m_parent:mxModuleScope
	Field m_description:String
	Field m_versions:dObjectMap

	Method New()
		m_versions = New dObjectMap
	End Method

'#region Field accessors

	Rem
		bbdoc: Set the module's parent.
		returns: Nothing.
	End Rem
	Method SetParent(parent:mxModuleScope)
		m_parent = parent
	End Method

	Rem
		bbdoc: Get the module's parent.
		returns: The module's parent.
	End Rem
	Method GetParent:mxModuleScope()
		Return m_parent
	End Method

	Rem
		bbdoc: Get the full name of the module (e.g. "modscope.module").
		returns: The full name of the module.
	End Rem
	Method GetFullName:String()
		Return m_parent.m_name + "." + m_name
	End Method

	Rem
		bbdoc: Set the module's description.
		returns: Nothing.
	End Rem
	Method SetDescription(description:String)
		m_description = description
	End Method

	Rem
		bbdoc: Get the module's description.
		returns: The module's description.
	End Rem
	Method GetDescription:String()
		Return m_description
	End Method

'#end region Field accessors

	Rem
		bbdoc: Get the installed version of the module (if it is installed).
		returns: The version of the module that is installed, or Null if the module is not installed.
	End Rem
	Method GetInstalledVersion:mxModuleVersion()
		Local version:String = mxModUtils.GetInstalledVersionFromVerID(GetFullName())
		If version
			Return New mxModuleVersion.Create(Self, version, Null)
		End If
		Return Null
	End Method

	Rem
		bbdoc: Add the given version to the module.
		returns: True if the version was added, or False if it was not (the version is Null).
	End Rem
	Method AddVersion:Int(version:mxModuleVersion)
		If version
			m_versions._Insert(version.GetName(), version)
			version.SetParent(Self)
			Return True
		End If
		Return False
	End Method

	Rem
		bbdoc: Check if there is a version with the given name.
		returns: True if the version was found, or False if it was not.
	End Rem
	Method HasVersion:Int(name:String)
		Return m_versions._Contains(name)
	End Method

	Rem
		bbdoc: Get a version with the given name.
		returns: The version with the given name, or Null if there is no version with the given name.
	End Rem
	Method GetVersionWithName:mxModuleVersion(name:String)
		Return mxModuleVersion(m_versions._ObjectWithKey(name))
	End Method

	Rem
		bbdoc: Get the latest (non-dev if available) version for this module.
		returns: The latest version for this module.
		about: The latest version will be returned, or the dev version if it is the only version.
	End Rem
	Method GetLatestVersion:mxModuleVersion()
		Local hver:mxModuleVersion
		For Local ver:mxModuleVersion = EachIn VersionEnumerator()
			If ver.GetName() <> "dev"
				If Not hver Or ver.Compare(hver) = 1
					hver = ver
				End If
			End If
		Next
		If Not hver
			hver = GetVersionWithName("dev")
		End If
		Return hver
	End Method

	Rem
		bbdoc: Set a common field from the given variable.
		returns: True if the given variable was handled, or False if it was not.
	End Rem
	Method SetCommonFromVariable:Int(variable:dVariable)
		If Super.SetCommonFromVariable(variable)
			Return True
		Else
			Select variable.GetName().ToLower()
				Case "desc"
					SetDescription(dValueVariable(variable).ValueAsString())
				Case "versions"
					For Local jobj:dJObject = EachIn dJObject(variable)
						AddVersion(New mxModuleVersion.FromJSON(jobj))
					Next
				Default
					Return False
			End Select
			Return True
		End If
	End Method

	Rem
		bbdoc: Load the given dJObject into the module.
		returns: Itself, or Null if @root is Null.
	End Rem
	Method FromJSON:mxModule(root:dJObject)
		If root
			SetName(root.GetName())
			For Local variable:dVariable = EachIn root
				SetCommonFromVariable(variable)
			Next
			Return Self
		End If
		Return Null
	End Method

	Rem
		bbdoc: Get the version enumerator for the module.
		returns: The version enumerator for the module.
	End Rem
	Method VersionEnumerator:TMapEnumerator()
		Return m_versions.ValueEnumerator()
	End Method

End Type

Rem
	bbdoc: Maximus module version.
End Rem
Type mxModuleVersion

	Field m_parent:mxModule
	Field m_name:String, m_url:String
	Field m_dependencies:mxModuleDependencies

	Method New()
		m_dependencies = New mxModuleDependencies
	End Method

	Rem
		bbdoc: Create a module version.
		returns: Itself.
	End Rem
	Method Create:mxModuleVersion(parent:mxModule, name:String, url:String)
		SetParent(parent)
		SetName(name)
		SetUrl(url)
		Return Self
	End Method

	Rem
		bbdoc: Clone the version.
		returns: A clone of the version.
		about: If @withdeps is True (default value), the dependencies will be added as well.
	End Rem
	Method Clone:mxModuleVersion(withdeps:Int = True)
		Local v:mxModuleVersion = New mxModuleVersion.Create(m_parent, m_name, m_url)
		For Local dep:mxModuleDependency = EachIn DependencyEnumerator()
			v.m_dependencies.AddDependency(dep)
		Next
		Return v
	End Method

'#region Field accessors

	Rem
		bbdoc: Set the version's parent.
		returns: Nothing.
	End Rem
	Method SetParent(parent:mxModule)
		m_parent = parent
	End Method

	Rem
		bbdoc: Get the version's parent.
		returns: The version's parent.
	End Rem
	Method GetParent:mxModule()
		Return m_parent
	End Method

	Rem
		bbdoc: Set the version's name.
		returns: Nothing.
	End Rem
	Method SetName(name:String)
		Assert name, "(mxModuleVersion.SetName) name cannot be Null!"
		m_name = name
	End Method

	Rem
		bbdoc: Get the version's name.
		returns: The version's name.
	End Rem
	Method GetName:String()
		Return m_name
	End Method

	Rem
		bbdoc: Set the version's url.
		returns: Nothing.
	End Rem
	Method SetUrl(url:String)
		'Assert url, "(mxModuleVersion.SetUrl) url cannot be Null!"
		m_url = url
	End Method

	Rem
		bbdoc: Get the version's url.
		returns: The version's url.
	End Rem
	Method GetUrl:String()
		Return m_url
	End Method

	Rem
		bbdoc: Get the temporary fetch path for the version's source archive.
		returns: The temporary file path for the version's source archive.
	End Rem
	Method GetTemporaryFilePath:String()
		Return "tmp/" + m_parent.GetFullName() + "-" + m_name + ".zip"
	End Method

	Rem
		bbdoc: Set the version's dependencies.
		returns: Nothing.
	End Rem
	Method SetDependencies(dependencies:mxModuleDependencies)
		m_dependencies = dependencies
	End Method

	Rem
		bbdoc: Get the version's dependencies.
		returns: The version's dependencies.
	End Rem
	Method GetDependencies:mxModuleDependencies()
		Return m_dependencies
	End Method

'#end region Field accessors

	Rem
		bbdoc: Set a common field from the given variable.
		returns: True if the given variable was handled, or False if it was not.
	End Rem
	Method SetCommonFromVariable:Int(variable:dVariable)
		Select variable.GetName().ToLower()
			Case "url"
				SetUrl(dValueVariable(variable).ValueAsString())
			Case "deps"
				m_dependencies.FromJSON(dJArray(variable))
			Default
				Return False
		End Select
		Return True
	End Method

	Rem
		bbdoc: Load the given dJObject into the version.
		returns: Itself, or Null if @root is Null.
	End Rem
	Method FromJSON:mxModuleVersion(root:dJObject)
		If root
			SetName(root.GetName())
			For Local variable:dVariable = EachIn root
				SetCommonFromVariable(variable)
			Next
			Return Self
		End If
		Return Null
	End Method

	Rem
		bbdoc: Get the dependency enumerator for the version.
		returns: The dependency enumerator for the version.
	End Rem
	Method DependencyEnumerator:TMapEnumerator()
		Return m_dependencies.DependencyEnumerator()
	End Method

	Rem
		bbdoc: Compare the version with the given version.
		returns: 0 if the two versions are the same (same in version number alone), 1 if this version is greater than the given, or -1 if the given version is greater than this version.
	End Rem
	Method Compare:Int(with:Object)
		Local ver:mxModuleVersion = mxModuleVersion(with)
		If ver
			Local version1:mxVersionObject = New mxVersionObject.Parse(m_name)
			Local version2:mxVersionObject = New mxVersionObject.Parse(ver.m_name)
			Return version1.Compare(version2)
		End If
		Return Super.Compare(with)
	End Method

	Rem
		bbdoc: Fetch the version's source archive.
		returns: Nothing.
	End Rem
	Method FetchSourceArchive()
		Local file:String = GetTemporaryFilePath()
		logger.LogMessage("fetching: " + GetUrl() + " -> " + file + "    ", False)
		If FileType(file) = FILETYPE_NONE
			Local stream:TStream = WriteFileExplicitly(file)
			If stream
				Local request:TRESTRequest = New TRESTRequest, response:TRESTResponse
				request.SetProgressCallback(_ProgressCallback, New _mxProgressStore)
				request.SetStream(stream)
				Try
					response = request.Call(GetUrl(), ["User-Agent: " + mainapp.m_useragent], "GET")
				Catch e:Object
					stream.Close()
					DeleteFile(file)
					logger.LogMessage("")
					ThrowError(_s("error.fetch.archive", [e.ToString()]))
				End Try
				stream.Close()
				If response.responseCode = 200
					logger.LogMessage(_s("message.fetch.done", [String(response.responseCode)]))
				Else
					DeleteFile(file)
					logger.LogMessage("")
					ThrowError(_s("error.fetch.archive", ["Bad response code: " + String(response.responseCode)]))
				End If
			Else
				ThrowError(_s("error.writeperms", [file]))
			End If
		Else
			logger.LogMessage(_s("message.fetch.alreadyfetched"))
		End If
	End Method

	Rem
		bbdoc: Progress callback for source archive fetching.
		returns: Zero (no error).
	End Rem
	Function _ProgressCallback:Int(data:Object, dltotal:Double, dlnow:Double, ultotal:Double, ulnow:Double)
		Local store:_mxProgressStore = _mxProgressStore(data)
		Local prog:Int = (dlnow / dltotal) * 100
		If prog > store.m_progress + 5
			store.m_progress = prog
			WriteStdOut(".")
		End If
		Return 0
	End Function

End Type

Rem
	bbdoc: A object representing a version, not to be confused with mxModuleVersion
	about: Supported formats are 1, 1.1 and 1.1.1.
	Do note that because the version is represented as a Double there's a change
	the precision of the final number is off.

	1.1.2 Would be converted to 1.001002 but when converted to a Double it results
	in 1.0010019999999999.

	1.1.12 However would be converted to 1.001012 and when converted to a Double it
	results in 1.0010120000000000.

	A version string of 'dev' will be represented as 99999
End Rem
Type mxVersionObject

	Field m_version:Double

	Rem
		bbdoc: Parse a version string
		returns: Itself.
	End Rem
	Method Parse:mxVersionObject(version:String)
		If version = "dev"
			m_version = 99999
			Return Self
		End If

		Local parts:String[] = version.Split(".")
		Local converted:String = parts[0]
		If parts.Length > 1
			converted:+"."
			For Local part:String = EachIn parts[1..]
				While part.Length <> 3
					part = "0" + part
				WEnd
				converted:+part
			Next

			'Make sure we format to x.x.x
			If parts.Length = 2
				converted:+"000"
			End If
		End If

		m_version = converted.ToDouble()
		Return Self
	End Method

	Rem
		bbdoc: Compare version numbers
	End Rem
	Method Compare:Int(withObject:Object)
		Local version:mxVersionObject = mxVersionObject(withObject)
		If version
			If m_version = version.m_version
				Return 0
			Else If m_version > version.m_version
				Return 1
			Else If m_version < version.m_version
				Return - 1
			End If
		End If
		Return Super.Compare(withObject)
	End Method
End Type

Rem
	bbdoc: Maximus metafile handler
End Rem
Type mxMetaFile

	Field m_metafile:String
	Field m_scope:String
	Field m_name:String
	Field m_version:String

	Rem
		bbdoc: Create a metafile handler.
		returns: Itself.
	End Rem
	Method Create:mxMetaFile(metafile:String)
		SetMetaFile(metafile)
		Return Self
	End Method

'#region Field accessors

	Rem
		bbdoc: Set the metafile file path.
		returns: Nothing.
	End Rem
	Method SetMetaFile(metafile:String)
		m_metafile = metafile
	End Method

	Rem
		bbdoc: Get the metafile path.
		returns: The metafile path.
	End Rem
	Method GetMetaFile:String()
		Return m_metafile
	End Method

	Rem
		bbdoc: Set metadata
		returns: Nothing.
	End Rem
	Method SetMetaData(scope:String, name:String, version:String)
		Self.m_scope = scope
		Self.m_name = name
		Self.m_version = version
	End Method

'#end region Field accessors

	Rem
		bbdoc: Load the configuration.
		returns: Nothing.
	End Rem
	Method Load()
		If FileType(GetMetaFile()) = FILETYPE_FILE
			Try
				Local contents:String = LoadText(GetMetaFile()).Trim()
				Local parts:String[] = contents.Split("/")

				If parts.Length <> 2
					Throw _s("error.load.meta.invalid_format") ;
				End If

				Local modinfo:String[] = parts[0].Split(".")
				m_scope = modinfo[0]
				m_name = modinfo[1]
				m_version = parts[1]
			Catch e:Object
				logger.LogError(_s("error.load.metafile.parse", [GetMetaFile()]))
				logger.LogError(e.ToString())
			End Try
		Else
			logger.LogWarning(_s("error.load.metafile.notfound", [GetMetaFile()]))
		End If
	End Method

	Rem
		bbdoc: Save the configuration
		returns: Nothing.
	End Rem
	Method Save()
		If m_scope.Length = 0 Or m_name.Length = 0 Or m_version.Length = 0
			ThrowError(_s("error.save.metafile.incomplete", [m_scope, m_name, m_version]))
		End If

		Local file:String = GetMetaFile()
		Local stream:TStream = WriteFileExplicitly(file)
		If stream
			stream.WriteString(m_scope + "." + m_name + "/" + m_version)
			stream.Close
		Else
			ThrowError(_s("error.writeperms", [file]))
		End If
	End Method
End Type

Rem
	bbdoc: Maximus module base.
	about: An abstract type for the similarities between module scopes and modules.
End Rem
Type mxModuleBase Abstract

	Field m_name:String

'#region Field accessors

	Rem
		bbdoc: Set the base's name.
		returns: Nothing.
	End Rem
	Method SetName(name:String)
		Assert name, "(mxModuleBase.SetName) name cannot be Null"
		m_name = name
	End Method

	Rem
		bbdoc: Get the base's name.
		returns: The base's name.
	End Rem
	Method GetName:String()
		Return m_name
	End Method

'#end region Field accessors

	Rem
		bbdoc: Set a common field from the given variable.
		returns: True if the given variable was handled, or False if it was not.
	End Rem
	Method SetCommonFromVariable:Int(variable:dVariable)
		Return False
	End Method

End Type

Rem
	bbdoc: Maximus module scope.
End Rem
Type mxModuleScope Extends mxModuleBase

	Field m_modules:dObjectMap

	Method New()
		m_modules = New dObjectMap
	End Method

	Rem
		bbdoc: Add the given module to the scope.
		returns: True if the module was added, or False if it was not (the module is Null).
	End Rem
	Method AddModule:Int(modul:mxModule)
		If modul
			m_modules._Insert(modul.GetName(), modul)
			modul.SetParent(Self)
			Return True
		End If
		Return False
	End Method

	Rem
		bbdoc: Check if the given module name is found in the scope.
		returns: True if the given module was found in the scope, or False if it was not.
	End Rem
	Method HasModule:Int(modname:String)
		Return m_modules._Contains(modname)
	End Method

	Rem
		bbdoc: Get a module with the given name.
		returns: The module with the given name, or Null if there is no module with the given name.
	End Rem
	Method GetModuleWithName:mxModule(modname:String)
		Return mxModule(m_modules._ObjectWithKey(modname))
	End Method

	Rem
		bbdoc: Load the given dJObject into the scope.
		returns: Itself, or Null if @root is Null.
	End Rem
	Method FromJSON:mxModuleScope(root:dJObject)
		If root
			SetName(root.GetName())
			For Local variable:dVariable = EachIn root
				If dJObject(variable)
					AddModule(New mxModule.FromJSON(dJObject(variable)))
				'Else
				'	SetCommonFromVariable(variable)
				End If
			Next
			Return Self
		End If
		Return Null
	End Method

	Rem
		bbdoc: Get the module enumerator for the scope.
		returns: The module enumerator for the scope.
	End Rem
	Method ModuleEnumerator:TMapEnumerator()
		Return m_modules.ValueEnumerator()
	End Method

End Type

Rem
	bbdoc: Maximus module.
End Rem
Type mxModule Extends mxModuleBase

	Field m_parent:mxModuleScope
	Field m_description:String
	Field m_versions:dObjectMap

	Method New()
		m_versions = New dObjectMap
	End Method

'#region Field accessors

	Rem
		bbdoc: Set the module's parent.
		returns: Nothing.
	End Rem
	Method SetParent(parent:mxModuleScope)
		m_parent = parent
	End Method

	Rem
		bbdoc: Get the module's parent.
		returns: The module's parent.
	End Rem
	Method GetParent:mxModuleScope()
		Return m_parent
	End Method

	Rem
		bbdoc: Get the full name of the module (e.g. "modscope.module").
		returns: The full name of the module.
	End Rem
	Method GetFullName:String()
		Return m_parent.m_name + "." + m_name
	End Method

	Rem
		bbdoc: Set the module's description.
		returns: Nothing.
	End Rem
	Method SetDescription(description:String)
		m_description = description
	End Method

	Rem
		bbdoc: Get the module's description.
		returns: The module's description.
	End Rem
	Method GetDescription:String()
		Return m_description
	End Method

'#end region Field accessors

	Rem
		bbdoc: Get the installed version of the module (if it is installed).
		returns: The version of the module that is installed, or Null if the module is not installed.
	End Rem
	Method GetInstalledVersion:mxModuleVersion()
		Local version:String = mxModUtils.GetInstalledVersionFromVerID(GetFullName())
		If version
			Return New mxModuleVersion.Create(Self, version, Null)
		End If
		Return Null
	End Method

	Rem
		bbdoc: Add the given version to the module.
		returns: True if the version was added, or False if it was not (the version is Null).
	End Rem
	Method AddVersion:Int(version:mxModuleVersion)
		If version
			m_versions._Insert(version.GetName(), version)
			version.SetParent(Self)
			Return True
		End If
		Return False
	End Method

	Rem
		bbdoc: Check if there is a version with the given name.
		returns: True if the version was found, or False if it was not.
	End Rem
	Method HasVersion:Int(name:String)
		Return m_versions._Contains(name)
	End Method

	Rem
		bbdoc: Get a version with the given name.
		returns: The version with the given name, or Null if there is no version with the given name.
	End Rem
	Method GetVersionWithName:mxModuleVersion(name:String)
		Return mxModuleVersion(m_versions._ObjectWithKey(name))
	End Method

	Rem
		bbdoc: Get the latest (non-dev if available) version for this module.
		returns: The latest version for this module.
		about: The latest version will be returned, or the dev version if it is the only version.
	End Rem
	Method GetLatestVersion:mxModuleVersion()
		Local hver:mxModuleVersion
		For Local ver:mxModuleVersion = EachIn VersionEnumerator()
			If ver.GetName() <> "dev"
				If Not hver Or ver.Compare(hver) = 1
					hver = ver
				End If
			End If
		Next
		If Not hver
			hver = GetVersionWithName("dev")
		End If
		Return hver
	End Method

	Rem
		bbdoc: Set a common field from the given variable.
		returns: True if the given variable was handled, or False if it was not.
	End Rem
	Method SetCommonFromVariable:Int(variable:dVariable)
		If Super.SetCommonFromVariable(variable)
			Return True
		Else
			Select variable.GetName().ToLower()
				Case "desc"
					SetDescription(dValueVariable(variable).ValueAsString())
				Case "versions"
					For Local jobj:dJObject = EachIn dJObject(variable)
						AddVersion(New mxModuleVersion.FromJSON(jobj))
					Next
				Default
					Return False
			End Select
			Return True
		End If
	End Method

	Rem
		bbdoc: Load the given dJObject into the module.
		returns: Itself, or Null if @root is Null.
	End Rem
	Method FromJSON:mxModule(root:dJObject)
		If root
			SetName(root.GetName())
			For Local variable:dVariable = EachIn root
				SetCommonFromVariable(variable)
			Next
			Return Self
		End If
		Return Null
	End Method

	Rem
		bbdoc: Get the version enumerator for the module.
		returns: The version enumerator for the module.
	End Rem
	Method VersionEnumerator:TMapEnumerator()
		Return m_versions.ValueEnumerator()
	End Method

End Type

Rem
	bbdoc: Maximus module version.
End Rem
Type mxModuleVersion

	Field m_parent:mxModule
	Field m_name:String, m_url:String
	Field m_dependencies:mxModuleDependencies

	Method New()
		m_dependencies = New mxModuleDependencies
	End Method

	Rem
		bbdoc: Create a module version.
		returns: Itself.
	End Rem
	Method Create:mxModuleVersion(parent:mxModule, name:String, url:String)
		SetParent(parent)
		SetName(name)
		SetUrl(url)
		Return Self
	End Method

	Rem
		bbdoc: Clone the version.
		returns: A clone of the version.
		about: If @withdeps is True (default value), the dependencies will be added as well.
	End Rem
	Method Clone:mxModuleVersion(withdeps:Int = True)
		Local v:mxModuleVersion = New mxModuleVersion.Create(m_parent, m_name, m_url)
		For Local dep:mxModuleDependency = EachIn DependencyEnumerator()
			v.m_dependencies.AddDependency(dep)
		Next
		Return v
	End Method

'#region Field accessors

	Rem
		bbdoc: Set the version's parent.
		returns: Nothing.
	End Rem
	Method SetParent(parent:mxModule)
		m_parent = parent
	End Method

	Rem
		bbdoc: Get the version's parent.
		returns: The version's parent.
	End Rem
	Method GetParent:mxModule()
		Return m_parent
	End Method

	Rem
		bbdoc: Set the version's name.
		returns: Nothing.
	End Rem
	Method SetName(name:String)
		Assert name, "(mxModuleVersion.SetName) name cannot be Null!"
		m_name = name
	End Method

	Rem
		bbdoc: Get the version's name.
		returns: The version's name.
	End Rem
	Method GetName:String()
		Return m_name
	End Method

	Rem
		bbdoc: Set the version's url.
		returns: Nothing.
	End Rem
	Method SetUrl(url:String)
		'Assert url, "(mxModuleVersion.SetUrl) url cannot be Null!"
		m_url = url
	End Method

	Rem
		bbdoc: Get the version's url.
		returns: The version's url.
	End Rem
	Method GetUrl:String()
		Return m_url
	End Method

	Rem
		bbdoc: Get the temporary fetch path for the version's source archive.
		returns: The temporary file path for the version's source archive.
	End Rem
	Method GetTemporaryFilePath:String()
		Return "tmp/" + m_parent.GetFullName() + "-" + m_name + ".zip"
	End Method

	Rem
		bbdoc: Set the version's dependencies.
		returns: Nothing.
	End Rem
	Method SetDependencies(dependencies:mxModuleDependencies)
		m_dependencies = dependencies
	End Method

	Rem
		bbdoc: Get the version's dependencies.
		returns: The version's dependencies.
	End Rem
	Method GetDependencies:mxModuleDependencies()
		Return m_dependencies
	End Method

'#end region Field accessors

	Rem
		bbdoc: Set a common field from the given variable.
		returns: True if the given variable was handled, or False if it was not.
	End Rem
	Method SetCommonFromVariable:Int(variable:dVariable)
		Select variable.GetName().ToLower()
			Case "url"
				SetUrl(dValueVariable(variable).ValueAsString())
			Case "deps"
				m_dependencies.FromJSON(dJArray(variable))
			Default
				Return False
		End Select
		Return True
	End Method

	Rem
		bbdoc: Load the given dJObject into the version.
		returns: Itself, or Null if @root is Null.
	End Rem
	Method FromJSON:mxModuleVersion(root:dJObject)
		If root
			SetName(root.GetName())
			For Local variable:dVariable = EachIn root
				SetCommonFromVariable(variable)
			Next
			Return Self
		End If
		Return Null
	End Method

	Rem
		bbdoc: Get the dependency enumerator for the version.
		returns: The dependency enumerator for the version.
	End Rem
	Method DependencyEnumerator:TMapEnumerator()
		Return m_dependencies.DependencyEnumerator()
	End Method

	Rem
		bbdoc: Compare the version with the given version.
		returns: 0 if the two versions are the same (same in version number alone), 1 if this version is greater than the given, or -1 if the given version is greater than this version.
	End Rem
	Method Compare:Int(with:Object)
		Local ver:mxModuleVersion = mxModuleVersion(with)
		If ver
			Local version1:mxVersionObject = New mxVersionObject.Parse(m_name)
			Local version2:mxVersionObject = New mxVersionObject.Parse(ver.m_name)
			Return version1.Compare(version2)
		End If
		Return Super.Compare(with)
	End Method

	Rem
		bbdoc: Fetch the version's source archive.
		returns: Nothing.
	End Rem
	Method FetchSourceArchive()
		Local file:String = GetTemporaryFilePath()
		logger.LogMessage("fetching: " + GetUrl() + " -> " + file + "    ", False)
		If FileType(file) = FILETYPE_NONE
			Local stream:TStream = WriteFileExplicitly(file)
			If stream
				Local request:TRESTRequest = New TRESTRequest, response:TRESTResponse
				request.SetProgressCallback(_ProgressCallback, New _mxProgressStore)
				request.SetStream(stream)
				Try
					response = request.Call(GetUrl(), ["User-Agent: " + mainapp.m_useragent], "GET")
				Catch e:Object
					stream.Close()
					DeleteFile(file)
					logger.LogMessage("")
					ThrowError(_s("error.fetch.archive", [e.ToString()]))
				End Try
				stream.Close()
				If response.responseCode = 200
					logger.LogMessage(_s("message.fetch.done", [String(response.responseCode)]))
				Else
					DeleteFile(file)
					logger.LogMessage("")
					ThrowError(_s("error.fetch.archive", ["Bad response code: " + String(response.responseCode)]))
				End If
			Else
				ThrowError(_s("error.writeperms", [file]))
			End If
		Else
			logger.LogMessage(_s("message.fetch.alreadyfetched"))
		End If
	End Method

	Rem
		bbdoc: Progress callback for source archive fetching.
		returns: Zero (no error).
	End Rem
	Function _ProgressCallback:Int(data:Object, dltotal:Double, dlnow:Double, ultotal:Double, ulnow:Double)
		Local store:_mxProgressStore = _mxProgressStore(data)
		Local prog:Int = (dlnow / dltotal) * 100
		If prog > store.m_progress + 5
			store.m_progress = prog
			WriteStdOut(".")
		End If
		Return 0
	End Function

End Type

Rem
	bbdoc: A object representing a version, not to be confused with mxModuleVersion
	about: Supported formats are 1, 1.1 and 1.1.1.
	Do note that because the version is represented as a Double there's a change
	the precision of the final number is off.

	1.1.2 Would be converted to 1.001002 but when converted to a Double it results
	in 1.0010019999999999.

	1.1.12 However would be converted to 1.001012 and when converted to a Double it
	results in 1.0010120000000000.

	A version string of 'dev' will be represented as 99999
End Rem
Type mxVersionObject

	Field m_version:Double

	Rem
		bbdoc: Parse a version string
		returns: Itself.
	End Rem
	Method Parse:mxVersionObject(version:String)
		If version = "dev"
			m_version = 99999
			Return Self
		End If

		Local parts:String[] = version.Split(".")
		Local converted:String = parts[0]
		If parts.Length > 1
			converted:+"."
			For Local part:String = EachIn parts[1..]
				While part.Length <> 3
					part = "0" + part
				WEnd
				converted:+part
			Next

			'Make sure we format to x.x.x
			If parts.Length = 2
				converted:+"000"
			End If
		End If

		m_version = converted.ToDouble()
		Return Self
	End Method

	Rem
		bbdoc: Compare version numbers
	End Rem
	Method Compare:Int(withObject:Object)
		Local version:mxVersionObject = mxVersionObject(withObject)
		If version
			If m_version = version.m_version
				Return 0
			Else If m_version > version.m_version
				Return 1
			Else If m_version < version.m_version
				Return - 1
			End If
		End If
		Return Super.Compare(withObject)
	End Method
End Type

Rem
	bbdoc: Maximus metafile handler
End Rem
Type mxMetaFile

	Field m_metafile:String
	Field m_scope:String
	Field m_name:String
	Field m_version:String

	Rem
		bbdoc: Create a metafile handler.
		returns: Itself.
	End Rem
	Method Create:mxMetaFile(metafile:String)
		SetMetaFile(metafile)
		Return Self
	End Method

'#region Field accessors

	Rem
		bbdoc: Set the metafile file path.
		returns: Nothing.
	End Rem
	Method SetMetaFile(metafile:String)
		m_metafile = metafile
	End Method

	Rem
		bbdoc: Get the metafile path.
		returns: The metafile path.
	End Rem
	Method GetMetaFile:String()
		Return m_metafile
	End Method

	Rem
		bbdoc: Set metadata
		returns: Nothing.
	End Rem
	Method SetMetaData(scope:String, name:String, version:String)
		Self.m_scope = scope
		Self.m_name = name
		Self.m_version = version
	End Method

'#end region Field accessors

	Rem
		bbdoc: Load the configuration.
		returns: Nothing.
	End Rem
	Method Load()
		If FileType(GetMetaFile()) = FILETYPE_FILE
			Try
				Local contents:String = LoadText(GetMetaFile()).Trim()
				Local parts:String[] = contents.Split("/")

				If parts.Length <> 2
					Throw _s("error.load.meta.invalid_format") ;
				End If

				Local modinfo:String[] = parts[0].Split(".")
				m_scope = modinfo[0]
				m_name = modinfo[1]
				m_version = parts[1]
			Catch e:Object
				logger.LogError(_s("error.load.metafile.parse", [GetMetaFile()]))
				logger.LogError(_s("error.load.metafile.parse", [e.ToString()]))
			End Try
		Else
			logger.LogWarning(_s("error.load.metafile.notfound", [GetMetaFile()]))
		End If
	End Method

	Rem
		bbdoc: Save the configuration
		returns: Nothing.
	End Rem
	Method Save()
		If m_scope.Length = 0 Or m_name.Length = 0 Or m_version.Length = 0
			ThrowError(_s("error.save.metafile.incomplete", [m_scope, m_name, m_version]))
		End If

		Local file:String = GetMetaFile()
		Local stream:TStream = WriteFileExplicitly(file)
		If stream
			stream.WriteString(m_scope + "." + m_name + "/" + m_version)
			stream.Close
		Else
			ThrowError(_s("error.writeperms", [file]))
		End If
	End Method
End Type


b3dloader is now slightly more efficient

Strict

Rem
	bbdoc: Blitz3D model loader for MaxB3D
End Rem
Module MaxB3D.B3DLoader
ModuleInfo "Author: Kevin Primm"
ModuleInfo "License: MIT"

Import MaxB3D.Core
Import MaxB3D.BoneAnimator
Import Prime.libB3D

Type TMeshLoaderB3D Extends TMeshLoader
	Field _mesh:TMesh

	Method Run(mesh:TMesh,stream:TStream,url:Object)
		Local model:TBB3DChunk=TBB3DChunk.Load(url)
		If model=Null Return False

		Local olddir$=CurrentDir()
		If String(url) ChangeDir(ExtractDir(String(url)))

		Local texture:TTexture[model.texs.length]
		Local brush:TBrush[model.brus.length]

		For Local i=0 To texture.length-1
			Local chunk:TTEXSChunk=model.texs[i]
			texture[i]=_currentworld.AddTexture(chunk.file,chunk.flags)
			If texture[i]
				texture[i].SetBlend chunk.blend
				texture[i].SetPosition chunk.x_pos,chunk.y_pos
				texture[i].SetScale chunk.x_scale,chunk.y_scale
				texture[i].SetRotation chunk.rotation*(180.0/Pi)
				If chunk.flags&65536 texture[i].SetCoords 1
			EndIf
		Next

		For Local i=0 To brush.length-1
			Local chunk:TBRUSChunk=model.brus[i]
			brush[i]=_currentworld.AddBrush()
			brush[i].SetName chunk.name
			brush[i].SetColor chunk.red*255,chunk.green*255,chunk.blue*255;brush[i].SetAlpha chunk.alpha
			brush[i].SetShine chunk.shininess
			brush[i].SetBlend chunk.blend;brush[i].SetFX chunk.fx
			For Local j=0 To chunk.n_texs-1
				If chunk.texture_id[j]>-1 brush[i].SetTexture texture[chunk.texture_id[j]],j
			Next
		Next
		ChangeDir olddir

		' model.dump StandardIOStream

		If model.node
			ParseNode model.node,mesh,brush,mesh
			Return True
		EndIf
		Return False
	End Method

	Method ParseNode(node:TNODEChunk,parent:TEntity,brush:TBrush[],entity:TEntity=Null)
		Local meshchunk:TMESHChunk=TMESHChunk(node.kind),bonechunk:TBONEChunk=TBONEChunk(node.kind)

		Select node.kind
		Case Null
			entity=_currentworld.AddPivot(parent)
		Case meshchunk
			If entity=Null entity=_currentworld.AddMesh("*empty*",parent)
			Local mesh:TMesh=TMesh(entity)
			mesh._animator=New TBoneAnimator
			_mesh=mesh

			If meshchunk.brush_id>-1 entity.SetBrush brush[meshchunk.brush_id]

			Local vrts:TVRTSChunk=meshchunk.vrts
			Local vertsurface:TSurface=New TSurface
			vertsurface.Resize(vrts.xyz.length/3,0)

			For Local i=0 To vertsurface.CountVertices()-1
				vertsurface.SetCoords i,vrts.xyz[i*3+0],vrts.xyz[i*3+1],vrts.xyz[i*3+2]
				If vrts.nxyz vertsurface.SetNormal i,vrts.nxyz[i*3+0],vrts.nxyz[i*3+1],vrts.nxyz[i*3+2]
				If vrts.rgba vertsurface.SetColor i,vrts.rgba[i*4+0]*255,vrts.rgba[i*4+1]*255,vrts.rgba[i*4+2]*255,vrts.rgba[i*4+3]
				If vrts.SetSize()>1
					For Local j=0 To vrts.SetCount()-1
						vertsurface.SetTexCoords i,1-vrts.tex_coords[i][j,0],vrts.tex_coords[i][j,1],j
					Next
				EndIf
			Next

			For Local i=0 To meshchunk.tris.length-1
				Local tri:TTRISChunk=meshchunk.tris[i]
				Local surface:TSurface=vertsurface.Copy()
				surface.Resize(-1,tri.vertex_id.length/3)
				If tri.brush_id>-1 surface.SetBrush brush[tri.brush_id]
				For Local t=0 To surface.CountTriangles()-1
					surface.SetTriangle t,tri.vertex_id[t*3+0],tri.vertex_id[t*3+1],tri.vertex_id[t*3+2]
				Next
				If vrts.nxyz=Null surface.UpdateNormals
				mesh.AppendSurface surface
			Next
		Case bonechunk
			entity=_currentworld.AddBone(parent)
			Local bone:TBone=TBone(entity)

			For Local surface:TSurface=EachIn _mesh._surfaces
				bone.AddSurface surface
				For Local i=0 To bonechunk.vertex_id.length-1
					bone.AddVertex surface,bonechunk.vertex_id[i],bonechunk.weight[i]
				Next
			Next

			Local ident_matrix:TMatrix=TMatrix.Identity()
			For Local chunk:TKEYSChunk=EachIn node.keys
				Local keys:TAnimKey[chunk.frame.length]
				Local pos#[]=chunk.position,rot#[]=chunk.rotation,scl#[]=chunk.scale
				For Local i=0 To chunk.frame.length-1
					Local bonekey:TBoneKey=New TBoneKey

					If pos.length>0 bonekey._px=pos[i*3+0];bonekey._py=pos[i*3+1];bonekey._pz=pos[i*3+2]
					If rot.length>0 bonekey._rw=rot[i*4+0];bonekey._rx=rot[i*4+1];bonekey._ry=rot[i*4+2];bonekey._rz=rot[i*4+3]
					If scl.length>0 bonekey._sx=scl[i*3+0];bonekey._sy=scl[i*3+1];bonekey._sz=scl[i*3+2]

					Local key:TAnimKey=New TAnimKey
					key._frame=chunk.frame[i]
					key._object=bonekey
					keys[i]=key
				Next
				TBoneAnimator(_mesh._animator).AddBone bone,keys
			Next
		Default
			entity=_currentworld.AddPivot(parent)
		End Select

		entity.SetName node.name
		entity.SetPosition node.position[0],node.position[1],node.position[2]
		Local pitch#,yaw#,roll#
		TQuaternion.Euler node.rotation[0],node.rotation[1],node.rotation[2],node.rotation[3],pitch,yaw,roll
		entity.SetRotation pitch,yaw,roll
		entity.SetScale node.scale[0],node.scale[1],node.scale[2]

		For Local child:TNODEChunk=EachIn node.node
			ParseNode child,entity,brush
		Next
		If TMesh(entity) If TBoneAnimator(TMesh(entity)._animator)._root=Null TMesh(entity)._animator=Null
		If TBone(entity) TBone(entity)._start_matrix=entity.GetMatrix()
	End Method

	Method Info$()
		Return "Blitz3D|b3d"
	End Method
	Method ModuleName$()
		Return "b3dloader"
	End Method
End Type
New TMeshLoaderB3D

Strict

Rem
	bbdoc: Blitz3D model loader for MaxB3D
End Rem
Module MaxB3D.B3DLoader
ModuleInfo "Author: Kevin Primm"
ModuleInfo "License: MIT"

Import MaxB3D.Core
Import MaxB3D.BoneAnimator
Import Prime.libB3D

Type TMeshLoaderB3D Extends TMeshLoader
	Field _mesh:TMesh

	Method Run(mesh:TMesh,stream:TStream,url:Object)
		Local model:TBB3DChunk=TBB3DChunk.Load(url)
		If model=Null Return False

		Local olddir$=CurrentDir()
		If String(url) ChangeDir(ExtractDir(String(url)))

		Local texture:TTexture[model.texs.length]
		Local brush:TBrush[model.brus.length]

		For Local i=0 To texture.length-1
			Local chunk:TTEXSChunk=model.texs[i]
			texture[i]=_currentworld.AddTexture(chunk.file,chunk.flags)
			If texture[i]
				texture[i].SetBlend chunk.blend
				texture[i].SetPosition chunk.x_pos,chunk.y_pos
				texture[i].SetScale chunk.x_scale,chunk.y_scale
				texture[i].SetRotation chunk.rotation*(180.0/Pi)
				If chunk.flags&65536 texture[i].SetCoords 1
			EndIf
		Next

		For Local i=0 To brush.length-1
			Local chunk:TBRUSChunk=model.brus[i]
			brush[i]=_currentworld.AddBrush()
			brush[i].SetName chunk.name
			brush[i].SetColor chunk.red*255,chunk.green*255,chunk.blue*255;brush[i].SetAlpha chunk.alpha
			brush[i].SetShine chunk.shininess
			brush[i].SetBlend chunk.blend;brush[i].SetFX chunk.fx
			For Local j=0 To chunk.n_texs-1
				If chunk.texture_id[j]>-1 brush[i].SetTexture texture[chunk.texture_id[j]],j
			Next
		Next
		ChangeDir olddir

		' model.dump StandardIOStream

		If model.node
			ParseNode model.node,mesh,brush,mesh
			Return True
		EndIf
		Return False
	End Method

	Method ParseNode(node:TNODEChunk,parent:TEntity,brush:TBrush[],entity:TEntity=Null)
		Local meshchunk:TMESHChunk=TMESHChunk(node.kind),bonechunk:TBONEChunk=TBONEChunk(node.kind)

		Select node.kind
		Case Null
			entity=_currentworld.AddPivot(parent)
		Case meshchunk
			If entity=Null entity=_currentworld.AddMesh("*empty*",parent)
			Local mesh:TMesh=TMesh(entity)
			mesh._animator=New TBoneAnimator
			_mesh=mesh

			If meshchunk.brush_id>-1 entity.SetBrush brush[meshchunk.brush_id]

			Local vrts:TVRTSChunk=meshchunk.vrts
			Local vertsurface:TSurface=New TSurface
			vertsurface.Resize(vrts.xyz.length/3,0)

			For Local i=0 To vertsurface.CountVertices()-1
				vertsurface.SetCoords i,vrts.xyz[i*3+0],vrts.xyz[i*3+1],vrts.xyz[i*3+2]
				If vrts.nxyz vertsurface.SetNormal i,vrts.nxyz[i*3+0],vrts.nxyz[i*3+1],vrts.nxyz[i*3+2]
				If vrts.rgba vertsurface.SetColor i,vrts.rgba[i*4+0]*255,vrts.rgba[i*4+1]*255,vrts.rgba[i*4+2]*255,vrts.rgba[i*4+3]
				For Local j=0 To vrts.SetCount()-1
					Local u#,v#
					If vrts.SetSize()>1 u=vrts.tex_coords[i][j,0];v=vrts.tex_coords[i][j,1]
					vertsurface.SetTexCoords i,1-u,v,j
				Next
			Next

			For Local i=0 To meshchunk.tris.length-1
				Local tri:TTRISChunk=meshchunk.tris[i]
				Local surface:TSurface=vertsurface.Copy()
				surface.Resize(-1,tri.vertex_id.length/3)
				If tri.brush_id>-1 surface.SetBrush brush[tri.brush_id]
				For Local t=0 To surface.CountTriangles()-1
					surface.SetTriangle t,tri.vertex_id[t*3+0],tri.vertex_id[t*3+1],tri.vertex_id[t*3+2]
				Next
				If vrts.nxyz=Null surface.UpdateNormals
				mesh.AppendSurface surface
			Next
		Case bonechunk
			entity=_currentworld.AddBone(parent)
			Local bone:TBone=TBone(entity)

			For Local surface:TSurface=EachIn _mesh._surfaces
				bone.AddSurface surface
				For Local i=0 To bonechunk.vertex_id.length-1
					bone.AddVertex surface,bonechunk.vertex_id[i],bonechunk.weight[i]
				Next
			Next

			Local ident_matrix:TMatrix=TMatrix.Identity()
			For Local chunk:TKEYSChunk=EachIn node.keys
				Local keys:TAnimKey[chunk.frame.length]
				Local pos#[]=chunk.position,rot#[]=chunk.rotation,scl#[]=chunk.scale
				For Local i=0 To chunk.frame.length-1
					Local bonekey:TBoneKey=New TBoneKey

					If pos.length>0 bonekey._px=pos[i*3+0];bonekey._py=pos[i*3+1];bonekey._pz=pos[i*3+2]
					If rot.length>0 bonekey._rw=rot[i*4+0];bonekey._rx=rot[i*4+1];bonekey._ry=rot[i*4+2];bonekey._rz=rot[i*4+3]
					If scl.length>0 bonekey._sx=scl[i*3+0];bonekey._sy=scl[i*3+1];bonekey._sz=scl[i*3+2]

					Local key:TAnimKey=New TAnimKey
					key._frame=chunk.frame[i]
					key._object=bonekey
					keys[i]=key
				Next
				TBoneAnimator(_mesh._animator).AddBone bone,keys
			Next
		Default
			entity=_currentworld.AddPivot(parent)
		End Select

		entity.SetName node.name
		entity.SetPosition node.position[0],node.position[1],node.position[2]
		Local pitch#,yaw#,roll#
		TQuaternion.Euler node.rotation[0],node.rotation[1],node.rotation[2],node.rotation[3],pitch,yaw,roll
		entity.SetRotation pitch,yaw,roll
		entity.SetScale node.scale[0],node.scale[1],node.scale[2]

		For Local child:TNODEChunk=EachIn node.node
			ParseNode child,entity,brush
		Next
		If TMesh(entity) If TBoneAnimator(TMesh(entity)._animator)._root=Null TMesh(entity)._animator=Null
		If TBone(entity) TBone(entity)._start_matrix=entity.GetMatrix()
	End Method

	Method Info$()
		Return "Blitz3D|b3d"
	End Method
	Method ModuleName$()
		Return "b3dloader"
	End Method
End Type
New TMeshLoaderB3D


core.bmx: Added some brief comments about the enumerators in there, since they're sort of mysterious otherwise.  They're probably still mysterious.  I'll have to work on that...

Rem
	Copyright (c) 2009 Noel R. Cower

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
EndRem

SuperStrict

Module LuGI.Core

ModuleInfo "Name: LuGI Core"
ModuleInfo "Description: Core API provided by LuGI to interact with BMax objects via Lua"
ModuleInfo "Author: Noel Cower"
ModuleInfo "License: MIT"
ModuleInfo "URL: http://github.com/nilium/lugi.mod/"

?Threaded
Import Brl.Threads
?
Import Brl.LinkedList
Import Pub.Lua
Import "lgcore.cpp"

Public

' LuGI field types - used by LuGI to determine how to access fields at runtime
' The only reason these are public is because the generated code uses them, otherwise I'd hide
' them away too.  But then if I hide them and use number constants in the generated code, we'll
' get the chance that changing these later will break existing glue code, and doing that is bad.
Const LUGI_BYTEFIELD:Int = $0001
Const LUGI_SHORTFIELD:Int = $0002
Const LUGI_INTFIELD:Int = $0004
Const LUGI_FLOATFIELD:Int = $0008
Const LUGI_LONGFIELD:Int = $0010
Const LUGI_DOUBLEFIELD:Int = $0020
Const LUGI_STRINGFIELD:Int = $0040
Const LUGI_OBJECTFIELD:Int = $0080
Const LUGI_ARRAYFIELD:Int = $0100
' Optional flag to specify that an integer field (Int, Short, Byte, or Long) should be treated as a
' boolean value when passed to Lua
Const LUGI_BOOLFIELDOPT:Int = $8000

Type LuGIInitFunction

	' Registers the callback provided as a pre-init stage routine
	Method PreInit:LuGIInitFunction(cb(vm:Byte Ptr, rfield(off%, typ%, name$, clas@ Ptr), rmethod(fn:Int(state@ Ptr), name$, clas@ Ptr)))
		_cb = cb
		?Threaded
		p_lugi_initlock.Lock()
		?
		If p_lugi_preinit_cb = Null Then
			p_lugi_preinit_cb = New TList
		EndIf
		p_lugi_preinit_cb.AddLast(Self)
		?Threaded
		p_lugi_initlock.Unlock()
		?
		Return Self
	End Method

	' Registers the callback provided as a post-init stage routine
	Method PostInit:LuGIInitFunction(cb(state:Byte Ptr, constructor:Int(state:Byte Ptr)))
		_cb = cb
		?Threaded
		p_lugi_initlock.Lock()
		?
		If p_lugi_postinit_cb = Null Then
			p_lugi_postinit_cb = New TList
		EndIf
		p_lugi_postinit_cb.AddLast(Self)
		?Threaded
		p_lugi_initlock.Unlock()
		?
		Return Self
	End Method

	' #region PRIVATE

	Field _cb:Byte Ptr

	Method Pre(vm:Byte Ptr, rfield(off%, typ%, name$, clas@ Ptr), rmethod(fn:Int(state@ Ptr), name$, clas@ Ptr))
		Local cb(vm:Byte Ptr, rfield(off%, typ%, name$, clas@ Ptr), rmethod(fn:Int(state@ Ptr), name$, clas@ Ptr)) = _cb
		cb(vm, rfield, rmethod)
	End Method

	Method Post(vm:Byte Ptr, constructor:Int(state:Byte Ptr))
		Local cb(vm:Byte Ptr, constructor:Int(state:Byte Ptr)) = _cb
		cb(vm, constructor)
	End Method

	' #endregion

End Type

Private

' There is really no good way to do this in a thread-safe manner, so the best I can do is to make
' all access to the initialization lists a critical section
?Threaded
Global p_lugi_initlock:TMutex = TMutex.Create()
?
Global p_lugi_preinit_cb:TList
Global p_lugi_postinit_cb:TList

' Executes init functions that are to occur prior to creation of any BMax objects, VMTs, etc.
' Doesn't lock the lists since it's only supposed to be called after the lists have been locked
Function PreInitLuGI(vm@ Ptr)
	If Not p_lugi_preinit_cb Then
		Return
	EndIf
	For Local cb:LuGIInitFunction = EachIn p_lugi_preinit_cb
		cb.Pre(vm, p_lugi_register_field, p_lugi_register_method)
	Next
End Function

' Executes init functions that are to occur following the creation of BMax VMTs, metatables, cache, etc.
' Doesn't lock the lists since it's only supposed to be called after the lists have been locked
Function PostInitLuGI(vm@ Ptr)
	If Not p_lugi_postinit_cb Then
		Return
	EndIf
	For Local cb:LuGIInitFunction = EachIn p_lugi_postinit_cb
		cb.Post(vm, p_lugi_new_object)
	Next
End Function

' Private/internal code that shouldn't be directly accessed (only provided)
Extern "C"
	Function p_lugi_register_method(fn:Int(state@ Ptr), name$, clas@ Ptr=Null)
	Function p_lugi_register_field(off%, typ%, name$, clas@ Ptr)

	Function p_lugi_init(state@ Ptr)

	' Constructor object - push with BBClass for type as upvalue
	Function p_lugi_new_object:Int(state@ Ptr)
End Extern

Public

Extern "C"
	' Pushing/getting BBObjects (BBObjects, BBStrings, and BBArrays to tables/tables to BBArrays respectively)
	Function lua_pushbmaxobject(state@ Ptr, obj:Object)
	Function lua_tobmaxobject:Object(state@ Ptr, index:Int)

	' Pushing/getting BBArrays (BBArrays to tables/tables to BBArrays respectively)
	' @arr has to be treated as an Object in BMax since passing an Int[] as an Object[] doesn't work
	Function lua_pushbmaxarray(state@ Ptr, arr:Object)
	Function lua_tobmaxarray:Object(state@ Ptr, index:Int)
End Extern

' Initializes the Lua state for use with BMax objects via LuGI
Function InitLuGI(vm:Byte Ptr)
	?Threaded
	p_lugi_initlock.Lock()
	?
	PreInitLuGI(vm)
	p_lugi_init(vm)
	PostInitLuGI(vm)
	?Threaded
	p_lugi_initlock.Unlock()
	?
End Function

Rem
	Copyright (c) 2009 Noel R. Cower

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
EndRem

SuperStrict

Module LuGI.Core

ModuleInfo "Name: LuGI Core"
ModuleInfo "Description: Core API provided by LuGI to interact with BMax objects via Lua"
ModuleInfo "Author: Noel Cower"
ModuleInfo "License: MIT"
ModuleInfo "URL: http://github.com/nilium/lugi.mod/"

?Threaded
Import Brl.Threads
?
Import Brl.LinkedList
Import Pub.Lua
Import "lgcore.cpp"

Public

Const LUGI_BYTEFIELD:Int = $0001
Const LUGI_SHORTFIELD:Int = $0002
Const LUGI_INTFIELD:Int = $0004
Const LUGI_FLOATFIELD:Int = $0008
Const LUGI_LONGFIELD:Int = $0010
Const LUGI_DOUBLEFIELD:Int = $0020
Const LUGI_STRINGFIELD:Int = $0040
Const LUGI_OBJECTFIELD:Int = $0080
Const LUGI_ARRAYFIELD:Int = $0100
Const LUGI_BOOLFIELDOPT:Int = $8000

Type LuGIInitFunction

	' Registers the callback provided as a pre-init stage routine
	Method PreInit:LuGIInitFunction(cb(vm:Byte Ptr, rfield(off%, typ%, name$, clas@ Ptr), rmethod(fn:Int(state@ Ptr), name$, clas@ Ptr)))
		_cb = cb
		?Threaded
		p_lugi_initlock.Lock()
		?
		If p_lugi_preinit_cb = Null Then
			p_lugi_preinit_cb = New TList
		EndIf
		p_lugi_preinit_cb.AddLast(Self)
		?Threaded
		p_lugi_initlock.Unlock()
		?
		Return Self
	End Method

	' Registers the callback provided as a post-init stage routine
	Method PostInit:LuGIInitFunction(cb(state:Byte Ptr, constructor:Int(state:Byte Ptr)))
		_cb = cb
		?Threaded
		p_lugi_initlock.Lock()
		?
		If p_lugi_postinit_cb = Null Then
			p_lugi_postinit_cb = New TList
		EndIf
		p_lugi_postinit_cb.AddLast(Self)
		?Threaded
		p_lugi_initlock.Unlock()
		?
		Return Self
	End Method

	' #region PRIVATE

	Field _cb:Byte Ptr

	Method Pre(vm:Byte Ptr, rfield(off%, typ%, name$, clas@ Ptr), rmethod(fn:Int(state@ Ptr), name$, clas@ Ptr))
		Local cb(vm:Byte Ptr, rfield(off%, typ%, name$, clas@ Ptr), rmethod(fn:Int(state@ Ptr), name$, clas@ Ptr)) = _cb
		cb(vm, rfield, rmethod)
	End Method

	Method Post(vm:Byte Ptr, constructor:Int(state:Byte Ptr))
		Local cb(vm:Byte Ptr, constructor:Int(state:Byte Ptr)) = _cb
		cb(vm, constructor)
	End Method

	' #endregion

End Type

Private

' There is really no good way to do this in a thread-safe manner, so the best I can do is to make
' all access to the initialization lists a critical section
?Threaded
Global p_lugi_initlock:TMutex = TMutex.Create()
?
Global p_lugi_preinit_cb:TList
Global p_lugi_postinit_cb:TList

' Executes init functions that are to occur prior to creation of any BMax objects, VMTs, etc.
' Doesn't lock the lists since it's only supposed to be called after the lists have been locked
Function PreInitLuGI(vm@ Ptr)
	If Not p_lugi_preinit_cb Then
		Return
	EndIf
	For Local cb:LuGIInitFunction = EachIn p_lugi_preinit_cb
		cb.Pre(vm, p_lugi_register_field, p_lugi_register_method)
	Next
End Function

' Executes init functions that are to occur following the creation of BMax VMTs, metatables, cache, etc.
' Doesn't lock the lists since it's only supposed to be called after the lists have been locked
Function PostInitLuGI(vm@ Ptr)
	If Not p_lugi_postinit_cb Then
		Return
	EndIf
	For Local cb:LuGIInitFunction = EachIn p_lugi_postinit_cb
		cb.Post(vm, p_lugi_new_object)
	Next
End Function

' Private/internal code that shouldn't be directly accessed (only provided)
Extern "C"
	Function p_lugi_register_method(fn:Int(state@ Ptr), name$, clas@ Ptr=Null)
	Function p_lugi_register_field(off%, typ%, name$, clas@ Ptr)

	Function p_lugi_init(state@ Ptr)

	' Constructor object - push with BBClass for type as upvalue
	Function p_lugi_new_object:Int(state@ Ptr)
End Extern

Public

Extern "C"
	' Pushing/getting BBObjects (BBObjects, BBStrings, and BBArrays to tables/tables to BBArrays respectively)
	Function lua_pushbmaxobject(state@ Ptr, obj:Object)
	Function lua_tobmaxobject:Object(state@ Ptr, index:Int)

	' Pushing/getting BBArrays (BBArrays to tables/tables to BBArrays respectively)
	' @arr has to be treated as an Object in BMax since passing an Int[] as an Object[] doesn't work
	Function lua_pushbmaxarray(state@ Ptr, arr:Object)
	Function lua_tobmaxarray:Object(state@ Ptr, index:Int)
End Extern

' Initializes the Lua state for use with BMax objects via LuGI
Function InitLuGI(vm:Byte Ptr)
	?Threaded
	p_lugi_initlock.Lock()
	?
	PreInitLuGI(vm)
	p_lugi_init(vm)
	PostInitLuGI(vm)
	?Threaded
	p_lugi_initlock.Unlock()
	?
End Function


remember all ops

remember all ops

' Blue Moon
' Bytecode compiler: accepts program fragments as strings, or program files (and outputs)

SuperStrict

Import "blueparser.bmx"
Import "blueerror.bmx"
Import "blueversion.bmx"
Private
Include "TFold.bmx"
Include "bluefoldsyntax.bmx"
Include "bluefoldsemantics.bmx"
Include "bluefoldcodegen.bmx"
Include "debugfold.bmx"
Public

Type BlueCompiler
	Function CompileFile(infile:String, outfile:String = "")
		If outfile = "" Then outfile = infile + ".so"
		Local n:TParseNode = BlueParseFile(infile)
		Local compiled:BlueBinary = BlueBinary.Make(infile, ASTToAssembly(n))
		compiled.WriteObjectFile(outFile)
	End Function

	Function CompileFileForLoad:BlueBinary(infile:String)
		Local n:TParseNode = BlueParseFile(infile)
		Local code:BlueBinary = BlueBinary.Make(infile, ASTToAssembly(n))
		Local sz:Int, mem:Byte Ptr = code.WriteMemory(sz)
		code = code.ReadMemory(mem, sz) ; MemFree(mem)
		Return code
	End Function

'	Function CompileString:BlueBinary(program:String)
'		Local n:TParseNode = BlueParseString(program)
'		Return BlueBinary.Make("<string>", ASTToAssembly(n))
'	End Function

	Function ShowBytecode:String(infile:String)
		Local n:TParseNode = BlueParseFile(infile), out:String = infile + ":~n"
		Local assemblies:TList = ASTToAssembly(n)
		For Local f:BlueAssembly = EachIn assemblies
			out :+ f.ToString()
		Next
		Return out
	End Function

	Function ASTToAssembly:TList(n:TParseNode)
		Local tbl:NodeTable = Node.FromParse(n), r:Node = tbl.n

		r = Node.Fold(r, NormalizeFold.Make(tbl))

		Local errf:SyntaxErrFold = SyntaxErrFold.Make(tbl)
		r = Node.Fold(r, errf) ; errf.BreakCheck()

		r = Node.Fold(r, New ConstantFold)

		Local semf:SemanticFold = SemanticFold.Make(r, tbl)
		r = Node.Fold(r, semf, Node.BOTH)	'fold both ways to push and pop environments
		SemanticFold.Close(semf, r)

		Local funs:FunDef[] = FunDef[](semf.funs.ToArray()) ; semf = Null

		' if we wanted a type checker, it would go here
		tbl = Null	'source information no longer needed

		Local assemblies:TList = CreateList()
		For Local f:FunDef = EachIn funs
			Local com:CompileFold = CompileFold.Make(BlueAssembly.Make(f))
		'	Print "F #" + f.id + ": " + f.n.ToString()
			Local inst:TList = BytecodeNode(Node.Fold(f.n, com)).inst
			com.f.Finish(inst, com.opc)
			assemblies.AddLast(com.f)
		Next

		?Debug
		Debug_KeyListFold.Run(funs, CompileFold.Make(Null))
		?
		Return assemblies
	End Function
End Type

Type BlueOpcode Final
	Field MOV:Int = 0, GETLC:Int = 1, SETLC:Int = 2		'these are constants, ignore their definition as fields
	Field LOADK:Int = 3, LOADSI:Int = 4, LOADBOOL:Int = 5, LOADNIL:Int = 6

	Field GETTAB:Int = 7, GETTABSI:Int = 8, SETTAB:Int = 9, SETTABSI:Int = 10
	Field GETTABI:Int = 11, SETTABI:Int = 12, GETUPV:Int = 13, SETUPV:Int = 14
	Field NEWTAB:Int = 15, CLOSURE:Int = 16, NEWUPV:Int = 17

	Field ADD:Int = 18, SUB:Int = 19, MUL:Int = 20, DIV:Int = 21, NMOD:Int = 22, POW:Int = 23, CAT:Int = 24
	Field IDIV:Int = 25, BAND:Int = 26, BOR:Int = 27, BXOR:Int = 28, BSHL:Int = 29, BSHR:Int = 30
	Field UNM:Int = 31, LNOT:Int = 32, ALEN:Int = 33, BNOT:Int = 34, UNP:Int = 35
	Field EQ:Int = 36, LT:Int = 37, LEQ:Int = 38

	Field JMP:Int = 39, JIF:Int = 40, JNOT:Int = 41
	Field CALL:Int = 42, TCALL:Int = 43, RET:Int = 44, RETVA:Int = 45
	Field POSTCALL:Int = 46, VARARG:Int = 47, VAINIT:Int = 48, CALLINIT:Int = 49

	Function Make:Int(i:BlueInstr)
		Global opc:BlueOpcode = New Self
		Local c:Int = i.c
		Select i.op
			Case opc.LOADK, opc.LOADSI, opc.LOADBOOL, opc.LOADNIL, opc.JMP, opc.JIF, opc.JNOT, opc.RET
				If c < 0 Then c :& (Int(2^18) - 1)
				Return i.op | ((i.ra & $FF) Shl 6) | (c Shl 14)
			Default
				If c < 0 Then c :& (Int(2^10) - 1)
				Return i.op | ((i.ra & $FF) Shl 6) | ((i.rb & $FF) Shl 14) | (c Shl 22)
		End Select
	End Function
	Function Read(i:Int, op:Int Var, ra:Int Var, rb:Int Var, c:Int Var)
		Global opc:BlueOpcode = New Self
		op = i & %111111 ; ra = (i Shr 6) & $FF
		Select op
			Case opc.LOADK, opc.LOADSI, opc.LOADBOOL, opc.LOADNIL, opc.JMP, opc.JIF, opc.JNOT, opc.RET
				rb = 0 ; c = i Sar 14	'sign-extend (already MSB)
			Default
				rb = (i Shr 14) & $FF ; c = i Sar 22
		End Select
	End Function
End Type

Type BlueInstr
	Field op:Int, ra:Int, rb:Int, c:Int
	Function Make:BlueInstr(op:Int, ra:Int, rb:Int, c:Int = 0)
		Local i:BlueInstr = New BlueInstr ; i.op = op ; i.ra = ra ; i.rb = rb ; i.c = c ; Return i
	End Function
	Function MakeK:BlueInstr(op:Int, ra:Int, c:Int)
		Return Make(op, ra, 0, c)
	End Function
	Method ToString:String()
		Global opc:BlueOpcode = New BlueOpcode
		Local n:String = "??"
		For Local f:TField = EachIn TTypeId.ForObject(opc).Fields()
			If f.GetInt(opc) = op Then n = f.Name()[..10] ; Exit
		Next
		Select op
			Case opc.LOADNIL, opc.NEWUPV, opc.NEWTAB
				Return n + " " + ra
			Case opc.MOV, opc.SETLC, opc.GETLC, opc.SETUPV, opc.GETUPV
				Return n + " " + ra + ", " + rb
			Case opc.LOADK, opc.LOADSI, opc.LOADBOOL, opc.CLOSURE, opc.RET, opc.POSTCALL, opc.VARARG
				Return n + " " + ra + ", " + c
			Default ; Return n + " " + ra + ", " + rb + ", " + c
		End Select
	End Method
End Type

Type BlueBinary
	Const MAGICNUMBER:Long = 5642816051412814914:Long	'BLUEMOON
	Field out:Int[], buf:Int[]	'out = output bytestream, buf = input/readable bytestream

	Function Make:BlueBinary(file:String, funs:TList)
		Local b:BlueBinary = New Self, ktbl:TList = CreateList(), kc:Int = 0

		Local binSz:Int = 0, fcount:Int = 0
		For Local f:BlueAssembly = EachIn funs
			Local ki:Int = 0
			For Local k:String = EachIn f.ktbl
				f.kidx[ki] = AddKTblEntry(k, ktbl, kc) ; ki :+ 1	'consolidate constants into single table
			Next

			If f.framesz > f.MAXVARS - 3 Or f.upvars > f.MAXVARS Then Throw ..
				BlueAssemblyError.Make(file, "too many variables/temporaries in function #" + f.id ..
				+ "; consider splitting into more blocks or helper functions")

			binSz :+ (7 ..	'id, instruction count, k count, param count, var count, upvar count, frame size
			       + 2 * f.upvars ..	'upvar levels and stack positions (interleaved)
			       + f.kc ..	'constant index table
			       + f.inst.Count())	'instructions
			fcount :+ 1
		Next

		binSz :+ (kc + 1)
		For Local k:String = EachIn ktbl
			If k[0] = "0"[0] Then binSz :+ 2 Else binSz :+ Ceil((k.Length - 2) / 2.0) + 2
		Next

		Local headerSz:Int = 3 + fcount	'function count, k count, k table offset, function offset list
		binSz :+ headerSz

		Local out:Int[] = New Int[binSz] ; b.out = out
		out[0] = fcount
		out[1] = kc

		Local pos:Int = headerSz, opc:BlueOpcode = New BlueOpcode
		For Local f:BlueAssembly = EachIn funs
			out[3 + f.id] = pos
			out[pos] = f.id ; out[pos + 1] = f.inst.Count() ; out[pos + 2] = f.kc ; out[pos + 3] = f.params
			out[pos + 4] = f.vars ; out[pos + 5] = f.upvars ; out[pos + 6] = f.framesz
			pos :+ 7
			For Local v:Int = 0 Until f.upvars
				out[pos] = f.upvarTier[v] ; out[pos + 1] = f.upvarStkPos[v] ; pos :+ 2
			Next
			For Local k:Int = 0 Until f.kc
				out[pos] = f.kidx[k] ; pos :+ 1
			Next
			For Local i:BlueInstr = EachIn f.inst
				out[pos] = BlueOpcode.Make(i) ; pos :+ 1
			Next
		Next

		out[2] = pos ; Local ktblpos:Int = pos ; pos :+ (kc + 1)
		For Local k:String = EachIn ktbl
			Local d:Double, dp:Byte Ptr = Varptr(d)
			out[ktblpos] = pos ; ktblpos :+ 1
			If k[0] = "0"[0]
				d = Double(k[2..])
				out[pos] = Int Ptr(dp)[0] ; out[pos + 1] = Int Ptr(dp)[1]
				pos :+ 2
			Else
				d = k.Length - 2
				out[pos] = Int Ptr(dp)[0] ; out[pos + 1] = Int Ptr(dp)[1] ; pos :+ 2
				If k.Length Mod 2 = 1 Then k :+ " "	'even the length for ease of writing
				For Local c:Int = 2 Until k.Length Step 2
					out[pos] = Short(k[c]) | (Short(k[c + 1]) Shl 16) ; pos :+ 1
				Next
			EndIf
		Next
		out[ktblpos] = pos
		Assert pos = binSz, "assembly write error: binary size mismatch (critical compiler error)"

		Return b
	End Function

	Function AddKTblEntry:Int(k:String, tbl:TList, kc:Int Var)
		Local ki:Int = 0
		For Local ek:String = EachIn tbl
			If ek = k Then Return ki
			ki :+ 1
		Next
		tbl.AddLast(k) ; kc :+ 1'ktbl.Count()
		Return ki
	End Function

	Method WriteMemory:Byte Ptr(size:Int Var)
		size = out.Length * 8 + 16
		Local mem:Byte Ptr = MemAlloc(size)
		Assert mem, "failed to allocate memory for instruction buffer"
		Write(CreateRamStream(mem, out.Length * 8 + 16, 0, 1))
		Return mem
	End Method

	Method WriteObjectFile(filename:String)
		Write(WriteFile(filename))
	End Method

	Function ReadMemory:BlueBinary(mem:Byte Ptr, size:Int)
		Return Read(CreateRamStream(mem, size, 1, 0), "<ram stream>", size)
	End Function

	Function ReadObjectFile:BlueBinary(filename:String)
		Return Read(ReadFile(filename), filename, FileSize(filename))
	End Function

	Function Read:BlueBinary(file:TStream, filename:String, sz:Int)
		Local b:BlueBinary
		Try
			Local opc:BlueOpcode = New BlueOpcode
			Local corrupt:BlueAssemblyError = BlueAssemblyError.Make(filename, "not a valid Blue Moon binary file (possibly corrupt)")
			Local badInst:BlueError = corrupt.Make(filename, "invalid Blue Moon instruction (out of bounds error); could not load file")
			sz :- (8 + 8) ; If sz < 12 Or (sz Mod 4 <> 0) Then Throw corrupt

			Local magic:Long = file.ReadLong() ; If magic <> MAGICNUMBER Then Throw corrupt
			Local major:Int = file.ReadInt()
			If major <> BlueMoonVersion.MAJORVERSION Then Throw corrupt.Make(filename, "incompatible major version of interpreter for bytecode")
			Local minor:Int = file.ReadInt()
			If minor > BlueMoonVersion.MINORVERSION Then Throw corrupt.Make(filename, "incompatible version of interpreter for bytecode")

			Local fc:Int = file.ReadInt(), kc:Int = file.ReadInt(), koffs:Int = file.ReadInt(), headerSz:Int = 3 + fc
			sz :/ 4 ; If sz < headerSz + fc * 7 Or sz < koffs + kc * 3 Then Throw corrupt

			Local bufSz:Int = headerSz + (koffs - headerSz) * 2 + (sz - koffs), buf:Int[bufSz]
			buf[0] = fc ; buf[1] = kc ; buf[2] = koffs

			For Local fi:Int = 0 Until fc
				Local foff:Int = file.ReadInt() ; If sz < foff Then Throw corrupt
				buf[3 + fi] = headerSz + (foff - headerSz) * 2
			Next

			Local pos:Int = headerSz, sizeExtension:Int = 0
			For Local fi:Int = 0 Until fc
			'binSz :+ (7 ..	'id, instruction count, k count, param count, var count, upvar count, frame size
			 '      + 2 * f.upvars ..	'upvar levels and stack positions (interleaved)
			  '     + f.kc ..	'constant index table
			   '    + f.inst.Count())	'instructions
				Local id:Int = file.ReadInt(), icount:Int = file.ReadInt(), kcount:Int = file.ReadInt(), pcount:Int = file.ReadInt()
				Local vars:Int = file.ReadInt(), upvars:Int = file.ReadInt(), framesz:Int = file.ReadInt()
				If pos <> buf[3 + fi] Or id <> fi Or (icount|kcount|pcount|vars|upvars|framesz) < 0 Then Throw corrupt
				buf[pos] = id ; buf[pos + 1] = icount ; buf[pos + 2] = kcount ; buf[pos + 3] = pcount
				buf[pos + 4] = vars ; buf[pos + 5] = upvars ; buf[pos + 6] = framesz ; pos :+ 7
				If (pos - sizeExtension) + 2 * upvars + kcount + icount > sz Then Throw corrupt
				sizeExtension :+ (7 + 2 * upvars + kcount + icount) * 2

				For Local up:Int = 0 Until upvars
					buf[pos] = file.ReadInt() ; buf[pos + 1] = file.ReadInt() ; pos :+ 2
				Next
				For Local ki:Int = 0 Until kcount
					buf[pos] = file.ReadInt()
					If buf[pos] >= kc Then Throw corrupt
					pos :+ 1
				Next

				For Local i:Int = 0 Until icount
					Local in:Int = file.ReadInt(), op:Int, ra:Int, rb:Int, c:Int
					BlueOpcode.Read(in, op, ra, rb, c)

					Select op		'verify operation as not going out of bounds!
						Case opc.RETVA	'ignore
						Case opc.JMP, opc.JNOT, opc.JIF
							If i + c >= icount Or i + c < 0 Then Throw badinst
						Case opc.CALL, opc.TCALL
							If Max(ra, rb + c) >= framesz Or Min(ra, Min(rb, c)) < 0 Then Throw badInst
						Case opc.RET, opc.POSTCALL
							If ra + c >= framesz Or ra < 0 Or c < 0 Then Throw badInst
						Case opc.GETUPV
							If ra >= framesz Or ra < 0 Or rb >= upvars Or rb < 0 Then Throw badInst
						Case opc.SETUPV
							If rb >= framesz Or rb < 0 Or ra >= upvars Or ra < 0 Then Throw badInst
						Default
							If ra >= framesz Or rb >= framesz Or ra < 0 Or rb < 0 Then Throw badInst
							If (op >= opc.ADD And op <= opc.BSHR) Or (op >= opc.EQ And op <= opc.LEQ)
								If c >= framesz Or c < 0 Then Throw badInst
							Else
								Select op
									Case opc.GETTAB, opc.SETTAB, opc.LOADK
										If c >= kcount Or c < 0 Then Throw badInst
									Case opc.VARARG', opc.VAINIT, opc.CALLINIT

									Case opc.CLOSURE
										If c >= fc  Or c < 0 Then Throw badInst
								End Select
							EndIf
					End Select

					buf[pos] = op | (ra Shl 8) | (rb Shl 16) ; buf[pos + 1] = c
					pos :+ 2
				Next

				pos :+ (7 + 2 * upvars + kcount)	'unused space because of buffer doubling
			Next

			sizeExtension :/ 2
			If pos - sizeExtension <> koffs Then Throw corrupt
			buf[2] = pos	'update koffs
			For Local ki:Int = 0 To kc	'[sic] - we need to copy the end marker too
				Local koff:Int = file.ReadInt() ; If sz < koff Or koff < koffs + kc + 1 Then Throw corrupt
				buf[pos] = koff + sizeExtension ; pos :+ 1
			Next
			For Local ki:Int = 0 Until kc
				Local d:Double = file.ReadDouble(), dp:Int Ptr = Int Ptr(Varptr(d))
				buf[pos] = dp[0] ; buf[pos + 1] = dp[1] ; pos :+ 2
				If pos <> buf[koffs + sizeExtension + ki + 1]	'string
					For Local c:Int = 0 Until Ceil(d / 2)
						buf[pos + c] = file.ReadInt()
					Next
					pos :+ Ceil(d / 2)
				EndIf
			Next

			b = New Self ; b.buf = buf
		Catch o:Object
			CloseStream(file) ; Throw o
		End Try
		CloseStream(file)
		Return b
	End Function

	Method Write(file:TStream)
		Try
			file.WriteLong MAGICNUMBER
			file.WriteInt BlueMoonVersion.MAJORVERSION ; file.WriteInt BlueMoonVersion.MINORVERSION
			For Local i:Int = 0 Until out.Length
				file.WriteInt out[i]
			Next
		Catch o:Object
			CloseStream(file) ; Throw o
		End Try
		CloseStream(file)
	End Method
End Type

Type BlueAssembly
	Const MAXVARS:Int = 255, SI_LIMIT:Int = 131071, VSI_LIMIT:Int = 511
	Field fd:FunDef, id:Int
	Field vsp:Int
	Field ktbl:TList, kc:Int, kidx:Int[]
	Field inst:TList
	Field lbl:LabelDef[], gto:GotoDef[]
	Field params:Int, vars:Int, upvars:Int, framesz:Int, upvarTier:Int[], upvarStkPos:Int[]

	Function Make:BlueAssembly(fd:FunDef)
		Local f:BlueAssembly = New BlueAssembly
		f.fd = fd ; f.id = fd.id
		f.params = fd.pcount ; f.vars = fd.vmax ; f.upvars = fd.clos.Length
		f.vsp = f.vars '+ f.upvars
		f.upvarTier = New Int[f.upvars] ; f.upvarStkPos = New Int[f.upvars]
		For Local v:Int = 0 Until f.upvars
			Local el:Int = 1, env:FunDef = fd.env
			If env
				f.upvarStkPos[v] = -1
				For Local l:Int = 0 Until env.vars.Length
					If env.vars[l] = fd.clos[v] Then f.upvarStkPos[v] = l ; Exit
				Next
				If f.upvarStkPos[v] = -1
					For Local c:Int = 0 Until env.clos.Length
						If env.clos[c] = fd.clos[v]
							f.upvarTier[v] = 1 ; f.upvarStkPos[v] = c ; Exit
						EndIf
					Next
					Assert f.upvarStkPos[v] <> -1, "compile error: unable to resolve closure (critical compiler error)"
				EndIf
			Else
				f.upvarTier[v] = -1 ; f.upvarStkPos[v] = 0
			EndIf
		Next
		f.ktbl = CreateList() ; f.kc = 0
		Return f
	End Function

	Method Push:Int(n:Int = 1)
		vsp :+ n ; Return vsp - 1
	End Method
	Method Pop:Int(n:Int = 1)
		vsp :- n ; Return vsp
	End Method
	Method Peek:Int()
		Return vsp - 1
	End Method

	Method GetConst:Int(t:String, val:String)
		Return BlueBinary.AddKTblEntry(t + ":" + val, ktbl, kc)
	End Method

	Method ToString:String()
		Local s:String = "F: " + id + "~n"
		s :+ "  Constants:~n"
		For Local k:String = EachIn ktbl
			s :+ "    " + k + "~n"
		Next
		s :+ "  Upvars:~n"
		For Local v:Int = 0 Until upvars
			s :+ "    " + upvarTier[v] + "/" + upvarStkPos[v] + "~n"
		Next
		s :+ "  Instructions:~n"
		For Local i:BlueInstr = EachIn inst
			s :+ "    " + i.ToString() + "~n"
		Next
		Return s
	End Method

	Method Finish(_inst:TList, opc:BlueOpcode)
		inst = _inst
		lbl = LabelDef[](fd._lt.ToArray())
		gto = GotoDef[](fd._gt.ToArray())

		' allow closures over parameters
		For Local p:Int = params - 1 To 0 Step -1
			If fd.vars[p].isClosure Then inst.AddFirst(BlueInstr.Make(opc.NEWUPV, fd.vars[p].stkpos, 0))
		Next

		' add final return so that jumps don't run off the end
		inst.AddLast BlueInstr.Make(opc.RET, 0, 0, 0)
		' remove redundant MOV instructions
		Self.ReduceMovs(opc)
		' resolve targeted jumps and remove dummy target instructions
		Self.ResolveJumps(opc)
		' resolve break jumps and remove dummy target instructions
		Self.ResolveBreaks(opc)

		framesz = vars'upvars + vars
		For Local i:BlueInstr = EachIn inst
			Select i.op
				Case opc.JMP, opc.VARARG, opc.VAINIT, opc.CALLINIT, opc.RETVA	'ignore
				Case opc.CALL, opc.TCALL
					framesz = Max(framesz, Max(i.ra, i.rb + i.c) + 1)
				Case opc.RET, opc.POSTCALL
					framesz = Max(framesz, i.ra + i.c + 1)
				Default
					framesz = Max(framesz, i.ra + 1)
			End Select
		Next

		kidx = New Int[kc]
		fd = Null
	End Method

	Method ResolveJumps(opc:BlueOpcode)
		Local il:TLink = inst.FirstLink(), i:Int = 0
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV And in.ra = 0 And in.rb = 0
				Local lab:LabelDef = lbl[in.c]
				lab.insl = il ; lab.insc = i
			ElseIf Not (in.op = opc.MOV And in.ra = in.rb)
				i :+ 1
			EndIf
			il = il.NextLink()
		Wend
		i = 0 ; il = inst.FirstLink()
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If (in.op >= opc.JMP And in.op <= opc.JNOT) And in.c >= 0
				Local jmp:GotoDef = gto[in.c]
				in.c = jmp.tgt.insc - i ; jmp.tgt.insl.Remove()
			EndIf
			If Not (in.op = opc.MOV And in.ra = in.rb) Then i :+ 1
			il = il.NextLink()
		Wend
	End Method

	Method ResolveBreaks(opc:BlueOpcode)
		Local il:TLink = inst.FirstLink()
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.JMP And in.ra = -1
				Local tgl:TLink = il.NextLink(), cnt:Int = 1
				While True
					Local tgt:BlueInstr = BlueInstr(tgl.Value())
					If tgt.op = opc.MOV And tgt.c = -1
						in.ra = 0 ; in.rb = 0 ; in.c = cnt ; Exit
					EndIf
					tgl = tgl.NextLink() ; cnt :+ 1
				Wend
			EndIf
			il = il.NextLink()
		Wend
		il = inst.FirstLink()	'remove break point markers
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV And in.c = -1 Then il.Remove()
			il = il.NextLink()
		Wend
	End Method

	Method ReduceMovs(opc:BlueOpcode)
		Function replaceMov1:TLink(ml:TLink Var, in:BlueInstr, ra:Int, rb:Int, c:Int)	'update a later instruction and then go back to look for more MOVs
			Local ret:TLink = ml.PrevLink()	'won't be null
			ml.Remove() ; ml = Null
			in.ra = ra ; in.rb = rb ; in.c = c
			Return ret
		End Function

		'first MOVs from variables to temps
		Local ml:TLink, mov:BlueInstr, il:TLink = inst.FirstLink(), vmax:Int = fd.vmax, tmpStk:Int = vmax' + fd.clos.Length
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV	'if it's a move to temp space and nothing is pending
				If in.ra = in.rb	'jump target; do not cross
					ml = Null
				ElseIf ml = Null And in.ra >= tmpStk And in.rb < vmax
					ml = il ; mov = BlueInstr(ml.Value())
				ElseIf ml <> Null And in.rb = mov.ra And in.ra <> mov.rb
					il = replaceMov1(ml, in, in.ra, mov.rb, in.c)
				EndIf
			ElseIf ml <> Null
				If in.op >= opc.JMP	'any control-flow related op cancels
					If (in.op = opc.JIF Or in.op = opc.JNOT) And mov.ra = in.ra Then il = replaceMov1(ml, in, mov.rb, in.rb, in.c) Else il = ml
					ml = Null
				ElseIf in.op = opc.GETTAB Or in.op = opc.GETTABSI Or (in.op >= opc.UNM And in.op <= opc.UNP)
					If mov.ra = in.rb Then il = replaceMov1(ml, in, in.ra, mov.rb, in.c)
				ElseIf in.op = opc.SETTAB Or in.op = opc.SETTABSI
					If mov.ra = in.ra Then il = replaceMov1(ml, in, mov.rb, in.rb, in.c)
				ElseIf in.op = opc.GETTABI Or (in.op >= opc.ADD And in.op < opc.UNM) Or (in.op >= opc.EQ And in.op <= opc.LEQ)
					If mov.ra = in.rb
						il = replaceMov1(ml, in, in.ra, mov.rb, in.c)
					ElseIf mov.ra = in.c
						il = replaceMov1(ml, in, in.ra, in.rb, mov.rb)
					EndIf
				ElseIf in.op = opc.SETTABI
					If mov.ra = in.ra
						il = replaceMov1(ml, in, mov.rb, in.rb, in.c)
					ElseIf mov.ra = in.c
						il = replaceMov1(ml, in, in.ra, in.rb, mov.rb)
					EndIf
				EndIf
			EndIf
			If il = Null Then il = inst.FirstLink()
			il = il.NextLink()
		Wend
		ml = Null ; il = inst.LastLink()	'then MOVs from temps to variables
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV
				If in.ra = in.rb
					ml = Null
				ElseIf ml = Null And in.ra < vmax And in.rb >= tmpStk
					ml = il ; mov = BlueInstr(ml.Value())
				EndIf
			ElseIf ml <> Null
				If in.op >= opc.JMP
					ml = Null
				ElseIf in.op = opc.GETTAB Or in.op = opc.GETTABI Or in.op = opc.GETTABSI Or in.op = opc.CLOSURE Or in.op = opc.NEWTAB Or ..
						(in.op >= opc.ADD And in.op <= opc.LEQ) Or (in.op >= opc.LOADK And in.op <= opc.LOADNIL)
					If mov.rb = in.ra
						il = ml.NextLink()	'won't be null
						ml.Remove() ; ml = Null ; in.ra = mov.ra
					EndIf
				EndIf
			EndIf
			il = il.PrevLink()
		Wend
	End Method
End Type

' Blue Moon
' Bytecode compiler: accepts program fragments as strings, or program files (and outputs)

SuperStrict

Import "blueparser.bmx"
Import "blueerror.bmx"
Import "blueversion.bmx"
Private
Include "TFold.bmx"
Include "bluefoldsyntax.bmx"
Include "bluefoldsemantics.bmx"
Include "bluefoldcodegen.bmx"
Include "debugfold.bmx"
Public

Type BlueCompiler
	Function CompileFile(infile:String, outfile:String = "")
		If outfile = "" Then outfile = infile + ".so"
		Local n:TParseNode = BlueParseFile(infile)
		Local compiled:BlueBinary = BlueBinary.Make(infile, ASTToAssembly(n))
		compiled.WriteObjectFile(outFile)
	End Function

	Function CompileFileForLoad:BlueBinary(infile:String)
		Local n:TParseNode = BlueParseFile(infile)
		Local code:BlueBinary = BlueBinary.Make(infile, ASTToAssembly(n))
		Local sz:Int, mem:Byte Ptr = code.WriteMemory(sz)
		code = code.ReadMemory(mem, sz) ; MemFree(mem)
		Return code
	End Function

'	Function CompileString:BlueBinary(program:String)
'		Local n:TParseNode = BlueParseString(program)
'		Return BlueBinary.Make("<string>", ASTToAssembly(n))
'	End Function

	Function ShowBytecode:String(infile:String)
		Local n:TParseNode = BlueParseFile(infile), out:String = infile + ":~n"
		Local assemblies:TList = ASTToAssembly(n)
		For Local f:BlueAssembly = EachIn assemblies
			out :+ f.ToString()
		Next
		Return out
	End Function

	Function ASTToAssembly:TList(n:TParseNode)
		Local tbl:NodeTable = Node.FromParse(n), r:Node = tbl.n

		r = Node.Fold(r, NormalizeFold.Make(tbl))

		Local errf:SyntaxErrFold = SyntaxErrFold.Make(tbl)
		r = Node.Fold(r, errf) ; errf.BreakCheck()

		r = Node.Fold(r, New ConstantFold)

		Local semf:SemanticFold = SemanticFold.Make(r, tbl)
		r = Node.Fold(r, semf, Node.BOTH)	'fold both ways to push and pop environments
		SemanticFold.Close(semf, r)

		Local funs:FunDef[] = FunDef[](semf.funs.ToArray()) ; semf = Null

		' if we wanted a type checker, it would go here
		tbl = Null	'source information no longer needed

		Local assemblies:TList = CreateList()
		For Local f:FunDef = EachIn funs
			Local com:CompileFold = CompileFold.Make(BlueAssembly.Make(f))
		'	Print "F #" + f.id + ": " + f.n.ToString()
			Local inst:TList = BytecodeNode(Node.Fold(f.n, com)).inst
			com.f.Finish(inst, com.opc)
			assemblies.AddLast(com.f)
		Next

		?Debug
		Debug_KeyListFold.Run(funs, CompileFold.Make(Null))
		?
		Return assemblies
	End Function
End Type

Type BlueOpcode Final
	Field MOV:Int = 0, GETLC:Int = 1, SETLC:Int = 2		'these are constants, ignore their definition as fields
	Field LOADK:Int = 3, LOADSI:Int = 4, LOADBOOL:Int = 5, LOADNIL:Int = 6

	Field GETTAB:Int = 7, GETTABSI:Int = 8, SETTAB:Int = 9, SETTABSI:Int = 10
	Field GETTABI:Int = 11, SETTABI:Int = 12, GETUPV:Int = 13, SETUPV:Int = 14
	Field NEWTAB:Int = 15, CLOSURE:Int = 16, NEWUPV:Int = 17

	Field ADD:Int = 18, SUB:Int = 19, MUL:Int = 20, DIV:Int = 21, NMOD:Int = 22, POW:Int = 23, CAT:Int = 24
	Field IDIV:Int = 25, BAND:Int = 26, BOR:Int = 27, BXOR:Int = 28, BSHL:Int = 29, BSHR:Int = 30
	Field UNM:Int = 31, LNOT:Int = 32, ALEN:Int = 33, BNOT:Int = 34, UNP:Int = 35
	Field EQ:Int = 36, LT:Int = 37, LEQ:Int = 38

	Field JMP:Int = 39, JIF:Int = 40, JNOT:Int = 41
	Field CALL:Int = 42, TCALL:Int = 43, RET:Int = 44, RETVA:Int = 45
	Field POSTCALL:Int = 46, VARARG:Int = 47, VAINIT:Int = 48, CALLINIT:Int = 49

	Function Make:Int(i:BlueInstr)
		Global opc:BlueOpcode = New Self
		Local c:Int = i.c
		Select i.op
			Case opc.LOADK, opc.LOADSI, opc.LOADBOOL, opc.LOADNIL, opc.JMP, opc.JIF, opc.JNOT, opc.RET
				If c < 0 Then c :& (Int(2^18) - 1)
				Return i.op | ((i.ra & $FF) Shl 6) | (c Shl 14)
			Default
				If c < 0 Then c :& (Int(2^10) - 1)
				Return i.op | ((i.ra & $FF) Shl 6) | ((i.rb & $FF) Shl 14) | (c Shl 22)
		End Select
	End Function
	Function Read(i:Int, op:Int Var, ra:Int Var, rb:Int Var, c:Int Var)
		Global opc:BlueOpcode = New Self
		op = i & %111111 ; ra = (i Shr 6) & $FF
		Select op
			Case opc.LOADK, opc.LOADSI, opc.LOADBOOL, opc.LOADNIL, opc.JMP, opc.JIF, opc.JNOT, opc.RET
				rb = 0 ; c = i Sar 14	'sign-extend (already MSB)
			Default
				rb = (i Shr 14) & $FF ; c = i Sar 22
		End Select
	End Function
End Type

Type BlueInstr
	Field op:Int, ra:Int, rb:Int, c:Int
	Function Make:BlueInstr(op:Int, ra:Int, rb:Int, c:Int = 0)
		Local i:BlueInstr = New BlueInstr ; i.op = op ; i.ra = ra ; i.rb = rb ; i.c = c ; Return i
	End Function
	Function MakeK:BlueInstr(op:Int, ra:Int, c:Int)
		Return Make(op, ra, 0, c)
	End Function
	Method ToString:String()
		Global opc:BlueOpcode = New BlueOpcode
		Local n:String = "??"
		For Local f:TField = EachIn TTypeId.ForObject(opc).Fields()
			If f.GetInt(opc) = op Then n = f.Name()[..10] ; Exit
		Next
		Select op
			Case opc.LOADNIL, opc.NEWUPV, opc.NEWTAB
				Return n + " " + ra
			Case opc.MOV, opc.SETLC, opc.GETLC, opc.SETUPV, opc.GETUPV
				Return n + " " + ra + ", " + rb
			Case opc.LOADK, opc.LOADSI, opc.LOADBOOL, opc.CLOSURE, opc.RET, opc.POSTCALL, opc.VARARG
				Return n + " " + ra + ", " + c
			Default ; Return n + " " + ra + ", " + rb + ", " + c
		End Select
	End Method
End Type

Type BlueBinary
	Const MAGICNUMBER:Long = 5642816051412814914:Long	'BLUEMOON
	Field out:Int[], buf:Int[]	'out = output bytestream, buf = input/readable bytestream

	Function Make:BlueBinary(file:String, funs:TList)
		Local b:BlueBinary = New Self, ktbl:TList = CreateList(), kc:Int = 0

		Local binSz:Int = 0, fcount:Int = 0
		For Local f:BlueAssembly = EachIn funs
			Local ki:Int = 0
			For Local k:String = EachIn f.ktbl
				f.kidx[ki] = AddKTblEntry(k, ktbl, kc) ; ki :+ 1	'consolidate constants into single table
			Next

			If f.framesz > f.MAXVARS - 3 Or f.upvars > f.MAXVARS Then Throw ..
				BlueAssemblyError.Make(file, "too many variables/temporaries in function #" + f.id ..
				+ "; consider splitting into more blocks or helper functions")

			binSz :+ (7 ..	'id, instruction count, k count, param count, var count, upvar count, frame size
			       + 2 * f.upvars ..	'upvar levels and stack positions (interleaved)
			       + f.kc ..	'constant index table
			       + f.inst.Count())	'instructions
			fcount :+ 1
		Next

		binSz :+ (kc + 1)
		For Local k:String = EachIn ktbl
			If k[0] = "0"[0] Then binSz :+ 2 Else binSz :+ Ceil((k.Length - 2) / 2.0) + 2
		Next

		Local headerSz:Int = 3 + fcount	'function count, k count, k table offset, function offset list
		binSz :+ headerSz

		Local out:Int[] = New Int[binSz] ; b.out = out
		out[0] = fcount
		out[1] = kc

		Local pos:Int = headerSz, opc:BlueOpcode = New BlueOpcode
		For Local f:BlueAssembly = EachIn funs
			out[3 + f.id] = pos
			out[pos] = f.id ; out[pos + 1] = f.inst.Count() ; out[pos + 2] = f.kc ; out[pos + 3] = f.params
			out[pos + 4] = f.vars ; out[pos + 5] = f.upvars ; out[pos + 6] = f.framesz
			pos :+ 7
			For Local v:Int = 0 Until f.upvars
				out[pos] = f.upvarTier[v] ; out[pos + 1] = f.upvarStkPos[v] ; pos :+ 2
			Next
			For Local k:Int = 0 Until f.kc
				out[pos] = f.kidx[k] ; pos :+ 1
			Next
			For Local i:BlueInstr = EachIn f.inst
				out[pos] = BlueOpcode.Make(i) ; pos :+ 1
			Next
		Next

		out[2] = pos ; Local ktblpos:Int = pos ; pos :+ (kc + 1)
		For Local k:String = EachIn ktbl
			Local d:Double, dp:Byte Ptr = Varptr(d)
			out[ktblpos] = pos ; ktblpos :+ 1
			If k[0] = "0"[0]
				d = Double(k[2..])
				out[pos] = Int Ptr(dp)[0] ; out[pos + 1] = Int Ptr(dp)[1]
				pos :+ 2
			Else
				d = k.Length - 2
				out[pos] = Int Ptr(dp)[0] ; out[pos + 1] = Int Ptr(dp)[1] ; pos :+ 2
				If k.Length Mod 2 = 1 Then k :+ " "	'even the length for ease of writing
				For Local c:Int = 2 Until k.Length Step 2
					out[pos] = Short(k[c]) | (Short(k[c + 1]) Shl 16) ; pos :+ 1
				Next
			EndIf
		Next
		out[ktblpos] = pos
		Assert pos = binSz, "assembly write error: binary size mismatch (critical compiler error)"

		Return b
	End Function

	Function AddKTblEntry:Int(k:String, tbl:TList, kc:Int Var)
		Local ki:Int = 0
		For Local ek:String = EachIn tbl
			If ek = k Then Return ki
			ki :+ 1
		Next
		tbl.AddLast(k) ; kc :+ 1'ktbl.Count()
		Return ki
	End Function

	Method WriteMemory:Byte Ptr(size:Int Var)
		size = out.Length * 8 + 16
		Local mem:Byte Ptr = MemAlloc(size)
		Assert mem, "failed to allocate memory for instruction buffer"
		Write(CreateRamStream(mem, out.Length * 8 + 16, 0, 1))
		Return mem
	End Method

	Method WriteObjectFile(filename:String)
		Write(WriteFile(filename))
	End Method

	Function ReadMemory:BlueBinary(mem:Byte Ptr, size:Int)
		Return Read(CreateRamStream(mem, size, 1, 0), "<ram stream>", size)
	End Function

	Function ReadObjectFile:BlueBinary(filename:String)
		Return Read(ReadFile(filename), filename, FileSize(filename))
	End Function

	Function Read:BlueBinary(file:TStream, filename:String, sz:Int)
		Local b:BlueBinary
		Try
			Local opc:BlueOpcode = New BlueOpcode
			Local corrupt:BlueAssemblyError = BlueAssemblyError.Make(filename, "not a valid Blue Moon binary file (possibly corrupt)")
			Local badInst:BlueError = corrupt.Make(filename, "invalid Blue Moon instruction (out of bounds error); could not load file")
			sz :- (8 + 8) ; If sz < 12 Or (sz Mod 4 <> 0) Then Throw corrupt

			Local magic:Long = file.ReadLong() ; If magic <> MAGICNUMBER Then Throw corrupt
			Local major:Int = file.ReadInt()
			If major <> BlueMoonVersion.MAJORVERSION Then Throw corrupt.Make(filename, "incompatible major version of interpreter for bytecode")
			Local minor:Int = file.ReadInt()
			If minor > BlueMoonVersion.MINORVERSION Then Throw corrupt.Make(filename, "incompatible version of interpreter for bytecode")

			Local fc:Int = file.ReadInt(), kc:Int = file.ReadInt(), koffs:Int = file.ReadInt(), headerSz:Int = 3 + fc
			sz :/ 4 ; If sz < headerSz + fc * 7 Or sz < koffs + kc * 3 Then Throw corrupt

			Local bufSz:Int = headerSz + (koffs - headerSz) * 2 + (sz - koffs), buf:Int[bufSz]
			buf[0] = fc ; buf[1] = kc ; buf[2] = koffs

			For Local fi:Int = 0 Until fc
				Local foff:Int = file.ReadInt() ; If sz < foff Then Throw corrupt
				buf[3 + fi] = headerSz + (foff - headerSz) * 2
			Next

			Local pos:Int = headerSz, sizeExtension:Int = 0
			For Local fi:Int = 0 Until fc
			'binSz :+ (7 ..	'id, instruction count, k count, param count, var count, upvar count, frame size
			 '      + 2 * f.upvars ..	'upvar levels and stack positions (interleaved)
			  '     + f.kc ..	'constant index table
			   '    + f.inst.Count())	'instructions
				Local id:Int = file.ReadInt(), icount:Int = file.ReadInt(), kcount:Int = file.ReadInt(), pcount:Int = file.ReadInt()
				Local vars:Int = file.ReadInt(), upvars:Int = file.ReadInt(), framesz:Int = file.ReadInt()
				If pos <> buf[3 + fi] Or id <> fi Or (icount|kcount|pcount|vars|upvars|framesz) < 0 Then Throw corrupt
				buf[pos] = id ; buf[pos + 1] = icount ; buf[pos + 2] = kcount ; buf[pos + 3] = pcount
				buf[pos + 4] = vars ; buf[pos + 5] = upvars ; buf[pos + 6] = framesz ; pos :+ 7
				If (pos - sizeExtension) + 2 * upvars + kcount + icount > sz Then Throw corrupt
				sizeExtension :+ (7 + 2 * upvars + kcount + icount) * 2

				For Local up:Int = 0 Until upvars
					buf[pos] = file.ReadInt() ; buf[pos + 1] = file.ReadInt() ; pos :+ 2
				Next
				For Local ki:Int = 0 Until kcount
					buf[pos] = file.ReadInt()
					If buf[pos] >= kc Then Throw corrupt
					pos :+ 1
				Next

				For Local i:Int = 0 Until icount
					Local in:Int = file.ReadInt(), op:Int, ra:Int, rb:Int, c:Int
					BlueOpcode.Read(in, op, ra, rb, c)

					Select op		'verify operation as not going out of bounds!
						Case opc.RETVA	'ignore
						Case opc.JMP, opc.JNOT, opc.JIF
							If i + c >= icount Or i + c < 0 Then Throw badinst
						Case opc.CALL, opc.TCALL
							If Max(ra, rb + c) >= framesz Or Min(ra, Min(rb, c)) < 0 Then Throw badInst
						Case opc.RET, opc.POSTCALL
							If ra + c >= framesz Or ra < 0 Or c < 0 Then Throw badInst
						Case opc.GETUPV
							If ra >= framesz Or ra < 0 Or rb >= upvars Or rb < 0 Then Throw badInst
						Case opc.SETUPV
							If rb >= framesz Or rb < 0 Or ra >= upvars Or ra < 0 Then Throw badInst
						Default
							If ra >= framesz Or rb >= framesz Or ra < 0 Or rb < 0 Then Throw badInst
							If (op >= opc.ADD And op <= opc.BSHR) Or (op >= opc.EQ And op <= opc.LEQ)
								If c >= framesz Or c < 0 Then Throw badInst
							Else
								Select op
									Case opc.GETTAB, opc.SETTAB, opc.LOADK
										If c >= kcount Or c < 0 Then Throw badInst
									Case opc.VARARG', opc.VAINIT, opc.CALLINIT

									Case opc.CLOSURE
										If c >= fc  Or c < 0 Then Throw badInst
								End Select
							EndIf
					End Select

					buf[pos] = op | (ra Shl 8) | (rb Shl 16) ; buf[pos + 1] = c
					pos :+ 2
				Next

				pos :+ (7 + 2 * upvars + kcount)	'unused space because of buffer doubling
			Next

			sizeExtension :/ 2
			If pos - sizeExtension <> koffs Then Throw corrupt
			buf[2] = pos	'update koffs
			For Local ki:Int = 0 To kc	'[sic] - we need to copy the end marker too
				Local koff:Int = file.ReadInt() ; If sz < koff Or koff < koffs + kc + 1 Then Throw corrupt
				buf[pos] = koff + sizeExtension ; pos :+ 1
			Next
			For Local ki:Int = 0 Until kc
				Local d:Double = file.ReadDouble(), dp:Int Ptr = Int Ptr(Varptr(d))
				buf[pos] = dp[0] ; buf[pos + 1] = dp[1] ; pos :+ 2
				If pos <> buf[koffs + sizeExtension + ki + 1]	'string
					For Local c:Int = 0 Until Ceil(d / 2)
						buf[pos + c] = file.ReadInt()
					Next
					pos :+ Ceil(d / 2)
				EndIf
			Next

			b = New Self ; b.buf = buf
		Catch o:Object
			CloseStream(file) ; Throw o
		End Try
		CloseStream(file)
		Return b
	End Function

	Method Write(file:TStream)
		Try
			file.WriteLong MAGICNUMBER
			file.WriteInt BlueMoonVersion.MAJORVERSION ; file.WriteInt BlueMoonVersion.MINORVERSION
			For Local i:Int = 0 Until out.Length
				file.WriteInt out[i]
			Next
		Catch o:Object
			CloseStream(file) ; Throw o
		End Try
		CloseStream(file)
	End Method
End Type

Type BlueAssembly
	Const MAXVARS:Int = 255, SI_LIMIT:Int = 131071, VSI_LIMIT:Int = 511
	Field fd:FunDef, id:Int
	Field vsp:Int
	Field ktbl:TList, kc:Int, kidx:Int[]
	Field inst:TList
	Field lbl:LabelDef[], gto:GotoDef[]
	Field params:Int, vars:Int, upvars:Int, framesz:Int, upvarTier:Int[], upvarStkPos:Int[]

	Function Make:BlueAssembly(fd:FunDef)
		Local f:BlueAssembly = New BlueAssembly
		f.fd = fd ; f.id = fd.id
		f.params = fd.pcount ; f.vars = fd.vmax ; f.upvars = fd.clos.Length
		f.vsp = f.vars '+ f.upvars
		f.upvarTier = New Int[f.upvars] ; f.upvarStkPos = New Int[f.upvars]
		For Local v:Int = 0 Until f.upvars
			Local el:Int = 1, env:FunDef = fd.env
			If env
				f.upvarStkPos[v] = -1
				For Local l:Int = 0 Until env.vars.Length
					If env.vars[l] = fd.clos[v] Then f.upvarStkPos[v] = l ; Exit
				Next
				If f.upvarStkPos[v] = -1
					For Local c:Int = 0 Until env.clos.Length
						If env.clos[c] = fd.clos[v]
							f.upvarTier[v] = 1 ; f.upvarStkPos[v] = c ; Exit
						EndIf
					Next
					Assert f.upvarStkPos[v] <> -1, "compile error: unable to resolve closure (critical compiler error)"
				EndIf
			Else
				f.upvarTier[v] = -1 ; f.upvarStkPos[v] = 0
			EndIf
		Next
		f.ktbl = CreateList() ; f.kc = 0
		Return f
	End Function

	Method Push:Int(n:Int = 1)
		vsp :+ n ; Return vsp - 1
	End Method
	Method Pop:Int(n:Int = 1)
		vsp :- n ; Return vsp
	End Method
	Method Peek:Int()
		Return vsp - 1
	End Method

	Method GetConst:Int(t:String, val:String)
		Return BlueBinary.AddKTblEntry(t + ":" + val, ktbl, kc)
	End Method

	Method ToString:String()
		Local s:String = "F: " + id + "~n"
		s :+ "  Constants:~n"
		For Local k:String = EachIn ktbl
			s :+ "    " + k + "~n"
		Next
		s :+ "  Upvars:~n"
		For Local v:Int = 0 Until upvars
			s :+ "    " + upvarTier[v] + "/" + upvarStkPos[v] + "~n"
		Next
		s :+ "  Instructions:~n"
		For Local i:BlueInstr = EachIn inst
			s :+ "    " + i.ToString() + "~n"
		Next
		Return s
	End Method

	Method Finish(_inst:TList, opc:BlueOpcode)
		inst = _inst
		lbl = LabelDef[](fd._lt.ToArray())
		gto = GotoDef[](fd._gt.ToArray())

		' allow closures over parameters
		For Local p:Int = params - 1 To 0 Step -1
			If fd.vars[p].isClosure Then inst.AddFirst(BlueInstr.Make(opc.NEWUPV, fd.vars[p].stkpos, 0))
		Next

		' add final return so that jumps don't run off the end
		inst.AddLast BlueInstr.Make(opc.RET, 0, 0, 0)
		' remove redundant MOV instructions
		Self.ReduceMovs(opc)
		' resolve targeted jumps and remove dummy target instructions
		Self.ResolveJumps(opc)
		' resolve break jumps and remove dummy target instructions
		Self.ResolveBreaks(opc)

		framesz = vars'upvars + vars
		For Local i:BlueInstr = EachIn inst
			Select i.op
				Case opc.JMP, opc.VARARG, opc.VAINIT, opc.CALLINIT, opc.RETVA	'ignore
				Case opc.CALL, opc.TCALL
					framesz = Max(framesz, Max(i.ra, i.rb + i.c) + 1)
				Case opc.RET, opc.POSTCALL
					framesz = Max(framesz, i.ra + i.c + 1)
				Default
					framesz = Max(framesz, i.ra + 1)
			End Select
		Next

		kidx = New Int[kc]
		fd = Null
	End Method

	Method ResolveJumps(opc:BlueOpcode)
		Local il:TLink = inst.FirstLink(), i:Int = 0
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV And in.ra = 0 And in.rb = 0
				Local lab:LabelDef = lbl[in.c]
				lab.insl = il ; lab.insc = i
			ElseIf Not (in.op = opc.MOV And in.ra = in.rb)
				i :+ 1
			EndIf
			il = il.NextLink()
		Wend
		i = 0 ; il = inst.FirstLink()
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If (in.op >= opc.JMP And in.op <= opc.JNOT) And in.c >= 0
				Local jmp:GotoDef = gto[in.c]
				in.c = jmp.tgt.insc - i ; jmp.tgt.insl.Remove()
			EndIf
			If Not (in.op = opc.MOV And in.ra = in.rb) Then i :+ 1
			il = il.NextLink()
		Wend
	End Method

	Method ResolveBreaks(opc:BlueOpcode)
		Local il:TLink = inst.FirstLink()
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.JMP And in.ra = -1
				Local tgl:TLink = il.NextLink(), cnt:Int = 1
				While True
					Local tgt:BlueInstr = BlueInstr(tgl.Value())
					If tgt.op = opc.MOV And tgt.c = -1
						in.ra = 0 ; in.rb = 0 ; in.c = cnt ; Exit
					EndIf
					tgl = tgl.NextLink() ; cnt :+ 1
				Wend
			EndIf
			il = il.NextLink()
		Wend
		il = inst.FirstLink()	'remove break point markers
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV And in.c = -1 Then il.Remove()
			il = il.NextLink()
		Wend
	End Method

	Method ReduceMovs(opc:BlueOpcode)
		Function replaceMov1:TLink(ml:TLink Var, in:BlueInstr, ra:Int, rb:Int, c:Int)	'update a later instruction and then go back to look for more MOVs
			Local ret:TLink = ml.PrevLink()	'won't be null
			ml.Remove() ; ml = Null
			in.ra = ra ; in.rb = rb ; in.c = c
			Return ret
		End Function

		'first MOVs from variables to temps
		Local ml:TLink, mov:BlueInstr, il:TLink = inst.FirstLink(), vmax:Int = fd.vmax, tmpStk:Int = vmax' + fd.clos.Length
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV	'if it's a move to temp space and nothing is pending
				If in.ra = in.rb	'jump target; do not cross
					ml = Null
				ElseIf ml = Null And in.ra >= tmpStk And in.rb < vmax
					ml = il ; mov = BlueInstr(ml.Value())
				ElseIf ml <> Null And in.rb = mov.ra And in.ra <> mov.rb
					il = replaceMov1(ml, in, in.ra, mov.rb, in.c)
				EndIf
			ElseIf ml <> Null
				If in.op >= opc.JMP	'any control-flow related op cancels
					If (in.op = opc.JIF Or in.op = opc.JNOT) And mov.ra = in.ra Then il = replaceMov1(ml, in, mov.rb, in.rb, in.c) Else il = ml
					ml = Null
				ElseIf in.op = opc.GETTAB Or in.op = opc.GETTABSI Or (in.op >= opc.UNM And in.op <= opc.UNP)
					If mov.ra = in.rb Then il = replaceMov1(ml, in, in.ra, mov.rb, in.c)
				ElseIf in.op = opc.SETTAB Or in.op = opc.SETTABSI
					If mov.ra = in.ra Then il = replaceMov1(ml, in, mov.rb, in.rb, in.c)
				ElseIf in.op = opc.GETTABI Or (in.op >= opc.ADD And in.op < opc.UNM) Or (in.op >= opc.EQ And in.op <= opc.LEQ)
					If mov.ra = in.rb
						il = replaceMov1(ml, in, in.ra, mov.rb, in.c)
					ElseIf mov.ra = in.c
						il = replaceMov1(ml, in, in.ra, in.rb, mov.rb)
					EndIf
				ElseIf in.op = opc.SETTABI
					If mov.ra = in.ra
						il = replaceMov1(ml, in, mov.rb, in.rb, in.c)
					ElseIf mov.ra = in.c
						il = replaceMov1(ml, in, in.ra, in.rb, mov.rb)
					EndIf
				EndIf
			EndIf
			If il = Null Then il = inst.FirstLink()
			il = il.NextLink()
		Wend
		ml = Null ; il = inst.LastLink()	'then MOVs from temps to variables
		While il
			Local in:BlueInstr = BlueInstr(il.Value())
			If in.op = opc.MOV
				If in.ra = in.rb
					ml = Null
				ElseIf ml = Null And in.ra < vmax And in.rb >= tmpStk
					ml = il ; mov = BlueInstr(ml.Value())
				EndIf
			ElseIf ml <> Null
				If in.op >= opc.JMP
					ml = Null
				ElseIf in.op = opc.GETTAB Or in.op = opc.GETTABI Or in.op = opc.GETTABSI Or in.op = opc.CLOSURE Or ..
						(in.op >= opc.ADD And in.op <= opc.LEQ) Or (in.op >= opc.LOADK And in.op <= opc.LOADNIL)
					If mov.rb = in.ra
						il = ml.NextLink()	'won't be null
						ml.Remove() ; ml = Null ; in.ra = mov.ra
					EndIf
				EndIf
			EndIf
			il = il.PrevLink()
		Wend
	End Method
End Type