Skip to content

Commit

Permalink
feat(stdlib): Number.parseInt (#1051)
Browse files Browse the repository at this point in the history
  • Loading branch information
ospencer committed Dec 8, 2021
1 parent 3ceb1cf commit abafb58
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 6 deletions.
38 changes: 38 additions & 0 deletions compiler/test/stdlib/number.test.gr
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Number from "number"
import Result from "result"

// add
assert Number.add(25, 5) == 30
// sub
Expand Down Expand Up @@ -96,3 +98,39 @@ assert Number.isInfinite(25.76) == false
assert Number.isInfinite(-25.00) == false
assert Number.isInfinite(1/2) == false
assert Number.isInfinite(-1/2) == false

// parseInt
assert Number.parseInt("42", 10) == Ok(42)
assert Number.parseInt("042", 10) == Ok(42)
assert Number.parseInt("_0___42___", 10) == Ok(42)
assert Number.parseInt("-42", 10) == Ok(-42)
assert Number.parseInt("-042", 10) == Ok(-42)
assert Number.parseInt("-_0___42___", 10) == Ok(-42)
assert Number.parseInt("1073741823", 10) == Ok(1073741823) // grain simple number max
assert Number.parseInt("-1073741824", 10) == Ok(-1073741824) // grain simple number min
assert Number.parseInt("2147483647", 10) == Ok(2147483647) // i32 max
assert Number.parseInt("-2147483648", 10) == Ok(-2147483648) // i32 min
assert Number.parseInt("9223372036854775807", 10) == Ok(9223372036854775807) // i64 max
assert Number.parseInt("-9223372036854775808", 10) == Ok(-9223372036854775808) // i64 min
assert Number.parseInt("0xabcdef", 10) == Ok(0xabcdef)
assert Number.parseInt("0Xabcdef", 10) == Ok(0xabcdef)
assert Number.parseInt("abcdef", 16) == Ok(0xabcdef)
assert Number.parseInt("AbCdEf", 16) == Ok(0xabcdef)
assert Number.parseInt("0o7654321", 10) == Ok(0o7654321)
assert Number.parseInt("0O7654321", 10) == Ok(0o7654321)
assert Number.parseInt("7654321", 8) == Ok(0o7654321)
assert Number.parseInt("0b100101110110", 10) == Ok(0b100101110110)
assert Number.parseInt("0B100101110110", 10) == Ok(0b100101110110)
assert Number.parseInt("100101110110", 2) == Ok(0b100101110110)
assert Number.parseInt("zyxw44ab", 36) == Ok(2818805666483)
assert Number.parseInt("ZYXW44AB", 36) == Ok(2818805666483)
assert Result.isErr(Number.parseInt("", 10))
assert Result.isErr(Number.parseInt("_", 10))
assert Result.isErr(Number.parseInt("1.23", 10))
assert Result.isErr(Number.parseInt("9223372036854775808", 10))
assert Result.isErr(Number.parseInt("-9223372036854775809", 10))
assert Result.isErr(Number.parseInt("000000", 1))
assert Result.isErr(Number.parseInt("zzzzz", 37))
assert Result.isErr(Number.parseInt("zzzzz", 9223372036854775807))
assert Result.isErr(Number.parseInt("10", 1.23))
assert Result.isErr(Number.parseInt("10", 2/3))
30 changes: 24 additions & 6 deletions stdlib/number.gr
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isFloat,
isBoxedNumber
} from "runtime/numbers"
import { parseInt } from "runtime/stringUtils"
import { newFloat64, newInt64 } from "runtime/dataStructures"
import Tags from "runtime/unsafe/tags"

Expand Down Expand Up @@ -76,9 +77,9 @@ export let rec sqrt = (x: Number) => {
let x = WasmI32.fromGrain(x)
let sqrtd = WasmF64.sqrt(xval)
let ret = if (!isFloat(x) && WasmF64.eq(sqrtd, WasmF64.trunc(sqrtd))) {
WasmI32.toGrain(reducedInteger(WasmI64.truncF64S(sqrtd))): Number
WasmI32.toGrain(reducedInteger(WasmI64.truncF64S(sqrtd))): (Number)
} else {
WasmI32.toGrain(newFloat64(sqrtd)): Number
WasmI32.toGrain(newFloat64(sqrtd)): (Number)
}
Memory.decRef(WasmI32.fromGrain(x))
Memory.decRef(WasmI32.fromGrain(sqrt))
Expand Down Expand Up @@ -119,7 +120,7 @@ export let max = (x: Number, y: Number) => if (x > y) x else y
export let rec ceil = (x: Number) => {
let xval = coerceNumberToWasmF64(x)
let ceiling = WasmI64.truncF64S(WasmF64.ceil(xval))
let ret = WasmI32.toGrain(reducedInteger(ceiling)): Number
let ret = WasmI32.toGrain(reducedInteger(ceiling)): (Number)
Memory.decRef(WasmI32.fromGrain(x))
Memory.decRef(WasmI32.fromGrain(ceil))
ret
Expand All @@ -137,7 +138,7 @@ export let rec ceil = (x: Number) => {
export let rec floor = (x: Number) => {
let xval = coerceNumberToWasmF64(x)
let floored = WasmI64.truncF64S(WasmF64.floor(xval))
let ret = WasmI32.toGrain(reducedInteger(floored)): Number
let ret = WasmI32.toGrain(reducedInteger(floored)): (Number)
Memory.decRef(WasmI32.fromGrain(x))
Memory.decRef(WasmI32.fromGrain(floor))
ret
Expand All @@ -155,7 +156,7 @@ export let rec floor = (x: Number) => {
export let rec trunc = (x: Number) => {
let xval = coerceNumberToWasmF64(x)
let trunced = WasmI64.truncF64S(xval)
let ret = WasmI32.toGrain(reducedInteger(trunced)): Number
let ret = WasmI32.toGrain(reducedInteger(trunced)): (Number)
Memory.decRef(WasmI32.fromGrain(x))
Memory.decRef(WasmI32.fromGrain(trunc))
ret
Expand All @@ -173,7 +174,7 @@ export let rec trunc = (x: Number) => {
export let rec round = (x: Number) => {
let xval = coerceNumberToWasmF64(x)
let rounded = WasmI64.truncF64S(WasmF64.nearest(xval))
let ret = WasmI32.toGrain(reducedInteger(rounded)): Number
let ret = WasmI32.toGrain(reducedInteger(rounded)): (Number)
Memory.decRef(WasmI32.fromGrain(x))
Memory.decRef(WasmI32.fromGrain(round))
ret
Expand Down Expand Up @@ -305,3 +306,20 @@ export let rec isInfinite = (x: Number) => {
Memory.decRef(WasmI32.fromGrain(isInfinite))
ret
}

/**
* Parses a string representation of an integer into a `Number` using the
* specified radix (also known as a number system "base").
*
* If the string has a radix prefix (i.e. "0x"/"0X", "0o"/"0O", or "0b"/"0B"
* for radixes 16, 8, or 2 respectively), the supplied radix is ignored in
* favor of the prefix. Underscores that appear in the numeric portion of the
* input are ignored.
*
* @param input: The string to parse
* @param radix: The number system base to use when parsing the input string
* @returns `Ok(value)` containing the parsed number on a successful parse or `Err(msg)` containing an error message string otherwise
*
* @since v0.4.5
*/
export parseInt
172 changes: 172 additions & 0 deletions stdlib/runtime/stringUtils.gr
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// TODO(#1050): Remove dependency on Pervasives once Option/Result types are imbedded in the compiler

import WasmI32, {
add as (+),
sub as (-),
gtU as (>),
geU as (>=),
ltU as (<),
shrS as (>>),
eq as (==),
ne as (!=),
and as (&),
} from "runtime/unsafe/wasmi32"
import WasmI64 from "runtime/unsafe/wasmi64"
import Memory from "runtime/unsafe/memory"
import Tags from "runtime/unsafe/tags"
import { reducedInteger } from "runtime/numbers"

@disableGC
export let rec parseInt = (string: String, radix: Number) => {
let _CHAR_0 = 0x30n
let _CHAR_B = 0x42n
let _CHAR_b = 0x62n
let _CHAR_O = 0x4fn
let _CHAR_o = 0x6fn
let _CHAR_X = 0x58n
let _CHAR_x = 0x78n

let _CHAR_A = 0x41n
let _CHAR_a = 0x61n

let _CHAR_UNDERSCORE = 0x5fn
let _CHAR_MINUS = 0x2dn

let _INT_MIN = -9223372036854775808N

// Don't need to process Unicode length since if the string
// contains non-ascii characters, it's not a valid integer
let strLen = WasmI32.load(WasmI32.fromGrain(string), 4n)

// Our pointer within the string we're parsing, offset by the
// header
let mut offset = WasmI32.fromGrain(string) + 8n

let strEnd = offset + strLen

let radix = WasmI32.fromGrain(radix)
let result = if (WasmI32.eqz(radix & Tags._GRAIN_NUMBER_TAG_MASK) || radix < WasmI32.fromGrain(2) || radix > WasmI32.fromGrain(36)) {
Memory.incRef(WasmI32.fromGrain(Err))
Err("Radix must be an integer between 2 and 36")
} else if (WasmI32.eqz(strLen)) {
Memory.incRef(WasmI32.fromGrain(Err))
Err("Invalid input")
} else {
let mut char = WasmI32.load8U(offset, 0n)

let mut limit = WasmI64.add(_INT_MIN, 1N)

// Check for a sign
let mut negative = false
if (char == _CHAR_MINUS) {
negative = true
offset += 1n
limit = _INT_MIN
char = WasmI32.load8U(offset, 0n)
}

let mut radix = WasmI64.extendI32S(radix >> 1n)

// Check if we should override the supplied radix
if (char == _CHAR_0 && strLen > 2n) {
match (WasmI32.load8U(offset, 1n)) {
c when c == _CHAR_B || c == _CHAR_b => {
radix = 2N
offset += 2n
},
c when c == _CHAR_O || c == _CHAR_o => {
radix = 8N
offset += 2n
},
c when c == _CHAR_X || c == _CHAR_x => {
radix = 16N
offset += 2n
},
_ => void,
}
}

let mut value = 0N

// we use 0 to represent no error, 1 to represent an invalid
// input, and 2 to represent an overflow
let mut error = 1n

for (let mut i = offset; i < strEnd; i += 1n) {
let char = WasmI32.load8U(i, 0n)

// Ignore underscore characters
if (char == _CHAR_UNDERSCORE) {
continue
}

// We've seen at least one non-underscore character, so we'll consider
// the input valid until we find out otherwise

error = 0n

let mut digit = 0n

match (char) {
c when c - _CHAR_0 < 10n => digit = char - _CHAR_0,
c when c - _CHAR_A < 26n => digit = char - _CHAR_A + 10n,
c when c - _CHAR_a < 26n => digit = char - _CHAR_a + 10n,
_ => {
error = 1n
// invalid digit
break
},
}

if (digit >= WasmI32.wrapI64(radix)) {
error = 1n
// invalid digit
break
}

let digit = WasmI64.extendI32U(digit)

value = WasmI64.mul(value, radix)

// Check for overflow
// 64-bit int min + 1
if (WasmI64.ltS(value, WasmI64.add(limit, digit))) {
error = 2n
// overflow
break
}

// To quote the OpenJDK,
// "Accumulating negatively avoids surprises near MAX_VALUE"
// The minimum value of a 64-bit integer (-9223372036854775808) can't be
// represented as a positive number because it would be larger than the
// maximum 64-bit integer (9223372036854775807), so we'd be unable to
// parse negatives as positives and multiply by the sign at the end.
// Instead, we represent all positive numbers as negative numbers since
// we have one unit more headroom.
value = WasmI64.sub(value, digit)
}

match (error) {
1n => {
Memory.incRef(WasmI32.fromGrain(Err))
Err("Invalid digit in input")
},
2n => {
Memory.incRef(WasmI32.fromGrain(Err))
Err("Input out of range of representable integers")
},
_ => {
let value = if (negative) value else WasmI64.mul(value, -1N)
let number = WasmI32.toGrain(reducedInteger(value)): (Number)
Memory.incRef(WasmI32.fromGrain(Ok))
Ok(number)
},
}
}

Memory.decRef(WasmI32.fromGrain(parseInt))
Memory.decRef(radix)

result
}

0 comments on commit abafb58

Please sign in to comment.