mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 06:15:44 +08:00
Added multi-os runners for benchmark & implemented luau analyze (#542)
This commit is contained in:
parent
e91d80ee25
commit
5e405b58b3
217
.github/workflows/benchmark.yml
vendored
217
.github/workflows/benchmark.yml
vendored
@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "papers/**"
|
||||
@ -13,12 +12,13 @@ on:
|
||||
- "prototyping/**"
|
||||
|
||||
jobs:
|
||||
benchmarks-run:
|
||||
name: Run ${{ matrix.bench.title }}
|
||||
windows:
|
||||
name: Run ${{ matrix.bench.title }} (Windows ${{matrix.arch}})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [windows-latest]
|
||||
arch: [Win32, x64]
|
||||
bench:
|
||||
- {
|
||||
script: "run-benchmarks",
|
||||
@ -32,7 +32,93 @@ 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/RelWithDebInfo/* .
|
||||
|
||||
- name: Check dir structure
|
||||
run: |
|
||||
ls build/RelWithDebInfo
|
||||
ls
|
||||
- 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
|
||||
alert-threshold: 150%
|
||||
fail-threshold: 200%
|
||||
fail-on-alert: true
|
||||
comment-on-alert: true
|
||||
comment-always: true
|
||||
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: Run ${{ matrix.bench.title }} (${{ 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 +134,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
|
||||
@ -78,12 +167,14 @@ jobs:
|
||||
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
|
||||
fail-threshold: 200%
|
||||
fail-on-alert: true
|
||||
comment-on-alert: true
|
||||
comment-always: true
|
||||
github-token: ${{ secrets.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)
|
||||
@ -97,7 +188,107 @@ jobs:
|
||||
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 ..
|
||||
|
||||
static-analysis:
|
||||
name: Run ${{ matrix.bench.title }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
engine:
|
||||
- { channel: stable, version: 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
|
||||
alert-threshold: 150%
|
||||
fail-threshold: 200%
|
||||
fail-on-alert: true
|
||||
comment-on-alert: true
|
||||
comment-always: true
|
||||
github-token: ${{ secrets.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
|
||||
alert-threshold: 150%
|
||||
fail-threshold: 200%
|
||||
fail-on-alert: true
|
||||
comment-on-alert: true
|
||||
comment-always: true
|
||||
github-token: ${{ secrets.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
43
bench/measure_time.py
Normal 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))
|
962
bench/static_analysis/LuauPolyfillMap.lua
Normal file
962
bench/static_analysis/LuauPolyfillMap.lua
Normal 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
|
@ -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}")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user