Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 8 additions & 35 deletions pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -572,35 +572,18 @@ public ExpressionNode visitBoolLiteralExpr(BoolLiteralExpr expr) {
}
}

private <T> T parseNumber(IntLiteralExpr expr, BiFunction<String, Integer, T> parser) {
var text = remove_(expr.getNumber());

var radix = 10;
if (text.startsWith("0x") || text.startsWith("0b") || text.startsWith("0o")) {
radix =
switch (text.charAt(1)) {
case 'x' -> 16;
case 'b' -> 2;
default -> 8;
};

text = text.substring(2);
}

// relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests
if (expr.parent() instanceof UnaryMinusExpr) {
// handle negation here to make parsing of base.MinInt work
// also moves negation from runtime to parse time
text = "-" + text;
}
return parser.apply(text, radix);
private <T> T parseInteger(IntLiteralExpr expr, BiFunction<String, Integer, T> parser) {
// negate relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests
// handle negation here to make parsing of base.MinInt work
// also moves negation from runtime to parse time
return VmUtils.parseInteger(expr.getNumber(), expr.parent() instanceof UnaryMinusExpr, parser);
}

@Override
public IntLiteralNode visitIntLiteralExpr(IntLiteralExpr expr) {
var section = createSourceSection(expr);
try {
var num = parseNumber(expr, Long::parseLong);
var num = parseInteger(expr, Long::parseLong);
return new IntLiteralNode(section, num);
} catch (NumberFormatException e) {
var text = expr.getNumber();
Expand All @@ -611,7 +594,7 @@ public IntLiteralNode visitIntLiteralExpr(IntLiteralExpr expr) {
@Override
public FloatLiteralNode visitFloatLiteralExpr(FloatLiteralExpr expr) {
var section = createSourceSection(expr);
var text = remove_(expr.getNumber());
var text = VmUtils.removeUnderscoresFromNumber(expr.getNumber(), false);
// relies on grammar rule nesting depth, but a breakage won't go unnoticed by tests
if (expr.parent() instanceof UnaryMinusExpr) {
// handle negation here for consistency with visitIntegerLiteral
Expand All @@ -627,16 +610,6 @@ public FloatLiteralNode visitFloatLiteralExpr(FloatLiteralExpr expr) {
}
}

private static String remove_(String number) {
var builder = new StringBuilder(number.length());
for (var i = 0; i < number.length(); i++) {
var ch = number.charAt(i);
if (ch == '_') continue;
builder.append(ch);
}
return builder.toString();
}

@Override
public ExpressionNode visitThrowExpr(ThrowExpr expr) {
return ThrowNodeGen.create(createSourceSection(expr), visitExpr(expr.getExpr()));
Expand Down Expand Up @@ -1314,7 +1287,7 @@ private Pair<ExpressionNode[], Boolean> createCollectionArgumentBytesNodes(Argum
var expr = args.get(i);
if (expr instanceof IntLiteralExpr intLiteralExpr && isAllByteLiterals) {
try {
var byt = parseNumber(intLiteralExpr, Byte::parseByte);
var byt = parseInteger(intLiteralExpr, Byte::parseByte);
expressionNodes[i] = new ByteConstantValueNode(byt);
} catch (NumberFormatException e) {
// proceed with initializing a constant value node; we'll throw an error inside
Expand Down
48 changes: 48 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
Expand Down Expand Up @@ -1092,4 +1093,51 @@ public static boolean isPklBug(VmStackOverflowException e) {
var truffleStackTraceElements = TruffleStackTrace.getStackTrace(e);
return truffleStackTraceElements != null && truffleStackTraceElements.size() < 100;
}

public static <T> T parseInteger(
String raw, boolean negate, BiFunction<String, Integer, T> parser)
throws NumberFormatException {
var text = raw;

var radix = 10;
if (text.length() >= 2 && text.charAt(0) == '0') {
radix =
switch (text.charAt(1)) {
case 'x' -> 16;
case 'o' -> 8;
case 'b' -> 2;
default -> 10;
};
if (radix != 10) {
text = text.substring(2);
}
}

text = removeUnderscoresFromNumber(text, radix == 16);
if (negate) {
text = "-" + text;
}
return parser.apply(text, radix);
}

/** Removes `_` from numbers to be parsed. Returns the string unmodified if it's invalid. */
public static String removeUnderscoresFromNumber(String number, boolean isHex) {
if (number.indexOf('_') < 0) return number;

var builder = new StringBuilder();
var numberStart = true;
for (var i = 0; i < number.length(); i++) {
var c = number.charAt(i);
if (c != '_') {
builder.append(c);
} else if (numberStart) {
// invalid: _ at start or after [.eE]
return number;
}

numberStart = c == '.' || (!isHex && (c == 'e' || c == 'E'));
}

return builder.toString();
}
}
29 changes: 6 additions & 23 deletions pkl-core/src/main/java/org/pkl/core/stdlib/base/StringNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,8 @@ public abstract static class toInt extends ExternalMethod0Node {
@Specialization
protected long eval(String self) {
try {
return Long.parseLong(removeUnderlinesFromNumber(self));
var negate = self.charAt(0) == '-';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw if given an empty string.

Looks like we're missing this from our tests

return VmUtils.parseInteger(negate ? self.substring(1) : self, negate, Long::parseLong);
} catch (NumberFormatException e) {
throw exceptionBuilder()
.evalError("cannotParseStringAs", "Int")
Expand All @@ -850,7 +851,8 @@ public abstract static class toIntOrNull extends ExternalMethod0Node {
@Specialization
protected Object eval(String self) {
try {
return Long.parseLong(removeUnderlinesFromNumber(self));
var negate = self.charAt(0) == '-';
return VmUtils.parseInteger(negate ? self.substring(1) : self, negate, Long::parseLong);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long.parseLong() accepts - and + signs (e.g. Long.parseLong("-5")); so this would parse 0x-5 as -5, 0x+5 as 5, etc, whereas this isn't actually accepted by Pkl.

One solution is to just reject any sequence after 0x/0b/etc that isn't a digit. However, that's starting to feel like a lexer. Maybe we should just use our Lexer? That would keep this in sync with our grammar.

} catch (NumberFormatException e) {
return VmNull.withoutDefault();
}
Expand All @@ -862,7 +864,7 @@ public abstract static class toFloat extends ExternalMethod0Node {
@Specialization
protected double eval(String self) {
try {
return Double.parseDouble(removeUnderlinesFromNumber(self));
return Double.parseDouble(VmUtils.removeUnderscoresFromNumber(self, false));
} catch (NumberFormatException e) {
throw exceptionBuilder()
.evalError("cannotParseStringAs", "Float")
Expand All @@ -877,7 +879,7 @@ public abstract static class toFloatOrNull extends ExternalMethod0Node {
@Specialization
protected Object eval(String self) {
try {
return Double.parseDouble(removeUnderlinesFromNumber(self));
return Double.parseDouble(VmUtils.removeUnderscoresFromNumber(self, false));
} catch (NumberFormatException e) {
return VmNull.withoutDefault();
}
Expand Down Expand Up @@ -1031,23 +1033,4 @@ private static String applyMapper(
var replacement = applyNode.executeString(mapper, regexMatch);
return Matcher.quoteReplacement(replacement);
}

/**
* Removes `_` from numbers to be parsed to be compatible with how Pkl parses numbers. Will return
* the string unmodified if it's invalid.
*/
private static String removeUnderlinesFromNumber(String number) {
var builder = new StringBuilder();
var numberStart = true;
for (var i = 0; i < number.length(); i++) {
var c = number.charAt(i);
if (c != '_') {
builder.append(c);
} else if (numberStart) return number;

numberStart = c == '.' || c == 'e' || c == 'E';
}

return builder.toString();
}
}
23 changes: 23 additions & 0 deletions pkl-core/src/test/files/LanguageSnippetTests/input/api/string.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,34 @@ examples {
"-1_2__3___".toInt()
"0".toInt()
"-0".toInt()

"0x123".toInt()
"-0x123".toInt()
"0x1_2__3___".toInt()
"-0x1_2__3___".toInt()
"0x0".toInt()
"-0x0".toInt()

"0b101".toInt()
"-0b101".toInt()
"0b1_0__1___".toInt()
"-0b1_0__1___".toInt()
"0b0".toInt()
"-0b0".toInt()

"0o123".toInt()
"-0o123".toInt()
"0o1_2__3___".toInt()
"-0o1_2__3___".toInt()
"0o0".toInt()
"-0o0".toInt()

module.catch(() -> "1.2".toInt())
module.catch(() -> "9223372036854775808".toInt())
module.catch(() -> "-9223372036854775809".toInt())
module.catch(() -> "abc".toInt())
module.catch(() -> "_1_000".toInt())
module.catch(() -> "0p0".toInt())
}

["toIntOrNull()"] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2255,9 +2255,15 @@ alias {
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
}
docComment = """
Parses this string as a signed decimal (base 10) integer.
Parses this string as a signed integer.

Throws if this string cannot be parsed as a signed decimal integer,
Supports integer formats supported by Pkl:
* Decimal (base 10)
* Hexadecimal (base 16) with prefix `0x` or `-0x`
* Binary (base 2) with prefix `0b` or `-0b`
* Octal (base 8) with prefix `0o` or `-0o`

Throws if this string cannot be parsed,
or if the integer is too large to fit into [Int].
"""
annotations = List()
Expand All @@ -2272,9 +2278,15 @@ alias {
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
}
docComment = """
Parses this string as a signed decimal (base 10) integer.
Parses this string as a signed integer.

Supports integer formats supported by Pkl:
* Decimal (base 10)
* Hexadecimal (base 16) with prefix `0x` or `-0x`
* Binary (base 2) with prefix `0b` or `-0b`
* Octal (base 8) with prefix `0o` or `-0o`

Returns [null] if this string cannot be parsed as a signed decimal integer,
Returns [null] if this string cannot be parsed,
or if the integer is too large to fit into [Int].
"""
annotations = List()
Expand Down Expand Up @@ -3192,9 +3204,15 @@ alias {
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
}
docComment = """
Parses this string as a signed decimal (base 10) integer.
Parses this string as a signed integer.

Throws if this string cannot be parsed as a signed decimal integer,
Supports integer formats supported by Pkl:
* Decimal (base 10)
* Hexadecimal (base 16) with prefix `0x` or `-0x`
* Binary (base 2) with prefix `0b` or `-0b`
* Octal (base 8) with prefix `0o` or `-0o`

Throws if this string cannot be parsed,
or if the integer is too large to fit into [Int].
"""
annotations = List()
Expand All @@ -3209,9 +3227,15 @@ alias {
displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX"
}
docComment = """
Parses this string as a signed decimal (base 10) integer.
Parses this string as a signed integer.

Supports integer formats supported by Pkl:
* Decimal (base 10)
* Hexadecimal (base 16) with prefix `0x` or `-0x`
* Binary (base 2) with prefix `0b` or `-0b`
* Octal (base 8) with prefix `0o` or `-0o`

Returns [null] if this string cannot be parsed as a signed decimal integer,
Returns [null] if this string cannot be parsed,
or if the integer is too large to fit into [Int].
"""
annotations = List()
Expand Down
19 changes: 19 additions & 0 deletions pkl-core/src/test/files/LanguageSnippetTests/output/api/string.pcf
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,30 @@ examples {
-123
0
0
291
-291
291
-291
0
0
5
-5
5
-5
0
0
83
-83
83
-83
0
0
"Cannot parse string as `Int`. String: \"1.2\""
"Cannot parse string as `Int`. String: \"9223372036854775808\""
"Cannot parse string as `Int`. String: \"-9223372036854775809\""
"Cannot parse string as `Int`. String: \"abc\""
"Cannot parse string as `Int`. String: \"_1_000\""
"Cannot parse string as `Int`. String: \"0p0\""
}
["toIntOrNull()"] {
123
Expand Down
20 changes: 16 additions & 4 deletions stdlib/base.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -1640,15 +1640,27 @@ external class String extends Any {
/// ```
external function decapitalize(): String

/// Parses this string as a signed decimal (base 10) integer.
/// Parses this string as a signed integer.
///
/// Throws if this string cannot be parsed as a signed decimal integer,
/// Supports integer formats supported by Pkl:
/// * Decimal (base 10)
/// * Hexadecimal (base 16) with prefix `0x` or `-0x`
/// * Binary (base 2) with prefix `0b` or `-0b`
/// * Octal (base 8) with prefix `0o` or `-0o`
///
/// Throws if this string cannot be parsed,
/// or if the integer is too large to fit into [Int].
external function toInt(): Int

/// Parses this string as a signed decimal (base 10) integer.
/// Parses this string as a signed integer.
///
/// Supports integer formats supported by Pkl:
/// * Decimal (base 10)
/// * Hexadecimal (base 16) with prefix `0x` or `-0x`
/// * Binary (base 2) with prefix `0b` or `-0b`
/// * Octal (base 8) with prefix `0o` or `-0o`
///
/// Returns [null] if this string cannot be parsed as a signed decimal integer,
/// Returns [null] if this string cannot be parsed,
/// or if the integer is too large to fit into [Int].
external function toIntOrNull(): Int?

Expand Down
Loading