diff --git a/frameworks/Luau/lute/lute.dockerfile b/frameworks/Luau/lute/lute.dockerfile index 2fca69214eb..9a0e49c640d 100644 --- a/frameworks/Luau/lute/lute.dockerfile +++ b/frameworks/Luau/lute/lute.dockerfile @@ -16,4 +16,4 @@ RUN rokit install --no-trust-check COPY ./src . -CMD ["sh", "-c", "lute parallel.luau -- $(nproc)"] +CMD ["sh", "-c", "lute parallel.luau"] diff --git a/frameworks/Luau/lute/rokit.toml b/frameworks/Luau/lute/rokit.toml index 83ad1526885..4b4d444e71c 100644 --- a/frameworks/Luau/lute/rokit.toml +++ b/frameworks/Luau/lute/rokit.toml @@ -4,4 +4,4 @@ # New tools can be added by running `rokit add ` in a terminal. [tools] -lute = "aatxe/lute@0.1.0-nightly.20250322" +lute = "luau-lang/lute@0.1.0-nightly.20250506" diff --git a/frameworks/Luau/lute/src/json.luau b/frameworks/Luau/lute/src/json.luau index bc8c40e1251..a4ba7becbf6 100644 --- a/frameworks/Luau/lute/src/json.luau +++ b/frameworks/Luau/lute/src/json.luau @@ -1,486 +1,197 @@ ---!strict - -local json = { - --- Not actually a nil value, a newproxy stand-in for a null value since Luau has no actual representation of `null` - NULL = newproxy() :: nil, -} - -type JSONPrimitive = nil | number | string | boolean -type Object = { [string]: Value } -type Array = { Value } -export type Value = JSONPrimitive | Array | Object - --- serialization - -type SerializerState = { - buf: buffer, - cursor: number, - prettyPrint: boolean, - depth: number, -} - -local function checkState(state: SerializerState, len: number) - local curLen = buffer.len(state.buf) - - if state.cursor + len < curLen then - return - end - - local newBuffer = buffer.create(curLen * 2) - - buffer.copy(newBuffer, 0, state.buf) - - state.buf = newBuffer -end - -local function writeByte(state: SerializerState, byte: number) - checkState(state, 1) - - buffer.writeu8(state.buf, state.cursor, byte) - - state.cursor += 1 -end - -local function writeSpaces(state: SerializerState) - if state.depth == 0 then - return - end - - if state.prettyPrint then - checkState(state, state.depth * 4) - - for i = 0, state.depth do - buffer.writeu32(state.buf, state.cursor, 0x_20_20_20_20) - state.cursor += 4 - end - else - buffer.writeu8(state.buf, state.cursor, string.byte(" ")) - - state.cursor += 1 - end -end - -local function writeString(state: SerializerState, str: string) - checkState(state, #str) - - buffer.writestring(state.buf, state.cursor, str) - - state.cursor += #str -end - -local serializeAny - +--!native +local COMMA = 44 +local QUOTATION = 34 +local OPEN_BRACKET = 91 +local CLOSE_BRACKET = 93 +local OPEN_BRACE = 123 +local CLOSE_BRACE = 125 +local BACKSLASH = 92 + +-- used for string serialization local ESCAPE_MAP = { - [0x5C] = string.byte("\\"), -- 5C = '\' - [0x08] = string.byte("b"), - [0x0C] = string.byte("f"), - [0x0A] = string.byte("n"), - [0x0D] = string.byte("r"), - [0x09] = string.byte("t"), + [0x5C] = string.byte("\\"), -- 5C = '\' + [0x08] = string.byte("b"), + [0x0C] = string.byte("f"), + [0x0A] = string.byte("n"), + [0x0D] = string.byte("r"), + [0x09] = string.byte("t"), } -local function serializeUnicode(codepoint: number) - if codepoint >= 0x10000 then - local high = math.floor((codepoint - 0x10000) / 0x400) + 0xD800 - local low = ((codepoint - 0x10000) % 0x400) + 0xDC00 - return string.format("\\u%04x\\u%04x", high, low) - end - - return string.format("\\u%04x", codepoint) -end +local types = require("./types") -local function serializeString(state: SerializerState, str: string) - checkState(state, #str) +local buff: buffer = buffer.create(1024) +local size = 1024 +local cursor = 0 - writeByte(state, string.byte('"')) +local function alloc(len: number) + if cursor + len < size then + return + end - for pos, codepoint in utf8.codes(str) do - if ESCAPE_MAP[codepoint] then - writeByte(state, string.byte("\\")) - writeByte(state, ESCAPE_MAP[codepoint]) - elseif codepoint < 32 or codepoint > 126 then - writeString(state, serializeUnicode(codepoint)) - else - writeString(state, utf8.char(codepoint)) - end - end + while cursor + len > size do + size *= 2 + end - writeByte(state, string.byte('"')) + local newbuff = buffer.create(size) + buffer.copy(newbuff, 0, buff) + buff = newbuff end -local function serializeArray(state: SerializerState, array: Array) - state.depth += 1 - - writeByte(state, string.byte("[")) - - if state.prettyPrint and #array ~= 0 then - writeByte(state, string.byte("\n")) - end - - for i, value in array do - if i ~= 1 then - writeByte(state, string.byte(",")) - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - end - - if i ~= 1 or state.prettyPrint then - writeSpaces(state) - end - - serializeAny(state, value) - end - - state.depth -= 1 - - if state.prettyPrint and #array ~= 0 then - writeByte(state, string.byte("\n")) - writeSpaces(state) - end - - writeByte(state, string.byte("]")) -end - -local function serializeTable(state: SerializerState, object: Object) - writeByte(state, string.byte("{")) - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - - state.depth += 1 - - local first = true - for key, value in object do - if not first then - writeByte(state, string.byte(",")) - writeByte(state, string.byte(" ")) - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - end - - first = false - - writeSpaces(state) - - writeByte(state, string.byte('"')) - writeString(state, key) - writeString(state, '": ') - - serializeAny(state, value) - end - - if state.prettyPrint then - writeByte(state, string.byte("\n")) - end - - state.depth -= 1 - - writeSpaces(state) - - writeByte(state, string.byte("}")) +local function WRITE_QUOTATION() + buffer.writeu8(buff, cursor, QUOTATION) + cursor += 1 end -serializeAny = function(state: SerializerState, value: Value) - local valueType = type(value) - - if value == json.NULL then - writeString(state, "null") - elseif valueType == "boolean" then - writeString(state, if value then "true" else "false") - elseif valueType == "number" then - writeString(state, tostring(value)) - elseif valueType == "string" then - serializeString(state, value :: string) - elseif valueType == "table" then - if #(value :: {}) == 0 and next(value :: {}) ~= nil then - serializeTable(state, value :: Object) - else - serializeArray(state, value :: Array) - end - else - error("Unknown value", 2) - end +local function WRITE_COMMA() + buffer.writeu8(buff, cursor, COMMA) + cursor += 1 end --- deserialization +local function writeString(str: string) + local len = #str + alloc(len) -type DeserializerState = { - src: string, - cursor: number, -} + buffer.writestring(buff, cursor, str) -local function deserializerError(state: DeserializerState, msg: string): never - return error(`JSON error - {msg} around {state.cursor}`) + cursor += len end -local function skipWhitespace(state: DeserializerState): boolean - state.cursor = string.find(state.src, "%S", state.cursor) :: number - - if not state.cursor then - return false - end - - return true -end +local function serializeUnicode(codepoint: number) + if codepoint >= 0x10000 then + local high = ((codepoint - 0x10000) // 0x400) + 0xD800 + local low = ((codepoint - 0x10000) % 0x400) + 0xDC00 + return string.format("\\u%04x\\u%04x", high, low) + end -local function currentByte(state: DeserializerState) - return string.byte(state.src, state.cursor) + return string.format("\\u%04x", codepoint) end -local function deserializeNumber(state: DeserializerState) - -- first "segment" - local nStart, nEnd = string.find(state.src, "^[%-%deE]*", state.cursor) - - if not nStart then - -- i dont think this is possible - deserializerError(state, "Could not match a number literal?") - end - - if string.byte(state.src, nEnd :: number + 1) == string.byte(".") then -- decimal! - local decStart, decEnd = string.find(state.src, "^[eE%-+%d]+", nEnd :: number + 2) - - if not decStart then - deserializerError(state, "Trailing '.' in number value") - end - - nEnd = decEnd - end - - local num = tonumber(string.sub(state.src, nStart :: number, nEnd)) - - if not num then - deserializerError(state, "Malformed number value") - end +local function serializeString(str: string) + -- covers the quotations & utf + alloc((#str * 4) + 2) - state.cursor = nEnd :: number + 1 + WRITE_QUOTATION() - return num -end - -local function decodeSurrogatePair(high, low): string? - local highVal = tonumber(high, 16) - local lowVal = tonumber(low, 16) + for pos, codepoint in utf8.codes(str) do + if ESCAPE_MAP[codepoint] then + -- write \ + buffer.writeu8(buff, cursor, BACKSLASH) + cursor += 1 + + buffer.writeu8(buff, cursor, ESCAPE_MAP[codepoint]) + cursor += 1 + elseif codepoint < 32 or codepoint > 126 then + writeString(serializeUnicode(codepoint)) + else + -- we are in ascii + + buffer.writeu8(buff, cursor, codepoint) + cursor += 1 + end + end - if not highVal or not lowVal then - return nil -- Invalid - end - - -- Calculate the actual Unicode codepoint - local codepoint = 0x10000 + ((highVal - 0xD800) * 0x400) + (lowVal - 0xDC00) - return utf8.char(codepoint) + WRITE_QUOTATION() end -local function deserializeString(state: DeserializerState): string - state.cursor += 1 - - local startPos = state.cursor - - if currentByte(state) == string.byte('"') then - state.cursor += 1 - - return "" - end - - while state.cursor <= #state.src do - if currentByte(state) == string.byte('"') then - state.cursor += 1 +local serializeAny: (value: types.Value) -> () - local source = string.sub(state.src, startPos, state.cursor - 2) +local function serializeArray(array: types.Array) + -- open close bracket + -- 1 comma for each array element + alloc(2 + #array) - source = string.gsub( - source, - "\\u([dD]83[dD])\\u(d[cC]%w%w)", - function(high, low) - return decodeSurrogatePair(high, low) - or deserializerError(state, "Invalid unicode surrogate pair") - end :: any - ) - -- Handle regular Unicode escapes - source = string.gsub(source, "\\u(%x%x%x%x)", function(code) - return utf8.char(tonumber(code, 16) :: number) - end) + -- write [ + buffer.writeu8(buff, cursor, OPEN_BRACKET) + cursor += 1 - source = string.gsub(source, "\\\\", "\0") - source = string.gsub(source, "\\b", "\b") - source = string.gsub(source, "\\f", "\f") - source = string.gsub(source, "\\n", "\n") - source = string.gsub(source, "\\r", "\r") - source = string.gsub(source, "\\t", "\t") - source = string.gsub(source, '\\"', '"') - source = string.gsub(source, '\0', '\\') + for _, value in array do + serializeAny(value) + WRITE_COMMA() + end - return source - end + -- get rid of comma + cursor -= 1 - if currentByte(state) == string.byte("\\") then - state.cursor += 1 - end - - state.cursor += 1 - end - - -- error - - state.cursor = startPos - - return deserializerError(state, "Unterminated string") + -- write ] + buffer.writeu8(buff, cursor, CLOSE_BRACKET) + cursor += 1 end -local deserialize - -local function deserializeArray(state: DeserializerState): Array - state.cursor += 1 - - local current: Array = {} +local function serializeTable(object: types.Object) + -- openbrace, newline + alloc(2) - local expectingValue = false - while state.cursor < #state.src do - skipWhitespace(state) + -- write { + buffer.writeu8(buff, cursor, OPEN_BRACE) + cursor += 1 - if currentByte(state) == string.byte(",") then - expectingValue = true - state.cursor += 1 - end + for key, value in object do + -- quotation 2x, colon and comma + alloc(4) - skipWhitespace(state) + WRITE_QUOTATION() + buffer.writestring(buff, cursor, key) + cursor += #key + 2 - if currentByte(state) == string.byte("]") then - break - end + buffer.writestring(buff, cursor - 2, '":') - table.insert(current, deserialize(state)) + serializeAny(value) - expectingValue = false - end + WRITE_COMMA() + end - if expectingValue then - deserializerError(state, "Trailing comma") - end + -- get rid of the comma + cursor -= 1 - if not skipWhitespace(state) or currentByte(state) ~= string.byte("]") then - deserializerError(state, "Unterminated array") - end - - state.cursor += 1 - - return current + -- write } + buffer.writeu8(buff, cursor, CLOSE_BRACE) + cursor += 1 end -local function deserializeObject(state: DeserializerState): Object - state.cursor += 1 - - local current = {} - - local expectingValue = false - while state.cursor < #state.src do - skipWhitespace(state) - - if currentByte(state) == string.byte("}") then - break - end - - skipWhitespace(state) - - if currentByte(state) ~= string.byte('"') then - return deserializerError(state, "Expected a string key") - end - - local key = deserializeString(state) - - skipWhitespace(state) - - if currentByte(state) ~= string.byte(":") then - return deserializerError(state, "Expected ':' for key value pair") - end - - state.cursor += 1 - - local value = deserialize(state) - - current[key] = value +serializeAny = function(value: types.Value) + local valueType = type(value) - if not skipWhitespace(state) then - deserializerError(state, "Unterminated object") - end + if valueType == "string" then + serializeString(value :: string) + elseif valueType == "table" then + if #(value :: {}) == 0 then + serializeTable(value :: types.Object) + else + serializeArray(value :: types.Array) + end + elseif valueType == "number" then + local numstr = tostring(value) - skipWhitespace(state) - - if currentByte(state) == string.byte(",") then - expectingValue = true - state.cursor += 1 - else - expectingValue = false - end - end - - if expectingValue then - return deserializerError(state, "Trailing comma") - end - - if not skipWhitespace(state) or currentByte(state) ~= string.byte("}") then - deserializerError(state, "Unterminated object") - end - - state.cursor += 1 - - return current -end - -deserialize = function(state: DeserializerState): Value - skipWhitespace(state) - - local fourChars = string.sub(state.src, state.cursor, state.cursor + 3) - - if fourChars == "null" then - state.cursor += 4 - return json.NULL - elseif fourChars == "true" then - state.cursor += 4 - return true - elseif string.sub(state.src, state.cursor, state.cursor + 4) == "false" then - state.cursor += 5 - return false - elseif string.match(state.src, "^[%d%.]", state.cursor) then - -- number - return deserializeNumber(state) - elseif string.byte(state.src, state.cursor) == string.byte('"') then - return deserializeString(state) - elseif string.byte(state.src, state.cursor) == string.byte("[") then - return deserializeArray(state) - elseif string.byte(state.src, state.cursor) == string.byte("{") then - return deserializeObject(state) - end - - return deserializerError(state, `Unexpected token '{string.sub(state.src, state.cursor, state.cursor)}'`) + buffer.writestring(buff, cursor, numstr) + cursor += #numstr + elseif value == types.NULL then + -- null as u32 + buffer.writeu32(buff, cursor, 1819047278) + cursor += 4 + elseif value == true then + -- true as u32 + buffer.writeu32(buff, cursor, 1702195828) + cursor += 4 + elseif value == false then + -- false as u32 + u8 + buffer.writeu32(buff, cursor, 1936482662) + buffer.writeu8(buff, cursor + 4, 101) + cursor += 5 + else + error("Unknown value", 2) + end end --- user-facing - -json.serialize = function(value: Value, prettyPrint: boolean?) - local state: SerializerState = { - buf = buffer.create(1024), - cursor = 0, - prettyPrint = prettyPrint or false, - depth = 0, - } - - serializeAny(state, value) +local function serialize(data: types.Value) + buff = buffer.create(1024) + size = 1024 + cursor = 0 - return buffer.readstring(state.buf, 0, state.cursor) -end - -json.deserialize = function(src: string) - local state = { - src = src, - cursor = 0, - } + serializeAny(data) - return deserialize(state) + return buffer.readstring(buff, 0, cursor) end -return table.freeze(json) +return { + serialize = serialize, +} diff --git a/frameworks/Luau/lute/src/parallel.luau b/frameworks/Luau/lute/src/parallel.luau index df3f97a505e..557c4dd0326 100644 --- a/frameworks/Luau/lute/src/parallel.luau +++ b/frameworks/Luau/lute/src/parallel.luau @@ -1,8 +1,9 @@ local vm = require("@lute/vm") +local system = require("@lute/system") -local threadCount = tonumber(...) +local threadCount = system.threadcount() -for i = 1, threadCount do +for _ = 1, threadCount do coroutine.resume(coroutine.create(vm.create("./serve").serve)) end diff --git a/frameworks/Luau/lute/src/serve.luau b/frameworks/Luau/lute/src/serve.luau index 53d34f06b72..1cb525512ad 100644 --- a/frameworks/Luau/lute/src/serve.luau +++ b/frameworks/Luau/lute/src/serve.luau @@ -1,18 +1,20 @@ local net = require("@lute/net") local json = require("./json") +local plaintext = { + status = 200, + headers = { + ["Content-Type"] = "text/plain", + ["Server"] = "Lute", + }, + body = "Hello, world!", +} + return { serve = function() net.serve(function(req) if req.path == "/plaintext" then - return { - status = 200, - headers = { - ["Content-Type"] = "text/plain", - ["Server"] = "Lute", - }, - body = "Hello, world!", - } + return plaintext elseif req.path == "/json" then return { status = 200, diff --git a/frameworks/Luau/lute/src/types.luau b/frameworks/Luau/lute/src/types.luau new file mode 100644 index 00000000000..bf3a5ce3771 --- /dev/null +++ b/frameworks/Luau/lute/src/types.luau @@ -0,0 +1,9 @@ +export type JSONPrimitive = nil | number | string | boolean +export type Object = { [string]: Value } +export type Array = { Value } + +export type Value = JSONPrimitive | Array | Object + +return { + NULL = newproxy() :: nil, +}