зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1449213: Create WebAssembly.Global objects for imported globals that received a primitive; r=luke
--HG-- extra : histedit_source : 89c921ca195814b992b9f3a0e09a3d6e3b07bfe9%2C98d3ade79462e054c7ee2984182d47771a3b4cd2
This commit is contained in:
Родитель
76ddd6f74d
Коммит
12c3f9f227
|
@ -109,9 +109,9 @@ assertEq(module.f, module.tbl.get(1));
|
|||
// Import/export rules.
|
||||
if (typeof WebAssembly.Global === "undefined") {
|
||||
wasmFailValidateText(`(module (import "globals" "x" (global (mut i32))))`,
|
||||
/can't import.* mutable globals in the MVP/);
|
||||
/can't import.* mutable globals in the MVP/);
|
||||
wasmFailValidateText(`(module (global (mut i32) (i32.const 42)) (export "" global 0))`,
|
||||
/can't .*export mutable globals in the MVP/);
|
||||
/can't .*export mutable globals in the MVP/);
|
||||
}
|
||||
|
||||
// Import/export semantics.
|
||||
|
@ -130,6 +130,11 @@ if (typeof WebAssembly.Global === "function")
|
|||
else
|
||||
assertEq(module.value, 42);
|
||||
|
||||
assertEq(wasmEvalText(`(module
|
||||
(global (import "a" "b") (mut i32))
|
||||
(func (export "get") (result i32) get_global 0)
|
||||
)`, { a: { b: 42 } }).exports.get(), 42);
|
||||
|
||||
// Can only import numbers (no implicit coercions).
|
||||
module = new WebAssembly.Module(wasmTextToBinary(`(module
|
||||
(global (import "globs" "i32") i32)
|
||||
|
@ -229,18 +234,18 @@ function testInitExpr(type, initialValue, nextValue, coercion, assertFunc = asse
|
|||
assertFunc(module.get1(), coercion(initialValue));
|
||||
// See comment earlier about WebAssembly.Global
|
||||
if (typeof WebAssembly.Global === "function")
|
||||
assertFunc(Number(module.global_imm), coercion(initialValue));
|
||||
assertFunc(Number(module.global_imm), coercion(initialValue));
|
||||
else
|
||||
assertFunc(module.global_imm, coercion(initialValue));
|
||||
assertFunc(module.global_imm, coercion(initialValue));
|
||||
|
||||
assertEq(module.set1(coercion(nextValue)), undefined);
|
||||
assertFunc(module.get1(), coercion(nextValue));
|
||||
assertFunc(module.get0(), coercion(initialValue));
|
||||
// See comment earlier about WebAssembly.Global
|
||||
if (typeof WebAssembly.Global === "function")
|
||||
assertFunc(Number(module.global_imm), coercion(initialValue));
|
||||
assertFunc(Number(module.global_imm), coercion(initialValue));
|
||||
else
|
||||
assertFunc(module.global_imm, coercion(initialValue));
|
||||
assertFunc(module.global_imm, coercion(initialValue));
|
||||
|
||||
assertFunc(module.get_cst(), coercion(initialValue));
|
||||
}
|
||||
|
@ -273,18 +278,18 @@ else {
|
|||
// import it into another.
|
||||
|
||||
let i = new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global (export "g") i64 (i64.const 37))
|
||||
(global (export "h") (mut i64) (i64.const 37)))`)));
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global (export "g") i64 (i64.const 37))
|
||||
(global (export "h") (mut i64) (i64.const 37)))`)));
|
||||
|
||||
let j = new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "globals" "g" (global i64))
|
||||
(func (export "f") (result i32)
|
||||
(i64.eq (get_global 0) (i64.const 37))))`)),
|
||||
{globals: {g: i.exports.g}});
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "globals" "g" (global i64))
|
||||
(func (export "f") (result i32)
|
||||
(i64.eq (get_global 0) (i64.const 37))))`)),
|
||||
{globals: {g: i.exports.g}});
|
||||
|
||||
assertEq(j.exports.f(), 1);
|
||||
|
||||
|
@ -346,17 +351,17 @@ if (typeof WebAssembly.Global === "function") {
|
|||
|
||||
// These types should not work:
|
||||
assertErrorMessage(() => new WebAssembly.Global({type: "i64"}),
|
||||
TypeError,
|
||||
/bad type for a WebAssembly.Global/);
|
||||
TypeError,
|
||||
/bad type for a WebAssembly.Global/);
|
||||
assertErrorMessage(() => new WebAssembly.Global({}),
|
||||
TypeError,
|
||||
/bad type for a WebAssembly.Global/);
|
||||
TypeError,
|
||||
/bad type for a WebAssembly.Global/);
|
||||
assertErrorMessage(() => new WebAssembly.Global({type: "fnord"}),
|
||||
TypeError,
|
||||
/bad type for a WebAssembly.Global/);
|
||||
TypeError,
|
||||
/bad type for a WebAssembly.Global/);
|
||||
assertErrorMessage(() => new WebAssembly.Global(),
|
||||
TypeError,
|
||||
/WebAssembly.Global requires more than 0 arguments/);
|
||||
TypeError,
|
||||
/WebAssembly.Global requires more than 0 arguments/);
|
||||
|
||||
// Coercion of init value; ".value" accessor
|
||||
assertEq((new WebAssembly.Global({type: "i32", value: 3.14})).value, 3);
|
||||
|
@ -366,12 +371,12 @@ if (typeof WebAssembly.Global === "function") {
|
|||
assertEq((new WebAssembly.Global({type: "i32", value: NaN})).value, 0);
|
||||
|
||||
{
|
||||
// "value" is enumerable
|
||||
let x = new WebAssembly.Global({type: "i32"});
|
||||
let s = "";
|
||||
for ( let i in x )
|
||||
s = s + i + ",";
|
||||
assertEq(s, "value,");
|
||||
// "value" is enumerable
|
||||
let x = new WebAssembly.Global({type: "i32"});
|
||||
let s = "";
|
||||
for ( let i in x )
|
||||
s = s + i + ",";
|
||||
assertEq(s, "value,");
|
||||
}
|
||||
|
||||
// "value" is defined on the prototype, not on the object
|
||||
|
@ -379,142 +384,142 @@ if (typeof WebAssembly.Global === "function") {
|
|||
|
||||
// Can't set the value of an immutable global
|
||||
assertErrorMessage(() => (new WebAssembly.Global({type: "i32"})).value = 10,
|
||||
TypeError,
|
||||
/can't set value of immutable global/);
|
||||
TypeError,
|
||||
/can't set value of immutable global/);
|
||||
|
||||
{
|
||||
// Can set the value of a mutable global
|
||||
let g = new WebAssembly.Global({type: "i32", mutable: true, value: 37});
|
||||
g.value = 10;
|
||||
assertEq(g.value, 10);
|
||||
// Can set the value of a mutable global
|
||||
let g = new WebAssembly.Global({type: "i32", mutable: true, value: 37});
|
||||
g.value = 10;
|
||||
assertEq(g.value, 10);
|
||||
}
|
||||
|
||||
{
|
||||
// Misc internal conversions
|
||||
let g = new WebAssembly.Global({type: "i32", value: 42});
|
||||
// Misc internal conversions
|
||||
let g = new WebAssembly.Global({type: "i32", value: 42});
|
||||
|
||||
// valueOf
|
||||
assertEq(g - 5, 37);
|
||||
// valueOf
|
||||
assertEq(g - 5, 37);
|
||||
|
||||
// @@toStringTag
|
||||
assertEq(g.toString(), "[object WebAssembly.Global]");
|
||||
// @@toStringTag
|
||||
assertEq(g.toString(), "[object WebAssembly.Global]");
|
||||
}
|
||||
|
||||
{
|
||||
// An exported global should appear as a WebAssembly.Global instance:
|
||||
let i =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module (global (export "g") i32 (i32.const 42)))`)));
|
||||
// An exported global should appear as a WebAssembly.Global instance:
|
||||
let i =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module (global (export "g") i32 (i32.const 42)))`)));
|
||||
|
||||
assertEq(typeof i.exports.g, "object");
|
||||
assertEq(i.exports.g instanceof WebAssembly.Global, true);
|
||||
assertEq(typeof i.exports.g, "object");
|
||||
assertEq(i.exports.g instanceof WebAssembly.Global, true);
|
||||
|
||||
// An exported global can be imported into another instance even if
|
||||
// it is an object:
|
||||
let j =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global (import "" "g") i32)
|
||||
(func (export "f") (result i32)
|
||||
(get_global 0)))`)),
|
||||
{ "": { "g": i.exports.g }});
|
||||
// An exported global can be imported into another instance even if
|
||||
// it is an object:
|
||||
let j =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global (import "" "g") i32)
|
||||
(func (export "f") (result i32)
|
||||
(get_global 0)))`)),
|
||||
{ "": { "g": i.exports.g }});
|
||||
|
||||
// And when it is then accessed it has the right value:
|
||||
assertEq(j.exports.f(), 42);
|
||||
// And when it is then accessed it has the right value:
|
||||
assertEq(j.exports.f(), 42);
|
||||
}
|
||||
|
||||
// Identity of WebAssembly.Global objects (independent of mutablity).
|
||||
{
|
||||
// When a global is exported twice, the two objects are the same.
|
||||
let i =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global i32 (i32.const 0))
|
||||
(export "a" global 0)
|
||||
(export "b" global 0))`)));
|
||||
assertEq(i.exports.a, i.exports.b);
|
||||
// When a global is exported twice, the two objects are the same.
|
||||
let i =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global i32 (i32.const 0))
|
||||
(export "a" global 0)
|
||||
(export "b" global 0))`)));
|
||||
assertEq(i.exports.a, i.exports.b);
|
||||
|
||||
// When a global is imported and then exported, the exported object is
|
||||
// the same as the imported object.
|
||||
let j =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "" "a" (global i32))
|
||||
(export "x" global 0))`)),
|
||||
{ "": {a: i.exports.a}});
|
||||
// When a global is imported and then exported, the exported object is
|
||||
// the same as the imported object.
|
||||
let j =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "" "a" (global i32))
|
||||
(export "x" global 0))`)),
|
||||
{ "": {a: i.exports.a}});
|
||||
|
||||
assertEq(i.exports.a, j.exports.x);
|
||||
assertEq(i.exports.a, j.exports.x);
|
||||
|
||||
// When a global is imported twice (ie aliased) and then exported twice,
|
||||
// the exported objects are the same, and are also the same as the
|
||||
// imported object.
|
||||
let k =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "" "a" (global i32))
|
||||
(import "" "b" (global i32))
|
||||
(export "x" global 0)
|
||||
(export "y" global 1))`)),
|
||||
{ "": {a: i.exports.a,
|
||||
b: i.exports.a}});
|
||||
// When a global is imported twice (ie aliased) and then exported twice,
|
||||
// the exported objects are the same, and are also the same as the
|
||||
// imported object.
|
||||
let k =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "" "a" (global i32))
|
||||
(import "" "b" (global i32))
|
||||
(export "x" global 0)
|
||||
(export "y" global 1))`)),
|
||||
{ "": {a: i.exports.a,
|
||||
b: i.exports.a}});
|
||||
|
||||
assertEq(i.exports.a, k.exports.x);
|
||||
assertEq(k.exports.x, k.exports.y);
|
||||
assertEq(i.exports.a, k.exports.x);
|
||||
assertEq(k.exports.x, k.exports.y);
|
||||
}
|
||||
|
||||
// Mutability
|
||||
{
|
||||
let i =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global (export "g") (mut i32) (i32.const 37))
|
||||
(func (export "getter") (result i32)
|
||||
(get_global 0))
|
||||
(func (export "setter") (param i32)
|
||||
(set_global 0 (get_local 0))))`)));
|
||||
let i =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(global (export "g") (mut i32) (i32.const 37))
|
||||
(func (export "getter") (result i32)
|
||||
(get_global 0))
|
||||
(func (export "setter") (param i32)
|
||||
(set_global 0 (get_local 0))))`)));
|
||||
|
||||
let j =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "" "g" (global (mut i32)))
|
||||
(func (export "getter") (result i32)
|
||||
(get_global 0))
|
||||
(func (export "setter") (param i32)
|
||||
(set_global 0 (get_local 0))))`)),
|
||||
{"": {g: i.exports.g}});
|
||||
let j =
|
||||
new WebAssembly.Instance(
|
||||
new WebAssembly.Module(
|
||||
wasmTextToBinary(`(module
|
||||
(import "" "g" (global (mut i32)))
|
||||
(func (export "getter") (result i32)
|
||||
(get_global 0))
|
||||
(func (export "setter") (param i32)
|
||||
(set_global 0 (get_local 0))))`)),
|
||||
{"": {g: i.exports.g}});
|
||||
|
||||
// Initial values
|
||||
assertEq(i.exports.g.value, 37);
|
||||
assertEq(i.exports.getter(), 37);
|
||||
assertEq(j.exports.getter(), 37);
|
||||
// Initial values
|
||||
assertEq(i.exports.g.value, 37);
|
||||
assertEq(i.exports.getter(), 37);
|
||||
assertEq(j.exports.getter(), 37);
|
||||
|
||||
// Set in i, observe everywhere
|
||||
i.exports.setter(42);
|
||||
// Set in i, observe everywhere
|
||||
i.exports.setter(42);
|
||||
|
||||
assertEq(i.exports.g.value, 42);
|
||||
assertEq(i.exports.getter(), 42);
|
||||
assertEq(j.exports.getter(), 42);
|
||||
assertEq(i.exports.g.value, 42);
|
||||
assertEq(i.exports.getter(), 42);
|
||||
assertEq(j.exports.getter(), 42);
|
||||
|
||||
// Set in j, observe everywhere
|
||||
j.exports.setter(78);
|
||||
// Set in j, observe everywhere
|
||||
j.exports.setter(78);
|
||||
|
||||
assertEq(i.exports.g.value, 78);
|
||||
assertEq(i.exports.getter(), 78);
|
||||
assertEq(j.exports.getter(), 78);
|
||||
assertEq(i.exports.g.value, 78);
|
||||
assertEq(i.exports.getter(), 78);
|
||||
assertEq(j.exports.getter(), 78);
|
||||
|
||||
// Set on global object, observe everywhere
|
||||
i.exports.g.value = 197;
|
||||
// Set on global object, observe everywhere
|
||||
i.exports.g.value = 197;
|
||||
|
||||
assertEq(i.exports.g.value, 197);
|
||||
assertEq(i.exports.getter(), 197);
|
||||
assertEq(j.exports.getter(), 197);
|
||||
assertEq(i.exports.g.value, 197);
|
||||
assertEq(i.exports.getter(), 197);
|
||||
assertEq(j.exports.getter(), 197);
|
||||
}
|
||||
|
||||
// TEST THIS LAST
|
||||
|
|
|
@ -455,10 +455,11 @@ Instance::Instance(JSContext* cx,
|
|||
uint8_t* globalAddr = globalData() + global.offset();
|
||||
switch (global.kind()) {
|
||||
case GlobalKind::Import: {
|
||||
size_t imported = global.importIndex();
|
||||
if (global.isIndirect())
|
||||
*(void**)globalAddr = globalObjs[global.importIndex()]->cell();
|
||||
*(void**)globalAddr = globalObjs[imported]->cell();
|
||||
else
|
||||
globalImportValues[global.importIndex()].writePayload(globalAddr);
|
||||
globalImportValues[imported].writePayload(globalAddr);
|
||||
break;
|
||||
}
|
||||
case GlobalKind::Variable: {
|
||||
|
|
|
@ -249,7 +249,7 @@ GetImports(JSContext* cx,
|
|||
#if defined(ENABLE_WASM_GLOBAL) && defined(EARLY_BETA_OR_EARLIER)
|
||||
if (v.isObject() && v.toObject().is<WasmGlobalObject>()) {
|
||||
RootedWasmGlobalObject obj(cx, &v.toObject().as<WasmGlobalObject>());
|
||||
if (globalObjs.length() <= index && !globalObjs.resize(index+1)) {
|
||||
if (globalObjs.length() <= index && !globalObjs.resize(index + 1)) {
|
||||
ReportOutOfMemory(cx);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1018,40 +1018,69 @@ ExtractGlobalValue(const ValVector& globalImportValues, uint32_t globalIndex, co
|
|||
MOZ_CRASH("Not a global value");
|
||||
}
|
||||
|
||||
#if defined(ENABLE_WASM_GLOBAL) && defined(EARLY_BETA_OR_EARLIER)
|
||||
static bool
|
||||
EnsureGlobalObject(JSContext* cx, const ValVector& globalImportValues, size_t globalIndex,
|
||||
const GlobalDesc& global, WasmGlobalObjectVector& globalObjs)
|
||||
{
|
||||
if (globalIndex < globalObjs.length() && globalObjs[globalIndex])
|
||||
return true;
|
||||
|
||||
Val val = ExtractGlobalValue(globalImportValues, globalIndex, global);
|
||||
RootedWasmGlobalObject go(cx, WasmGlobalObject::create(cx, val, global.isMutable()));
|
||||
if (!go)
|
||||
return false;
|
||||
|
||||
if (globalObjs.length() <= globalIndex && !globalObjs.resize(globalIndex + 1)) {
|
||||
ReportOutOfMemory(cx);
|
||||
return false;
|
||||
}
|
||||
|
||||
globalObjs[globalIndex] = go;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool
|
||||
Module::instantiateGlobalExports(JSContext* cx,
|
||||
const ValVector& globalImportValues,
|
||||
WasmGlobalObjectVector& globalObjs) const
|
||||
Module::instantiateGlobals(JSContext* cx, const ValVector& globalImportValues,
|
||||
WasmGlobalObjectVector& globalObjs) const
|
||||
{
|
||||
#if defined(ENABLE_WASM_GLOBAL) && defined(EARLY_BETA_OR_EARLIER)
|
||||
// If there are exported globals that aren't in the globalObjs because they
|
||||
// If there are exported globals that aren't in globalObjs because they
|
||||
// originate in this module or because they were immutable imports that came
|
||||
// in as values (not cells) then we must create cells in the globalObjs for
|
||||
// in as primitive values then we must create cells in the globalObjs for
|
||||
// them here, as WasmInstanceObject::create() and CreateExportObject() will
|
||||
// need the cells to exist.
|
||||
|
||||
const GlobalDescVector& globals = metadata().globals;
|
||||
|
||||
for (const Export& exp : exports_) {
|
||||
if (exp.kind() == DefinitionKind::Global) {
|
||||
unsigned globalIndex = exp.globalIndex();
|
||||
|
||||
if (globalIndex >= globalObjs.length() || !globalObjs[globalIndex]) {
|
||||
const GlobalDesc& global = globals[globalIndex];
|
||||
|
||||
Val val = ExtractGlobalValue(globalImportValues, globalIndex, global);
|
||||
RootedWasmGlobalObject go(cx, WasmGlobalObject::create(cx, val,
|
||||
global.isMutable()));
|
||||
if (!go)
|
||||
return false;
|
||||
if (globalObjs.length() <= globalIndex && !globalObjs.resize(globalIndex+1)) {
|
||||
ReportOutOfMemory(cx);
|
||||
return false;
|
||||
}
|
||||
globalObjs[globalIndex] = go;
|
||||
}
|
||||
}
|
||||
if (exp.kind() != DefinitionKind::Global)
|
||||
continue;
|
||||
unsigned globalIndex = exp.globalIndex();
|
||||
const GlobalDesc& global = globals[globalIndex];
|
||||
if (!EnsureGlobalObject(cx, globalImportValues, globalIndex, global, globalObjs))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Imported globals may also have received only a primitive value, thus
|
||||
// they may need their own Global object, because the compiled code assumed
|
||||
// they were indirect.
|
||||
|
||||
size_t numGlobalImports = 0;
|
||||
for (const Import& import : imports_) {
|
||||
if (import.kind != DefinitionKind::Global)
|
||||
continue;
|
||||
size_t globalIndex = numGlobalImports++;
|
||||
const GlobalDesc& global = globals[globalIndex];
|
||||
MOZ_ASSERT(global.importIndex() == globalIndex);
|
||||
if (!global.isIndirect())
|
||||
continue;
|
||||
if (!EnsureGlobalObject(cx, globalImportValues, globalIndex, global, globalObjs))
|
||||
return false;
|
||||
}
|
||||
MOZ_ASSERT_IF(!metadata().isAsmJS(),
|
||||
numGlobalImports == globals.length() || !globals[numGlobalImports].isImport());
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
@ -1196,7 +1225,7 @@ Module::instantiate(JSContext* cx,
|
|||
if (!instantiateTable(cx, &table, &tables))
|
||||
return false;
|
||||
|
||||
if (!instantiateGlobalExports(cx, globalImportValues, globalObjs))
|
||||
if (!instantiateGlobals(cx, globalImportValues, globalObjs))
|
||||
return false;
|
||||
|
||||
UniqueTlsData tlsData = CreateTlsData(metadata().globalDataLength);
|
||||
|
|
|
@ -149,9 +149,8 @@ class Module : public JS::WasmModule
|
|||
bool instantiateTable(JSContext* cx,
|
||||
MutableHandleWasmTableObject table,
|
||||
SharedTableVector* tables) const;
|
||||
bool instantiateGlobalExports(JSContext* cx,
|
||||
const ValVector& globalImportValues,
|
||||
WasmGlobalObjectVector& globalObjs) const;
|
||||
bool instantiateGlobals(JSContext* cx, const ValVector& globalImportValues,
|
||||
WasmGlobalObjectVector& globalObjs) const;
|
||||
bool initSegments(JSContext* cx,
|
||||
HandleWasmInstanceObject instance,
|
||||
Handle<FunctionVector> funcImports,
|
||||
|
|
|
@ -806,7 +806,11 @@ class GlobalDesc
|
|||
//
|
||||
// We don't want to indirect unless we must, so only mutable, exposed
|
||||
// globals are indirected - in all other cases we copy values into and out
|
||||
// of them module.
|
||||
// of their module.
|
||||
//
|
||||
// Note that isIndirect() isn't equivalent to getting a WasmGlobalObject:
|
||||
// an immutable exported global will still get an object, but will not be
|
||||
// indirect.
|
||||
bool isIndirect() const { return isMutable() && isWasm() && (isImport() || isExport()); }
|
||||
|
||||
ValType type() const {
|
||||
|
|
Загрузка…
Ссылка в новой задаче