Compare commits
3 Commits
caabaeeb9c
...
746643527d
| Author | SHA1 | Date | |
|---|---|---|---|
| 746643527d | |||
| 091ff1e422 | |||
| 1fc472a54c |
@@ -697,8 +697,9 @@ pub enum Pattern {
|
|||||||
Var(Ident),
|
Var(Ident),
|
||||||
/// Literal: 42, "hello", true
|
/// Literal: 42, "hello", true
|
||||||
Literal(Literal),
|
Literal(Literal),
|
||||||
/// Constructor: Some(x), None, Ok(v)
|
/// Constructor: Some(x), None, Ok(v), module.Constructor(x)
|
||||||
Constructor {
|
Constructor {
|
||||||
|
module: Option<Ident>,
|
||||||
name: Ident,
|
name: Ident,
|
||||||
fields: Vec<Pattern>,
|
fields: Vec<Pattern>,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
|||||||
@@ -2087,6 +2087,42 @@ impl CBackend {
|
|||||||
self.writeln(" return result;");
|
self.writeln(" return result;");
|
||||||
self.writeln("}");
|
self.writeln("}");
|
||||||
self.writeln("");
|
self.writeln("");
|
||||||
|
// Sort helper: compare two void* as boxed ints
|
||||||
|
self.writeln("static int lux_compare_int(const void* a, const void* b) {");
|
||||||
|
self.writeln(" LuxInt va = *(LuxInt*)(*(void**)a);");
|
||||||
|
self.writeln(" LuxInt vb = *(LuxInt*)(*(void**)b);");
|
||||||
|
self.writeln(" return (va > vb) - (va < vb);");
|
||||||
|
self.writeln("}");
|
||||||
|
self.writeln("");
|
||||||
|
self.writeln("static int lux_compare_string(const void* a, const void* b) {");
|
||||||
|
self.writeln(" LuxString sa = (LuxString)(*(void**)a);");
|
||||||
|
self.writeln(" LuxString sb = (LuxString)(*(void**)b);");
|
||||||
|
self.writeln(" return strcmp(sa, sb);");
|
||||||
|
self.writeln("}");
|
||||||
|
self.writeln("");
|
||||||
|
self.writeln("static LuxList* lux_list_sort(LuxList* list) {");
|
||||||
|
self.writeln(" if (list->length <= 1) {");
|
||||||
|
self.writeln(" lux_incref(list);");
|
||||||
|
self.writeln(" return list;");
|
||||||
|
self.writeln(" }");
|
||||||
|
self.writeln(" LuxList* result = lux_list_new(list->length);");
|
||||||
|
self.writeln(" for (int64_t i = 0; i < list->length; i++) {");
|
||||||
|
self.writeln(" lux_incref(list->elements[i]);");
|
||||||
|
self.writeln(" result->elements[i] = list->elements[i];");
|
||||||
|
self.writeln(" }");
|
||||||
|
self.writeln(" result->length = list->length;");
|
||||||
|
self.writeln(" // Determine element type from first element and sort");
|
||||||
|
self.writeln(" if (result->length > 0 && result->elements[0] != NULL) {");
|
||||||
|
self.writeln(" uint32_t tag = LUX_RC_HEADER(result->elements[0])->tag;");
|
||||||
|
self.writeln(" if (tag == LUX_TAG_BOXED_INT) {");
|
||||||
|
self.writeln(" qsort(result->elements, result->length, sizeof(void*), lux_compare_int);");
|
||||||
|
self.writeln(" } else if (tag == LUX_TAG_STRING) {");
|
||||||
|
self.writeln(" qsort(result->elements, result->length, sizeof(void*), lux_compare_string);");
|
||||||
|
self.writeln(" }");
|
||||||
|
self.writeln(" }");
|
||||||
|
self.writeln(" return result;");
|
||||||
|
self.writeln("}");
|
||||||
|
self.writeln("");
|
||||||
// === Map Runtime Functions ===
|
// === Map Runtime Functions ===
|
||||||
self.writeln("static LuxMap* lux_map_new(int64_t capacity) {");
|
self.writeln("static LuxMap* lux_map_new(int64_t capacity) {");
|
||||||
self.writeln(" LuxMap* map = (LuxMap*)malloc(sizeof(LuxMap));");
|
self.writeln(" LuxMap* map = (LuxMap*)malloc(sizeof(LuxMap));");
|
||||||
@@ -4679,6 +4715,82 @@ impl CBackend {
|
|||||||
Ok(result_var)
|
Ok(result_var)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"sort" => {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(CGenError {
|
||||||
|
message: "List.sort takes 1 argument".to_string(),
|
||||||
|
span: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let result_var = format!("_sorted_{}", self.fresh_name());
|
||||||
|
self.writeln(&format!(
|
||||||
|
"LuxList* {} = lux_list_sort({});",
|
||||||
|
result_var, list
|
||||||
|
));
|
||||||
|
self.register_rc_var(&result_var, "LuxList*");
|
||||||
|
Ok(result_var)
|
||||||
|
}
|
||||||
|
"sortBy" => {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(CGenError {
|
||||||
|
message: "List.sortBy takes 2 arguments".to_string(),
|
||||||
|
span: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let closure = self.emit_expr(&args[1])?;
|
||||||
|
let result_var = format!("_sorted_{}", self.fresh_name());
|
||||||
|
// Copy the list then do insertion sort with custom comparator
|
||||||
|
self.writeln(&format!(
|
||||||
|
"LuxList* {} = lux_list_new({}->length);",
|
||||||
|
result_var, list
|
||||||
|
));
|
||||||
|
self.writeln(&format!(
|
||||||
|
"for (int64_t _i = 0; _i < {}->length; _i++) {{",
|
||||||
|
list
|
||||||
|
));
|
||||||
|
self.writeln(&format!(
|
||||||
|
" lux_incref({}->elements[_i]);",
|
||||||
|
list
|
||||||
|
));
|
||||||
|
self.writeln(&format!(
|
||||||
|
" {}->elements[_i] = {}->elements[_i];",
|
||||||
|
result_var, list
|
||||||
|
));
|
||||||
|
self.writeln("}");
|
||||||
|
self.writeln(&format!(
|
||||||
|
"{}->length = {}->length;",
|
||||||
|
result_var, list
|
||||||
|
));
|
||||||
|
// Insertion sort using the comparator closure
|
||||||
|
self.writeln(&format!(
|
||||||
|
"for (int64_t _i = 1; _i < {}->length; _i++) {{",
|
||||||
|
result_var
|
||||||
|
));
|
||||||
|
self.writeln(&format!(
|
||||||
|
" void* _key = {}->elements[_i];",
|
||||||
|
result_var
|
||||||
|
));
|
||||||
|
self.writeln(" int64_t _j = _i - 1;");
|
||||||
|
self.writeln(&format!(
|
||||||
|
" while (_j >= 0 && *(LuxInt*){}.fn({}.env, {}->elements[_j], _key) > 0) {{",
|
||||||
|
closure, closure, result_var
|
||||||
|
));
|
||||||
|
self.writeln(&format!(
|
||||||
|
" {}->elements[_j + 1] = {}->elements[_j];",
|
||||||
|
result_var, result_var
|
||||||
|
));
|
||||||
|
self.writeln(" _j--;");
|
||||||
|
self.writeln(" }");
|
||||||
|
self.writeln(&format!(
|
||||||
|
" {}->elements[_j + 1] = _key;",
|
||||||
|
result_var
|
||||||
|
));
|
||||||
|
self.writeln("}");
|
||||||
|
self.register_rc_var(&result_var, "LuxList*");
|
||||||
|
Ok(result_var)
|
||||||
|
}
|
||||||
_ => Err(CGenError {
|
_ => Err(CGenError {
|
||||||
message: format!("Unsupported List operation: {}", op),
|
message: format!("Unsupported List operation: {}", op),
|
||||||
span: None,
|
span: None,
|
||||||
@@ -5206,7 +5318,7 @@ impl CBackend {
|
|||||||
if effect.name == "List" {
|
if effect.name == "List" {
|
||||||
match operation.name.as_str() {
|
match operation.name.as_str() {
|
||||||
// Operations returning lists
|
// Operations returning lists
|
||||||
"map" | "filter" | "concat" | "reverse" | "take" | "drop" | "range" => Some("LuxList*".to_string()),
|
"map" | "filter" | "concat" | "reverse" | "take" | "drop" | "range" | "sort" | "sortBy" => Some("LuxList*".to_string()),
|
||||||
// Operations returning Option
|
// Operations returning Option
|
||||||
"head" | "tail" | "get" | "find" => Some("Option".to_string()),
|
"head" | "tail" | "get" | "find" => Some("Option".to_string()),
|
||||||
// Operations returning Int
|
// Operations returning Int
|
||||||
|
|||||||
@@ -1812,6 +1812,18 @@ impl JsBackend {
|
|||||||
end, start, start
|
end, start, start
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
"sort" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
Ok(format!(
|
||||||
|
"[...{}].sort((a, b) => a < b ? -1 : a > b ? 1 : 0)",
|
||||||
|
list
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"sortBy" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let func = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("[...{}].sort({})", list, func))
|
||||||
|
}
|
||||||
_ => Err(JsGenError {
|
_ => Err(JsGenError {
|
||||||
message: format!("Unknown List operation: {}", operation),
|
message: format!("Unknown List operation: {}", operation),
|
||||||
span: None,
|
span: None,
|
||||||
|
|||||||
@@ -333,11 +333,13 @@ mod tests {
|
|||||||
fn test_option_exhaustive() {
|
fn test_option_exhaustive() {
|
||||||
let patterns = vec![
|
let patterns = vec![
|
||||||
Pattern::Constructor {
|
Pattern::Constructor {
|
||||||
|
module: None,
|
||||||
name: make_ident("None"),
|
name: make_ident("None"),
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
span: span(),
|
span: span(),
|
||||||
},
|
},
|
||||||
Pattern::Constructor {
|
Pattern::Constructor {
|
||||||
|
module: None,
|
||||||
name: make_ident("Some"),
|
name: make_ident("Some"),
|
||||||
fields: vec![Pattern::Wildcard(span())],
|
fields: vec![Pattern::Wildcard(span())],
|
||||||
span: span(),
|
span: span(),
|
||||||
@@ -352,6 +354,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_option_missing_none() {
|
fn test_option_missing_none() {
|
||||||
let patterns = vec![Pattern::Constructor {
|
let patterns = vec![Pattern::Constructor {
|
||||||
|
module: None,
|
||||||
name: make_ident("Some"),
|
name: make_ident("Some"),
|
||||||
fields: vec![Pattern::Wildcard(span())],
|
fields: vec![Pattern::Wildcard(span())],
|
||||||
span: span(),
|
span: span(),
|
||||||
@@ -391,11 +394,13 @@ mod tests {
|
|||||||
fn test_result_exhaustive() {
|
fn test_result_exhaustive() {
|
||||||
let patterns = vec![
|
let patterns = vec![
|
||||||
Pattern::Constructor {
|
Pattern::Constructor {
|
||||||
|
module: None,
|
||||||
name: make_ident("Ok"),
|
name: make_ident("Ok"),
|
||||||
fields: vec![Pattern::Wildcard(span())],
|
fields: vec![Pattern::Wildcard(span())],
|
||||||
span: span(),
|
span: span(),
|
||||||
},
|
},
|
||||||
Pattern::Constructor {
|
Pattern::Constructor {
|
||||||
|
module: None,
|
||||||
name: make_ident("Err"),
|
name: make_ident("Err"),
|
||||||
fields: vec![Pattern::Wildcard(span())],
|
fields: vec![Pattern::Wildcard(span())],
|
||||||
span: span(),
|
span: span(),
|
||||||
|
|||||||
@@ -733,7 +733,30 @@ impl Formatter {
|
|||||||
match &lit.kind {
|
match &lit.kind {
|
||||||
LiteralKind::Int(n) => n.to_string(),
|
LiteralKind::Int(n) => n.to_string(),
|
||||||
LiteralKind::Float(f) => format!("{}", f),
|
LiteralKind::Float(f) => format!("{}", f),
|
||||||
LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}")),
|
LiteralKind::String(s) => {
|
||||||
|
if s.contains('\n') {
|
||||||
|
// Use triple-quoted multiline string
|
||||||
|
let tab = " ".repeat(self.config.indent_size);
|
||||||
|
let base_indent = tab.repeat(self.indent_level);
|
||||||
|
let content_indent = tab.repeat(self.indent_level + 1);
|
||||||
|
let lines: Vec<&str> = s.split('\n').collect();
|
||||||
|
let mut result = String::from("\"\"\"\n");
|
||||||
|
for line in &lines {
|
||||||
|
if line.is_empty() {
|
||||||
|
result.push('\n');
|
||||||
|
} else {
|
||||||
|
result.push_str(&content_indent);
|
||||||
|
result.push_str(&line.replace('{', "\\{").replace('}', "\\}"));
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push_str(&base_indent);
|
||||||
|
result.push_str("\"\"\"");
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}"))
|
||||||
|
}
|
||||||
|
},
|
||||||
LiteralKind::Char(c) => format!("'{}'", c),
|
LiteralKind::Char(c) => format!("'{}'", c),
|
||||||
LiteralKind::Bool(b) => b.to_string(),
|
LiteralKind::Bool(b) => b.to_string(),
|
||||||
LiteralKind::Unit => "()".to_string(),
|
LiteralKind::Unit => "()".to_string(),
|
||||||
@@ -772,12 +795,22 @@ impl Formatter {
|
|||||||
Pattern::Wildcard(_) => "_".to_string(),
|
Pattern::Wildcard(_) => "_".to_string(),
|
||||||
Pattern::Var(ident) => ident.name.clone(),
|
Pattern::Var(ident) => ident.name.clone(),
|
||||||
Pattern::Literal(lit) => self.format_literal(lit),
|
Pattern::Literal(lit) => self.format_literal(lit),
|
||||||
Pattern::Constructor { name, fields, .. } => {
|
Pattern::Constructor {
|
||||||
|
module,
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let prefix = match module {
|
||||||
|
Some(m) => format!("{}.", m.name),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
if fields.is_empty() {
|
if fields.is_empty() {
|
||||||
name.name.clone()
|
format!("{}{}", prefix, name.name)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"{}({})",
|
"{}{}({})",
|
||||||
|
prefix,
|
||||||
name.name,
|
name.name,
|
||||||
fields
|
fields
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub enum BuiltinFn {
|
|||||||
ListGet,
|
ListGet,
|
||||||
ListRange,
|
ListRange,
|
||||||
ListForEach,
|
ListForEach,
|
||||||
|
ListSort,
|
||||||
|
ListSortBy,
|
||||||
|
|
||||||
// String operations
|
// String operations
|
||||||
StringSplit,
|
StringSplit,
|
||||||
@@ -980,6 +982,11 @@ impl Interpreter {
|
|||||||
"forEach".to_string(),
|
"forEach".to_string(),
|
||||||
Value::Builtin(BuiltinFn::ListForEach),
|
Value::Builtin(BuiltinFn::ListForEach),
|
||||||
),
|
),
|
||||||
|
("sort".to_string(), Value::Builtin(BuiltinFn::ListSort)),
|
||||||
|
(
|
||||||
|
"sortBy".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::ListSortBy),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
env.define("List", list_module);
|
env.define("List", list_module);
|
||||||
|
|
||||||
@@ -2742,6 +2749,67 @@ impl Interpreter {
|
|||||||
Ok(EvalResult::Value(Value::Unit))
|
Ok(EvalResult::Value(Value::Unit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuiltinFn::ListSort => {
|
||||||
|
// List.sort(list) - sort using natural ordering (Int, Float, String, Bool)
|
||||||
|
let mut list =
|
||||||
|
Self::expect_arg_1::<Vec<Value>>(&args, "List.sort", span)?;
|
||||||
|
list.sort_by(|a, b| Self::compare_values(a, b));
|
||||||
|
Ok(EvalResult::Value(Value::List(list)))
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltinFn::ListSortBy => {
|
||||||
|
// List.sortBy(list, fn(a, b) => Int) - sort with custom comparator
|
||||||
|
// Comparator returns negative (a < b), 0 (a == b), or positive (a > b)
|
||||||
|
let (list, func) =
|
||||||
|
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.sortBy", span)?;
|
||||||
|
let mut indexed: Vec<(usize, Value)> =
|
||||||
|
list.into_iter().enumerate().collect();
|
||||||
|
let mut err: Option<RuntimeError> = None;
|
||||||
|
let func_ref = &func;
|
||||||
|
let self_ptr = self as *mut Self;
|
||||||
|
indexed.sort_by(|a, b| {
|
||||||
|
if err.is_some() {
|
||||||
|
return std::cmp::Ordering::Equal;
|
||||||
|
}
|
||||||
|
// Safety: we're in a single-threaded context and the closure
|
||||||
|
// needs mutable access to call eval_call_to_value
|
||||||
|
let interp = unsafe { &mut *self_ptr };
|
||||||
|
match interp.eval_call_to_value(
|
||||||
|
func_ref.clone(),
|
||||||
|
vec![a.1.clone(), b.1.clone()],
|
||||||
|
span,
|
||||||
|
) {
|
||||||
|
Ok(Value::Int(n)) => {
|
||||||
|
if n < 0 {
|
||||||
|
std::cmp::Ordering::Less
|
||||||
|
} else if n > 0 {
|
||||||
|
std::cmp::Ordering::Greater
|
||||||
|
} else {
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
err = Some(RuntimeError {
|
||||||
|
message: "List.sortBy comparator must return Int"
|
||||||
|
.to_string(),
|
||||||
|
span: Some(span),
|
||||||
|
});
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
err = Some(e);
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(e) = err {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
let result: Vec<Value> =
|
||||||
|
indexed.into_iter().map(|(_, v)| v).collect();
|
||||||
|
Ok(EvalResult::Value(Value::List(result)))
|
||||||
|
}
|
||||||
|
|
||||||
// Additional String operations
|
// Additional String operations
|
||||||
BuiltinFn::StringStartsWith => {
|
BuiltinFn::StringStartsWith => {
|
||||||
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
|
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
|
||||||
@@ -3357,6 +3425,18 @@ impl Interpreter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compare two values for natural ordering (used by List.sort)
|
||||||
|
fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
|
||||||
|
match (a, b) {
|
||||||
|
(Value::Int(x), Value::Int(y)) => x.cmp(y),
|
||||||
|
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
|
||||||
|
(Value::String(x), Value::String(y)) => x.cmp(y),
|
||||||
|
(Value::Bool(x), Value::Bool(y)) => x.cmp(y),
|
||||||
|
(Value::Char(x), Value::Char(y)) => x.cmp(y),
|
||||||
|
_ => std::cmp::Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> {
|
fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> {
|
||||||
match pattern {
|
match pattern {
|
||||||
Pattern::Wildcard(_) => Some(Vec::new()),
|
Pattern::Wildcard(_) => Some(Vec::new()),
|
||||||
|
|||||||
226
src/lexer.rs
226
src/lexer.rs
@@ -411,7 +411,26 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// String literals
|
// String literals
|
||||||
'"' => self.scan_string(start)?,
|
'"' => {
|
||||||
|
// Check for triple-quote multiline string """
|
||||||
|
if self.peek() == Some('"') {
|
||||||
|
// Clone to peek at the second char
|
||||||
|
let mut lookahead = self.chars.clone();
|
||||||
|
lookahead.next(); // consume first peeked "
|
||||||
|
if lookahead.peek() == Some(&'"') {
|
||||||
|
// It's a triple-quote: consume both remaining quotes
|
||||||
|
self.advance(); // second "
|
||||||
|
self.advance(); // third "
|
||||||
|
self.scan_multiline_string(start)?
|
||||||
|
} else {
|
||||||
|
// It's an empty string ""
|
||||||
|
self.advance(); // consume closing "
|
||||||
|
TokenKind::String(String::new())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.scan_string(start)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Char literals
|
// Char literals
|
||||||
'\'' => self.scan_char(start)?,
|
'\'' => self.scan_char(start)?,
|
||||||
@@ -669,6 +688,211 @@ impl<'a> Lexer<'a> {
|
|||||||
Ok(TokenKind::InterpolatedString(parts))
|
Ok(TokenKind::InterpolatedString(parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scan_multiline_string(&mut self, _start: usize) -> Result<TokenKind, LexError> {
|
||||||
|
let mut parts: Vec<StringPart> = Vec::new();
|
||||||
|
let mut current_literal = String::new();
|
||||||
|
|
||||||
|
// Skip the first newline after opening """ if present
|
||||||
|
if self.peek() == Some('\n') {
|
||||||
|
self.advance();
|
||||||
|
} else if self.peek() == Some('\r') {
|
||||||
|
self.advance();
|
||||||
|
if self.peek() == Some('\n') {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self.advance() {
|
||||||
|
Some('"') => {
|
||||||
|
// Check for closing """
|
||||||
|
if self.peek() == Some('"') {
|
||||||
|
let mut lookahead = self.chars.clone();
|
||||||
|
lookahead.next(); // consume first peeked "
|
||||||
|
if lookahead.peek() == Some(&'"') {
|
||||||
|
// Closing """ found
|
||||||
|
self.advance(); // second "
|
||||||
|
self.advance(); // third "
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not closing triple-quote, just a regular " in the string
|
||||||
|
current_literal.push('"');
|
||||||
|
}
|
||||||
|
Some('\\') => {
|
||||||
|
// Handle escape sequences (same as regular strings)
|
||||||
|
match self.peek() {
|
||||||
|
Some('{') => {
|
||||||
|
self.advance();
|
||||||
|
current_literal.push('{');
|
||||||
|
}
|
||||||
|
Some('}') => {
|
||||||
|
self.advance();
|
||||||
|
current_literal.push('}');
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let escape_start = self.pos;
|
||||||
|
let escaped = match self.advance() {
|
||||||
|
Some('n') => '\n',
|
||||||
|
Some('r') => '\r',
|
||||||
|
Some('t') => '\t',
|
||||||
|
Some('\\') => '\\',
|
||||||
|
Some('"') => '"',
|
||||||
|
Some('0') => '\0',
|
||||||
|
Some('\'') => '\'',
|
||||||
|
Some(c) => {
|
||||||
|
return Err(LexError {
|
||||||
|
message: format!("Invalid escape sequence: \\{}", c),
|
||||||
|
span: Span::new(escape_start - 1, self.pos),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(LexError {
|
||||||
|
message: "Unterminated multiline string".into(),
|
||||||
|
span: Span::new(_start, self.pos),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
current_literal.push(escaped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some('{') => {
|
||||||
|
// Interpolation (same as regular strings)
|
||||||
|
if !current_literal.is_empty() {
|
||||||
|
parts.push(StringPart::Literal(std::mem::take(&mut current_literal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut expr_text = String::new();
|
||||||
|
let mut brace_depth = 1;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self.advance() {
|
||||||
|
Some('{') => {
|
||||||
|
brace_depth += 1;
|
||||||
|
expr_text.push('{');
|
||||||
|
}
|
||||||
|
Some('}') => {
|
||||||
|
brace_depth -= 1;
|
||||||
|
if brace_depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
expr_text.push('}');
|
||||||
|
}
|
||||||
|
Some(c) => expr_text.push(c),
|
||||||
|
None => {
|
||||||
|
return Err(LexError {
|
||||||
|
message: "Unterminated interpolation in multiline string"
|
||||||
|
.into(),
|
||||||
|
span: Span::new(_start, self.pos),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push(StringPart::Expr(expr_text));
|
||||||
|
}
|
||||||
|
Some(c) => current_literal.push(c),
|
||||||
|
None => {
|
||||||
|
return Err(LexError {
|
||||||
|
message: "Unterminated multiline string".into(),
|
||||||
|
span: Span::new(_start, self.pos),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip common leading whitespace from all lines
|
||||||
|
let strip_indent = |s: &str| -> String {
|
||||||
|
if s.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let lines: Vec<&str> = s.split('\n').collect();
|
||||||
|
// Find minimum indentation of non-empty lines
|
||||||
|
let min_indent = lines
|
||||||
|
.iter()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.map(|line| line.len() - line.trim_start().len())
|
||||||
|
.min()
|
||||||
|
.unwrap_or(0);
|
||||||
|
// Strip that indentation from each line
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| {
|
||||||
|
if line.len() >= min_indent {
|
||||||
|
&line[min_indent..]
|
||||||
|
} else {
|
||||||
|
line.trim_start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Strip trailing whitespace-only line before closing """
|
||||||
|
let trim_trailing = |s: &mut String| {
|
||||||
|
// Remove trailing spaces/tabs (indent before closing """)
|
||||||
|
while s.ends_with(' ') || s.ends_with('\t') {
|
||||||
|
s.pop();
|
||||||
|
}
|
||||||
|
// Remove the trailing newline
|
||||||
|
if s.ends_with('\n') {
|
||||||
|
s.pop();
|
||||||
|
if s.ends_with('\r') {
|
||||||
|
s.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
trim_trailing(&mut current_literal);
|
||||||
|
let result = strip_indent(¤t_literal);
|
||||||
|
return Ok(TokenKind::String(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining literal
|
||||||
|
if !current_literal.is_empty() {
|
||||||
|
trim_trailing(&mut current_literal);
|
||||||
|
parts.push(StringPart::Literal(current_literal));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For interpolated multiline strings, strip indent from literal parts
|
||||||
|
// First, collect all literal content to find min indent
|
||||||
|
let mut all_text = String::new();
|
||||||
|
for part in &parts {
|
||||||
|
if let StringPart::Literal(lit) = part {
|
||||||
|
all_text.push_str(lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let lines: Vec<&str> = all_text.split('\n').collect();
|
||||||
|
let min_indent = lines
|
||||||
|
.iter()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.map(|line| line.len() - line.trim_start().len())
|
||||||
|
.min()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if min_indent > 0 {
|
||||||
|
for part in &mut parts {
|
||||||
|
if let StringPart::Literal(lit) = part {
|
||||||
|
let stripped_lines: Vec<&str> = lit
|
||||||
|
.split('\n')
|
||||||
|
.map(|line| {
|
||||||
|
if line.len() >= min_indent {
|
||||||
|
&line[min_indent..]
|
||||||
|
} else {
|
||||||
|
line.trim_start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
*lit = stripped_lines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TokenKind::InterpolatedString(parts))
|
||||||
|
}
|
||||||
|
|
||||||
fn scan_char(&mut self, start: usize) -> Result<TokenKind, LexError> {
|
fn scan_char(&mut self, start: usize) -> Result<TokenKind, LexError> {
|
||||||
let c = match self.advance() {
|
let c = match self.advance() {
|
||||||
Some('\\') => match self.advance() {
|
Some('\\') => match self.advance() {
|
||||||
|
|||||||
43
src/main.rs
43
src/main.rs
@@ -3925,6 +3925,49 @@ c")"#;
|
|||||||
assert_eq!(eval(source).unwrap(), r#""literal {braces}""#);
|
assert_eq!(eval(source).unwrap(), r#""literal {braces}""#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_string() {
|
||||||
|
let source = r#"
|
||||||
|
let s = """
|
||||||
|
hello
|
||||||
|
world
|
||||||
|
"""
|
||||||
|
let result = String.length(s)
|
||||||
|
"#;
|
||||||
|
// "hello\nworld" = 11 chars
|
||||||
|
assert_eq!(eval(source).unwrap(), "11");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_string_with_quotes() {
|
||||||
|
// Quotes are fine in the middle of triple-quoted strings
|
||||||
|
let source = "let s = \"\"\"\n She said \"hello\" to him.\n\"\"\"";
|
||||||
|
assert_eq!(eval(source).unwrap(), r#""She said "hello" to him.""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_string_interpolation() {
|
||||||
|
let source = r#"
|
||||||
|
let name = "Lux"
|
||||||
|
let s = """
|
||||||
|
Hello, {name}!
|
||||||
|
"""
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), r#""Hello, Lux!""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_string_empty() {
|
||||||
|
let source = r#"let s = """""""#;
|
||||||
|
assert_eq!(eval(source).unwrap(), r#""""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_string_inline() {
|
||||||
|
let source = r#"let s = """hello world""""#;
|
||||||
|
assert_eq!(eval(source).unwrap(), r#""hello world""#);
|
||||||
|
}
|
||||||
|
|
||||||
// Option tests
|
// Option tests
|
||||||
#[test]
|
#[test]
|
||||||
fn test_option_constructors() {
|
fn test_option_constructors() {
|
||||||
|
|||||||
@@ -279,6 +279,12 @@ impl ModuleLoader {
|
|||||||
}
|
}
|
||||||
Declaration::Type(t) if t.visibility == Visibility::Public => {
|
Declaration::Type(t) if t.visibility == Visibility::Public => {
|
||||||
exports.insert(t.name.name.clone());
|
exports.insert(t.name.name.clone());
|
||||||
|
// Also export constructors for ADT types
|
||||||
|
if let crate::ast::TypeDef::Enum(variants) = &t.definition {
|
||||||
|
for variant in variants {
|
||||||
|
exports.insert(variant.name.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Declaration::Effect(e) => {
|
Declaration::Effect(e) => {
|
||||||
// Effects are always exported
|
// Effects are always exported
|
||||||
|
|||||||
@@ -1922,9 +1922,27 @@ impl Parser {
|
|||||||
TokenKind::Ident(name) => {
|
TokenKind::Ident(name) => {
|
||||||
// Check if it starts with uppercase (constructor) or lowercase (variable)
|
// Check if it starts with uppercase (constructor) or lowercase (variable)
|
||||||
if name.chars().next().map_or(false, |c| c.is_uppercase()) {
|
if name.chars().next().map_or(false, |c| c.is_uppercase()) {
|
||||||
self.parse_constructor_pattern()
|
self.parse_constructor_pattern_with_module(None)
|
||||||
} else {
|
} else {
|
||||||
let ident = self.parse_ident()?;
|
let ident = self.parse_ident()?;
|
||||||
|
// Check for module-qualified constructor: module.Constructor
|
||||||
|
if self.check(TokenKind::Dot) {
|
||||||
|
// Peek ahead to see if next is an uppercase identifier
|
||||||
|
let dot_pos = self.pos;
|
||||||
|
self.advance(); // skip dot
|
||||||
|
if let TokenKind::Ident(next_name) = self.peek_kind() {
|
||||||
|
if next_name
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.map_or(false, |c| c.is_uppercase())
|
||||||
|
{
|
||||||
|
return self
|
||||||
|
.parse_constructor_pattern_with_module(Some(ident));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not a module-qualified constructor, backtrack
|
||||||
|
self.pos = dot_pos;
|
||||||
|
}
|
||||||
Ok(Pattern::Var(ident))
|
Ok(Pattern::Var(ident))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1934,8 +1952,14 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_constructor_pattern(&mut self) -> Result<Pattern, ParseError> {
|
fn parse_constructor_pattern_with_module(
|
||||||
let start = self.current_span();
|
&mut self,
|
||||||
|
module: Option<Ident>,
|
||||||
|
) -> Result<Pattern, ParseError> {
|
||||||
|
let start = module
|
||||||
|
.as_ref()
|
||||||
|
.map(|m| m.span)
|
||||||
|
.unwrap_or_else(|| self.current_span());
|
||||||
let name = self.parse_ident()?;
|
let name = self.parse_ident()?;
|
||||||
|
|
||||||
if self.check(TokenKind::LParen) {
|
if self.check(TokenKind::LParen) {
|
||||||
@@ -1952,10 +1976,16 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
self.expect(TokenKind::RParen)?;
|
self.expect(TokenKind::RParen)?;
|
||||||
let span = start.merge(self.previous_span());
|
let span = start.merge(self.previous_span());
|
||||||
Ok(Pattern::Constructor { name, fields, span })
|
|
||||||
} else {
|
|
||||||
let span = name.span;
|
|
||||||
Ok(Pattern::Constructor {
|
Ok(Pattern::Constructor {
|
||||||
|
module,
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
span,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let span = start.merge(name.span);
|
||||||
|
Ok(Pattern::Constructor {
|
||||||
|
module,
|
||||||
name,
|
name,
|
||||||
fields: Vec::new(),
|
fields: Vec::new(),
|
||||||
span,
|
span,
|
||||||
|
|||||||
@@ -981,6 +981,13 @@ impl TypeChecker {
|
|||||||
if !fields.is_empty() {
|
if !fields.is_empty() {
|
||||||
self.env.bind(&name, TypeScheme::mono(Type::Record(fields)));
|
self.env.bind(&name, TypeScheme::mono(Type::Record(fields)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also copy type definitions so imported types are usable
|
||||||
|
for (type_name, type_def) in &module_checker.env.types {
|
||||||
|
if !self.env.types.contains_key(type_name) {
|
||||||
|
self.env.types.insert(type_name.clone(), type_def.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ImportKind::Direct => {
|
ImportKind::Direct => {
|
||||||
// Import a specific name directly
|
// Import a specific name directly
|
||||||
@@ -2476,7 +2483,7 @@ impl TypeChecker {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern::Constructor { name, fields, span } => {
|
Pattern::Constructor { name, fields, span, .. } => {
|
||||||
// Look up constructor
|
// Look up constructor
|
||||||
// For now, handle Option specially
|
// For now, handle Option specially
|
||||||
match name.name.as_str() {
|
match name.name.as_str() {
|
||||||
|
|||||||
20
src/types.rs
20
src/types.rs
@@ -1551,6 +1551,26 @@ impl TypeEnv {
|
|||||||
Type::Unit,
|
Type::Unit,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"sort".to_string(),
|
||||||
|
Type::function(
|
||||||
|
vec![Type::List(Box::new(Type::var()))],
|
||||||
|
Type::List(Box::new(Type::var())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sortBy".to_string(),
|
||||||
|
{
|
||||||
|
let elem = Type::var();
|
||||||
|
Type::function(
|
||||||
|
vec![
|
||||||
|
Type::List(Box::new(elem.clone())),
|
||||||
|
Type::function(vec![elem.clone(), elem], Type::Int),
|
||||||
|
],
|
||||||
|
Type::List(Box::new(Type::var())),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
env.bind("List", TypeScheme::mono(list_module_type));
|
env.bind("List", TypeScheme::mono(list_module_type));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user