diff --git a/Cargo.lock b/Cargo.lock index aa72fe2b..70c88018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3674,6 +3674,24 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3977,6 +3995,7 @@ name = "techscript_package_manager" version = "2.0.0" dependencies = [ "anyhow", + "criterion", "serde", "serde_json", "toml 1.1.4+spec-1.1.0", @@ -4081,6 +4100,8 @@ version = "2.0.0" dependencies = [ "serde", "serde_json", + "strum", + "strum_macros", "techscript_common", ] diff --git a/compiler/ast/tests/ast_tests.rs b/compiler/ast/tests/ast_tests.rs index 584d5ea6..ffe7f12b 100644 --- a/compiler/ast/tests/ast_tests.rs +++ b/compiler/ast/tests/ast_tests.rs @@ -1,6 +1,6 @@ use techscript_ast::{ - AssignmentExpr, EnumDecl, EnumVariant, Expression, FieldSpec, Ident, LiteralExpr, LiteralVal, - Pattern, StructDecl, VarDecl, + AssignmentExpr, Block, BreakStmt, EnumDecl, EnumVariant, Expression, FieldSpec, Ident, + LiteralExpr, LiteralVal, Pattern, Program, Statement, StructDecl, VarDecl, }; use techscript_common::{NodeId, Span}; @@ -101,5 +101,47 @@ fn test_ast_serialization() { assert_eq!(lit, deserialized); } +#[test] +fn test_ast_program_construction_and_serialization() { + let span = Span::new(0, 50); + let id = NodeId(1); + + // Create a dummy statement (Break) + let break_stmt = Statement::Break(BreakStmt::new(NodeId(2), span)); + + let program = Program::new(id, vec![break_stmt.clone()], span); + + assert_eq!(program.id, id); + assert_eq!(program.statements.len(), 1); + assert_eq!(program.span, span); + + let serialized = serde_json::to_string(&program).expect("serialize should succeed"); + let deserialized: Program = + serde_json::from_str(&serialized).expect("deserialize should succeed"); + + assert_eq!(program, deserialized); +} + +#[test] +fn test_ast_block_construction_and_serialization() { + let span = Span::new(10, 20); + let id = NodeId(3); + + // Create a dummy statement (Break) + let break_stmt = Statement::Break(BreakStmt::new(NodeId(4), span)); + + let block = Block::new(id, vec![break_stmt.clone()], span); + + assert_eq!(block.id, id); + assert_eq!(block.statements.len(), 1); + assert_eq!(block.span, span); + + let serialized = serde_json::to_string(&block).expect("serialize should succeed"); + let deserialized: Block = + serde_json::from_str(&serialized).expect("deserialize should succeed"); + + assert_eq!(block, deserialized); +} + // Internal helper just to satisfy TypeSpec compilation in test_ast_struct_decl use techscript_ast::TypeSpec; diff --git a/compiler/lexer/src/lib.rs b/compiler/lexer/src/lib.rs index c59e1798..034560cf 100644 --- a/compiler/lexer/src/lib.rs +++ b/compiler/lexer/src/lib.rs @@ -12,7 +12,6 @@ use techscript_syntax::{lookup_keyword, Token, TokenKind}; /// Private token enumeration used internally by Logos for scanning. #[derive(Logos, Debug, Clone, Copy, PartialEq, Eq)] #[logos(skip r"[ \t\r]+")] // Skip spaces, tabs, and carriage returns -#[allow(dead_code)] enum LogosToken { #[token("\n")] #[token("\r\n")] diff --git a/compiler/semantic/src/dsl_schema.rs b/compiler/semantic/src/dsl_schema.rs index b912d27c..d39e3c73 100644 --- a/compiler/semantic/src/dsl_schema.rs +++ b/compiler/semantic/src/dsl_schema.rs @@ -298,8 +298,7 @@ fn register_web_schemas(reg: &mut HashMap) { ); } -fn register_canvas_schemas(reg: &mut HashMap) { - // Canvas module schemas ────────────────────────────────────────── +fn register_canvas_shape_schemas(reg: &mut HashMap) { reg.insert( "logo".to_string(), DSLSchema::new( @@ -318,22 +317,6 @@ fn register_canvas_schemas(reg: &mut HashMap) { ), ); - reg.insert( - "rings".to_string(), - DSLSchema::new( - vec![ - "count".into(), - "color".into(), - "size".into(), - "thickness".into(), - "spacing".into(), - "rotation".into(), - ], - vec![], - vec![], - ), - ); - reg.insert( "emblem".to_string(), DSLSchema::new( @@ -365,7 +348,9 @@ fn register_canvas_schemas(reg: &mut HashMap) { vec![], ), ); +} +fn register_canvas_text_schemas(reg: &mut HashMap) { reg.insert( "letter".to_string(), DSLSchema::new( @@ -383,21 +368,6 @@ fn register_canvas_schemas(reg: &mut HashMap) { ), ); - reg.insert( - "circuits".to_string(), - DSLSchema::new( - vec![ - "color".into(), - "density".into(), - "width".into(), - "animated".into(), - "complexity".into(), - ], - vec![], - vec![], - ), - ); - reg.insert( "title".to_string(), DSLSchema::new( @@ -437,6 +407,39 @@ fn register_canvas_schemas(reg: &mut HashMap) { vec![], ), ); +} + +fn register_canvas_misc_schemas(reg: &mut HashMap) { + reg.insert( + "rings".to_string(), + DSLSchema::new( + vec![ + "count".into(), + "color".into(), + "size".into(), + "thickness".into(), + "spacing".into(), + "rotation".into(), + ], + vec![], + vec![], + ), + ); + + reg.insert( + "circuits".to_string(), + DSLSchema::new( + vec![ + "color".into(), + "density".into(), + "width".into(), + "animated".into(), + "complexity".into(), + ], + vec![], + vec![], + ), + ); reg.insert( "theme".to_string(), @@ -487,6 +490,13 @@ fn register_canvas_schemas(reg: &mut HashMap) { ); } +fn register_canvas_schemas(reg: &mut HashMap) { + // Canvas module schemas ────────────────────────────────────────── + register_canvas_shape_schemas(reg); + register_canvas_text_schemas(reg); + register_canvas_misc_schemas(reg); +} + fn register_generic_schemas(reg: &mut HashMap) { // Generic DSL blocks ──────────────────────────────────────────── reg.insert( diff --git a/compiler/syntax/Cargo.toml b/compiler/syntax/Cargo.toml index 251d67b8..b8ae747b 100644 --- a/compiler/syntax/Cargo.toml +++ b/compiler/syntax/Cargo.toml @@ -7,6 +7,8 @@ description = "Token definitions, keywords, and operator precedence for TechScri [dependencies] techscript_common = { path = "../common" } serde = { workspace = true } +strum = "0.28.0" +strum_macros = "0.28.0" [dev-dependencies] serde_json = { workspace = true } diff --git a/compiler/syntax/src/token_kind.rs b/compiler/syntax/src/token_kind.rs index 1092b8e8..42c33cb6 100644 --- a/compiler/syntax/src/token_kind.rs +++ b/compiler/syntax/src/token_kind.rs @@ -5,7 +5,9 @@ use serde::{Deserialize, Serialize}; use std::fmt; /// Classification of literal values in TechScript 2.0. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum_macros::EnumIter, +)] pub enum LiteralKind { /// Integer literal (e.g., `42`). Int, @@ -50,7 +52,9 @@ impl fmt::Display for NumericLiteralKind { /// - **Canonical 2.0**: The one true spelling — no warnings emitted. /// - **Deprecated Alias**: Old spelling — still parsed, but emits a `TSW1xxx` warning. /// - **Reserved**: Recognised by the lexer but not yet active. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum_macros::EnumIter, +)] pub enum TokenKind { // ── Canonical 2.0 Keywords ────────────────────────────────────────────────────── /// `do` — function/method declaration (replaces `build`, `fun`, `function`) diff --git a/compiler/syntax/tests/syntax_tests.rs b/compiler/syntax/tests/syntax_tests.rs index f895bfc5..f0bfcdac 100644 --- a/compiler/syntax/tests/syntax_tests.rs +++ b/compiler/syntax/tests/syntax_tests.rs @@ -378,114 +378,20 @@ fn test_serde_roundtrips() { #[test] fn test_token_uniqueness() { + use strum::IntoEnumIterator; // Ensure all TokenKind static_lexemes are unique let mut lexemes = HashSet::new(); - let kinds = [ - TokenKind::Do, - TokenKind::Send, - TokenKind::When, - TokenKind::Loop, - TokenKind::Repeat, - TokenKind::For, - TokenKind::In, - TokenKind::Match, - TokenKind::Case, - TokenKind::Default, - TokenKind::Try, - TokenKind::Catch, - TokenKind::Throw, - TokenKind::Use, - TokenKind::Class, - TokenKind::Struct, - TokenKind::Enum, - TokenKind::Trait, - TokenKind::Interface, - TokenKind::Const, - TokenKind::Null, - TokenKind::Say, - TokenKind::Ask, - TokenKind::Break, - TokenKind::Continue, - TokenKind::Else, - TokenKind::Async, - TokenKind::Await, - TokenKind::Parallel, - TokenKind::End, - TokenKind::Export, - TokenKind::New, - TokenKind::SelfKw, - TokenKind::True, - TokenKind::False, - TokenKind::Typeof, - TokenKind::With, - TokenKind::Build, - TokenKind::Make, - TokenKind::Return, - TokenKind::Model, - TokenKind::If, - TokenKind::Elif, - TokenKind::While, - TokenKind::Import, - TokenKind::From, - TokenKind::Let, - TokenKind::Var, - TokenKind::Fun, - TokenKind::Function, - TokenKind::Attempt, - TokenKind::None, - TokenKind::Keep, - TokenKind::Give, - TokenKind::Stop, - TokenKind::Skip, - TokenKind::Each, - TokenKind::Switch, - TokenKind::Be, - TokenKind::Equals, - TokenKind::Then, - TokenKind::Plus, - TokenKind::Minus, - TokenKind::Star, - TokenKind::Slash, - TokenKind::DoubleSlash, - TokenKind::Percent, - TokenKind::DoubleStar, - TokenKind::EqualEqual, - TokenKind::BangEqual, - TokenKind::TripleEqual, - TokenKind::BangEqualEqual, - TokenKind::Less, - TokenKind::Greater, - TokenKind::LessEqual, - TokenKind::GreaterEqual, - TokenKind::Equal, - TokenKind::PlusEqual, - TokenKind::MinusEqual, - TokenKind::StarEqual, - TokenKind::SlashEqual, - TokenKind::PercentEqual, - TokenKind::DotDot, - TokenKind::DotDotEqual, - TokenKind::QuestionDot, - TokenKind::QuestionQuestion, - TokenKind::Arrow, - TokenKind::LeftParen, - TokenKind::RightParen, - TokenKind::LeftBrace, - TokenKind::RightBrace, - TokenKind::LeftBracket, - TokenKind::RightBracket, - TokenKind::Comma, - TokenKind::Dot, - TokenKind::Colon, - TokenKind::Semicolon, - ]; - for kind in kinds { - let lexeme = kind.static_lexeme().unwrap(); - assert!( - lexemes.insert(lexeme), - "Duplicate static lexeme detected for: {:?}", - kind - ); + for kind in TokenKind::iter() { + if kind == TokenKind::FStringExprStart || kind == TokenKind::FStringExprEnd { + continue; // Overlaps with LeftBrace/RightBrace logically, but handled contextually + } + if let Some(lexeme) = kind.static_lexeme() { + assert!( + lexemes.insert(lexeme), + "Duplicate static lexeme detected for: {:?}", + kind + ); + } } } diff --git a/runtime/vm/src/executor.rs b/runtime/vm/src/executor.rs index 718997a7..5b909abe 100644 --- a/runtime/vm/src/executor.rs +++ b/runtime/vm/src/executor.rs @@ -450,8 +450,8 @@ impl VM { } // PERFORMANCE OPTIMIZATION (Bolt): - // We reuse the existing mutable `frame` reference acquired at the start of - // the loop iteration rather than redundantly calling `self.frames.last_mut()` + // We reuse the existing mutable frame reference acquired at the start of + // the loop iteration rather than redundantly fetching the last mutable frame // for these control flow and exception opcodes. This reduces bounds checking // and RefCell borrow overhead on the hottest execution paths. Opcode::Jump => { diff --git a/scripts/migrate_syntax.py b/scripts/migrate_syntax.py index b6edc7bc..9d98f91f 100644 --- a/scripts/migrate_syntax.py +++ b/scripts/migrate_syntax.py @@ -34,7 +34,7 @@ (r'std\.io\.println\((.+?)\)', r'say \1', 0), (r'std\.io\.print\((.+?)\)', r'say \1', 0), - # ── std.xxx.yyy() calls → module.yyy() ───────────────────────────────── + # ── std..yyy() calls → module.yyy() ──────────────────────────── (r'std\.math\.', r'math.', 0), (r'std\.strings\.', r'string.', 0), (r'std\.fs\.', r'file.', 0), diff --git a/stdlib/src/database.rs b/stdlib/src/database.rs index 15987f53..a09c1d22 100644 --- a/stdlib/src/database.rs +++ b/stdlib/src/database.rs @@ -21,18 +21,31 @@ fn get_params_list(args: &[RuntimeValue]) -> Vec { } } -fn params_list_to_sqlite_params(params_list: &[RuntimeValue]) -> Vec { - params_list - .iter() - .map(|p| match p { - RuntimeValue::Null => rusqlite::types::Value::Null, - RuntimeValue::Bool(b) => rusqlite::types::Value::Integer(if *b { 1 } else { 0 }), - RuntimeValue::Int(i) => rusqlite::types::Value::Integer(*i), - RuntimeValue::Float(f) => rusqlite::types::Value::Real(*f), - RuntimeValue::Str(s) => rusqlite::types::Value::Text(s.clone()), - _ => rusqlite::types::Value::Null, - }) - .collect() +struct SqlParam<'a>(&'a RuntimeValue); + +impl<'a> rusqlite::types::ToSql for SqlParam<'a> { + fn to_sql(&self) -> rusqlite::Result> { + match self.0 { + RuntimeValue::Null => Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Null, + )), + RuntimeValue::Bool(b) => Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Integer(if *b { 1 } else { 0 }), + )), + RuntimeValue::Int(i) => Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Integer(*i), + )), + RuntimeValue::Float(f) => Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Real(*f), + )), + RuntimeValue::Str(s) => Ok(rusqlite::types::ToSqlOutput::Borrowed( + rusqlite::types::ValueRef::Text(s.as_bytes()), + )), + _ => Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Null, + )), + } + } } fn std_database_connect( @@ -76,10 +89,8 @@ fn std_database_query( ) -> Result { let handle = args[0].try_into_int()? as u32; let sql = args[1].try_into_string()?; - let params_list = get_params_list(&args); - let resources = ctx.resources.clone(); - let resources_borrow = resources.borrow(); + let resources_borrow = ctx.resources.borrow(); let conn = resources_borrow .get::(handle) .ok_or_else(|| { @@ -101,7 +112,8 @@ fn std_database_query( ) })?; - let params_converted = params_list_to_sqlite_params(¶ms_list); + let params_list = get_params_list(&args); + let params_converted: Vec> = params_list.iter().map(SqlParam).collect(); let column_names: Vec = stmt .column_names() .into_iter() @@ -171,10 +183,8 @@ fn std_database_execute( ) -> Result { let handle = args[0].try_into_int()? as u32; let sql = args[1].try_into_string()?; - let params_list = get_params_list(&args); - let resources = ctx.resources.clone(); - let resources_borrow = resources.borrow(); + let resources_borrow = ctx.resources.borrow(); let conn = resources_borrow .get::(handle) .ok_or_else(|| { @@ -188,7 +198,8 @@ fn std_database_execute( ) })?; - let params_converted = params_list_to_sqlite_params(¶ms_list); + let params_list = get_params_list(&args); + let params_converted: Vec> = params_list.iter().map(SqlParam).collect(); let params_refs: Vec<&dyn rusqlite::types::ToSql> = params_converted .iter() .map(|p| p as &dyn rusqlite::types::ToSql) diff --git a/stdlib/src/encoding.rs b/stdlib/src/encoding.rs index 325d5ecf..483de578 100644 --- a/stdlib/src/encoding.rs +++ b/stdlib/src/encoding.rs @@ -83,7 +83,7 @@ impl StdlibRegistry { StdlibModule { name: "std.hex".to_string(), version: "1.0.0".to_string(), - exports: exports.clone(), + exports, required_capabilities: Vec::new(), }, ); diff --git a/stdlib/src/json.rs b/stdlib/src/json.rs index 06528ccf..3b87d1af 100644 --- a/stdlib/src/json.rs +++ b/stdlib/src/json.rs @@ -205,4 +205,95 @@ mod tests { let err = result.unwrap_err(); assert!(err.to_string().contains("Cannot stringify type")); } + + #[test] + fn test_parse_json_value_simple() { + // Null + assert_eq!( + parse_json_value(serde_json::Value::Null), + RuntimeValue::Null + ); + + // Bool + assert_eq!( + parse_json_value(serde_json::Value::Bool(true)), + RuntimeValue::Bool(true) + ); + assert_eq!( + parse_json_value(serde_json::Value::Bool(false)), + RuntimeValue::Bool(false) + ); + + // Int + assert_eq!( + parse_json_value(serde_json::Value::Number(serde_json::Number::from(42))), + RuntimeValue::Int(42) + ); + assert_eq!( + parse_json_value(serde_json::Value::Number(serde_json::Number::from(-10))), + RuntimeValue::Int(-10) + ); + + // Float + assert_eq!( + parse_json_value(serde_json::Value::Number( + serde_json::Number::from_f64(3.14).unwrap() + )), + RuntimeValue::Float(3.14) + ); + + // String + assert_eq!( + parse_json_value(serde_json::Value::String("hello".to_string())), + RuntimeValue::Str("hello".to_string()) + ); + } + + #[test] + fn test_parse_json_value_complex() { + // Array + let arr = serde_json::Value::Array(vec![ + serde_json::Value::Number(serde_json::Number::from(1)), + serde_json::Value::String("two".to_string()), + serde_json::Value::Bool(false), + ]); + let parsed_arr = parse_json_value(arr); + match parsed_arr { + RuntimeValue::List { items, is_const } => { + assert!(!is_const); + let borrowed = items.borrow(); + assert_eq!(borrowed.len(), 3); + assert_eq!(borrowed[0], RuntimeValue::Int(1)); + assert_eq!(borrowed[1], RuntimeValue::Str("two".to_string())); + assert_eq!(borrowed[2], RuntimeValue::Bool(false)); + } + _ => panic!("Expected RuntimeValue::List"), + } + + // Object + let mut obj_map = serde_json::Map::new(); + obj_map.insert( + "key1".to_string(), + serde_json::Value::Number(serde_json::Number::from(100)), + ); + obj_map.insert( + "key2".to_string(), + serde_json::Value::String("value2".to_string()), + ); + let obj = serde_json::Value::Object(obj_map); + let parsed_obj = parse_json_value(obj); + match parsed_obj { + RuntimeValue::Map { entries, is_const } => { + assert!(!is_const); + let borrowed = entries.borrow(); + assert_eq!(borrowed.len(), 2); + assert_eq!(borrowed.get("key1").unwrap(), &RuntimeValue::Int(100)); + assert_eq!( + borrowed.get("key2").unwrap(), + &RuntimeValue::Str("value2".to_string()) + ); + } + _ => panic!("Expected RuntimeValue::Map"), + } + } } diff --git a/stdlib/src/notification.rs b/stdlib/src/notification.rs index a29dde0b..e7e033a9 100644 --- a/stdlib/src/notification.rs +++ b/stdlib/src/notification.rs @@ -34,13 +34,11 @@ impl StdlibRegistry { { use std::process::Command; let _ = Command::new("powershell") + .env("__TS_BODY", &body) + .env("__TS_TITLE", &title) .args([ "-Command", - &format!( - "[System.Windows.MessageBox]::Show('{}','{}')", - body.replace("'", "''"), - title.replace("'", "''") - ), + "[System.Windows.MessageBox]::Show($env:__TS_BODY, $env:__TS_TITLE)", ]) .spawn(); } diff --git a/stdlib/src/sqlite.rs b/stdlib/src/sqlite.rs index 487d44da..6800e2f0 100644 --- a/stdlib/src/sqlite.rs +++ b/stdlib/src/sqlite.rs @@ -63,15 +63,6 @@ impl StdlibRegistry { ) })?; let sql = args[1].to_string(); - let params_list = if args.len() > 2 { - if let RuntimeValue::List { items, .. } = &args[2] { - items.borrow().clone() - } else { - Vec::new() - } - } else { - Vec::new() - }; let params: Vec = if let Some(arg) = args.get(2) { if let RuntimeValue::List { items, .. } = arg { @@ -138,15 +129,6 @@ impl StdlibRegistry { ) })?; let sql = args[1].to_string(); - let params_list = if args.len() > 2 { - if let RuntimeValue::List { items, .. } = &args[2] { - items.borrow().clone() - } else { - Vec::new() - } - } else { - Vec::new() - }; let params: Vec = if let Some(arg) = args.get(2) { if let RuntimeValue::List { items, .. } = arg { @@ -183,16 +165,29 @@ impl StdlibRegistry { let mut rows = Vec::new(); let row_iter = stmt .query_map(rusqlite::params_from_iter(params), |row| { - let mut map = IndexMap::new(); - for name in &col_names { - map.insert(name.clone(), RuntimeValue::Null); - } + let mut map = IndexMap::with_capacity(col_count); for i in 0..col_count { - let val: String = - row.get::<_, String>(i).unwrap_or_default(); - if let Some((_, v)) = map.get_index_mut(i) { - *v = RuntimeValue::Str(val); - } + let val = row.get_ref(i)?; + let rt_val = match val { + rusqlite::types::ValueRef::Null => RuntimeValue::Null, + rusqlite::types::ValueRef::Integer(v) => { + RuntimeValue::Int(v) + } + rusqlite::types::ValueRef::Real(v) => { + RuntimeValue::Float(v) + } + rusqlite::types::ValueRef::Text(v) => { + RuntimeValue::Str( + String::from_utf8_lossy(v).into_owned(), + ) + } + rusqlite::types::ValueRef::Blob(v) => { + RuntimeValue::Str( + String::from_utf8_lossy(v).into_owned(), + ) + } + }; + map.insert(col_names[i].clone(), rt_val); } Ok(map) }) diff --git a/stdlib/src/sys.rs b/stdlib/src/sys.rs index 42d264b3..3753b553 100644 --- a/stdlib/src/sys.rs +++ b/stdlib/src/sys.rs @@ -100,7 +100,7 @@ impl StdlibRegistry { StdlibModule { name: "std.fs".to_string(), version: "1.0.0".to_string(), - exports: exports.clone(), + exports, required_capabilities: vec![Capability::FileSystem], }, ); diff --git a/stdlib/src/web.rs b/stdlib/src/web.rs index 500b9235..b4c6e034 100644 --- a/stdlib/src/web.rs +++ b/stdlib/src/web.rs @@ -98,9 +98,7 @@ static PAGE_CONTENT: Mutex = Mutex::new(String::new()); fn render_children(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue) { for child in &dsl.children { - html.push_str(&dsl_to_html( - &techscript_runtime::value::RuntimeValue::DslBlock(std::rc::Rc::new(child.clone())), - )); + html.push_str(&dsl_block_to_html(child)); } } @@ -234,129 +232,131 @@ fn render_card(html: &mut String, dsl: &techscript_runtime::value::DslBlockValue /// Convert a DslBlockValue tree to HTML string. fn dsl_to_html(val: &RuntimeValue) -> String { match val { - RuntimeValue::DslBlock(dsl) => { - let mut html = String::new(); - match dsl.kind.as_str() { - "website" => { - render_website(&mut html, dsl); - } - "page" => { - render_page(&mut html, dsl); - } - "hero" => { - render_hero(&mut html, dsl); - } - "section" => { - render_section(&mut html, dsl); - } - "card" => { - render_card(&mut html, dsl); - } - "button" => { - let label = dsl - .properties - .iter() - .find(|p| p.name == "label") - .and_then(|p| p.value.as_ref()) - .map(|v| v.to_string()) - .unwrap_or_else(|| "Button".to_string()); - let _ = write!(html, "", label); - } - "link" => { - let label = dsl - .properties - .iter() - .find(|p| p.name == "label") - .and_then(|p| p.value.as_ref()) - .map(|v| v.to_string()) - .unwrap_or_else(|| "Link".to_string()); - let url = dsl - .properties - .iter() - .find(|p| p.name == "url") - .and_then(|p| p.value.as_ref()) - .map(|v| v.to_string()); - if let Some(u) = url { - let _ = write!(html, "{}", u, label); - } else { - let _ = write!(html, "{}", label); - } - } - "nav" => { - html.push_str(""); - } - "header" => { - html.push_str("
"); - for prop in &dsl.properties { - if prop.name == "title" { - if let Some(RuntimeValue::Str(t)) = &prop.value { - let _ = write!(html, "

{}

", t); - } - } - } - render_children(&mut html, dsl); - html.push_str("
"); - } - "footer" => { - html.push_str("
"); - for prop in &dsl.properties { - if prop.name == "text" { - if let Some(RuntimeValue::Str(t)) = &prop.value { - let _ = write!(html, "

{}

", t); - } - } + RuntimeValue::DslBlock(dsl) => dsl_block_to_html(dsl), + _ => String::new(), + } +} + +fn dsl_block_to_html(dsl: &techscript_runtime::value::DslBlockValue) -> String { + let mut html = String::new(); + match dsl.kind.as_str() { + "website" => { + render_website(&mut html, dsl); + } + "page" => { + render_page(&mut html, dsl); + } + "hero" => { + render_hero(&mut html, dsl); + } + "section" => { + render_section(&mut html, dsl); + } + "card" => { + render_card(&mut html, dsl); + } + "button" => { + let label = dsl + .properties + .iter() + .find(|p| p.name == "label") + .and_then(|p| p.value.as_ref()) + .map(|v| v.to_string()) + .unwrap_or_else(|| "Button".to_string()); + let _ = write!(html, "", label); + } + "link" => { + let label = dsl + .properties + .iter() + .find(|p| p.name == "label") + .and_then(|p| p.value.as_ref()) + .map(|v| v.to_string()) + .unwrap_or_else(|| "Link".to_string()); + let url = dsl + .properties + .iter() + .find(|p| p.name == "url") + .and_then(|p| p.value.as_ref()) + .map(|v| v.to_string()); + if let Some(u) = url { + let _ = write!(html, "{}", u, label); + } else { + let _ = write!(html, "{}", label); + } + } + "nav" => { + html.push_str(""); + } + "header" => { + html.push_str("
"); + for prop in &dsl.properties { + if prop.name == "title" { + if let Some(RuntimeValue::Str(t)) = &prop.value { + let _ = write!(html, "

{}

", t); } - render_children(&mut html, dsl); - html.push_str("
"); } - "input" => { - let placeholder = dsl - .properties - .iter() - .find(|p| p.name == "placeholder") - .and_then(|p| p.value.as_ref()) - .map(|v| v.to_string()); - if let Some(p) = placeholder { - let _ = write!(html, "", p); - } else { - html.push_str(""); + } + render_children(&mut html, dsl); + html.push_str(""); + } + "footer" => { + html.push_str("
"); + for prop in &dsl.properties { + if prop.name == "text" { + if let Some(RuntimeValue::Str(t)) = &prop.value { + let _ = write!(html, "

{}

", t); } } - "form" => { - html.push_str("
"); - render_children(&mut html, dsl); - html.push_str("
"); - } - "main" => { - html.push_str("
"); - render_children(&mut html, dsl); - html.push_str("
"); - } - "aside" => { - html.push_str(""); - } - "start" => { - let label = dsl - .properties - .iter() - .find(|p| p.name == "label") - .and_then(|p| p.value.as_ref()) - .map(|v| v.to_string()) - .unwrap_or_else(|| "Get Started".to_string()); - let _ = write!(html, "{}", label); - } - _ => { - let _ = write!(html, "", dsl.kind); - } } - html + render_children(&mut html, dsl); + html.push_str("
"); + } + "input" => { + let placeholder = dsl + .properties + .iter() + .find(|p| p.name == "placeholder") + .and_then(|p| p.value.as_ref()) + .map(|v| v.to_string()); + if let Some(p) = placeholder { + let _ = write!(html, "", p); + } else { + html.push_str(""); + } + } + "form" => { + html.push_str("
"); + render_children(&mut html, dsl); + html.push_str("
"); + } + "main" => { + html.push_str("
"); + render_children(&mut html, dsl); + html.push_str("
"); + } + "aside" => { + html.push_str(""); + } + "start" => { + let label = dsl + .properties + .iter() + .find(|p| p.name == "label") + .and_then(|p| p.value.as_ref()) + .map(|v| v.to_string()) + .unwrap_or_else(|| "Get Started".to_string()); + let _ = write!(html, "{}", label); + } + _ => { + let _ = write!(html, "", dsl.kind); } - _ => String::new(), } + html } impl StdlibRegistry { @@ -398,9 +398,9 @@ impl StdlibRegistry { Mutex::new(tiny_http::Server::http(format!("0.0.0.0:{}", port)).unwrap()); thread::spawn(move || { while SERVER_RUNNING.load(Ordering::SeqCst) { - let page = PAGE_CONTENT.lock().unwrap().clone(); if let Ok(mut req) = server.lock().unwrap().recv() { - let r = tiny_http::Response::from_string(&page).with_header( + let page = PAGE_CONTENT.lock().unwrap(); + let r = tiny_http::Response::from_string(&*page).with_header( tiny_http::Header::from_bytes( &b"Content-Type"[..], &b"text/html; charset=utf-8"[..], @@ -466,9 +466,9 @@ impl StdlibRegistry { Mutex::new(tiny_http::Server::http(format!("0.0.0.0:{}", port)).unwrap()); thread::spawn(move || { while SERVER_RUNNING.load(Ordering::SeqCst) { - let page = PAGE_CONTENT.lock().unwrap().clone(); if let Ok(mut req) = server.lock().unwrap().recv() { - let r = tiny_http::Response::from_string(&page).with_header( + let page = PAGE_CONTENT.lock().unwrap(); + let r = tiny_http::Response::from_string(&*page).with_header( tiny_http::Header::from_bytes( &b"Content-Type"[..], &b"text/html; charset=utf-8"[..], @@ -576,12 +576,12 @@ impl StdlibRegistry { arity: 0, callback: |ctx, _args| { let env = ctx.global_env.borrow(); - let blocks = match env.lookup("_dsl_blocks") { - Ok(RuntimeValue::List { items, .. }) => items.borrow().clone(), + let items = match env.lookup("_dsl_blocks") { + Ok(RuntimeValue::List { items, .. }) => items, _ => return Ok(RuntimeValue::Str(String::new())), }; let mut html = String::new(); - for block in &blocks { + for block in &*items.borrow() { html.push_str(&dsl_to_html(block)); } Ok(RuntimeValue::Str(html)) diff --git a/stdlib/tests/stdlib_tests.rs b/stdlib/tests/stdlib_tests.rs index 8fe9cdf7..785a0793 100644 --- a/stdlib/tests/stdlib_tests.rs +++ b/stdlib/tests/stdlib_tests.rs @@ -915,11 +915,9 @@ fn test_async_and_channels() { } #[test] -fn test_crypto_hash_and_compression() { +fn test_hash_operations() { let registry = StdlibRegistry::new(); - let crypto = registry.get_module("std.crypto").unwrap(); let hash = registry.get_module("std.hash").unwrap(); - let compress = registry.get_module("std.compress").unwrap(); let mut config_unprivileged = RuntimeConfig::default(); config_unprivileged @@ -927,11 +925,6 @@ fn test_crypto_hash_and_compression() { .remove(&Capability::FileSystem); let mut ctx_unprivileged = RuntimeContext::new(config_unprivileged); - let mut config_fs = RuntimeConfig::default(); - config_fs.capabilities.insert(Capability::FileSystem); - let mut ctx_fs = RuntimeContext::new(config_fs); - - // 1. Test hash operations let md5_fn = hash.exports.get("md5").unwrap(); let val = md5_fn .call( @@ -961,8 +954,19 @@ fn test_crypto_hash_and_compression() { ) .unwrap(); assert!(val.as_int().is_some()); +} + +#[test] +fn test_crypto_operations() { + let registry = StdlibRegistry::new(); + let crypto = registry.get_module("std.crypto").unwrap(); + + let mut config_unprivileged = RuntimeConfig::default(); + config_unprivileged + .capabilities + .remove(&Capability::FileSystem); + let mut ctx_unprivileged = RuntimeContext::new(config_unprivileged); - // 2. Test crypto operations (AES-GCM & Bcrypt) let aes_enc = crypto.exports.get("aes_encrypt").unwrap(); let aes_dec = crypto.exports.get("aes_decrypt").unwrap(); @@ -1010,8 +1014,23 @@ fn test_crypto_hash_and_compression() { .call(&mut ctx_unprivileged, vec![pass, hashed.clone()]) .unwrap(); assert_eq!(is_valid.as_bool(), Some(true)); +} + +#[test] +fn test_compression_operations() { + let registry = StdlibRegistry::new(); + let compress = registry.get_module("std.compress").unwrap(); + + let mut config_unprivileged = RuntimeConfig::default(); + config_unprivileged + .capabilities + .remove(&Capability::FileSystem); + let mut ctx_unprivileged = RuntimeContext::new(config_unprivileged); + + let mut config_fs = RuntimeConfig::default(); + config_fs.capabilities.insert(Capability::FileSystem); + let mut ctx_fs = RuntimeContext::new(config_fs); - // 3. Test compression capabilities & operations let temp_dir = std::env::temp_dir().join("techscript_archive_test"); std::fs::create_dir_all(&temp_dir).ok(); @@ -1073,15 +1092,10 @@ fn test_graphics_canvas_drawing() { .remove(&Capability::FileSystem); let mut ctx_unprivileged = RuntimeContext::new(config_unprivileged); - let mut config_fs = RuntimeConfig::default(); - config_fs.capabilities.insert(Capability::FileSystem); - let mut ctx_fs = RuntimeContext::new(config_fs); - let create_canvas_fn = graphics.exports.get("create_canvas").unwrap(); let draw_rect_fn = graphics.exports.get("draw_rect").unwrap(); let draw_circle_fn = graphics.exports.get("draw_circle").unwrap(); let draw_line_fn = graphics.exports.get("draw_line").unwrap(); - let save_png_fn = graphics.exports.get("save_png").unwrap(); // 1. Create a 100x100 canvas let canvas_handle_val = create_canvas_fn @@ -1137,6 +1151,34 @@ fn test_graphics_canvas_drawing() { ], ) .unwrap(); +} + +#[test] +fn test_graphics_canvas_save_png() { + let registry = StdlibRegistry::new(); + let graphics = registry.get_module("std.graphics").unwrap(); + + let mut config_unprivileged = RuntimeConfig::default(); + config_unprivileged + .capabilities + .remove(&Capability::FileSystem); + let mut ctx_unprivileged = RuntimeContext::new(config_unprivileged); + + let mut config_fs = RuntimeConfig::default(); + config_fs.capabilities.insert(Capability::FileSystem); + let mut ctx_fs = RuntimeContext::new(config_fs); + + let create_canvas_fn = graphics.exports.get("create_canvas").unwrap(); + let save_png_fn = graphics.exports.get("save_png").unwrap(); + + // Create a canvas to save + let canvas_handle_val = create_canvas_fn + .call( + &mut ctx_unprivileged, + vec![RuntimeValue::Int(100), RuntimeValue::Int(100)], + ) + .unwrap(); + let handle = canvas_handle_val.as_int().unwrap(); // 3. Save to PNG file (requires FileSystem capability) let temp_file = std::env::temp_dir().join("test_canvas.png"); diff --git a/tools/lsp/src/lib.rs b/tools/lsp/src/lib.rs index 9e8845d5..6f5eb42d 100644 --- a/tools/lsp/src/lib.rs +++ b/tools/lsp/src/lib.rs @@ -1291,11 +1291,18 @@ impl LanguageServer for Backend { let start = range.start.line as usize; let end = (range.end.line as usize).min(lines.len() - 1); - let mut selection = String::new(); - for i in start..=end { - selection.push_str(lines[i]); - selection.push('\n'); - } + let selection = if start <= end { + let slice = &lines[start..=end]; + let capacity: usize = slice.iter().map(|l| l.len() + 1).sum(); + let mut s = String::with_capacity(capacity); + for line in slice { + s.push_str(line); + s.push('\n'); + } + s + } else { + String::new() + }; let formatted = self.format_source(&selection); Ok(Some(vec![TextEdit { @@ -1431,7 +1438,7 @@ impl LanguageServer for Backend { for diagnostic in params.context.diagnostics { if let Some(ref code) = diagnostic.code { if let NumberOrString::String(ref s) = code { - if s.contains("warning") { + if s == "warning" { let mut const_edits = Vec::new(); const_edits.push(TextEdit { range: diagnostic.range, diff --git a/tools/package-manager/Cargo.toml b/tools/package-manager/Cargo.toml index b891de41..19c89e09 100644 --- a/tools/package-manager/Cargo.toml +++ b/tools/package-manager/Cargo.toml @@ -10,3 +10,10 @@ serde_json = { workspace = true } toml = "1.1" anyhow = { workspace = true } ureq = { version = "2.9" } + +[[bench]] +name = "capability_validation_bench" +harness = false + +[dev-dependencies] +criterion = "0.8.2" diff --git a/tools/package-manager/benches/capability_validation_bench.rs b/tools/package-manager/benches/capability_validation_bench.rs new file mode 100644 index 00000000..ce74d3e7 --- /dev/null +++ b/tools/package-manager/benches/capability_validation_bench.rs @@ -0,0 +1,29 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use std::hint::black_box; +use techscript_package_manager::CapabilityValidator; + +fn benchmark_validate_elevation(c: &mut Criterion) { + let mut group = c.benchmark_group("capability_validation"); + + let root_caps: Vec = (0..100).map(|i| format!("cap_{}", i)).collect(); + let dep_caps: Vec = (50..150).map(|i| format!("cap_{}", i)).collect(); + let allowed_elevations: Vec = (0..100).map(|i| format!("dep_{}", i)).collect(); + + let dep_name = "dep_50"; // Will be found in allowed_elevations + + group.bench_function("validate_elevation", |b| { + b.iter(|| { + let _ = CapabilityValidator::validate_elevation( + black_box(&root_caps), + black_box(&dep_caps), + black_box(&allowed_elevations), + black_box(dep_name), + ); + }); + }); + + group.finish(); +} + +criterion_group!(benches, benchmark_validate_elevation); +criterion_main!(benches); diff --git a/tools/package-manager/src/lib.rs b/tools/package-manager/src/lib.rs index 6debbdfc..f6b03271 100644 --- a/tools/package-manager/src/lib.rs +++ b/tools/package-manager/src/lib.rs @@ -340,8 +340,10 @@ impl CapabilityValidator { dep_name: &str, ) -> Result<()> { let root_set: HashSet<&String> = root_caps.iter().collect(); + let allowed_elevations_set: HashSet<&str> = + allowed_elevations.iter().map(|s| s.as_str()).collect(); for cap in dependency_caps { - if !root_set.contains(cap) && !allowed_elevations.contains(&dep_name.to_string()) { + if !root_set.contains(cap) && !allowed_elevations_set.contains(dep_name) { return Err(anyhow!( "Security validation failed: Dependency '{}' requests capability '{}' which is not granted to the parent package.", dep_name, diff --git a/tools/packager/src/main.rs b/tools/packager/src/main.rs index 38d24ebd..9e256107 100644 --- a/tools/packager/src/main.rs +++ b/tools/packager/src/main.rs @@ -22,6 +22,19 @@ struct ReleaseDirectories { examples_dir: PathBuf, } +struct HashWriter<'a>(&'a mut Sha256); + +impl<'a> std::io::Write for HashWriter<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.update(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + fn main() -> anyhow::Result<()> { println!("=== Starting TechScript 2.0 Packaging Pipeline ==="); @@ -726,9 +739,7 @@ fn calculate_checksums_json(release_dir: &Path) -> anyhow::Result anyhow::Result anyhow::Result<()> { if file_path.exists() { let mut file = File::open(file_path)?; let mut hasher = Sha256::new(); - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - hasher.update(&buffer); + std::io::copy(&mut file, &mut HashWriter(&mut hasher))?; let hash = hasher.finalize(); // Format to show filename relative to the release root