fix: dot & index in var

Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
Pakin 2026-05-25 14:19:30 +07:00
parent 2ed4d1dcc3
commit ddcbe1b316
7 changed files with 1623 additions and 834 deletions

View file

@ -10,6 +10,17 @@ pub enum Statement {
value: Option<Expression>,
assignment_type: AssignmentType,
},
VarDotAssign {
object: String,
property: String,
value: Expression,
},
VarIndexAssign {
name: String,
indices: Vec<Expression>,
value: Expression,
assignment_type: AssignmentType,
},
IfStmt {
condition: Expression,
then_branch: Vec<Statement>,

View file

@ -93,5 +93,17 @@ fn walk_statement(codegen: &dyn CodeGen, stmt: &Statement) -> String {
let rendered_args: Vec<String> = args.iter().map(|a| codegen.generate_expression(a)).collect();
codegen.generate_call(name, &rendered_args)
}
Statement::VarDotAssign { object, property, value } => {
let v = codegen.generate_expression(value);
codegen.generate_var_decl(&format!("{}.{}", object, property), Some(&v), AssignmentType::Equals)
}
Statement::VarIndexAssign { name, indices, value, assignment_type } => {
let v = codegen.generate_expression(value);
let idx_str: String = indices.iter().map(|i| format!("[{}]", codegen.generate_expression(i))).collect();
match assignment_type {
AssignmentType::Equals => codegen.generate_var_decl(&format!("{}{}", name, idx_str), Some(&v), AssignmentType::Equals),
AssignmentType::NotAssigned => codegen.generate_var_decl(&format!("{}{}", name, idx_str), Some(&v), AssignmentType::NotAssigned),
}
}
}
}

View file

@ -7,11 +7,44 @@ pub Program: Program = {
};
Stmt: Statement = {
"Var" <name:Ident> "." <prop:Ident> "=" <v:Expr> => Statement::VarDotAssign {
object: name,
property: prop,
value: v,
},
"Var" "#" <s:STRING> "." <prop:Ident> "=" <v:Expr> => Statement::VarDotAssign {
object: s,
property: prop,
value: v,
},
"Var" <name:Ident> "[" <idx:Expr> "]" "=" <v:Expr> => Statement::VarIndexAssign {
name,
indices: vec![idx],
value: v,
assignment_type: AssignmentType::Equals,
},
"Var" <name:Ident> "[" <idx:Expr> "]" "[" <idx2:Expr> "]" "=" <v:Expr> => Statement::VarIndexAssign {
name,
indices: vec![idx, idx2],
value: v,
assignment_type: AssignmentType::Equals,
},
"Var" <name:Ident> "[" <idx:Expr> "]" "!" "assigned" <v:Expr> => Statement::VarIndexAssign {
name,
indices: vec![idx],
value: v,
assignment_type: AssignmentType::NotAssigned,
},
"Var" <name:Ident> "=" <v:Expr> => Statement::VarDecl {
name,
value: Some(v),
assignment_type: AssignmentType::Equals,
},
"Var" "#" <s:STRING> "=" <v:Expr> => Statement::VarDecl {
name: s,
value: Some(v),
assignment_type: AssignmentType::Equals,
},
"Var" <name:Ident> "!" "assigned" => Statement::VarDecl {
name,
value: None,
@ -33,8 +66,11 @@ Stmt: Statement = {
body,
},
"DEBUGVAR" <name:Ident> => Statement::DebugVar { name },
"DEBUGVAR" <name:Ident> "[" <_expr:Expr> "]" => Statement::DebugVar { name },
"DEBUGVAR" "#" <s:STRING> => Statement::DebugVar { name: s },
"Open" <filename:STRING> => Statement::Open { filename },
<name:Ident> "(" <args:Comma<Expr>?> ")" => Statement::Call { name, args: args.unwrap_or_default() },
"#" <s:STRING> "(" <args:Comma<Expr>?> ")" => Statement::Call { name: s, args: args.unwrap_or_default() },
};
Expr: Expression = {
@ -93,6 +129,7 @@ AtomExpr: Expression = {
"True" => Expression::Literal(LiteralValue::Bool(true)),
"False" => Expression::Literal(LiteralValue::Bool(false)),
<name:Ident> "(" <args:Comma<Expr>?> ")" => Expression::FunctionCall { name, args: args.unwrap_or_default() },
"#" <s:STRING> => Expression::Identifier(s),
"$" <name:Ident> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", name, prop), is_negative: false }) },
"$" <s:STRING> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", s, prop), is_negative: false }) },
"$" "-" <name:Ident> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", name, prop), is_negative: true }) },
@ -161,6 +198,7 @@ match {
"!",
"$",
"@",
"#",
"(",
")",
",",
@ -172,6 +210,8 @@ match {
">",
".",
"=",
"[",
"]",
} else {
r"[a-zA-Z_][a-zA-Z0-9_#]*",
}

File diff suppressed because it is too large Load diff

View file

@ -350,8 +350,144 @@ CacheVarStr "get" XMLProfile
}
#[test]
fn test_neq_in_condition() {
let result = ScriptParser::parse("If x != \"\" Then Var y = 1 EndIf");
fn test_var_dot_assign() {
let result = ScriptParser::parse("Var Seeker.thankLidFlag = 0");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_var_index_assign() {
let result = ScriptParser::parse("Var PriceMain[0] = $12-01-01-0003.Price");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_var_2d_index_assign() {
let result = ScriptParser::parse(r#"Var NameLang[0][0] = "HOT Americano""#);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_var_index_not_assigned() {
let result = ScriptParser::parse("Var PriceTag[0] !assigned StringFmt(1, 2)");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_if_else_with_call() {
let script = r#"If TaobinPremiumEnable = 1 Then
Else
RootLayoutVisible(3, "hide")
EndIf"#;
let result = ScriptParser::parse(script);
if let Err(ref e) = result {
eprintln!("ERROR: {}", e);
}
assert!(result.is_ok(), "parse failed");
}
#[test]
fn test_multiple_if_blocks() {
let result = ScriptParser::parse(
"If x = 1 Then Var y = 2 EndIf\nIf z = 3 Then Var w = 4 EndIf"
);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
assert_eq!(program.statements.len(), 2);
}
#[test]
fn test_if_with_empty_then_else() {
let result = ScriptParser::parse(
"If x = 1 Then\nElse\nVar y = 2\nEndIf"
);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_hash_quoted_ident() {
let script = r#"STRCONTAIN("1037", MaterialAvailable, #"7UpSyrupEnable")"#;
let parser = ProgramParser::new();
let result = parser.parse(script);
if let Err(e) = result {
let err = crate::xml::error::ParseError::from_lalrpop_error(script, e);
eprintln!("ERROR for quoted ident: {}", err);
panic!("parse failed");
}
}
#[test]
fn test_hyphenated_action_call() {
let script = r#"#"SET-MENU-SHOW"("Hot", 1)"#;
let parser = ProgramParser::new();
let result = parser.parse(script);
if let Err(e) = result {
let err = crate::xml::error::ParseError::from_lalrpop_error(script, e);
eprintln!("ERROR for hyphenated call: {}", err);
panic!("parse failed");
}
}
#[test]
fn test_led_action_call() {
let result = ScriptParser::parse(r#"LEDv2("LedDoorCupV2", "Off", 255, 194, 166, 20, 6)"#);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
match &program.statements[0] {
Statement::Call { name, args } => {
assert_eq!(name, "LEDv2");
assert_eq!(args.len(), 7);
}
_ => panic!("Expected Call statement"),
}
}
#[test]
fn test_read_file_and_strcontain() {
let script = r#"VAR cock_tail_str ""
VAR cock_tail_enable ""
READ_FILE("/mnt/sdcard/cock_tail_enable", cock_tail_str)
STRCONTAIN("1", cock_tail_str, cock_tail_enable)
If cock_tail_enable = "true" Then
Var WheyShow = "false"
EndIf"#;
let result = ScriptParser::parse(script);
if let Err(ref e) = result {
eprintln!("Parse error: {:?}", e);
}
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_strcontain_with_if_block() {
let script = r#"Var cock_tail_str = ""
Var BeerTrapEnable = ""
READ_FILE("/mnt/sdcard/cock_tail_enable", cock_tail_str)
STRCONTAIN("1", cock_tail_str, cock_tail_enable)
If cock_tail_enable = "true" Then
Var WheyShow = "false"
Var CocktailShow = "true"
Var RoadShow = "true"
STRCONTAIN("1401", MaterialAvailable, BeerTrapEnable)
Else
Var WheyShow = "true"
Var CocktailShow = "false"
EndIf"#;
let result = ScriptParser::parse(script);
if let Err(ref e) = result {
eprintln!("ERROR: {}", e);
}
assert!(result.is_ok(), "parse failed");
}
#[test]
fn test_cmd_with_dashes() {
// __CMD is a valid identifier (no hyphens, starts with _)
let script = r#"__CMD("mcu-version", "-", "-", "-")"#;
let result = ScriptParser::parse(script);
if let Err(ref e) = result {
eprintln!("ERROR: {}", e);
}
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
@ -363,7 +499,18 @@ CacheVarStr "get" XMLProfile
let end = content.find("</EventOpen>").unwrap();
let script = &content[start + 12..end].trim();
let result = ScriptParser::parse(script);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let preprocessed = preprocess::preprocess(script);
let parser = ProgramParser::new();
match parser.parse(&preprocessed) {
Ok(prog) => {
// Successfully parsed! Check we got a reasonable number of statements
assert!(prog.statements.len() > 0, "No statements parsed");
}
Err(e) => {
let err = crate::xml::error::ParseError::from_lalrpop_error(&preprocessed, e);
std::fs::write("/tmp/page_board_parse_error.txt", format!("{}", err)).unwrap();
panic!("LALRPOP parse error - written to /tmp/page_board_parse_error.txt");
}
}
}
}

View file

@ -6,7 +6,7 @@ const KEYWORDS: &[&str] = &[
];
fn is_keyword(word: &str) -> bool {
KEYWORDS.contains(&word)
KEYWORDS.contains(&word) || word.eq_ignore_ascii_case("EndIf")
}
fn strip_comments(source: &str) -> String {
@ -44,7 +44,7 @@ fn strip_comment_from_line(line: &str) -> String {
result
}
fn needs_quoting(name: &str) -> bool {
fn needs_dollar_quoting(name: &str) -> bool {
name.contains('-') || name.starts_with(|c: char| c.is_ascii_digit())
}
@ -52,8 +52,22 @@ fn normalize_dollar_vars(text: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
let mut in_string = false;
while i < chars.len() {
if chars[i] == '"' {
in_string = !in_string;
result.push(chars[i]);
i += 1;
continue;
}
if in_string {
result.push(chars[i]);
i += 1;
continue;
}
if chars[i] == '$' {
i += 1;
let neg = i < chars.len() && chars[i] == '-';
@ -80,7 +94,7 @@ fn normalize_dollar_vars(text: &str) -> String {
} else {
write!(result, "@\"{}\".{}", name, prop).unwrap();
}
} else if needs_quoting(&name) {
} else if needs_dollar_quoting(&name) {
if neg {
write!(result, "$-\"{}\"", name).unwrap();
} else {
@ -104,6 +118,179 @@ fn is_action_start(word: &str) -> bool {
!is_keyword(word) && !word.is_empty()
}
fn needs_quoting(word: &str) -> bool {
(word.starts_with(|c: char| c.is_ascii_digit()) && word.chars().any(|c: char| c.is_ascii_alphabetic()))
|| word.contains('-')
}
fn quote_identifier(word: &str) -> String {
format!("#\"{}\"", word)
}
fn normalize_digit_idents(text: &str) -> String {
let mut result = String::new();
let mut in_string = false;
let mut word_start = None;
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if ch == '"' {
if word_start.is_some() {
let start = word_start.unwrap();
let word: String = chars[start..i].iter().collect();
if needs_quoting(&word) {
result.push_str(&quote_identifier(&word));
} else {
result.push_str(&word);
}
word_start = None;
}
if in_string {
in_string = false;
result.push(ch);
} else {
in_string = true;
result.push(ch);
}
i += 1;
continue;
}
if in_string {
result.push(ch);
i += 1;
continue;
}
// Skip dollar-variable patterns: $var, $-var, $"var", $-"var"
if ch == '$' || ch == '@' {
if word_start.is_some() {
let start = word_start.unwrap();
let word: String = chars[start..i].iter().collect();
if needs_quoting(&word) {
result.push_str(&quote_identifier(&word));
} else {
result.push_str(&word);
}
word_start = None;
}
result.push(ch);
i += 1;
// Skip optional '-' for negative vars
if i < chars.len() && chars[i] == '-' {
result.push(chars[i]);
i += 1;
}
// Skip over quoted var names like "var-name"
if i < chars.len() && chars[i] == '"' {
result.push(chars[i]);
i += 1;
while i < chars.len() && chars[i] != '"' {
result.push(chars[i]);
i += 1;
}
if i < chars.len() {
result.push(chars[i]); // closing "
i += 1;
}
// Skip .property after $"var" or @"var"
if i < chars.len() && chars[i] == '.' {
result.push(chars[i]);
i += 1;
// Skip property name (possibly quoted)
if i < chars.len() && chars[i] == '"' {
result.push(chars[i]);
i += 1;
while i < chars.len() && chars[i] != '"' {
result.push(chars[i]);
i += 1;
}
if i < chars.len() {
result.push(chars[i]);
i += 1;
}
} else {
// Unquoted property: alphanumeric/_
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '#') {
result.push(chars[i]);
i += 1;
}
}
}
} else {
// Unquoted var name: alphanumeric/hyphen/_ until separator
// But we already handled $- above, so skip to letter/digit/_
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '#') {
result.push(chars[i]);
i += 1;
}
// Check for .property
if i < chars.len() && chars[i] == '.' {
result.push(chars[i]);
i += 1;
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '#') {
result.push(chars[i]);
i += 1;
}
}
}
continue;
}
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '#' {
if word_start.is_none() {
word_start = Some(i);
}
i += 1;
continue;
}
// Hyphen: part of a word if preceded by word chars (like SET-MENU-SHOW)
if ch == '-' {
if word_start.is_some() {
// Continue the word through hyphens
i += 1;
continue;
}
// Not part of a word, just output it
result.push(ch);
i += 1;
continue;
}
if let Some(start) = word_start {
let word: String = chars[start..i].iter().collect();
if needs_quoting(&word) {
result.push_str(&quote_identifier(&word));
} else {
result.push_str(&word);
}
word_start = None;
}
result.push(ch);
i += 1;
}
if let Some(start) = word_start {
let word: String = chars[start..].iter().collect();
if needs_quoting(&word) {
result.push_str(&quote_identifier(&word));
} else {
result.push_str(&word);
}
}
result
}
fn normalize_keywords(line: &str) -> String {
line.replace("Endif", "EndIf")
}
fn tokenize_action_args(rest: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
@ -134,19 +321,19 @@ fn tokenize_action_args(rest: &str) -> Vec<String> {
paren_depth -= 1;
}
if paren_depth == 0 && !current.trim().is_empty() {
args.push(current.trim().to_string());
args.push(tokenize_arg(current.trim()));
current.clear();
}
}
' ' | '\t' if paren_depth == 0 => {
if !current.is_empty() {
args.push(current.trim().to_string());
args.push(tokenize_arg(current.trim()));
current.clear();
}
}
',' if paren_depth == 0 => {
if !current.is_empty() {
args.push(current.trim().to_string());
args.push(tokenize_arg(current.trim()));
current.clear();
}
}
@ -157,12 +344,23 @@ fn tokenize_action_args(rest: &str) -> Vec<String> {
}
if !current.trim().is_empty() {
args.push(current.trim().to_string());
args.push(tokenize_arg(current.trim()));
}
args
}
fn tokenize_arg(arg: &str) -> String {
// Don't quote string literals or numbers
if arg.starts_with('"') || arg.parse::<f64>().is_ok() {
normalize_dollar_vars(arg).to_string()
} else if needs_quoting(arg) {
quote_identifier(arg)
} else {
normalize_dollar_vars(arg).to_string()
}
}
fn handle_var_line(line: &str) -> String {
let line = normalize_dollar_vars(line);
let trimmed = line.trim();
@ -207,6 +405,8 @@ fn handle_var_line(line: &str) -> String {
fn normalize_line(line: &str) -> String {
let line = normalize_dollar_vars(line);
let line = normalize_digit_idents(&line);
let line = normalize_keywords(&line);
let trimmed = line.trim();
if trimmed.is_empty() {
@ -358,4 +558,39 @@ mod tests {
fn test_dollar_var_negative_with_property() {
assert_eq!(normalize_dollar_vars("$-12-01-01-0003.Price"), "$-\"12-01-01-0003\".Price");
}
#[test]
fn test_digit_quoting() {
assert_eq!(normalize_digit_idents("7UpSyrupEnable"), "#\"7UpSyrupEnable\"");
assert_eq!(normalize_digit_idents("SET-MENU-SHOW"), "#\"SET-MENU-SHOW\"");
assert_eq!(normalize_digit_idents("normalVar"), "normalVar");
assert_eq!(normalize_digit_idents("__CMD"), "__CMD");
assert_eq!(normalize_digit_idents("$-myVar"), "$-myVar");
}
#[test]
fn test_normalize_hyphen_ident() {
assert_eq!(normalize_line("SET-MENU-SHOW \"Hot\" 1"), "#\"SET-MENU-SHOW\"(\"Hot\", 1)");
}
#[test]
fn test_normalize_digit_in_function_arg() {
assert_eq!(normalize_line("STRCONTAIN \"1037\" MaterialAvailable 7UpSyrupEnable"), "STRCONTAIN(\"1037\", MaterialAvailable, #\"7UpSyrupEnable\")");
}
#[test]
fn test_normalize_cmd_action() {
assert_eq!(normalize_line("__CMD \"mcu-version\" \"-\" \"-\" \"-\""), "__CMD(\"mcu-version\", \"-\", \"-\", \"-\")");
}
#[test]
fn test_normalize_endif_lowercase() {
assert_eq!(normalize_line("Endif"), "EndIf");
}
#[test]
fn test_dollar_inside_string_preserved() {
assert_eq!(normalize_dollar_vars(r#"Var x = "HK$""#), r#"Var x = "HK$""#);
assert_eq!(normalize_dollar_vars(r#"$var = "price is $5""#), r#"$var = "price is $5""#);
}
}

View file

@ -452,6 +452,40 @@ impl Vm {
}
Ok(())
}
Statement::VarDotAssign { object, property, value } => {
let val = self.eval_expr(value)?;
let key = format!("{}.{}", object, property);
self.globals.insert(key, val);
Ok(())
}
Statement::VarIndexAssign { name, indices, value, assignment_type } => {
let val = self.eval_expr(value)?;
if indices.len() == 1 {
let idx = self.eval_expr(&indices[0])?;
let idx_key = match idx {
Value::Number(n) => format!("{}[{}]", name, n as i64),
Value::String(s) => format!("{}[{}]", name, s),
_ => format!("{}[{:?}]", name, idx),
};
if *assignment_type == AssignmentType::NotAssigned {
self.globals.entry(idx_key).or_insert(val);
} else {
self.globals.insert(idx_key, val);
}
} else {
let idx_parts: VmResult<Vec<String>> = indices.iter().map(|i| {
self.eval_expr(i).map(|v| match v {
Value::Number(n) => format!("{}", n as i64),
Value::String(s) => s,
_ => format!("{:?}", v),
})
}).collect();
let idx_parts = idx_parts?;
let idx_key = format!("{}[{}]", name, idx_parts.join("]["));
self.globals.insert(idx_key, val);
}
Ok(())
}
}
}