feat: *= /= operators + 6 new array methods
New assignment operators (full pipeline: lexer → parser → analyzer → codegen): - *= (MulAssign): count *= 2 - /= (DivAssign): count /= 2 New array methods in event handler (all flush signal + stream diff): - .clear() → items = [] - .insert(i, v) → splice at index - .sort() → immutable sort - .reverse() → immutable reverse - .filter(fn) → filter in place - .map(fn) → map in place New example: language-features.ds (65KB) - Tests all assignment ops (+=, -=, *=, /=) - Tests array methods (push, pop, sort, reverse, clear) - Tests match expressions - Tests comparison operators - Tests derived signals (let doubled = count * 2) - Browser-verified: zero console errors All 11 examples pass.
This commit is contained in:
parent
0125c6e714
commit
f4e5ace37c
6 changed files with 158 additions and 0 deletions
|
|
@ -48,6 +48,8 @@ pub enum MutationOp {
|
|||
Set(String), // expression source
|
||||
AddAssign(String),
|
||||
SubAssign(String),
|
||||
MulAssign(String),
|
||||
DivAssign(String),
|
||||
}
|
||||
|
||||
/// A dependency edge in the signal graph.
|
||||
|
|
@ -360,6 +362,8 @@ fn extract_mutations(expr: &Expr) -> Vec<Mutation> {
|
|||
ds_parser::AssignOp::Set => MutationOp::Set(format!("{value:?}")),
|
||||
ds_parser::AssignOp::AddAssign => MutationOp::AddAssign(format!("{value:?}")),
|
||||
ds_parser::AssignOp::SubAssign => MutationOp::SubAssign(format!("{value:?}")),
|
||||
ds_parser::AssignOp::MulAssign => MutationOp::MulAssign(format!("{value:?}")),
|
||||
ds_parser::AssignOp::DivAssign => MutationOp::DivAssign(format!("{value:?}")),
|
||||
};
|
||||
mutations.push(Mutation { target: name.clone(), op: mutation_op });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1260,6 +1260,8 @@ impl JsEmitter {
|
|||
AssignOp::Set => format!("{name}.value = {value_js}"),
|
||||
AssignOp::AddAssign => format!("{name}.value += {value_js}"),
|
||||
AssignOp::SubAssign => format!("{name}.value -= {value_js}"),
|
||||
AssignOp::MulAssign => format!("{name}.value *= {value_js}"),
|
||||
AssignOp::DivAssign => format!("{name}.value /= {value_js}"),
|
||||
};
|
||||
(a, name.clone())
|
||||
}
|
||||
|
|
@ -1270,6 +1272,8 @@ impl JsEmitter {
|
|||
AssignOp::Set => format!("{target_str} = {value_js}"),
|
||||
AssignOp::AddAssign => format!("{target_str} += {value_js}"),
|
||||
AssignOp::SubAssign => format!("{target_str} -= {value_js}"),
|
||||
AssignOp::MulAssign => format!("{target_str} *= {value_js}"),
|
||||
AssignOp::DivAssign => format!("{target_str} /= {value_js}"),
|
||||
};
|
||||
(a, base_str)
|
||||
}
|
||||
|
|
@ -1285,6 +1289,8 @@ impl JsEmitter {
|
|||
AssignOp::Set => format!("{target_str} = {value_js}"),
|
||||
AssignOp::AddAssign => format!("{target_str} += {value_js}"),
|
||||
AssignOp::SubAssign => format!("{target_str} -= {value_js}"),
|
||||
AssignOp::MulAssign => format!("{target_str} *= {value_js}"),
|
||||
AssignOp::DivAssign => format!("{target_str} /= {value_js}"),
|
||||
};
|
||||
(a, root)
|
||||
}
|
||||
|
|
@ -1294,6 +1300,8 @@ impl JsEmitter {
|
|||
AssignOp::Set => format!("{s} = {value_js}"),
|
||||
AssignOp::AddAssign => format!("{s} += {value_js}"),
|
||||
AssignOp::SubAssign => format!("{s} -= {value_js}"),
|
||||
AssignOp::MulAssign => format!("{s} *= {value_js}"),
|
||||
AssignOp::DivAssign => format!("{s} /= {value_js}"),
|
||||
};
|
||||
(a, s)
|
||||
}
|
||||
|
|
@ -1345,6 +1353,52 @@ impl JsEmitter {
|
|||
sig = signal_name
|
||||
)
|
||||
}
|
||||
"clear" => {
|
||||
// items.clear() → items.value = []
|
||||
format!(
|
||||
"{sig}.value = []; DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||
sig = signal_name
|
||||
)
|
||||
}
|
||||
"insert" => {
|
||||
// items.insert(idx, val) → splice
|
||||
let idx = args_js.first().map(|s| s.as_str()).unwrap_or("0");
|
||||
let val = args_js.get(1).map(|s| s.as_str()).unwrap_or("undefined");
|
||||
format!(
|
||||
"{{ const _a = [...{sig}.value]; _a.splice({idx}, 0, {val}); {sig}.value = _a; DS._streamDiff(\"{sig}\", {sig}.value) }}",
|
||||
sig = signal_name, idx = idx, val = val
|
||||
)
|
||||
}
|
||||
"sort" => {
|
||||
// items.sort() → sort a copy
|
||||
format!(
|
||||
"{sig}.value = [...{sig}.value].sort(); DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||
sig = signal_name
|
||||
)
|
||||
}
|
||||
"reverse" => {
|
||||
// items.reverse() → reverse a copy
|
||||
format!(
|
||||
"{sig}.value = [...{sig}.value].reverse(); DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||
sig = signal_name
|
||||
)
|
||||
}
|
||||
"filter" => {
|
||||
// items.filter(fn) → filter in place
|
||||
let fn_js = args_js.first().map(|s| s.as_str()).unwrap_or("() => true");
|
||||
format!(
|
||||
"{sig}.value = {sig}.value.filter({fn_js}); DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||
sig = signal_name, fn_js = fn_js
|
||||
)
|
||||
}
|
||||
"map" => {
|
||||
// items.map(fn) → map in place
|
||||
let fn_js = args_js.first().map(|s| s.as_str()).unwrap_or("x => x");
|
||||
format!(
|
||||
"{sig}.value = {sig}.value.map({fn_js}); DS._streamDiff(\"{sig}\", {sig}.value)",
|
||||
sig = signal_name, fn_js = fn_js
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// Generic method call: obj.method(args)
|
||||
format!("{}.{}({})", obj_js, method, args_js.join(", "))
|
||||
|
|
|
|||
|
|
@ -353,6 +353,8 @@ pub enum AssignOp {
|
|||
Set,
|
||||
AddAssign,
|
||||
SubAssign,
|
||||
MulAssign,
|
||||
DivAssign,
|
||||
}
|
||||
|
||||
/// A UI element: `text label`, `button "+" { click: handler }`
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ pub enum TokenKind {
|
|||
Not, // !
|
||||
PlusEq, // +=
|
||||
MinusEq, // -=
|
||||
StarEq, // *=
|
||||
SlashEq, // /=
|
||||
Arrow, // ->
|
||||
Semicolon, // ;
|
||||
Pipe, // |
|
||||
|
|
@ -215,8 +217,10 @@ impl Lexer {
|
|||
'|' if self.peek_next() == '|' => { self.advance(); self.advance(); Token { kind: TokenKind::Or, lexeme: "||".into(), line, col } }
|
||||
'+' => { self.advance(); Token { kind: TokenKind::Plus, lexeme: "+".into(), line, col } }
|
||||
'-' => { self.advance(); Token { kind: TokenKind::Minus, lexeme: "-".into(), line, col } }
|
||||
'*' if self.peek_next() == '=' => { self.advance(); self.advance(); Token { kind: TokenKind::StarEq, lexeme: "*=".into(), line, col } }
|
||||
'*' => { self.advance(); Token { kind: TokenKind::Star, lexeme: "*".into(), line, col } }
|
||||
'/' if self.peek_next() == '/' => self.lex_comment(),
|
||||
'/' if self.peek_next() == '=' => { self.advance(); self.advance(); Token { kind: TokenKind::SlashEq, lexeme: "/=".into(), line, col } }
|
||||
'/' => { self.advance(); Token { kind: TokenKind::Slash, lexeme: "/".into(), line, col } }
|
||||
'%' => { self.advance(); Token { kind: TokenKind::Percent, lexeme: "%".into(), line, col } }
|
||||
'=' => { self.advance(); Token { kind: TokenKind::Eq, lexeme: "=".into(), line, col } }
|
||||
|
|
|
|||
|
|
@ -751,6 +751,16 @@ impl Parser {
|
|||
let value = self.parse_expr()?;
|
||||
Ok(Expr::Assign(Box::new(expr), AssignOp::SubAssign, Box::new(value)))
|
||||
}
|
||||
TokenKind::StarEq => {
|
||||
self.advance();
|
||||
let value = self.parse_expr()?;
|
||||
Ok(Expr::Assign(Box::new(expr), AssignOp::MulAssign, Box::new(value)))
|
||||
}
|
||||
TokenKind::SlashEq => {
|
||||
self.advance();
|
||||
let value = self.parse_expr()?;
|
||||
Ok(Expr::Assign(Box::new(expr), AssignOp::DivAssign, Box::new(value)))
|
||||
}
|
||||
_ => Ok(expr),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
examples/language-features.ds
Normal file
84
examples/language-features.ds
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
-- DreamStack Language Features Test
|
||||
-- Exercises all core language features in one place
|
||||
-- Tests: operators, assignment ops, array methods, computed signals,
|
||||
-- when/else, match, each, method calls, string interpolation
|
||||
|
||||
import { Card } from "../registry/components/card"
|
||||
import { Badge } from "../registry/components/badge"
|
||||
import { Button } from "../registry/components/button"
|
||||
|
||||
-- State
|
||||
let count = 10
|
||||
let items = ["Alpha", "Beta", "Gamma"]
|
||||
let loggedIn = false
|
||||
let mode = "normal"
|
||||
|
||||
-- Computed signals (derived)
|
||||
let doubled = count * 2
|
||||
let isHigh = count > 50
|
||||
|
||||
view test = column [
|
||||
|
||||
text "🧪 Language Features" { variant: "title" }
|
||||
text "Testing all operators and methods" { variant: "subtitle" }
|
||||
|
||||
-- 1. All assignment operators: +=, -=, *=, /=
|
||||
Card { title: "Assignment Operators", subtitle: "+= -= *= /=" } [
|
||||
text "count: {count} | doubled: {doubled}" { variant: "title" }
|
||||
row [
|
||||
Button { label: "+5", variant: "primary", onClick: count += 5 }
|
||||
Button { label: "-3", variant: "secondary", onClick: count -= 3 }
|
||||
Button { label: "×2", variant: "primary", onClick: count *= 2 }
|
||||
Button { label: "÷2", variant: "secondary", onClick: count /= 2 }
|
||||
Button { label: "=1", variant: "ghost", onClick: count = 1 }
|
||||
]
|
||||
]
|
||||
|
||||
-- 2. Boolean operators && || !
|
||||
Card { title: "Boolean Logic", subtitle: "&& || ! operators" } [
|
||||
Button { label: "Toggle Login", variant: "primary", onClick: loggedIn = !loggedIn }
|
||||
when loggedIn ->
|
||||
Badge { label: "Logged In ✓", variant: "success" }
|
||||
else ->
|
||||
Badge { label: "Logged Out", variant: "error" }
|
||||
]
|
||||
|
||||
-- 3. Array methods: push, pop, clear, sort, reverse
|
||||
Card { title: "Array Methods", subtitle: "push pop clear sort reverse" } [
|
||||
text "Items: {items}" { variant: "title" }
|
||||
text "{items}" { variant: "subtitle" }
|
||||
row [
|
||||
Button { label: "Push", variant: "primary", onClick: items.push("New") }
|
||||
Button { label: "Pop", variant: "secondary", onClick: items.pop() }
|
||||
Button { label: "Sort", variant: "ghost", onClick: items.sort() }
|
||||
Button { label: "Reverse", variant: "ghost", onClick: items.reverse() }
|
||||
Button { label: "Clear", variant: "destructive", onClick: items.clear() }
|
||||
]
|
||||
each item in items ->
|
||||
row [
|
||||
text "→"
|
||||
text item
|
||||
]
|
||||
]
|
||||
|
||||
-- 4. Match expressions
|
||||
Card { title: "Pattern Matching", subtitle: "match with fall-through" } [
|
||||
row [
|
||||
Button { label: "Normal", variant: "secondary", onClick: mode = "normal" }
|
||||
Button { label: "Turbo", variant: "primary", onClick: mode = "turbo" }
|
||||
Button { label: "Zen", variant: "ghost", onClick: mode = "zen" }
|
||||
]
|
||||
match mode
|
||||
"turbo" -> Badge { label: "🔥 TURBO", variant: "warning" }
|
||||
"zen" -> Badge { label: "🧘 ZEN", variant: "info" }
|
||||
_ -> Badge { label: "📝 Normal", variant: "default" }
|
||||
]
|
||||
|
||||
-- 5. Comparison operators
|
||||
Card { title: "Comparisons", subtitle: "> < >= <= == !=" } [
|
||||
text "count = {count}"
|
||||
when count > 50 -> Badge { label: "HIGH (>50)", variant: "warning" }
|
||||
when count > 20 -> Badge { label: "MEDIUM (>20)", variant: "info" }
|
||||
else -> Badge { label: "LOW (≤20)", variant: "success" }
|
||||
]
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue