replace go json with pure lua, much speed

This commit is contained in:
Sky Johnson 2025-07-17 20:17:53 -05:00
parent 12ba756b95
commit 74faa76dbd
5 changed files with 533 additions and 142 deletions

2
.gitignore vendored
View File

@ -23,6 +23,6 @@ luajit/.git
go.work
# Test directories and files
test.lua
/*.lua
test_fs_dir
public

View File

@ -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
}

View File

@ -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

View File

@ -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())

View File

@ -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 = "<script>" -- Generic name for inline scripts
end
local line = info.currentline or "unknown"
local error_msg = message or "assertion failed"
local full_msg = string.format("%s:%s: %s", file, line, error_msg)