extend Cordial with strings, booleans, while loops, functions, type annotations, arrays, and error recovery

This commit is contained in:
jess 2026-04-06 02:45:02 -07:00
parent 5ebf308104
commit 479c8a9482
4 changed files with 1474 additions and 44 deletions

View File

@ -38,15 +38,42 @@ pub fn classify_line(index: usize, raw: &str) -> ClassifiedLine {
fn is_cordial(line: &str) -> bool { fn is_cordial(line: &str) -> bool {
if line.starts_with("let ") { if line.starts_with("let ") {
let rest = &line[4..]; let rest = &line[4..];
if let Some(colon_pos) = rest.find(':') {
let before_colon = rest[..colon_pos].trim();
if is_ident(before_colon) {
let after_colon = &rest[colon_pos + 1..];
if after_colon.contains('=') {
return true;
}
}
}
if let Some(eq_pos) = rest.find('=') { if let Some(eq_pos) = rest.find('=') {
let after_eq = rest.as_bytes().get(eq_pos + 1);
if after_eq != Some(&b'=') {
let name = rest[..eq_pos].trim(); let name = rest[..eq_pos].trim();
if is_ident(name) { if is_ident(name) {
return true; return true;
} }
} }
}
return false; return false;
} }
// while (...) {
if line.starts_with("while ") || line.starts_with("while(") {
return true;
}
// fn name(...) {
if line.starts_with("fn ") {
return true;
}
// lone closing brace
if line == "}" || line.starts_with("} ") {
return true;
}
if let Some(eq_pos) = line.find('=') { if let Some(eq_pos) = line.find('=') {
if eq_pos > 0 { if eq_pos > 0 {
let before = &line[..eq_pos]; let before = &line[..eq_pos];
@ -88,16 +115,32 @@ fn is_ident(s: &str) -> bool {
pub fn classify_document(text: &str) -> Vec<ClassifiedLine> { pub fn classify_document(text: &str) -> Vec<ClassifiedLine> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut depth: usize = 0; let mut comment_depth: usize = 0;
let mut brace_depth: i32 = 0;
for (i, line) in text.lines().enumerate() { for (i, line) in text.lines().enumerate() {
let was_in_comment = depth > 0; let was_in_comment = comment_depth > 0;
depth = scan_comment_depth(line, depth); comment_depth = scan_comment_depth(line, comment_depth);
if was_in_comment || line.trim().starts_with("/*") { if was_in_comment || line.trim().starts_with("/*") {
result.push(ClassifiedLine { index: i, kind: LineKind::Comment, content: line.to_string() }); result.push(ClassifiedLine { index: i, kind: LineKind::Comment, content: line.to_string() });
} else if brace_depth > 0 {
let trimmed = line.trim();
let opens = trimmed.matches('{').count() as i32;
let closes = trimmed.matches('}').count() as i32;
brace_depth += opens - closes;
if brace_depth < 0 { brace_depth = 0; }
result.push(ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() });
} else { } else {
result.push(classify_line(i, line)); let cl = classify_line(i, line);
if cl.kind == LineKind::Cordial {
let trimmed = line.trim();
let opens = trimmed.matches('{').count() as i32;
let closes = trimmed.matches('}').count() as i32;
brace_depth += opens - closes;
if brace_depth < 0 { brace_depth = 0; }
}
result.push(cl);
} }
} }
@ -221,4 +264,52 @@ mod tests {
assert_eq!(lines[3].kind, LineKind::Comment); assert_eq!(lines[3].kind, LineKind::Comment);
assert_eq!(lines[4].kind, LineKind::Cordial); assert_eq!(lines[4].kind, LineKind::Cordial);
} }
#[test]
fn while_line() {
let c = classify_line(0, "while (i < 10) {");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn fn_line() {
let c = classify_line(0, "fn add(a, b) {");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn closing_brace() {
let c = classify_line(0, "}");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn while_block_body_classified() {
let doc = "while (x > 0) {\n x = x - 1\n}";
let lines = classify_document(doc);
assert_eq!(lines[0].kind, LineKind::Cordial);
assert_eq!(lines[1].kind, LineKind::Cordial);
assert_eq!(lines[2].kind, LineKind::Cordial);
}
#[test]
fn fn_block_body_classified() {
let doc = "fn add(a, b) {\n a + b\n}";
let lines = classify_document(doc);
assert_eq!(lines[0].kind, LineKind::Cordial);
assert_eq!(lines[1].kind, LineKind::Cordial);
assert_eq!(lines[2].kind, LineKind::Cordial);
}
#[test]
fn let_with_type_annotation() {
let c = classify_line(0, "let x: int = 5");
assert_eq!(c.kind, LineKind::Cordial);
}
#[test]
fn let_with_bool_type() {
let c = classify_line(0, "let flag: bool = 1");
assert_eq!(c.kind, LineKind::Cordial);
}
} }

View File

@ -1,5 +1,6 @@
use serde::Serialize; use serde::Serialize;
use crate::doc::{classify_document, LineKind}; use crate::doc::{classify_document, LineKind};
use crate::interp;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct EvalResult { pub struct EvalResult {
@ -21,56 +22,48 @@ pub struct DocumentResult {
pub fn evaluate_document(text: &str) -> DocumentResult { pub fn evaluate_document(text: &str) -> DocumentResult {
let classified = classify_document(text); let classified = classify_document(text);
let mut cordial_lines: Vec<String> = Vec::new();
let mut results = Vec::new(); let mut results = Vec::new();
let mut errors = Vec::new(); let mut errors = Vec::new();
let mut lines: Vec<(usize, &str, bool)> = Vec::new();
for cl in &classified { for cl in &classified {
match cl.kind { match cl.kind {
LineKind::Cordial => { LineKind::Cordial => lines.push((cl.index, &cl.content, false)),
cordial_lines.push(cl.content.clone()); LineKind::Eval => lines.push((cl.index, &cl.content, true)),
}
LineKind::Eval => {
let expr = cl.content.trim().strip_prefix("/=").unwrap_or("").trim();
if expr.is_empty() {
errors.push(EvalError {
line: cl.index,
error: "empty expression".into(),
});
continue;
}
let mut eval_program = cordial_lines.clone();
eval_program.push(expr.to_string());
let program = eval_program.join("\n");
match cord_expr::parse_expr(&program) {
Ok(graph) => {
let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0);
results.push(EvalResult {
line: cl.index,
result: format_value(val),
});
}
Err(e) => {
errors.push(EvalError {
line: cl.index,
error: e,
});
}
}
}
LineKind::Comment | LineKind::Markdown => {} LineKind::Comment | LineKind::Markdown => {}
} }
} }
let interp_results = interp::interpret_document(&lines);
for ir in interp_results {
match ir.value {
Some(interp::Value::Error(e)) => {
errors.push(EvalError { line: ir.line, error: e });
}
Some(v) => {
let s = v.display();
if !s.is_empty() {
results.push(EvalResult { line: ir.line, result: s });
}
}
None => {}
}
}
DocumentResult { results, errors } DocumentResult { results, errors }
} }
pub fn evaluate_line(text: &str) -> Result<String, String> { pub fn evaluate_line(text: &str) -> Result<String, String> {
let mut interp = interp::Interpreter::new();
match interp.eval_expr_str(text) {
Ok(v) => Ok(v.display()),
Err(_) => {
// fall back to cord-expr/cord-trig for trig and CORDIC expressions
let graph = cord_expr::parse_expr(text)?; let graph = cord_expr::parse_expr(text)?;
let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0); let val = cord_trig::eval::evaluate(&graph, 0.0, 0.0, 0.0);
Ok(format_value(val)) Ok(format_value(val))
}
}
} }
fn format_value(val: f64) -> String { fn format_value(val: f64) -> String {
@ -152,4 +145,78 @@ mod tests {
assert_eq!(result.results.len(), 1); assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "15"); assert_eq!(result.results[0].result, "15");
} }
#[test]
fn eval_string_concat() {
let doc = "let x = \"hello\"\nlet y = \"world\"\n/= x + \" \" + y";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "hello world");
}
#[test]
fn eval_booleans() {
let doc = "let x = true\n/= x\n/= 1 > 0";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 2);
assert_eq!(result.results[0].result, "true");
assert_eq!(result.results[1].result, "true");
}
#[test]
fn eval_while_loop() {
let doc = "let i = 0\nlet sum = 0\nwhile (i < 10) {\n sum = sum + i\n i = i + 1\n}\n/= sum";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "45");
}
#[test]
fn eval_fn_block() {
let doc = "fn add(a, b) {\n a + b\n}\n/= add(3, 4)";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "7");
}
#[test]
fn eval_type_annotation() {
let doc = "let x: int = 3.7\n/= x";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "3");
}
#[test]
fn eval_type_annotation_bool_error() {
let doc = "let x: bool = 2\n/= x";
let result = evaluate_document(doc);
assert!(result.errors.len() >= 1);
assert!(result.errors[0].error.contains("not a valid bool"));
}
#[test]
fn eval_array() {
let doc = "let arr = [1, \"two\", true]\n/= arr";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "[1, \"two\", true]");
}
#[test]
fn eval_error_recovery() {
let doc = "let x = undefined_var\nlet y = 5\n/= y";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "5");
assert!(result.errors.len() >= 1);
}
#[test]
fn eval_mixed_markdown_and_code() {
let doc = "# Notes\nlet x = 10\nSome text here\nwhile (x > 0) {\n x = x - 1\n}\n/= x";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "0");
}
} }

1271
core/src/interp.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
pub mod doc; pub mod doc;
pub mod document; pub mod document;
pub mod eval; pub mod eval;
pub mod interp;
pub mod persist; pub mod persist;
pub mod ffi; pub mod ffi;