///|
fn parse_int_text(text : String) -> Int64 {
// PKL-147: Apple Pkl integer literals come in four shapes:
// 1. decimal `123`, with optional `_` separators (`1_000_000`),
// 2. hex `0xFF_FF` / `0X1A`,
// 3. binary `0b1010` / `0B1010`,
// 4. octal `0o17` / `0O17`.
// PKL-150: accumulate in Int64 so 64-bit literals like
// `0xFFFF_FFFF_FFFF_FFFF` / `9_223_372_036_854_775_807` round-trip
// without truncation.
let len = text.length()
if len >= 2 && text[0].to_int().unsafe_to_char() == '0' {
let prefix = text[1].to_int().unsafe_to_char()
if prefix == 'x' || prefix == 'X' {
let mut value : Int64 = 0L
for i = 2; i < len; i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c == '_' {
continue
}
let d = if c >= '0' && c <= '9' {
c.to_int() - '0'.to_int()
} else if c >= 'a' && c <= 'f' {
c.to_int() - 'a'.to_int() + 10
} else if c >= 'A' && c <= 'F' {
c.to_int() - 'A'.to_int() + 10
} else {
continue
}
value = value * 16L + d.to_int64()
}
return value
}
if prefix == 'b' || prefix == 'B' {
let mut value : Int64 = 0L
for i = 2; i < len; i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c == '0' || c == '1' {
value = value * 2L + (c.to_int() - '0'.to_int()).to_int64()
}
}
return value
}
if prefix == 'o' || prefix == 'O' {
let mut value : Int64 = 0L
for i = 2; i < len; i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c >= '0' && c <= '7' {
value = value * 8L + (c.to_int() - '0'.to_int()).to_int64()
}
}
return value
}
}
let mut value : Int64 = 0L
for i = 0; i < len; i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c >= '0' && c <= '9' {
value = value * 10L + (c.to_int() - '0'.to_int()).to_int64()
}
}
value
}
///|
fn parse_float_text(text : String) -> Double {
// PKL-092 / PKL-148ay: strip Pkl's `_` digit separators and delegate
// to the platform double parser. Hand-rolled `mantissa / 10^exp`
// underflows the subnormal edge (`4.9E-324`) to zero; the native
// parser keeps IEEE-754's minimum positive Double.
let buf = StringBuilder::new()
for c in text.iter() {
if c != '_' {
buf.write_char(c)
}
}
@string.parse_double(buf.to_string()[:]) catch {
_ => 0.0
}
}
///|
/// PKL-128: build either a plain `StringLiteral` or an
/// `InterpolatedString` from a raw quoted string token. Splits the
/// inner text at `\(...)` boundaries (balanced parens, escape-aware)
/// and parses each `\(...)` content as a Pkl expression. Strings with
/// no interpolation segments collapse back to a plain `StringLiteral`.
fn parse_string_literal_text(text : String) -> Expr {
// PKL-148an / PKL-148ao: raw string literal `"..."`
// (single-line) or `"""..."""` (heredoc). The lexer
// pairs a leading run of `#`s with the same count on the close.
// Strip both runs + the opening / closing quotes, then assemble
// the inner text through `assemble_string_parts` with the same
// hash count — escape sequences (`\n`, `\t`, etc.) stay verbatim
// in raw form, and only the `\(expr)` marker (with matching
// hash count) opens an interpolation segment. Heredoc indent
// stripping still applies — Apple Pkl drops the leading newline
// and the closing delimiter's indentation in raw heredocs the
// same way it does for the non-raw form.
if text.length() >= 2 && text.has_prefix("#") {
let mut hash_count = 0
while hash_count < text.length() &&
text[hash_count].to_int().unsafe_to_char() == '#' {
hash_count = hash_count + 1
}
if hash_count >= 1 &&
hash_count + 1 <= text.length() &&
text[hash_count].to_int().unsafe_to_char() == '"' {
let is_heredoc = text.length() >= hash_count * 2 + 6 &&
hash_count + 2 < text.length() &&
text[hash_count + 1].to_int().unsafe_to_char() == '"' &&
text[hash_count + 2].to_int().unsafe_to_char() == '"'
let inner = if is_heredoc {
strip_heredoc_indent(
String::unsafe_substring(
text,
start=hash_count + 3,
end=text.length() - hash_count - 3,
),
)
} else {
String::unsafe_substring(
text,
start=hash_count + 1,
end=text.length() - hash_count - 1,
)
}
return assemble_string_parts(inner, hash_count)
}
}
// Strip the surrounding quotes (if present) but track the inner text
// so escape sequences and interpolations can be split independently.
// PKL-128: a triple-quoted heredoc (`"""..."""`) strips its leading
// newline and the closing delimiter's indentation from each line
// before the regular escape / interpolation processing kicks in.
let inner = if text.length() >= 6 &&
text.has_prefix("\"\"\"") &&
text.has_suffix("\"\"\"") {
strip_heredoc_indent(
String::unsafe_substring(text, start=3, end=text.length() - 3),
)
} else if text.length() >= 2 &&
text[0].to_int().unsafe_to_char() == '"' &&
text[text.length() - 1].to_int().unsafe_to_char() == '"' {
String::unsafe_substring(text, start=1, end=text.length() - 1)
} else {
text
}
assemble_string_parts(inner, 0)
}
///|
/// PKL-148ao: detect a string-literal start at `start` and return the
/// index just past its closing delimiter. Returns `start` unchanged
/// when no string literal begins here. Handles four shapes:
///
/// - `"..."` (single-line non-raw): scans to the matching `"` skipping
/// `\` escape pairs so a `\"` inside doesn't close the string
/// prematurely.
/// - `"""..."""` (heredoc non-raw): scans to the matching `"""`.
/// - `"..."` (single-line raw): scans to a `"` followed by
/// exactly `hash_count` `#`s.
/// - `"""..."""` (heredoc raw): scans to `"""` followed by
/// `hash_count` `#`s.
///
/// Used by the interpolation walker in `assemble_string_parts` so a
/// `\#()` segment is sliced
/// at the correct closing `)` instead of being cut short by a `)` that
/// happens to live inside a nested string literal.
fn skip_string_at(s : String, start : Int) -> Int {
if start >= s.length() {
return start
}
let c = s[start].to_int().unsafe_to_char()
let mut hash_count = 0
let mut quote_pos = start
if c == '#' {
while quote_pos < s.length() &&
s[quote_pos].to_int().unsafe_to_char() == '#' {
hash_count = hash_count + 1
quote_pos = quote_pos + 1
}
if quote_pos >= s.length() || s[quote_pos].to_int().unsafe_to_char() != '"' {
return start
}
} else if c != '"' {
return start
}
let is_heredoc = quote_pos + 2 < s.length() &&
s[quote_pos + 1].to_int().unsafe_to_char() == '"' &&
s[quote_pos + 2].to_int().unsafe_to_char() == '"'
if is_heredoc {
let mut p = quote_pos + 3
while p + 2 + hash_count < s.length() {
if s[p].to_int().unsafe_to_char() == '"' &&
s[p + 1].to_int().unsafe_to_char() == '"' &&
s[p + 2].to_int().unsafe_to_char() == '"' {
let mut ok = true
for h = 0; h < hash_count; h = h + 1 {
if s[p + 3 + h].to_int().unsafe_to_char() != '#' {
ok = false
break
}
}
if ok {
return p + 3 + hash_count
}
}
p = p + 1
}
return s.length()
}
let mut p = quote_pos + 1
while p < s.length() {
let ch = s[p].to_int().unsafe_to_char()
if hash_count == 0 && ch == '\\' && p + 1 < s.length() {
// Non-raw escape — skip the two-char sequence so `\"` doesn't
// close the string here.
p = p + 2
continue
}
if ch == '"' {
if hash_count == 0 {
return p + 1
}
let mut ok = true
for h = 0; h < hash_count; h = h + 1 {
if p + 1 + h >= s.length() ||
s[p + 1 + h].to_int().unsafe_to_char() != '#' {
ok = false
break
}
}
if ok {
return p + 1 + hash_count
}
}
if ch == '\n' {
// Unterminated single-line string — bail at the newline so the
// outer walker can resume.
return p
}
p = p + 1
}
s.length()
}
///|
/// PKL-148ao: walk a string literal's inner body and split it into
/// `StringLiteral` / interpolation `Expr` segments. `hash_count`
/// controls both the interpolation marker shape and the literal-
/// segment decoding:
///
/// - `hash_count == 0` (non-raw): `\(expr)` opens an interpolation
/// segment; literal segments run through `decode_string_escapes`
/// so the standard `\n` / `\t` / `\u{...}` escapes resolve.
/// - `hash_count >= 1` (raw): the marker is `\(expr)` —
/// the `\` followed by exactly `hash_count` `#`s and an opening
/// `(`. Inside a raw string the regular `\n` / `\t` escapes stay
/// verbatim (no decoding), but interpolation still works through
/// the hashed marker.
///
/// The function returns a single `StringLiteral` when no interpolation
/// fires, or `InterpolatedString(parts)` when at least one segment
/// was lifted into an expression.
fn assemble_string_parts(inner : String, hash_count : Int) -> Expr {
let is_raw = hash_count > 0
let decode = fn(s : String) -> String {
if is_raw {
s
} else {
decode_string_escapes(s)
}
}
let parts : Array[Expr] = []
let buf = StringBuilder::new()
let mut buf_has_content = false
let mut i = 0
while i < inner.length() {
let c = inner[i].to_int().unsafe_to_char()
// PKL-148ao: in raw mode (`hash_count > 0`), the `\X` form
// re-enables escape decoding for X (`\#t` → tab, `\#n` → newline,
// `\##u{61}` → `a`, etc.). The hash count must match exactly; a
// `\#t` inside a `##"..."##` literal is still verbatim. The
// marker is detected by reading the hash run after `\`; if `X` is
// `(`, the marker arm below handles it as interpolation. For
// every other recognised escape char we decode and emit, then
// skip past the escape sequence.
if is_raw && c == '\\' && i + 1 + hash_count < inner.length() {
let mut hashes_ok = true
for h = 0; h < hash_count; h = h + 1 {
if inner[i + 1 + h].to_int().unsafe_to_char() != '#' {
hashes_ok = false
break
}
}
if hashes_ok {
let escape_char = inner[i + 1 + hash_count].to_int().unsafe_to_char()
if escape_char != '(' {
// Decode the escape inline and emit the resolved char(s).
// Reuse `decode_string_escapes` by feeding it the equivalent
// non-raw escape (`\#t` → `\t`); the helper already knows
// every Apple-Pkl escape including `\u{...}`.
let consumed = if escape_char == 'u' &&
i + 2 + hash_count < inner.length() &&
inner[i + 2 + hash_count].to_int().unsafe_to_char() == '{' {
let mut k = i + 3 + hash_count
while k < inner.length() &&
inner[k].to_int().unsafe_to_char() != '}' {
k = k + 1
}
if k < inner.length() {
k + 1 - i
} else {
inner.length() - i
}
} else {
2 + hash_count
}
let escape_payload = String::unsafe_substring(
inner,
start=i + 1 + hash_count,
end=i + consumed,
)
let resolved = decode_string_escapes("\\" + escape_payload)
buf.write_string(resolved)
buf_has_content = true
i = i + consumed
continue
}
}
}
// Detect interpolation marker: `\` + `hash_count` `#`s + `(`.
let mut marker = false
if c == '\\' && i + 1 + hash_count < inner.length() {
let mut hashes_ok = true
for h = 0; h < hash_count; h = h + 1 {
if inner[i + 1 + h].to_int().unsafe_to_char() != '#' {
hashes_ok = false
break
}
}
if hashes_ok && inner[i + 1 + hash_count].to_int().unsafe_to_char() == '(' {
marker = true
}
}
if marker {
if buf_has_content {
parts.push(StringLiteral(decode(buf.to_string())))
buf.reset()
buf_has_content = false
}
let open_paren = i + 1 + hash_count
let mut depth = 1
let mut j = open_paren + 1
while j < inner.length() && depth > 0 {
let cc = inner[j].to_int().unsafe_to_char()
// PKL-148ao: skip the entire content of any string literal we
// encounter so embedded `(` / `)` inside a nested string don't
// disturb the balanced-paren count. Handles single-line raw
// (`"..."`), heredoc raw (`"""..."""`),
// non-raw (`"..."`), and non-raw heredoc (`"""..."""`). The
// outer hash count is independent — a `#"..."#` argument inside
// a `\#(...)` segment is still recognised verbatim because the
// skip walker only consumes balanced delimiters.
let skipped = skip_string_at(inner, j)
if skipped > j {
j = skipped
continue
}
if cc == '(' {
depth = depth + 1
} else if cc == ')' {
depth = depth - 1
if depth == 0 {
break
}
}
j = j + 1
}
let segment = String::unsafe_substring(inner, start=open_paren + 1, end=j)
let parsed = parse_source(segment)
let expr = match parsed.program.body {
Some(body_expr) => body_expr
None =>
if parsed.program.bindings.length() > 0 {
parsed.program.bindings[parsed.program.bindings.length() - 1].value
} else {
ErrorExpr("empty interpolation")
}
}
parts.push(expr)
i = j + 1
} else if c == '\\' && !is_raw && i + 1 < inner.length() {
// Regular escape in a non-raw string — accumulate the two-char
// sequence so the eventual `decode_string_escapes` call resolves
// it. In raw strings we fall through to the generic char branch
// because escapes are verbatim.
buf.write_char(c)
buf.write_char(inner[i + 1].to_int().unsafe_to_char())
buf_has_content = true
i = i + 2
} else {
buf.write_char(c)
buf_has_content = true
i = i + 1
}
}
if buf_has_content {
parts.push(StringLiteral(decode(buf.to_string())))
}
if parts.length() == 0 {
return StringLiteral("")
}
if parts.length() == 1 {
match parts[0] {
StringLiteral(_) as plain => return plain
_ => ()
}
}
InterpolatedString(parts)
}
///|
/// PKL-128: dedent a triple-quoted heredoc body.
///
/// Apple Pkl's `"""..."""` removes the leading newline
/// and trims the indentation of the closing delimiter from every line.
/// The body passed here is the raw content between the opening and
/// closing `"""` markers; the result is the content with the leading
/// newline stripped and each line's `closing_indent` prefix removed.
fn strip_heredoc_indent(body : String) -> String {
let mut start = 0
// Strip a single leading newline (`"""`).
if start < body.length() {
let c = body[start].to_int().unsafe_to_char()
if c == '\n' {
start = start + 1
} else if c == '\r' &&
start + 1 < body.length() &&
body[start + 1].to_int().unsafe_to_char() == '\n' {
start = start + 2
}
}
// Find the indentation immediately before the trailing close. The
// close marker has already been stripped, so the indent we want is
// the run of horizontal whitespace at the very end of `body`.
let mut indent_start = body.length()
while indent_start > start {
let c = body[indent_start - 1].to_int().unsafe_to_char()
if c == ' ' || c == '\t' {
indent_start = indent_start - 1
} else {
break
}
}
let indent_len = body.length() - indent_start
let indent = String::unsafe_substring(
body,
start=indent_start,
end=body.length(),
)
// PKL-148e: Apple Pkl treats the newline immediately preceding the
// closing delimiter's own line as the line terminator for that line
// and strips it from the content. Without this, `"""x"""`
// (and indented variants) carry a stray trailing `\n` that breaks
// gold-match for fixtures like `basic/identifier`.
let mut body_end = indent_start
if body_end > start {
let c = body[body_end - 1].to_int().unsafe_to_char()
if c == '\n' {
body_end = body_end - 1
if body_end > start &&
body[body_end - 1].to_int().unsafe_to_char() == '\r' {
body_end = body_end - 1
}
}
}
// Walk each line, strip the indent prefix when present.
let buf = StringBuilder::new()
let mut i = start
let mut at_line_start = true
while i < body_end {
if at_line_start && indent_len > 0 && i + indent_len <= body_end {
let candidate = String::unsafe_substring(
body,
start=i,
end=i + indent_len,
)
if candidate == indent {
i = i + indent_len
at_line_start = false
continue
}
}
let c = body[i].to_int().unsafe_to_char()
buf.write_char(c)
at_line_start = c == '\n'
i = i + 1
}
buf.to_string()
}
///|
fn unquote(text : String) -> String {
let mut hash_count = 0
while hash_count < text.length() &&
text[hash_count].to_int().unsafe_to_char() == '#' {
hash_count = hash_count + 1
}
if hash_count > 0 &&
hash_count + 1 <= text.length() &&
text[hash_count].to_int().unsafe_to_char() == '"' {
let suffix_len = hash_count + 1
if text.length() >= hash_count + suffix_len {
let quote_end = text.length() - hash_count - 1
if quote_end >= hash_count &&
text[quote_end].to_int().unsafe_to_char() == '"' {
return String::unsafe_substring(
text,
start=hash_count + 1,
end=quote_end,
)
}
}
}
if text.length() >= 2 && text[0].to_int().unsafe_to_char() == '"' {
let end = text.length() - 1
if text[end].to_int().unsafe_to_char() == '"' {
let inner = String::unsafe_substring(text, start=1, end~)
if inner.find("\\") is None {
return inner
}
return decode_string_escapes(inner)
}
}
text
}
///|
fn decode_string_escapes(text : String) -> String {
let buf = StringBuilder::new()
let mut i = 0
while i < text.length() {
let c = text[i].to_int().unsafe_to_char()
if c == '\\' && i + 1 < text.length() {
let next = text[i + 1].to_int().unsafe_to_char()
// PKL-148ao: `\u{HHHH}` unicode escape — read hex digits up to
// the matching `}` and emit the resulting code point. Used by
// both non-raw strings (`"\u{1F920}"`) and raw strings through
// the `\u{...}` form (decoded inline before reaching here).
if next == 'u' &&
i + 2 < text.length() &&
text[i + 2].to_int().unsafe_to_char() == '{' {
let mut j = i + 3
let mut code = 0
while j < text.length() {
let ch = text[j].to_int().unsafe_to_char()
if ch == '}' {
break
}
let digit = if ch >= '0' && ch <= '9' {
ch.to_int() - '0'.to_int()
} else if ch >= 'a' && ch <= 'f' {
ch.to_int() - 'a'.to_int() + 10
} else if ch >= 'A' && ch <= 'F' {
ch.to_int() - 'A'.to_int() + 10
} else {
-1
}
if digit < 0 {
break
}
code = code * 16 + digit
j = j + 1
}
if j < text.length() && text[j].to_int().unsafe_to_char() == '}' {
// Apple Pkl accepts a lone UTF-16 surrogate escape but
// normalizes it to `?`. Passing D800-DFFF to MoonBit's
// `unsafe_to_char` creates an invalid String and later
// operations such as `char_length` abort.
if code >= 0xd800 && code <= 0xdfff {
buf.write_char('?')
} else {
buf.write_char(code.unsafe_to_char())
}
i = j + 1
continue
}
}
// PKL-148bb: `\` is a heredoc line continuation — Apple
// Pkl drops both the backslash and the following newline so the
// surrounding lines join (`basic/stringMultilineContinuation`).
// Skip the `\r\n` variant too.
if next == '\n' {
i += 2
continue
}
if next == '\r' &&
i + 2 < text.length() &&
text[i + 2].to_int().unsafe_to_char() == '\n' {
i += 3
continue
}
match next {
'n' => buf.write_char('\n')
't' => buf.write_char('\t')
'r' => buf.write_char('\r')
'"' => buf.write_char('"')
'\\' => buf.write_char('\\')
_ => buf.write_char(next)
}
i += 2
} else {
buf.write_char(c)
i += 1
}
}
buf.to_string()
}
///|
fn import_alias_from_uri(uri : String) -> String {
let without_scheme = match uri.find(":") {
Some(idx) => String::unsafe_substring(uri, start=idx + 1, end=uri.length())
None => uri
}
let base = match without_scheme.rev_find("/") {
Some(idx) =>
String::unsafe_substring(
without_scheme,
start=idx + 1,
end=without_scheme.length(),
)
None => without_scheme
}
if base.has_suffix(".pkl") {
String::unsafe_substring(base, start=0, end=base.length() - 4)
} else {
base
}
}
///|
pub fn parse_source(source : String) -> ParseResult {
Parser::new(source).parse()
}