#!/usr/bin/tclsh
# Part of MCU 8051 IDE ( http://mcu8051ide.sf.net )

############################################################################
#    Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 by Martin Ošmera     #
#    martin.osmera@gmail.com                                               #
#                                                                          #
#    This program is free software; you can redistribute it and#or modify  #
#    it under the terms of the GNU General Public License as published by  #
#    the Free Software Foundation; either version 2 of the License, or     #
#    (at your option) any later version.                                   #
#                                                                          #
#    This program is distributed in the hope that it will be useful,       #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of        #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
#    GNU General Public License for more details.                          #
#                                                                          #
#    You should have received a copy of the GNU General Public License     #
#    along with this program; if not, write to the                         #
#    Free Software Foundation, Inc.,                                       #
#    59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             #
############################################################################

# >>> File inclusion guard
if { ! [ info exists _DISASSEMBLER_TCL ] } {
set _DISASSEMBLER_TCL _
# <<< File inclusion guard

# --------------------------------------------------------------------------
# Disassembler 8051
#
# DESCRIPTION:
#	Converts data from Intel HEX 8 fromat to assembler 51.
#
# SUMMARY:
#	* Generated code doesn't contain any compiler directives except 'DB', 'ORG' and 'END'.
#	* Unrecognized opcodes are traslated to 'DB opcode'
#	* Labels referencing to "nowhere" are traslated as 'label EQU address'
#	* Input code must be absolutely clear so it mustn't contain any
#	  ambiguity and all addresses must be in icremental order
#	* Lines in hex code which don't start with colon ':' are ignored
#
# USAGE:
#	disassembler::compile $hex_data ;# -> asm code
#
# --------------------------------------------------------------------------

namespace eval disassembler {

	variable tmp_asm	{}	;# Tempotary variable for code procedure: 'final_stage'
	variable hex_data	{}	;# Raw input data
	variable hex		{}	;# Adjusted input data, list: {addr hex0 hex1 hex2 ...}
	variable lineNum	{}	;# Number of line currently beeing parsed
	variable error_count	{}	;# Number of errors raised during disassembly
	variable warning_count	{}	;# Number of warnings occurred
	variable label_idx	{}	;# Label index
	variable final_lbls	0	;# Number of final labels
	variable label			;# Array of tempotary labels, label(int) -> addr
	variable asm		{}	;# Resulting source code


	# ----------------------------------------------------------------
	# GENERAL PURPOSE PROCEDURES
	# ----------------------------------------------------------------

	## Initiate disassembly
	 # @parm string data - Input IHEX8 code
	 # @return string - output asm code or {}
	proc compile {data} {

		variable hex_data	;# Raw input
		variable hex		;# Adjusted input data, list: {addr hex0 hex1 hex2 ...}
		variable lineNum	;# Number of line currently beeing parsed
		variable error_count	;# Number of errors raised during disassembly
		variable warning_count	;# Number of warnings occurred
		variable asm		;# Resulting source code

		set error_count 0	;# reset errors count
		set warning_count 0	;# reset errors count
		set hex {}		;# clear hex data used for futher prosessing
		set hex_data $data	;# set input data

		# Adjust input data
		regexp -all {\r\n?} hex_data "\n" hex_data
		set hex_data [split $hex_data "\n"]

		${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {SN}][mc "Initializing disassembler ..."]

		# Verify input code validity and set variable 'hex'
		adjust_code

		# Exit if the code does not seem to be valid
		if {$error_count != 0} {
			if {${::Compiler::Settings::NOCOLOR}} {
				${::Compiler::Settings::TEXT_OUPUT_COMMAND}	\
					[::Compiler::msgc {EN}][mc "Disassembly FAILED ..."]
			} else {
				${::Compiler::Settings::TEXT_OUPUT_COMMAND}	\
					[::Compiler::msgc {EN}][mc "\033\[31;1mDisassembly FAILED\033\[m ..."]
			}
			return {}
		}

		if {${::Compiler::Settings::ABORT_VARIABLE}} {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {EN}][mc "Aborted"]
			free_resources
			return {}
		}

		# Convert processor code into asm code
		decompile_code

		if {${::Compiler::Settings::ABORT_VARIABLE}} {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {EN}][mc "Aborted"]
			free_resources
			return {}
		}

		# Create labels in resulting code
		parse_labels

		if {${::Compiler::Settings::ABORT_VARIABLE}} {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {EN}][mc "Aborted"]
			free_resources
			return {}
		}

		# Final stage
		final_stage

		if {${::Compiler::Settings::ABORT_VARIABLE}} {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {EN}][mc "Aborted"]
			free_resources
			return {}
		}

		# Free memory used during disassembly
		free_resources

		if {${::Compiler::Settings::ABORT_VARIABLE}} {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {EN}][mc "Aborted"]
			free_resources
			return {}
		}

		if {${::Compiler::Settings::NOCOLOR}} {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND}	\
				[::Compiler::msgc {SN}][mc "Disassembly complete"]
		} else {
			${::Compiler::Settings::TEXT_OUPUT_COMMAND}	\
				[mc "\033\[32;1mDisassembly complete\033\[m"]
		}

		# Return resulting source code
		return $asm
	}


	# ----------------------------------------------------------------
	# INTERNAL AUXILIARY PROCEDURES
	# ----------------------------------------------------------------

	## Verify input code validity and set variable 'hex'
	 # @return void
	proc adjust_code {} {
		variable hex		;# Adjusted input data
		variable lineNum	;# Number of line currently beeing parsed
		variable hex_data	;# Raw input (hex data)

		set pointer -1		;# Program address pointer
		set lineNum 0		;# Line number

		foreach line $hex_data {

			if {[expr {$lineNum % 10}] == 0} {
				${::Compiler::Settings::UPDATE_COMMAND}
			}

			incr lineNum	;# line number

			# Skip comments
			if {[string index $line 0] != {:}} {continue}

			# Check for valid characters
			set line [string range $line 1 end]
			if {![regexp {^[0-9A-Fa-f]*$} $line]} {
				Error $lineNum [mc "Invalid line (line contain not allowed characters)"]
				continue
			}

			# Check for odd number of characters
			set len [string length $line]
			if {[expr {$len % 2}] != 0} {
				Error $lineNum [mc "Line do not contain odd number of chars"]
				continue
			}

			# Check for valid checksum
			set check [string range $line {end-1} end]
			set new_check [::IHexTools::getCheckSum [string range $line 0 {end-2}]]
			if {$check != $new_check} {
				Error $lineNum [mc "Bad checksum, given: %s ; computed: %s" $check $new_check]
				continue
			}

			# Check for correct record type
			set type [string range $line 6 7]
			if {$type == {01}} {
				break
			} elseif {$type != {00}} {
				Error $lineNum [mc "Unknown record type number `%s' (Intel HEX 8 can contain only 00 and 01)" $type]
			}

			# Check valid line length
			set len [string range $line 0 1]
			set len [expr "0x$len"]
			set data [string range $line 8 {end-2}]
			if {$len != ([string length $data] / 2)} {
				Error $lineNum [mc "Length field do not correspond true data length"]
				continue
			}

			# Check for valid incremental addressing without any ambiguity
			set addr_hex [string range $line 2 5]
			set addr [expr "0x$addr_hex"]
			if {$addr <= $pointer} {
				Error $lineNum [mc "Unexpected address -- code is not well formatted"]
				continue
			} elseif {$addr > ($pointer + 1)} {
				set pointer $addr
			} else {
				incr pointer $len
			}

			## Convert line into this form:
			 # {
			 #	{addr hex hex hex ...}
			 #	...
			 # }
			set len [expr {($len * 2) - 1}]
			set line {}
			for {set i 0} {$i <= $len} {incr i} {
				append line { }
				append line [string index $data $i]
				incr i
				append line [string index $data $i]
			}
			lappend hex [string toupper "${addr_hex}${line}"]
		}
		set hex_data {}		;# delete input data
	}

	## Convert processor code into source code
	 # @return void
	proc decompile_code {} {

		variable hex		;# Adjusted input data, list: {{addr hex0 hex1 hex2 ...} ...}
		variable asm		;# Resulting source code
		variable label		;# Array of tempotary labels, label(int) -> addr
		variable label_idx	;# Number of usages of code memory addressing

		set pointer	0	;# reset code memory pointer
		set label_idx	-1	;# set label counter
		set asm		{}	;# reset asm
		set idx		0

		set trailing_data	[list]	;# data remained after parsing last line
		set trailing_data_length 0	;# length od trailing_data

		foreach line $hex {

			if {[expr {$idx % 10}] == 0} {
				${::Compiler::Settings::UPDATE_COMMAND}
			}

   			incr idx

			set addr	[lindex $line 0]	;# address field (hex)
			set line	[lreplace $line 0 0]	;# data fields (hex)
			set addr_dec	[expr "0x$addr"]	;# decimal value of address

			## If requested address overlaping expected address then
			 # adjust pointer and write trailing data by DB directive
			if {$addr_dec > ($pointer + 1 + $trailing_data_length) || ($pointer == 0)} {
				if {$trailing_data_length} {
					# Write trailing data
					foreach opcode $trailing_data {
						lappend asm _$pointer [list {DB} "0${opcode}h" {}]
						incr pointer
					}
					# Reset trailing data
					set trailing_data_length 0
					set trailing_data [list]
				}

				# Adjust pointer
				set pointer $addr_dec
				lappend asm {} {} {} [list {ORG} "[HEX $addr]h" {}]
			}

			# Number of data fields
			set len [llength $line]

			# Append trailing data from last parsing to the current line
			if {$trailing_data_length} {
				# append
				incr len $trailing_data_length
				set line [concat $trailing_data $line]
				# reset
				set trailing_data_length 0
				set trailing_data [list]
			}

			# Translate opcodes to source code
			set instruction_skipped 0
			set remaining_bytes $len
			incr len -1
			for {set idx 0} {$idx <= $len} {incr idx} {

				set opcode [lindex $line $idx]	;# current opcode
				# Search for he given opcode
				if {[lsearch ${::CompilerConsts::defined_OPCODE} $opcode] == -1} {
					# opcode not found -> write opcode directly to source code
					lappend asm "_$pointer" [list {DB} "0${opcode}h" {}]
					set length 1
				} else {
					# opcode found -> resolve it's definition
					set def $::CompilerConsts::Opcode($opcode)

					set instruction	[lindex $def 0]		;# Instruction name
					set opr_types	[lindex $def 1]		;# Oprand types
					set length	[lindex $def 2]		;# Instruction length
					set mask_opr	[lindex $def 3]		;# Opreand mask
					set operands	{}			;# reset operand values

					## If remaining code on this line has insufficient length to
					 # make valid instruction then continue to next line and
					 # append remainder of current line to the next line
					if {$length > $remaining_bytes} {
						set trailing_data_length $remaining_bytes
						set trailing_data [lrange $line $idx end]

						break
					}

					# Resolve operands
					set opr {}
					foreach type $opr_types {

						if {[lsearch ${::CompilerConsts::FixedOperands} [string tolower $type]] != -1} {
							# Fixed operand -> only copy
							set opr $type
						} else {
							# Get operand value
							incr idx

							if {$idx > $len} {
								lappend asm "_$pointer" [{DB} "0${opcode}h" {}]
								set instruction_skipped 1
								set length 1
								incr idx -1
								break
							}
							switch -- $type {
								{imm8}	{	;# Immediate addressing 8 bit
									set opr "#[HEX [lindex $line $idx]]h"
								}
								{imm16}	{	;# Immmediate addressing 16 bit
									set opr "#[HEX [lindex $line $idx]]"
									incr idx

									if {$idx > $len} {
										lappend asm "_$pointer" [list {DB} "0${opcode}h" {}]
										set instruction_skipped 1
										set length 1
										incr idx -2
										break
									}
									append opr "[lindex $line $idx]h"
								}
								{bit}	{	;# Direct addressing bit
									set opr "[HEX [lindex $line $idx]]h"
									set tmp_opr [string range $opr 0 {end-1}]
									set tmp_opr [expr "0x$tmp_opr"]
									foreach item ${::CompilerConsts::MapOfSFRBitArea} {
										if {[expr "0x[lindex $item 1]"] == $tmp_opr} {
											set opr [lindex $item 0]
											break
										}
									}
								}
								{/bit}	{	;# Direct inverted addressing bit
									set opr "/[HEX [lindex $line $idx]]h"
									set tmp_opr [string range $opr 1 {end-1}]
									set tmp_opr [expr "0x$tmp_opr"]
									foreach item ${::CompilerConsts::MapOfSFRBitArea} {
										if {[expr "0x[lindex $item 1]"] == $tmp_opr} {
											set opr [lindex $item 0]
											break
										}
									}
								}
								{data}	{	;# Direct addressing
									set opr "[HEX [lindex $line $idx]]h"
									set tmp_opr [string range $opr 0 {end-1}]
									set tmp_opr [expr "0x$tmp_opr"]
									foreach item ${::CompilerConsts::MapOfSFRArea} {
										if {[expr "0x[lindex $item 1]"] == $tmp_opr} {
											set opr [lindex $item 0]
											break
										}
									}
								}
								{code8}	{	;# Immediate addressing code memory, 8 bit
									incr label_idx
									set opr "lbl${label_idx}-"

									set label($label_idx) [expr "0x[lindex $line $idx]"]

									if {$label($label_idx) > 127} {
										incr label($label_idx) -256
									}

									incr label($label_idx) $pointer
									incr label($label_idx) $length

									if {$label($label_idx) > 0x0FFFF || $label($label_idx) < 0} {
										set label($label_idx) [expr {$label($label_idx) & 0x0FFFF}]
										Warning [mc "Code address overflow, instruction: %s" $instruction]
									} elseif {$label($label_idx) == $pointer} {
										unset label($label_idx)
										incr label_idx -1
										set opr {$}
									}
								}
								{code11} {	;# Immediate addressing code memory, 11 bit
									incr label_idx
									set opr "lbl${label_idx}-"
									set label($label_idx) "$mask_opr[lindex $line $idx]"
									set label($label_idx) [expr "0x$label($label_idx)"]
									set label($label_idx) [expr {($label($label_idx) & 0x007ff) | ($pointer & 0x0f800)}]

									if {$label($label_idx) == $pointer} {
										unset label($label_idx)
										incr label_idx -1
										set opr {$}
									}
								}
								{code16} {	;# Immediate addressing code memory, 16 bit
									incr label_idx
									set opr "lbl${label_idx}-"
									set label($label_idx) [lindex $line $idx]
									incr idx

									if {$idx > $len} {
										lappend asm "_$pointer" [list {DB} "0${opcode}h" {}]
										set length 1
										incr idx -2
										set instruction_skipped 1
										break
									}
									append label($label_idx) [lindex $line $idx]
									set label($label_idx) [expr "0x$label($label_idx)"]

									if {$label($label_idx) == $pointer} {
										unset label($label_idx)
										incr label_idx -1
										set opr {$}
									}
								}
							}
						}
						# Resulting operand value to operand list
						lappend operands $opr
					}

					if {$instruction_skipped} {
						set instruction_skipped 0
						incr pointer $length
						incr remaining_bytes -$length
						continue
					}

					# Swap operands in case if of instruction "MOV data, data"
					if {
						$instruction == {mov}		&&
						[lindex $opr_types 0] == {data}	&&
						[lindex $opr_types 1] == {data}
					} then {
						set operands [list [lindex $operands 1] [lindex $operands 0]]
					}

					# Append line to source code list
					lappend asm "_$pointer" [list $instruction $operands {}]
				}
				# Increment program address pointer
				incr pointer $length
				incr remaining_bytes -$length
			}
		}
	}

	## Create labels in resulting code
	 # Replace tempotary labels references by theirs final forms
	 # and add appropriate labels to lines (label:   mov   A, #56q)
	proc parse_labels {} {

		variable asm		;# Resulting source code
		variable label		;# Array of tempotary labels, label(int) -> addr
		variable label_idx	;# Label index
		variable final_lbls	;# Number of final labels

		set addrs	{}	;# List of set addresses
		set equ_block	{}	;# Reset block of declarations of unresoved labels

		# Replace each tempotary label with final label
		set lbl_idx -1
		for {set i 0} {$i <= $label_idx} {incr i} {
			set idx [lsearch $addrs $label($i)]
			if {$idx != -1} {
				# Reuse an existing label
				regsub -all "lbl${i}-" $asm "label$idx" asm
			} else {
				# Realy a new label
				lappend addrs $label($i)
				incr lbl_idx
				regsub -all "lbl${i}-" $asm "label$lbl_idx" asm
			}
		}
		set final_lbls $lbl_idx

		# Write final labels to the source code
		set i 0
		foreach addr $addrs {
			set idx [lsearch $asm "_$addr"]
			if {$idx == -1} {
				# Not found
				append equ_block "{} {CODE [HEX [format %X $addr]]h label$i} "
			} else {
				# Found
				incr idx
				lset asm [list $idx 2] "label$i"
			}
			incr i
		}

		# Append block of declarations of unresolved labels to resulting source code
		append equ_block $asm
		set asm $equ_block
	}

	## Final stage
	 # list -> plain text
	proc final_stage {} {
		variable asm		;# Resulting code
		variable tmp_asm	;# Tempotary variable only this procedure
		variable final_lbls	;# Number of final labels

		set tmp_asm {}		;# reset tempotary code
		set len [llength $asm]	;# length of source code list
		incr len -1

		# Rewrite the source code
		for {set i 1} {$i <= $len} {incr i 2} {

			if {[expr {$len % 5}] == 0} {
				${::Compiler::Settings::UPDATE_COMMAND}
			}

			set line [lindex $asm $i]	;# Get line

			# Empty line
			if {$line == {}} {
				append tmp_asm "\n"
				continue

			# Not an empty line
			} else {
				set label [lindex $line 2]	;# label
				set instr [lindex $line 0]	;# instruction
				set oprs [lindex $line 1]	;# oprands

				if {$label != {} && $instr != {CODE}} {
					append label {:}
					if {[string length $label] > 7} {
						append tmp_asm "$label\n"
						set label {}
					}
				}
				if {$instr == {CODE}} {
					if {$final_lbls > 999 && [string length $label] < 8} {
						append label "\t"
					}
				}
			}

			# Write line
			append label "\t" $instr "\t" [join $oprs {, }]
			append tmp_asm [string trimright $label] "\n"
		}

		# Append 'END' directive at the end
		append tmp_asm "\n\tEND"
		set asm $tmp_asm
	}

	## Free memory used during processing
	proc free_resources {} {
		variable tmp_asm	;# Tempotary variable for procedure: 'final_stage'
		variable hex_data	;# Raw input data
		variable hex		;# Adjusted input data, list: {addr hex0 hex1 hex2 ...}
		variable lineNum	;# Number of line currently beeing parsed
		variable error_count	;# Number of errors raised during disassembly
		variable label_idx	;# Label index
		variable label		;# Array of tempotary labels, label(int) -> addr
		variable asm		;# Resulting source code

		set tmp_asm	{}
		set hex_data	{}
		set hex		{}
		set lineNum	{}
		set error_count	{}
		set label_idx	{}
		catch {array unset label}
	}

	## Adjust hexadecimal number
	 # note: resulting number starts with digit
	 # @parm String number - input
	 # @return String - result
	proc HEX {number} {

		set number [string trimleft $number 0]
		if {$number == {}} {return 0}

		if {[regexp {^[a-fA-F]} $number]} {
			return "0$number"
		}

		return $number
	}

	## Report warning message
	 # @parm Int LineNumber		- Number of line where it occurred
	 # @parm String ErrorInfo	- Text of the warning
	 # @return void
	proc Warning {ErrorInfo} {
		variable idx		;# Current position in asm list
		variable warning_count	;# Number of warnings occurred

		# Increment warning counter
		incr warning_count

		# Report the warning
		if {${::Compiler::Settings::WARNING_LEVEL} < 2} {
			if {${::Compiler::Settings::NOCOLOR}} {
				${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {WN}][mc "Warning: %s" $ErrorInfo]
			} else {
				${::Compiler::Settings::TEXT_OUPUT_COMMAND} [mc "\033\[33mWarning\033\[m: %s" $ErrorInfo]
			}
		}
	}

	## Error
	 # @parm Int lineNumber	- number of line, where the error occurred
	 # @parm String info	- error string
	proc Error {lineNumber info} {
		variable error_count
		incr error_count
		if {$lineNumber != {}} {
			if {${::Compiler::Settings::NOCOLOR}} {
				set lineNumber [mc " at %s" $lineNumber]
			} else {
				set lineNumber [mc " at \033\[31;1;4m%s\033\[m" $lineNumber]
			}
		}
		if {${::Compiler::Settings::WARNING_LEVEL} < 3} {
			if {${::Compiler::Settings::NOCOLOR}} {
				${::Compiler::Settings::TEXT_OUPUT_COMMAND} [::Compiler::msgc {EL}][mc "Error%s: %s" $lineNumber $info]
			} else {
				${::Compiler::Settings::TEXT_OUPUT_COMMAND} [mc "\033\[31;1mError%s\033\[m: %s" $lineNumber $info]
			}
		}
	}
}

# >>> File inclusion guard
}
# <<< File inclusion guard
