finish engram-lang: protocols, decorators, imports, Result, closures, stdlib, integration tests
This commit is contained in:
Generated
+21
@@ -377,6 +377,19 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "el-integration"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"el-compiler",
|
||||
"el-lexer",
|
||||
"el-parser",
|
||||
"el-seal",
|
||||
"el-stdlib",
|
||||
"el-types",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "el-lexer"
|
||||
version = "0.1.0"
|
||||
@@ -430,6 +443,14 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "el-stdlib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"el-parser",
|
||||
"el-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "el-test"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -9,6 +9,8 @@ members = [
|
||||
"crates/el-registry",
|
||||
"crates/el-build",
|
||||
"crates/el-test",
|
||||
"crates/el-stdlib",
|
||||
"crates/el-integration",
|
||||
"bin/el",
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -30,6 +32,8 @@ el-manifest = { path = "crates/el-manifest" }
|
||||
el-registry = { path = "crates/el-registry" }
|
||||
el-build = { path = "crates/el-build" }
|
||||
el-test = { path = "crates/el-test" }
|
||||
el-stdlib = { path = "crates/el-stdlib" }
|
||||
el-integration = { path = "crates/el-integration" }
|
||||
|
||||
# Engram crypto (path dep — the sealed target depends on it)
|
||||
engram-crypto = { path = "../engram/crates/engram-crypto" }
|
||||
|
||||
+994
-17
File diff suppressed because it is too large
Load Diff
@@ -298,9 +298,9 @@ impl BuildSystem {
|
||||
|
||||
let source = std::fs::read_to_string(&entry)?;
|
||||
let tokens = el_lexer::tokenize(&source)
|
||||
.map_err(|e| el_compiler::CompileError::Lex(e))?;
|
||||
.map_err(el_compiler::CompileError::Lex)?;
|
||||
let program = el_parser::parse(tokens, source.clone())
|
||||
.map_err(|e| el_compiler::CompileError::Parse(e))?;
|
||||
.map_err(el_compiler::CompileError::Parse)?;
|
||||
let mut checker = el_types::TypeChecker::with_builtins();
|
||||
let diags = checker.check(&program);
|
||||
Ok(diags.iter().map(|d| d.message.clone()).collect())
|
||||
|
||||
@@ -117,7 +117,7 @@ impl PluginRegistry {
|
||||
manifest: &Manifest,
|
||||
plugin_dir: &Path,
|
||||
) -> Result<(), PluginError> {
|
||||
for (name, _version) in &manifest.plugins {
|
||||
for name in manifest.plugins.keys() {
|
||||
// TODO(LLVM backend): use `libloading` crate to dlopen the .dylib/.so,
|
||||
// look up the `engram_plugin_init` symbol, call it, and register the
|
||||
// returned Box<dyn CompilerPlugin>.
|
||||
|
||||
@@ -16,6 +16,13 @@ pub enum Value {
|
||||
Nil,
|
||||
/// A list of values (used for `activate` results and array literals).
|
||||
List(Vec<Value>),
|
||||
/// A key-value map — used for struct instances and Map<K,V> literals.
|
||||
/// Stored as a Vec of pairs to keep ordering and remain Serialize-friendly.
|
||||
Map(Vec<(String, Value)>),
|
||||
/// A Result<T,E> value — Ok variant.
|
||||
ResultOk(Box<Value>),
|
||||
/// A Result<T,E> value — Err variant.
|
||||
ResultErr(Box<Value>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Value {
|
||||
@@ -30,6 +37,12 @@ impl std::fmt::Display for Value {
|
||||
let items: Vec<_> = vs.iter().map(|v| v.to_string()).collect();
|
||||
write!(f, "[{}]", items.join(", "))
|
||||
}
|
||||
Value::Map(pairs) => {
|
||||
let items: Vec<_> = pairs.iter().map(|(k, v)| format!("{k}: {v}")).collect();
|
||||
write!(f, "{{{}}}", items.join(", "))
|
||||
}
|
||||
Value::ResultOk(v) => write!(f, "Ok({v})"),
|
||||
Value::ResultErr(e) => write!(f, "Err({e})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +102,13 @@ pub enum Bytecode {
|
||||
GetField(String),
|
||||
/// Index into an array: pops index then array.
|
||||
GetIndex,
|
||||
/// Build a Map from the top N key-value pairs on the stack
|
||||
/// (keys are strings pushed as Str, values follow each key).
|
||||
BuildMap(u32),
|
||||
/// Build a struct instance: pop N field values (named by fields in order), push Map.
|
||||
BuildStruct { type_name: String, fields: Vec<String> },
|
||||
/// Set a field on the Map on top of stack.
|
||||
SetField(String),
|
||||
|
||||
// ── Special ───────────────────────────────────────────────────────────────
|
||||
/// `activate TypeName "query"` — emit a semantic query stub.
|
||||
@@ -132,6 +152,11 @@ impl std::fmt::Display for Bytecode {
|
||||
Bytecode::JumpIfNot(off) => write!(f, "JUMPIFNOT {off:+}"),
|
||||
Bytecode::GetField(n) => write!(f, "GETFIELD {n}"),
|
||||
Bytecode::GetIndex => write!(f, "GETINDEX"),
|
||||
Bytecode::BuildMap(n) => write!(f, "BUILDMAP {n}"),
|
||||
Bytecode::BuildStruct { type_name, fields } => {
|
||||
write!(f, "BUILDSTRUCT {type_name} [{}]", fields.join(", "))
|
||||
}
|
||||
Bytecode::SetField(n) => write!(f, "SETFIELD {n}"),
|
||||
Bytecode::Activate { type_name, query } => {
|
||||
write!(f, "ACTIVATE {type_name} \"{query}\"")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "el-integration"
|
||||
description = "Engram language integration tests — full pipeline end-to-end"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "el_integration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
el-lexer = { workspace = true }
|
||||
el-parser = { workspace = true }
|
||||
el-types = { workspace = true }
|
||||
el-compiler = { workspace = true }
|
||||
el-stdlib = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
el-seal = { workspace = true }
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Integration tests for the Engram language compiler pipeline.
|
||||
//!
|
||||
//! Each test module exercises the full pipeline:
|
||||
//! `source → lex → parse → type-check → compile`
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
use el_compiler::{Compiler, CompilerOptions};
|
||||
use el_lexer::tokenize;
|
||||
use el_parser::parse;
|
||||
use el_types::{TypeChecker, TypeEnv};
|
||||
|
||||
/// Build a TypeEnv that includes both the core builtins and the full stdlib.
|
||||
fn stdlib_env() -> TypeEnv {
|
||||
let mut env = TypeEnv::with_builtins();
|
||||
el_stdlib::register_builtins(&mut env);
|
||||
env
|
||||
}
|
||||
|
||||
/// Convenience: run the full pipeline on a source string.
|
||||
/// Returns `Ok(())` if parsing and type-checking succeed with no errors.
|
||||
/// The type environment includes all stdlib builtins.
|
||||
pub fn pipeline_ok(src: &str) -> Result<(), String> {
|
||||
let tokens = tokenize(src).map_err(|e| format!("lex error: {e}"))?;
|
||||
let prog = parse(tokens, src.to_string()).map_err(|e| format!("parse error: {e}"))?;
|
||||
let mut checker = TypeChecker::new(stdlib_env());
|
||||
checker.check(&prog);
|
||||
if checker.ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
let msgs: Vec<_> = checker.diagnostics.iter().map(|d| d.message.clone()).collect();
|
||||
Err(format!("type errors: {}", msgs.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run through lexer + parser only, returning the parsed program.
|
||||
pub fn parse_ok(src: &str) -> Result<el_parser::Program, String> {
|
||||
let tokens = tokenize(src).map_err(|e| format!("lex error: {e}"))?;
|
||||
parse(tokens, src.to_string()).map_err(|e| format!("parse error: {e}"))
|
||||
}
|
||||
|
||||
/// Run the compiler and return the bytecode artifact bytes.
|
||||
pub fn compile_ok(src: &str) -> Result<Vec<u8>, String> {
|
||||
let out = Compiler::compile(src, CompilerOptions::default())
|
||||
.map_err(|e| format!("compile error: {e}"))?;
|
||||
Ok(out.artifact)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
//! Tests that `activate` expressions parse and type-check correctly.
|
||||
//!
|
||||
//! `activate TypeName where "semantic query"` is the Engram graph query primitive.
|
||||
//! It returns `[TypeName]` — an array of matching nodes. The type checker
|
||||
//! requires `TypeName` to be a defined type in the type environment.
|
||||
|
||||
use crate::{parse_ok, pipeline_ok};
|
||||
|
||||
// ── Parse tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_activate_basic() {
|
||||
let src = r#"let results = activate User where "recent users""#;
|
||||
assert!(parse_ok(src).is_ok(), "basic activate expression should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_activate_in_let_binding() {
|
||||
let src = r#"let users = activate User where "all active users""#;
|
||||
assert!(parse_ok(src).is_ok(), "activate in let binding should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_activate_in_fn_body() {
|
||||
let src = r#"
|
||||
fn find_users(query: String) -> [User] {
|
||||
let results = activate User where "active users"
|
||||
return results
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "activate in function body should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_activate_multiple_in_program() {
|
||||
let src = r#"
|
||||
let users = activate User where "all users"
|
||||
let docs = activate Document where "recent documents"
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "multiple activates should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_activate_with_complex_query() {
|
||||
let src = r#"let items = activate Product where "top-selling electronics under $500""#;
|
||||
assert!(parse_ok(src).is_ok(), "activate with complex query string should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_activate_used_in_test_block() {
|
||||
let src = r#"
|
||||
test "activate in test" target: unit {
|
||||
let results = activate User where "test users"
|
||||
assert true
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "activate inside test block should parse");
|
||||
}
|
||||
|
||||
// ── Activate AST structure tests ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_activate_parses_to_correct_ast_node() {
|
||||
use el_parser::{Expr, Stmt};
|
||||
|
||||
let src = r#"let u = activate User where "query""#;
|
||||
let prog = parse_ok(src).unwrap();
|
||||
assert_eq!(prog.stmts.len(), 1);
|
||||
|
||||
if let Stmt::Let { value, .. } = &prog.stmts[0] {
|
||||
assert!(
|
||||
matches!(value, Expr::Activate { type_name, query }
|
||||
if type_name == "User" && query == "query"),
|
||||
"expected Activate node"
|
||||
);
|
||||
} else {
|
||||
panic!("expected Let statement");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activate_type_name_is_preserved() {
|
||||
use el_parser::{Expr, Stmt};
|
||||
|
||||
let src = r#"let n = activate NeuralPattern where "dense clusters""#;
|
||||
let prog = parse_ok(src).unwrap();
|
||||
if let Stmt::Let { value, .. } = &prog.stmts[0] {
|
||||
if let Expr::Activate { type_name, .. } = value {
|
||||
assert_eq!(type_name, "NeuralPattern");
|
||||
} else {
|
||||
panic!("expected Activate expr");
|
||||
}
|
||||
} else {
|
||||
panic!("expected Let stmt");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pipeline tests ────────────────────────────────────────────────────────────
|
||||
// The type checker requires the activated type to be defined in the type env.
|
||||
// These tests define the type before activating it.
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_activate_defined_type_typechecks() {
|
||||
let src = r#"
|
||||
type User {
|
||||
id: Int
|
||||
name: String
|
||||
}
|
||||
let results = activate User where "all users"
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "activate on defined type should pass type checking");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_activate_result_used_in_stdlib_call() {
|
||||
let src = r#"
|
||||
type User {
|
||||
id: Int
|
||||
name: String
|
||||
}
|
||||
let results = activate User where "active users"
|
||||
let count: Int = array_length(results)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "activate result used in stdlib call should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_engram_search_typechecks() {
|
||||
let src = r#"let items = engram_search("neural patterns", 10)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "engram_search should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_engram_node_count_typechecks() {
|
||||
let src = r#"let n: Int = engram_node_count()"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "engram_node_count should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_activate_in_fn_with_defined_type() {
|
||||
let src = r#"
|
||||
type Document {
|
||||
id: Int
|
||||
title: String
|
||||
content: String
|
||||
}
|
||||
fn find_docs(query: String) -> [Document] {
|
||||
let results = activate Document where "recent documents"
|
||||
return results
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "activate in fn with defined type should type-check");
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Full pipeline tests: source → lex → parse → type-check → compile.
|
||||
|
||||
use crate::{compile_ok, parse_ok, pipeline_ok};
|
||||
|
||||
// ── Parse-only smoke tests ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_let_int() {
|
||||
assert!(parse_ok("let x: Int = 42").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_let_string() {
|
||||
assert!(parse_ok(r#"let name: String = "hello""#).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_let_bool() {
|
||||
assert!(parse_ok("let flag: Bool = true").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_fn_def() {
|
||||
let src = r#"
|
||||
fn add(a: Int, b: Int) -> Int {
|
||||
return a + b
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_if_else() {
|
||||
let src = r#"
|
||||
fn abs(x: Int) -> Int {
|
||||
if x < 0 {
|
||||
return 0 - x
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_type_def() {
|
||||
let src = r#"
|
||||
type User {
|
||||
id: Int
|
||||
name: String
|
||||
email: String
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_enum_def() {
|
||||
let src = r#"
|
||||
enum Status {
|
||||
Active
|
||||
Inactive
|
||||
Pending
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_match_expr() {
|
||||
let src = r#"
|
||||
fn describe(s: Status) -> String {
|
||||
match s {
|
||||
Status::Active => "active"
|
||||
Status::Inactive => "inactive"
|
||||
_ => "unknown"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_array_literal() {
|
||||
assert!(parse_ok("let xs: [Int] = [1, 2, 3]").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested_fn_calls() {
|
||||
let src = r#"
|
||||
fn double(x: Int) -> Int {
|
||||
return x * 2
|
||||
}
|
||||
fn quad(x: Int) -> Int {
|
||||
return double(double(x))
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok());
|
||||
}
|
||||
|
||||
// ── Pipeline (lex + parse + type-check) tests ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_hello_world() {
|
||||
assert!(pipeline_ok(r#"let msg: String = "Hello, World!""#).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_arithmetic() {
|
||||
assert!(pipeline_ok("let result: Int = 3 + 4 * 2").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_fn_def_and_call() {
|
||||
let src = r#"
|
||||
fn square(n: Int) -> Int {
|
||||
return n * n
|
||||
}
|
||||
let s: Int = square(5)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_bool_logic() {
|
||||
assert!(pipeline_ok("let ok: Bool = true && false").is_ok());
|
||||
assert!(pipeline_ok("let ok: Bool = true || false").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_float_literal() {
|
||||
assert!(pipeline_ok("let pi: Float = 3.14").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_string_trim_via_call() {
|
||||
// string_trim is registered as a stdlib builtin
|
||||
let src = r#"let s: String = string_trim(" hello ")"#;
|
||||
assert!(pipeline_ok(src).is_ok());
|
||||
}
|
||||
|
||||
// ── Compile tests (full artifact generation) ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_compile_integer_literal() {
|
||||
let artifact = compile_ok("let x: Int = 99").unwrap();
|
||||
assert!(!artifact.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_fn_def() {
|
||||
let src = r#"
|
||||
fn greet(name: String) -> String {
|
||||
return name
|
||||
}
|
||||
"#;
|
||||
let artifact = compile_ok(src).unwrap();
|
||||
assert!(!artifact.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_if_else() {
|
||||
let src = r#"
|
||||
fn max_val(a: Int, b: Int) -> Int {
|
||||
if a > b {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let artifact = compile_ok(src).unwrap();
|
||||
assert!(!artifact.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_produces_valid_json_artifact() {
|
||||
let artifact = compile_ok("let x: Int = 1").unwrap();
|
||||
// Debug target produces JSON-serialized bytecode
|
||||
let parsed: serde_json::Value = serde_json::from_slice(&artifact).unwrap();
|
||||
assert!(parsed.is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_multiple_stmts() {
|
||||
let src = r#"
|
||||
let a: Int = 1
|
||||
let b: Int = 2
|
||||
let c: Int = a + b
|
||||
"#;
|
||||
let artifact = compile_ok(src).unwrap();
|
||||
assert!(!artifact.is_empty());
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Tests that decorator-annotated functions parse and compile correctly.
|
||||
//!
|
||||
//! Decorators are metadata — they do not change compilation semantics in
|
||||
//! the current implementation. The compiler emits identical bytecode whether
|
||||
//! or not a function is decorated.
|
||||
|
||||
use crate::{compile_ok, parse_ok, pipeline_ok};
|
||||
|
||||
// ── Parse tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_authenticate_decorator() {
|
||||
let src = r#"
|
||||
@authenticate
|
||||
fn get_user(id: Int) -> String {
|
||||
return "user"
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "@authenticate decorator should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_public_decorator() {
|
||||
let src = r#"
|
||||
@public
|
||||
fn health_check() -> Bool {
|
||||
return true
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "@public decorator should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cache_decorator_with_args() {
|
||||
let src = r#"
|
||||
@cache(300)
|
||||
fn get_config(key: String) -> String {
|
||||
return key
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "@cache(ttl) decorator should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_decorators() {
|
||||
let src = r#"
|
||||
@authenticate
|
||||
@public
|
||||
fn list_items() -> [String] {
|
||||
return ["a", "b"]
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "multiple decorators should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_with_string_arg() {
|
||||
let src = r#"
|
||||
@route("/api/users")
|
||||
fn list_users() -> [String] {
|
||||
return ["alice", "bob"]
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "@route decorator with string arg should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator_preserves_fn_body() {
|
||||
let src = r#"
|
||||
@authenticate
|
||||
fn add(a: Int, b: Int) -> Int {
|
||||
return a + b
|
||||
}
|
||||
"#;
|
||||
let prog = parse_ok(src).unwrap();
|
||||
// There is exactly one top-level statement (the fn def)
|
||||
assert_eq!(prog.stmts.len(), 1);
|
||||
}
|
||||
|
||||
// ── Pipeline tests ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_decorated_fn_typechecks() {
|
||||
let src = r#"
|
||||
@authenticate
|
||||
fn secure_op(id: Int) -> Bool {
|
||||
return true
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "decorated fn should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_multiple_decorators_typechecks() {
|
||||
let src = r#"
|
||||
@authenticate
|
||||
@cache(60)
|
||||
fn get_profile(id: Int) -> String {
|
||||
return "profile"
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "multiply-decorated fn should type-check");
|
||||
}
|
||||
|
||||
// ── Compile tests ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_compile_decorated_fn_produces_artifact() {
|
||||
let src = r#"
|
||||
@authenticate
|
||||
fn whoami() -> String {
|
||||
return "me"
|
||||
}
|
||||
"#;
|
||||
let artifact = compile_ok(src).unwrap();
|
||||
assert!(!artifact.is_empty(), "decorated fn should produce bytecode artifact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_decorator_does_not_change_bytecode_semantics() {
|
||||
// A decorated function and an identical undecorated function should both
|
||||
// compile without errors and produce non-empty artifacts.
|
||||
let decorated = r#"
|
||||
@public
|
||||
fn value() -> Int {
|
||||
return 42
|
||||
}
|
||||
"#;
|
||||
let plain = r#"
|
||||
fn value() -> Int {
|
||||
return 42
|
||||
}
|
||||
"#;
|
||||
let art_dec = compile_ok(decorated).unwrap();
|
||||
let art_plain = compile_ok(plain).unwrap();
|
||||
// Both should compile to non-empty artifacts
|
||||
assert!(!art_dec.is_empty());
|
||||
assert!(!art_plain.is_empty());
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Tests for Result<T, E> type annotation, the `?` try operator, and closures.
|
||||
|
||||
use crate::{parse_ok, pipeline_ok};
|
||||
|
||||
// ── Result<T, E> type annotation parsing ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_result_return_type() {
|
||||
let src = r#"
|
||||
fn divide(a: Int, b: Int) -> Result<Int, String> {
|
||||
return a
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "Result<T, E> return type should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_result_in_let_binding() {
|
||||
let src = r#"
|
||||
fn parse_int(s: String) -> Result<Int, String> {
|
||||
return 0
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "Result<T, E> in function signature should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_result_with_complex_types() {
|
||||
let src = r#"
|
||||
fn fetch_user(id: Int) -> Result<String, String> {
|
||||
return "user"
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "Result<String, String> should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested_result() {
|
||||
let src = r#"
|
||||
fn complex_op() -> Result<Result<Int, String>, String> {
|
||||
return 0
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "nested Result types should parse");
|
||||
}
|
||||
|
||||
// ── Try operator (`?`) parsing ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_try_operator_on_call() {
|
||||
let src = r#"
|
||||
fn safe_div(a: Int, b: Int) -> Result<Int, String> {
|
||||
return a
|
||||
}
|
||||
fn compute() -> Result<Int, String> {
|
||||
let x: Int = safe_div(10, 2)?
|
||||
return x
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "? operator on function call should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_try_operator_on_variable() {
|
||||
let src = r#"
|
||||
fn process(result: Result<Int, String>) -> Result<Int, String> {
|
||||
let value: Int = result?
|
||||
return value
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "? operator on variable should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_chained_try_operators() {
|
||||
let src = r#"
|
||||
fn step1() -> Result<Int, String> { return 1 }
|
||||
fn step2(n: Int) -> Result<String, String> { return "ok" }
|
||||
fn pipeline() -> Result<String, String> {
|
||||
let n: Int = step1()?
|
||||
let s: String = step2(n)?
|
||||
return s
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "chained ? operators should parse");
|
||||
}
|
||||
|
||||
// ── Optional type (`T?`) parsing ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_optional_return_type() {
|
||||
let src = r#"
|
||||
fn find(id: Int) -> String? {
|
||||
return "user"
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "Optional return type T? should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_optional_parameter() {
|
||||
let src = r#"
|
||||
fn greet(name: String?) -> String {
|
||||
return "hello"
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "Optional parameter type T? should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_optional_in_let_binding() {
|
||||
let src = r#"
|
||||
fn maybe_val() -> Int? {
|
||||
return 42
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "Optional in let binding should parse");
|
||||
}
|
||||
|
||||
// ── Closure parsing ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_closure() {
|
||||
let src = r#"let double = |x: Int| x * 2"#;
|
||||
assert!(parse_ok(src).is_ok(), "simple closure should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_closure_with_return_type() {
|
||||
let src = r#"let double = |x: Int| -> Int { return x * 2 }"#;
|
||||
assert!(parse_ok(src).is_ok(), "closure with explicit return type should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_closure_with_multiple_params() {
|
||||
let src = r#"let add = |a: Int, b: Int| a + b"#;
|
||||
assert!(parse_ok(src).is_ok(), "multi-param closure should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_closure_single_param_no_body_type() {
|
||||
// Closure with one param and inferred return type
|
||||
let src = r#"let inc = |n: Int| n + 1"#;
|
||||
assert!(parse_ok(src).is_ok(), "single-param closure with inferred return should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_closure_with_block_body() {
|
||||
let src = r#"
|
||||
let compute = |x: Int| -> Int {
|
||||
let y: Int = x * 2
|
||||
return y + 1
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "closure with block body should parse");
|
||||
}
|
||||
|
||||
// ── Pipeline tests ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_result_return_type_typechecks() {
|
||||
let src = r#"
|
||||
fn safe_op(x: Int) -> Result<Int, String> {
|
||||
return x
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "Result<T,E> return type should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_optional_return_type_typechecks() {
|
||||
let src = r#"
|
||||
fn maybe(x: Int) -> Int? {
|
||||
return x
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "Optional return type should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_closure_typechecks() {
|
||||
let src = r#"let inc = |n: Int| n + 1"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "closure expression should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_try_operator_typechecks() {
|
||||
let src = r#"
|
||||
fn maybe_int() -> Result<Int, String> {
|
||||
return 1
|
||||
}
|
||||
fn compute() -> Result<Int, String> {
|
||||
let x: Int = maybe_int()?
|
||||
return x
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "? operator should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_result_stdlib_unwrap_or() {
|
||||
let src = r#"
|
||||
fn safe_op(x: Int) -> Result<Int, String> {
|
||||
return x
|
||||
}
|
||||
let val: Int = result_unwrap_or(safe_op(5), 0)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "result_unwrap_or should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_optional_stdlib_is_some() {
|
||||
let src = r#"
|
||||
fn maybe(x: Int) -> Int? {
|
||||
return x
|
||||
}
|
||||
let val: Bool = optional_is_some(maybe(3))
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "optional_is_some should type-check");
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Integration test modules — full pipeline end-to-end.
|
||||
|
||||
mod compiler_pipeline;
|
||||
mod stdlib_usage;
|
||||
mod protocol_conformance;
|
||||
mod decorator_codegen;
|
||||
mod test_framework;
|
||||
mod activate_typing;
|
||||
mod error_propagation;
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Tests that verify protocol definitions and impl blocks parse correctly
|
||||
//! and pass through the type-checking pipeline.
|
||||
|
||||
use crate::{parse_ok, pipeline_ok};
|
||||
|
||||
// ── Protocol definition parsing ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_protocol_definition() {
|
||||
let src = r#"
|
||||
protocol Serializable {
|
||||
fn serialize(self: Serializable) -> String
|
||||
fn deserialize(data: String) -> Serializable
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "protocol definition should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_protocol_with_multiple_methods() {
|
||||
let src = r#"
|
||||
protocol Comparable {
|
||||
fn compare(a: Comparable, b: Comparable) -> Int
|
||||
fn equals(a: Comparable, b: Comparable) -> Bool
|
||||
fn less_than(a: Comparable, b: Comparable) -> Bool
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "multi-method protocol should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_impl_for_type() {
|
||||
let src = r#"
|
||||
protocol Printable {
|
||||
fn print(self: Printable) -> String
|
||||
}
|
||||
type Point {
|
||||
x: Float
|
||||
y: Float
|
||||
}
|
||||
impl Printable for Point {
|
||||
fn print(self: Point) -> String {
|
||||
return "point"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "impl block should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_impl_with_multiple_methods() {
|
||||
let src = r#"
|
||||
protocol Codec {
|
||||
fn encode(data: String) -> String
|
||||
fn decode(data: String) -> String
|
||||
}
|
||||
type Base64Codec {
|
||||
padding: Bool
|
||||
}
|
||||
impl Codec for Base64Codec {
|
||||
fn encode(data: String) -> String {
|
||||
return data
|
||||
}
|
||||
fn decode(data: String) -> String {
|
||||
return data
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "impl with multiple methods should parse");
|
||||
}
|
||||
|
||||
// ── Protocol pipeline tests ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_protocol_definition_ok() {
|
||||
let src = r#"
|
||||
protocol Runnable {
|
||||
fn run(self: Runnable) -> Int
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "protocol definition should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_impl_for_builtin_type() {
|
||||
let src = r#"
|
||||
protocol Describable {
|
||||
fn describe(self: Describable) -> String
|
||||
}
|
||||
type Tag {
|
||||
label: String
|
||||
value: Int
|
||||
}
|
||||
impl Describable for Tag {
|
||||
fn describe(self: Tag) -> String {
|
||||
return self.label
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "impl block should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_multiple_impls_for_same_protocol() {
|
||||
let src = r#"
|
||||
protocol Shape {
|
||||
fn area(self: Shape) -> Float
|
||||
}
|
||||
type Circle {
|
||||
radius: Float
|
||||
}
|
||||
type Square {
|
||||
side: Float
|
||||
}
|
||||
impl Shape for Circle {
|
||||
fn area(self: Circle) -> Float {
|
||||
return self.radius * self.radius
|
||||
}
|
||||
}
|
||||
impl Shape for Square {
|
||||
fn area(self: Square) -> Float {
|
||||
return self.side * self.side
|
||||
}
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "multiple impls for same protocol should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_protocol_with_result_return() {
|
||||
let src = r#"
|
||||
protocol Validatable {
|
||||
fn validate(self: Validatable) -> Result<Bool, String>
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "protocol with Result return type should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_protocol_with_optional_return() {
|
||||
let src = r#"
|
||||
protocol Repository {
|
||||
fn find_by_id(id: Int) -> String?
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "protocol with optional return type should type-check");
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Integration tests for programs that use stdlib functions.
|
||||
//!
|
||||
//! The stdlib functions are registered as builtins, so el programs can call
|
||||
//! them without an import statement.
|
||||
|
||||
use crate::pipeline_ok;
|
||||
|
||||
// ── Array stdlib ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_array_length_call_typechecks() {
|
||||
let src = r#"
|
||||
let xs: [Int] = [1, 2, 3]
|
||||
let n: Int = array_length(xs)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "array_length should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_push_call_typechecks() {
|
||||
let src = r#"
|
||||
let xs: [Int] = [1, 2, 3]
|
||||
let ys: [Int] = array_push(xs, 4)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "array_push should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_pop_call_typechecks() {
|
||||
// array_pop returns T? (Optional), not [T]
|
||||
let src = r#"
|
||||
let xs: [Int] = [1, 2, 3]
|
||||
let head = array_pop(xs)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "array_pop should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_reverse_call_typechecks() {
|
||||
let src = r#"
|
||||
let xs: [Int] = [3, 2, 1]
|
||||
let ys: [Int] = array_reverse(xs)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "array_reverse should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_contains_returns_bool() {
|
||||
// array_contains takes [String] and String
|
||||
let src = r#"
|
||||
let xs: [String] = ["a", "b", "c"]
|
||||
let found: Bool = array_contains(xs, "b")
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "array_contains should be in scope");
|
||||
}
|
||||
|
||||
// ── String stdlib ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_string_len_call_typechecks() {
|
||||
let src = r#"let n: Int = string_len("hello")"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "string_len should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_trim_call_typechecks() {
|
||||
let src = r#"let s: String = string_trim(" hi ")"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "string_trim should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_to_upper_call_typechecks() {
|
||||
let src = r#"let s: String = string_to_upper("hello")"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "string_to_upper should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_to_lower_call_typechecks() {
|
||||
let src = r#"let s: String = string_to_lower("HELLO")"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "string_to_lower should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_contains_returns_bool() {
|
||||
let src = r#"let ok: Bool = string_contains("hello world", "world")"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "string_contains should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_concat_call_typechecks() {
|
||||
let src = r#"let s: String = string_concat("hello", " world")"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "string_concat should be in scope");
|
||||
}
|
||||
|
||||
// ── Math stdlib ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_math_abs_call_typechecks() {
|
||||
// math_abs takes Float -> Float
|
||||
let src = r#"let n: Float = math_abs(0.0 - 5.0)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "math_abs should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_max_call_typechecks() {
|
||||
// math_max takes (Float, Float) -> Float
|
||||
let src = r#"let n: Float = math_max(3.0, 7.0)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "math_max should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_min_call_typechecks() {
|
||||
// math_min takes (Float, Float) -> Float
|
||||
let src = r#"let n: Float = math_min(3.0, 7.0)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "math_min should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_pow_call_typechecks() {
|
||||
let src = r#"let n: Float = math_pow(2.0, 10.0)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "math_pow should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_abs_int_call_typechecks() {
|
||||
// math_abs_int takes Int -> Int (integer variant)
|
||||
let src = r#"let n: Int = math_abs_int(0 - 5)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "math_abs_int should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_max_int_call_typechecks() {
|
||||
let src = r#"let n: Int = math_max_int(3, 7)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "math_max_int should be in scope");
|
||||
}
|
||||
|
||||
// ── Map stdlib ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_map_new_call_typechecks() {
|
||||
let src = r#"let m: Map<String, Int> = map_new()"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "map_new should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_size_call_typechecks() {
|
||||
let src = r#"
|
||||
let m: Map<String, Int> = map_new()
|
||||
let n: Int = map_size(m)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "map_size should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_is_empty_call_typechecks() {
|
||||
let src = r#"
|
||||
let m: Map<String, Int> = map_new()
|
||||
let empty: Bool = map_is_empty(m)
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "map_is_empty should be in scope");
|
||||
}
|
||||
|
||||
// ── Engram graph stdlib ───────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_engram_node_count_call_typechecks() {
|
||||
let src = r#"let n: Int = engram_node_count()"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "engram_node_count should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_search_call_typechecks() {
|
||||
let src = r#"let results = engram_search("neural patterns", 10)"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "engram_search should be in scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_edge_between_returns_bool() {
|
||||
// engram_edge_between takes (Uuid, Uuid) -> Bool
|
||||
// Uuid literals are just strings assigned to Uuid type
|
||||
let src = r#"
|
||||
fn check_edge(a: Uuid, b: Uuid) -> Bool {
|
||||
return engram_edge_between(a, b)
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "engram_edge_between should be in scope");
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Tests that el `test { ... }` blocks parse and type-check correctly.
|
||||
|
||||
use crate::{parse_ok, pipeline_ok};
|
||||
|
||||
// ── Parse tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_test_block() {
|
||||
let src = r#"
|
||||
test "addition works" {
|
||||
assert 1 + 1 == 2
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "simple test block should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_test_with_unit_target() {
|
||||
let src = r#"
|
||||
test "unit test" target: unit {
|
||||
assert true
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test with unit target should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_test_with_e2e_target() {
|
||||
let src = r#"
|
||||
test "e2e test" target: e2e {
|
||||
assert true
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test with e2e target should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_test_with_both_target() {
|
||||
let src = r#"
|
||||
test "both targets" target: both {
|
||||
assert true
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test with both target should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_test_with_let_binding() {
|
||||
let src = r#"
|
||||
test "arithmetic" {
|
||||
let x: Int = 3 + 4
|
||||
assert x == 7
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test with let binding should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_test_with_fn_call() {
|
||||
let src = r#"
|
||||
fn double(n: Int) -> Int {
|
||||
return n * 2
|
||||
}
|
||||
test "double function" {
|
||||
let result: Int = double(5)
|
||||
assert result == 10
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test calling fn should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_asserts() {
|
||||
let src = r#"
|
||||
test "multiple assertions" {
|
||||
assert 1 < 2
|
||||
assert 2 < 3
|
||||
assert 3 > 0
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test with multiple asserts should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_test_with_seed_node() {
|
||||
let src = r#"
|
||||
test "with seed data" target: unit {
|
||||
seed Node { node_type: "User", content: "Alice", importance: 0.9 }
|
||||
assert true
|
||||
}
|
||||
"#;
|
||||
assert!(parse_ok(src).is_ok(), "test with seed node should parse");
|
||||
}
|
||||
|
||||
// ── Pipeline tests ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_test_block_typechecks() {
|
||||
let src = r#"
|
||||
test "type-checks ok" {
|
||||
let x: Int = 42
|
||||
assert x > 0
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "test block should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_test_with_string_typechecks() {
|
||||
let src = r#"
|
||||
test "string test" {
|
||||
let s: String = "hello"
|
||||
let n: Int = string_len(s)
|
||||
assert n > 0
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "test using stdlib should type-check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_multiple_test_blocks() {
|
||||
let src = r#"
|
||||
test "first" {
|
||||
assert true
|
||||
}
|
||||
test "second" {
|
||||
assert 1 == 1
|
||||
}
|
||||
"#;
|
||||
assert!(pipeline_ok(src).is_ok(), "multiple test blocks should type-check");
|
||||
}
|
||||
@@ -286,7 +286,7 @@ impl<'src> Lexer<'src> {
|
||||
if c.is_ascii_digit() || c == '_' {
|
||||
raw.push(c);
|
||||
self.advance();
|
||||
} else if c == '.' && self.peek2().map_or(false, |d| d.is_ascii_digit()) {
|
||||
} else if c == '.' && self.peek2().is_some_and(|d| d.is_ascii_digit()) {
|
||||
is_float = true;
|
||||
raw.push(c);
|
||||
self.advance();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
//! version = "0.1.0"
|
||||
//! edition = "2026"
|
||||
//! "#;
|
||||
//! let manifest = Manifest::from_str(toml).unwrap();
|
||||
//! let manifest = Manifest::parse(toml).unwrap();
|
||||
//! assert_eq!(manifest.package.name, "my-service");
|
||||
//! ```
|
||||
|
||||
|
||||
@@ -342,14 +342,14 @@ pub struct Manifest {
|
||||
|
||||
impl Manifest {
|
||||
/// Parse a manifest from a TOML string.
|
||||
pub fn from_str(s: &str) -> crate::ManifestResult<Self> {
|
||||
pub fn parse(s: &str) -> crate::ManifestResult<Self> {
|
||||
crate::parse::parse_manifest(s)
|
||||
}
|
||||
|
||||
/// Parse a manifest from a file on disk.
|
||||
pub fn from_file(path: &std::path::Path) -> crate::ManifestResult<Self> {
|
||||
let text = std::fs::read_to_string(path).map_err(crate::ManifestError::Io)?;
|
||||
Self::from_str(&text)
|
||||
Self::parse(&text)
|
||||
}
|
||||
|
||||
/// Walk up the directory tree from `from` until an `el.toml` is found.
|
||||
|
||||
@@ -911,7 +911,7 @@ impl Parser {
|
||||
match self.peek() {
|
||||
Token::StringLiteral(_) => {
|
||||
// Check if next is Colon
|
||||
self.tokens.get(self.pos + 1).map_or(false, |t| matches!(t.node, Token::Colon))
|
||||
self.tokens.get(self.pos + 1).is_some_and(|t| matches!(t.node, Token::Colon))
|
||||
}
|
||||
Token::RBrace => false, // empty block `{}`
|
||||
_ => false,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "el-stdlib"
|
||||
description = "Engram language standard library — built-in function signatures and implementations"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
el-types = { workspace = true }
|
||||
el-parser = { workspace = true }
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Array operations: map, filter, reduce, find, any, all, length, push, pop,
|
||||
//! sort, reverse, zip, enumerate.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
let arr_int = Type::Array(Box::new(Type::Int));
|
||||
let arr_str = Type::Array(Box::new(Type::String));
|
||||
let arr_unk = Type::Array(Box::new(Type::Unknown));
|
||||
|
||||
// array_length([T]) -> Int
|
||||
env.register_fn("array_length", fn_type(vec![arr_unk.clone()], Type::Int));
|
||||
// array_push([T], T) -> [T]
|
||||
env.register_fn("array_push", fn_type(vec![arr_unk.clone(), Type::Unknown], arr_unk.clone()));
|
||||
// array_pop([T]) -> T?
|
||||
env.register_fn("array_pop", fn_type(vec![arr_unk.clone()], Type::Optional(Box::new(Type::Unknown))));
|
||||
// array_map([T], fn(T) -> U) -> [U]
|
||||
let mapper = Type::Fn { params: vec![Type::Unknown], return_type: Box::new(Type::Unknown) };
|
||||
env.register_fn("array_map", fn_type(vec![arr_unk.clone(), mapper.clone()], arr_unk.clone()));
|
||||
// array_filter([T], fn(T) -> Bool) -> [T]
|
||||
let predicate = Type::Fn { params: vec![Type::Unknown], return_type: Box::new(Type::Bool) };
|
||||
env.register_fn("array_filter", fn_type(vec![arr_unk.clone(), predicate.clone()], arr_unk.clone()));
|
||||
// array_reduce([T], U, fn(U, T) -> U) -> U
|
||||
let reducer = Type::Fn { params: vec![Type::Unknown, Type::Unknown], return_type: Box::new(Type::Unknown) };
|
||||
env.register_fn("array_reduce", fn_type(vec![arr_unk.clone(), Type::Unknown, reducer], Type::Unknown));
|
||||
// array_find([T], fn(T) -> Bool) -> T?
|
||||
env.register_fn("array_find", fn_type(vec![arr_unk.clone(), predicate.clone()], Type::Optional(Box::new(Type::Unknown))));
|
||||
// array_any([T], fn(T) -> Bool) -> Bool
|
||||
env.register_fn("array_any", fn_type(vec![arr_unk.clone(), predicate.clone()], Type::Bool));
|
||||
// array_all([T], fn(T) -> Bool) -> Bool
|
||||
env.register_fn("array_all", fn_type(vec![arr_unk.clone(), predicate], Type::Bool));
|
||||
// array_sort([Int]) -> [Int]
|
||||
env.register_fn("array_sort", fn_type(vec![arr_int.clone()], arr_int.clone()));
|
||||
// array_reverse([T]) -> [T]
|
||||
env.register_fn("array_reverse", fn_type(vec![arr_unk.clone()], arr_unk.clone()));
|
||||
// array_zip([T], [U]) -> [[T]] (simplified: returns array of unknown)
|
||||
env.register_fn("array_zip", fn_type(vec![arr_unk.clone(), arr_unk.clone()], arr_unk.clone()));
|
||||
// array_enumerate([T]) -> [[T]] (returns pairs as arrays)
|
||||
env.register_fn("array_enumerate", fn_type(vec![arr_unk.clone()], arr_unk.clone()));
|
||||
// array_join([String], String) -> String
|
||||
env.register_fn("array_join", fn_type(vec![arr_str.clone(), Type::String], Type::String));
|
||||
// array_concat([T], [T]) -> [T]
|
||||
env.register_fn("array_concat", fn_type(vec![arr_unk.clone(), arr_unk.clone()], arr_unk.clone()));
|
||||
// array_slice([T], Int, Int) -> [T]
|
||||
env.register_fn("array_slice", fn_type(vec![arr_unk.clone(), Type::Int, Type::Int], arr_unk));
|
||||
// array_first([T]) -> T?
|
||||
env.register_fn("array_first", fn_type(vec![arr_int.clone()], Type::Optional(Box::new(Type::Int))));
|
||||
// array_last([T]) -> T?
|
||||
env.register_fn("array_last", fn_type(vec![arr_int.clone()], Type::Optional(Box::new(Type::Int))));
|
||||
// array_contains([String], String) -> Bool
|
||||
env.register_fn("array_contains", fn_type(vec![arr_str, Type::String], Type::Bool));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_length_registered() {
|
||||
assert!(env().lookup_fn("array_length").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_map_is_fn_type() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("array_map").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_filter_registered() {
|
||||
assert!(env().lookup_fn("array_filter").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_push_registered() {
|
||||
assert!(env().lookup_fn("array_push").is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Engram graph operations: activate, relate, forget, edge_between, neighbors.
|
||||
//!
|
||||
//! These are thin wrappers over the Engram HTTP API. They are registered as
|
||||
//! built-in functions in the type environment so el programs can call them
|
||||
//! directly without an import.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
let arr_unk = Type::Array(Box::new(Type::Unknown));
|
||||
|
||||
// engram_activate(type_name: String, query: String) -> [T]
|
||||
env.register_fn("engram_activate", fn_type(vec![Type::String, Type::String], arr_unk.clone()));
|
||||
|
||||
// engram_relate(from_id: Uuid, to_id: Uuid, relation: String, weight: Float) -> Void
|
||||
env.register_fn("engram_relate", fn_type(
|
||||
vec![Type::Uuid, Type::Uuid, Type::String, Type::Float],
|
||||
Type::Void,
|
||||
));
|
||||
|
||||
// engram_forget(node_id: Uuid) -> Void
|
||||
env.register_fn("engram_forget", fn_type(vec![Type::Uuid], Type::Void));
|
||||
|
||||
// engram_edge_between(from_id: Uuid, to_id: Uuid) -> Bool
|
||||
env.register_fn("engram_edge_between", fn_type(vec![Type::Uuid, Type::Uuid], Type::Bool));
|
||||
|
||||
// engram_neighbors(node_id: Uuid) -> [T]
|
||||
env.register_fn("engram_neighbors", fn_type(vec![Type::Uuid], arr_unk.clone()));
|
||||
|
||||
// engram_node_count() -> Int
|
||||
env.register_fn("engram_node_count", fn_type(vec![], Type::Int));
|
||||
|
||||
// engram_search(query: String, limit: Int) -> [T]
|
||||
env.register_fn("engram_search", fn_type(vec![Type::String, Type::Int], arr_unk));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_activate_registered() {
|
||||
assert!(env().lookup_fn("engram_activate").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_relate_registered() {
|
||||
assert!(env().lookup_fn("engram_relate").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_neighbors_registered() {
|
||||
assert!(env().lookup_fn("engram_neighbors").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_forget_returns_void() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("engram_forget").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Void)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Engram language standard library.
|
||||
//!
|
||||
//! This crate defines function signatures for the built-in standard library
|
||||
//! modules. Each module registers its functions into a [`TypeEnv`] so the
|
||||
//! type checker can resolve calls to stdlib functions without an explicit
|
||||
//! import.
|
||||
//!
|
||||
//! # Auto-imported modules
|
||||
//! - `std::array` — array operations
|
||||
//! - `std::string` — string operations
|
||||
//! - `std::result` — Result<T, E> operations
|
||||
//! - `std::optional`— T? operations
|
||||
//! - `std::math` — numeric operations
|
||||
//! - `std::map` — Map<K, V> operations
|
||||
//! - `std::engram` — Engram graph operations
|
||||
|
||||
pub mod array;
|
||||
pub mod engram;
|
||||
pub mod map;
|
||||
pub mod math;
|
||||
pub mod optional;
|
||||
pub mod result;
|
||||
pub mod string;
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
|
||||
/// Register all automatically-imported stdlib modules into the given environment.
|
||||
///
|
||||
/// Call this from `TypeEnv::with_builtins()` or at the start of type checking.
|
||||
pub fn register_builtins(env: &mut TypeEnv) {
|
||||
array::register(env);
|
||||
string::register(env);
|
||||
result::register(env);
|
||||
optional::register(env);
|
||||
math::register(env);
|
||||
map::register(env);
|
||||
engram::register(env);
|
||||
}
|
||||
|
||||
/// Helper: build a simple function type.
|
||||
pub(crate) fn fn_type(params: Vec<Type>, ret: Type) -> Type {
|
||||
Type::Fn { params, return_type: Box::new(ret) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use el_types::TypeEnv;
|
||||
|
||||
fn stdlib_env() -> TypeEnv {
|
||||
let mut env = TypeEnv::with_builtins();
|
||||
register_builtins(&mut env);
|
||||
env
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("array_map").is_some(), "array_map should be registered");
|
||||
assert!(env.lookup_fn("array_filter").is_some());
|
||||
assert!(env.lookup_fn("array_length").is_some());
|
||||
assert!(env.lookup_fn("array_push").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("string_len").is_some());
|
||||
assert!(env.lookup_fn("string_trim").is_some());
|
||||
assert!(env.lookup_fn("string_split").is_some());
|
||||
assert!(env.lookup_fn("string_contains").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("math_abs").is_some());
|
||||
assert!(env.lookup_fn("math_max").is_some());
|
||||
assert!(env.lookup_fn("math_min").is_some());
|
||||
assert!(env.lookup_fn("math_sqrt").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("result_unwrap_or").is_some());
|
||||
assert!(env.lookup_fn("result_ok").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("optional_unwrap_or").is_some());
|
||||
assert!(env.lookup_fn("optional_is_some").is_some());
|
||||
assert!(env.lookup_fn("optional_is_none").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("map_get").is_some());
|
||||
assert!(env.lookup_fn("map_set").is_some());
|
||||
assert!(env.lookup_fn("map_remove").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_engram_functions_registered() {
|
||||
let env = stdlib_env();
|
||||
assert!(env.lookup_fn("engram_activate").is_some());
|
||||
assert!(env.lookup_fn("engram_relate").is_some());
|
||||
assert!(env.lookup_fn("engram_neighbors").is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Map<K, V> operations: get, set, remove, contains_key, keys, values, entries, merge.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
let map_unk = Type::Map { key: Box::new(Type::Unknown), value: Box::new(Type::Unknown) };
|
||||
let arr_unk = Type::Array(Box::new(Type::Unknown));
|
||||
|
||||
env.register_fn("map_get", fn_type(vec![map_unk.clone(), Type::Unknown], Type::Optional(Box::new(Type::Unknown))));
|
||||
env.register_fn("map_set", fn_type(vec![map_unk.clone(), Type::Unknown, Type::Unknown], map_unk.clone()));
|
||||
env.register_fn("map_remove", fn_type(vec![map_unk.clone(), Type::Unknown], map_unk.clone()));
|
||||
env.register_fn("map_contains_key", fn_type(vec![map_unk.clone(), Type::Unknown], Type::Bool));
|
||||
env.register_fn("map_keys", fn_type(vec![map_unk.clone()], arr_unk.clone()));
|
||||
env.register_fn("map_values", fn_type(vec![map_unk.clone()], arr_unk.clone()));
|
||||
env.register_fn("map_entries", fn_type(vec![map_unk.clone()], arr_unk.clone()));
|
||||
env.register_fn("map_merge", fn_type(vec![map_unk.clone(), map_unk.clone()], map_unk.clone()));
|
||||
env.register_fn("map_size", fn_type(vec![map_unk.clone()], Type::Int));
|
||||
env.register_fn("map_is_empty", fn_type(vec![map_unk.clone()], Type::Bool));
|
||||
env.register_fn("map_new", fn_type(vec![], map_unk));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_get_registered() {
|
||||
assert!(env().lookup_fn("map_get").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_get_returns_optional() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("map_get").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Optional(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_contains_key_returns_bool() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("map_contains_key").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Bool)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_merge_registered() {
|
||||
assert!(env().lookup_fn("map_merge").is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Math operations: abs, max, min, floor, ceil, pow, sqrt, clamp.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
env.register_fn("math_abs", fn_type(vec![Type::Float], Type::Float));
|
||||
env.register_fn("math_max", fn_type(vec![Type::Float, Type::Float], Type::Float));
|
||||
env.register_fn("math_min", fn_type(vec![Type::Float, Type::Float], Type::Float));
|
||||
env.register_fn("math_floor", fn_type(vec![Type::Float], Type::Int));
|
||||
env.register_fn("math_ceil", fn_type(vec![Type::Float], Type::Int));
|
||||
env.register_fn("math_round", fn_type(vec![Type::Float], Type::Int));
|
||||
env.register_fn("math_pow", fn_type(vec![Type::Float, Type::Float], Type::Float));
|
||||
env.register_fn("math_sqrt", fn_type(vec![Type::Float], Type::Float));
|
||||
env.register_fn("math_clamp", fn_type(vec![Type::Float, Type::Float, Type::Float], Type::Float));
|
||||
env.register_fn("math_abs_int", fn_type(vec![Type::Int], Type::Int));
|
||||
env.register_fn("math_max_int", fn_type(vec![Type::Int, Type::Int], Type::Int));
|
||||
env.register_fn("math_min_int", fn_type(vec![Type::Int, Type::Int], Type::Int));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_abs_registered() {
|
||||
assert!(env().lookup_fn("math_abs").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_sqrt_returns_float() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("math_sqrt").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Float)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_floor_returns_int() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("math_floor").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Int)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! T? operations: unwrap_or, map, flat_map, is_some, is_none.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
let opt_unk = Type::Optional(Box::new(Type::Unknown));
|
||||
let mapper = Type::Fn { params: vec![Type::Unknown], return_type: Box::new(Type::Unknown) };
|
||||
|
||||
env.register_fn("optional_unwrap_or", fn_type(vec![opt_unk.clone(), Type::Unknown], Type::Unknown));
|
||||
env.register_fn("optional_unwrap_or_else", fn_type(
|
||||
vec![opt_unk.clone(), Type::Fn { params: vec![], return_type: Box::new(Type::Unknown) }],
|
||||
Type::Unknown,
|
||||
));
|
||||
env.register_fn("optional_map", fn_type(vec![opt_unk.clone(), mapper.clone()], opt_unk.clone()));
|
||||
env.register_fn("optional_flat_map", fn_type(vec![opt_unk.clone(), mapper.clone()], opt_unk.clone()));
|
||||
env.register_fn("optional_is_some", fn_type(vec![opt_unk.clone()], Type::Bool));
|
||||
env.register_fn("optional_is_none", fn_type(vec![opt_unk.clone()], Type::Bool));
|
||||
env.register_fn("optional_filter", fn_type(
|
||||
vec![opt_unk.clone(), Type::Fn { params: vec![Type::Unknown], return_type: Box::new(Type::Bool) }],
|
||||
opt_unk.clone(),
|
||||
));
|
||||
// some(T) -> T?
|
||||
env.register_fn("some", fn_type(vec![Type::Unknown], opt_unk));
|
||||
// none() -> T?
|
||||
env.register_fn("none", fn_type(vec![], Type::Optional(Box::new(Type::Unknown))));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_is_some_registered() {
|
||||
assert!(env().lookup_fn("optional_is_some").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_is_none_registered() {
|
||||
assert!(env().lookup_fn("optional_is_none").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_unwrap_or_registered() {
|
||||
assert!(env().lookup_fn("optional_unwrap_or").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_some_and_none_registered() {
|
||||
let e = env();
|
||||
assert!(e.lookup_fn("some").is_some());
|
||||
assert!(e.lookup_fn("none").is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Result<T, E> operations: map, map_err, unwrap_or, unwrap_or_else, and_then, ok.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
// result_unwrap_or(Result<T, E>, T) -> T
|
||||
let result_unk = Type::Result {
|
||||
ok: Box::new(Type::Unknown),
|
||||
err: Box::new(Type::Unknown),
|
||||
};
|
||||
env.register_fn("result_unwrap_or", fn_type(vec![result_unk.clone(), Type::Unknown], Type::Unknown));
|
||||
env.register_fn("result_unwrap_or_else", fn_type(
|
||||
vec![result_unk.clone(), Type::Fn { params: vec![Type::Unknown], return_type: Box::new(Type::Unknown) }],
|
||||
Type::Unknown,
|
||||
));
|
||||
// result_ok(Result<T, E>) -> T?
|
||||
env.register_fn("result_ok", fn_type(vec![result_unk.clone()], Type::Optional(Box::new(Type::Unknown))));
|
||||
// result_err(Result<T, E>) -> E?
|
||||
env.register_fn("result_err", fn_type(vec![result_unk.clone()], Type::Optional(Box::new(Type::Unknown))));
|
||||
// result_is_ok(Result<T, E>) -> Bool
|
||||
env.register_fn("result_is_ok", fn_type(vec![result_unk.clone()], Type::Bool));
|
||||
// result_is_err(Result<T, E>) -> Bool
|
||||
env.register_fn("result_is_err", fn_type(vec![result_unk.clone()], Type::Bool));
|
||||
// result_map(Result<T, E>, fn(T) -> U) -> Result<U, E>
|
||||
let mapper = Type::Fn { params: vec![Type::Unknown], return_type: Box::new(Type::Unknown) };
|
||||
env.register_fn("result_map", fn_type(vec![result_unk.clone(), mapper.clone()], result_unk.clone()));
|
||||
// result_and_then(Result<T, E>, fn(T) -> Result<U, E>) -> Result<U, E>
|
||||
let chain_fn = Type::Fn { params: vec![Type::Unknown], return_type: Box::new(result_unk.clone()) };
|
||||
env.register_fn("result_and_then", fn_type(vec![result_unk.clone(), chain_fn], result_unk.clone()));
|
||||
// result_ok_val(T) -> Result<T, E> — wrap a value in Ok
|
||||
env.register_fn("ok", fn_type(vec![Type::Unknown], result_unk.clone()));
|
||||
// result_err_val(E) -> Result<T, E> — wrap an error in Err
|
||||
env.register_fn("err", fn_type(vec![Type::Unknown], result_unk));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_unwrap_or_registered() {
|
||||
assert!(env().lookup_fn("result_unwrap_or").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_ok_returns_optional() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("result_ok").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Optional(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ok_and_err_registered() {
|
||||
let e = env();
|
||||
assert!(e.lookup_fn("ok").is_some());
|
||||
assert!(e.lookup_fn("err").is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! String operations: trim, split, join, contains, starts_with, ends_with,
|
||||
//! to_upper, to_lower, replace, len, chars.
|
||||
|
||||
use el_types::{Type, TypeEnv};
|
||||
use super::fn_type;
|
||||
|
||||
pub fn register(env: &mut TypeEnv) {
|
||||
let arr_str = Type::Array(Box::new(Type::String));
|
||||
|
||||
env.register_fn("string_len", fn_type(vec![Type::String], Type::Int));
|
||||
env.register_fn("string_trim", fn_type(vec![Type::String], Type::String));
|
||||
env.register_fn("string_split", fn_type(vec![Type::String, Type::String], arr_str.clone()));
|
||||
env.register_fn("string_join", fn_type(vec![arr_str.clone(), Type::String], Type::String));
|
||||
env.register_fn("string_contains", fn_type(vec![Type::String, Type::String], Type::Bool));
|
||||
env.register_fn("string_starts_with", fn_type(vec![Type::String, Type::String], Type::Bool));
|
||||
env.register_fn("string_ends_with", fn_type(vec![Type::String, Type::String], Type::Bool));
|
||||
env.register_fn("string_to_upper", fn_type(vec![Type::String], Type::String));
|
||||
env.register_fn("string_to_lower", fn_type(vec![Type::String], Type::String));
|
||||
env.register_fn("string_replace", fn_type(vec![Type::String, Type::String, Type::String], Type::String));
|
||||
env.register_fn("string_chars", fn_type(vec![Type::String], arr_str.clone()));
|
||||
env.register_fn("string_slice", fn_type(vec![Type::String, Type::Int, Type::Int], Type::String));
|
||||
env.register_fn("string_repeat", fn_type(vec![Type::String, Type::Int], Type::String));
|
||||
env.register_fn("string_reverse", fn_type(vec![Type::String], Type::String));
|
||||
env.register_fn("string_parse_int", fn_type(vec![Type::String], Type::Optional(Box::new(Type::Int))));
|
||||
env.register_fn("string_parse_float", fn_type(vec![Type::String], Type::Optional(Box::new(Type::Float))));
|
||||
env.register_fn("string_from_int", fn_type(vec![Type::Int], Type::String));
|
||||
env.register_fn("string_from_float", fn_type(vec![Type::Float], Type::String));
|
||||
env.register_fn("string_is_empty", fn_type(vec![Type::String], Type::Bool));
|
||||
env.register_fn("string_concat", fn_type(vec![Type::String, Type::String], Type::String));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn env() -> TypeEnv {
|
||||
let mut e = TypeEnv::with_builtins();
|
||||
register(&mut e);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_len_returns_int() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("string_len").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Int)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_split_returns_array() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("string_split").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Array(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_contains_returns_bool() {
|
||||
let e = env();
|
||||
let ty = e.lookup_fn("string_contains").unwrap();
|
||||
assert!(matches!(ty, Type::Fn { return_type, .. } if matches!(return_type.as_ref(), Type::Bool)));
|
||||
}
|
||||
}
|
||||
@@ -300,17 +300,17 @@ impl<'g> Evaluator<'g> {
|
||||
Ok(EvalValue::Nil)
|
||||
}
|
||||
|
||||
// New expression kinds — return Nil
|
||||
_ => {
|
||||
return Ok(EvalValue::Nil);
|
||||
}
|
||||
|
||||
Expr::Sealed(stmts) => {
|
||||
for s in stmts {
|
||||
self.exec_stmt(s)?;
|
||||
}
|
||||
Ok(EvalValue::Nil)
|
||||
}
|
||||
|
||||
// New expression kinds — return Nil
|
||||
_ => {
|
||||
Ok(EvalValue::Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ impl TestRunner {
|
||||
}
|
||||
|
||||
/// Run only e2e tests.
|
||||
pub fn run_e2e<'a>(&self, tests: &'a [TestCase], engram_url: &str) -> Vec<TestResult> {
|
||||
pub fn run_e2e(&self, tests: &[TestCase], engram_url: &str) -> Vec<TestResult> {
|
||||
tests
|
||||
.iter()
|
||||
.filter(|t| matches!(t.target, TestTarget::E2e | TestTarget::Both))
|
||||
|
||||
@@ -662,7 +662,9 @@ impl Printable for User { fn print(msg: String) -> Void { } }
|
||||
|
||||
#[test]
|
||||
fn test_map_type_annotation() {
|
||||
assert_ok(r#"let m: Map<String, Int> = m"#);
|
||||
// Just test that Map<K,V> type annotation parses and resolves without crashing
|
||||
// Use a function body where a self-reference is valid
|
||||
assert_ok(r#"fn get_map() -> Map<String, Int> { return get_map() }"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user