--
-- Uxn core unroller script
--
-- This script updates src/uxn-fast.c when Uxn's opcode set changes, so that
-- updates in the human-readable src/uxn.c core can be easily converted into
-- high-performance code.
--
-- To run, you need Lua or LuaJIT, and just run etc/mkuxn-fast.lua from the top
-- directory of Uxn's git repository:
--
--     lua etc/mkuxn-fast.lua
--
-- This file is written in MoonScript, which is a language that compiles to
-- Lua, the same way as e.g. CoffeeScript compiles to JavaScript. Since
-- installing MoonScript has more dependencies than Lua, the compiled
-- etc/mkuxn-fast.lua is kept in Uxn's repository and will be kept updated as
-- this file changes.
--

replacements =
	op_and16: '{ Uint8 a = pop8(u->src), b = pop8(u->src), c = pop8(u->src), d = pop8(u->src); push8(u->src, d & b); push8(u->src, c & a); }'
	op_ora16: '{ Uint8 a = pop8(u->src), b = pop8(u->src), c = pop8(u->src), d = pop8(u->src); push8(u->src, d | b); push8(u->src, c | a); }'
	op_eor16: '{ Uint8 a = pop8(u->src), b = pop8(u->src), c = pop8(u->src), d = pop8(u->src); push8(u->src, d ^ b); push8(u->src, c ^ a); }'
	op_lit16: '{ push8(u->src, mempeek8(u->ram.dat, u->ram.ptr++)); push8(u->src, mempeek8(u->ram.dat, u->ram.ptr++)); }'
	op_swp16: '{ Uint8 a = pop8(u->src), b = pop8(u->src), c = pop8(u->src), d = pop8(u->src); push8(u->src, b); push8(u->src, a); push8(u->src, d); push8(u->src, c); }'
	op_ovr16: '{ Uint8 a = pop8(u->src), b = pop8(u->src), c = pop8(u->src), d = pop8(u->src); push8(u->src, d); push8(u->src, c); push8(u->src, b); push8(u->src, a); push8(u->src, d); push8(u->src, c); }'
	op_dup16: '{ Uint8 a = pop8(u->src), b = pop8(u->src); push8(u->src, b); push8(u->src, a); push8(u->src, b); push8(u->src, a); }'
	op_rot16: '{ Uint8 a = pop8(u->src), b = pop8(u->src), c = pop8(u->src), d = pop8(u->src), e = pop8(u->src), f = pop8(u->src); push8(u->src, d); push8(u->src, c); push8(u->src, b); push8(u->src, a); push8(u->src, f); push8(u->src, e); }'
	op_sth16: '{ Uint8 a = pop8(u->src), b = pop8(u->src); push8(u->dst, b); push8(u->dst, a); }'

local top, bottom, pushtop

offset = (n, s = '') ->
	if n < 0
		' -%s %d'\format s, -n
	elseif n > 0
		' +%s %d'\format s, n
	elseif s != ''
		' +%s 0'\format s
	else
		''

pop_push = (k, n, s) ->
	switch k
		when 'pop'
			s = s\match '^%((%S+)%)$'
			assert s == 'src'
			switch n
				when '8'
					top[s] -= 1
					if bottom[s] > top[s]
						bottom[s] = top[s]
					'%s.dat[%s.ptr%s]'\format s, s, offset(top[s])
				when '16'
					top[s] -= 2
					if bottom[s] > top[s]
						bottom[s] = top[s]
					'(%s.dat[%s.ptr%s] | (%s.dat[%s.ptr%s] << 8))'\format s, s, offset(top[s] + 1), s, s, offset(top[s])
		when 'push'
			s, v = s\match '^%((%S+), (.*)%)$'
			assert s == 'src' or s == 'dst', s
			switch n
				when '8'
					pushtop[s] += 1
					'%s.dat[%s.ptr%s] = %s'\format s, s, offset(pushtop[s] - 1), v
				when '16'
					if v\match'%+%+' or v\match'%-%-'
						error 'push16 has side effects: ' .. v
					peek, args = v\match '^([md]e[mv]peek)16(%b())$'
					if peek
						args = args\sub 2, -2
						return pop_push('push', '8', '(%s, %s8(%s))'\format s, peek, args) .. ';\n' .. pop_push('push', '8', '(%s, %s8(%s + 1))'\format s, peek, args)
					pushtop[s] += 2
					if v\match ' '
						v = '(' .. v .. ')'
					'%s.dat[%s.ptr%s] = %s >> 8;\n%s.dat[%s.ptr%s] = %s & 0xff'\format s, s, offset(pushtop[s] - 2), v, s, s, offset(pushtop[s] - 1), v
		else
			nil

indented_block = (s) ->
	s = s\gsub('^%{ *', '{\n')\gsub('\n', '\n\t')\gsub('\t%} *$', '}\n')
	s = s\gsub('\n[^\n]+%.error = [^\n]+', '%0\n#ifndef NO_STACK_CHECKS\n\tgoto error;\n#endif')
	s

process = (body) ->
	out_body = body\gsub('^%{ *', '')\gsub(' *%}$', '')\gsub('; ', ';\n')\gsub('%b{} *', indented_block)\gsub '(%a+)(%d+)(%b())', pop_push
	in_ifdef = false
	for k in *{'src', 'dst'}
		if bottom[k] != 0
			if not in_ifdef
				out_body ..= '\n#ifndef NO_STACK_CHECKS'
				in_ifdef = true
			out_body ..= '\nif(__builtin_expect(%s.ptr < %d, 0)) {\n\t%s.error = 1;\n\tgoto error;\n}'\format k, -bottom[k], k
		if pushtop[k] != 0
			if pushtop[k] > 0
				if not in_ifdef
					out_body ..= '\n#ifndef NO_STACK_CHECKS'
					in_ifdef = true
				out_body ..= '\nif(__builtin_expect(%s.ptr > %d, 0)) {\n\t%s.error = 2;\n\tgoto error;\n}'\format k, 255 - pushtop[k], k
			if in_ifdef
				out_body ..= '\n#endif'
				in_ifdef = false
			out_body ..= '\n%s.ptr %s= %d;'\format k, pushtop[k] < 0 and '-' or '+', math.abs pushtop[k]
	if in_ifdef
		out_body ..= '\n#endif'
		in_ifdef = false
	t = {}
	out_body\gsub '[^%w_]([a-f]) = (src%.dat%[[^]]+%])[,;]', (v, k) -> t[k] = v
	out_body = out_body\gsub '(src%.dat%[[^]]+%]) = ([a-f]);\n', (k, v) ->
		if t[k] and t[k] == v
			return ''
		return nil
	out_body

ops = {}

for l in assert io.lines 'src/uxn.c'
	name, body = l\match 'void (op_%S*)%(Uxn %*u%) (%b{})'
	if not name
		continue
	if replacements[name]
		body = replacements[name]
	body = body\gsub 'u%-%>src%-%>', 'src.'
	body = body\gsub 'u%-%>dst%-%>', 'dst.'
	body = body\gsub 'u%-%>src', 'src'
	body = body\gsub 'u%-%>dst', 'dst'
	top = { src: 0, dst: 0 }
	bottom = { src: 0, dst: 0 }
	pushtop = top
	ops[name] = process body
	top = { src: 0, dst: 0 }
	bottom = { src: 0, dst: 0 }
	pushtop = { src: 0, dst: 0 }
	ops['keep_' .. name] = process body

dump = (s, src, dst) ->
	ret = '\t\t\t{\n'
	for l in s\gmatch '[^\n]+'
		if not l\match '^%#'
			ret ..= '\t\t\t\t'
		ret ..= '%s\n'\format l
	ret ..= '\t\t\t}\n\t\t\tbreak;\n'
	(ret\gsub('src', src)\gsub('dst', dst))

i = 0
allops = {}
wanted = false
for l in assert io.lines 'src/uxn.c'
	if l == 'static void (*ops[])(Uxn *u) = {'
		wanted = true
	elseif l == '};'
		wanted = false
	elseif wanted
		l = l\gsub '%/%b**%/', ''
		for op in l\gmatch '[%w_]+'
			if not ops[op]
				error 'missing ' .. op
			allops[i + 0x00 + 1] = { n: { i + 0x00 }, body: dump ops[op], 'u->wst', 'u->rst' }
			allops[i + 0x40 + 1] = { n: { i + 0x40 }, body: dump ops[op], 'u->rst', 'u->wst' }
			allops[i + 0x80 + 1] = { n: { i + 0x80 }, body: dump ops['keep_' .. op], 'u->wst', 'u->rst' }
			allops[i + 0xc0 + 1] = { n: { i + 0xc0 }, body: dump ops['keep_' .. op], 'u->rst', 'u->wst' }
			i += 1

i = 0
wanted = false
for l in assert io.lines 'src/uxnasm.c'
	if l == 'static char ops[][4] = {'
		wanted = true
	elseif l == '};'
		wanted = false
	elseif wanted
		for op in l\gmatch '"(...)"'
			i += 1
			allops[i + 0x00].name = op
			allops[i + 0x20].name = op .. '2'
			allops[i + 0x40].name = op .. 'r'
			allops[i + 0x60].name = op .. '2r'
			allops[i + 0x80].name = op .. 'k'
			allops[i + 0xa0].name = op .. '2k'
			allops[i + 0xc0].name = op .. 'kr'
			allops[i + 0xe0].name = op .. '2kr'

for i = 1, 256
	if not allops[i]
		continue
	for j = i + 1, 256
		if allops[i].body == allops[j].body
			table.insert allops[i].n, (table.remove allops[j].n)
			allops[j].body = nil

with assert io.open 'src/uxn-fast.c', 'w'
	f = assert io.open 'src/uxn.c'
	while true
		l = f\read '*l'
		\write '%s\n'\format l
		if l == '*/'
			break
	\write '\n'
	\write [[
/*
 ^
/!\ THIS FILE IS AUTOMATICALLY GENERATED
---

Its contents can get overwritten with the processed contents of src/uxn.c.
See etc/mkuxn-fast.moon for instructions.

*/
]]
	wanted = true
	while true
		l = f\read '*l'
		if l\match' push' or l\match'[ *]pop' or l\match'devpeek16'
			continue
		if l == '/* Stack */'
			wanted = false
		if wanted
			\write '%s\n'\format l
		if l == '}'
			\write '\n'
			break
	\write [[
/* clang-format on */

#pragma mark - Core

int
uxn_eval(Uxn *u, Uint16 vec)
{
	Uint8 instr;
	if(u->dev[0].dat[0xf]) 
		return 0;
	u->ram.ptr = vec;
	if(u->wst.ptr > 0xf8) u->wst.ptr = 0xf8;
	while((instr = u->ram.dat[u->ram.ptr++])) {
		switch(instr) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-value"
#pragma GCC diagnostic ignored "-Wunused-variable"
]]
	for i = 1, 256
		if not allops[i].body
			continue
		for n in *allops[i].n
			\write '\t\tcase 0x%02x: /* %s */\n'\format n, allops[n + 1].name
		\write '\t\t\t__asm__("evaluxn_%02x_%s:");\n'\format allops[i].n[1], allops[i].name
		\write allops[i].body
	\write [[
#pragma GCC diagnostic pop
		}
	}
	return 1;
#ifndef NO_STACK_CHECKS
error:
	if(u->wst.error)
		return uxn_halt(u, u->wst.error, "Working-stack", instr);
	else
		return uxn_halt(u, u->rst.error, "Return-stack", instr);
#endif
}

int
]]
	wanted = false
	while true
		l = f\read '*l'
		if not l
			break
		if l\match '^uxn_boot'
			wanted = true
		if wanted
			\write '%s\n'\format l
	f\close!
	\close!