Berry animation DSL: fix transpilation of complex expressions (#23829)

This commit is contained in:
s-hadinger 2025-08-25 20:54:37 +02:00 committed by GitHub
parent 57d2225a97
commit 9aaed29a54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 658 additions and 492 deletions

View File

@ -12,8 +12,8 @@ animation comet_main = comet_animation(
color=0xFFFFFF # White head
tail_length=10 # tail length
speed=2s # speed
priority = 7
)
comet_main.priority = 7
# Secondary comet in different color, opposite direction
animation comet_secondary = comet_animation(
@ -21,16 +21,16 @@ animation comet_secondary = comet_animation(
tail_length=8 # shorter tail
speed=3s # slower speed
direction=-1 # other direction
priority = 5
)
comet_secondary.priority = 5
# Add sparkle trail behind comets but on top of blue background
animation comet_sparkles = twinkle_animation(
color=0xAAAAFF # Light blue sparkles
density=8 # density (moderate sparkles)
twinkle_speed=400ms # twinkle speed (quick sparkle)
priority = 8
)
comet_sparkles.priority = 8
# Start all animations
run background

View File

@ -20,8 +20,8 @@
# color=0xFFFFFF # White head
# tail_length=10 # tail length
# speed=2s # speed
# priority = 7
# )
# comet_main.priority = 7
#
# # Secondary comet in different color, opposite direction
# animation comet_secondary = comet_animation(
@ -29,16 +29,16 @@
# tail_length=8 # shorter tail
# speed=3s # slower speed
# direction=-1 # other direction
# priority = 5
# )
# comet_secondary.priority = 5
#
# # Add sparkle trail behind comets but on top of blue background
# animation comet_sparkles = twinkle_animation(
# color=0xAAAAFF # Light blue sparkles
# density=8 # density (moderate sparkles)
# twinkle_speed=400ms # twinkle speed (quick sparkle)
# priority = 8
# )
# comet_sparkles.priority = 8
#
# # Start all animations
# run background

View File

@ -24,7 +24,7 @@
# set base_speed = 2.0
# animation stream2 = comet_animation(
# color=blue
# tail_length=strip_len / 8 + 2 # computed with addition
# tail_length=strip_len / 8 + (2 * strip_len) -10 # computed with addition
# speed=base_speed * 1.5 # computed with multiplication
# direction=-1
# priority=5
@ -51,14 +51,14 @@ var strip_len_ = temp_strip_length_11
# Create animation with computed values
var stream1_ = animation.comet_animation(engine)
stream1_.color = 0xFFFF0000
stream1_.tail_length = animation.create_closure_value(engine, def (self, param_name, time_ms) return (self.abs(self.resolve(self.resolve(strip_len_, param_name, time_ms), param_name, time_ms) / 4)) end) # computed value
stream1_.tail_length = animation.create_closure_value(engine, def (self, param_name, time_ms) return (self.abs(self.resolve(strip_len_, param_name, time_ms) / 4)) end) # computed value
stream1_.speed = 1.5
stream1_.priority = 10
# More complex computed values
var base_speed_ = 2.0
var stream2_ = animation.comet_animation(engine)
stream2_.color = 0xFF0000FF
stream2_.tail_length = animation.create_closure_value(engine, def (self, param_name, time_ms) return (self.resolve(strip_len_, param_name, time_ms) / 8 + 2) end) # computed with addition
stream2_.tail_length = animation.create_closure_value(engine, def (self, param_name, time_ms) return (self.resolve(strip_len_, param_name, time_ms) / 8 + (2 * self.resolve(strip_len_, param_name, time_ms)) - 10) end) # computed with addition
stream2_.speed = animation.create_closure_value(engine, def (self, param_name, time_ms) return (self.resolve(base_speed_, param_name, time_ms) * 1.5) end) # computed with multiplication
stream2_.direction = (-1)
stream2_.priority = 5

View File

@ -16,7 +16,7 @@ animation stream1 = comet_animation(
set base_speed = 2.0
animation stream2 = comet_animation(
color=blue
tail_length=strip_len / 8 + 2 # computed with addition
tail_length=strip_len / 8 + (2 * strip_len) -10 # computed with addition
speed=base_speed * 1.5 # computed with multiplication
direction=-1
priority=5

View File

@ -561,32 +561,32 @@ class SimpleDSLTranspiler
# Process any value - unified approach
def process_value(context)
return self.process_expression(context)
return self.process_expression(context) # This calls process_additive_expression with is_top_level=true
end
# Process expressions with arithmetic operations
def process_expression(context)
return self.process_additive_expression(context)
return self.process_additive_expression(context, true) # true = top-level expression
end
# Process additive expressions (+ and -)
def process_additive_expression(context)
var left = self.process_multiplicative_expression(context)
def process_additive_expression(context, is_top_level)
var left = self.process_multiplicative_expression(context, is_top_level)
while !self.at_end()
var tok = self.current()
if tok != nil && (tok.type == animation_dsl.Token.PLUS || tok.type == animation_dsl.Token.MINUS)
var op = tok.value
self.next() # consume operator
var right = self.process_multiplicative_expression(context)
var right = self.process_multiplicative_expression(context, false) # sub-expressions are not top-level
left = f"{left} {op} {right}"
else
break
end
end
# Check if the entire expression needs a closure (after building the full expression)
if self.is_computed_expression_string(left)
# Only create closures at the top level
if is_top_level && self.is_computed_expression_string(left)
return self.create_computation_closure_from_string(left)
else
return left
@ -594,15 +594,15 @@ class SimpleDSLTranspiler
end
# Process multiplicative expressions (* and /)
def process_multiplicative_expression(context)
var left = self.process_unary_expression(context)
def process_multiplicative_expression(context, is_top_level)
var left = self.process_unary_expression(context, is_top_level)
while !self.at_end()
var tok = self.current()
if tok != nil && (tok.type == animation_dsl.Token.MULTIPLY || tok.type == animation_dsl.Token.DIVIDE)
var op = tok.value
self.next() # consume operator
var right = self.process_unary_expression(context)
var right = self.process_unary_expression(context, false) # sub-expressions are not top-level
left = f"{left} {op} {right}"
else
break
@ -613,7 +613,7 @@ class SimpleDSLTranspiler
end
# Process unary expressions (- and +)
def process_unary_expression(context)
def process_unary_expression(context, is_top_level)
var tok = self.current()
if tok == nil
self.error("Expected value")
@ -623,21 +623,21 @@ class SimpleDSLTranspiler
# Handle unary minus for negative numbers
if tok.type == animation_dsl.Token.MINUS
self.next() # consume the minus
var expr = self.process_unary_expression(context)
var expr = self.process_unary_expression(context, false) # sub-expressions are not top-level
return f"(-{expr})"
end
# Handle unary plus (optional)
if tok.type == animation_dsl.Token.PLUS
self.next() # consume the plus
return self.process_unary_expression(context)
return self.process_unary_expression(context, false) # sub-expressions are not top-level
end
return self.process_primary_expression(context)
return self.process_primary_expression(context, is_top_level)
end
# Process primary expressions (literals, identifiers, function calls, parentheses)
def process_primary_expression(context)
def process_primary_expression(context, is_top_level)
var tok = self.current()
if tok == nil
self.error("Expected value")
@ -647,7 +647,7 @@ class SimpleDSLTranspiler
# Parenthesized expression
if tok.type == animation_dsl.Token.LEFT_PAREN
self.next() # consume '('
var expr = self.process_expression(context)
var expr = self.process_additive_expression(context, false) # parenthesized expressions are not top-level
self.expect_right_paren()
return f"({expr})"
end
@ -894,15 +894,22 @@ class SimpleDSLTranspiler
start_pos -= 1
end
# Check if this is a user variable (not preceded by "animation." or "self.")
# Check if this is a user variable (not preceded by "animation." or "self." or already inside a resolve call)
var is_user_var = true
if start_pos >= 10
if start_pos >= 13
var check_start = start_pos >= 13 ? start_pos - 13 : 0
var prefix = result[check_start..start_pos-1]
if string.find(prefix, "self.resolve(") >= 0
is_user_var = false
end
end
if is_user_var && start_pos >= 10
var check_start = start_pos >= 10 ? start_pos - 10 : 0
var prefix = result[check_start..start_pos-1]
if string.find(prefix, "animation.") >= 0 || string.find(prefix, "self.") >= 0
is_user_var = false
end
elif start_pos >= 5
elif is_user_var && start_pos >= 5
var check_start = start_pos >= 5 ? start_pos - 5 : 0
var prefix = result[check_start..start_pos-1]
if string.find(prefix, "self.") >= 0

View File

@ -187,7 +187,7 @@ end
# @param closure: function - the closure to evaluate at run-time
# @return ClosureValueProvider - New ClosureValueProvider instance
def create_closure_value(engine, closure)
var provider = animation.closure_value(engine)
var provider = ClosureValueProvider(engine)
provider.closure = closure
return provider
end

View File

@ -6794,34 +6794,33 @@ be_local_closure(shift_fast_scroll, /* name */
);
/*******************************************************************/
--> Unsupported upvals in closure in 'create_closure_value' <---
/********************************************************************
** Solidified function: create_closure_value
********************************************************************/
be_local_closure(create_closure_value, /* name */
be_nested_proto(
5, /* nstack */
4, /* nstack */
2, /* argc */
0, /* varg */
0, /* has upvals */
NULL, /* no upvals */
1, /* has upvals */
( &(const bupvaldesc[ 1]) { /* upvals */
be_local_const_upval(1, 0),
}),
0, /* has sup protos */
NULL, /* no sub protos */
1, /* has constants */
( &(const bvalue[ 3]) { /* constants */
/* K0 */ be_nested_str_weak(animation),
/* K1 */ be_nested_str_weak(closure_value),
/* K2 */ be_nested_str_weak(closure),
( &(const bvalue[ 1]) { /* constants */
/* K0 */ be_nested_str_weak(closure),
}),
be_str_weak(create_closure_value),
&be_const_str_solidified,
( &(const binstruction[ 6]) { /* code */
0xB80A0000, // 0000 GETNGBL R2 K0
0x8C080501, // 0001 GETMET R2 R2 K1
0x5C100000, // 0002 MOVE R4 R0
0x7C080400, // 0003 CALL R2 2
0x900A0401, // 0004 SETMBR R2 K2 R1
0x80040400, // 0005 RET 1 R2
( &(const binstruction[ 5]) { /* code */
0x68080000, // 0000 GETUPV R2 U0
0x5C0C0000, // 0001 MOVE R3 R0
0x7C080200, // 0002 CALL R2 1
0x900A0001, // 0003 SETMBR R2 K0 R1
0x80040400, // 0004 RET 1 R2
})
)
);

View File

@ -290,6 +290,132 @@ def test_variable_assignments()
return true
end
# Test computed values and expressions (regression tests)
def test_computed_values()
print("Testing computed values and expressions...")
# Test computed values with single resolve calls (regression test for double resolve issue)
var computed_dsl = "set strip_len = strip_length()\n" +
"animation stream1 = comet_animation(\n" +
" color=red\n" +
" tail_length=abs(strip_len / 4)\n" +
" speed=1.5\n" +
" priority=10\n" +
")"
var computed_code = animation_dsl.compile(computed_dsl)
assert(computed_code != nil, "Should compile computed values")
# Check for single resolve calls (no double wrapping)
var expected_single_resolve = "self.abs(self.resolve(strip_len_, param_name, time_ms) / 4)"
assert(string.find(computed_code, expected_single_resolve) >= 0, "Should generate single resolve call in computed expression")
# Check that there are no double resolve calls
var double_resolve_count = 0
var pos = 0
while true
pos = string.find(computed_code, "self.resolve(self.resolve(", pos)
if pos < 0
break
end
double_resolve_count += 1
pos += 1
end
assert(double_resolve_count == 0, f"Should have no double resolve calls, found {double_resolve_count}")
# Test complex expressions with single closure (regression test for nested closure issue)
var complex_expr_dsl = "set strip_len = strip_length()\n" +
"set base_value = 5\n" +
"animation stream2 = comet_animation(\n" +
" color=blue\n" +
" tail_length=strip_len / 8 + (2 * strip_len) - 10\n" +
" speed=(base_value + strip_len) * 2.5\n" +
" priority=max(1, min(10, strip_len / 6))\n" +
")"
var complex_code = animation_dsl.compile(complex_expr_dsl)
assert(complex_code != nil, "Should compile complex expressions")
# Count closure creations - each computed parameter should have exactly one closure
var closure_count = 0
pos = 0
while true
pos = string.find(complex_code, "animation.create_closure_value(", pos)
if pos < 0
break
end
closure_count += 1
pos += 1
end
assert(closure_count == 3, f"Should have exactly 3 closures for 3 computed parameters, found {closure_count}")
# Check that complex expressions are in single closures (no nested closures)
var nested_closure_count = 0
pos = 0
while true
# Look for closure inside closure pattern
var closure_start = string.find(complex_code, "animation.create_closure_value(", pos)
if closure_start < 0
break
end
var closure_end = string.find(complex_code, ") end)", closure_start)
if closure_end < 0
break
end
var closure_content = complex_code[closure_start..closure_end]
if string.find(closure_content, "animation.create_closure_value(") > 0
nested_closure_count += 1
end
pos = closure_end + 1
end
assert(nested_closure_count == 0, f"Should have no nested closures, found {nested_closure_count}")
# Verify specific complex expression patterns
var expected_complex_tail = "self.resolve(strip_len_, param_name, time_ms) / 8 + (2 * self.resolve(strip_len_, param_name, time_ms)) - 10"
assert(string.find(complex_code, expected_complex_tail) >= 0, "Should generate correct complex tail_length expression")
var expected_complex_speed = "(self.resolve(base_value_, param_name, time_ms) + self.resolve(strip_len_, param_name, time_ms)) * 2.5"
assert(string.find(complex_code, expected_complex_speed) >= 0, "Should generate correct complex speed expression")
var expected_complex_priority = "self.max(1, self.min(10, self.resolve(strip_len_, param_name, time_ms) / 6))"
assert(string.find(complex_code, expected_complex_priority) >= 0, "Should generate correct complex priority expression with math functions")
# Test simple expressions that don't need closures
var simple_expr_dsl = "set strip_len = strip_length()\n" +
"animation simple = comet_animation(\n" +
" color=red\n" +
" tail_length=strip_len\n" +
" speed=1.5\n" +
" priority=10\n" +
")"
var simple_code = animation_dsl.compile(simple_expr_dsl)
assert(simple_code != nil, "Should compile simple expressions")
# Simple variable reference should not create a closure
assert(string.find(simple_code, "simple_.tail_length = strip_len_") >= 0, "Should generate direct variable reference without closure")
# Test mathematical functions in computed expressions
var math_expr_dsl = "set strip_len = strip_length()\n" +
"animation math_test = comet_animation(\n" +
" color=red\n" +
" tail_length=max(1, min(strip_len, 20))\n" +
" speed=abs(strip_len - 30)\n" +
" priority=round(strip_len / 6)\n" +
")"
var math_code = animation_dsl.compile(math_expr_dsl)
assert(math_code != nil, "Should compile mathematical expressions")
# Check that mathematical functions are prefixed with self. in closures
assert(string.find(math_code, "self.max(1, self.min(") >= 0, "Should prefix math functions with self. in closures")
assert(string.find(math_code, "self.abs(") >= 0, "Should prefix abs function with self. in closures")
assert(string.find(math_code, "self.round(") >= 0, "Should prefix round function with self. in closures")
print("✓ Computed values test passed")
return true
end
# Test error handling
def test_error_handling()
print("Testing error handling...")
@ -758,6 +884,7 @@ def run_dsl_transpiler_tests()
test_sequences,
test_multiple_run_statements,
test_variable_assignments,
test_computed_values,
test_error_handling,
test_forward_references,
test_complex_dsl,