mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-13 13:30:40 +00:00
Added multi-os runners for benchmark & implemented luau analyze (#542)
This commit is contained in:
parent
e91d80ee25
commit
5e405b58b3
4 changed files with 1217 additions and 14 deletions
217
.github/workflows/benchmark.yml
vendored
217
.github/workflows/benchmark.yml
vendored
|
@ -4,7 +4,6 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "papers/**"
|
- "papers/**"
|
||||||
|
@ -13,12 +12,13 @@ on:
|
||||||
- "prototyping/**"
|
- "prototyping/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
benchmarks-run:
|
windows:
|
||||||
name: Run ${{ matrix.bench.title }}
|
name: Run ${{ matrix.bench.title }} (Windows ${{matrix.arch}})
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [windows-latest]
|
||||||
|
arch: [Win32, x64]
|
||||||
bench:
|
bench:
|
||||||
- {
|
- {
|
||||||
script: "run-benchmarks",
|
script: "run-benchmarks",
|
||||||
|
@ -32,7 +32,93 @@ jobs:
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
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
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build Luau
|
- name: Build Luau
|
||||||
|
@ -48,18 +134,21 @@ jobs:
|
||||||
python -m pip install requests
|
python -m pip install requests
|
||||||
python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose
|
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
|
- name: Run benchmark
|
||||||
run: |
|
run: |
|
||||||
python bench/bench.py | tee ${{ matrix.bench.script }}-output.txt
|
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)
|
- 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
|
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)
|
- 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
|
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
|
- name: Checkout Benchmark Results repository
|
||||||
|
@ -78,12 +167,14 @@ jobs:
|
||||||
output-file-path: ./${{ matrix.bench.script }}-output.txt
|
output-file-path: ./${{ matrix.bench.script }}-output.txt
|
||||||
external-data-json-path: ./gh-pages/dev/bench/data.json
|
external-data-json-path: ./gh-pages/dev/bench/data.json
|
||||||
alert-threshold: 150%
|
alert-threshold: 150%
|
||||||
fail-threshold: 1000%
|
fail-threshold: 200%
|
||||||
fail-on-alert: false
|
fail-on-alert: true
|
||||||
comment-on-alert: true
|
comment-on-alert: true
|
||||||
|
comment-always: true
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
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
|
uses: Roblox/rhysd-github-action-benchmark@v-luau
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.bench.title }} (CacheGrind)
|
name: ${{ matrix.bench.title }} (CacheGrind)
|
||||||
|
@ -97,7 +188,107 @@ jobs:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Push benchmark results
|
- 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: |
|
run: |
|
||||||
echo "Pushing benchmark results..."
|
echo "Pushing benchmark results..."
|
||||||
cd gh-pages
|
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
|
ITERATION_COUNT=$4
|
||||||
START_TIME=$(now_ms)
|
START_TIME=$(now_ms)
|
||||||
|
|
||||||
|
ARGS=( "$@" )
|
||||||
|
REST_ARGS="${ARGS[@]:4}"
|
||||||
|
|
||||||
valgrind \
|
valgrind \
|
||||||
--quiet \
|
--quiet \
|
||||||
--tool=cachegrind \
|
--tool=cachegrind \
|
||||||
"$1" "$2" >/dev/null
|
"$1" "$2" $REST_ARGS>/dev/null
|
||||||
|
|
||||||
|
ARGS=( "$@" )
|
||||||
|
REST_ARGS="${ARGS[@]:4}"
|
||||||
|
|
||||||
|
|
||||||
TIME_ELAPSED=$(bc <<< "$(now_ms) - ${START_TIME}")
|
TIME_ELAPSED=$(bc <<< "$(now_ms) - ${START_TIME}")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue