From 74faa76dbddcd06690aefa52ee22b2c77f6adba3 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 17 Jul 2025 20:17:53 -0500 Subject: [PATCH] replace go json with pure lua, much speed --- .gitignore | 2 +- modules/json/json.go | 45 ---- modules/json/json.lua | 615 +++++++++++++++++++++++++++++++++++------- modules/registry.go | 2 - tests/tests.lua | 11 +- 5 files changed, 533 insertions(+), 142 deletions(-) delete mode 100644 modules/json/json.go diff --git a/.gitignore b/.gitignore index db6628d..943ea12 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,6 @@ luajit/.git go.work # Test directories and files -test.lua +/*.lua test_fs_dir public \ No newline at end of file diff --git a/modules/json/json.go b/modules/json/json.go deleted file mode 100644 index 06b2560..0000000 --- a/modules/json/json.go +++ /dev/null @@ -1,45 +0,0 @@ -package json - -import ( - luajit "git.sharkk.net/Sky/LuaJIT-to-Go" - "github.com/goccy/go-json" -) - -func GetFunctionList() map[string]luajit.GoFunction { - return map[string]luajit.GoFunction{ - "json_encode": json_encode, - "json_decode": json_decode, - } -} - -func json_encode(s *luajit.State) int { - value, err := s.ToValue(1) - if err != nil { - s.PushNil() - s.PushString("failed to read value") - return 2 - } - - data, err := json.Marshal(value) - if err != nil { - s.PushNil() - s.PushString("encoding failed") - return 2 - } - - s.PushString(string(data)) - return 1 -} - -func json_decode(s *luajit.State) int { - jsonStr := s.ToString(1) - var result any - if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { - s.PushNil() - s.PushString("invalid JSON") - return 2 - } - - s.PushValue(result) - return 1 -} diff --git a/modules/json/json.lua b/modules/json/json.lua index 46437f1..f69cac5 100644 --- a/modules/json/json.lua +++ b/modules/json/json.lua @@ -1,105 +1,380 @@ --- modules/json.lua - High-performance JSON module using Go functions +-- modules/json.lua - High-performance JSON module local json = {} --- Use the fast Go JSON encoder/decoder function json.encode(value) - return moonshark.json_encode(value) + local buffer = {} + local pos = 1 + + local function encode_string(s) + buffer[pos] = '"' + pos = pos + 1 + + local start = 1 + for i = 1, #s do + local c = s:byte(i) + if c == 34 then -- " + if i > start then + buffer[pos] = s:sub(start, i - 1) + pos = pos + 1 + end + buffer[pos] = '\\"' + pos = pos + 1 + start = i + 1 + elseif c == 92 then -- \ + if i > start then + buffer[pos] = s:sub(start, i - 1) + pos = pos + 1 + end + buffer[pos] = '\\\\' + pos = pos + 1 + start = i + 1 + elseif c < 32 then + if i > start then + buffer[pos] = s:sub(start, i - 1) + pos = pos + 1 + end + if c == 8 then + buffer[pos] = '\\b' + elseif c == 9 then + buffer[pos] = '\\t' + elseif c == 10 then + buffer[pos] = '\\n' + elseif c == 12 then + buffer[pos] = '\\f' + elseif c == 13 then + buffer[pos] = '\\r' + else + buffer[pos] = ('\\u%04x'):format(c) + end + pos = pos + 1 + start = i + 1 + end + end + + if start <= #s then + buffer[pos] = s:sub(start) + pos = pos + 1 + end + + buffer[pos] = '"' + pos = pos + 1 + end + + local function encode_value(v, depth) + local t = type(v) + + if t == 'string' then + encode_string(v) + elseif t == 'number' then + if v ~= v then -- NaN + buffer[pos] = 'null' + elseif v == 1/0 or v == -1/0 then -- Infinity + buffer[pos] = 'null' + else + buffer[pos] = tostring(v) + end + pos = pos + 1 + elseif t == 'boolean' then + buffer[pos] = v and 'true' or 'false' + pos = pos + 1 + elseif t == 'table' then + if depth > 100 then error('circular reference') end + + local is_array = true + local max_index = 0 + local count = 0 + + for k, _ in pairs(v) do + count = count + 1 + if type(k) ~= 'number' or k <= 0 or k % 1 ~= 0 then + is_array = false + break + end + if k > max_index then max_index = k end + end + + if is_array and count == max_index then + buffer[pos] = '[' + pos = pos + 1 + + for i = 1, max_index do + if i > 1 then + buffer[pos] = ',' + pos = pos + 1 + end + encode_value(v[i], depth + 1) + end + + buffer[pos] = ']' + pos = pos + 1 + else + buffer[pos] = '{' + pos = pos + 1 + + local first = true + for k, val in pairs(v) do + if not first then + buffer[pos] = ',' + pos = pos + 1 + end + first = false + + encode_string(tostring(k)) + buffer[pos] = ':' + pos = pos + 1 + encode_value(val, depth + 1) + end + + buffer[pos] = '}' + pos = pos + 1 + end + else + buffer[pos] = 'null' + pos = pos + 1 + end + end + + encode_value(value, 0) + return table.concat(buffer) end function json.decode(str) - local result, err = moonshark.json_decode(str) - if result == nil and err then - error("json_decode: " .. err) + local pos = 1 + local len = #str + + local function skip_whitespace() + while pos <= len do + local c = str:byte(pos) + if c ~= 32 and c ~= 9 and c ~= 10 and c ~= 13 then break end + pos = pos + 1 + end end + + local function decode_string() + local start = pos + 1 + pos = pos + 1 + + while pos <= len do + local c = str:byte(pos) + if c == 34 then -- " + local result = str:sub(start, pos - 1) + pos = pos + 1 + + if result:find('\\') then + result = result:gsub('\\(.)', { + ['"'] = '"', + ['\\'] = '\\', + ['/'] = '/', + ['b'] = '\b', + ['f'] = '\f', + ['n'] = '\n', + ['r'] = '\r', + ['t'] = '\t' + }) + result = result:gsub('\\u(%x%x%x%x)', function(hex) + return string.char(tonumber(hex, 16)) + end) + end + + return result + elseif c == 92 then -- \ + pos = pos + 2 + else + pos = pos + 1 + end + end + + error('unterminated string') + end + + local function decode_number() + local start = pos + local c = str:byte(pos) + + if c == 45 then pos = pos + 1 end -- - + + c = str:byte(pos) + if not c or c < 48 or c > 57 then error('invalid number') end + + if c == 48 then + pos = pos + 1 + else + while pos <= len do + c = str:byte(pos) + if c < 48 or c > 57 then break end + pos = pos + 1 + end + end + + if pos <= len and str:byte(pos) == 46 then -- . + pos = pos + 1 + local found_digit = false + while pos <= len do + c = str:byte(pos) + if c < 48 or c > 57 then break end + found_digit = true + pos = pos + 1 + end + if not found_digit then error('invalid number') end + end + + if pos <= len then + c = str:byte(pos) + if c == 101 or c == 69 then -- e or E + pos = pos + 1 + if pos <= len then + c = str:byte(pos) + if c == 43 or c == 45 then pos = pos + 1 end -- + or - + end + local found_digit = false + while pos <= len do + c = str:byte(pos) + if c < 48 or c > 57 then break end + found_digit = true + pos = pos + 1 + end + if not found_digit then error('invalid number') end + end + end + + return tonumber(str:sub(start, pos - 1)) + end + + local function decode_value() + skip_whitespace() + if pos > len then error('unexpected end') end + + local c = str:byte(pos) + + if c == 34 then -- " + return decode_string() + elseif c == 123 then -- { + local result = {} + pos = pos + 1 + skip_whitespace() + + if pos <= len and str:byte(pos) == 125 then -- } + pos = pos + 1 + return result + end + + while true do + skip_whitespace() + if pos > len or str:byte(pos) ~= 34 then error('expected string key') end + + local key = decode_string() + skip_whitespace() + + if pos > len or str:byte(pos) ~= 58 then error('expected :') end + pos = pos + 1 + + result[key] = decode_value() + skip_whitespace() + + if pos > len then error('unexpected end') end + c = str:byte(pos) + + if c == 125 then -- } + pos = pos + 1 + return result + elseif c == 44 then -- , + pos = pos + 1 + else + error('expected , or }') + end + end + + elseif c == 91 then -- [ + local result = {} + local index = 1 + pos = pos + 1 + skip_whitespace() + + if pos <= len and str:byte(pos) == 93 then -- ] + pos = pos + 1 + return result + end + + while true do + result[index] = decode_value() + index = index + 1 + skip_whitespace() + + if pos > len then error('unexpected end') end + c = str:byte(pos) + + if c == 93 then -- ] + pos = pos + 1 + return result + elseif c == 44 then -- , + pos = pos + 1 + else + error('expected , or ]') + end + end + + elseif c == 116 then -- true + if str:sub(pos, pos + 3) == 'true' then + pos = pos + 4 + return true + end + error('invalid literal') + + elseif c == 102 then -- false + if str:sub(pos, pos + 4) == 'false' then + pos = pos + 5 + return false + end + error('invalid literal') + + elseif c == 110 then -- null + if str:sub(pos, pos + 3) == 'null' then + pos = pos + 4 + return nil + end + error('invalid literal') + + elseif (c >= 48 and c <= 57) or c == 45 then -- 0-9 or - + return decode_number() + + else + error('unexpected character') + end + end + + local result = decode_value() + skip_whitespace() + if pos <= len then error('unexpected content after JSON') end return result end --- Pretty print JSON with indentation -function json.pretty(value, indent) - indent = indent or 2 - local encoded = json.encode(value) - local result = {} - local depth = 0 - local in_string = false - local escape_next = false - - for i = 1, #encoded do - local char = encoded:sub(i, i) - - if escape_next then - table.insert(result, char) - escape_next = false - elseif char == "\\" and in_string then - table.insert(result, char) - escape_next = true - elseif char == '"' then - table.insert(result, char) - in_string = not in_string - elseif not in_string then - if char == "{" or char == "[" then - table.insert(result, char) - depth = depth + 1 - table.insert(result, "\n" .. string.rep(" ", depth * indent)) - elseif char == "}" or char == "]" then - depth = depth - 1 - table.insert(result, "\n" .. string.rep(" ", depth * indent)) - table.insert(result, char) - elseif char == "," then - table.insert(result, char) - table.insert(result, "\n" .. string.rep(" ", depth * indent)) - elseif char == ":" then - table.insert(result, char .. " ") - else - table.insert(result, char) - end - else - table.insert(result, char) - end - end - - return table.concat(result) -end - --- Load JSON from file function json.load_file(filename) - if not moonshark.file_exists(filename) then - error("File not found: " .. filename) - end - local file = io.open(filename, "r") if not file then error("Cannot open file: " .. filename) end - + local content = file:read("*all") file:close() - + return json.decode(content) end --- Save data to JSON file -function json.save_file(filename, data, pretty) - local content - if pretty then - content = json.pretty(data) - else - content = json.encode(data) - end - +function json.save_file(filename, data) local file = io.open(filename, "w") if not file then error("Cannot write to file: " .. filename) end - - file:write(content) + + file:write(json.encode(data)) file:close() end --- Merge JSON objects function json.merge(...) local result = {} - for i = 1, select("#", ...) do + local n = select("#", ...) + for i = 1, n do local obj = select(i, ...) if type(obj) == "table" then for k, v in pairs(obj) do @@ -110,61 +385,217 @@ function json.merge(...) return result end --- Extract values by JSONPath-like syntax (simplified) function json.extract(data, path) - local parts = moonshark.string_split(path, ".") local current = data - - for _, part in ipairs(parts) do + local start = 1 + local len = #path + + while start <= len do + local dot_pos = path:find(".", start, true) + local part = dot_pos and path:sub(start, dot_pos - 1) or path:sub(start) + if type(current) ~= "table" then return nil end - - -- Handle array indices [0], [1], etc. - local array_match = part:match("^%[(%d+)%]$") - if array_match then - local index = tonumber(array_match) + 1 -- Lua is 1-indexed + + local bracket_start, bracket_end = part:find("^%[(%d+)%]$") + if bracket_start then + local index = tonumber(part:sub(2, -2)) + 1 current = current[index] else current = current[part] end - + if current == nil then return nil end + + start = dot_pos and dot_pos + 1 or len + 1 end - + return current end --- Validate JSON structure against schema (basic) +function json.pretty(value, indent) + local buffer = {} + local pos = 1 + indent = indent or " " + + local function encode_string(s) + buffer[pos] = '"' + pos = pos + 1 + + local start = 1 + for i = 1, #s do + local c = s:byte(i) + if c == 34 then -- " + if i > start then + buffer[pos] = s:sub(start, i - 1) + pos = pos + 1 + end + buffer[pos] = '\\"' + pos = pos + 1 + start = i + 1 + elseif c == 92 then -- \ + if i > start then + buffer[pos] = s:sub(start, i - 1) + pos = pos + 1 + end + buffer[pos] = '\\\\' + pos = pos + 1 + start = i + 1 + elseif c < 32 then + if i > start then + buffer[pos] = s:sub(start, i - 1) + pos = pos + 1 + end + if c == 8 then + buffer[pos] = '\\b' + elseif c == 9 then + buffer[pos] = '\\t' + elseif c == 10 then + buffer[pos] = '\\n' + elseif c == 12 then + buffer[pos] = '\\f' + elseif c == 13 then + buffer[pos] = '\\r' + else + buffer[pos] = ('\\u%04x'):format(c) + end + pos = pos + 1 + start = i + 1 + end + end + + if start <= #s then + buffer[pos] = s:sub(start) + pos = pos + 1 + end + + buffer[pos] = '"' + pos = pos + 1 + end + + local function encode_value(v, depth) + local t = type(v) + local current_indent = string.rep(indent, depth) + local next_indent = string.rep(indent, depth + 1) + + if t == 'string' then + encode_string(v) + elseif t == 'number' then + if v ~= v then -- NaN + buffer[pos] = 'null' + elseif v == 1/0 or v == -1/0 then -- Infinity + buffer[pos] = 'null' + else + buffer[pos] = tostring(v) + end + pos = pos + 1 + elseif t == 'boolean' then + buffer[pos] = v and 'true' or 'false' + pos = pos + 1 + elseif t == 'table' then + if depth > 100 then error('circular reference') end + + local is_array = true + local max_index = 0 + local count = 0 + + for k, _ in pairs(v) do + count = count + 1 + if type(k) ~= 'number' or k <= 0 or k % 1 ~= 0 then + is_array = false + break + end + if k > max_index then max_index = k end + end + + if is_array and count == max_index then + buffer[pos] = '[\n' + pos = pos + 1 + + for i = 1, max_index do + buffer[pos] = next_indent + pos = pos + 1 + encode_value(v[i], depth + 1) + if i < max_index then + buffer[pos] = ',' + pos = pos + 1 + end + buffer[pos] = '\n' + pos = pos + 1 + end + + buffer[pos] = current_indent .. ']' + pos = pos + 1 + else + buffer[pos] = '{\n' + pos = pos + 1 + + local keys = {} + for k in pairs(v) do + keys[#keys + 1] = k + end + + for i, k in ipairs(keys) do + buffer[pos] = next_indent + pos = pos + 1 + encode_string(tostring(k)) + buffer[pos] = ': ' + pos = pos + 1 + encode_value(v[k], depth + 1) + if i < #keys then + buffer[pos] = ',' + pos = pos + 1 + end + buffer[pos] = '\n' + pos = pos + 1 + end + + buffer[pos] = current_indent .. '}' + pos = pos + 1 + end + else + buffer[pos] = 'null' + pos = pos + 1 + end + end + + encode_value(value, 0) + return table.concat(buffer) +end + function json.validate(data, schema) local function validate_value(value, schema_value) local value_type = type(value) local schema_type = schema_value.type - + if schema_type and value_type ~= schema_type then return false, "Expected " .. schema_type .. ", got " .. value_type end - + if schema_type == "table" and schema_value.properties then + local required = schema_value.required for prop, prop_schema in pairs(schema_value.properties) do - if schema_value.required and schema_value.required[prop] and value[prop] == nil then + local prop_value = value[prop] + + if required and required[prop] and prop_value == nil then return false, "Missing required property: " .. prop end - - if value[prop] ~= nil then - local valid, err = validate_value(value[prop], prop_schema) + + if prop_value ~= nil then + local valid, err = validate_value(prop_value, prop_schema) if not valid then return false, "Property " .. prop .. ": " .. err end end end end - + return true end - + return validate_value(data, schema) end diff --git a/modules/registry.go b/modules/registry.go index ee7ccaa..81e6cf1 100644 --- a/modules/registry.go +++ b/modules/registry.go @@ -8,7 +8,6 @@ import ( "Moonshark/modules/crypto" "Moonshark/modules/fs" "Moonshark/modules/http" - "Moonshark/modules/json" "Moonshark/modules/math" lua_string "Moonshark/modules/string" @@ -35,7 +34,6 @@ func New() *Registry { } // Load all Go functions - maps.Copy(r.goFuncs, json.GetFunctionList()) maps.Copy(r.goFuncs, lua_string.GetFunctionList()) maps.Copy(r.goFuncs, math.GetFunctionList()) maps.Copy(r.goFuncs, fs.GetFunctionList()) diff --git a/tests/tests.lua b/tests/tests.lua index 3652369..ec8a19f 100644 --- a/tests/tests.lua +++ b/tests/tests.lua @@ -13,9 +13,16 @@ function assert(condition, message, level) level = level or 2 local info = debug.getinfo(level, "Sl") - local file = info.source:match("@?(.+)") or "unknown" - local line = info.currentline or "unknown" + local file = info.source + -- Extract filename from source or use generic name + if file:sub(1,1) == "@" then + file = file:sub(2) -- Remove @ prefix for files + else + file = "