1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: set ts=8 sts=2 et sw=2 tw=80:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /* Implementation of the Intl.PluralRules proposal. */
8 
9 #include "builtin/intl/PluralRules.h"
10 
11 #include "mozilla/Assertions.h"
12 #include "mozilla/Casting.h"
13 
14 #include "builtin/Array.h"
15 #include "builtin/intl/CommonFunctions.h"
16 #include "builtin/intl/NumberFormat.h"
17 #include "builtin/intl/ScopedICUObject.h"
18 #include "gc/FreeOp.h"
19 #include "js/CharacterEncoding.h"
20 #include "js/PropertySpec.h"
21 #include "unicode/uenum.h"
22 #include "unicode/uloc.h"
23 #include "unicode/unumberformatter.h"
24 #include "unicode/upluralrules.h"
25 #include "unicode/utypes.h"
26 #include "vm/GlobalObject.h"
27 #include "vm/JSContext.h"
28 #include "vm/PlainObject.h"  // js::PlainObject
29 #include "vm/StringType.h"
30 
31 #include "vm/JSObject-inl.h"
32 #include "vm/NativeObject-inl.h"
33 
34 using namespace js;
35 
36 using mozilla::AssertedCast;
37 
38 using js::intl::CallICU;
39 using js::intl::IcuLocale;
40 
41 const JSClassOps PluralRulesObject::classOps_ = {
42     nullptr,                      // addProperty
43     nullptr,                      // delProperty
44     nullptr,                      // enumerate
45     nullptr,                      // newEnumerate
46     nullptr,                      // resolve
47     nullptr,                      // mayResolve
48     PluralRulesObject::finalize,  // finalize
49     nullptr,                      // call
50     nullptr,                      // hasInstance
51     nullptr,                      // construct
52     nullptr,                      // trace
53 };
54 
55 const JSClass PluralRulesObject::class_ = {
56     js_Object_str,
57     JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) |
58         JSCLASS_HAS_CACHED_PROTO(JSProto_PluralRules) |
59         JSCLASS_FOREGROUND_FINALIZE,
60     &PluralRulesObject::classOps_, &PluralRulesObject::classSpec_};
61 
62 const JSClass& PluralRulesObject::protoClass_ = PlainObject::class_;
63 
pluralRules_toSource(JSContext * cx,unsigned argc,Value * vp)64 static bool pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) {
65   CallArgs args = CallArgsFromVp(argc, vp);
66   args.rval().setString(cx->names().PluralRules);
67   return true;
68 }
69 
70 static const JSFunctionSpec pluralRules_static_methods[] = {
71     JS_SELF_HOSTED_FN("supportedLocalesOf",
72                       "Intl_PluralRules_supportedLocalesOf", 1, 0),
73     JS_FS_END};
74 
75 static const JSFunctionSpec pluralRules_methods[] = {
76     JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0,
77                       0),
78     JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0),
79     JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), JS_FS_END};
80 
81 static bool PluralRules(JSContext* cx, unsigned argc, Value* vp);
82 
83 const ClassSpec PluralRulesObject::classSpec_ = {
84     GenericCreateConstructor<PluralRules, 0, gc::AllocKind::FUNCTION>,
85     GenericCreatePrototype<PluralRulesObject>,
86     pluralRules_static_methods,
87     nullptr,
88     pluralRules_methods,
89     nullptr,
90     nullptr,
91     ClassSpec::DontDefineConstructor};
92 
93 /**
94  * PluralRules constructor.
95  * Spec: ECMAScript 402 API, PluralRules, 13.2.1
96  */
PluralRules(JSContext * cx,unsigned argc,Value * vp)97 static bool PluralRules(JSContext* cx, unsigned argc, Value* vp) {
98   CallArgs args = CallArgsFromVp(argc, vp);
99 
100   // Step 1.
101   if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules")) {
102     return false;
103   }
104 
105   // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
106   RootedObject proto(cx);
107   if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PluralRules,
108                                           &proto)) {
109     return false;
110   }
111 
112   Rooted<PluralRulesObject*> pluralRules(cx);
113   pluralRules = NewObjectWithClassProto<PluralRulesObject>(cx, proto);
114   if (!pluralRules) {
115     return false;
116   }
117 
118   HandleValue locales = args.get(0);
119   HandleValue options = args.get(1);
120 
121   // Step 3.
122   if (!intl::InitializeObject(cx, pluralRules,
123                               cx->names().InitializePluralRules, locales,
124                               options)) {
125     return false;
126   }
127 
128   args.rval().setObject(*pluralRules);
129   return true;
130 }
131 
finalize(JSFreeOp * fop,JSObject * obj)132 void js::PluralRulesObject::finalize(JSFreeOp* fop, JSObject* obj) {
133   MOZ_ASSERT(fop->onMainThread());
134 
135   auto* pluralRules = &obj->as<PluralRulesObject>();
136   UPluralRules* pr = pluralRules->getPluralRules();
137   UNumberFormatter* nf = pluralRules->getNumberFormatter();
138   UFormattedNumber* formatted = pluralRules->getFormattedNumber();
139 
140   if (pr) {
141     intl::RemoveICUCellMemory(
142         fop, obj, PluralRulesObject::UPluralRulesEstimatedMemoryUse);
143   }
144   if (nf) {
145     intl::RemoveICUCellMemory(
146         fop, obj, PluralRulesObject::UNumberFormatterEstimatedMemoryUse);
147 
148     // UFormattedNumber memory tracked as part of UNumberFormatter.
149   }
150 
151   if (pr) {
152     uplrules_close(pr);
153   }
154   if (nf) {
155     unumf_close(nf);
156   }
157   if (formatted) {
158     unumf_closeResult(formatted);
159   }
160 }
161 
162 /**
163  * This creates a new UNumberFormatter with calculated digit formatting
164  * properties for PluralRules.
165  *
166  * This is similar to NewUNumberFormatter but doesn't allow for currency or
167  * percent types.
168  */
NewUNumberFormatterForPluralRules(JSContext * cx,Handle<PluralRulesObject * > pluralRules)169 static UNumberFormatter* NewUNumberFormatterForPluralRules(
170     JSContext* cx, Handle<PluralRulesObject*> pluralRules) {
171   RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules));
172   if (!internals) {
173     return nullptr;
174   }
175 
176   RootedValue value(cx);
177 
178   if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
179     return nullptr;
180   }
181   UniqueChars locale = intl::EncodeLocale(cx, value.toString());
182   if (!locale) {
183     return nullptr;
184   }
185 
186   intl::NumberFormatterSkeleton skeleton(cx);
187 
188   bool hasMinimumSignificantDigits;
189   if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits,
190                    &hasMinimumSignificantDigits)) {
191     return nullptr;
192   }
193 
194   if (hasMinimumSignificantDigits) {
195     if (!GetProperty(cx, internals, internals,
196                      cx->names().minimumSignificantDigits, &value)) {
197       return nullptr;
198     }
199     uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
200 
201     if (!GetProperty(cx, internals, internals,
202                      cx->names().maximumSignificantDigits, &value)) {
203       return nullptr;
204     }
205     uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32());
206 
207     if (!skeleton.significantDigits(minimumSignificantDigits,
208                                     maximumSignificantDigits)) {
209       return nullptr;
210     }
211   } else {
212     if (!GetProperty(cx, internals, internals,
213                      cx->names().minimumFractionDigits, &value)) {
214       return nullptr;
215     }
216     uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
217 
218     if (!GetProperty(cx, internals, internals,
219                      cx->names().maximumFractionDigits, &value)) {
220       return nullptr;
221     }
222     uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
223 
224     if (!skeleton.fractionDigits(minimumFractionDigits,
225                                  maximumFractionDigits)) {
226       return nullptr;
227     }
228   }
229 
230   if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
231                    &value)) {
232     return nullptr;
233   }
234   uint32_t minimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
235 
236   if (!skeleton.integerWidth(minimumIntegerDigits)) {
237     return nullptr;
238   }
239 
240   if (!skeleton.roundingModeHalfUp()) {
241     return nullptr;
242   }
243 
244   return skeleton.toFormatter(cx, locale.get());
245 }
246 
NewUFormattedNumberForPluralRules(JSContext * cx)247 static UFormattedNumber* NewUFormattedNumberForPluralRules(JSContext* cx) {
248   UErrorCode status = U_ZERO_ERROR;
249   UFormattedNumber* formatted = unumf_openResult(&status);
250   if (U_FAILURE(status)) {
251     intl::ReportInternalError(cx);
252     return nullptr;
253   }
254   return formatted;
255 }
256 
257 /**
258  * Returns a new UPluralRules with the locale and type options of the given
259  * PluralRules.
260  */
NewUPluralRules(JSContext * cx,Handle<PluralRulesObject * > pluralRules)261 static UPluralRules* NewUPluralRules(JSContext* cx,
262                                      Handle<PluralRulesObject*> pluralRules) {
263   RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules));
264   if (!internals) {
265     return nullptr;
266   }
267 
268   RootedValue value(cx);
269 
270   if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
271     return nullptr;
272   }
273   UniqueChars locale = intl::EncodeLocale(cx, value.toString());
274   if (!locale) {
275     return nullptr;
276   }
277 
278   if (!GetProperty(cx, internals, internals, cx->names().type, &value)) {
279     return nullptr;
280   }
281 
282   UPluralType category;
283   {
284     JSLinearString* type = value.toString()->ensureLinear(cx);
285     if (!type) {
286       return nullptr;
287     }
288 
289     if (StringEqualsLiteral(type, "cardinal")) {
290       category = UPLURAL_TYPE_CARDINAL;
291     } else {
292       MOZ_ASSERT(StringEqualsLiteral(type, "ordinal"));
293       category = UPLURAL_TYPE_ORDINAL;
294     }
295   }
296 
297   UErrorCode status = U_ZERO_ERROR;
298   UPluralRules* pr =
299       uplrules_openForType(IcuLocale(locale.get()), category, &status);
300   if (U_FAILURE(status)) {
301     intl::ReportInternalError(cx);
302     return nullptr;
303   }
304   return pr;
305 }
306 
intl_SelectPluralRule(JSContext * cx,unsigned argc,Value * vp)307 bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) {
308   CallArgs args = CallArgsFromVp(argc, vp);
309   MOZ_ASSERT(args.length() == 2);
310 
311   Rooted<PluralRulesObject*> pluralRules(
312       cx, &args[0].toObject().as<PluralRulesObject>());
313 
314   double x = args[1].toNumber();
315 
316   // Obtain a cached UPluralRules object.
317   UPluralRules* pr = pluralRules->getPluralRules();
318   if (!pr) {
319     pr = NewUPluralRules(cx, pluralRules);
320     if (!pr) {
321       return false;
322     }
323     pluralRules->setPluralRules(pr);
324 
325     intl::AddICUCellMemory(pluralRules,
326                            PluralRulesObject::UPluralRulesEstimatedMemoryUse);
327   }
328 
329   // Obtain a cached UNumberFormat object.
330   UNumberFormatter* nf = pluralRules->getNumberFormatter();
331   if (!nf) {
332     nf = NewUNumberFormatterForPluralRules(cx, pluralRules);
333     if (!nf) {
334       return false;
335     }
336     pluralRules->setNumberFormatter(nf);
337 
338     intl::AddICUCellMemory(
339         pluralRules, PluralRulesObject::UNumberFormatterEstimatedMemoryUse);
340   }
341 
342   // Obtain a cached UFormattedNumber object.
343   UFormattedNumber* formatted = pluralRules->getFormattedNumber();
344   if (!formatted) {
345     formatted = NewUFormattedNumberForPluralRules(cx);
346     if (!formatted) {
347       return false;
348     }
349     pluralRules->setFormattedNumber(formatted);
350 
351     // UFormattedNumber memory tracked as part of UNumberFormatter.
352   }
353 
354   UErrorCode status = U_ZERO_ERROR;
355   unumf_formatDouble(nf, x, formatted, &status);
356   if (U_FAILURE(status)) {
357     intl::ReportInternalError(cx);
358     return false;
359   }
360 
361   JSString* str = CallICU(
362       cx, [pr, formatted](UChar* chars, int32_t size, UErrorCode* status) {
363         return uplrules_selectFormatted(pr, formatted, chars, size, status);
364       });
365   if (!str) {
366     return false;
367   }
368 
369   args.rval().setString(str);
370   return true;
371 }
372 
intl_GetPluralCategories(JSContext * cx,unsigned argc,Value * vp)373 bool js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) {
374   CallArgs args = CallArgsFromVp(argc, vp);
375   MOZ_ASSERT(args.length() == 1);
376 
377   Rooted<PluralRulesObject*> pluralRules(
378       cx, &args[0].toObject().as<PluralRulesObject>());
379 
380   // Obtain a cached UPluralRules object.
381   UPluralRules* pr = pluralRules->getPluralRules();
382   if (!pr) {
383     pr = NewUPluralRules(cx, pluralRules);
384     if (!pr) {
385       return false;
386     }
387     pluralRules->setPluralRules(pr);
388 
389     intl::AddICUCellMemory(pluralRules,
390                            PluralRulesObject::UPluralRulesEstimatedMemoryUse);
391   }
392 
393   UErrorCode status = U_ZERO_ERROR;
394   UEnumeration* ue = uplrules_getKeywords(pr, &status);
395   if (U_FAILURE(status)) {
396     intl::ReportInternalError(cx);
397     return false;
398   }
399   ScopedICUObject<UEnumeration, uenum_close> closeEnum(ue);
400 
401   RootedObject res(cx, NewDenseEmptyArray(cx));
402   if (!res) {
403     return false;
404   }
405 
406   do {
407     int32_t catSize;
408     const char* cat = uenum_next(ue, &catSize, &status);
409     if (U_FAILURE(status)) {
410       intl::ReportInternalError(cx);
411       return false;
412     }
413 
414     if (!cat) {
415       break;
416     }
417 
418     MOZ_ASSERT(catSize >= 0);
419     JSString* str = NewStringCopyN<CanGC>(cx, cat, catSize);
420     if (!str) {
421       return false;
422     }
423 
424     if (!NewbornArrayPush(cx, res, StringValue(str))) {
425       return false;
426     }
427   } while (true);
428 
429   args.rval().setObject(*res);
430   return true;
431 }
432