Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Translate Chalk to Lua #594

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 44 additions & 2 deletions .gitignore
@@ -1,4 +1,46 @@
# always ignore files
*.DS_Store
.idea
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
# include common debug launch configs
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/launch.sample.json
*.sublime-*
.settings/

# test related, or directories generated by tests
test/actual
actual
coverage
.nyc*
debug.log
/luacov.*
/lcov*
/site
/output.txt
cachegrind.out.*
callgrind.out.*

# npm
node_modules
npm-debug.log

# yarn
yarn.lock
coverage
.nyc_output
yarn-error.log

# misc
_gh_pages
_draft
_drafts
bower_components
vendor
temp
tmp
TODO.md
package-lock.json


6 changes: 6 additions & 0 deletions default.project.json
@@ -0,0 +1,6 @@
{
"name": "chalk",
"tree": {
"$path": "source"
}
}
6 changes: 6 additions & 0 deletions foreman.toml
@@ -0,0 +1,6 @@
[tools]
luau-lsp = { source = "JohnnyMorganz/luau-lsp", version = "1.18.1" }
rojo = { source = "rojo-rbx/rojo", version = "7.2.1" }
selene = { source = "Kampfkarren/selene", version = "0.25.0" }
stylua = { source = "JohnnyMorganz/StyLua", version = "0.17.1" }
wally = { source = "UpliftGames/wally", version = "=0.3.1" }
7 changes: 7 additions & 0 deletions selene.toml
@@ -0,0 +1,7 @@
std="roblox+testez"
[config]
empty_if = { comments_count = true }

[rules]
incorrect_standard_library_use = "allow"
shadowing = "allow"
186 changes: 186 additions & 0 deletions source/array.lua
@@ -0,0 +1,186 @@
--[[
derived from documentation and reference implementation at:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf
Attributions and copyright licensing by Mozilla Contributors is licensed under CC-BY-SA 2.5

These are heuristcs tested across 300+KLOC of JS translated to Lua
It isn't perfect, but it works for dozens of packages in the React, Jest, and GraphQL ecosystem
In those dozens of packages, hundreds and hundreds of tests were ported to Lua and passing
Additionally, the packages were integrated into a 500+KLOC 60fps product and shipped to 150+M global users
Reimplemented by Matt Hargett, 2023

]]
--!strict
local exports = {}
export type Array<T> = { [number]: T }
type Object = { [string]: any }
exports.indexOf = function<T>(haystack: Array<T>, needle: T, startIndex_: number?): number
local length = #haystack
local startIndex = if startIndex_ == nil
then 1
else if startIndex_ < 1 then math.max(1, length - math.abs(startIndex_)) else startIndex_

for i = startIndex, length, 1 do
if haystack[i] == needle then
return i
end
end

-- we maintain the JS not found value, as it's often checked for explicitly and as < 0
return -1
end

-- we pulled a few refinement ideas from this stackoverflow article, but found:
-- 1. no single one answer worked enough of the time in terms of transliterated JS expectations
-- 2. most had very poor accuracy versus performance tradeoffs
-- https://stackoverflow.com/questions/7526223/how-do-i-know-if-a-table-is-an-array/20958869#20958869
exports.isArray = function(val: any): boolean
if type(val) ~= "table" then
return false
end

if next(val) == nil then
-- it's table with nothing in it, which we express is an array
-- this works 99% of the time for transliterated Lua
return true
end

local tableLength = #val
if tableLength == 0 then
-- getting past the preceding clause says the table isn't an empty iterable
-- if the length of the table is reported as 0, that means it has non-numeric indices
return false
end

-- the slow part, verifying each index is a positive, whole number. a Lua VM built-in would be nice.
for key, _ in pairs(val) do
if type(key) ~= "number" then
return false
end
if key < 1 then
-- Lua arrays start at 1, a 0 index means it's not a pure Lua array
return false
end
-- Lua TODO: would math.floor be faster? needs a benchmark
if (key % 1) ~= 0 then
-- if the number key isn't a whole number, it's not a pure Lua array
return false
end

if key > tableLength then
-- if we get a numeric key larger than the length operator reports, this isn't a contiguous array
return false
end

if key ~= tableLength then
-- if we're not at the end of a contiguous array, the value in the index slot should be non-nil
if nil == val[key + 1] then
return false
end
end
end

return true
end

-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join
exports.join = function<T>(array: Array<T>, separator: string?): string
-- this behavior is relied on by GraphQL and jest
return if 0 == #array
then ""
-- some lua implementations of concat don't behave when passed nil
else table.concat(array, separator or ",")
end

-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
exports.reverse = function(array: Array<any>): Array<any>
local end_ = #array
local start = 1
while start < end_ do
-- this one-liner is a Lua idiom for swapping two values
array[start], array[end_] = array[end_], array[start]
end_ -= 1
start += 1
end

return array
end

-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
exports.slice = function<T>(array: Array<T>, startIndex_: number?, endIndex_: number?): Array<T>
local length = #array

-- JS translates indices > length to be length; in Lua, we translate to length + 1
local endIndex = if (nil == endIndex_ or endIndex_ > length + 1)
then length + 1
else if endIndex_ < 1 then math.max(1, length - math.abs(endIndex_)) else endIndex_

-- JS translates negative indices to 0; in Lua, we translate it to 1
local startIndex = if startIndex_ == nil
then 1
else if startIndex_ < 1 then math.max(1, length - math.abs(startIndex_)) else startIndex_

local i = 1
local index = startIndex
local result = {}
while index < endIndex do
result[i] = array[index]
i += 1
index += 1
end

return result
end

-- this replicates the behavior that mixed Arrays of numbers and strings containing numbers
-- sort the *number* 6 before the *string* 6, and before *userdata* 6, which is relied upon by jest and GraphQL
local function builtinSort<T>(one: T, another: T): boolean
return type(another) .. tostring(another) > type(one) .. tostring(one)
end

-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
type Comparator = (one: any, another: any) -> number
exports.sort = function<T>(array: Array<T>, compare: Comparator?): Array<T>
-- Lua BUG: this is a workaround for a typr solver bug where it says template types aren't compatible with `any`
local translateJsSortReturnToLuaSortReturn: (any, any) -> boolean = if compare == nil
then builtinSort
else function<T>(x: T, y: T): boolean
return 0 > compare(x, y)
end

table.sort(array, translateJsSortReturnToLuaSortReturn)
return array
end

type callbackFunction<T, U> = (value: T, index: number, array: Array<T>) -> U
type callbackFunctionWithSelfArgument<T, U> = (self: Object, value: T, index: number, array: Array<T>) -> U
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
exports.map = function<T, U>(
arr: Array<T>,
callback: callbackFunction<T, U> | callbackFunctionWithSelfArgument<T, U>,
selfArgument: Object?
): Array<U>
local i = 1
local length = #arr
local result = {}

if selfArgument == nil then
while i <= length do
local inputValue = arr[i]
-- Lua BUG: type solver says callback isn't callable, but it is
result[i] = (callback :: callbackFunction<T, U>)(inputValue, i, arr)
i += 1
end
else
while i <= length do
local inputValue = arr[i]
-- Lua BUG: type solver says callback isn't callable, but it is
result[i] = (callback :: callbackFunctionWithSelfArgument<T, U>)(selfArgument, inputValue, i, arr)
i += 1
end
end

return result
end

return exports