fix: dot & index in var
Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
parent
2ed4d1dcc3
commit
ddcbe1b316
7 changed files with 1623 additions and 834 deletions
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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("e_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("e_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("e_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("e_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""#);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue