@trioforge/crucible 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/client/package.json +4 -0
- package/emit/package.json +4 -0
- package/out/all.d.ts +7 -0
- package/out/all.luau +2 -0
- package/out/attribute.d.ts +7 -0
- package/out/attribute.luau +7 -0
- package/out/client/index.d.ts +10 -0
- package/out/client/init.luau +55 -0
- package/out/configs.d.ts +37 -0
- package/out/configs.luau +46 -0
- package/out/crucible-behaviour.d.ts +16 -0
- package/out/crucible-behaviour.luau +78 -0
- package/out/crucible-object.d.ts +10 -0
- package/out/crucible-object.luau +69 -0
- package/out/data.d.ts +224 -0
- package/out/data.luau +97 -0
- package/out/emit/configs.d.ts +4 -0
- package/out/emit/configs.luau +22 -0
- package/out/emit/data.d.ts +6 -0
- package/out/emit/data.luau +19 -0
- package/out/emit/index.d.ts +45 -0
- package/out/emit/init.luau +101 -0
- package/out/emit/network.d.ts +6 -0
- package/out/emit/network.luau +36 -0
- package/out/emit/replicated.d.ts +4 -0
- package/out/emit/replicated.luau +13 -0
- package/out/index.d.ts +20 -0
- package/out/init.luau +34 -0
- package/out/inject.d.ts +1 -0
- package/out/inject.luau +7 -0
- package/out/internal/binding.d.ts +1 -0
- package/out/internal/binding.luau +615 -0
- package/out/internal/boot.d.ts +1 -0
- package/out/internal/boot.luau +562 -0
- package/out/internal/configs/limits.d.ts +10 -0
- package/out/internal/configs/limits.luau +25 -0
- package/out/internal/configs/port.d.ts +29 -0
- package/out/internal/configs/port.luau +99 -0
- package/out/internal/configs/runtime.d.ts +21 -0
- package/out/internal/configs/runtime.luau +981 -0
- package/out/internal/construction-claim.d.ts +1 -0
- package/out/internal/construction-claim.luau +26 -0
- package/out/internal/data/admission.d.ts +1 -0
- package/out/internal/data/admission.luau +673 -0
- package/out/internal/data/declarations.d.ts +1 -0
- package/out/internal/data/declarations.luau +384 -0
- package/out/internal/data/envelope.d.ts +1 -0
- package/out/internal/data/envelope.luau +318 -0
- package/out/internal/data/handles.d.ts +1 -0
- package/out/internal/data/handles.luau +255 -0
- package/out/internal/data/inbox-transforms.d.ts +1 -0
- package/out/internal/data/inbox-transforms.luau +458 -0
- package/out/internal/data/inbox.d.ts +1 -0
- package/out/internal/data/inbox.luau +437 -0
- package/out/internal/data/lease.d.ts +1 -0
- package/out/internal/data/lease.luau +304 -0
- package/out/internal/data/limits.d.ts +1 -0
- package/out/internal/data/limits.luau +234 -0
- package/out/internal/data/messages.d.ts +1 -0
- package/out/internal/data/messages.luau +69 -0
- package/out/internal/data/operation-id.d.ts +1 -0
- package/out/internal/data/operation-id.luau +61 -0
- package/out/internal/data/paths.d.ts +2 -0
- package/out/internal/data/paths.luau +323 -0
- package/out/internal/data/port.d.ts +1 -0
- package/out/internal/data/port.luau +290 -0
- package/out/internal/data/profiles.d.ts +1 -0
- package/out/internal/data/profiles.luau +862 -0
- package/out/internal/data/receipts.d.ts +1 -0
- package/out/internal/data/receipts.luau +190 -0
- package/out/internal/data/record-bindings.d.ts +1 -0
- package/out/internal/data/record-bindings.luau +92 -0
- package/out/internal/data/record-declarations.d.ts +1 -0
- package/out/internal/data/record-declarations.luau +140 -0
- package/out/internal/data/records.d.ts +1 -0
- package/out/internal/data/records.luau +400 -0
- package/out/internal/data/runtime.d.ts +1 -0
- package/out/internal/data/runtime.luau +918 -0
- package/out/internal/data/scheduler.d.ts +1 -0
- package/out/internal/data/scheduler.luau +507 -0
- package/out/internal/data/session.d.ts +1 -0
- package/out/internal/data/session.luau +234 -0
- package/out/internal/data/shutdown.d.ts +1 -0
- package/out/internal/data/shutdown.luau +91 -0
- package/out/internal/data/waiters.d.ts +1 -0
- package/out/internal/data/waiters.luau +65 -0
- package/out/internal/dense-list.d.ts +1 -0
- package/out/internal/dense-list.luau +155 -0
- package/out/internal/diagnostics.d.ts +1 -0
- package/out/internal/diagnostics.luau +29 -0
- package/out/internal/hooks.d.ts +1 -0
- package/out/internal/hooks.luau +53 -0
- package/out/internal/implementers.d.ts +1 -0
- package/out/internal/implementers.luau +119 -0
- package/out/internal/lifecycle.d.ts +1 -0
- package/out/internal/lifecycle.luau +912 -0
- package/out/internal/log-output.d.ts +1 -0
- package/out/internal/log-output.luau +106 -0
- package/out/internal/network/catalog.d.ts +26 -0
- package/out/internal/network/catalog.luau +182 -0
- package/out/internal/network/claims.d.ts +1 -0
- package/out/internal/network/claims.luau +154 -0
- package/out/internal/network/client-session.d.ts +1 -0
- package/out/internal/network/client-session.luau +403 -0
- package/out/internal/network/codec.d.ts +63 -0
- package/out/internal/network/codec.luau +1183 -0
- package/out/internal/network/envelope.d.ts +1 -0
- package/out/internal/network/envelope.luau +247 -0
- package/out/internal/network/limits.d.ts +26 -0
- package/out/internal/network/limits.luau +55 -0
- package/out/internal/network/port.d.ts +1 -0
- package/out/internal/network/port.luau +19 -0
- package/out/internal/network/roblox-network-port.d.ts +1 -0
- package/out/internal/network/roblox-network-port.luau +105 -0
- package/out/internal/network/schema.d.ts +72 -0
- package/out/internal/network/schema.luau +386 -0
- package/out/internal/network/server-session.d.ts +1 -0
- package/out/internal/network/server-session.luau +393 -0
- package/out/internal/network/session.d.ts +1 -0
- package/out/internal/network/session.luau +266 -0
- package/out/internal/port.d.ts +1 -0
- package/out/internal/port.luau +12 -0
- package/out/internal/reactive/context.d.ts +1 -0
- package/out/internal/reactive/context.luau +149 -0
- package/out/internal/reactive/flush.d.ts +1 -0
- package/out/internal/reactive/flush.luau +355 -0
- package/out/internal/reactive/graph.d.ts +1 -0
- package/out/internal/reactive/graph.luau +626 -0
- package/out/internal/reactive/limits.d.ts +1 -0
- package/out/internal/reactive/limits.luau +54 -0
- package/out/internal/reactive/ownership.d.ts +1 -0
- package/out/internal/reactive/ownership.luau +626 -0
- package/out/internal/registry.d.ts +1 -0
- package/out/internal/registry.luau +455 -0
- package/out/internal/roblox-port.d.ts +1 -0
- package/out/internal/roblox-port.luau +117 -0
- package/out/internal/runner.d.ts +1 -0
- package/out/internal/runner.luau +86 -0
- package/out/internal/scheduler.d.ts +1 -0
- package/out/internal/scheduler.luau +207 -0
- package/out/internal/state.d.ts +1 -0
- package/out/internal/state.luau +41 -0
- package/out/internal/sync/client-sync.d.ts +1 -0
- package/out/internal/sync/client-sync.luau +157 -0
- package/out/internal/sync/declarations.d.ts +1 -0
- package/out/internal/sync/declarations.luau +276 -0
- package/out/internal/sync/frame.d.ts +1 -0
- package/out/internal/sync/frame.luau +91 -0
- package/out/internal/sync/server-sync.d.ts +1 -0
- package/out/internal/sync/server-sync.luau +502 -0
- package/out/internal/sync/values.d.ts +1 -0
- package/out/internal/sync/values.luau +930 -0
- package/out/log-level.d.ts +7 -0
- package/out/log-level.luau +21 -0
- package/out/logging.d.ts +34 -0
- package/out/logging.luau +233 -0
- package/out/network.d.ts +66 -0
- package/out/network.luau +24 -0
- package/out/reactive.d.ts +76 -0
- package/out/reactive.luau +136 -0
- package/out/replicated.d.ts +38 -0
- package/out/replicated.luau +45 -0
- package/out/server/data.d.ts +54 -0
- package/out/server/data.luau +44 -0
- package/out/server/index.d.ts +18 -0
- package/out/server/init.luau +62 -0
- package/out/server/publish.d.ts +20 -0
- package/out/server/publish.luau +24 -0
- package/out/subsystem.d.ts +4 -0
- package/out/subsystem.luau +30 -0
- package/out/vide/index.d.ts +7 -0
- package/out/vide/init.luau +50 -0
- package/package.json +52 -5
- package/server/package.json +4 -0
- package/transformer/data.cjs +72 -0
- package/transformer/declarations.cjs +153 -0
- package/transformer/diagnostics.cjs +19 -0
- package/transformer/index.js +741 -0
- package/transformer/network.cjs +961 -0
- package/transformer/type-id.cjs +80 -0
- package/vide/package.json +4 -0
- package/README.md +0 -3
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _codec = TS.import(script, script.Parent.Parent, "network", "codec")
|
|
4
|
+
local checkFrame = _codec.frame
|
|
5
|
+
local validString = _codec.validString
|
|
6
|
+
local _limits = TS.import(script, script.Parent.Parent, "reactive", "limits")
|
|
7
|
+
local SYNC_ADMISSION_VISITS = _limits.SYNC_ADMISSION_VISITS
|
|
8
|
+
local SYNC_FRAME_BYTES = _limits.SYNC_FRAME_BYTES
|
|
9
|
+
local SYNC_FRAME_COLLECTION_ENTRIES = _limits.SYNC_FRAME_COLLECTION_ENTRIES
|
|
10
|
+
local SYNC_FRAME_VALUE_VISITS = _limits.SYNC_FRAME_VALUE_VISITS
|
|
11
|
+
local SYNC_STRING_BYTES = _limits.SYNC_STRING_BYTES
|
|
12
|
+
local SYNC_VALUE_DEPTH = _limits.SYNC_VALUE_DEPTH
|
|
13
|
+
--* @internal What the dynamic codec charges for a value: wire.md's bytes, entries and visits, and table nesting.
|
|
14
|
+
--* @internal A peer examination's visit budget. A visit that would pass `limit` is not made.
|
|
15
|
+
--* @internal Thrown when a visit would pass a `Work` limit: the peer is over budget, not the value.
|
|
16
|
+
local OVER_BUDGET = table.freeze({
|
|
17
|
+
overBudget = true,
|
|
18
|
+
})
|
|
19
|
+
--* @internal
|
|
20
|
+
local function isOverBudget(err)
|
|
21
|
+
return rawequal(err, OVER_BUDGET)
|
|
22
|
+
end
|
|
23
|
+
--* @internal Charges one visit to `work`, checked before it is made.
|
|
24
|
+
local function spend(work)
|
|
25
|
+
if work.visits >= work.limit then
|
|
26
|
+
error(OVER_BUDGET)
|
|
27
|
+
end
|
|
28
|
+
work.visits += 1
|
|
29
|
+
end
|
|
30
|
+
local function weak()
|
|
31
|
+
return setmetatable({}, {
|
|
32
|
+
__mode = "k",
|
|
33
|
+
})
|
|
34
|
+
end
|
|
35
|
+
--* @internal A descriptor compiled for admission: lookups by name and by discriminant, and a conformance cache.
|
|
36
|
+
local RUNTIME_KINDS = {
|
|
37
|
+
bool = "boolean",
|
|
38
|
+
f64 = "number",
|
|
39
|
+
f32 = "number",
|
|
40
|
+
u8 = "number",
|
|
41
|
+
u16 = "number",
|
|
42
|
+
u32 = "number",
|
|
43
|
+
string = "string",
|
|
44
|
+
vector3 = "Vector3",
|
|
45
|
+
color3 = "Color3",
|
|
46
|
+
cframe = "CFrame",
|
|
47
|
+
}
|
|
48
|
+
local function admitsNil(plan)
|
|
49
|
+
return plan.kind == "nil" or (plan.kind == "union" and plan["nil"] ~= nil)
|
|
50
|
+
end
|
|
51
|
+
--* @internal Compiles a descriptor the codec already accepted. Shared subtrees compile once.
|
|
52
|
+
local function compilePlan(root)
|
|
53
|
+
local memo = {}
|
|
54
|
+
local build
|
|
55
|
+
local function object(fields, extra)
|
|
56
|
+
local byName = {}
|
|
57
|
+
local order = {}
|
|
58
|
+
local optional = {}
|
|
59
|
+
for _, _binding in fields do
|
|
60
|
+
local name = _binding[1]
|
|
61
|
+
local child = _binding[2]
|
|
62
|
+
local plan = build(child)
|
|
63
|
+
byName[name] = plan
|
|
64
|
+
table.insert(order, name)
|
|
65
|
+
if admitsNil(plan) then
|
|
66
|
+
optional[name] = true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
if extra ~= nil then
|
|
70
|
+
local _arg0 = extra[1]
|
|
71
|
+
local _arg1 = {
|
|
72
|
+
kind = "literal",
|
|
73
|
+
value = extra[2],
|
|
74
|
+
}
|
|
75
|
+
byName[_arg0] = _arg1
|
|
76
|
+
local _arg0_1 = extra[1]
|
|
77
|
+
table.insert(order, _arg0_1)
|
|
78
|
+
end
|
|
79
|
+
return {
|
|
80
|
+
kind = "object",
|
|
81
|
+
fields = byName,
|
|
82
|
+
order = order,
|
|
83
|
+
optional = optional,
|
|
84
|
+
conforms = weak(),
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
function build(node)
|
|
88
|
+
local _node = node
|
|
89
|
+
local known = memo[_node]
|
|
90
|
+
if known ~= nil then
|
|
91
|
+
return known
|
|
92
|
+
end
|
|
93
|
+
local plan
|
|
94
|
+
local _exp = node.kind
|
|
95
|
+
repeat
|
|
96
|
+
local _fallthrough = false
|
|
97
|
+
if _exp == "nil" then
|
|
98
|
+
_fallthrough = true
|
|
99
|
+
end
|
|
100
|
+
if _fallthrough or _exp == "bool" then
|
|
101
|
+
_fallthrough = true
|
|
102
|
+
end
|
|
103
|
+
if _fallthrough or _exp == "string" then
|
|
104
|
+
_fallthrough = true
|
|
105
|
+
end
|
|
106
|
+
if _fallthrough or _exp == "vector3" then
|
|
107
|
+
_fallthrough = true
|
|
108
|
+
end
|
|
109
|
+
if _fallthrough or _exp == "color3" then
|
|
110
|
+
_fallthrough = true
|
|
111
|
+
end
|
|
112
|
+
if _fallthrough or _exp == "cframe" then
|
|
113
|
+
plan = {
|
|
114
|
+
kind = node.kind,
|
|
115
|
+
}
|
|
116
|
+
break
|
|
117
|
+
end
|
|
118
|
+
if _exp == "f64" then
|
|
119
|
+
_fallthrough = true
|
|
120
|
+
end
|
|
121
|
+
if _fallthrough or _exp == "f32" then
|
|
122
|
+
_fallthrough = true
|
|
123
|
+
end
|
|
124
|
+
if _fallthrough or _exp == "u8" then
|
|
125
|
+
_fallthrough = true
|
|
126
|
+
end
|
|
127
|
+
if _fallthrough or _exp == "u16" then
|
|
128
|
+
_fallthrough = true
|
|
129
|
+
end
|
|
130
|
+
if _fallthrough or _exp == "u32" then
|
|
131
|
+
plan = {
|
|
132
|
+
kind = "number",
|
|
133
|
+
width = node.kind,
|
|
134
|
+
}
|
|
135
|
+
break
|
|
136
|
+
end
|
|
137
|
+
if _exp == "literal" then
|
|
138
|
+
plan = {
|
|
139
|
+
kind = "literal",
|
|
140
|
+
value = node.value,
|
|
141
|
+
}
|
|
142
|
+
break
|
|
143
|
+
end
|
|
144
|
+
if _exp == "instance" then
|
|
145
|
+
plan = {
|
|
146
|
+
kind = "refuse",
|
|
147
|
+
reason = "replicated state cannot hold Instances; replicate a stable id",
|
|
148
|
+
}
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
if _exp == "dynamic" then
|
|
152
|
+
plan = {
|
|
153
|
+
kind = "refuse",
|
|
154
|
+
reason = "replicated state cannot hold an unknown value",
|
|
155
|
+
}
|
|
156
|
+
break
|
|
157
|
+
end
|
|
158
|
+
if _exp == "object" then
|
|
159
|
+
plan = object(node.fields)
|
|
160
|
+
break
|
|
161
|
+
end
|
|
162
|
+
if _exp == "variant" then
|
|
163
|
+
plan = object(node.body.fields, { node.key, node.value })
|
|
164
|
+
break
|
|
165
|
+
end
|
|
166
|
+
if _exp == "array" then
|
|
167
|
+
plan = {
|
|
168
|
+
kind = "array",
|
|
169
|
+
element = build(node.element),
|
|
170
|
+
conforms = weak(),
|
|
171
|
+
}
|
|
172
|
+
break
|
|
173
|
+
end
|
|
174
|
+
if _exp == "tuple" then
|
|
175
|
+
local _object = {
|
|
176
|
+
kind = "tuple",
|
|
177
|
+
}
|
|
178
|
+
local _left = "elements"
|
|
179
|
+
local _exp_1 = node.elements
|
|
180
|
+
-- ▼ ReadonlyArray.map ▼
|
|
181
|
+
local _newValue = table.create(#_exp_1)
|
|
182
|
+
for _k, _v in _exp_1 do
|
|
183
|
+
_newValue[_k] = build(_v, _k - 1, _exp_1)
|
|
184
|
+
end
|
|
185
|
+
-- ▲ ReadonlyArray.map ▲
|
|
186
|
+
_object[_left] = _newValue
|
|
187
|
+
_object.conforms = weak()
|
|
188
|
+
plan = _object
|
|
189
|
+
break
|
|
190
|
+
end
|
|
191
|
+
if _exp == "map" then
|
|
192
|
+
local keys
|
|
193
|
+
if node.key.kind == "union" then
|
|
194
|
+
keys = {}
|
|
195
|
+
for _, branch in node.key.branches do
|
|
196
|
+
local _keys = keys
|
|
197
|
+
local _arg0 = tostring(branch.value)
|
|
198
|
+
_keys[_arg0] = true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
plan = {
|
|
202
|
+
kind = "map",
|
|
203
|
+
keys = keys,
|
|
204
|
+
value = build(node.value),
|
|
205
|
+
conforms = weak(),
|
|
206
|
+
}
|
|
207
|
+
break
|
|
208
|
+
end
|
|
209
|
+
if _exp == "union" then
|
|
210
|
+
local literals = {}
|
|
211
|
+
local variants = {}
|
|
212
|
+
local kinds = {}
|
|
213
|
+
local absent
|
|
214
|
+
local discriminant
|
|
215
|
+
for _, branch in node.branches do
|
|
216
|
+
local child = build(branch)
|
|
217
|
+
if branch.kind == "nil" then
|
|
218
|
+
absent = child
|
|
219
|
+
elseif branch.kind == "literal" then
|
|
220
|
+
local _value = branch.value
|
|
221
|
+
literals[_value] = child
|
|
222
|
+
elseif branch.kind == "variant" then
|
|
223
|
+
discriminant = branch.key
|
|
224
|
+
local _value = branch.value
|
|
225
|
+
variants[_value] = child
|
|
226
|
+
else
|
|
227
|
+
local _condition = RUNTIME_KINDS[branch.kind]
|
|
228
|
+
if _condition == nil then
|
|
229
|
+
_condition = "table"
|
|
230
|
+
end
|
|
231
|
+
kinds[_condition] = child
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
plan = {
|
|
235
|
+
kind = "union",
|
|
236
|
+
["nil"] = absent,
|
|
237
|
+
literals = literals,
|
|
238
|
+
discriminant = discriminant,
|
|
239
|
+
variants = variants,
|
|
240
|
+
kinds = kinds,
|
|
241
|
+
}
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
until true
|
|
245
|
+
local _node_1 = node
|
|
246
|
+
local _plan = plan
|
|
247
|
+
memo[_node_1] = _plan
|
|
248
|
+
return plan
|
|
249
|
+
end
|
|
250
|
+
return build(root)
|
|
251
|
+
end
|
|
252
|
+
local costs = setmetatable({}, {
|
|
253
|
+
__mode = "k",
|
|
254
|
+
})
|
|
255
|
+
local function varuintSize(value)
|
|
256
|
+
local size = 1
|
|
257
|
+
while value >= 128 do
|
|
258
|
+
value = math.floor(value / 128)
|
|
259
|
+
size += 1
|
|
260
|
+
end
|
|
261
|
+
return size
|
|
262
|
+
end
|
|
263
|
+
local function stringBytes(text)
|
|
264
|
+
return varuintSize(#text) + #text
|
|
265
|
+
end
|
|
266
|
+
--* @internal A leaf's dynamic cost, or an admitted table's cached cost.
|
|
267
|
+
local function costOf(value)
|
|
268
|
+
local _value = value
|
|
269
|
+
if type(_value) == "table" then
|
|
270
|
+
local _value_1 = value
|
|
271
|
+
local _condition = costs[_value_1]
|
|
272
|
+
if _condition == nil then
|
|
273
|
+
_condition = error("Crucible sync: costing a value admission never saw")
|
|
274
|
+
end
|
|
275
|
+
return _condition
|
|
276
|
+
end
|
|
277
|
+
local bytes = 1
|
|
278
|
+
local _value_1 = value
|
|
279
|
+
if type(_value_1) == "number" then
|
|
280
|
+
bytes = 9
|
|
281
|
+
else
|
|
282
|
+
local _value_2 = value
|
|
283
|
+
if type(_value_2) == "string" then
|
|
284
|
+
bytes = 1 + stringBytes(value)
|
|
285
|
+
else
|
|
286
|
+
local _value_3 = value
|
|
287
|
+
local _condition = typeof(_value_3) == "Vector3"
|
|
288
|
+
if not _condition then
|
|
289
|
+
local _value_4 = value
|
|
290
|
+
_condition = typeof(_value_4) == "Color3"
|
|
291
|
+
end
|
|
292
|
+
if _condition then
|
|
293
|
+
bytes = 25
|
|
294
|
+
else
|
|
295
|
+
local _value_4 = value
|
|
296
|
+
if typeof(_value_4) == "CFrame" then
|
|
297
|
+
bytes = 97
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
return {
|
|
303
|
+
bytes = bytes,
|
|
304
|
+
entries = 0,
|
|
305
|
+
visits = 1,
|
|
306
|
+
height = 0,
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
local function describe(value)
|
|
310
|
+
local _result
|
|
311
|
+
if value == nil then
|
|
312
|
+
_result = "nil"
|
|
313
|
+
else
|
|
314
|
+
local _value = value
|
|
315
|
+
_result = typeof(_value)
|
|
316
|
+
end
|
|
317
|
+
return _result
|
|
318
|
+
end
|
|
319
|
+
local Admission
|
|
320
|
+
do
|
|
321
|
+
Admission = setmetatable({}, {
|
|
322
|
+
__tostring = function()
|
|
323
|
+
return "Admission"
|
|
324
|
+
end,
|
|
325
|
+
})
|
|
326
|
+
Admission.__index = Admission
|
|
327
|
+
function Admission.new(...)
|
|
328
|
+
local self = setmetatable({}, Admission)
|
|
329
|
+
return self:constructor(...) or self
|
|
330
|
+
end
|
|
331
|
+
function Admission:constructor(name, work)
|
|
332
|
+
self.name = name
|
|
333
|
+
self.work = work
|
|
334
|
+
self.steps = 0
|
|
335
|
+
self.bytes = 0
|
|
336
|
+
self.entries = 0
|
|
337
|
+
self.visits = 0
|
|
338
|
+
self.depth = 0
|
|
339
|
+
self.height = 0
|
|
340
|
+
self.active = {}
|
|
341
|
+
self.pending = {}
|
|
342
|
+
end
|
|
343
|
+
function Admission:reject(path, reason)
|
|
344
|
+
error(`{self.name}{path}: {reason}`, 0)
|
|
345
|
+
end
|
|
346
|
+
function Admission:step(path)
|
|
347
|
+
if self.work ~= nil then
|
|
348
|
+
spend(self.work)
|
|
349
|
+
end
|
|
350
|
+
if self.steps >= SYNC_ADMISSION_VISITS then
|
|
351
|
+
self:reject(path, "over SYNC_ADMISSION_VISITS")
|
|
352
|
+
end
|
|
353
|
+
self.steps += 1
|
|
354
|
+
end
|
|
355
|
+
function Admission:charge(bytes, entries, visits, path)
|
|
356
|
+
self.bytes += bytes
|
|
357
|
+
self.entries += entries
|
|
358
|
+
self.visits += visits
|
|
359
|
+
if self.bytes > SYNC_FRAME_BYTES then
|
|
360
|
+
self:reject(path, "over SYNC_FRAME_BYTES")
|
|
361
|
+
end
|
|
362
|
+
if self.entries > SYNC_FRAME_COLLECTION_ENTRIES then
|
|
363
|
+
self:reject(path, "over SYNC_FRAME_COLLECTION_ENTRIES")
|
|
364
|
+
end
|
|
365
|
+
if self.visits > SYNC_FRAME_VALUE_VISITS then
|
|
366
|
+
self:reject(path, "over SYNC_FRAME_VALUE_VISITS")
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
function Admission:chargeLeaf(value, path)
|
|
370
|
+
local cost = costOf(value)
|
|
371
|
+
self:charge(cost.bytes, 0, 1, path)
|
|
372
|
+
end
|
|
373
|
+
function Admission:open(plan, value, path)
|
|
374
|
+
local _value = value
|
|
375
|
+
if not (type(_value) == "table") then
|
|
376
|
+
self:reject(path, `expected table, got {describe(value)}`)
|
|
377
|
+
end
|
|
378
|
+
if getmetatable(value) ~= nil then
|
|
379
|
+
self:reject(path, "metatable")
|
|
380
|
+
end
|
|
381
|
+
local _active = self.active
|
|
382
|
+
local _value_1 = value
|
|
383
|
+
if _active[_value_1] ~= nil then
|
|
384
|
+
self:reject(path, "cycle")
|
|
385
|
+
end
|
|
386
|
+
local _condition = table.isfrozen(value)
|
|
387
|
+
if _condition then
|
|
388
|
+
local _conforms = plan.conforms
|
|
389
|
+
local _value_2 = value
|
|
390
|
+
_condition = _conforms[_value_2] ~= nil
|
|
391
|
+
end
|
|
392
|
+
if _condition then
|
|
393
|
+
local _value_2 = value
|
|
394
|
+
local cost = costs[_value_2]
|
|
395
|
+
if self.depth + cost.height > SYNC_VALUE_DEPTH then
|
|
396
|
+
self:reject(path, "deeper than SYNC_VALUE_DEPTH")
|
|
397
|
+
end
|
|
398
|
+
self:charge(cost.bytes, cost.entries, cost.visits, path)
|
|
399
|
+
self.height = math.max(self.height, cost.height)
|
|
400
|
+
return value
|
|
401
|
+
end
|
|
402
|
+
return nil
|
|
403
|
+
end
|
|
404
|
+
function Admission:nest(plan, value, path, walk)
|
|
405
|
+
self.depth += 1
|
|
406
|
+
if self.depth > SYNC_VALUE_DEPTH then
|
|
407
|
+
self:reject(path, "deeper than SYNC_VALUE_DEPTH")
|
|
408
|
+
end
|
|
409
|
+
local _active = self.active
|
|
410
|
+
local _value = value
|
|
411
|
+
_active[_value] = true
|
|
412
|
+
local outer = self.height
|
|
413
|
+
self.height = 0
|
|
414
|
+
local bytes, entries, visits = self.bytes, self.entries, self.visits
|
|
415
|
+
local result = walk()
|
|
416
|
+
local height = self.height + 1
|
|
417
|
+
local _pending = self.pending
|
|
418
|
+
local _arg0 = {
|
|
419
|
+
plan = plan,
|
|
420
|
+
value = result,
|
|
421
|
+
cost = {
|
|
422
|
+
bytes = self.bytes - bytes,
|
|
423
|
+
entries = self.entries - entries,
|
|
424
|
+
visits = self.visits - visits,
|
|
425
|
+
height = height,
|
|
426
|
+
},
|
|
427
|
+
}
|
|
428
|
+
table.insert(_pending, _arg0)
|
|
429
|
+
self.height = math.max(outer, height)
|
|
430
|
+
local _active_1 = self.active
|
|
431
|
+
local _value_1 = value
|
|
432
|
+
_active_1[_value_1] = nil
|
|
433
|
+
self.depth -= 1
|
|
434
|
+
return result
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
local function admitNumber(a, width, value, path)
|
|
438
|
+
local _value = value
|
|
439
|
+
if not (type(_value) == "number") then
|
|
440
|
+
a:reject(path, `expected number, got {describe(value)}`)
|
|
441
|
+
end
|
|
442
|
+
if value ~= value or math.abs(value) == math.huge then
|
|
443
|
+
a:reject(path, "expected finite number")
|
|
444
|
+
end
|
|
445
|
+
if width == "f32" then
|
|
446
|
+
local scratch = buffer.create(4)
|
|
447
|
+
buffer.writef32(scratch, 0, value)
|
|
448
|
+
if math.abs(buffer.readf32(scratch, 0)) == math.huge then
|
|
449
|
+
a:reject(path, "f32 out of range")
|
|
450
|
+
end
|
|
451
|
+
elseif width ~= "f64" then
|
|
452
|
+
local maximum = if width == "u8" then 255 elseif width == "u16" then 65535 else 4294967295
|
|
453
|
+
if value < 0 or value > maximum or value % 1 ~= 0 then
|
|
454
|
+
a:reject(path, `{width} out of range`)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
local function admitString(a, value, path)
|
|
459
|
+
local _value = value
|
|
460
|
+
if not (type(_value) == "string") then
|
|
461
|
+
a:reject(path, `expected string, got {describe(value)}`)
|
|
462
|
+
end
|
|
463
|
+
if #value > SYNC_STRING_BYTES then
|
|
464
|
+
a:reject(path, "longer than SYNC_STRING_BYTES")
|
|
465
|
+
end
|
|
466
|
+
local ok = pcall(validString, value)
|
|
467
|
+
if not ok then
|
|
468
|
+
a:reject(path, "invalid UTF-8")
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
local admitObject, admitArray, admitMap, admitUnion
|
|
472
|
+
local function admitNode(a, plan, value, path)
|
|
473
|
+
a:step(path)
|
|
474
|
+
local _exp = plan.kind
|
|
475
|
+
repeat
|
|
476
|
+
local _fallthrough = false
|
|
477
|
+
if _exp == "nil" then
|
|
478
|
+
if value ~= nil then
|
|
479
|
+
a:reject(path, `expected nil, got {describe(value)}`)
|
|
480
|
+
end
|
|
481
|
+
break
|
|
482
|
+
end
|
|
483
|
+
if _exp == "bool" then
|
|
484
|
+
local _value = value
|
|
485
|
+
if not (type(_value) == "boolean") then
|
|
486
|
+
a:reject(path, `expected boolean, got {describe(value)}`)
|
|
487
|
+
end
|
|
488
|
+
break
|
|
489
|
+
end
|
|
490
|
+
if _exp == "number" then
|
|
491
|
+
admitNumber(a, plan.width, value, path)
|
|
492
|
+
break
|
|
493
|
+
end
|
|
494
|
+
if _exp == "string" then
|
|
495
|
+
admitString(a, value, path)
|
|
496
|
+
break
|
|
497
|
+
end
|
|
498
|
+
if _exp == "literal" then
|
|
499
|
+
if value ~= plan.value then
|
|
500
|
+
a:reject(path, `expected a declared literal, got {describe(value)}`)
|
|
501
|
+
end
|
|
502
|
+
local _value = value
|
|
503
|
+
if type(_value) == "string" then
|
|
504
|
+
admitString(a, value, path)
|
|
505
|
+
end
|
|
506
|
+
break
|
|
507
|
+
end
|
|
508
|
+
if _exp == "vector3" then
|
|
509
|
+
local _value = value
|
|
510
|
+
if not (typeof(_value) == "Vector3") then
|
|
511
|
+
a:reject(path, `expected Vector3, got {describe(value)}`)
|
|
512
|
+
end
|
|
513
|
+
for _, component in { value.X, value.Y, value.Z } do
|
|
514
|
+
if component ~= component or math.abs(component) == math.huge then
|
|
515
|
+
a:reject(path, "expected finite number")
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
break
|
|
519
|
+
end
|
|
520
|
+
if _exp == "color3" then
|
|
521
|
+
local _value = value
|
|
522
|
+
if not (typeof(_value) == "Color3") then
|
|
523
|
+
a:reject(path, `expected Color3, got {describe(value)}`)
|
|
524
|
+
end
|
|
525
|
+
for _, component in { value.R, value.G, value.B } do
|
|
526
|
+
if component ~= component or math.abs(component) == math.huge then
|
|
527
|
+
a:reject(path, "expected finite number")
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
break
|
|
531
|
+
end
|
|
532
|
+
if _exp == "cframe" then
|
|
533
|
+
local _value = value
|
|
534
|
+
if not (typeof(_value) == "CFrame") then
|
|
535
|
+
a:reject(path, `expected CFrame, got {describe(value)}`)
|
|
536
|
+
end
|
|
537
|
+
local _array = {}
|
|
538
|
+
local _length = #_array
|
|
539
|
+
local _array_1 = { value:GetComponents() }
|
|
540
|
+
table.move(_array_1, 1, #_array_1, _length + 1, _array)
|
|
541
|
+
local ok = pcall(checkFrame, _array, path)
|
|
542
|
+
if not ok then
|
|
543
|
+
a:reject(path, "invalid CFrame")
|
|
544
|
+
end
|
|
545
|
+
break
|
|
546
|
+
end
|
|
547
|
+
if _exp == "refuse" then
|
|
548
|
+
return a:reject(path, plan.reason)
|
|
549
|
+
end
|
|
550
|
+
if _exp == "object" then
|
|
551
|
+
return admitObject(a, plan, value, path)
|
|
552
|
+
end
|
|
553
|
+
if _exp == "array" then
|
|
554
|
+
_fallthrough = true
|
|
555
|
+
end
|
|
556
|
+
if _fallthrough or _exp == "tuple" then
|
|
557
|
+
return admitArray(a, plan, value, path)
|
|
558
|
+
end
|
|
559
|
+
if _exp == "map" then
|
|
560
|
+
return admitMap(a, plan, value, path)
|
|
561
|
+
end
|
|
562
|
+
if _exp == "union" then
|
|
563
|
+
return admitUnion(a, plan, value, path)
|
|
564
|
+
end
|
|
565
|
+
until true
|
|
566
|
+
a:chargeLeaf(value, path)
|
|
567
|
+
return value
|
|
568
|
+
end
|
|
569
|
+
local function admitField(a, name, plan, value, path)
|
|
570
|
+
a:charge(stringBytes(name), 1, 1, path)
|
|
571
|
+
return admitNode(a, plan, value, `{path}.{name}`)
|
|
572
|
+
end
|
|
573
|
+
function admitObject(a, plan, value, path)
|
|
574
|
+
local cached = a:open(plan, value, path)
|
|
575
|
+
if cached ~= nil then
|
|
576
|
+
return cached
|
|
577
|
+
end
|
|
578
|
+
local source = value
|
|
579
|
+
return a:nest(plan, source, path, function()
|
|
580
|
+
a:charge(1, 0, 1, path)
|
|
581
|
+
local admitted = {}
|
|
582
|
+
local copy = false
|
|
583
|
+
local stopped = false
|
|
584
|
+
for key, child in pairs(source) do
|
|
585
|
+
a:step(path)
|
|
586
|
+
local field = if type(key) == "string" then plan.fields[key] else nil
|
|
587
|
+
if field == nil then
|
|
588
|
+
copy = true
|
|
589
|
+
stopped = true
|
|
590
|
+
break
|
|
591
|
+
end
|
|
592
|
+
local result = admitField(a, key, field, child, path)
|
|
593
|
+
if not rawequal(result, child) then
|
|
594
|
+
copy = true
|
|
595
|
+
end
|
|
596
|
+
admitted[key] = result
|
|
597
|
+
end
|
|
598
|
+
for _, name in plan.order do
|
|
599
|
+
if admitted[name] ~= nil then
|
|
600
|
+
continue
|
|
601
|
+
end
|
|
602
|
+
local child
|
|
603
|
+
if stopped then
|
|
604
|
+
a:step(path)
|
|
605
|
+
child = source[name]
|
|
606
|
+
end
|
|
607
|
+
if child == nil then
|
|
608
|
+
if not (plan.optional[name] ~= nil) then
|
|
609
|
+
a:reject(`{path}.{name}`, "missing")
|
|
610
|
+
end
|
|
611
|
+
continue
|
|
612
|
+
end
|
|
613
|
+
local result = admitField(a, name, plan.fields[name], child, path)
|
|
614
|
+
if not rawequal(result, child) then
|
|
615
|
+
copy = true
|
|
616
|
+
end
|
|
617
|
+
admitted[name] = result
|
|
618
|
+
end
|
|
619
|
+
local _a = a
|
|
620
|
+
-- ▼ ReadonlyMap.size ▼
|
|
621
|
+
local _size = 0
|
|
622
|
+
for _ in admitted do
|
|
623
|
+
_size += 1
|
|
624
|
+
end
|
|
625
|
+
-- ▲ ReadonlyMap.size ▲
|
|
626
|
+
_a:charge(varuintSize(_size), 0, 0, path)
|
|
627
|
+
if not copy then
|
|
628
|
+
return source
|
|
629
|
+
end
|
|
630
|
+
local out = {}
|
|
631
|
+
for name, child in admitted do
|
|
632
|
+
out[name] = child
|
|
633
|
+
end
|
|
634
|
+
return out
|
|
635
|
+
end)
|
|
636
|
+
end
|
|
637
|
+
function admitArray(a, plan, value, path)
|
|
638
|
+
local cached = a:open(plan, value, path)
|
|
639
|
+
if cached ~= nil then
|
|
640
|
+
return cached
|
|
641
|
+
end
|
|
642
|
+
local source = value
|
|
643
|
+
return a:nest(plan, source, path, function()
|
|
644
|
+
local count = #source
|
|
645
|
+
if plan.kind == "tuple" and count ~= #plan.elements then
|
|
646
|
+
a:reject(path, "tuple length")
|
|
647
|
+
end
|
|
648
|
+
a:charge(1 + varuintSize(count), count, 1, path)
|
|
649
|
+
local seen = 0
|
|
650
|
+
for key in pairs(source) do
|
|
651
|
+
a:step(path)
|
|
652
|
+
seen += 1
|
|
653
|
+
if seen > count or not (type(key) == "number") or key % 1 ~= 0 or key < 1 or key > count then
|
|
654
|
+
a:reject(path, "sparse array")
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
if seen ~= count then
|
|
658
|
+
a:reject(path, "sparse array")
|
|
659
|
+
end
|
|
660
|
+
local copy
|
|
661
|
+
do
|
|
662
|
+
local i = 0
|
|
663
|
+
local _shouldIncrement = false
|
|
664
|
+
while true do
|
|
665
|
+
if _shouldIncrement then
|
|
666
|
+
i += 1
|
|
667
|
+
else
|
|
668
|
+
_shouldIncrement = true
|
|
669
|
+
end
|
|
670
|
+
if not (i < count) then
|
|
671
|
+
break
|
|
672
|
+
end
|
|
673
|
+
local child = source[i + 1]
|
|
674
|
+
local result = admitNode(a, if plan.kind == "array" then plan.element else plan.elements[i + 1], child, `{path}[{i + 1}]`)
|
|
675
|
+
if not rawequal(result, child) and copy == nil then
|
|
676
|
+
copy = table.clone(source)
|
|
677
|
+
end
|
|
678
|
+
if copy ~= nil then
|
|
679
|
+
copy[i + 1] = result
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
return copy or source
|
|
684
|
+
end)
|
|
685
|
+
end
|
|
686
|
+
function admitMap(a, plan, value, path)
|
|
687
|
+
local cached = a:open(plan, value, path)
|
|
688
|
+
if cached ~= nil then
|
|
689
|
+
return cached
|
|
690
|
+
end
|
|
691
|
+
local source = value
|
|
692
|
+
return a:nest(plan, source, path, function()
|
|
693
|
+
a:charge(1, 0, 1, path)
|
|
694
|
+
local count = 0
|
|
695
|
+
local copy
|
|
696
|
+
for key, child in pairs(source) do
|
|
697
|
+
a:step(path)
|
|
698
|
+
if not (type(key) == "string") or (plan.keys ~= nil and not (plan.keys[key] ~= nil)) then
|
|
699
|
+
a:reject(path, "invalid map key")
|
|
700
|
+
end
|
|
701
|
+
count += 1
|
|
702
|
+
a:charge(stringBytes(key), 1, 1, path)
|
|
703
|
+
local result = admitNode(a, plan.value, child, `{path}[]`)
|
|
704
|
+
if not rawequal(result, child) and copy == nil then
|
|
705
|
+
copy = table.clone(source)
|
|
706
|
+
end
|
|
707
|
+
if copy ~= nil then
|
|
708
|
+
copy[key] = result
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
a:charge(varuintSize(count), 0, 0, path)
|
|
712
|
+
return copy or source
|
|
713
|
+
end)
|
|
714
|
+
end
|
|
715
|
+
function admitUnion(a, plan, value, path)
|
|
716
|
+
if value == nil then
|
|
717
|
+
if plan["nil"] == nil then
|
|
718
|
+
a:reject(path, "no branch for nil")
|
|
719
|
+
end
|
|
720
|
+
return admitNode(a, plan["nil"], value, path)
|
|
721
|
+
end
|
|
722
|
+
local _literals = plan.literals
|
|
723
|
+
local _value = value
|
|
724
|
+
local literal = _literals[_value]
|
|
725
|
+
if literal ~= nil then
|
|
726
|
+
return admitNode(a, literal, value, path)
|
|
727
|
+
end
|
|
728
|
+
local key = plan.discriminant
|
|
729
|
+
local _condition = key ~= nil
|
|
730
|
+
if _condition then
|
|
731
|
+
local _value_1 = value
|
|
732
|
+
_condition = type(_value_1) == "table"
|
|
733
|
+
if _condition then
|
|
734
|
+
_condition = getmetatable(value) == nil
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
if _condition then
|
|
738
|
+
a:step(path)
|
|
739
|
+
local _variants = plan.variants
|
|
740
|
+
local _arg0 = value[key]
|
|
741
|
+
local variant = _variants[_arg0]
|
|
742
|
+
if variant == nil then
|
|
743
|
+
a:reject(path, "no variant for its discriminant")
|
|
744
|
+
end
|
|
745
|
+
return admitNode(a, variant, value, path)
|
|
746
|
+
end
|
|
747
|
+
local _kinds = plan.kinds
|
|
748
|
+
local _value_1 = value
|
|
749
|
+
local _arg0 = typeof(_value_1)
|
|
750
|
+
local branch = _kinds[_arg0]
|
|
751
|
+
if branch == nil then
|
|
752
|
+
a:reject(path, `no branch for {describe(value)}`)
|
|
753
|
+
end
|
|
754
|
+
return admitNode(a, branch, value, path)
|
|
755
|
+
end
|
|
756
|
+
--[[
|
|
757
|
+
*
|
|
758
|
+
* @internal
|
|
759
|
+
* Rule 49. Projects `value` through `plan`: undeclared keys are dropped unread, declared positions are checked, and
|
|
760
|
+
* the result is frozen. Throws "<name><path>: <reason>" for a rejected value, never the value itself, and
|
|
761
|
+
* `OverBudget` when a visit would pass `work`'s limit.
|
|
762
|
+
|
|
763
|
+
]]
|
|
764
|
+
local function admit(plan, name, value, work)
|
|
765
|
+
local a = Admission.new(name, work)
|
|
766
|
+
local result = admitNode(a, plan, value, "")
|
|
767
|
+
for _, entry in a.pending do
|
|
768
|
+
if not table.isfrozen(entry.value) then
|
|
769
|
+
table.freeze(entry.value)
|
|
770
|
+
end
|
|
771
|
+
local _value = entry.value
|
|
772
|
+
local _cost = entry.cost
|
|
773
|
+
costs[_value] = _cost
|
|
774
|
+
local _conforms = entry.plan.conforms
|
|
775
|
+
local _value_1 = entry.value
|
|
776
|
+
_conforms[_value_1] = true
|
|
777
|
+
end
|
|
778
|
+
return result
|
|
779
|
+
end
|
|
780
|
+
local BOUND = table.freeze({
|
|
781
|
+
bounded = true,
|
|
782
|
+
})
|
|
783
|
+
--[[
|
|
784
|
+
*
|
|
785
|
+
* @internal
|
|
786
|
+
* Rule 61. Structural equality of two admitted values, free across `rawequal` subtrees. One visit per node pair and
|
|
787
|
+
* per key iterated, charged to `work`; past SYNC_FRAME_VALUE_VISITS it answers not equal.
|
|
788
|
+
|
|
789
|
+
]]
|
|
790
|
+
local function equal(a, b, work)
|
|
791
|
+
local spent = 0
|
|
792
|
+
local function visit()
|
|
793
|
+
if work ~= nil then
|
|
794
|
+
spend(work)
|
|
795
|
+
end
|
|
796
|
+
spent += 1
|
|
797
|
+
if spent > SYNC_FRAME_VALUE_VISITS then
|
|
798
|
+
error(BOUND)
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
local function same(x, y)
|
|
802
|
+
visit()
|
|
803
|
+
if rawequal(x, y) then
|
|
804
|
+
return true
|
|
805
|
+
end
|
|
806
|
+
local _x = x
|
|
807
|
+
local _condition = not (type(_x) == "table")
|
|
808
|
+
if not _condition then
|
|
809
|
+
local _y = y
|
|
810
|
+
_condition = not (type(_y) == "table")
|
|
811
|
+
end
|
|
812
|
+
if _condition then
|
|
813
|
+
local _x_1 = x
|
|
814
|
+
local _exp = typeof(_x_1)
|
|
815
|
+
local _y = y
|
|
816
|
+
local _condition_1 = _exp == typeof(_y)
|
|
817
|
+
if _condition_1 then
|
|
818
|
+
_condition_1 = x == y
|
|
819
|
+
end
|
|
820
|
+
return _condition_1
|
|
821
|
+
end
|
|
822
|
+
local xs = x
|
|
823
|
+
local ys = y
|
|
824
|
+
if #xs > 0 or #ys > 0 then
|
|
825
|
+
if #xs ~= #ys then
|
|
826
|
+
return false
|
|
827
|
+
end
|
|
828
|
+
for i = 0, #ys - 1 do
|
|
829
|
+
visit()
|
|
830
|
+
if not same(xs[i + 1], ys[i + 1]) then
|
|
831
|
+
return false
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
return true
|
|
835
|
+
end
|
|
836
|
+
local xr = x
|
|
837
|
+
local count = 0
|
|
838
|
+
for key, child in pairs(y) do
|
|
839
|
+
visit()
|
|
840
|
+
count += 1
|
|
841
|
+
local other = xr[key]
|
|
842
|
+
if other == nil or not same(other, child) then
|
|
843
|
+
return false
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
local seen = 0
|
|
847
|
+
for _element, _element_1 in pairs(xr) do
|
|
848
|
+
local _ = { _element, _element_1 }
|
|
849
|
+
visit()
|
|
850
|
+
seen += 1
|
|
851
|
+
if seen > count then
|
|
852
|
+
return false
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
return seen == count
|
|
856
|
+
end
|
|
857
|
+
local ok, result = pcall(same, a, b)
|
|
858
|
+
if ok then
|
|
859
|
+
return result
|
|
860
|
+
end
|
|
861
|
+
if rawequal(result, BOUND) then
|
|
862
|
+
return false
|
|
863
|
+
end
|
|
864
|
+
error(result, 0)
|
|
865
|
+
end
|
|
866
|
+
--[[
|
|
867
|
+
*
|
|
868
|
+
* @internal
|
|
869
|
+
* Rule 63, client. The value to store for a `decoded` one: every subtree structurally equal to the matching subtree
|
|
870
|
+
* of `current` is that subtree, so unchanged parts keep their identity. Returns `current` when the whole value is
|
|
871
|
+
* equal. `decoded` is a fresh tree, so its new tables are frozen in place.
|
|
872
|
+
|
|
873
|
+
]]
|
|
874
|
+
local function reuse(current, decoded)
|
|
875
|
+
local _decoded = decoded
|
|
876
|
+
if not (type(_decoded) == "table") then
|
|
877
|
+
local _condition = rawequal(current, decoded)
|
|
878
|
+
if not _condition then
|
|
879
|
+
local _current = current
|
|
880
|
+
local _exp = typeof(_current)
|
|
881
|
+
local _decoded_1 = decoded
|
|
882
|
+
local _condition_1 = _exp == typeof(_decoded_1)
|
|
883
|
+
if _condition_1 then
|
|
884
|
+
_condition_1 = current == decoded
|
|
885
|
+
end
|
|
886
|
+
_condition = _condition_1
|
|
887
|
+
end
|
|
888
|
+
return if _condition then current else decoded
|
|
889
|
+
end
|
|
890
|
+
local _current = current
|
|
891
|
+
local old = if type(_current) == "table" then current else nil
|
|
892
|
+
local fresh = decoded
|
|
893
|
+
local same = old ~= nil
|
|
894
|
+
local count = 0
|
|
895
|
+
for key, child in pairs(fresh) do
|
|
896
|
+
count += 1
|
|
897
|
+
local _before = old
|
|
898
|
+
if _before ~= nil then
|
|
899
|
+
_before = _before[key]
|
|
900
|
+
end
|
|
901
|
+
local before = _before
|
|
902
|
+
local result = reuse(before, child)
|
|
903
|
+
if not rawequal(result, child) then
|
|
904
|
+
fresh[key] = result
|
|
905
|
+
end
|
|
906
|
+
if before == nil or not rawequal(result, before) then
|
|
907
|
+
same = false
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
if same then
|
|
911
|
+
local seen = 0
|
|
912
|
+
for _element, _element_1 in pairs(old) do
|
|
913
|
+
local _ = { _element, _element_1 }
|
|
914
|
+
seen += 1
|
|
915
|
+
end
|
|
916
|
+
if seen == count then
|
|
917
|
+
return old
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
return table.freeze(fresh)
|
|
921
|
+
end
|
|
922
|
+
return {
|
|
923
|
+
isOverBudget = isOverBudget,
|
|
924
|
+
spend = spend,
|
|
925
|
+
compilePlan = compilePlan,
|
|
926
|
+
costOf = costOf,
|
|
927
|
+
admit = admit,
|
|
928
|
+
equal = equal,
|
|
929
|
+
reuse = reuse,
|
|
930
|
+
}
|