Add fast path for Smi and non-decimal in NumberPrototypeToString

Just a fast iteration over bytes written in Torque for Smi number and
non-decimal radix, also only for more than one string character result.

Improve following micro-benchmark by ~75%

Before
toHexString
toHexString-Numbers(Score): 7905000

After
toHexString
toHexString-Numbers(Score): 14419000

Bug: v8:10477
Change-Id: I366092d4d70156ad33830352c1122af8794bea76
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2330221
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Jakob Gruber <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69272}
diff --git a/src/builtins/base.tq b/src/builtins/base.tq
index 37cb1cf..e85769e 100644
--- a/src/builtins/base.tq
+++ b/src/builtins/base.tq
@@ -604,6 +604,7 @@
 extern macro ToObject_Inline(Context, JSAny): JSReceiver;
 extern macro IsNullOrUndefined(Object): bool;
 extern macro IsString(HeapObject): bool;
+extern macro IsSeqOneByteString(HeapObject): bool;
 extern transitioning builtin NonPrimitiveToPrimitive_String(
     Context, JSAny): JSPrimitive;
 extern transitioning builtin NonPrimitiveToPrimitive_Default(
diff --git a/src/builtins/cast.tq b/src/builtins/cast.tq
index c4f9a03..1562b7b 100644
--- a/src/builtins/cast.tq
+++ b/src/builtins/cast.tq
@@ -537,6 +537,11 @@
   return %RawDownCast<FastJSArrayForReadWithNoCustomIteration>(a);
 }
 
+Cast<SeqOneByteString>(o: HeapObject): SeqOneByteString labels CastError {
+  if (!IsSeqOneByteString(o)) goto CastError;
+  return %RawDownCast<SeqOneByteString>(o);
+}
+
 Cast<JSReceiver|Null>(o: HeapObject): JSReceiver|Null
     labels CastError {
   typeswitch (o) {
diff --git a/src/builtins/convert.tq b/src/builtins/convert.tq
index 670d475..d3b0f91 100644
--- a/src/builtins/convert.tq
+++ b/src/builtins/convert.tq
@@ -57,6 +57,12 @@
   static_assert(-128 <= i && i <= 127);
   return %RawDownCast<int8>(i);
 }
+FromConstexpr<char8, constexpr int31>(i: constexpr int31): char8 {
+  const i: int32 = i;
+  static_assert(i <= 255);
+  static_assert(0 <= i);
+  return %RawDownCast<char8>(i);
+}
 FromConstexpr<Number, constexpr Smi>(s: constexpr Smi): Number {
   return SmiConstant(s);
 }
diff --git a/src/builtins/number.tq b/src/builtins/number.tq
index 7539984..27313c2 100644
--- a/src/builtins/number.tq
+++ b/src/builtins/number.tq
@@ -61,6 +61,12 @@
       ToThisValue(receiver, PrimitiveType::kNumber, method));
 }
 
+macro ToCharCode(input: int32): char8 {
+  assert(0 <= input && input < 36);
+  return input < 10 ? %RawDownCast<char8>(input + kAsciiZero) :
+                      %RawDownCast<char8>(input - 10 + kAsciiLowerCaseA);
+}
+
 // https://tc39.github.io/ecma262/#sec-number.prototype.tostring
 transitioning javascript builtin NumberPrototypeToString(
     js-implicit context: NativeContext, receiver: JSAny)(...arguments): String {
@@ -86,15 +92,48 @@
   // 7. Return the String representation of this Number
   //    value using the radix specified by radixNumber.
 
-  // Fast case where the result is a one character string.
-  if (TaggedIsPositiveSmi(x) && x < radixNumber) {
-    let charCode = Convert<int32>(UnsafeCast<Smi>(x));
-    if (charCode < 10) {
-      charCode += kAsciiZero;
+  if (TaggedIsSmi(x)) {
+    const isNegative: bool = x < 0;
+    let n: int32 = Convert<int32>(UnsafeCast<Smi>(x));
+    if (!isNegative) {
+      // Fast case where the result is a one character string.
+      if (x < radixNumber) {
+        return StringFromSingleCharCode(ToCharCode(n));
+      }
     } else {
-      charCode = charCode - 10 + kAsciiLowerCaseA;
+      assert(isNegative);
+      if (n == kMinInt32) {
+        return runtime::DoubleToStringWithRadix(x, radixNumber);
+      }
+      n = 0 - n;
     }
-    return StringFromSingleCharCode(charCode);
+
+    const radix: int32 = Convert<int32>(radixNumber);
+    // Calculate length and pre-allocate the result string.
+    let temp: int32 = n;
+    let length: int32 = isNegative ? 1 : 0;
+    while (temp > 0) {
+      temp = temp / radix;
+      length = length + 1;
+    }
+    assert(length > 0);
+    const strSeq = UnsafeCast<SeqOneByteString>(
+        AllocateSeqOneByteString(Unsigned(length)));
+    let cursor: intptr = Convert<intptr>(length) - 1;
+    while (n > 0) {
+      const digit: int32 = n % radix;
+      n = n / radix;
+      strSeq.chars[cursor] = ToCharCode(digit);
+      cursor = cursor - 1;
+    }
+    if (isNegative) {
+      assert(cursor == 0);
+      // Insert '-' to result.
+      strSeq.chars[0] = 45;
+    } else {
+      assert(cursor == -1);
+    }
+    return strSeq;
   }
 
   if (x == -0) {
@@ -106,6 +145,7 @@
   } else if (x == MINUS_V8_INFINITY) {
     return MinusInfinityStringConstant();
   }
+
   return runtime::DoubleToStringWithRadix(x, radixNumber);
 }
 
diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc
index 1496c21..c912979 100644
--- a/src/codegen/code-stub-assembler.cc
+++ b/src/codegen/code-stub-assembler.cc
@@ -5811,6 +5811,15 @@
       Int32Constant(kSeqStringTag));
 }
 
+TNode<BoolT> CodeStubAssembler::IsSeqOneByteStringInstanceType(
+    TNode<Int32T> instance_type) {
+  CSA_ASSERT(this, IsStringInstanceType(instance_type));
+  return Word32Equal(
+      Word32And(instance_type,
+                Int32Constant(kStringRepresentationMask | kStringEncodingMask)),
+      Int32Constant(kSeqStringTag | kOneByteStringTag));
+}
+
 TNode<BoolT> CodeStubAssembler::IsConsStringInstanceType(
     SloppyTNode<Int32T> instance_type) {
   CSA_ASSERT(this, IsStringInstanceType(instance_type));
@@ -6082,6 +6091,10 @@
   return IsStringInstanceType(LoadInstanceType(object));
 }
 
+TNode<BoolT> CodeStubAssembler::IsSeqOneByteString(TNode<HeapObject> object) {
+  return IsSeqOneByteStringInstanceType(LoadInstanceType(object));
+}
+
 TNode<BoolT> CodeStubAssembler::IsSymbolInstanceType(
     SloppyTNode<Int32T> instance_type) {
   return InstanceTypeEqual(instance_type, SYMBOL_TYPE);
diff --git a/src/codegen/code-stub-assembler.h b/src/codegen/code-stub-assembler.h
index 567eccd..a584b96 100644
--- a/src/codegen/code-stub-assembler.h
+++ b/src/codegen/code-stub-assembler.h
@@ -2465,6 +2465,7 @@
   TNode<BoolT> IsNullOrUndefined(SloppyTNode<Object> object);
   TNode<BoolT> IsNumberDictionary(SloppyTNode<HeapObject> object);
   TNode<BoolT> IsOneByteStringInstanceType(TNode<Int32T> instance_type);
+  TNode<BoolT> IsSeqOneByteStringInstanceType(TNode<Int32T> instance_type);
   TNode<BoolT> IsPrimitiveInstanceType(SloppyTNode<Int32T> instance_type);
   TNode<BoolT> IsPrivateName(SloppyTNode<Symbol> symbol);
   TNode<BoolT> IsPropertyArray(SloppyTNode<HeapObject> object);
@@ -2492,6 +2493,8 @@
   TNode<BoolT> IsSpecialReceiverMap(SloppyTNode<Map> map);
   TNode<BoolT> IsStringInstanceType(SloppyTNode<Int32T> instance_type);
   TNode<BoolT> IsString(SloppyTNode<HeapObject> object);
+  TNode<BoolT> IsSeqOneByteString(TNode<HeapObject> object);
+
   TNode<BoolT> IsSymbolInstanceType(SloppyTNode<Int32T> instance_type);
   TNode<BoolT> IsInternalizedStringInstanceType(TNode<Int32T> instance_type);
   TNode<BoolT> IsUniqueName(TNode<HeapObject> object);