From 299984cd4bab323166c6eb8acc050d2d43ee2809 Mon Sep 17 00:00:00 2001 From: Andre Wagner Date: Mon, 4 May 2026 14:05:42 -0500 Subject: [PATCH] . --- lua-temp/pprint.lua | 584 ++++++++++++++++++++++++++++++++++++++++++ lua-temp/tests.lua | 60 +++-- lua-temp/tyche-vm.lua | 87 +++++++ 3 files changed, 710 insertions(+), 21 deletions(-) create mode 100644 lua-temp/pprint.lua diff --git a/lua-temp/pprint.lua b/lua-temp/pprint.lua new file mode 100644 index 0000000..1d8fba1 --- /dev/null +++ b/lua-temp/pprint.lua @@ -0,0 +1,584 @@ +local pprint = { VERSION = '0.1' } + +local depth = 1 + +pprint.defaults = { + -- If set to number N, then limit table recursion to N deep. + depth_limit = false, + -- type display trigger, hide not useful datatypes by default + -- custom types are treated as table + show_nil = true, + show_boolean = true, + show_number = true, + show_string = true, + show_table = true, + show_function = false, + show_thread = false, + show_userdata = false, + -- additional display trigger + show_metatable = false, -- show metatable + show_all = false, -- override other show settings and show everything + use_tostring = false, -- use __tostring to print table if available + filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide + object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache + -- per process, falsy value to disable (might cause infinite loop) + -- format settings + indent_size = 2, -- indent for each nested table level + level_width = 80, -- max width per indent level + wrap_string = true, -- wrap string when it's longer than level_width + wrap_array = false, -- wrap every array elements + string_is_utf8 = true, -- treat string as utf8, and count utf8 char when wrapping, if possible + sort_keys = true, -- sort table keys +} + +local TYPES = { + ['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4, + ['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8 +} + +-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a' +local ESCAPE_MAP = { + ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', + ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\', +} + +-- generic utilities +local tokenize_string = function(s) + local t = {} + for i = 1, #s do + local c = s:sub(i, i) + local b = c:byte() + local e = ESCAPE_MAP[c] + if (b >= 0x20 and b < 0x80) or e then + local s = e or c + t[i] = { char = s, len = #s } + else + t[i] = { char = string.format('\\x%02x', b), len = 4 } + end + if c == '"' then + t.has_double_quote = true + elseif c == "'" then + t.has_single_quote = true + end + end + return t +end +local tokenize_utf8_string = tokenize_string + +local has_lpeg, lpeg = pcall(require, 'lpeg') + +if has_lpeg then + local function utf8_valid_char(c) + return { char = c, len = 1 } + end + + local function utf8_invalid_char(c) + local b = c:byte() + local e = ESCAPE_MAP[c] + if (b >= 0x20 and b < 0x80) or e then + local s = e or c + return { char = s, len = #s } + else + return { char = string.format('\\x%02x', b), len = 4 } + end + end + + local cont = lpeg.R('\x80\xbf') + local utf8_char = + lpeg.R('\x20\x7f') + + lpeg.R('\xc0\xdf') * cont + + lpeg.R('\xe0\xef') * cont * cont + + lpeg.R('\xf0\xf7') * cont * cont * cont + + local utf8_capture = (((utf8_char / utf8_valid_char) + (lpeg.P(1) / utf8_invalid_char)) ^ 0) * -1 + + tokenize_utf8_string = function(s) + local dq = s:find('"') + local sq = s:find("'") + local t = table.pack(utf8_capture:match(s)) + t.has_double_quote = not not dq + t.has_single_quote = not not sq + return t + end +end + +local function is_plain_key(key) + return type(key) == 'string' and key:match('^[%a_][%a%d_]*$') +end + +local CACHE_TYPES = { + ['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true +} + +-- cache would be populated to be like: +-- { +-- function = { `fun1` = 1, _cnt = 1 }, -- object id +-- table = { `table1` = 1, `table2` = 2, _cnt = 2 }, +-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count +-- } +-- use weakrefs to avoid accidentall adding refcount +local function cache_apperance(obj, cache, option) + if not cache.visited_tables then + cache.visited_tables = setmetatable({}, {__mode = 'k'}) + end + local t = type(obj) + + -- TODO can't test filter_function here as we don't have the ix and key, + -- might cause different results? + -- respect show_xxx and filter_function to be consistent with print results + if (not TYPES[t] and not option.show_table) + or (TYPES[t] and not option['show_'..t]) then + return + end + + if CACHE_TYPES[t] or TYPES[t] == nil then + if not cache[t] then + cache[t] = setmetatable({}, {__mode = 'k'}) + cache[t]._cnt = 0 + end + if not cache[t][obj] then + cache[t]._cnt = cache[t]._cnt + 1 + cache[t][obj] = cache[t]._cnt + end + end + if t == 'table' or TYPES[t] == nil then + if cache.visited_tables[obj] == false then + -- already printed, no need to mark this and its children anymore + return + elseif cache.visited_tables[obj] == nil then + cache.visited_tables[obj] = 1 + else + -- visited already, increment and continue + cache.visited_tables[obj] = cache.visited_tables[obj] + 1 + return + end + for k, v in pairs(obj) do + cache_apperance(k, cache, option) + cache_apperance(v, cache, option) + end + local mt = getmetatable(obj) + if mt and option.show_metatable then + cache_apperance(mt, cache, option) + end + end +end + +-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method +local function str_natural_cmp(lhs, rhs) + while #lhs > 0 and #rhs > 0 do + local lmid, lend = lhs:find('%d+') + local rmid, rend = rhs:find('%d+') + if not (lmid and rmid) then return lhs < rhs end + + local lsub = lhs:sub(1, lmid-1) + local rsub = rhs:sub(1, rmid-1) + if lsub ~= rsub then + return lsub < rsub + end + + local lnum = tonumber(lhs:sub(lmid, lend)) + local rnum = tonumber(rhs:sub(rmid, rend)) + if lnum ~= rnum then + return lnum < rnum + end + + lhs = lhs:sub(lend+1) + rhs = rhs:sub(rend+1) + end + return lhs < rhs +end + +local function cmp(lhs, rhs) + local tleft = type(lhs) + local tright = type(rhs) + if tleft == 'number' and tright == 'number' then return lhs < rhs end + if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end + if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end + + -- allow custom types + local oleft = TYPES[tleft] or 9 + local oright = TYPES[tright] or 9 + return oleft < oright +end + +-- setup option with default +local function make_option(option) + if option == nil then + option = {} + end + for k, v in pairs(pprint.defaults) do + if option[k] == nil then + option[k] = v + end + if option.show_all then + for t, _ in pairs(TYPES) do + option['show_'..t] = true + end + option.show_metatable = true + end + end + return option +end + +-- override defaults and take effects for all following calls +function pprint.setup(option) + pprint.defaults = make_option(option) +end + +-- format lua object into a string +function pprint.pformat(obj, option, printer) + option = make_option(option) + local buf = {} + local function default_printer(s) + table.insert(buf, s) + end + printer = printer or default_printer + + local cache + if option.object_cache == 'global' then + -- steal the cache into a local var so it's not visible from _G or anywhere + -- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway + cache = pprint._cache or {} + pprint._cache = nil + elseif option.object_cache == 'local' then + cache = {} + end + + local last = '' -- used for look back and remove trailing comma + local status = { + indent = '', -- current indent + len = 0, -- current line length + printed_something = false, -- used to remove leading new lines + } + + local wrapped_printer = function(s) + status.printed_something = true + printer(last) + last = s + end + + local function _indent(d) + status.indent = string.rep(' ', d + #(status.indent)) + end + + local function _n(d) + if not status.printed_something then return end + wrapped_printer('\n') + wrapped_printer(status.indent) + if d then + _indent(d) + end + status.len = 0 + return true -- used to close bracket correctly + end + + local function _p(s, nowrap) + status.len = status.len + #s + if not nowrap and status.len > option.level_width then + _n() + wrapped_printer(s) + status.len = #s + else + wrapped_printer(s) + end + end + + local formatter = {} + local function format(v) + local f = formatter[type(v)] + f = f or formatter.table -- allow patched type() + if option.filter_function and option.filter_function(v, nil, nil) then + return '' + else + return f(v) + end + end + + local function tostring_formatter(v) + return tostring(v) + end + + local function number_formatter(n) + return n == math.huge and '[[math.huge]]' or tostring(n) + end + + local function nop_formatter(v) + return '' + end + + local function make_fixed_formatter(t, has_cache) + if has_cache then + return function (v) + return string.format('[[%s %d]]', t, cache[t][v]) + end + else + return function (v) + return '[['..t..']]' + end + end + end + + local function string_formatter(s, force_long_quote) + local tokens = option.string_is_utf8 and tokenize_utf8_string(s) or tokenize_string(s) + local string_len = 0 + local escape_quotes = tokens.has_double_quote and tokens.has_single_quote + for _, token in ipairs(tokens) do + if escape_quotes and token.char == '"' then + string_len = string_len + 2 + else + string_len = string_len + token.len + end + end + local quote_len = 2 + local long_quote_dashes = 0 + local function compute_long_quote_dashes() + local keep_looking = true + while keep_looking do + if s:find('%]' .. string.rep('=', long_quote_dashes) .. '%]') then + long_quote_dashes = long_quote_dashes + 1 + else + keep_looking = false + end + end + end + if force_long_quote then + compute_long_quote_dashes() + quote_len = 2 + long_quote_dashes + end + if quote_len + string_len + status.len > option.level_width then + _n() + -- only wrap string when is longer than level_width + if option.wrap_string and string_len + quote_len > option.level_width then + if not force_long_quote then + compute_long_quote_dashes() + quote_len = 2 + long_quote_dashes + end + -- keep the quotes together + local dashes = string.rep('=', long_quote_dashes) + _p('[' .. dashes .. '[', true) + local status_len = status.len + local line_len = 0 + local line = '' + for _, token in ipairs(tokens) do + if line_len + token.len + status_len > option.level_width then + _n() + _p(line, true) + line_len = token.len + line = token.char + else + line_len = line_len + token.len + line = line .. token.char + end + end + + return line .. ']' .. dashes .. ']' + end + end + + if tokens.has_double_quote and tokens.has_single_quote and not force_long_quote then + for i, token in ipairs(tokens) do + if token.char == '"' then + tokens[i].char = '\\"' + end + end + end + local flat_table = {} + for _, token in ipairs(tokens) do + table.insert(flat_table, token.char) + end + local concat = table.concat(flat_table) + + if force_long_quote then + local dashes = string.rep('=', long_quote_dashes) + return '[' .. dashes .. '[' .. concat .. ']' .. dashes .. ']' + elseif tokens.has_single_quote then + -- use double quote + return '"' .. concat .. '"' + else + -- use single quote + return "'" .. concat .. "'" + end + end + + local function table_formatter(t) + if option.use_tostring then + local mt = getmetatable(t) + if mt and mt.__tostring then + return string_formatter(tostring(t), true) + end + end + + local print_header_ix = nil + local ttype = type(t) + if option.object_cache then + local cache_state = cache.visited_tables[t] + local tix = cache[ttype][t] + -- FIXME should really handle `cache_state == nil` + -- as user might add things through filter_function + if cache_state == false then + -- already printed, just print the the number + return string_formatter(string.format('%s %d', ttype, tix), true) + elseif cache_state > 1 then + -- appeared more than once, print table header with number + print_header_ix = tix + cache.visited_tables[t] = false + else + -- appeared exactly once, print like a normal table + end + end + + local limit = tonumber(option.depth_limit) + if limit and depth > limit then + if print_header_ix then + return string.format('[[%s %d]]...', ttype, print_header_ix) + end + return string_formatter(tostring(t), true) + end + + local tlen = #t + local wrapped = false + _p('{') + _indent(option.indent_size) + _p(string.rep(' ', option.indent_size - 1)) + if print_header_ix then + _p(string.format('--[[%s %d]] ', ttype, print_header_ix)) + end + for ix = 1,tlen do + local v = t[ix] + if formatter[type(v)] == nop_formatter or + (option.filter_function and option.filter_function(v, ix, t)) then + -- pass + else + if option.wrap_array then + wrapped = _n() + end + depth = depth+1 + _p(format(v)..', ') + depth = depth-1 + end + end + + -- hashmap part of the table, in contrast to array part + local function is_hash_key(k) + if type(k) ~= 'number' then + return true + end + + local numkey = math.floor(tonumber(k)) + if numkey ~= k or numkey > tlen or numkey <= 0 then + return true + end + end + + local function print_kv(k, v, t) + -- can't use option.show_x as obj may contain custom type + if formatter[type(v)] == nop_formatter or + formatter[type(k)] == nop_formatter or + (option.filter_function and option.filter_function(v, k, t)) then + return + end + wrapped = _n() + if is_plain_key(k) then + _p(k, true) + else + _p('[') + -- [[]] type string in key is illegal, needs to add spaces inbetween + local k = format(k) + if string.match(k, '%[%[') then + _p(' '..k..' ', true) + else + _p(k, true) + end + _p(']') + end + _p(' = ', true) + depth = depth+1 + _p(format(v), true) + depth = depth-1 + _p(',', true) + end + + if option.sort_keys then + local keys = {} + for k, _ in pairs(t) do + if is_hash_key(k) then + table.insert(keys, k) + end + end + table.sort(keys, cmp) + for _, k in ipairs(keys) do + print_kv(k, t[k], t) + end + else + for k, v in pairs(t) do + if is_hash_key(k) then + print_kv(k, v, t) + end + end + end + + if option.show_metatable then + local mt = getmetatable(t) + if mt then + print_kv('__metatable', mt, t) + end + end + + _indent(-option.indent_size) + -- make { } into {} + last = string.gsub(last, '^ +$', '') + -- peek last to remove trailing comma + last = string.gsub(last, ',%s*$', ' ') + if wrapped then + _n() + end + _p('}') + + return '' + end + + -- set formatters + formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter + formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter + formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge + formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter + formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter + formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter + formatter['string'] = option.show_string and string_formatter or nop_formatter + formatter['table'] = option.show_table and table_formatter or nop_formatter + + if option.object_cache then + -- needs to visit the table before start printing + cache_apperance(obj, cache, option) + end + + _p(format(obj)) + printer(last) -- close the buffered one + + -- put cache back if global + if option.object_cache == 'global' then + pprint._cache = cache + end + + return table.concat(buf) +end + +-- pprint all the arguments +function pprint.pprint( ... ) + local args = {...} + -- select will get an accurate count of array len, counting trailing nils + local len = select('#', ...) + for ix = 1,len do + pprint.pformat(args[ix], nil, io.write) + io.write('\n') + end +end + +setmetatable(pprint, { + __call = function (_, ...) + pprint.pprint(...) + end +}) + +return pprint + diff --git a/lua-temp/tests.lua b/lua-temp/tests.lua index b56215d..7595117 100644 --- a/lua-temp/tests.lua +++ b/lua-temp/tests.lua @@ -1,26 +1,16 @@ +local pprint = require('pprint') local assemble = require('tyche-as') +local VM = require('tyche-vm') -function tprint(o, indent) - indent = indent or 0 - local spacing = string.rep(" ", indent) +---------------------- +-- -- +-- SUPPORT -- +-- -- +---------------------- - if type(o) == 'table' then - local s = '{\n' - for k, v in pairs(o) do - -- Format keys: quote strings, leave numbers as is - local key = type(k) == 'string' and '["'..k..'"]' or '['..k..']' - s = s .. spacing .. " " .. key .. " = " .. tprint(v, indent + 1) .. ",\n" - end - return s .. spacing .. '}' - elseif type(o) == 'string' then - return '"' .. o .. '"' - else - return tostring(o) - end -end function assert_eq(found, expected, key) - assert(type(found) == type(expected), "Types not matching " .. ((key ~= nil) and ('(key: ' .. key .. ')') or '')) + assert(type(found) == type(expected), 'Types not matching , expected "' .. pprint.pformat(expected) .. '", found "' .. pprint.pformat(found) .. '".' .. ((key ~= nil) and ('(key: ' .. key .. ')') or '')) if type(found) == 'table' then assert(#found == #expected, "Tables are of different sizes " .. ((key ~= nil) and ('(key: ' .. key .. ')') or '')) for k,v in pairs(found) do @@ -30,10 +20,16 @@ function assert_eq(found, expected, key) assert_eq(v, found[k], k) end else - assert(found == expected, 'Assertion failed, expected "' .. tprint(expected) .. '", found "' .. tprint(found) .. '".') + assert(found == expected, 'Assertion failed, expected "' .. pprint.pformat(expected) .. '", found "' .. pprint.pformat(found) .. '".') end end +---------------------- +-- -- +-- PARSER -- +-- -- +---------------------- + do local source = [[ .const @@ -66,9 +62,31 @@ do } local found = assemble(source) - -- tprint(expected) - tprint(found) + -- pprint(expected) + -- pprint(found) assert_eq(found, expected) end +---------------------- +-- -- +-- STACK -- +-- -- +---------------------- + +do + local stack = VM.new().stack + stack:push(10) + stack:push(20) + stack:push(30) + + assert_eq(#stack, 3) + assert_eq(stack[0], 10) + assert_eq(stack[1], 20) + assert_eq(stack[-1], 30) + assert_eq(stack[-2], 20) +end + +do +end + print('End.') \ No newline at end of file diff --git a/lua-temp/tyche-vm.lua b/lua-temp/tyche-vm.lua index e69de29..1164131 100644 --- a/lua-temp/tyche-vm.lua +++ b/lua-temp/tyche-vm.lua @@ -0,0 +1,87 @@ +local pprint = require('pprint') + +---------------------- +-- -- +-- STACK -- +-- -- +---------------------- + +local Stack = {} +Stack.__index = Stack + +function Stack.new() + return setmetatable({ + stack = {}, + fps = { 0 }, + }, Stack) +end + +function Stack:top_fps() + return self.fps[#self.fps] +end + +function Stack:push(value) + table.insert(self.stack, value) +end + +function Stack:pop(value) + if #self.stack <= self:top_fps() then error("Stack underflow") end + local v = self.stack[#self.stack] + self.stack[#self.stack] = nil + return v +end + +function Stack:peek(value) + if #self.stack <= self:top_fps() then error("Stack underflow") end + return self.stack[#self.stack] +end + +Stack.__len = function(self) + return #self.stack - self:top_fps() +end + +Stack.__index = function(self, key) + local idx = tonumber(key) + if idx then + if idx >= 0 then + return self.stack[self:top_fps() + idx + 1] + else + if self:top_fps() + #self.stack + idx < 0 then error("Stack access out of range") end + return self.stack[#self.stack + idx + 1] + end + else + return Stack[key] -- other methods + end +end + +Stack.__newindex = function(self, key, value) +end + +function Stack:push_fp() +end + +function Stack:pop_fp() +end + +function Stack:fp_level() +end + +function Stack:debug() +end + +---------------------- +-- -- +-- VM -- +-- -- +---------------------- + +local VM = {} +VM.__index = VM + +function VM.new() + return setmetatable({ + stack = Stack.new() + }, VM) +end + +return VM \ No newline at end of file