// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
pub(all) suberror WeslCompileError {
Resolve(ResolveError)
Parse(String)
Validation(String)
Validate(ValidateError)
InvalidExpression(String)
DuplicateSymbol(String)
CircularDecl(String)
MissingDecl(ModulePath, String)
Private(String, ModulePath)
}
///|
pub fn WeslCompileError::message(self : WeslCompileError) -> String {
match self {
Resolve(err) => err.message()
Parse(reason) => reason
Validation(reason) => reason
Validate(err) => err.message()
InvalidExpression(expr) => "invalid if attribute expression: `\{expr}`"
DuplicateSymbol(symbol) => "duplicate declaration of `\{symbol}`"
CircularDecl(symbol) => "circular declaration of `\{symbol}`"
MissingDecl(path, item) =>
"module `\{path.to_string()}` has no declaration `\{item}`"
Private(item, path) =>
"import of `\{item}` in module `\{path.to_string()}` is not `@publish`, but another module tried to import it"
}
}
///|
pub(all) suberror ValidateError {
UndefinedSymbol(String)
ParamCount(String, Int, Int)
NotCallable(String)
Duplicate(String)
Cycle(String, String)
}
///|
pub fn ValidateError::message(self : ValidateError) -> String {
match self {
UndefinedSymbol(name) => "cannot find declaration of `\{name}`"
ParamCount(name, expected, got) =>
"incorrect number of arguments to `\{name}`, expected `\{expected}`, got `\{got}`"
NotCallable(name) => "`\{name}` is not callable"
Duplicate(name) => "duplicate declaration of `\{name}`"
Cycle(name, via) => "declaration of `\{name}` is cyclic via `\{via}`"
}
}
///|
pub(all) struct SourcePosition {
line : Int
column : Int
offset : Int
} derive(Eq, Debug)
///|
pub fn SourcePosition::new(
line : Int,
column : Int,
offset : Int,
) -> SourcePosition {
{ line, column, offset }
}
///|
pub(all) struct SourceSpan {
start : SourcePosition
end : SourcePosition
} derive(Eq, Debug)
///|
pub fn SourceSpan::new(
start : SourcePosition,
end : SourcePosition,
) -> SourceSpan {
{ start, end }
}
///|
fn diagnostic_is_newline(code : Int) -> Bool {
code == 10 || code == 13
}
///|
fn diagnostic_line_start(source : String, line : Int) -> Int {
if line <= 1 {
return 0
}
let mut current_line = 1
let mut index = 0
while index < source.length() {
let code = source.code_unit_at(index).to_int()
if code == 13 {
current_line += 1
index += 1
if index < source.length() && source.code_unit_at(index).to_int() == 10 {
index += 1
}
if current_line == line {
return index
}
continue
} else if code == 10 {
current_line += 1
index += 1
if current_line == line {
return index
}
continue
}
index += 1
}
source.length()
}
///|
fn diagnostic_line_end(source : String, line_start : Int) -> Int {
let mut index = line_start
while index < source.length() {
let code = source.code_unit_at(index).to_int()
if diagnostic_is_newline(code) {
return index
}
index += 1
}
source.length()
}
///|
fn diagnostic_repeat(ch : String, count : Int) -> String {
let parts : Array[String] = []
let mut index = 0
while index < count {
parts.push(ch)
index += 1
}
parts.join("")
}
///|
pub fn SourceSpan::from_line_range(
source : String,
start_line : Int,
end_line : Int,
) -> SourceSpan {
let bounded_start_line = if start_line < 1 { 1 } else { start_line }
let bounded_end_line = if end_line < bounded_start_line {
bounded_start_line
} else {
end_line
}
let start_offset = diagnostic_line_start(source, bounded_start_line)
let end_line_start = diagnostic_line_start(source, bounded_end_line)
let end_offset = diagnostic_line_end(source, end_line_start)
{
start: { line: bounded_start_line, column: 1, offset: start_offset },
end: {
line: bounded_end_line,
column: end_offset - end_line_start + 1,
offset: end_offset,
},
}
}
///|
pub(all) struct DiagnosticDetail {
source : String?
output : String?
module_path : ModulePath?
display_name : String?
declaration : String?
span : SourceSpan?
message : String?
} derive(Eq, Debug)
///|
pub fn DiagnosticDetail::default() -> DiagnosticDetail {
{
source: None,
output: None,
module_path: None,
display_name: None,
declaration: None,
span: None,
message: None,
}
}
///|
pub(all) struct Diagnostic {
error : WeslCompileError
detail : DiagnosticDetail
}
///|
pub(all) suberror WeslDiagnosticError {
Diagnostic(Diagnostic)
}
///|
pub fn WeslDiagnosticError::message(self : WeslDiagnosticError) -> String {
match self {
Diagnostic(diagnostic) => diagnostic.to_string()
}
}
///|
pub fn Diagnostic::new(error : WeslCompileError) -> Diagnostic {
{ error, detail: DiagnosticDetail::default() }
}
///|
pub fn WeslCompileError::diagnostic(self : WeslCompileError) -> Diagnostic {
Diagnostic::new(self)
}
///|
pub fn Diagnostic::message(self : Diagnostic) -> String {
match self.detail.message {
Some(message) => message
None => self.error.message()
}
}
///|
pub fn Diagnostic::with_source(
self : Diagnostic,
source : String,
) -> Diagnostic {
match self.detail.source {
Some(_) => self
None => { ..self, detail: { ..self.detail, source: Some(source) } }
}
}
///|
pub fn Diagnostic::with_output(
self : Diagnostic,
output : String,
) -> Diagnostic {
match self.detail.output {
Some(_) => self
None => { ..self, detail: { ..self.detail, output: Some(output) } }
}
}
///|
pub fn Diagnostic::with_span(
self : Diagnostic,
span : SourceSpan,
) -> Diagnostic {
match self.detail.span {
Some(_) => self
None => { ..self, detail: { ..self.detail, span: Some(span) } }
}
}
///|
pub fn Diagnostic::with_syntax_span(
self : Diagnostic,
span : SyntaxSpan,
) -> Diagnostic {
match self.detail.source {
Some(source) =>
self.with_span(
SourceSpan::from_line_range(source, span.start_line, span.end_line),
)
None => self
}
}
///|
pub fn Diagnostic::with_declaration(
self : Diagnostic,
declaration : String,
) -> Diagnostic {
match self.detail.declaration {
Some(_) => self
None =>
{ ..self, detail: { ..self.detail, declaration: Some(declaration) } }
}
}
///|
pub fn Diagnostic::with_module_path(
self : Diagnostic,
path : ModulePath,
display_name : String?,
) -> Diagnostic {
match self.detail.module_path {
Some(_) => self
None =>
{
..self,
detail: { ..self.detail, module_path: Some(path), display_name },
}
}
}
///|
pub fn[S : SourceMap] Diagnostic::with_sourcemap(
self : Diagnostic,
sourcemap : S,
) -> Diagnostic {
let mut diagnostic = self
match diagnostic.detail.declaration {
Some(decl) =>
match S::get_decl(sourcemap, decl) {
Some((path, original_decl)) => {
diagnostic = {
..diagnostic,
detail: {
..diagnostic.detail,
module_path: Some(path),
declaration: Some(original_decl),
display_name: S::get_display_name(sourcemap, path),
},
}
match S::get_source(sourcemap, path) {
Some(source) =>
diagnostic = {
..diagnostic,
detail: { ..diagnostic.detail, source: Some(source) },
}
None => ()
}
}
None => ()
}
None => ()
}
match diagnostic.detail.source {
Some(_) => diagnostic
None =>
match diagnostic.detail.module_path {
Some(path) =>
match S::get_source(sourcemap, path) {
Some(source) =>
{
..diagnostic,
detail: { ..diagnostic.detail, source: Some(source) },
}
None => diagnostic
}
None =>
match S::get_default_source(sourcemap) {
Some(source) =>
{
..diagnostic,
detail: { ..diagnostic.detail, source: Some(source) },
}
None => diagnostic
}
}
}
}
///|
pub fn[S : SourceMap] Diagnostic::unmangle_with_sourcemap(
self : Diagnostic,
sourcemap : S,
) -> Diagnostic {
let message = S::unmangle_text(sourcemap, self.message())
Diagnostic::{ ..self, detail: { ..self.detail, message: Some(message) } }
.with_sourcemap(sourcemap)
.infer_span_from_message()
}
///|
pub fn Diagnostic::unmangle_with_mangler(
self : Diagnostic,
mangler : ManglerKind,
) -> Diagnostic {
let text = self.message()
let parts : Array[String] = []
let mut start = 0
let mut index = 0
while index < text.length() {
let code = text.code_unit_at(index).to_int()
if diagnostic_is_identifier_char(code) {
if start < index {
parts.push(text[start:index].to_owned())
}
let ident_start = index
index += 1
while index < text.length() &&
diagnostic_is_identifier_char(text.code_unit_at(index).to_int()) {
index += 1
}
let ident = text[ident_start:index].to_owned()
match ManglerKind::unmangle(mangler, ident) {
Some((path, item)) => parts.push("\{path.to_string()}::\{item}")
None => parts.push(ident)
}
start = index
} else {
index += 1
}
}
if start < text.length() {
parts.push(text[start:text.length()].to_owned())
}
{ ..self, detail: { ..self.detail, message: Some(parts.join("")) } }
}
///|
fn diagnostic_is_identifier_char(code : Int) -> Bool {
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
(code >= 48 && code <= 57) ||
code == 95
}
///|
pub fn Diagnostic::display_origin(self : Diagnostic) -> String {
match (self.detail.module_path, self.detail.display_name) {
(Some(path), Some(name)) => "\{path.to_string()} (\{name})"
(Some(path), None) => path.to_string()
(None, Some(name)) => name
(None, None) => "unknown module"
}
}
///|
pub fn Diagnostic::display_short_origin(self : Diagnostic) -> String? {
match self.detail.display_name {
Some(name) => Some(name)
None =>
match self.detail.module_path {
Some(path) => Some(path.to_string())
None => None
}
}
}
///|
fn diagnostic_parse_decimal_at(text : String, start : Int) -> (Int, Int)? {
let mut index = start
let mut value = 0
let mut saw_digit = false
while index < text.length() {
let code = text.code_unit_at(index).to_int()
if code < 48 || code > 57 {
break
}
saw_digit = true
value = value * 10 + code - 48
index += 1
}
if saw_digit {
Some((value, index))
} else {
None
}
}
///|
fn diagnostic_line_from_origin(message : String, origin : String) -> Int? {
match message.find(origin + ":") {
Some(start) =>
match diagnostic_parse_decimal_at(message, start + origin.length() + 1) {
Some((line, _)) => Some(line)
None => None
}
None => None
}
}
///|
pub fn Diagnostic::infer_span_from_message(self : Diagnostic) -> Diagnostic {
match (self.detail.source, self.detail.span) {
(Some(source), None) => {
let message = self.message()
let mut inferred_line : Int? = None
match self.detail.module_path {
Some(path) =>
inferred_line = diagnostic_line_from_origin(message, path.to_string())
None => ()
}
match inferred_line {
None =>
match self.detail.display_name {
Some(name) =>
inferred_line = diagnostic_line_from_origin(message, name)
None => ()
}
Some(_) => ()
}
match inferred_line {
Some(line) =>
self.with_span(SourceSpan::from_line_range(source, line, line))
None => self
}
}
_ => self
}
}
///|
fn Diagnostic::snippet_line(self : Diagnostic) -> String? {
match (self.detail.source, self.detail.span) {
(Some(source), Some(span)) => {
let line_start = diagnostic_line_start(source, span.start.line)
let line_end = diagnostic_line_end(source, line_start)
Some(source[line_start:line_end].to_owned())
}
_ => None
}
}
///|
fn Diagnostic::snippet_caret(self : Diagnostic) -> String? {
match self.detail.span {
Some(span) => {
let start_column = if span.start.column < 1 {
1
} else {
span.start.column
}
let end_column = if span.end.line == span.start.line &&
span.end.column > start_column {
span.end.column
} else {
start_column + 1
}
Some(
diagnostic_repeat(" ", start_column - 1) +
diagnostic_repeat("^", end_column - start_column),
)
}
None => None
}
}
///|
pub fn Diagnostic::to_string(self : Diagnostic) -> String {
let message = self.message()
let origin = self.display_origin()
let note = match self.detail.declaration {
Some(decl) => "in declaration of `\{decl}` in \{origin}"
None => "in \{origin}"
}
let parts : Array[String] = ["error: \{message}"]
match self.detail.span {
Some(span) => {
match self.display_short_origin() {
Some(path) =>
parts.push(" --> \{path}:\{span.start.line}:\{span.start.column}")
None =>
parts.push(" --> :\{span.start.line}:\{span.start.column}")
}
match self.snippet_line() {
Some(line) => {
parts.push(" |")
parts.push("\{span.start.line} | \{line}")
match self.snippet_caret() {
Some(caret) => parts.push(" | \{caret} \{message}")
None => ()
}
}
None =>
match self.detail.source {
Some(_) =>
parts.push(
"note: cannot display snippet: invalid source location",
)
None =>
parts.push("note: cannot display snippet: missing source file")
}
}
}
None => ()
}
parts.push("note: \{note}")
parts.join("\n")
}