package quickjs

  1. Overview
  2. Docs

Source file Global.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
(** JavaScript Global object functions

    This module contains global functions from the JavaScript specification like
    parseFloat and parseInt. *)

module Atod = Quickjs_c.Atod
module Libunicode = Quickjs_c.Libunicode

(* JS_ATOD flags from dtoa.h *)
let js_atod_int_only = 1 lsl 0
let js_atod_accept_bin_oct = 1 lsl 1
let js_atod_accept_legacy_octal = 1 lsl 2
let js_atod_accept_underscores = 1 lsl 3

(* Workaround for QuickJS bug: incomplete exponents like "1e", "1e+", "1e-"
   return NaN instead of backtracking to parse "1".
   See: https://github.com/quickjs-ng/quickjs/issues/1259
   JavaScript spec: parseFloat("1e") === 1, parseFloat("1e+") === 1, parseFloat("1e-") === 1

   This function strips incomplete exponent suffixes from the end of a string.
   Only applies to radix 10 (decimal) parsing. *)
let strip_incomplete_exponent str =
  let len = Stdlib.String.length str in
  if len = 0 then str
  else
    (* Find the last non-whitespace character position *)
    let rec find_end i =
      if i < 0 then -1
      else
        let c = Stdlib.String.get str i in
        if c = ' ' || c = '\t' || c = '\n' || c = '\r' then find_end (i - 1)
        else i
    in
    let last_idx = find_end (len - 1) in
    if last_idx < 0 then str
    else
      let last_char = Stdlib.String.get str last_idx in
      (* Check for incomplete exponent: ends with e, E, e+, E+, e-, E- *)
      if last_char = 'e' || last_char = 'E' then
        (* "1e" -> "1" *)
        Stdlib.String.sub str 0 last_idx
      else if (last_char = '+' || last_char = '-') && last_idx > 0 then
        let prev_char = Stdlib.String.get str (last_idx - 1) in
        if prev_char = 'e' || prev_char = 'E' then
          (* "1e+" or "1e-" -> "1" *)
          Stdlib.String.sub str 0 (last_idx - 1)
        else str
      else str

type parse_options = {
  radix : int;
  int_only : bool;
  accept_bin_oct : bool;
  accept_legacy_octal : bool;
  accept_underscores : bool;
}

let default_parse_options =
  {
    radix = 10;
    int_only = false;
    accept_bin_oct = false;
    accept_legacy_octal = false;
    accept_underscores = false;
  }

let js_number_options =
  {
    radix = 0;
    int_only = false;
    accept_bin_oct = true;
    accept_legacy_octal = false;
    accept_underscores = false;
  }

let validate_radix radix =
  if radix <> 0 && (radix < 2 || radix > 36) then
    invalid_arg
      (Printf.sprintf
         "Global.parse_float: radix must be 0 (auto) or between 2 and 36, got \
          %d"
         radix)

let options_to_flags options =
  let flags = 0 in
  let flags = if options.int_only then flags lor js_atod_int_only else flags in
  let flags =
    if options.accept_bin_oct then flags lor js_atod_accept_bin_oct else flags
  in
  let flags =
    if options.accept_legacy_octal then flags lor js_atod_accept_legacy_octal
    else flags
  in
  let flags =
    if options.accept_underscores then flags lor js_atod_accept_underscores
    else flags
  in
  flags

(* Number of leading bytes of [str] that are JavaScript whitespace
   (WhiteSpace or LineTerminator, the set skipped by parseInt/parseFloat).
   UTF-8 aware: skips non-ASCII whitespace like NBSP or U+2028 as well. *)
let js_whitespace_prefix str =
  let len = Stdlib.String.length str in
  let rec skip i =
    if i >= len then i
    else
      let d = Stdlib.String.get_utf_8_uchar str i in
      let code = Uchar.to_int (Uchar.utf_decode_uchar d) in
      if Libunicode.is_space code then skip (i + Uchar.utf_decode_length d)
      else i
  in
  skip 0

(* Parse the longest numeric prefix of [str]. Returns the value and the
   number of bytes consumed, or [None] when no prefix parses. js_atod never
   returns NaN for a successfully parsed input, so NaN means failure. *)
let atod_prefix ~options str =
  let flags = options_to_flags options in
  let poffset = Ctypes.allocate Ctypes.int 0 in
  let tmp_mem = Ctypes.allocate_n Ctypes.uint64_t ~count:27 in
  let tmp_mem_ptr = Ctypes.to_voidp tmp_mem in
  let result = Atod.parse str poffset options.radix flags tmp_mem_ptr in
  if Float.is_nan result then None else Some (result, Ctypes.( !@ ) poffset)

let parse_float_raw ~options str =
  match atod_prefix ~options str with
  | Some (value, _consumed) -> Some value
  | None ->
      (* Workaround for QuickJS incomplete exponent bug.
         Only applies to decimal (radix 10 or auto-detect radix 0) *)
      if options.radix = 10 || options.radix = 0 then
        let stripped = strip_incomplete_exponent str in
        if stripped <> str && Stdlib.String.length stripped > 0 then
          match atod_prefix ~options stripped with
          | Some (value, _consumed) -> Some value
          | None -> None
        else None
      else None

let parse_float ?(options = default_parse_options) str =
  validate_radix options.radix;
  (* JavaScript's parseFloat skips leading whitespace *)
  let start = js_whitespace_prefix str in
  let trimmed =
    Stdlib.String.sub str start (Stdlib.String.length str - start)
  in
  parse_float_raw ~options trimmed

let parse_float_partial ?(options = default_parse_options) str =
  validate_radix options.radix;
  let start = js_whitespace_prefix str in
  let trimmed =
    Stdlib.String.sub str start (Stdlib.String.length str - start)
  in
  match atod_prefix ~options trimmed with
  | None -> None
  | Some (value, consumed) ->
      let remaining =
        Stdlib.String.sub trimmed consumed
          (Stdlib.String.length trimmed - consumed)
      in
      Some (value, remaining)

(* Exact float bound of the OCaml int range: 2^(int_size-1). A float f is
   losslessly convertible to int iff -bound <= f < bound (any float strictly
   below bound is at most max_int; -bound itself is min_int). *)
let int_bound = Stdlib.ldexp 1.0 (Sys.int_size - 1)

let parse_int_float ?(radix = 0) str =
  (* Radix must be 0 (auto-detect, the JavaScript default) or 2-36 *)
  if radix <> 0 && (radix < 2 || radix > 36) then None
  else
    let ws_offset = js_whitespace_prefix str in
    let str_len = Stdlib.String.length str in
    if ws_offset >= str_len then None
    else
      let trimmed = Stdlib.String.sub str ws_offset (str_len - ws_offset) in
      (* int_only: no dot, no exponent. Note: JavaScript's parseInt only
         auto-detects the 0x/0X prefix (radix 16), never 0b/0o, so
         JS_ATOD_ACCEPT_BIN_OCT must not be set. *)
      let options = { default_parse_options with radix; int_only = true } in
      match atod_prefix ~options trimmed with
      | None -> None
      | Some (result, _consumed) -> Some result

let parse_int ?radix str =
  match parse_int_float ?radix str with
  | None -> None
  | Some result ->
      if not (Float.is_finite result) then None
      else if result >= int_bound || result < -.int_bound then
        (* Out of OCaml int range: refuse to return a corrupted value *)
        None
      else Some (int_of_float result)