Merge branch 'master' into merge

This commit is contained in:
Arseny Kapoulkine 2022-06-30 16:29:59 -07:00
commit 3f716ea007
10 changed files with 1263 additions and 41 deletions

View File

@ -1,10 +1,9 @@
name: Luau Benchmarks
name: benchmark
on:
push:
branches:
- master
paths-ignore:
- "docs/**"
- "papers/**"
@ -13,12 +12,13 @@ on:
- "prototyping/**"
jobs:
benchmarks-run:
name: Run ${{ matrix.bench.title }}
windows:
name: windows-${{matrix.arch}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [windows-latest]
arch: [Win32, x64]
bench:
- {
script: "run-benchmarks",
@ -32,7 +32,84 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Luau
- name: Checkout Luau repository
uses: actions/checkout@v3
- name: Build Luau
shell: bash # necessary for fail-fast
run: |
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . --target Luau.Repl.CLI --config Release
cmake --build . --target Luau.Analyze.CLI --config Release
- name: Move build files to root
run: |
move build/Release/* .
- uses: actions/setup-python@v3
with:
python-version: "3.9"
architecture: "x64"
- name: Install python dependencies
run: |
python -m pip install requests
python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose
- name: Run benchmark
run: |
python bench/bench.py | tee ${{ matrix.bench.script }}-output.txt
- name: Checkout Benchmark Results repository
uses: actions/checkout@v3
with:
repository: ${{ matrix.benchResultsRepo.name }}
ref: ${{ matrix.benchResultsRepo.branch }}
token: ${{ secrets.BENCH_GITHUB_TOKEN }}
path: "./gh-pages"
- name: Store ${{ matrix.bench.title }} result
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:
name: ${{ matrix.bench.title }} (Windows ${{matrix.arch}})
tool: "benchmarkluau"
output-file-path: ./${{ matrix.bench.script }}-output.txt
external-data-json-path: ./gh-pages/dev/bench/data.json
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Push benchmark results
if: github.event_name == 'push'
run: |
echo "Pushing benchmark results..."
cd gh-pages
git config user.name github-actions
git config user.email github@users.noreply.github.com
git add ./dev/bench/data.json
git commit -m "Add benchmarks results for ${{ github.sha }}"
git push
cd ..
unix:
name: ${{matrix.os}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
bench:
- {
script: "run-benchmarks",
timeout: 12,
title: "Luau Benchmarks",
cachegrindTitle: "Performance",
cachegrindIterCount: 20,
}
benchResultsRepo:
- { name: "luau-lang/benchmark-data", branch: "main" }
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Luau repository
uses: actions/checkout@v3
- name: Build Luau
@ -48,18 +125,21 @@ jobs:
python -m pip install requests
python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose
- name: Install valgrind
run: |
sudo apt-get install valgrind
- name: Run benchmark
run: |
python bench/bench.py | tee ${{ matrix.bench.script }}-output.txt
- name: Install valgrind
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install valgrind
- name: Run ${{ matrix.bench.title }} (Cold Cachegrind)
if: matrix.os == 'ubuntu-latest'
run: sudo bash ./scripts/run-with-cachegrind.sh python ./bench/bench.py "${{ matrix.bench.cachegrindTitle}}Cold" 1 | tee -a ${{ matrix.bench.script }}-output.txt
- name: Run ${{ matrix.bench.title }} (Warm Cachegrind)
if: matrix.os == 'ubuntu-latest'
run: sudo bash ./scripts/run-with-cachegrind.sh python ./bench/bench.py "${{ matrix.bench.cachegrindTitle }}" ${{ matrix.bench.cachegrindIterCount }} | tee -a ${{ matrix.bench.script }}-output.txt
- name: Checkout Benchmark Results repository
@ -77,27 +157,108 @@ jobs:
tool: "benchmarkluau"
output-file-path: ./${{ matrix.bench.script }}-output.txt
external-data-json-path: ./gh-pages/dev/bench/data.json
alert-threshold: 150%
fail-threshold: 1000%
fail-on-alert: false
comment-on-alert: true
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BENCH_GITHUB_TOKEN }}
- name: Store ${{ matrix.bench.title }} result
- name: Store ${{ matrix.bench.title }} result (CacheGrind)
if: matrix.os == 'ubuntu-latest'
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:
name: ${{ matrix.bench.title }} (CacheGrind)
tool: "roblox"
output-file-path: ./${{ matrix.bench.script }}-output.txt
external-data-json-path: ./gh-pages/dev/bench/data.json
alert-threshold: 150%
fail-threshold: 1000%
fail-on-alert: false
comment-on-alert: true
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.BENCH_GITHUB_TOKEN }}
- name: Push benchmark results
if: github.event_name == 'push'
run: |
echo "Pushing benchmark results..."
cd gh-pages
git config user.name github-actions
git config user.email github@users.noreply.github.com
git add ./dev/bench/data.json
git commit -m "Add benchmarks results for ${{ github.sha }}"
git push
cd ..
static-analysis:
name: luau-analyze
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
bench:
- {
script: "run-analyze",
timeout: 12,
title: "Luau Analyze",
cachegrindTitle: "Performance",
cachegrindIterCount: 20,
}
benchResultsRepo:
- { name: "luau-lang/benchmark-data", branch: "main" }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
token: "${{ secrets.BENCH_GITHUB_TOKEN }}"
- name: Build Luau
run: make config=release luau luau-analyze
- uses: actions/setup-python@v4
with:
python-version: "3.9"
architecture: "x64"
- name: Install python dependencies
run: |
sudo pip install requests numpy scipy matplotlib ipython jupyter pandas sympy nose
- name: Install valgrind
run: |
sudo apt-get install valgrind
- name: Run Luau Analyze on static file
run: sudo python ./bench/measure_time.py ./build/release/luau-analyze bench/static_analysis/LuauPolyfillMap.lua | tee ${{ matrix.bench.script }}-output.txt
- name: Run ${{ matrix.bench.title }} (Cold Cachegrind)
run: sudo ./scripts/run-with-cachegrind.sh python ./bench/measure_time.py "${{ matrix.bench.cachegrindTitle}}Cold" 1 ./build/release/luau-analyze bench/static_analysis/LuauPolyfillMap.lua | tee -a ${{ matrix.bench.script }}-output.txt
- name: Run ${{ matrix.bench.title }} (Warm Cachegrind)
run: sudo bash ./scripts/run-with-cachegrind.sh python ./bench/measure_time.py "${{ matrix.bench.cachegrindTitle}}" 1 ./build/release/luau-analyze bench/static_analysis/LuauPolyfillMap.lua | tee -a ${{ matrix.bench.script }}-output.txt
- name: Checkout Benchmark Results repository
uses: actions/checkout@v3
with:
repository: ${{ matrix.benchResultsRepo.name }}
ref: ${{ matrix.benchResultsRepo.branch }}
token: ${{ secrets.BENCH_GITHUB_TOKEN }}
path: "./gh-pages"
- name: Store ${{ matrix.bench.title }} result
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:
name: ${{ matrix.bench.title }}
tool: "benchmarkluau"
gh-pages-branch: "main"
output-file-path: ./${{ matrix.bench.script }}-output.txt
external-data-json-path: ./gh-pages/dev/bench/data.json
github-token: ${{ secrets.BENCH_GITHUB_TOKEN }}
- name: Store ${{ matrix.bench.title }} result (CacheGrind)
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:
name: ${{ matrix.bench.title }}
tool: "roblox"
gh-pages-branch: "main"
output-file-path: ./${{ matrix.bench.script }}-output.txt
external-data-json-path: ./gh-pages/dev/bench/data.json
github-token: ${{ secrets.BENCH_GITHUB_TOKEN }}
- name: Push benchmark results
if: github.event_name == 'push'
run: |
echo "Pushing benchmark results..."
cd gh-pages

43
bench/measure_time.py Normal file
View File

@ -0,0 +1,43 @@
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
import os, sys, time, numpy
try:
import scipy
from scipy import mean, stats
except ModuleNotFoundError:
print("Warning: scipy package is not installed, confidence values will not be available")
stats = None
duration_list = []
DEFAULT_CYCLES_TO_RUN = 100
cycles_to_run = DEFAULT_CYCLES_TO_RUN
try:
cycles_to_run = sys.argv[3] if sys.argv[3] else DEFAULT_CYCLES_TO_RUN
cycles_to_run = int(cycles_to_run)
except IndexError:
pass
except (ValueError, TypeError):
cycles_to_run = DEFAULT_CYCLES_TO_RUN
print("Error: Cycles to run argument must be an integer. Using default value of {}".format(DEFAULT_CYCLES_TO_RUN))
# Numpy complains if we provide a cycle count of less than 3 ~ default to 3 whenever a lower value is provided
cycles_to_run = cycles_to_run if cycles_to_run > 2 else 3
for i in range(1,cycles_to_run):
start = time.perf_counter()
# Run the code you want to measure here
os.system(sys.argv[1])
end = time.perf_counter()
duration_ms = (end - start) * 1000
duration_list.append(duration_ms)
# Stats
mean = numpy.mean(duration_list)
std_err = stats.sem(duration_list)
print("SUCCESS: {} : {:.2f}ms +/- {:.2f}% on luau ".format('duration', mean,std_err))

View File

@ -0,0 +1,962 @@
-- This file is part of the Roblox luau-polyfill repository and is licensed under MIT License; see LICENSE.txt for details
--!nonstrict
-- #region Array
-- Array related
local Array = {}
local Object = {}
local Map = {}
type Array<T> = { [number]: T }
type callbackFn<K, V> = (element: V, key: K, map: Map<K, V>) -> ()
type callbackFnWithThisArg<K, V> = (thisArg: Object, value: V, key: K, map: Map<K, V>) -> ()
type Map<K, V> = {
size: number,
-- method definitions
set: (self: Map<K, V>, K, V) -> Map<K, V>,
get: (self: Map<K, V>, K) -> V | nil,
clear: (self: Map<K, V>) -> (),
delete: (self: Map<K, V>, K) -> boolean,
forEach: (self: Map<K, V>, callback: callbackFn<K, V> | callbackFnWithThisArg<K, V>, thisArg: Object?) -> (),
has: (self: Map<K, V>, K) -> boolean,
keys: (self: Map<K, V>) -> Array<K>,
values: (self: Map<K, V>) -> Array<V>,
entries: (self: Map<K, V>) -> Array<Tuple<K, V>>,
ipairs: (self: Map<K, V>) -> any,
[K]: V,
_map: { [K]: V },
_array: { [number]: K },
}
type mapFn<T, U> = (element: T, index: number) -> U
type mapFnWithThisArg<T, U> = (thisArg: any, element: T, index: number) -> U
type Object = { [string]: any }
type Table<T, V> = { [T]: V }
type Tuple<T, V> = Array<T | V>
local Set = {}
-- #region Array
function Array.isArray(value: any): boolean
if typeof(value) ~= "table" then
return false
end
if next(value) == nil then
-- an empty table is an empty array
return true
end
local length = #value
if length == 0 then
return false
end
local count = 0
local sum = 0
for key in pairs(value) do
if typeof(key) ~= "number" then
return false
end
if key % 1 ~= 0 or key < 1 then
return false
end
count += 1
sum += key
end
return sum == (count * (count + 1) / 2)
end
function Array.from<T, U>(
value: string | Array<T> | Object,
mapFn: (mapFn<T, U> | mapFnWithThisArg<T, U>)?,
thisArg: Object?
): Array<U>
if value == nil then
error("cannot create array from a nil value")
end
local valueType = typeof(value)
local array = {}
if valueType == "table" and Array.isArray(value) then
if mapFn then
for i = 1, #(value :: Array<T>) do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, (value :: Array<T>)[i], i)
else
array[i] = (mapFn :: mapFn<T, U>)((value :: Array<T>)[i], i)
end
end
else
for i = 1, #(value :: Array<T>) do
array[i] = (value :: Array<any>)[i]
end
end
elseif instanceOf(value, Set) then
if mapFn then
for i, v in (value :: any):ipairs() do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, v, i)
else
array[i] = (mapFn :: mapFn<T, U>)(v, i)
end
end
else
for i, v in (value :: any):ipairs() do
array[i] = v
end
end
elseif instanceOf(value, Map) then
if mapFn then
for i, v in (value :: any):ipairs() do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, v, i)
else
array[i] = (mapFn :: mapFn<T, U>)(v, i)
end
end
else
for i, v in (value :: any):ipairs() do
array[i] = v
end
end
elseif valueType == "string" then
if mapFn then
for i = 1, (value :: string):len() do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, (value :: any):sub(i, i), i)
else
array[i] = (mapFn :: mapFn<T, U>)((value :: any):sub(i, i), i)
end
end
else
for i = 1, (value :: string):len() do
array[i] = (value :: any):sub(i, i)
end
end
end
return array
end
type callbackFnArrayMap<T, U> = (element: T, index: number, array: Array<T>) -> U
type callbackFnWithThisArgArrayMap<T, U, V> = (thisArg: V, element: T, index: number, array: Array<T>) -> U
-- Implements Javascript's `Array.prototype.map` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
function Array.map<T, U, V>(
t: Array<T>,
callback: callbackFnArrayMap<T, U> | callbackFnWithThisArgArrayMap<T, U, V>,
thisArg: V?
): Array<U>
if typeof(t) ~= "table" then
error(string.format("Array.map called on %s", typeof(t)))
end
if typeof(callback) ~= "function" then
error("callback is not a function")
end
local len = #t
local A = {}
local k = 1
while k <= len do
local kValue = t[k]
if kValue ~= nil then
local mappedValue
if thisArg ~= nil then
mappedValue = (callback :: callbackFnWithThisArgArrayMap<T, U, V>)(thisArg, kValue, k, t)
else
mappedValue = (callback :: callbackFnArrayMap<T, U>)(kValue, k, t)
end
A[k] = mappedValue
end
k += 1
end
return A
end
type Function = (any, any, number, any) -> any
-- Implements Javascript's `Array.prototype.reduce` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
function Array.reduce<T>(array: Array<T>, callback: Function, initialValue: any?): any
if typeof(array) ~= "table" then
error(string.format("Array.reduce called on %s", typeof(array)))
end
if typeof(callback) ~= "function" then
error("callback is not a function")
end
local length = #array
local value
local initial = 1
if initialValue ~= nil then
value = initialValue
else
initial = 2
if length == 0 then
error("reduce of empty array with no initial value")
end
value = array[1]
end
for i = initial, length do
value = callback(value, array[i], i, array)
end
return value
end
type callbackFnArrayForEach<T> = (element: T, index: number, array: Array<T>) -> ()
type callbackFnWithThisArgArrayForEach<T, U> = (thisArg: U, element: T, index: number, array: Array<T>) -> ()
-- Implements Javascript's `Array.prototype.forEach` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
function Array.forEach<T, U>(
t: Array<T>,
callback: callbackFnArrayForEach<T> | callbackFnWithThisArgArrayForEach<T, U>,
thisArg: U?
): ()
if typeof(t) ~= "table" then
error(string.format("Array.forEach called on %s", typeof(t)))
end
if typeof(callback) ~= "function" then
error("callback is not a function")
end
local len = #t
local k = 1
while k <= len do
local kValue = t[k]
if thisArg ~= nil then
(callback :: callbackFnWithThisArgArrayForEach<T, U>)(thisArg, kValue, k, t)
else
(callback :: callbackFnArrayForEach<T>)(kValue, k, t)
end
if #t < len then
-- don't iterate on removed items, don't iterate more than original length
len = #t
end
k += 1
end
end
-- #endregion
-- #region Set
Set.__index = Set
type callbackFnSet<T> = (value: T, key: T, set: Set<T>) -> ()
type callbackFnWithThisArgSet<T> = (thisArg: Object, value: T, key: T, set: Set<T>) -> ()
export type Set<T> = {
size: number,
-- method definitions
add: (self: Set<T>, T) -> Set<T>,
clear: (self: Set<T>) -> (),
delete: (self: Set<T>, T) -> boolean,
forEach: (self: Set<T>, callback: callbackFnSet<T> | callbackFnWithThisArgSet<T>, thisArg: Object?) -> (),
has: (self: Set<T>, T) -> boolean,
ipairs: (self: Set<T>) -> any,
}
type Iterable = { ipairs: (any) -> any }
function Set.new<T>(iterable: Array<T> | Set<T> | Iterable | string | nil): Set<T>
local array = {}
local map = {}
if iterable ~= nil then
local arrayIterable: Array<any>
-- ROBLOX TODO: remove type casting from (iterable :: any).ipairs in next release
if typeof(iterable) == "table" then
if Array.isArray(iterable) then
arrayIterable = Array.from(iterable :: Array<any>)
elseif typeof((iterable :: Iterable).ipairs) == "function" then
-- handle in loop below
elseif _G.__DEV__ then
error("cannot create array from an object-like table")
end
elseif typeof(iterable) == "string" then
arrayIterable = Array.from(iterable :: string)
else
error(("cannot create array from value of type `%s`"):format(typeof(iterable)))
end
if arrayIterable then
for _, element in ipairs(arrayIterable) do
if not map[element] then
map[element] = true
table.insert(array, element)
end
end
elseif typeof(iterable) == "table" and typeof((iterable :: Iterable).ipairs) == "function" then
for _, element in (iterable :: Iterable):ipairs() do
if not map[element] then
map[element] = true
table.insert(array, element)
end
end
end
end
return (setmetatable({
size = #array,
_map = map,
_array = array,
}, Set) :: any) :: Set<T>
end
function Set:add(value)
if not self._map[value] then
-- Luau FIXME: analyze should know self is Set<T> which includes size as a number
self.size = self.size :: number + 1
self._map[value] = true
table.insert(self._array, value)
end
return self
end
function Set:clear()
self.size = 0
table.clear(self._map)
table.clear(self._array)
end
function Set:delete(value): boolean
if not self._map[value] then
return false
end
-- Luau FIXME: analyze should know self is Map<K, V> which includes size as a number
self.size = self.size :: number - 1
self._map[value] = nil
local index = table.find(self._array, value)
if index then
table.remove(self._array, index)
end
return true
end
-- Implements Javascript's `Map.prototype.forEach` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/forEach
function Set:forEach<T>(callback: callbackFnSet<T> | callbackFnWithThisArgSet<T>, thisArg: Object?): ()
if typeof(callback) ~= "function" then
error("callback is not a function")
end
return Array.forEach(self._array, function(value: T)
if thisArg ~= nil then
(callback :: callbackFnWithThisArgSet<T>)(thisArg, value, value, self)
else
(callback :: callbackFnSet<T>)(value, value, self)
end
end)
end
function Set:has(value): boolean
return self._map[value] ~= nil
end
function Set:ipairs()
return ipairs(self._array)
end
-- #endregion Set
-- #region Object
function Object.entries(value: string | Object | Array<any>): Array<any>
assert(value :: any ~= nil, "cannot get entries from a nil value")
local valueType = typeof(value)
local entries: Array<Tuple<string, any>> = {}
if valueType == "table" then
for key, keyValue in pairs(value :: Object) do
-- Luau FIXME: Luau should see entries as Array<any>, given object is [string]: any, but it sees it as Array<Array<string>> despite all the manual annotation
table.insert(entries, { key :: string, keyValue :: any })
end
elseif valueType == "string" then
for i = 1, string.len(value :: string) do
entries[i] = { tostring(i), string.sub(value :: string, i, i) }
end
end
return entries
end
-- #endregion
-- #region instanceOf
-- ROBLOX note: Typed tbl as any to work with strict type analyze
-- polyfill for https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof
function instanceOf(tbl: any, class)
assert(typeof(class) == "table", "Received a non-table as the second argument for instanceof")
if typeof(tbl) ~= "table" then
return false
end
local ok, hasNew = pcall(function()
return class.new ~= nil and tbl.new == class.new
end)
if ok and hasNew then
return true
end
local seen = { tbl = true }
while tbl and typeof(tbl) == "table" do
tbl = getmetatable(tbl)
if typeof(tbl) == "table" then
tbl = tbl.__index
if tbl == class then
return true
end
end
-- if we still have a valid table then check against seen
if typeof(tbl) == "table" then
if seen[tbl] then
return false
end
seen[tbl] = true
end
end
return false
end
-- #endregion
function Map.new<K, V>(iterable: Array<Array<any>>?): Map<K, V>
local array = {}
local map = {}
if iterable ~= nil then
local arrayFromIterable
local iterableType = typeof(iterable)
if iterableType == "table" then
if #iterable > 0 and typeof(iterable[1]) ~= "table" then
error("cannot create Map from {K, V} form, it must be { {K, V}... }")
end
arrayFromIterable = Array.from(iterable)
else
error(("cannot create array from value of type `%s`"):format(iterableType))
end
for _, entry in ipairs(arrayFromIterable) do
local key = entry[1]
if _G.__DEV__ then
if key == nil then
error("cannot create Map from a table that isn't an array.")
end
end
local val = entry[2]
-- only add to array if new
if map[key] == nil then
table.insert(array, key)
end
-- always assign
map[key] = val
end
end
return (setmetatable({
size = #array,
_map = map,
_array = array,
}, Map) :: any) :: Map<K, V>
end
function Map:set<K, V>(key: K, value: V): Map<K, V>
-- preserve initial insertion order
if self._map[key] == nil then
-- Luau FIXME: analyze should know self is Map<K, V> which includes size as a number
self.size = self.size :: number + 1
table.insert(self._array, key)
end
-- always update value
self._map[key] = value
return self
end
function Map:get(key)
return self._map[key]
end
function Map:clear()
local table_: any = table
self.size = 0
table_.clear(self._map)
table_.clear(self._array)
end
function Map:delete(key): boolean
if self._map[key] == nil then
return false
end
-- Luau FIXME: analyze should know self is Map<K, V> which includes size as a number
self.size = self.size :: number - 1
self._map[key] = nil
local index = table.find(self._array, key)
if index then
table.remove(self._array, index)
end
return true
end
-- Implements Javascript's `Map.prototype.forEach` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
function Map:forEach<K, V>(callback: callbackFn<K, V> | callbackFnWithThisArg<K, V>, thisArg: Object?): ()
if typeof(callback) ~= "function" then
error("callback is not a function")
end
return Array.forEach(self._array, function(key: K)
local value: V = self._map[key] :: V
if thisArg ~= nil then
(callback :: callbackFnWithThisArg<K, V>)(thisArg, value, key, self)
else
(callback :: callbackFn<K, V>)(value, key, self)
end
end)
end
function Map:has(key): boolean
return self._map[key] ~= nil
end
function Map:keys()
return self._array
end
function Map:values()
return Array.map(self._array, function(key)
return self._map[key]
end)
end
function Map:entries()
return Array.map(self._array, function(key)
return { key, self._map[key] }
end)
end
function Map:ipairs()
return ipairs(self:entries())
end
function Map.__index(self, key)
local mapProp = rawget(Map, key)
if mapProp ~= nil then
return mapProp
end
return Map.get(self, key)
end
function Map.__newindex(table_, key, value)
table_:set(key, value)
end
local function coerceToMap(mapLike: Map<any, any> | Table<any, any>): Map<any, any>
return instanceOf(mapLike, Map) and mapLike :: Map<any, any> -- ROBLOX: order is preservered
or Map.new(Object.entries(mapLike)) -- ROBLOX: order is not preserved
end
-- local function coerceToTable(mapLike: Map<any, any> | Table<any, any>): Table<any, any>
-- if not instanceOf(mapLike, Map) then
-- return mapLike
-- end
-- -- create table from map
-- return Array.reduce(mapLike:entries(), function(tbl, entry)
-- tbl[entry[1]] = entry[2]
-- return tbl
-- end, {})
-- end
-- #region Tests to verify it works as expected
local function it(description: string, fn: () -> ())
local ok, result = pcall(fn)
if not ok then
error("Failed test: " .. description .. "\n" .. result)
end
end
local AN_ITEM = "bar"
local ANOTHER_ITEM = "baz"
-- #region [Describe] "Map"
-- #region [Child Describe] "constructors"
it("creates an empty array", function()
local foo = Map.new()
assert(foo.size == 0)
end)
it("creates a Map from an array", function()
local foo = Map.new({
{ AN_ITEM, "foo" },
{ ANOTHER_ITEM, "val" },
})
assert(foo.size == 2)
assert(foo:has(AN_ITEM) == true)
assert(foo:has(ANOTHER_ITEM) == true)
end)
it("creates a Map from an array with duplicate keys", function()
local foo = Map.new({
{ AN_ITEM, "foo1" },
{ AN_ITEM, "foo2" },
})
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == "foo2")
assert(#foo:keys() == 1 and foo:keys()[1] == AN_ITEM)
assert(#foo:values() == 1 and foo:values()[1] == "foo2")
assert(#foo:entries() == 1)
assert(#foo:entries()[1] == 2)
assert(foo:entries()[1][1] == AN_ITEM)
assert(foo:entries()[1][2] == "foo2")
end)
it("preserves the order of keys first assignment", function()
local foo = Map.new({
{ AN_ITEM, "foo1" },
{ ANOTHER_ITEM, "bar" },
{ AN_ITEM, "foo2" },
})
assert(foo.size == 2)
assert(foo:get(AN_ITEM) == "foo2")
assert(foo:get(ANOTHER_ITEM) == "bar")
assert(foo:keys()[1] == AN_ITEM)
assert(foo:keys()[2] == ANOTHER_ITEM)
assert(foo:values()[1] == "foo2")
assert(foo:values()[2] == "bar")
assert(foo:entries()[1][1] == AN_ITEM)
assert(foo:entries()[1][2] == "foo2")
assert(foo:entries()[2][1] == ANOTHER_ITEM)
assert(foo:entries()[2][2] == "bar")
end)
-- #endregion
-- #region [Child Describe] "type"
it("instanceOf return true for an actual Map object", function()
local foo = Map.new()
assert(instanceOf(foo, Map) == true)
end)
it("instanceOf return false for an regular plain object", function()
local foo = {}
assert(instanceOf(foo, Map) == false)
end)
-- #endregion
-- #region [Child Describe] "set"
it("returns the Map object", function()
local foo = Map.new()
assert(foo:set(1, "baz") == foo)
end)
it("increments the size if the element is added for the first time", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo.size == 1)
end)
it("does not increment the size the second time an element is added", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(AN_ITEM, "val")
assert(foo.size == 1)
end)
it("sets values correctly to true/false", function()
-- Luau FIXME: Luau insists that arrays can't be mixed type
local foo = Map.new({ { AN_ITEM, false :: any } })
foo:set(AN_ITEM, false)
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == false)
foo:set(AN_ITEM, true)
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == true)
foo:set(AN_ITEM, false)
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == false)
end)
-- #endregion
-- #region [Child Describe] "get"
it("returns value of item from provided key", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo:get(AN_ITEM) == "foo")
end)
it("returns nil if the item is not in the Map", function()
local foo = Map.new()
assert(foo:get(AN_ITEM) == nil)
end)
-- #endregion
-- #region [Child Describe] "clear"
it("sets the size to zero", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:clear()
assert(foo.size == 0)
end)
it("removes the items from the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:clear()
assert(foo:has(AN_ITEM) == false)
end)
-- #endregion
-- #region [Child Describe] "delete"
it("removes the items from the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:delete(AN_ITEM)
assert(foo:has(AN_ITEM) == false)
end)
it("returns true if the item was in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo:delete(AN_ITEM) == true)
end)
it("returns false if the item was not in the Map", function()
local foo = Map.new()
assert(foo:delete(AN_ITEM) == false)
end)
it("decrements the size if the item was in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:delete(AN_ITEM)
assert(foo.size == 0)
end)
it("does not decrement the size if the item was not in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:delete(ANOTHER_ITEM)
assert(foo.size == 1)
end)
it("deletes value set to false", function()
-- Luau FIXME: Luau insists arrays can't be mixed type
local foo = Map.new({ { AN_ITEM, false :: any } })
foo:delete(AN_ITEM)
assert(foo.size == 0)
assert(foo:get(AN_ITEM) == nil)
end)
-- #endregion
-- #region [Child Describe] "has"
it("returns true if the item is in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo:has(AN_ITEM) == true)
end)
it("returns false if the item is not in the Map", function()
local foo = Map.new()
assert(foo:has(AN_ITEM) == false)
end)
it("returns correctly with value set to false", function()
-- Luau FIXME: Luau insists arrays can't be mixed type
local foo = Map.new({ { AN_ITEM, false :: any } })
assert(foo:has(AN_ITEM) == true)
end)
-- #endregion
-- #region [Child Describe] "keys / values / entries"
it("returns array of elements", function()
local myMap = Map.new()
myMap:set(AN_ITEM, "foo")
myMap:set(ANOTHER_ITEM, "val")
assert(myMap:keys()[1] == AN_ITEM)
assert(myMap:keys()[2] == ANOTHER_ITEM)
assert(myMap:values()[1] == "foo")
assert(myMap:values()[2] == "val")
assert(myMap:entries()[1][1] == AN_ITEM)
assert(myMap:entries()[1][2] == "foo")
assert(myMap:entries()[2][1] == ANOTHER_ITEM)
assert(myMap:entries()[2][2] == "val")
end)
-- #endregion
-- #region [Child Describe] "__index"
it("can access fields directly without using get", function()
local typeName = "size"
local foo = Map.new({
{ AN_ITEM, "foo" },
{ ANOTHER_ITEM, "val" },
{ typeName, "buzz" },
})
assert(foo.size == 3)
assert(foo[AN_ITEM] == "foo")
assert(foo[ANOTHER_ITEM] == "val")
assert(foo:get(typeName) == "buzz")
end)
-- #endregion
-- #region [Child Describe] "__newindex"
it("can set fields directly without using set", function()
local foo = Map.new()
assert(foo.size == 0)
foo[AN_ITEM] = "foo"
foo[ANOTHER_ITEM] = "val"
foo.fizz = "buzz"
assert(foo.size == 3)
assert(foo:get(AN_ITEM) == "foo")
assert(foo:get(ANOTHER_ITEM) == "val")
assert(foo:get("fizz") == "buzz")
end)
-- #endregion
-- #region [Child Describe] "ipairs"
local function makeArray(...)
local array = {}
for _, item in ... do
table.insert(array, item)
end
return array
end
it("iterates on the elements by their insertion order", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(ANOTHER_ITEM, "val")
assert(makeArray(foo:ipairs())[1][1] == AN_ITEM)
assert(makeArray(foo:ipairs())[1][2] == "foo")
assert(makeArray(foo:ipairs())[2][1] == ANOTHER_ITEM)
assert(makeArray(foo:ipairs())[2][2] == "val")
end)
it("does not iterate on removed elements", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(ANOTHER_ITEM, "val")
foo:delete(AN_ITEM)
assert(makeArray(foo:ipairs())[1][1] == ANOTHER_ITEM)
assert(makeArray(foo:ipairs())[1][2] == "val")
end)
it("iterates on elements if the added back to the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(ANOTHER_ITEM, "val")
foo:delete(AN_ITEM)
foo:set(AN_ITEM, "food")
assert(makeArray(foo:ipairs())[1][1] == ANOTHER_ITEM)
assert(makeArray(foo:ipairs())[1][2] == "val")
assert(makeArray(foo:ipairs())[2][1] == AN_ITEM)
assert(makeArray(foo:ipairs())[2][2] == "food")
end)
-- #endregion
-- #region [Child Describe] "Integration Tests"
-- it("MDN Examples", function()
-- local myMap = Map.new() :: Map<string | Object | Function, string>
-- local keyString = "a string"
-- local keyObj = {}
-- local keyFunc = function() end
-- -- setting the values
-- myMap:set(keyString, "value associated with 'a string'")
-- myMap:set(keyObj, "value associated with keyObj")
-- myMap:set(keyFunc, "value associated with keyFunc")
-- assert(myMap.size == 3)
-- -- getting the values
-- assert(myMap:get(keyString) == "value associated with 'a string'")
-- assert(myMap:get(keyObj) == "value associated with keyObj")
-- assert(myMap:get(keyFunc) == "value associated with keyFunc")
-- assert(myMap:get("a string") == "value associated with 'a string'")
-- assert(myMap:get({}) == nil) -- nil, because keyObj !== {}
-- assert(myMap:get(function() -- nil because keyFunc !== function () {}
-- end) == nil)
-- end)
it("handles non-traditional keys", function()
local myMap = Map.new() :: Map<boolean | number | string, string>
local falseKey = false
local trueKey = true
local negativeKey = -1
local emptyKey = ""
myMap:set(falseKey, "apple")
myMap:set(trueKey, "bear")
myMap:set(negativeKey, "corgi")
myMap:set(emptyKey, "doge")
assert(myMap.size == 4)
assert(myMap:get(falseKey) == "apple")
assert(myMap:get(trueKey) == "bear")
assert(myMap:get(negativeKey) == "corgi")
assert(myMap:get(emptyKey) == "doge")
myMap:delete(falseKey)
myMap:delete(trueKey)
myMap:delete(negativeKey)
myMap:delete(emptyKey)
assert(myMap.size == 0)
end)
-- #endregion
-- #endregion [Describe] "Map"
-- #region [Describe] "coerceToMap"
it("returns the same object if instance of Map", function()
local map = Map.new()
assert(coerceToMap(map) == map)
map = Map.new({})
assert(coerceToMap(map) == map)
map = Map.new({ { AN_ITEM, "foo" } })
assert(coerceToMap(map) == map)
end)
-- #endregion [Describe] "coerceToMap"
-- #endregion Tests to verify it works as expected

View File

@ -49,20 +49,20 @@ Sandboxing challenges are [covered in the dedicated section](sandbox).
|---------|--------|------|
| yieldable pcall/xpcall | ✔️ | |
| yieldable metamethods | ❌ | significant performance implications |
| ephemeron tables | ❌ | this complicates the garbage collector esp. for large weak tables |
| emergency garbage collector | | Luau runs in environments where handling memory exhaustion in emergency situations is not tenable |
| ephemeron tables | ❌ | this complicates and slows down the garbage collector esp. for large weak tables |
| emergency garbage collector | 🤷‍ | Luau runs in environments where handling memory exhaustion in emergency situations is not tenable |
| goto statement | ❌ | this complicates the compiler, makes control flow unstructured and doesn't address a significant need |
| finalizers for tables | ❌ | no `__gc` support due to sandboxing and performance/complexity |
| no more fenv for threads or functions | 😞 | we love this, but it breaks compatibility |
| tables honor the `__len` metamethod | 🤷‍♀️ | performance implications, no strong use cases
| hex and `\z` escapes in strings | ✔️ | |
| support for hexadecimal floats | 🤷‍♀️ | no strong use cases |
| order metamethods work for different types | ❌ | no strong use cases and more complicated semantics + compat |
| order metamethods work for different types | ❌ | no strong use cases and more complicated semantics, compatibility and performance implications |
| empty statement | 🤷‍♀️ | less useful in Lua than in JS/C#/C/C++ |
| `break` statement may appear in the middle of a block | 🤷‍♀️ | we'd like to do it for return/continue as well but there be dragons |
| `break` statement may appear in the middle of a block | 🤷‍♀️ | we'd like to do it consistently for `break`/`return`/`continue` but there be dragons |
| arguments for function called through `xpcall` | ✔️ | |
| optional base in `math.log` | ✔️ | |
| optional separator in `string.rep` | 🤷‍♀️ | no real use cases |
| optional separator in `string.rep` | 🤷‍♀️ | no strong use cases |
| new metamethods `__pairs` and `__ipairs` | ❌ | superseded by `__iter` |
| frontier patterns | ✔️ | |
| `%g` in patterns | ✔️ | |
@ -83,7 +83,7 @@ Ephemeron tables may be implemented at some point since they do have valid uses
|---------|--------|------|
| `\u` escapes in strings | ✔️ | |
| integers (64-bit by default) | ❌ | backwards compatibility and performance implications |
| bitwise operators | ❌ | `bit32` library covers this |
| bitwise operators | ❌ | `bit32` library covers this in absence of 64-bit integers |
| basic utf-8 support | ✔️ | we include `utf8` library and other UTF8 features |
| functions for packing and unpacking values (string.pack/unpack/packsize) | ✔️ | |
| floor division | ❌ | no strong use cases, syntax overlaps with C comments |
@ -95,16 +95,16 @@ Ephemeron tables may be implemented at some point since they do have valid uses
It's important to highlight integer support and bitwise operators. For Luau, it's rare that a full 64-bit integer type is necessary - double-precision types support integers up to 2^53 (in Lua which is used in embedded space, integers may be more appealing in environments without a native 64-bit FPU). However, there's a *lot* of value in having a single number type, both from performance perspective and for consistency. Notably, Lua doesn't handle integer overflow properly, so using integers also carries compatibility implications.
If integers are taken out of the equation, bitwise operators make much less sense; additionally, `bit32` library is more fully featured (includes commonly used operations such as rotates and arithmetic shift; bit extraction/replacement is also more readable). Adding operators along with metamethods for all of them increases complexity, which means this feature isn't worth it on the balance.
If integers are taken out of the equation, bitwise operators make less sense, as integers aren't a first class feature; additionally, `bit32` library is more fully featured (includes commonly used operations such as rotates and arithmetic shift; bit extraction/replacement is also more readable). Adding operators along with metamethods for all of them increases complexity, which means this feature isn't worth it on the balance. Common arguments for this include a more familiar syntax, which, while true, gets more nuanced as `^` isn't available as a xor operator, and arithmetic right shift isn't expressible without yet another operator, and performance, which in Luau is substantially better than in Lua because `bit32` library uses VM builtins instead of expensive function calls.
Floor division is less harmful, but it's used rarely enough that `math.floor(a/b)` seems like an adequate replacement; additionally, `//` is a comment in C-derived languages and we may decide to adopt it in addition to `--` at some point.
Floor division is much less complex, but it's used rarely enough that `math.floor(a/b)` seems like an adequate replacement; additionally, `//` is a comment in C-derived languages and we may decide to adopt it in addition to `--` at some point.
## Lua 5.4
| feature | status | notes |
|--|--|--|
| new generational mode for garbage collection | 🔜 | we're working on gc optimizations and generational mode is on our radar
| to-be-closed variables | ❌ | the syntax is ugly and inconsistent with how we'd like to do attributes long-term; no strong use cases in our domain |
| to-be-closed variables | ❌ | the syntax is inconsistent with how we'd like to do attributes long-term; no strong use cases in our domain |
| const variables | ❌ | while there's some demand for const variables, we'd never adopt this syntax |
| new implementation for math.random | ✔️ | our RNG is based on PCG, unlike Lua 5.4 which uses Xoroshiro |
| optional `init` argument to `string.gmatch` | 🤷‍♀️ | no strong use cases |
@ -112,14 +112,14 @@ Floor division is less harmful, but it's used rarely enough that `math.floor(a/b
| coercions string-to-number moved to the string library | 😞 | we love this, but it breaks compatibility |
| new format `%p` in `string.format` | 🤷‍♀️ | no strong use cases |
| `utf8` library accepts codepoints up to 2^31 | 🤷‍♀️ | no strong use cases |
| The use of the `__lt` metamethod to emulate `__le` has been removed | 😞 | breaks compatibility and doesn't seem very interesting otherwise |
| The use of the `__lt` metamethod to emulate `__le` has been removed | ❌ | breaks compatibility and complicates comparison overloading story |
| When finalizing objects, Lua will call `__gc` metamethods that are not functions | ❌ | no `__gc` support due to sandboxing and performance/complexity |
| The function print calls `__tostring` instead of tostring to format its arguments. | ✔️ | |
| By default, the decoding functions in the utf8 library do not accept surrogates. | 😞 | breaks compatibility and doesn't seem very interesting otherwise |
Lua has a beautiful syntax and frankly we're disappointed in the `<const>`/`<close>` which takes away from that beauty. Taking syntax aside, `<close>` isn't very useful in Luau - its dominant use case is for code that works with external resources like files or sockets, but we don't provide such APIs - and has a very large complexity cost, evidences by a lot of bug fixes since the initial implementation in 5.4 work versions. `<const>` in Luau doesn't matter for performance - our multi-pass compiler is already able to analyze the usage of the variable to know if it's modified or not and extract all performance gains from it - so the only use here is for code readability, where the `<const>` syntax is... suboptimal.
Taking syntax aside (which doesn't feel idiomatic or beautiful), `<close>` isn't very useful in Luau - its dominant use case is for code that works with external resources like files or sockets, but we don't provide such APIs - and has a very large complexity cost, evidences by a lot of bug fixes since the initial implementation in 5.4 work versions. `<const>` in Luau doesn't matter for performance - our multi-pass compiler is already able to analyze the usage of the variable to know if it's modified or not and extract all performance gains from it - so the only use here is for code readability, where the `<const>` syntax is... suboptimal.
If we do end up introducing const variables, it would be through a `const var = value` syntax, which is backwards compatible through a context-sensitive keyword similar to `type`.
If we do end up introducing const variables, it would be through a `const var = value` syntax, which is backwards compatible through a context-sensitive keyword similar to `type`. That said, there's ambiguity wrt whether `const` should simply behave like a read-only variable, ala JavaScript, or if it should represent a stronger contract, for example by limiting the expressions on the right hand side to ones compiler can evaluate ahead of time, or by freezing table values and thus guaranteeing immutability.
## Differences from Lua

View File

@ -647,7 +647,7 @@ All functions in the `bit32` library treat input numbers as 32-bit unsigned inte
function bit32.arshift(n: number, i: number): number
```
Shifts `n` by `i` bits to the right (if `i` is negative, a left shift is performed instead). The most significant bit of `n` is propagated during the shift.
Shifts `n` by `i` bits to the right (if `i` is negative, a left shift is performed instead). The most significant bit of `n` is propagated during the shift. When `i` is larger than 31, returns an integer with all bits set to the sign bit of `n`. When `i` is smaller than `-31`, 0 is returned.
```
function bit32.band(args: ...number): number
@ -695,7 +695,7 @@ Rotates `n` to the left by `i` bits (if `i` is negative, a right rotate is perfo
function bit32.lshift(n: number, i: number): number
```
Shifts `n` to the left by `i` bits (if `i` is negative, a right shift is performed instead).
Shifts `n` to the left by `i` bits (if `i` is negative, a right shift is performed instead). When `i` is outside of `[-31..31]` range, returns 0.
```
function bit32.replace(n: number, r: number, f: number, w: number?): number
@ -713,7 +713,7 @@ Rotates `n` to the right by `i` bits (if `i` is negative, a left rotate is perfo
function bit32.rshift(n: number, i: number): number
```
Shifts `n` to the right by `i` bits (if `i` is negative, a left shift is performed instead).
Shifts `n` to the right by `i` bits (if `i` is negative, a left shift is performed instead). When `i` is outside of `[-31..31]` range, returns 0.
```
function bit32.countlz(n: number): number

View File

@ -4,11 +4,11 @@ title: Sandboxing
toc: true
---
Luau is safe to embed. Broadly speaking, this means that even in the face of untrusted (and in Roblox case, actively malicious) code, the language and the standard library don't allow any unsafe access to the underlying system, and don't have any bugs that allow escaping out of the sandbox (e.g. to gain native code execution through ROP gadgets et al). Additionally, the VM provides extra features to implement isolation of privileged code from unprivileged code and protect one from the other; this is important if the embedding environment decides to expose some APIs that may not be safe to call from untrusted code, for example because they do provide controlled access to the underlying system or risk PII exposure through fingerprinting etc.
Luau is safe to embed. Broadly speaking, this means that even in the face of untrusted (and in Roblox case, actively malicious) code, the language and the standard library don't allow unsafe access to the underlying system, and don't have known bugs that allow escaping out of the sandbox (e.g. to gain native code execution through ROP gadgets et al). Additionally, the VM provides extra features to implement isolation of privileged code from unprivileged code and protect one from the other; this is important if the embedding environment decides to expose some APIs that may not be safe to call from untrusted code, for example because they do provide controlled access to the underlying system or risk PII exposure through fingerprinting etc.
This safety is achieved through a combination of removing features from the standard library that are unsafe, adding features to the VM that make it possible to implement sandboxing and isolation, and making sure the implementation is safe from memory safety issues using fuzzing.
Of course, since the entire stack is implemented in C++, the sandboxing isn't formally proven - in theory, compiler or the standard library can have exploitable vulnerabilities. In practice these are usually found and fixed quickly. While implementing the stack in a safer language such as Rust would make it easier to provide these guarantees, to our knowledge (based on prior art) this would make it difficult to reach the level of performance required.
Of course, since the entire stack is implemented in C++, the sandboxing isn't formally proven - in theory, compiler or the standard library can have exploitable vulnerabilities. In practice these are very rare and usually found and fixed quickly. While implementing the stack in a safer language such as Rust would make it easier to provide these guarantees, to our knowledge (based on prior art) this would make it difficult to reach the level of performance required.
## Library

View File

@ -48,7 +48,7 @@ local b2: B = a1 -- not ok
## Primitive types
Lua VM supports 8 primitive types: `nil`, `string`, `number`, `boolean`, `table`, `function`, `thread`, and `userdata`. Of these, `table` and `function` are not represented by name, but have their dedicated syntax as covered in this [syntax document](syntax), and `userdata` is represented by [concrete types](#Roblox-types); other types can be specified by their name.
Lua VM supports 8 primitive types: `nil`, `string`, `number`, `boolean`, `table`, `function`, `thread`, and `userdata`. Of these, `table` and `function` are not represented by name, but have their dedicated syntax as covered in this [syntax document](syntax), and `userdata` is represented by [concrete types](#roblox-types); other types can be specified by their name.
Additionally, we also have `any` which is a special built-in type. It effectively disables all type checking, and thus should be used as last resort.

View File

@ -32,3 +32,9 @@ This document tracks unimplemented RFCs.
[RFC: never and unknown types](https://github.com/Roblox/luau/blob/master/rfcs/never-and-unknown-types.md)
**Status**: Needs implementation
## __len metamethod for tables and rawlen function
[RFC: Support __len metamethod for tables and rawlen function](https://github.com/Roblox/luau/blob/master/rfcs/len-metamethod-rawlen.md)
**Status**: Needs implementation

View File

@ -0,0 +1,43 @@
# Support `__len` metamethod for tables and `rawlen` function
## Summary
`__len` metamethod will be called by `#` operator on tables, matching Lua 5.2
## Motivation
Lua 5.1 invokes `__len` only on userdata objects, whereas Lua 5.2 extends this to tables. In addition to making `__len` metamethod more uniform and making Luau
more compatible with later versions of Lua, this has the important advantage which is that it makes it possible to implement an index based container.
Before `__iter` and `__len` it was possible to implement a custom container using `__index`/`__newindex`, but to iterate through the container a custom function was
necessary, because Luau didn't support generalized iteration, `__pairs`/`__ipairs` from Lua 5.2, or `#` override.
With generalized iteration, a custom container can implement its own iteration behavior so as long as code uses `for k,v in obj` iteration style, the container can
be interfaced with the same way as a table. However, when the container uses integer indices, manual iteration via `#` would still not work - which is required for some
more complicated algorithms, or even to simply iterate through the container backwards.
Supporting `__len` would make it possible to implement a custom integer based container that exposes the same interface as a table does.
## Design
`#v` will call `__len` metamethod if the object is a table and the metamethod exists; the result of the metamethod will be returned if it's a number (an error will be raised otherwise).
`table.` functions that implicitly compute table length, such as `table.getn`, `table.insert`, will continue using the actual table length. This is consistent with the
general policy that Luau doesn't support metamethods in `table.` functions.
A new function, `rawlen(v)`, will be added to the standard library; given a string or a table, it will return the length of the object without calling any metamethods.
The new function has the previous behavior of `#` operator with the exception of not supporting userdata inputs, as userdata doesn't have an inherent definition of length.
## Drawbacks
`#` is an operator that is used frequently and as such an extra metatable check here may impact performance. However, `#` is usually called on tables without metatables,
and even when it is, using the existing metamethod-absence-caching approach we use for many other metamethods a test version of the change to support `__len` shows no
statistically significant difference on existing benchmark suite. This does complicate the `#` computation a little more which may affect JIT as well, but even if the
table doesn't have a metatable the process of computing `#` involves a series of condition checks and as such will likely require slow paths anyway.
This is technically changing semantics of `#` when called on tables with an existing `__len` metamethod, and as such has a potential to change behavior of an existing valid program.
That said, it's unlikely that any table would have a metatable with `__len` metamethod as outside of userdata it would not anything, and this drawback is not feasible to resolve with any alternate version of the proposal.
## Alternatives
Do not implement `__len`.

View File

@ -25,10 +25,17 @@ now_ms() {
ITERATION_COUNT=$4
START_TIME=$(now_ms)
ARGS=( "$@" )
REST_ARGS="${ARGS[@]:4}"
valgrind \
--quiet \
--tool=cachegrind \
"$1" "$2" >/dev/null
"$1" "$2" $REST_ARGS>/dev/null
ARGS=( "$@" )
REST_ARGS="${ARGS[@]:4}"
TIME_ELAPSED=$(bc <<< "$(now_ms) - ${START_TIME}")