Berry animation transpiler refactoring (#23890)

This commit is contained in:
s-hadinger 2025-09-08 23:30:57 +02:00 committed by GitHub
parent e865041c4b
commit 1eb38f0b78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 29447 additions and 23182 deletions

View File

@ -1,36 +0,0 @@
# Aurora Borealis - Northern lights simulation
# Flowing green and purple aurora colors
#strip length 60
# Define aurora color palette
palette aurora_colors = [
(0, 0x000022), # Dark night sky
(64, 0x004400), # Dark green
(128, 0x00AA44), # Aurora green
(192, 0x44AA88), # Light green
(255, 0x88FFAA) # Bright aurora
]
# Secondary purple palette
palette aurora_purple = [
(0, 0x220022), # Dark purple
(64, 0x440044), # Medium purple
(128, 0x8800AA), # Bright purple
(192, 0xAA44CC), # Light purple
(255, 0xCCAAFF) # Pale purple
]
# Base aurora animation with slow flowing colors
animation aurora_base = rich_palette_animation(
palette=aurora_colors # palette
cycle_period=10s # cycle period
transition_type=SINE # transition type (explicit for clarity)
brightness=180 # brightness (dimmed for aurora effect)
)
sequence demo {
play aurora_base # infinite duration (no 'for' clause)
}
run demo

View File

@ -1,80 +0,0 @@
# Generated Berry code from Animation DSL
# Source: aurora_borealis.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Aurora Borealis - Northern lights simulation
# Flowing green and purple aurora colors
#strip length 60
# Define aurora color palette
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var aurora_colors_ = bytes(
"00000022" # Dark night sky
"40004400" # Dark green
"8000AA44" # Aurora green
"C044AA88" # Light green
"FF88FFAA" # Bright aurora
)
# Secondary purple palette
var aurora_purple_ = bytes(
"00220022" # Dark purple
"40440044" # Medium purple
"808800AA" # Bright purple
"C0AA44CC" # Light purple
"FFCCAAFF" # Pale purple
)
# Base aurora animation with slow flowing colors
var aurora_base_ = animation.rich_palette_animation(engine)
aurora_base_.palette = aurora_colors_ # palette
aurora_base_.cycle_period = 10000 # cycle period
aurora_base_.transition_type = animation.SINE # transition type (explicit for clarity)
aurora_base_.brightness = 180 # brightness (dimmed for aurora effect)
var demo_ = animation.SequenceManager(engine)
.push_play_step(aurora_base_, nil) # infinite duration (no 'for' clause)
engine.add(demo_)
engine.run()
#- Original DSL source:
# Aurora Borealis - Northern lights simulation
# Flowing green and purple aurora colors
#strip length 60
# Define aurora color palette
palette aurora_colors = [
(0, 0x000022) # Dark night sky
(64, 0x004400) # Dark green
(128, 0x00AA44) # Aurora green
(192, 0x44AA88) # Light green
(255, 0x88FFAA) # Bright aurora
]
# Secondary purple palette
palette aurora_purple = [
(0, 0x220022) # Dark purple
(64, 0x440044) # Medium purple
(128, 0x8800AA) # Bright purple
(192, 0xAA44CC) # Light purple
(255, 0xCCAAFF) # Pale purple
]
# Base aurora animation with slow flowing colors
animation aurora_base = rich_palette_animation(
palette=aurora_colors # palette
cycle_period=10s # cycle period
transition_type=SINE # transition type (explicit for clarity)
brightness=180 # brightness (dimmed for aurora effect)
)
sequence demo {
play aurora_base # infinite duration (no 'for' clause)
}
run demo
-#

File diff suppressed because it is too large Load Diff

View File

@ -1,144 +0,0 @@
# Generated Berry code from Animation DSL
# Source: demo_shutter_rainbow.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors, then right to left
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_bidir
def shutter_bidir_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - self.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
engine.add(shutter_seq_)
end
animation.register_user_function('shutter_bidir', shutter_bidir_template)
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
"FFFFFF00"
"FF008000" # comma left on-purpose to test transpiler
"FF0000FF" # need for a lighter blue
"FF4B0082"
"FFFFFFFF"
)
shutter_bidir_template(engine, rainbow_with_white_, 1500)
engine.run()
#- Original DSL source:
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving from right to left
animation shutter_rl_animation = beacon_animation(
color = col1
back_color = col2
pos = 0
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
reset shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
reset shutter_size
play shutter_rl_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_bidir(rainbow_with_white, 1.5s)
-#

View File

@ -9,9 +9,6 @@ import animation
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_central
def shutter_central_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
@ -30,24 +27,43 @@ def shutter_central_template(engine, colors_, duration_)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_central_animation_ = animation.beacon_animation(engine)
shutter_central_animation_.color = col2_
shutter_central_animation_.back_color = col1_
shutter_central_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - animation.resolve(shutter_size_) / 2 end)
shutter_central_animation_.beacon_size = shutter_size_
shutter_central_animation_.slew_size = 0
shutter_central_animation_.priority = 5
# shutter moving in to out
var shutter_inout_animation_ = animation.beacon_animation(engine)
shutter_inout_animation_.color = col2_
shutter_inout_animation_.back_color = col1_
shutter_inout_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(shutter_size_) + 1) / 2 end)
shutter_inout_animation_.beacon_size = shutter_size_
shutter_inout_animation_.slew_size = 0
shutter_inout_animation_.priority = 5
# shutter moving out to in
var shutter_outin_animation_ = animation.beacon_animation(engine)
shutter_outin_animation_.color = col1_
shutter_outin_animation_.back_color = col2_
shutter_outin_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(strip_len_) - animation.resolve(shutter_size_) + 1) / 2 end)
shutter_outin_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_outin_animation_.slew_size = 0
shutter_outin_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_central_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_inout_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_outin_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
engine.add(shutter_seq_)
end
animation.register_user_function('shutter_central', shutter_central_template)
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
@ -78,21 +94,39 @@ template shutter_central {
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_central_animation = beacon_animation(
# shutter moving in to out
animation shutter_inout_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - shutter_size / 2
pos = strip_len2 - (shutter_size + 1) / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving out to in
animation shutter_outin_animation = beacon_animation(
color = col1
back_color = col2
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
col1.next = 1
col2.next = 1
repeat col1.palette_size times {
restart shutter_size
play shutter_inout_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_outin_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq

View File

@ -23,11 +23,13 @@ var disco_colors_ = bytes(
"FFFF00FF" # Magenta
)
# Fast color cycling base
var disco_base_ = animation.rich_palette_animation(engine)
disco_base_.palette = disco_colors_
disco_base_.cycle_period = 1000
disco_base_.transition_type = animation.LINEAR
disco_base_.brightness = 255
var disco_rich_color_ = animation.rich_palette(engine)
disco_rich_color_.palette = disco_colors_
disco_rich_color_.cycle_period = 1000
disco_rich_color_.transition_type = animation.LINEAR
disco_rich_color_.brightness = 255
var disco_base_ = animation.solid(engine)
disco_base_.color = disco_rich_color_
# Add strobe effect
disco_base_.opacity = (def (engine)
var provider = animation.square(engine)
@ -105,7 +107,8 @@ palette disco_colors = [
]
# Fast color cycling base
animation disco_base = rich_palette_animation(palette=disco_colors, cycle_period=1s, transition_type=LINEAR, brightness=255)
color disco_rich_color = rich_palette(palette=disco_colors, cycle_period=1s, transition_type=LINEAR, brightness=255)
animation disco_base = solid(color=disco_rich_color)
# Add strobe effect
disco_base.opacity = square(min_value=0, max_value=255, duration=100ms, duty_cycle=30) # Fast strobe

View File

@ -21,11 +21,13 @@ var fire_colors_ = bytes(
"FFFFFF00" # Yellow
)
# Create base fire animation with palette
var fire_base_ = animation.rich_palette_animation(engine)
fire_base_.palette = fire_colors_
fire_base_.cycle_period = 3000
fire_base_.transition_type = animation.LINEAR
fire_base_.brightness = 255
var fire_base_color_ = animation.rich_palette(engine)
fire_base_color_.palette = fire_colors_
fire_base_color_.cycle_period = 3000
fire_base_color_.transition_type = animation.LINEAR
fire_base_color_.brightness = 255
var fire_base_ = animation.solid(engine)
fire_base_.color = fire_base_color_
# Add flickering effect with random intensity changes
fire_base_.opacity = (def (engine)
var provider = animation.smooth(engine)
@ -67,7 +69,8 @@ palette fire_colors = [
]
# Create base fire animation with palette
animation fire_base = rich_palette_animation(palette=fire_colors, cycle_period=3s, transition_type=LINEAR, brightness=255)
color fire_base_color = rich_palette(palette=fire_colors, cycle_period=3s, transition_type=LINEAR, brightness=255)
animation fire_base = solid(color=fire_base_color)
# Add flickering effect with random intensity changes
fire_base.opacity = smooth(min_value=180, max_value=255, duration=800ms)

View File

@ -42,13 +42,13 @@ import user_functions
# Create animations that use imported user functions
animation random_red = solid(color=red)
random_red.opacity = user.rand_demo()
random_red.opacity = rand_demo()
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = max(50, min(255, user.rand_demo() + 100))
breathing_blue.opacity = max(50, min(255, rand_demo() + 100))
animation dynamic_green = solid(color=green)
dynamic_green.opacity = abs(user.rand_demo() - 128) + 64
dynamic_green.opacity = abs(rand_demo() - 128) + 64
# Create a sequence that cycles through the animations
sequence import_demo {

View File

@ -49,9 +49,13 @@ var sunset_sky_ = bytes(
"FFFFFF00" # Sun
)
# Create animations using each palette
var fire_effect_ = animation.rich_palette_animation(engine)
fire_effect_.palette = fire_gradient_
fire_effect_.cycle_period = 3000
var fire_effect_ = animation.solid(engine)
fire_effect_.color = (def (engine)
var provider = animation.rich_palette(engine)
provider.palette = fire_gradient_
provider.cycle_period = 3000
return provider
end)(engine)
var ocean_waves_ = animation.rich_palette_animation(engine)
ocean_waves_.palette = ocean_depths_
ocean_waves_.cycle_period = 8000
@ -139,7 +143,7 @@ palette sunset_sky = [
]
# Create animations using each palette
animation fire_effect = rich_palette_animation(palette=fire_gradient, cycle_period=3s)
animation fire_effect = solid(color=rich_palette(palette=fire_gradient, cycle_period=3s))
animation ocean_waves = rich_palette_animation(palette=ocean_depths, cycle_period=8s, transition_type=SINE, brightness=200)

View File

@ -1,55 +0,0 @@
#!/usr/bin/env berry
# Test runner for compiled DSL examples
# Generated automatically by compile_dsl_examples.be
import os
import sys
# Add animation library path
sys.path().push("lib/libesp32/berry_animation/src")
# Import animation framework
import animation
import animation_dsl
def run_compiled_example(filename)
print(f"Running {filename}...")
try
var f = open(f"lib/libesp32/berry_animation/anim_examples/compiled/{filename}", "r")
var code = f.read()
f.close()
var compiled_func = compile(code)
if compiled_func != nil
compiled_func()
print(f" ✓ {filename} executed successfully")
return true
else
print(f" ✗ {filename} failed to compile")
return false
end
except .. as e, msg
print(f" ✗ {filename} execution failed: {msg}")
return false
end
end
def run_all_examples()
var files = os.listdir("lib/libesp32/berry_animation/anim_examples/compiled")
var success_count = 0
var total_count = 0
for file : files
if string.endswith(file, ".be")
total_count += 1
if run_compiled_example(file)
success_count += 1
end
end
end
print(f"\nTest Results: {success_count}/{total_count} examples ran successfully")
end
# Run all examples if script is executed directly
run_all_examples()

View File

@ -0,0 +1,29 @@
# Generated Berry code from Animation DSL
# Source: test_compute_multiple.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Computed Values Demo - Example from the original request
# Shows how to use computed values from value providers
# Get the current strip length
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var strip_len_ = animation.strip_length(engine)
var a_ = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) / 4 end)
var b_ = animation.create_closure_value(engine, def (engine) return animation._math.abs(animation.resolve(strip_len_) / 4) end)
#- Original DSL source:
# Computed Values Demo - Example from the original request
# Shows how to use computed values from value providers
# Get the current strip length
set strip_len = strip_length()
set a = strip_len / 4
set b = abs(strip_len / 4)
-#

View File

@ -9,9 +9,6 @@ import animation
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_bidir
def shutter_bidir_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
@ -43,16 +40,18 @@ def shutter_bidir_template(engine, colors_, duration_)
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = animation.create_closure_value(engine, def (engine) return 0 + 0 end)
shutter_rl_animation_.slew_size = 0 + 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) log(f"begin 1", 3) end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.SequenceManager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) log(f"begin 2", 3) end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
@ -63,6 +62,9 @@ end
animation.register_user_function('shutter_bidir', shutter_bidir_template)
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
@ -114,12 +116,14 @@ template shutter_bidir {
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
log("begin 1")
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
log("begin 2")
restart shutter_size
play shutter_rl_animation for duration
col1.next = 1

View File

@ -1,116 +0,0 @@
# Generated Berry code from Animation DSL
# Source: test_shutter_rainbow_central.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Template function: shutter_central
def shutter_central_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var strip_len2_ = animation.create_closure_value(engine, def (engine) return (animation.resolve(strip_len_) + 1) / 2 end)
# the following is highly discouraged because it creates a new value provider at each tick
var strip_len3_ = animation.create_closure_value(engine, def (engine) return (animation.resolve(animation.strip_length(engine)) + 1) / 2 end)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_central_animation_ = animation.beacon_animation(engine)
shutter_central_animation_.color = col2_
shutter_central_animation_.back_color = col1_
shutter_central_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len3_) - animation.resolve(shutter_size_) / 2 end)
shutter_central_animation_.beacon_size = shutter_size_
shutter_central_animation_.slew_size = 0
shutter_central_animation_.priority = 5
var shutter_seq_ = animation.SequenceManager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_central_animation_, duration_)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
engine.add(shutter_seq_)
end
animation.register_user_function('shutter_central', shutter_central_template)
var rainbow_with_white_ = bytes(
"FFFF0000"
"FFFFA500"
"FFFFFF00"
"FF008000" # comma left on-purpose to test transpiler
"FF0000FF" # need for a lighter blue
"FF4B0082"
"FFFFFFFF"
)
shutter_central_template(engine, rainbow_with_white_, 1500)
engine.run()
#- Original DSL source:
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
template shutter_central {
param colors type palette
param duration
set strip_len = strip_length()
set strip_len2 = (strip_len + 1) / 2
# the following is highly discouraged because it creates a new value provider at each tick
set strip_len3 = (strip_length() + 1) / 2
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_central_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len3 - shutter_size / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_central(rainbow_with_white, 1.5s)
-#

View File

@ -0,0 +1,23 @@
# Generated Berry code from Animation DSL
# Source: test_simple_math.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Test simple math
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var x_ = 2000
var y_ = animation.create_closure_value(engine, def (engine) return animation.resolve(x_) + 4 end)
#- Original DSL source:
# Test simple math
set x = 2s
set y = x + 4
-#

View File

@ -68,7 +68,7 @@ animation random_base = solid(
priority=10
)
# Use user function in property assignment
random_base.opacity = user.rand_demo()
random_base.opacity = rand_demo()
# Example 2: User function with mathematical operations
animation random_bounded = solid(
@ -76,7 +76,7 @@ animation random_bounded = solid(
priority=8
)
# User function with bounds using math functions
random_bounded.opacity = max(50, min(255, user.rand_demo() + 100))
random_bounded.opacity = max(50, min(255, rand_demo() + 100))
# Example 3: User function in arithmetic expressions
animation random_variation = solid(
@ -84,7 +84,7 @@ animation random_variation = solid(
priority=15
)
# Mix user function with arithmetic operations
random_variation.opacity = abs(user.rand_demo() - 128) + 64
random_variation.opacity = abs(rand_demo() - 128) + 64
# Example 4: User function affecting different properties
animation random_multi = solid(
@ -92,7 +92,7 @@ animation random_multi = solid(
priority=12
)
# Use user function for multiple properties
random_multi.opacity = max(100, user.rand_demo())
random_multi.opacity = max(100, rand_demo())
# Example 5: Complex expression with user function
animation random_complex = solid(
@ -100,7 +100,7 @@ animation random_complex = solid(
priority=20
)
# Complex expression with user function and math operations
random_complex.opacity = round((user.rand_demo() + 128) / 2 + abs(user.rand_demo() - 100))
random_complex.opacity = round((rand_demo() + 128) / 2 + abs(rand_demo() - 100))
# Run all animations to demonstrate the effects
run random_base

View File

@ -14,21 +14,39 @@ template shutter_central {
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_central_animation = beacon_animation(
# shutter moving in to out
animation shutter_inout_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - shutter_size / 2
pos = strip_len2 - (shutter_size + 1) / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving out to in
animation shutter_outin_animation = beacon_animation(
color = col1
back_color = col2
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
col1.next = 1
col2.next = 1
repeat col1.palette_size times {
restart shutter_size
play shutter_inout_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_outin_animation for duration
col1.next = 1
col2.next = 1
}
}
run shutter_seq

View File

@ -15,7 +15,8 @@ palette disco_colors = [
]
# Fast color cycling base
animation disco_base = rich_palette_animation(palette=disco_colors, cycle_period=1s, transition_type=LINEAR, brightness=255)
color disco_rich_color = rich_palette(palette=disco_colors, cycle_period=1s, transition_type=LINEAR, brightness=255)
animation disco_base = solid(color=disco_rich_color)
# Add strobe effect
disco_base.opacity = square(min_value=0, max_value=255, duration=100ms, duty_cycle=30) # Fast strobe

View File

@ -0,0 +1,4 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
set red = 0xFF0000

View File

@ -0,0 +1,4 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
set abs = 0xFF0000

View File

@ -0,0 +1,6 @@
# Fail value_provider addition
#1+1
set a = linear() + triangle()
set b = triangle()
set c = linear() + triangle()

View File

@ -13,7 +13,8 @@ palette fire_colors = [
]
# Create base fire animation with palette
animation fire_base = rich_palette_animation(palette=fire_colors, cycle_period=3s, transition_type=LINEAR, brightness=255)
color fire_base_color = rich_palette(palette=fire_colors, cycle_period=3s, transition_type=LINEAR, brightness=255)
animation fire_base = solid(color=fire_base_color)
# Add flickering effect with random intensity changes
fire_base.opacity = smooth(min_value=180, max_value=255, duration=800ms)

View File

@ -6,13 +6,13 @@ import user_functions
# Create animations that use imported user functions
animation random_red = solid(color=red)
random_red.opacity = user.rand_demo()
random_red.opacity = rand_demo()
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = max(50, min(255, user.rand_demo() + 100))
breathing_blue.opacity = max(50, min(255, rand_demo() + 100))
animation dynamic_green = solid(color=green)
dynamic_green.opacity = abs(user.rand_demo() - 128) + 64
dynamic_green.opacity = abs(rand_demo() - 128) + 64
# Create a sequence that cycles through the animations
sequence import_demo {

View File

@ -44,7 +44,7 @@ palette sunset_sky = [
]
# Create animations using each palette
animation fire_effect = rich_palette_animation(palette=fire_gradient, cycle_period=3s)
animation fire_effect = solid(color=rich_palette(palette=fire_gradient, cycle_period=3s))
animation ocean_waves = rich_palette_animation(palette=ocean_depths, cycle_period=8s, transition_type=SINE, brightness=200)

View File

@ -0,0 +1,7 @@
# Computed Values Demo - Example from the original request
# Shows how to use computed values from value providers
# Get the current strip length
set strip_len = strip_length()
set a = strip_len / 4
set b = abs(strip_len / 4)

View File

@ -0,0 +1,29 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
param eye_color type color
param back_color type color
param duration
set aaa = abs2(45) + 2
set strip_len = strip_length()
set strip_len_div_2 = abs(strip_len / 2)
set strip_len2 = strip_length(is_running=0) # artifically putting an argument to cause a closure
set osc1 = sawtooth()
set osc2 = triangle()
set osc3 = scale(osc1 + osc2, 0, 200, 0, 255)
set osc9 = sawtooth() + triangle() # this should fail
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5
)
run eye_animation
}

View File

@ -35,12 +35,14 @@ template shutter_bidir {
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
log("begin 1")
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
log("begin 2")
restart shutter_size
play shutter_rl_animation for duration
col1.next = 1

View File

@ -9,7 +9,8 @@ template shutter_central {
set strip_len = strip_length()
set strip_len2 = (strip_len + 1) / 2
# the following is highly discouraged because it creates a new value provider at each tick
set strip_len3 = (strip_length() + 1) / 2
set strip_len3 = 1 + strip_length()
set strip_len4 = (strip_length() + 1) / 2
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)

View File

@ -0,0 +1,4 @@
# Test simple math
set x = 2s
set y = x + 4

View File

@ -12,7 +12,7 @@ animation random_base = solid(
priority=10
)
# Use user function in property assignment
random_base.opacity = user.rand_demo()
random_base.opacity = rand_demo()
# Example 2: User function with mathematical operations
animation random_bounded = solid(
@ -20,7 +20,7 @@ animation random_bounded = solid(
priority=8
)
# User function with bounds using math functions
random_bounded.opacity = max(50, min(255, user.rand_demo() + 100))
random_bounded.opacity = max(50, min(255, rand_demo() + 100))
# Example 3: User function in arithmetic expressions
animation random_variation = solid(
@ -28,7 +28,7 @@ animation random_variation = solid(
priority=15
)
# Mix user function with arithmetic operations
random_variation.opacity = abs(user.rand_demo() - 128) + 64
random_variation.opacity = abs(rand_demo() - 128) + 64
# Example 4: User function affecting different properties
animation random_multi = solid(
@ -36,7 +36,7 @@ animation random_multi = solid(
priority=12
)
# Use user function for multiple properties
random_multi.opacity = max(100, user.rand_demo())
random_multi.opacity = max(100, rand_demo())
# Example 5: Complex expression with user function
animation random_complex = solid(
@ -44,7 +44,7 @@ animation random_complex = solid(
priority=20
)
# Complex expression with user function and math operations
random_complex.opacity = round((user.rand_demo() + 128) / 2 + abs(user.rand_demo() - 100))
random_complex.opacity = round((rand_demo() + 128) / 2 + abs(rand_demo() - 100))
# Run all animations to demonstrate the effects
run random_base

View File

@ -18,21 +18,14 @@ ParameterizedObject
├── Animation
│ ├── BreatheAnimation
│ ├── CometAnimation
│ ├── SparkleAnimation
│ ├── BounceAnimation
│ ├── FireAnimation
│ ├── GradientAnimation
│ ├── JitterAnimation
│ ├── NoiseAnimation
│ ├── PlasmaAnimation
│ ├── PulseAnimation
│ ├── BeaconAnimation
│ ├── CrenelPositionAnimation
│ ├── RichPaletteAnimation
│ ├── TwinkleAnimation
│ ├── WaveAnimation
│ ├── ShiftAnimation
│ ├── ScaleAnimation
│ ├── PalettePatternAnimation
│ │ ├── PaletteWaveAnimation
│ │ ├── PaletteGradientAnimation
@ -442,119 +435,8 @@ Creates a comet effect with a bright head and fading tail. Inherits from `Animat
**Factory**: `animation.comet_animation(engine)`
### SparkleAnimation
Creates random twinkling effects where individual pixels appear as sparkles that fade out over time. Perfect for starfield effects, magical sparkles, or glitter-like accents. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color` | int | 0xFFFFFFFF | - | Sparkle color |
| `back_color` | int | 0xFF000000 | - | Background color shown when no sparkle is active |
| `density` | int | 30 | 0-255 | Sparkle frequency (0=none, 255=maximum rate) |
| `fade_speed` | int | 50 | 0-255 | How quickly sparkles dim and fade out |
| `sparkle_duration` | int | 60 | 0-255 | How long sparkles last in frames (~30 FPS) |
| `min_brightness` | int | 100 | 0-255 | Minimum brightness for new sparkles |
| `max_brightness` | int | 255 | 0-255 | Maximum brightness for new sparkles |
| *(inherits all Animation parameters)* | | | | |
#### Sparkle Lifecycle
Each sparkle follows a predictable lifecycle:
1. **Creation**: Random appearance based on density parameter
2. **Brightness**: Random value between min_brightness and max_brightness
3. **Aging**: Frame-by-frame age tracking
4. **Fading**: Brightness reduction based on age and fade_speed
5. **Death**: Sparkle removed when too dim or duration exceeded
#### Density Effects
- **Low density (10-40)**: Occasional, subtle sparkles
- **Medium density (50-100)**: Regular twinkling effect
- **High density (120-200)**: Frequent, busy sparkles
- **Maximum density (255)**: Nearly constant sparkles
#### Fade Speed Effects
- **Slow fade (10-30)**: Long, gentle fade-out
- **Medium fade (40-80)**: Balanced sparkle lifecycle
- **Fast fade (100-200)**: Quick, snappy sparkles
#### Usage Examples
```berry
# Basic white starfield
animation starfield = sparkle_animation(
color=white,
back_color=black,
density=80,
fade_speed=60
)
# Magical rainbow sparkles
animation magic_sparkles = sparkle_animation(
color=rainbow_cycle,
density=100,
fade_speed=50,
min_brightness=80,
max_brightness=220
)
# Subtle ambient sparkles
animation ambient_sparkles = sparkle_animation(
color=0xFFFFFFAA,
density=20,
fade_speed=30
)
```
#### Common Use Cases
- **Starfield**: White sparkles on black background with low density
- **Magic Effects**: Rainbow sparkles with medium density
- **Accent Lighting**: Subtle colored sparkles over other effects
- **Party Atmosphere**: High-density, fast-fading sparkles
- **Ambient Decoration**: Low-density, slow-fading warm sparkles
### BounceAnimation
Creates physics-based bouncing effects with configurable gravity, damping, and motion parameters. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `source_animation` | instance | nil | - | Animation to bounce |
| `bounce_speed` | int | 128 | 0-255 | Initial bounce speed |
| `bounce_range` | int | 0 | 0-1000 | Bounce range in pixels (0 = full strip) |
| `damping` | int | 250 | 0-255 | Velocity damping factor (255 = no damping) |
| `gravity` | int | 0 | 0-255 | Gravity strength (0 = no gravity) |
| *(inherits all Animation parameters)* | | | | |
#### Physics Behavior
- **Realistic Physics**: Simulates gravity, velocity, and damping
- **Bounce Range**: Can be constrained to specific strip regions
- **Damping Effects**: Controls energy loss on each bounce
- **Gravity Simulation**: Optional downward acceleration
#### Usage Examples
```berry
# Bouncing ball effect with gravity
animation ball = pulsating_animation(color=green, period=2s)
animation bouncing_ball = bounce_animation(
source_animation=ball,
bounce_speed=150,
gravity=80
)
# Elastic bounce without gravity
animation elastic_bounce = bounce_animation(
source_animation=ball,
bounce_speed=120,
damping=240
)
```
**Factories**: `animation.bounce_animation(engine)`, `animation.bounce_basic(engine)`, `animation.bounce_gravity(engine)`, `animation.bounce_constrained(engine)`
### FireAnimation
@ -588,50 +470,7 @@ Creates smooth color gradients that can be linear or radial. Inherits from `Anim
**Factories**: `animation.gradient_animation(engine)`, `animation.gradient_rainbow_linear(engine)`, `animation.gradient_rainbow_radial(engine)`, `animation.gradient_two_color_linear(engine)`
### JitterAnimation
Adds random shake effects to patterns with configurable intensity, frequency, and jitter types. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `source_animation` | instance | nil | - | Animation to apply jitter to |
| `jitter_intensity` | int | 100 | 0-255 | Overall jitter intensity |
| `jitter_frequency` | int | 60 | 0-255 | Jitter frequency in Hz (0-30 Hz) |
| `jitter_type` | int | 0 | 0-3 | 0=position, 1=color, 2=brightness, 3=all |
| `position_range` | int | 50 | 0-255 | Position jitter range in pixels |
| `color_range` | int | 30 | 0-255 | Color jitter range |
| `brightness_range` | int | 40 | 0-255 | Brightness jitter range |
| *(inherits all Animation parameters)* | | | | |
#### Jitter Types
- **0 - Position**: Random position shifts
- **1 - Color**: Random color variations
- **2 - Brightness**: Random brightness changes
- **3 - All**: Combination of all jitter types
#### Usage Examples
```berry
# Digital glitch effect
animation base_pattern = gradient_animation(color=rainbow_cycle)
animation glitch_effect = jitter_animation(
source_animation=base_pattern,
jitter_intensity=200,
jitter_frequency=120,
jitter_type=3
)
# Subtle shake effect
animation subtle_shake = jitter_animation(
source_animation=base_pattern,
jitter_intensity=60,
jitter_frequency=40,
jitter_type=0
)
```
**Factories**: `animation.jitter_animation(engine)`, `animation.jitter_position(engine)`, `animation.jitter_color(engine)`, `animation.jitter_brightness(engine)`, `animation.jitter_all(engine)`
### NoiseAnimation
@ -701,78 +540,7 @@ animation cloud_pattern = noise_animation(
- **Cloud Simulation**: White/gray colors with large-scale patterns
- **Abstract Art**: Rainbow colors with high detail and multiple octaves
### PlasmaAnimation
Creates classic plasma effects using sine wave interference patterns. Generates smooth, flowing patterns reminiscent of 1990s demoscene effects and natural phenomena like aurora. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color` | instance | nil | - | Color provider for plasma mapping (nil = rainbow) |
| `freq_x` | int | 32 | 1-255 | Primary wave frequency |
| `freq_y` | int | 23 | 1-255 | Secondary wave frequency |
| `phase_x` | int | 0 | 0-255 | Primary wave phase shift |
| `phase_y` | int | 64 | 0-255 | Secondary wave phase shift |
| `time_speed` | int | 50 | 0-255 | Animation speed (0 = static pattern) |
| `blend_mode` | int | 0 | 0-2 | 0=add, 1=multiply, 2=average |
| *(inherits all Animation parameters)* | | | | |
#### Wave Interference
The plasma effect combines two sine waves with different frequencies to create interference patterns. The interaction between these waves produces complex, organic-looking effects.
#### Frequency Effects
- **Low frequencies (10-30)**: Large, flowing waves
- **Medium frequencies (30-80)**: Balanced wave patterns
- **High frequencies (100-200)**: Fine, detailed interference
#### Blend Modes
- **Add (0)**: Bright, energetic patterns with high contrast
- **Multiply (1)**: Darker patterns with rich color depth
- **Average (2)**: Balanced patterns with smooth transitions
#### Phase Relationships
- **In-phase (phase_y = 0)**: Aligned waves create regular patterns
- **Quarter-phase (phase_y = 64)**: Creates diagonal flow effects
- **Opposite-phase (phase_y = 128)**: Creates standing wave patterns
#### Usage Examples
```berry
# Classic rainbow plasma
animation rainbow_plasma = plasma_animation(
freq_x=32,
freq_y=23,
time_speed=60,
blend_mode=0
)
# High-frequency intense plasma
animation intense_plasma = plasma_animation(
color=purple,
freq_x=100,
freq_y=80,
time_speed=120,
blend_mode=1
)
# Static plasma pattern for backgrounds
animation static_plasma = plasma_animation(
color=blue,
freq_x=40,
freq_y=30,
time_speed=0
)
```
#### Common Use Cases
- **Ambient Lighting**: Slow, smooth plasma for relaxing environments
- **Party Effects**: Fast, rainbow plasma for energetic atmospheres
- **Retro Gaming**: Classic plasma effects for nostalgic themes
- **Abstract Art**: Complex frequency combinations for artistic displays
### PulseAnimation
@ -1133,90 +901,7 @@ animation strobe = wave_animation(
- **Color Cycling**: Rainbow waves for spectrum effects
- **Pulse Patterns**: Triangle waves for rhythmic pulses
### ShiftAnimation
Creates scrolling and translation effects by moving patterns horizontally across the LED strip. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `source_animation` | instance | nil | - | Animation to shift/scroll |
| `shift_speed` | int | 128 | 0-255 | Scrolling speed (0=static, 255=fastest) |
| `direction` | int | 1 | -1 to 1 | Scroll direction (1=right, -1=left) |
| `wrap_around` | bool | true | - | Whether to wrap around the strip edges |
| *(inherits all Animation parameters)* | | | | |
#### Movement Behavior
- **Horizontal Scrolling**: Moves patterns left or right across the strip
- **Wrap-Around**: Patterns can wrap around strip edges or disappear
- **Variable Speed**: Configurable scrolling speed from static to very fast
- **Direction Control**: Forward and reverse scrolling
#### Usage Examples
```berry
# Scrolling text effect
animation text_pattern = solid(color=white)
animation scrolling_text = shift_animation(
source_animation=text_pattern,
shift_speed=100,
direction=1,
wrap_around=true
)
# Moving rainbow pattern
animation rainbow_base = gradient_animation(color=rainbow_cycle)
animation moving_rainbow = shift_animation(
source_animation=rainbow_base,
shift_speed=80,
direction=-1
)
```
**Factories**: `animation.shift_animation(engine)`, `animation.shift_scroll_right(engine)`, `animation.shift_scroll_left(engine)`, `animation.shift_fast_scroll(engine)`
### ScaleAnimation
Creates size transformation effects with multiple animation modes including static scaling, oscillation, growing, and shrinking. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `source_animation` | instance | nil | - | Animation to scale |
| `scale_factor` | int | 128 | 1-255 | Scale factor (128=1.0x, 64=0.5x, 255=2.0x) |
| `scale_speed` | int | 0 | 0-255 | Animation speed for dynamic modes |
| `scale_mode` | int | 0 | 0-3 | 0=static, 1=oscillate, 2=grow, 3=shrink |
| `scale_center` | int | 128 | 0-255 | Center point for scaling (128=center) |
| `interpolation` | int | 1 | 0-1 | 0=nearest neighbor, 1=linear interpolation |
| *(inherits all Animation parameters)* | | | | |
#### Scale Modes
- **0 - Static**: Fixed scale factor
- **1 - Oscillate**: Oscillates between 0.5x and 2.0x (breathing effect)
- **2 - Grow**: Grows from 0.5x to 2.0x
- **3 - Shrink**: Shrinks from 2.0x to 0.5x
#### Usage Examples
```berry
# Breathing effect with oscillating scale
animation base_pattern = gradient_animation(color=rainbow_cycle)
animation breathing_effect = scale_animation(
source_animation=base_pattern,
scale_mode=1,
scale_speed=60
)
# Static zoom effect
animation zoomed_sparkles = scale_animation(
source_animation=sparkle_animation(color=white),
scale_factor=180,
scale_mode=0
)
# Scale factor 180 = 1.4x zoom
```
**Factories**: `animation.scale_animation(engine)`, `animation.scale_static(engine)`, `animation.scale_oscillate(engine)`, `animation.scale_grow(engine)`
### PalettePatternAnimation
@ -1305,34 +990,31 @@ Motion effects can be chained to create sophisticated transformations:
# Base animation
animation base_pulse = pulsating_animation(color=blue, period=3s)
# Chain multiple transformations
animation scaled_pulse = scale_animation(
source_animation=base_pulse,
scale_factor=150
# Simple animation composition
animation fire_effect = fire_animation(
color=fire_colors,
intensity=180,
flicker_speed=8
)
animation scrolling_scaled = shift_animation(
source_animation=scaled_pulse,
shift_speed=60,
direction=-1
animation gradient_wave = gradient_animation(
color=rainbow_cycle,
gradient_type=0,
movement_speed=50
)
animation final_effect = jitter_animation(
source_animation=scrolling_scaled,
jitter_intensity=40,
jitter_type=1
)
# Result: A scaled, scrolling, color-jittered pulse
run final_effect
# Result: Multiple independent animations
run base_pulse
run fire_effect
run gradient_wave
```
### Performance Considerations
- Each motion effect uses approximately 4 bytes per pixel for color storage
- Bounce animation includes additional physics calculations
- Scale animation requires interpolation calculations
- Limit jitter frequency for better performance
- Each animation uses approximately 4 bytes per pixel for color storage
- Fire animation includes additional flicker calculations
- Gradient animation requires color interpolation calculations
- Noise animation includes pseudo-random pattern generation
- Consider strip length impact on transformation calculations
## Parameter Constraints

View File

@ -58,6 +58,7 @@ The following keywords are reserved and cannot be used as identifiers:
- `strip` - Strip configuration (temporarily disabled, reserved keyword)
- `set` - Variable assignment
- `import` - Import Berry modules
- `berry` - Embed arbitrary Berry code
**Definition Keywords:**
- `color` - Color definition
@ -237,7 +238,7 @@ import string # Import utility modules
import user_functions
animation dynamic = solid(color=blue)
dynamic.opacity = user.my_custom_function()
dynamic.opacity = my_custom_function()
# Import custom animation libraries
import fire_effects
@ -254,6 +255,46 @@ import user_functions
import "user_functions"
```
### Berry Code Blocks
The `berry` keyword allows embedding arbitrary Berry code within DSL files using triple-quoted strings:
```berry
berry """
import math
var custom_value = math.pi * 2
print("Custom calculation:", custom_value)
"""
berry '''
# Alternative syntax with single quotes
def helper_function(x)
return x * 1.5
end
'''
```
**Berry Code Block Features:**
- Code is copied verbatim to the generated Berry code
- Supports both `"""` and `'''` triple-quote syntax
- Can span multiple lines and include complex Berry syntax
- Variables and functions defined in one block are available in subsequent blocks
- Can interact with DSL-generated objects (e.g., `animation_name_.property = value`)
**Example with DSL Integration:**
```berry
animation pulse = pulsating_animation(color=red, period=2s)
berry """
# Modify animation using Berry code
pulse_.opacity = 200
pulse_.priority = 10
print("Animation configured")
"""
run pulse
```
## Color Definitions
The `color` keyword defines static colors or color providers:
@ -379,7 +420,7 @@ animation.register_user_function("custom_palette", create_custom_palette)
```berry
# Use in DSL
animation dynamic_anim = rich_palette(
palette=user.custom_palette(0xFF0000, 200)
palette=custom_palette(0xFF0000, 200)
cycle_period=3s
)
```
@ -446,10 +487,15 @@ pulse_red.opacity = opacity_mask # Dynamic opacity from animation
The DSL supports computed values using arithmetic expressions with value providers and mathematical functions:
### Safe Patterns
```berry
# Get strip dimensions
# ✅ RECOMMENDED: Single value provider assignment
set strip_len = strip_length()
# ✅ RECOMMENDED: Computation with existing values
set strip_len2 = (strip_len + 1) / 2
# Use computed values in animation parameters
animation stream1 = comet_animation(
color=red
@ -457,7 +503,43 @@ animation stream1 = comet_animation(
speed=1.5
priority=10
)
```
### ⚠️ Dangerous Patterns (Prevented by Transpiler)
The transpiler prevents dangerous patterns that would create new value provider instances at each evaluation:
```berry
# ❌ DANGEROUS: Function creation in computed expression
# This would create a new strip_length() instance at each evaluation
set strip_len3 = (strip_length() + 1) / 2
# ❌ ERROR: Transpiler will reject this with:
# "Function 'strip_length()' cannot be used in computed expressions.
# This creates a new instance at each evaluation."
```
**Why This Is Dangerous:**
- Creates a new function instance every time the expression is evaluated
- Causes memory leaks and performance degradation
- Each new instance has its own timing and state, leading to inconsistent behavior
**Safe Alternative:**
```berry
# ✅ CORRECT: Separate the value provider creation from computation
set strip_len = strip_length() # Single value provider
set strip_len3 = (strip_len + 1) / 2 # Computation with existing value
```
**Functions That Are Restricted in Computed Expressions:**
- Any function that creates instances (value providers, animations, etc.) when called
- Examples: `strip_length()`, `triangle()`, `smooth()`, `solid()`, etc.
**Note:** These functions are allowed in `set` statements as they create the instance once, but they cannot be used inside arithmetic expressions that get wrapped in closures, as this would create new instances at each evaluation.
### Advanced Computed Values
```berry
# Complex expressions with multiple operations
set base_speed = 2.0
animation stream2 = comet_animation(
@ -541,37 +623,37 @@ test.opacity = min(255, max(50, scale(sqrt(strip_len), 0, 16, 100, 255)))
When the DSL detects arithmetic expressions containing value providers, variable references, or mathematical functions, it automatically creates closure functions that capture the computation. These closures are called with `(self, param_name, time_ms)` parameters, allowing the computation to be re-evaluated dynamically as needed. Mathematical functions are automatically prefixed with `animation._math.` in the closure context to access the ClosureValueProvider's mathematical methods.
**User Functions in Computed Parameters:**
User-defined functions can also be used in computed parameter expressions, providing powerful custom effects. User functions must be called with the `user.` prefix:
User-defined functions can also be used in computed parameter expressions, providing powerful custom effects:
```berry
# Simple user function in computed parameter
animation base = solid(color=blue)
base.opacity = user.rand_demo()
base.opacity = rand_demo()
# User functions mixed with math operations
animation dynamic = solid(
color=purple
opacity=max(50, min(255, user.rand_demo() + 100))
opacity=max(50, min(255, rand_demo() + 100))
)
```
### User Functions
User functions are custom Berry functions that can be called from computed parameters. They provide dynamic values that change over time. User functions must be called with the `user.` prefix.
User functions are custom Berry functions that can be called from computed parameters. They provide dynamic values that change over time.
**Available User Functions:**
- `user.rand_demo()` - Returns random values for demonstration purposes
- `rand_demo()` - Returns random values for demonstration purposes
**Usage in Computed Parameters:**
```berry
# Simple user function
animation.opacity = user.rand_demo()
animation.opacity = rand_demo()
# User function with math operations
animation.opacity = max(100, user.rand_demo())
animation.opacity = max(100, rand_demo())
# User function in arithmetic expressions
animation.opacity = abs(user.rand_demo() - 128) + 64
animation.opacity = abs(rand_demo() - 128) + 64
```
**Available User Functions:**
@ -579,7 +661,7 @@ The following user functions are available by default (see [User Functions Guide
| Function | Parameters | Description |
|----------|------------|-------------|
| `user.rand_demo()` | none | Returns a random value (0-255) for demonstration |
| `rand_demo()` | none | Returns a random value (0-255) for demonstration |
**User Function Behavior:**
- User functions are automatically detected by the transpiler
@ -825,6 +907,8 @@ sequence clean_transitions {
Templates provide a powerful way to create reusable, parameterized animation patterns. They allow you to define animation blueprints that can be instantiated with different parameters, promoting code reuse and maintainability.
**Template-Only Files**: DSL files containing only template definitions transpile to pure Berry functions without engine initialization or execution code. This allows templates to be used as reusable function libraries.
### Template Definition
Templates are defined using the `template` keyword followed by a parameter block and body:
@ -971,6 +1055,9 @@ end
animation.register_user_function('pulse_effect', pulse_effect_template)
```
**Template-Only Transpilation:**
Files containing only templates generate pure Berry function definitions without `var engine = animation.init_strip()` or `engine.run()` calls, making them suitable as reusable function libraries.
**Parameter Handling:**
- Parameters get `_` suffix in generated code to avoid naming conflicts
- Templates receive `engine` as the first parameter automatically
@ -1194,13 +1281,7 @@ Animation classes create visual effects on LED strips:
| `twinkle_animation` | Twinkling stars effect |
| `gradient_animation` | Color gradient effects |
| `noise_animation` | Perlin noise-based patterns |
| `plasma_animation` | Plasma wave effects |
| `sparkle_animation` | Sparkling/glitter effects |
| `wave_animation` | Wave propagation effects |
| `shift_animation` | Shifting/scrolling patterns |
| `bounce_animation` | Bouncing ball effects |
| `scale_animation` | Scaling/zooming effects |
| `jitter_animation` | Random jitter/shake effects |
| `rich_palette_animation` | Palette-based color cycling |
| `palette_wave_animation` | Wave patterns using palettes |
| `palette_gradient_animation` | Gradient patterns using palettes |

View File

@ -104,7 +104,7 @@ The Animation DSL uses a declarative syntax with named parameters. All animation
- **Named colors**: `red`, `blue`, `white`, etc.
- **Comments**: `# This is a comment`
- **Property assignment**: `animation.property = value`
- **User functions**: `user.function_name()` for custom functions
- **User functions**: `function_name()` for custom functions
### Basic Structure
@ -125,7 +125,7 @@ animation comet_blue = comet_animation(color=blue, tail_length=10, speed=1500)
# Property assignments with user functions
pulse_red.priority = 10
pulse_red.opacity = user.breathing_effect()
pulse_red.opacity = breathing_effect()
comet_blue.direction = -1
# Execution
@ -242,7 +242,7 @@ animation.register_user_function("rand_demo", rand_demo)
import user_functions
animation test = solid(color=blue)
test.opacity = user.rand_demo()
test.opacity = rand_demo()
run test
```
@ -260,12 +260,69 @@ engine.add(test_)
engine.run()
```
## Berry Code Block Transpilation
The DSL supports embedding arbitrary Berry code using the `berry` keyword with triple-quoted strings. This provides an escape hatch for complex logic while maintaining the declarative nature of the DSL.
### Berry Code Block Syntax
```berry
# DSL Berry Code Block
berry """
import math
var custom_value = math.pi * 2
print("Custom calculation:", custom_value)
"""
```
### Transpilation Behavior
Berry code blocks are copied verbatim to the generated Berry code with comment markers:
```berry
# DSL Code
berry """
var test_var = 42
print("Hello from berry block")
"""
# Transpiles to Berry Code
# Berry code block
var test_var = 42
print("Hello from berry block")
# End berry code block
```
### Integration with DSL Objects
Berry code can interact with DSL-generated objects by using the underscore suffix naming convention:
```berry
# DSL Code
animation pulse = pulsating_animation(color=red, period=2s)
berry """
pulse_.opacity = 200
pulse_.priority = 10
"""
# Transpiles to Berry Code
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = animation.red
pulse_.period = 2000
# Berry code block
pulse_.opacity = 200
pulse_.priority = 10
# End berry code block
```
## Advanced DSL Features
### Templates
Templates provide a DSL-native way to create reusable animation patterns with parameters. Templates are transpiled into Berry functions and automatically registered for use.
**Template-Only Files**: DSL files containing only template definitions generate pure Berry function code without engine initialization or execution, creating reusable function libraries.
#### Template Definition Transpilation
```berry
@ -361,6 +418,7 @@ animation.register_user_function("comet_chase", comet_chase)
- Automatically registered
- Type annotations supported
- Transpiled to Berry functions
- Template-only files generate pure function libraries
**User Functions** (Berry-native):
- Defined in Berry code
@ -375,23 +433,24 @@ Register custom Berry functions for use in DSL. User functions must take `engine
```berry
# Define custom function in Berry - engine must be first parameter
def custom_sparkle(engine, color, density, speed)
def custom_twinkle(engine, color, count, period)
var anim = animation.twinkle_animation(engine)
anim.color = color
anim.density = density
anim.speed = speed
anim.count = count
atml:parameter>
</invoke>
return anim
end
# Register the function for DSL use
animation.register_user_function("sparkle", custom_sparkle)
animation.register_user_function("twinkle", custom_twinkle)
```
```berry
# Use in DSL - engine is automatically passed as first argument
animation gold_sparkle = sparkle(#FFD700, 8, 500ms)
animation blue_sparkle = sparkle(blue, 12, 300ms)
run gold_sparkle
animation gold_twinkle = twinkle(#FFD700, 8, 500ms)
animation blue_twinkle = twinkle(blue, 12, 300ms)
run gold_twinkle
```
**Important**: The DSL transpiler automatically passes `engine` as the first argument to all user functions. Your function signature must include `engine` as the first parameter, but DSL users don't need to provide it when calling the function.
@ -433,16 +492,13 @@ DSL supports nested function calls for complex compositions:
```berry
# Nested calls in animation definitions (now supported)
animation complex = pulsating_animation(
source=shift_animation(
source=solid(color=red),
offset=triangle(min=0, max=29, period=3s)
),
color=red,
period=2s
)
# Nested calls in run statements
sequence demo {
play pulsating_animation(source=shift_animation(source=solid(color=blue), offset=5), period=1s) for 10s
play pulsating_animation(color=blue, period=1s) for 10s
}
```
@ -521,6 +577,29 @@ sequence demo {
# Transpiler error: "Undefined reference 'nonexistent_animation' in sequence play"
```
**Function Call Safety Validation:**
```berry
# Error: Dangerous function creation in computed expression
set strip_len3 = (strip_length() + 1) / 2
# Transpiler error: "Function 'strip_length()' cannot be used in computed expressions.
# This creates a new instance at each evaluation. Use either:
# set var_name = strip_length() # Single function call
# set computed = (existing_var + 1) / 2 # Computation with existing values"
```
**Why This Validation Exists:**
The transpiler prevents dangerous patterns where functions that create instances are called inside computed expressions that get wrapped in closures. This would create a new instance every time the closure is evaluated, leading to:
- Memory leaks
- Performance degradation
- Inconsistent behavior due to multiple timing states
**Safe Alternative:**
```berry
# ✅ CORRECT: Separate function call from computation
set strip_len = strip_length() # Single function call
set strip_len3 = (strip_len + 1) / 2 # Computation with existing value
```
### Error Categories
- **Syntax errors**: Invalid DSL syntax (lexer/parser errors)
@ -529,6 +608,7 @@ sequence demo {
- **Constraint validation**: Parameter values that violate defined constraints (min/max, enums, types)
- **Reference validation**: Using undefined colors, animations, or variables
- **Type validation**: Incorrect parameter types or incompatible assignments
- **Safety validation**: Dangerous patterns that could cause memory leaks or performance issues
- **Runtime errors**: Errors during Berry code execution (rare with good validation)
## Performance Considerations

View File

@ -123,11 +123,11 @@ animation comet = comet_animation(
speed=2000
)
# Sparkle effect
animation sparkles = sparkle_animation(
# Twinkling effect
animation sparkles = twinkle_animation(
color=white
density=80
fade_speed=60
count=8
period=800ms
)
run breathing
@ -241,22 +241,22 @@ For complex logic, create custom functions in Berry:
```berry
# Define custom function - engine must be first parameter
def my_sparkle(engine, color, density, speed)
def my_twinkle(engine, color, count, period)
var anim = animation.twinkle_animation(engine)
anim.color = color
anim.density = density
anim.speed = speed
anim.count = count
anim.period = period
return anim
end
# Register for DSL use
animation.register_user_function("sparkle", my_sparkle)
animation.register_user_function("twinkle", my_twinkle)
```
```berry
# Use in DSL - engine is automatically passed
animation gold_sparkles = sparkle(0xFFD700, 8, 500ms)
run gold_sparkles
animation gold_twinkles = twinkle(0xFFD700, 8, 500ms)
run gold_twinkles
```
**Note**: The DSL automatically passes `engine` as the first argument to user functions.

View File

@ -56,6 +56,8 @@ transpile()
│ │ ├── process_restart_statement_fluent()
│ │ └── process_sequence_assignment_fluent()
│ ├── process_import() (direct Berry import generation)
│ ├── process_event_handler() (basic event system support)
│ ├── process_berry_code_block() (embed arbitrary Berry code)
│ ├── process_run() (collect for single engine.run())
│ └── process_property_assignment()
└── generate_engine_start() (single call for all run statements)
@ -217,24 +219,210 @@ is_anonymous_function(expr_str)
└── Skip closure wrapping for already-wrapped functions
```
## Enhanced Symbol Table System
The transpiler uses a sophisticated **SymbolTable** system for holistic symbol management and caching. This system provides dynamic symbol detection, type validation, and conflict prevention.
### SymbolTable Architecture
The symbol table consists of two main classes in `symbol_table.be`:
#### SymbolEntry Class
```
SymbolEntry
├── name: string # Symbol name
├── type: string # Symbol type classification
├── instance: object # Actual instance for validation
├── takes_args: boolean # Whether symbol accepts arguments
├── arg_type: string # "positional", "named", or "none"
└── is_builtin: boolean # Whether this is a built-in symbol from animation module
```
**Symbol Types Supported:**
- `"palette"` - Palette objects like `PALETTE_RAINBOW` (bytes instances)
- `"constant"` - Integer constants like `LINEAR`, `SINE`, `COSINE`
- `"math_function"` - Mathematical functions like `max`, `min`
- `"user_function"` - User-defined functions registered at runtime
- `"value_provider"` - Value provider constructors
- `"animation"` - Animation constructors
- `"color"` - Color definitions and providers
- `"variable"` - User-defined variables
- `"sequence"` - Sequence definitions
- `"template"` - Template definitions
#### SymbolTable Class
```
SymbolTable
├── entries: map # Map of name -> SymbolEntry
├── mock_engine: MockEngine # For validation testing
├── Dynamic Detection Methods:
│ ├── _detect_and_cache_symbol() # On-demand symbol detection
│ ├── contains() # Existence check with auto-detection
│ └── get() # Retrieval with auto-detection
├── Creation Methods:
│ ├── create_palette()
│ ├── create_color()
│ ├── create_animation()
│ ├── create_value_provider()
│ ├── create_variable()
│ ├── create_sequence()
│ └── create_template()
└── Validation Methods:
├── symbol_exists()
├── get_reference()
└── takes_args() / takes_positional_args() / takes_named_args()
```
### Dynamic Symbol Detection
The SymbolTable uses **lazy detection** to identify and cache symbols as they are encountered:
```
_detect_and_cache_symbol(name)
├── Check if already cached → return cached entry
├── Check animation module using introspection:
│ ├── Detect bytes() instances → create_palette()
│ ├── Detect integer constants (type == "int") → create_constant()
│ ├── Detect math functions in animation._math → create_math_function()
│ ├── Detect user functions via animation.is_user_function() → create_user_function()
│ ├── Test constructors with MockEngine:
│ │ ├── Create instance with mock_engine
│ │ ├── Check isinstance(instance, animation.value_provider) → create_value_provider()
│ │ └── Check isinstance(instance, animation.animation) → create_animation()
│ └── Cache result for future lookups
└── Return nil if not found (handled as user-defined)
```
### Symbol Type Detection Examples
**Palette Detection:**
```berry
# DSL: animation rainbow = rich_palette_animation(palette=PALETTE_RAINBOW)
# Detection: PALETTE_RAINBOW exists in animation module, isinstance(obj, bytes)
# Result: SymbolEntry("PALETTE_RAINBOW", "palette", bytes_instance, true)
# Reference: "animation.PALETTE_RAINBOW"
```
**Constant Detection:**
```berry
# DSL: animation wave = wave_animation(waveform=LINEAR)
# Detection: LINEAR exists in animation module, type(LINEAR) == "int"
# Result: SymbolEntry("LINEAR", "constant", 1, true)
# Reference: "animation.LINEAR"
```
**Math Function Detection:**
```berry
# DSL: animation.opacity = max(100, min(255, brightness))
# Detection: max exists in animation._math, is callable
# Result: SymbolEntry("max", "math_function", nil, true)
# Reference: "animation.max" (transformed to "animation._math.max" in closures)
```
**Value Provider Detection:**
```berry
# DSL: set oscillator = triangle(min_value=0, max_value=100, period=2s)
# Detection: triangle(mock_engine) creates instance, isinstance(instance, animation.value_provider)
# Result: SymbolEntry("triangle", "value_provider", instance, true)
# Reference: "animation.triangle"
```
**User Function Detection:**
```berry
# DSL: animation demo = rand_demo(color=red)
# Detection: animation.is_user_function("rand_demo") returns true
# Result: SymbolEntry("rand_demo", "user_function", nil, true)
# Reference: "rand_demo_" (handled specially in function calls)
```
### Symbol Conflict Prevention
The SymbolTable prevents symbol redefinition conflicts:
```
add(name, entry)
├── Check for built-in symbol conflicts:
│ ├── _detect_and_cache_symbol(name)
│ └── Raise "symbol_redefinition_error" if types differ
├── Check existing user-defined symbols:
│ ├── Compare entry.type with existing.type
│ └── Raise "symbol_redefinition_error" if types differ
├── Allow same-type updates (reassignment)
└── Return entry for method chaining
```
**Example Conflict Detection:**
```berry
# This would raise an error:
color max = #FF0000 # Conflicts with built-in math function "max"
# This would also raise an error:
color red = #FF0000
animation red = solid(color=blue) # Redefining "red" as different type
```
### Integration with Transpiler
The SymbolTable integrates seamlessly with the transpiler's processing flow:
### Performance Optimizations
**Caching Strategy:**
- **Lazy Detection**: Symbols detected only when first encountered
- **Instance Reuse**: MockEngine instances reused for validation
- **Introspection Caching**: Built-in symbol detection cached permanently
**Memory Efficiency:**
- **Minimal Storage**: Only essential information stored per symbol
- **Shared MockEngine**: Single MockEngine instance for all validation
- **Reference Counting**: Automatic cleanup of unused entries
### MockEngine Integration
The SymbolTable uses a lightweight MockEngine for constructor validation:
```
MockEngine
├── time_ms: 0 # Mock time for validation
├── get_strip_length(): 30 # Default strip length
└── Minimal interface for instance creation testing
```
**Usage in Detection:**
```berry
# Test if function creates value provider
try
var instance = factory_func(self.mock_engine)
if isinstance(instance, animation.value_provider)
return SymbolEntry.create_value_provider(name, instance, animation.value_provider)
end
except .. as e, msg
# Constructor failed - not a valid provider
end
```
## Validation System (Comprehensive)
The transpiler includes **extensive compile-time validation** with robust error handling:
### Factory Function Validation (Enhanced)
### Factory Function Validation (Simplified using SymbolTable)
```
_validate_factory_function(func_name, expected_base_class)
├── Check if function exists in animation module using introspection
├── Check if it's callable (function or class)
├── Create mock instance with MockEngine for type checking
├── Validate instance type matches expected base class
├── Handle mathematical functions separately (skip validation)
└── Graceful error handling with try/catch
_validate_animation_factory_exists(func_name)
├── Skip validation for mathematical functions
├── Use symbol_table.get(func_name) for dynamic detection
└── Return true if entry exists (any callable function is valid)
MockEngine class provides:
├── time_ms property for validation
├── get_strip_length() method returning default 30
└── Minimal interface for instance creation
_validate_animation_factory_creates_animation(func_name)
├── Use symbol_table.get(func_name) for dynamic detection
└── Return true if entry.type == "animation"
_validate_color_provider_factory_onsts(func_name)
├── Use symbol_table.get(func_name) for dynamic detection
└── Return true if entry exists (any callable function is valid)
_validate_value_provider_factory_exists(func_name)
├── Use symbol_table.get(func_name) for dynamic detection
└── Return true if entry.type == "value_provider"
```
### Parameter Validation (Real-time)
@ -246,33 +434,29 @@ _validate_single_parameter(func_name, param_name, animation_instance)
├── Validate immediately as parameters are parsed
└── Graceful error handling to ensure transpiler robustness
_create_instance_for_validation(func_name)
├── Create MockEngine for validation
├── Call factory function with mock engine
├── Return instance for parameter validation
└── Handle creation failures gracefully
_create_instance_for_validation(func_name) - Simplified using SymbolTable
├── Use symbol_table.get(func_name) for dynamic detection
└── Return entry.instance if available, nil otherwise
```
### Reference Validation (Consolidated)
### Reference Validation (Simplified using SymbolTable)
```
resolve_symbol_reference(name) - Unified symbol resolution
├── Check if it's a named color → get_named_color_value()
├── Check symbol_table for user-defined objects → name_
├── Check sequence_names for sequences → name_
├── Check animation module using introspection → animation.name
└── Default to user-defined format → name_
resolve_symbol_reference(name) - Simplified using SymbolTable
└── Use symbol_table.get_reference(name) for all symbol resolution
validate_symbol_reference(name, context) - With error reporting
├── Use symbol_exists() to check all sources
├── Use symbol_exists() to check symbol_table
├── Report detailed error with context information
└── Return validation status
symbol_exists(name) - Existence check
├── Check animation_dsl.is_color_name(name)
├── Check symbol_table.contains(name)
├── Check sequence_names.contains(name)
├── Check introspect.contains(animation, name)
└── Return boolean result
symbol_exists(name) - Simplified existence check
└── Use symbol_table.symbol_exists(name) for unified checking
_validate_value_provider_reference(object_name, context) - Simplified
├── Check symbol_exists() using symbol_table
├── Use symbol_table.get(name) for type information
├── Check entry.type == "value_provider" || entry.type == "animation"
└── Report detailed error messages for invalid types
```
### User Name Validation (Reserved Names)
@ -309,13 +493,15 @@ pulse_.color = animation.red
pulse_.period = 2000
```
### Symbol Resolution (Consolidated)
The transpiler resolves symbols at compile time using **unified resolution logic**:
```berry
# Built-in symbols from animation module → animation.symbol
animation.red, animation.PALETTE_RAINBOW, animation.SINE, animation.COSINE
**Template-Only Exception**: Files containing only template definitions skip engine initialization and `engine.run()` generation, producing pure function libraries.
# User-defined symbols → symbol_
### Symbol Resolution (Consolidated)
The transpiler resolves symbols at compile time using **unified resolution logic** based on the `is_builtin` flag:
```berry
# Built-in symbols (is_builtin=true) from animation module → animation.symbol
animation.linear, animation.PALETTE_RAINBOW, animation.SINE, animation.solid
# User-defined symbols (is_builtin=false) → symbol_
my_color_, my_animation_, my_sequence_
# Named colors → direct ARGB values (resolved at compile time)
@ -323,7 +509,7 @@ red → 0xFFFF0000, blue → 0xFF0000FF
# Template calls → template_function(engine, args)
my_template(red, 2s) → my_template_template(engine, 0xFFFF0000, 2000)
```
### Closure Generation (Enhanced)
Dynamic expressions are wrapped in closures with **mathematical function support**:
@ -333,7 +519,7 @@ Dynamic expressions are wrapped in closures with **mathematical function support
animation.opacity = animation.create_closure_value(engine,
def (self) return animation.resolve(strip_length_(engine)) / 2 + 50 end)
# DSL: animation.opacity = max(100, min(255, user.rand_demo() + 50))
# DSL: animation.opacity = max(100, min(255, rand_demo() + 50))
# Generated:
animation.opacity = animation.create_closure_value(engine,
def (self) return animation._math.max(100, animation._math.min(255, animation.get_user_function('rand_demo')(engine) + 50)) end)
@ -385,6 +571,8 @@ var demo_ = animation.SequenceManager(engine)
Templates are transpiled into Berry functions with **comprehensive parameter handling**:
**Template-Only Optimization**: Files containing only template definitions skip engine initialization and execution code generation, producing pure Berry function libraries.
```
process_template()
├── expect_identifier() → template name
@ -445,6 +633,7 @@ The transpiler provides **comprehensive error reporting** with graceful degradat
- **Reference validation** - Undefined object references with context information
- **Constraint validation** - Parameter values outside valid ranges
- **Type validation** - Incorrect parameter types with expected types
- **Safety validation** - Dangerous patterns that could cause memory leaks or performance issues
- **Template errors** - Template definition and call validation
- **Reserved name conflicts** - User names conflicting with built-ins
@ -509,7 +698,7 @@ get_error_report()
### User Function Integration
- **Template registration** as user functions with automatic naming
- **User function call detection** with user. prefix handling
- **User function call detection** usable as normal functions with positional arguments
- **Closure generation** for computed parameters with mathematical functions
- **Template call resolution** in multiple contexts (animation, property, standalone)
- **Import statement processing** for user function modules
@ -535,9 +724,86 @@ The refactored transpiler emphasizes:
1. **Simplicity** - Ultra-simplified single-pass architecture
2. **Robustness** - Comprehensive error handling and graceful degradation
3. **Validation** - Extensive compile-time validation with detailed error messages
4. **Flexibility** - Support for templates, multiple syntax variants, and user functions
5. **Performance** - Efficient processing with minimal memory overhead
6. **Maintainability** - Clear separation of concerns and unified processing methods
3. **Enhanced Symbol Management** - Dynamic SymbolTable system with intelligent caching and conflict detection
4. **Validation** - Extensive compile-time validation with detailed error messages
5. **Flexibility** - Support for templates, multiple syntax variants, and user functions
6. **Performance** - Efficient processing with minimal memory overhead and lazy symbol detection
7. **Maintainability** - Clear separation of concerns and unified processing methods
This architecture ensures robust, efficient transpilation from DSL to executable Berry code while providing comprehensive validation, detailed error reporting, and extensive language features.
## Recent Refactoring Improvements
### Code Simplification Using SymbolTable
The transpiler has been significantly refactored to leverage the `symbol_table.be` system more extensively:
#### **Factory Validation Simplification**
- **Before**: Complex validation with introspection and manual instance creation (~50 lines)
- **After**: Simple validation using symbol_table's dynamic detection (~25 lines)
- **Improvement**: 50% code reduction with better maintainability
#### **Symbol Resolution Consolidation**
- **Before**: Multiple separate checks for sequences, introspection, etc.
- **After**: Unified resolution through `symbol_table.get_reference()`
- **Improvement**: Single source of truth for all symbol resolution
#### **Duplicate Code Elimination**
- **Before**: Duplicate code patterns in `process_color()` and `process_animation()` methods
- **After**: Consolidated into reusable `_process_simple_value_assignment()` helper
- **Improvement**: 70% reduction in duplicate code blocks
#### **Legacy Variable Removal**
- **Before**: Separate tracking of sequences in `sequence_names` variable
- **After**: All symbols tracked uniformly in `symbol_table`
- **Improvement**: Eliminated redundancy and simplified state management
### Major Enhancements
**SymbolTable System:**
- **Dynamic Detection**: Automatically detects and caches symbol types as encountered
- **Conflict Prevention**: Prevents redefinition of symbols with different types
- **Performance Optimization**: Lazy loading and efficient symbol resolution for optimal performance
- **Type Safety**: Comprehensive type checking with MockEngine validation
- **Modular Design**: Separated into `symbol_table.be` for reusability
- **Constant Detection**: Added support for integer constants like `LINEAR`, `SINE`, `COSINE`
**Enhanced Symbol Detection:**
- **Palette Objects**: `PALETTE_RAINBOW``animation.PALETTE_RAINBOW`
- **Integer Constants**: `LINEAR`, `SINE`, `COSINE``animation.LINEAR`, `animation.SINE`, `animation.COSINE`
- **Math Functions**: `max`, `min``animation.max`, `animation.min` (transformed to `animation._math.*` in closures)
- **Value Providers**: `triangle`, `smooth``animation.triangle`, `animation.smooth`
- **Animation Constructors**: `solid`, `pulsating_animation``animation.solid`, `animation.pulsating_animation`
- **User-defined Symbols**: `my_color`, `my_animation``my_color_`, `my_animation_`
**Validation Improvements:**
- **Real-time Validation**: Parameter validation as symbols are parsed
- **Instance-based Checking**: Uses actual instances for accurate validation
- **Graceful Error Handling**: Robust error recovery with detailed error messages
- **Simplified Validation Methods**: Factory validation reduced from ~50 to ~25 lines using symbol_table
- **Unified Symbol Checking**: All symbol existence checks go through symbol_table system
- **Enhanced Type Detection**: Automatic detection of constants, palettes, functions, and constructors
This architecture ensures robust, efficient transpilation from DSL to executable Berry code while providing comprehensive validation, detailed error reporting, intelligent symbol management, and extensive language features.
### Symbol Reference Generation
The enhanced SymbolEntry system uses the `is_builtin` flag to determine correct reference generation:
```berry
# SymbolEntry.get_reference() method
def get_reference()
if self.is_builtin
return f"animation.{self.name}" # Built-in symbols: animation.LINEAR
else
return f"{self.name}_" # User-defined symbols: my_color_
end
end
```
**Examples:**
- **Built-in Constants**: `LINEAR``animation.LINEAR`
- **Built-in Functions**: `triangle``animation.triangle`
- **Built-in Palettes**: `PALETTE_RAINBOW``animation.PALETTE_RAINBOW`
- **User-defined Colors**: `my_red``my_red_`
- **User-defined Animations**: `pulse_anim``pulse_anim_`
This ensures consistent and correct symbol resolution throughout the transpilation process.

View File

@ -30,7 +30,7 @@ animation.register_user_function("breathing", my_breathing)
### 3. Use It in DSL
First, import your user functions module, then call your function with the `user.` prefix in computed parameters:
First, import your user functions module, then call your function directly in computed parameters:
```berry
# Import your user functions module
@ -38,10 +38,10 @@ import user_functions
# Use your custom function in computed parameters
animation calm = solid(color=blue)
calm.opacity = user.breathing_effect()
calm.opacity = breathing_effect()
animation energetic = solid(color=red)
energetic.opacity = user.breathing_effect()
energetic.opacity = breathing_effect()
sequence demo {
play calm for 10s
@ -61,9 +61,9 @@ The DSL supports importing Berry modules using the `import` keyword. This is the
# Import user functions at the beginning of your DSL file
import user_functions
# Now user functions are available with the user. prefix
# Now user functions are available directly
animation test = solid(color=blue)
test.opacity = user.my_function()
test.opacity = my_function()
```
### Import Behavior
@ -107,15 +107,15 @@ import user_functions
# Simple user function call
animation random_test = solid(color=red)
random_test.opacity = user.rand_demo()
random_test.opacity = rand_demo()
# User function with parameters
animation breathing_blue = solid(color=blue)
breathing_blue.opacity = user.breathing(128, 64)
breathing_blue.opacity = breathing(128, 64)
# User functions in mathematical expressions
animation complex = solid(color=green)
complex.opacity = max(50, min(255, user.rand_demo() + 100))
complex.opacity = max(50, min(255, rand_demo() + 100))
run random_test
```
@ -129,11 +129,11 @@ import user_functions # Basic user functions
import fire_effects # Fire animation functions
import color_utilities # Color manipulation functions
animation base = solid(color=user.random_color())
base.opacity = user.breathing(200, 50)
animation base = solid(color=random_color())
base.opacity = breathing(200, 50)
animation flames = solid(color=red)
flames.opacity = user.fire_intensity(180)
flames.opacity = fire_intensity(180)
```
## Common Patterns
@ -153,10 +153,10 @@ animation.register_user_function("bright", solid_bright)
```berry
animation bright_red = solid(color=red)
bright_red.opacity = user.bright(80)
bright_red.opacity = bright(80)
animation dim_blue = solid(color=blue)
dim_blue.opacity = user.bright(30)
dim_blue.opacity = bright(30)
```
### Fire Effects
@ -178,32 +178,32 @@ animation.register_user_function("fire", custom_fire)
```berry
animation campfire = solid(color=red)
campfire.opacity = user.fire(200, 2000)
campfire.opacity = fire(200, 2000)
animation torch = solid(color=orange)
torch.opacity = user.fire(255, 500)
torch.opacity = fire(255, 500)
```
### Sparkle Effects
### Twinkling Effects
```berry
def sparkles(engine, color, density, speed)
def twinkles(engine, color, count, period)
var anim = animation.twinkle_animation(engine)
anim.color = color
anim.density = density
anim.speed = speed
anim.count = count
anim.period = period
return anim
end
animation.register_user_function("sparkles", sparkles)
animation.register_user_function("twinkles", twinkles)
```
```berry
animation stars = solid(color=white)
stars.opacity = user.sparkles(12, 300)
stars.opacity = twinkles(12, 800ms)
animation fairy_dust = solid(color=#FFD700)
fairy_dust.opacity = user.sparkles(8, 500)
fairy_dust.opacity = twinkles(8, 600ms)
```
### Position-Based Effects
@ -223,10 +223,10 @@ animation.register_user_function("pulse_at", pulse_at)
```berry
animation left_pulse = solid(color=green)
left_pulse.position = user.pulse_at(5, 3, 2000)
left_pulse.position = pulse_at(5, 3, 2000)
animation right_pulse = solid(color=blue)
right_pulse.position = user.pulse_at(25, 3, 2000)
right_pulse.position = pulse_at(25, 3, 2000)
```
## Advanced Examples
@ -234,7 +234,7 @@ right_pulse.position = user.pulse_at(25, 3, 2000)
### Multi-Layer Effects
```berry
def rainbow_sparkle(engine, base_speed, sparkle_density)
def rainbow_twinkle(engine, base_speed, twinkle_density)
# Create base rainbow animation
var rainbow_provider = animation.rich_palette(engine)
rainbow_provider.palette = animation.PALETTE_RAINBOW
@ -291,7 +291,7 @@ animation.register_user_function("custom_palette", create_custom_palette)
```berry
# Use dynamic palette in DSL
animation gradient_effect = rich_palette(
palette=user.custom_palette(0xFF6B35, 5, 255)
palette=custom_palette(0xFF6B35, 5, 255)
cycle_period=4s
)
@ -325,13 +325,13 @@ animation.register_user_function("alert", gentle_alert)
```berry
animation emergency = solid(color=red)
emergency.opacity = user.strobe()
emergency.opacity = strobe()
animation notification = solid(color=yellow)
notification.opacity = user.alert()
notification.opacity = alert()
animation custom_police = solid(color=blue)
custom_police.opacity = user.police(500)
custom_police.opacity = police(500)
```
## Function Organization
@ -350,14 +350,14 @@ def fire_effect(engine, intensity, speed)
# ... implementation
end
def sparkle_effect(engine, color, density, speed)
def twinkle_effect(engine, color, count, period)
# ... implementation
end
# Register all functions
animation.register_user_function("breathing", breathing)
animation.register_user_function("fire", fire_effect)
animation.register_user_function("sparkle", sparkle_effect)
animation.register_user_function("twinkle", twinkle_effect)
print("Custom animations loaded!")
```
@ -456,7 +456,7 @@ User functions can be used in computed parameter expressions alongside mathemati
```berry
# Simple user function call in property assignment
animation base = solid(color=blue, priority=10)
base.opacity = user.rand_demo() # User function as computed parameter
base.opacity = rand_demo() # User function as computed parameter
```
### User Functions with Mathematical Operations
@ -468,7 +468,7 @@ set strip_len = strip_length()
# Mix user functions with mathematical functions
animation dynamic_solid = solid(
color=purple
opacity=max(50, min(255, user.rand_demo() + 100)) # Random opacity with bounds
opacity=max(50, min(255, rand_demo() + 100)) # Random opacity with bounds
priority=15
)
```
@ -479,7 +479,7 @@ animation dynamic_solid = solid(
# Use user function in arithmetic expressions
animation random_effect = solid(
color=cyan
opacity=abs(user.rand_demo() - 128) + 64 # Random variation around middle value
opacity=abs(rand_demo() - 128) + 64 # Random variation around middle value
priority=12
)
```
@ -496,7 +496,7 @@ When you use user functions in computed parameters:
**Generated Code Example:**
```berry
# DSL code
animation.opacity = max(100, user.breathing(red, 2000))
animation.opacity = max(100, breathing(red, 2000))
```
**Transpiles to:**
@ -513,7 +513,7 @@ The following user functions are available by default:
| Function | Parameters | Description |
|----------|------------|-------------|
| `user.rand_demo()` | none | Returns a random value (0-255) for demonstration |
| `rand_demo()` | none | Returns a random value (0-255) for demonstration |
### Best Practices for Computed Parameters
@ -537,13 +537,13 @@ var dsl_code =
"import user_functions\n"
"\n"
"animation my_fire = solid(color=red)\n"
"my_fire.opacity = user.fire(200, 1500)\n"
"animation my_sparkles = solid(color=white)\n"
"my_sparkles.opacity = user.sparkle(8, 400)\n"
"my_fire.opacity = fire(200, 1500)\n"
"animation my_twinkles = solid(color=white)\n"
"my_twinkles.opacity = twinkle(8, 400ms)\n"
"\n"
"sequence show {\n"
" play my_fire for 10s\n"
" play my_sparkles for 5s\n"
" play my_twinkles for 5s\n"
"}\n"
"\n"
"run show"
@ -559,9 +559,9 @@ var my_show =
"import user_functions\n"
"\n"
"animation campfire = solid(color=orange)\n"
"campfire.opacity = user.fire(180, 2000)\n"
"campfire.opacity = fire(180, 2000)\n"
"animation stars = solid(color=#FFFFFF)\n"
"stars.opacity = user.sparkle(6, 600)\n"
"stars.opacity = twinkle(6, 600ms)\n"
"\n"
"sequence night_scene {\n"
" play campfire for 30s\n"

View File

@ -107,6 +107,8 @@ import "providers/oscillator_value_provider.be" as oscillator_value_provider
register_to_animation(oscillator_value_provider)
import "providers/strip_length_provider.be" as strip_length_provider
register_to_animation(strip_length_provider)
import "providers/iteration_number_provider.be" as iteration_number_provider
register_to_animation(iteration_number_provider)
import "providers/closure_value_provider.be" as closure_value_provider
register_to_animation(closure_value_provider)
@ -145,24 +147,26 @@ import "animations/gradient" as gradient_animation
register_to_animation(gradient_animation)
import "animations/noise" as noise_animation
register_to_animation(noise_animation)
import "animations/plasma" as plasma_animation
register_to_animation(plasma_animation)
import "animations/sparkle" as sparkle_animation
register_to_animation(sparkle_animation)
# import "animations/plasma" as plasma_animation
# register_to_animation(plasma_animation)
# import "animations/sparkle" as sparkle_animation
# register_to_animation(sparkle_animation)
import "animations/wave" as wave_animation
register_to_animation(wave_animation)
import "animations/shift" as shift_animation
register_to_animation(shift_animation)
import "animations/bounce" as bounce_animation
register_to_animation(bounce_animation)
import "animations/scale" as scale_animation
register_to_animation(scale_animation)
import "animations/jitter" as jitter_animation
register_to_animation(jitter_animation)
# import "animations/shift" as shift_animation
# register_to_animation(shift_animation)
# import "animations/bounce" as bounce_animation
# register_to_animation(bounce_animation)
# import "animations/scale" as scale_animation
# register_to_animation(scale_animation)
# import "animations/jitter" as jitter_animation
# register_to_animation(jitter_animation)
# Import palette examples
import "animations/palettes" as palettes
register_to_animation(palettes)
# import "animations/all_wled_palettes" as all_wled_palettes
# register_to_animation(all_wled_palettes)
# Import specialized animation classes
import "animations/rich_palette_animation" as rich_palette_animation

View File

@ -25,6 +25,7 @@ import animation
# We don't include it to not create a closure, but use the global instead
# Create the DSL module and make it globally accessible
#@ solidify:animation_dsl.SimpleDSLTranspiler.ExpressionResult,weak
#@ solidify:animation_dsl,weak
var animation_dsl = module("animation_dsl")
global.animation_dsl = animation_dsl
@ -46,8 +47,12 @@ import "dsl/lexer.be" as dsl_lexer
register_to_dsl(dsl_lexer)
import "dsl/transpiler.be" as dsl_transpiler
register_to_dsl(dsl_transpiler)
import "dsl/symbol_table.be" as dsl_symbol_table
register_to_dsl(dsl_symbol_table)
import "dsl/runtime.be" as dsl_runtime
register_to_dsl(dsl_runtime)
import "dsl/named_colors.be" as dsl_named_colors
register_to_dsl(dsl_named_colors)
# Main DSL compilation function
# Compiles DSL source code to Berry code

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ class CometAnimation : animation.animation
# Parameter definitions following parameterized class specification
static var PARAMS = {
"color": {"default": 0xFFFFFFFF}, # Color for the comet head (32-bit ARGB value)
# 'color' for the comet head (32-bit ARGB value), inherited from animation class
"tail_length": {"min": 1, "max": 50, "default": 5}, # Length of the comet tail in pixels
"speed": {"min": 1, "max": 25600, "default": 2560}, # Movement speed in 1/256th pixels per second
"direction": {"enum": [-1, 1], "default": 1}, # Direction of movement (1 = forward, -1 = backward)
@ -58,6 +58,9 @@ class CometAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Cache parameter values for performance (read once, use multiple times)
var current_speed = self.speed
var current_direction = self.direction
@ -179,8 +182,6 @@ class CometAnimation : animation.animation
return true
end
# String representation of the animation
def tostring()
var color_str
@ -193,4 +194,4 @@ class CometAnimation : animation.animation
end
end
return {'comet_animation': CometAnimation}
return {'comet_animation': CometAnimation}

View File

@ -24,12 +24,12 @@ class CrenelPositionAnimation : animation.animation
# Parameter definitions with constraints
static var PARAMS = {
"color": {"default": 0xFFFFFFFF},
"back_color": {"default": 0xFF000000},
"pos": {"default": 0},
"pulse_size": {"min": 0, "default": 1},
"low_size": {"min": 0, "default": 3},
"nb_pulse": {"default": -1}
# 'color' for the comet head (32-bit ARGB value), inherited from animation class
"back_color": {"default": 0xFF000000}, # background color, TODO change to transparent
"pos": {"default": 0}, # start of the pulse (in pixel)
"pulse_size": {"min": 0, "default": 1}, # number of pixels of the pulse
"low_size": {"min": 0, "default": 3}, # number of pixel until next pos - full cycle is 2 + 3
"nb_pulse": {"default": -1} # number of pulses, or `-1` for infinite
}
# Render the crenel pattern to the provided frame buffer
@ -127,4 +127,4 @@ class CrenelPositionAnimation : animation.animation
end
end
return {'crenel_position_animation': CrenelPositionAnimation}
return {'crenel_position_animation': CrenelPositionAnimation}

View File

@ -6,14 +6,14 @@
#@ solidify:FireAnimation,weak
class FireAnimation : animation.animation
# Non-parameter instance variables only
var heat_map # Array storing heat values for each pixel (0-255)
var current_colors # Array of current colors for each pixel
var heat_map # bytes() buffer storing heat values for each pixel (0-255)
var current_colors # bytes() buffer storing ARGB colors (4 bytes per pixel)
var last_update # Last update time for flicker timing
var random_seed # Seed for random number generation
# Parameter definitions following parameterized class specification
static var PARAMS = {
"color": {"default": nil},
# 'color' for the comet head (32-bit ARGB value), inherited from animation class
"intensity": {"min": 0, "max": 255, "default": 180},
"flicker_speed": {"min": 1, "max": 20, "default": 8},
"flicker_amount": {"min": 0, "max": 255, "default": 100},
@ -29,8 +29,8 @@ class FireAnimation : animation.animation
super(self).init(engine)
# Initialize non-parameter instance variables only
self.heat_map = []
self.current_colors = []
self.heat_map = bytes() # Use bytes() buffer for efficient 0-255 value storage
self.current_colors = bytes() # Use bytes() buffer for ARGB colors (4 bytes per pixel)
self.last_update = 0
# Initialize random seed using engine time
@ -40,14 +40,19 @@ class FireAnimation : animation.animation
# Initialize buffers based on current strip length
def _initialize_buffers()
var strip_length = self.engine.get_strip_length()
self.heat_map.resize(strip_length)
self.current_colors.resize(strip_length)
# Initialize all pixels to zero heat
# Create new bytes() buffer for heat values (1 byte per pixel)
self.heat_map.clear()
self.heat_map.resize(strip_length)
# Create new bytes() buffer for colors (4 bytes per pixel: ARGB)
self.current_colors.clear()
self.current_colors.resize(strip_length * 4)
# Initialize all pixels to zero heat and black color (0xFF000000)
var i = 0
while i < strip_length
self.heat_map[i] = 0
self.current_colors[i] = 0xFF000000 # Black with full alpha
self.current_colors.set(i * 4, 0xFF000000, -4) # Black with full alpha
i += 1
end
end
@ -77,6 +82,9 @@ class FireAnimation : animation.animation
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Check if it's time to update the fire simulation
# Update frequency is based on flicker_speed (Hz)
var flicker_speed = self.flicker_speed # Cache parameter value
@ -99,8 +107,8 @@ class FireAnimation : animation.animation
var color_param = self.color
var strip_length = self.engine.get_strip_length()
# Ensure buffers are correct size
if size(self.heat_map) != strip_length
# Ensure buffers are correct size (bytes() uses .size() method)
if self.heat_map.size() != strip_length || self.current_colors.size() != strip_length * 4
self._initialize_buffers()
end
@ -122,7 +130,13 @@ class FireAnimation : animation.animation
var k = strip_length - 1
while k >= 2
var heat_avg = (self.heat_map[k-1] + self.heat_map[k-2] + self.heat_map[k-2]) / 3
self.heat_map[k] = heat_avg
# Ensure the result is an integer in valid range (0-255)
if heat_avg < 0
heat_avg = 0
elif heat_avg > 255
heat_avg = 255
end
self.heat_map[k] = int(heat_avg)
k -= 1
end
end
@ -130,7 +144,11 @@ class FireAnimation : animation.animation
# Step 3: Randomly ignite new 'sparks' of heat near the bottom
if self._random_range(255) < sparking_rate
var spark_pos = self._random_range(7) # Sparks only in bottom 7 pixels
var spark_heat = self._random_range(95) + 160 # Heat between 160-255
var spark_heat = self._random_range(95) + 160 # Heat between 160-254
# Ensure spark heat is in valid range (should already be, but be explicit)
if spark_heat > 255
spark_heat = 255
end
if spark_pos < strip_length
self.heat_map[spark_pos] = spark_heat
end
@ -205,7 +223,7 @@ class FireAnimation : animation.animation
end
end
self.current_colors[i] = color
self.current_colors.set(i * 4, color, -4)
i += 1
end
end
@ -229,7 +247,7 @@ class FireAnimation : animation.animation
var i = 0
while i < strip_length
if i < frame.width
frame.set_pixel_color(i, self.current_colors[i])
frame.set_pixel_color(i, self.current_colors.get(i * 4, -4))
end
i += 1
end

View File

@ -9,7 +9,7 @@ class TwinkleAnimation : animation.animation
# Non-parameter instance variables only
var twinkle_states # Array storing twinkle state for each pixel
var current_colors # Array of current colors for each pixel
var current_colors # bytes() buffer storing ARGB colors (4 bytes per pixel)
var last_update # Last update time for timing
var random_seed # Seed for random number generation
@ -32,7 +32,7 @@ class TwinkleAnimation : animation.animation
# Initialize non-parameter instance variables only
self.twinkle_states = []
self.current_colors = []
self.current_colors = bytes() # Use bytes() buffer for ARGB colors (4 bytes per pixel)
self.last_update = 0
# Initialize random seed using engine time
@ -48,13 +48,16 @@ class TwinkleAnimation : animation.animation
# Resize arrays
self.twinkle_states.resize(strip_length)
self.current_colors.resize(strip_length)
# Create new bytes() buffer for colors (4 bytes per pixel: ARGB)
self.current_colors.clear()
self.current_colors.resize(strip_length * 4)
# Initialize all pixels to off state
var i = 0
while i < strip_length
self.twinkle_states[i] = 0 # 0 = off, >0 = brightness level
self.current_colors[i] = 0x00000000 # Transparent (alpha = 0)
self.current_colors.set(i * 4, 0x00000000, -4) # Transparent (alpha = 0)
i += 1
end
end
@ -133,14 +136,14 @@ class TwinkleAnimation : animation.animation
var strip_length = self.engine.get_strip_length()
# Ensure arrays are properly sized
if size(self.twinkle_states) != strip_length
if size(self.twinkle_states) != strip_length || self.current_colors.size() != strip_length * 4
self._initialize_arrays()
end
# Step 1: Fade existing twinkles by reducing alpha
var i = 0
while i < strip_length
var current_color = self.current_colors[i]
var current_color = self.current_colors.get(i * 4, -4)
var alpha = (current_color >> 24) & 0xFF
if alpha > 0
@ -149,12 +152,12 @@ class TwinkleAnimation : animation.animation
if alpha <= fade_amount
# Star has faded completely - reset to transparent
self.twinkle_states[i] = 0
self.current_colors[i] = 0x00000000
self.current_colors.set(i * 4, 0x00000000, -4)
else
# Reduce alpha while keeping RGB components unchanged
var new_alpha = alpha - fade_amount
var rgb = current_color & 0x00FFFFFF # Keep RGB, clear alpha
self.current_colors[i] = (new_alpha << 24) | rgb
self.current_colors.set(i * 4, (new_alpha << 24) | rgb, -4)
end
end
i += 1
@ -181,7 +184,7 @@ class TwinkleAnimation : animation.animation
# Create new star with full-brightness color and variable alpha
self.twinkle_states[j] = 1 # Mark as active (non-zero)
self.current_colors[j] = (star_alpha << 24) | (r << 16) | (g << 8) | b
self.current_colors.set(j * 4, (star_alpha << 24) | (r << 16) | (g << 8) | b, -4)
end
end
j += 1
@ -204,7 +207,7 @@ class TwinkleAnimation : animation.animation
var strip_length = self.engine.get_strip_length()
# Ensure arrays are properly sized
if size(self.twinkle_states) != strip_length
if size(self.twinkle_states) != strip_length || self.current_colors.size() != strip_length * 4
self._initialize_arrays()
end
@ -213,7 +216,7 @@ class TwinkleAnimation : animation.animation
var i = 0
while i < strip_length
if i < frame.width
var color = self.current_colors[i]
var color = self.current_colors.get(i * 4, -4)
# Only set pixels that have some alpha (are visible)
if (color >> 24) & 0xFF > 0
frame.set_pixel_color(i, color)

View File

@ -19,6 +19,9 @@ class AnimationEngine
# Performance optimization
var render_needed # Whether a render pass is needed
# Sequence iteration tracking (stack-based for nested sequences)
var iteration_stack # Stack of iteration numbers for nested sequences
# Initialize the animation engine for a specific LED strip
def init(strip)
if strip == nil
@ -40,6 +43,9 @@ class AnimationEngine
self.time_ms = 0
self.fast_loop_closure = nil
self.render_needed = false
# Initialize iteration tracking stack
self.iteration_stack = []
end
# Run the animation engine
@ -454,6 +460,46 @@ class AnimationEngine
self.strip = nil
end
# Sequence iteration tracking methods
# Push a new iteration context onto the stack
# Called when a sequence starts repeating
#
# @param iteration_number: int - The current iteration number (0-based)
def push_iteration_context(iteration_number)
self.iteration_stack.push(iteration_number)
end
# Pop the current iteration context from the stack
# Called when a sequence finishes repeating
def pop_iteration_context()
if size(self.iteration_stack) > 0
return self.iteration_stack.pop()
end
return nil
end
# Update the current iteration number in the top context
# Called when a sequence advances to the next iteration
#
# @param iteration_number: int - The new iteration number (0-based)
def update_current_iteration(iteration_number)
if size(self.iteration_stack) > 0
self.iteration_stack[-1] = iteration_number
end
end
# Get the current iteration number from the innermost sequence context
# Used by IterationNumberProvider to return the current iteration
#
# @return int|nil - Current iteration number (0-based) or nil if not in sequence
def get_current_iteration_number()
if size(self.iteration_stack) > 0
return self.iteration_stack[-1]
end
return nil
end
# String representation
def tostring()
return f"AnimationEngine(running={self.is_running}, animations={size(self.animations)}, width={self.width})"

View File

@ -90,6 +90,11 @@ class SequenceManager
self.current_iteration = 0
self.is_running = true
# Push iteration context to engine stack if this is a repeat sequence
if self.is_repeat_sequence
self.engine.push_iteration_context(self.current_iteration)
end
# Start executing if we have steps
if size(self.steps) > 0
# Execute all consecutive closure steps at the beginning atomically
@ -120,6 +125,11 @@ class SequenceManager
if self.is_running
self.is_running = false
# Pop iteration context from engine stack if this is a repeat sequence
if self.is_repeat_sequence
self.engine.pop_iteration_context()
end
# Stop any currently playing animations
if self.step_index < size(self.steps)
var current_step = self.steps[self.step_index]
@ -347,6 +357,11 @@ class SequenceManager
def complete_iteration(current_time)
self.current_iteration += 1
# Update iteration context in engine stack if this is a repeat sequence
if self.is_repeat_sequence
self.engine.update_current_iteration(self.current_iteration)
end
# Resolve repeat count (may be a function)
var resolved_repeat_count = self.get_resolved_repeat_count()
@ -376,6 +391,11 @@ class SequenceManager
else
# All iterations complete
self.is_running = false
# Pop iteration context from engine stack if this is a repeat sequence
if self.is_repeat_sequence
self.engine.pop_iteration_context()
end
end
end

View File

@ -68,7 +68,13 @@ class DSLLexer
elif self.is_digit(ch)
self.scan_number()
elif ch == '"' || ch == "'"
self.scan_string(ch)
# Check for triple quotes
if (ch == '"' && self.peek() == '"' && self.peek_ahead(1) == '"') ||
(ch == "'" && self.peek() == "'" && self.peek_ahead(1) == "'")
self.scan_triple_quoted_string(ch)
else
self.scan_string(ch)
end
elif ch == '$'
self.scan_variable_reference()
else
@ -268,6 +274,49 @@ class DSLLexer
end
end
# Scan triple-quoted string literal (for berry code blocks)
def scan_triple_quoted_string(quote_char)
var start_pos = self.position - 1 # Include first opening quote
var start_column = self.column - 1
var value = ""
# Consume the two remaining opening quotes
self.advance() # second quote
self.advance() # third quote
# Look for the closing triple quotes
while !self.at_end()
var ch = self.peek()
# Check for closing triple quotes
if ch == quote_char &&
self.peek_ahead(1) == quote_char &&
self.peek_ahead(2) == quote_char
# Found closing triple quotes - consume them
self.advance() # first closing quote
self.advance() # second closing quote
self.advance() # third closing quote
break
end
# Regular character - add to value
ch = self.advance()
if ch == '\n'
self.line += 1
self.column = 1
end
value += ch
end
# Check if we reached end without finding closing quotes
if self.at_end() && !(self.source[self.position-3..self.position-1] == quote_char + quote_char + quote_char)
self.add_error("Unterminated triple-quoted string literal")
self.add_token(39 #-animation_dsl.Token.ERROR-#, value, self.position - start_pos)
else
self.add_token(3 #-animation_dsl.Token.STRING-#, value, self.position - start_pos)
end
end
# Scan variable reference ($identifier)
def scan_variable_reference()
var start_pos = self.position - 1 # Include $
@ -419,6 +468,14 @@ class DSLLexer
return self.source[self.position + 1]
end
# Peek ahead by n characters without advancing
def peek_ahead(n)
if self.position + n >= size(self.source)
return ""
end
return self.source[self.position + n]
end
# Check if current character matches expected and advance if so
def match(expected)
if self.at_end() || self.source[self.position] != expected

View File

@ -0,0 +1,62 @@
# Named Colors Module for Animation DSL
# Provides color name to ARGB value mappings for the DSL transpiler
# Static color mapping for named colors (helps with solidification)
# Maps color names to ARGB hex values (0xAARRGGBB format)
# All colors have full alpha (0xFF) except transparent
var named_colors = {
# Primary colors
"red": "0xFFFF0000", # Pure red
"green": "0xFF008000", # HTML/CSS standard green (darker, more readable)
"blue": "0xFF0000FF", # Pure blue
# Achromatic colors
"white": "0xFFFFFFFF", # Pure white
"black": "0xFF000000", # Pure black
"gray": "0xFF808080", # Medium gray
"grey": "0xFF808080", # Alternative spelling
"silver": "0xFFC0C0C0", # Light gray
# Secondary colors
"yellow": "0xFFFFFF00", # Pure yellow (red + green)
"cyan": "0xFF00FFFF", # Pure cyan (green + blue)
"magenta": "0xFFFF00FF", # Pure magenta (red + blue)
# Extended web colors
"orange": "0xFFFFA500", # Orange
"purple": "0xFF800080", # Purple (darker magenta)
"pink": "0xFFFFC0CB", # Light pink
"lime": "0xFF00FF00", # Pure green (HTML/CSS lime = full intensity)
"navy": "0xFF000080", # Dark blue
"olive": "0xFF808000", # Dark yellow-green
"maroon": "0xFF800000", # Dark red
"teal": "0xFF008080", # Dark cyan
"aqua": "0xFF00FFFF", # Same as cyan
"fuchsia": "0xFFFF00FF", # Same as magenta
# Precious metals
"gold": "0xFFFFD700", # Metallic gold
# Natural colors
"brown": "0xFFA52A2A", # Saddle brown
"tan": "0xFFD2B48C", # Light brown/beige
"beige": "0xFFF5F5DC", # Very light brown
"ivory": "0xFFFFFFF0", # Off-white with yellow tint
"snow": "0xFFFFFAFA", # Off-white with slight blue tint
# Flower/nature colors
"indigo": "0xFF4B0082", # Deep blue-purple
"violet": "0xFFEE82EE", # Light purple
"crimson": "0xFFDC143C", # Deep red
"coral": "0xFFFF7F50", # Orange-pink
"salmon": "0xFFFA8072", # Pink-orange
"khaki": "0xFFF0E68C", # Pale yellow-brown
"plum": "0xFFDDA0DD", # Light purple
"orchid": "0xFFDA70D6", # Medium purple
"turquoise": "0xFF40E0D0", # Blue-green
# Special
"transparent": "0x00000000" # Fully transparent (alpha = 0)
}
return {"named_colors": named_colors}

View File

@ -0,0 +1,601 @@
# Symbol Table Classes for DSL Transpiler
# Enhanced symbol caching and management for the Animation DSL
# Symbol table entry class for enhanced symbol caching
#@ solidify:SymbolEntry,weak
class SymbolEntry
# Type constants
static var TYPE_PALETTE_CONSTANT = 1
static var TYPE_PALETTE = 2
static var TYPE_CONSTANT = 3
static var TYPE_MATH_FUNCTION = 4
static var TYPE_USER_FUNCTION = 5
static var TYPE_VALUE_PROVIDER_CONSTRUCTOR = 6
static var TYPE_VALUE_PROVIDER = 7
static var TYPE_ANIMATION_CONSTRUCTOR = 8
static var TYPE_ANIMATION = 9
static var TYPE_COLOR_CONSTRUCTOR = 10
static var TYPE_COLOR = 11
static var TYPE_VARIABLE = 12
static var TYPE_SEQUENCE = 13
static var TYPE_TEMPLATE = 14
var name # Symbol name
var type # Symbol type (int constant)
var instance # Actual instance (for validation) or nil
var takes_args # Boolean: whether this symbol takes arguments
var arg_type # "positional", "named", or "none"
var is_builtin # Boolean: whether this is a built-in symbol from animation module
var is_dangerous # Boolean: whether calling this symbol creates a new instance (dangerous in computed expressions)
var param_types # Map of parameter names to types (for templates and user functions)
def init(name, typ, instance, is_builtin)
self.name = name
self.type = typ
self.instance = instance
self.is_builtin = is_builtin != nil ? is_builtin : false
self.takes_args = false
self.arg_type = "none"
self.is_dangerous = false
self.param_types = {}
# Auto-detect argument characteristics and danger level based on type
self._detect_arg_characteristics()
self._detect_danger_level()
end
# Detect if this symbol takes arguments and what type
def _detect_arg_characteristics()
if self.type == self.TYPE_PALETTE_CONSTANT || self.type == self.TYPE_PALETTE || self.type == self.TYPE_CONSTANT
# Palette objects and constants don't take arguments
self.takes_args = false
self.arg_type = "none"
elif self.type == self.TYPE_MATH_FUNCTION
# Math functions like max, min take positional arguments
self.takes_args = true
self.arg_type = "positional"
elif self.type == self.TYPE_USER_FUNCTION
# User functions take positional arguments (engine + user args)
self.takes_args = true
self.arg_type = "positional"
elif self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR || self.type == self.TYPE_ANIMATION_CONSTRUCTOR || self.type == self.TYPE_COLOR_CONSTRUCTOR
# Constructor functions take named arguments
self.takes_args = true
self.arg_type = "named"
else
# Instances, variables, sequences, templates don't take arguments when referenced
self.takes_args = false
self.arg_type = "none"
end
end
# Detect if this symbol is dangerous (creates new instances when called)
def _detect_danger_level()
if self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR
# Value provider constructors create new instances - dangerous in computed expressions
self.is_dangerous = true
elif self.type == self.TYPE_ANIMATION_CONSTRUCTOR
# Animation constructors create new instances - dangerous in computed expressions
self.is_dangerous = true
elif self.type == self.TYPE_COLOR_CONSTRUCTOR
# Color provider constructors create new instances - dangerous in computed expressions
self.is_dangerous = true
else
# Constants, math functions, variables, instances, user functions, etc. are safe
self.is_dangerous = false
end
end
# Check if this symbol is a bytes() instance (for palettes)
def is_bytes_instance()
return (self.type == self.TYPE_PALETTE_CONSTANT || self.type == self.TYPE_PALETTE) && self.instance != nil && isinstance(self.instance, bytes)
end
# Check if this symbol is a math function
def is_math_function()
return self.type == self.TYPE_MATH_FUNCTION
end
# Check if this symbol is a user function
def is_user_function()
return self.type == self.TYPE_USER_FUNCTION
end
# Check if this symbol is a value provider constructor
def is_value_provider_constructor()
return self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR
end
# Check if this symbol is a value provider instance
def is_value_provider_instance()
return self.type == self.TYPE_VALUE_PROVIDER
end
# Check if this symbol is an animation constructor
def is_animation_constructor()
return self.type == self.TYPE_ANIMATION_CONSTRUCTOR
end
# Check if this symbol is an animation instance
def is_animation_instance()
return self.type == self.TYPE_ANIMATION
end
# Check if this symbol is a color constructor
def is_color_constructor()
return self.type == self.TYPE_COLOR_CONSTRUCTOR
end
# Check if this symbol is a color instance
def is_color_instance()
return self.type == self.TYPE_COLOR
end
# Check if this symbol takes positional arguments
def takes_positional_args()
return self.takes_args && self.arg_type == "positional"
end
# Check if this symbol takes named arguments
def takes_named_args()
return self.takes_args && self.arg_type == "named"
end
# Check if this symbol is dangerous (creates new instances when called)
def is_dangerous_call()
return self.is_dangerous
end
# Set parameter types for templates and user functions
def set_param_types(param_types)
self.param_types = param_types != nil ? param_types : {}
end
# Get parameter types
def get_param_types()
return self.param_types
end
# Convert type constant to string for debugging
def type_to_string()
if self.type == self.TYPE_PALETTE_CONSTANT return "palette_constant"
elif self.type == self.TYPE_PALETTE return "palette"
elif self.type == self.TYPE_CONSTANT return "constant"
elif self.type == self.TYPE_MATH_FUNCTION return "math_function"
elif self.type == self.TYPE_USER_FUNCTION return "user_function"
elif self.type == self.TYPE_VALUE_PROVIDER_CONSTRUCTOR return "value_provider_constructor"
elif self.type == self.TYPE_VALUE_PROVIDER return "value_provider"
elif self.type == self.TYPE_ANIMATION_CONSTRUCTOR return "animation_constructor"
elif self.type == self.TYPE_ANIMATION return "animation"
elif self.type == self.TYPE_COLOR_CONSTRUCTOR return "color_constructor"
elif self.type == self.TYPE_COLOR return "color"
elif self.type == self.TYPE_VARIABLE return "variable"
elif self.type == self.TYPE_SEQUENCE return "sequence"
elif self.type == self.TYPE_TEMPLATE return "template"
else return f"unknown({self.type})"
end
end
# Get the resolved symbol reference for code generation
def get_reference()
# Generate appropriate reference based on whether it's built-in
if self.is_builtin
# Special handling for math functions
if self.type == self.TYPE_MATH_FUNCTION
return f"animation._math.{self.name}"
else
return f"animation.{self.name}"
end
else
# User-defined symbols get underscore suffix
return f"{self.name}_"
end
end
# String representation for debugging
def tostring()
import string
var instance_str = "nil"
if self.instance != nil
var instance_type = type(self.instance)
if instance_type == "instance"
instance_str = f"<{classname(self.instance)}>"
else
instance_str = f"<{instance_type}:{str(self.instance)}>"
end
end
var param_types_str = ""
if size(self.param_types) > 0
var params_list = ""
var first = true
for key : self.param_types.keys()
if !first
params_list += ","
end
params_list += f"{key}:{self.param_types[key]}"
first = false
end
param_types_str = f" params=[{params_list}]"
end
return f"SymbolEntry(name='{self.name}', type='{self.type_to_string()}', instance={instance_str}, " +
f"takes_args={self.takes_args}, arg_type='{self.arg_type}', " +
f"is_builtin={self.is_builtin}, is_dangerous={self.is_dangerous}{param_types_str})"
end
# Create a symbol entry for a palette constant (built-in like PALETTE_RAINBOW)
static def create_palette_constant(name, instance, is_builtin)
return _class(name, _class.TYPE_PALETTE_CONSTANT, instance, is_builtin)
end
# Create a symbol entry for a palette instance (user-defined)
static def create_palette_instance(name, instance, is_builtin)
return _class(name, _class.TYPE_PALETTE, instance, is_builtin)
end
# Create a symbol entry for an integer constant
static def create_constant(name, instance, is_builtin)
return _class(name, _class.TYPE_CONSTANT, instance, is_builtin)
end
# Create a symbol entry for a math function
static def create_math_function(name, is_builtin)
return _class(name, _class.TYPE_MATH_FUNCTION, nil, is_builtin)
end
# Create a symbol entry for a user function
static def create_user_function(name, is_builtin)
return _class(name, _class.TYPE_USER_FUNCTION, nil, is_builtin)
end
# Create a symbol entry for a value provider constructor (built-in like triangle, smooth)
static def create_value_provider_constructor(name, instance, is_builtin)
return _class(name, _class.TYPE_VALUE_PROVIDER_CONSTRUCTOR, instance, is_builtin)
end
# Create a symbol entry for a value provider instance (user-defined)
static def create_value_provider_instance(name, instance, is_builtin)
return _class(name, _class.TYPE_VALUE_PROVIDER, instance, is_builtin)
end
# Create a symbol entry for an animation constructor (built-in like solid, pulsating_animation)
static def create_animation_constructor(name, instance, is_builtin)
return _class(name, _class.TYPE_ANIMATION_CONSTRUCTOR, instance, is_builtin)
end
# Create a symbol entry for an animation instance (user-defined)
static def create_animation_instance(name, instance, is_builtin)
return _class(name, _class.TYPE_ANIMATION, instance, is_builtin)
end
# Create a symbol entry for a color constructor (built-in like color_cycle, breathe_color)
static def create_color_constructor(name, instance, is_builtin)
return _class(name, _class.TYPE_COLOR_CONSTRUCTOR, instance, is_builtin)
end
# Create a symbol entry for a color instance (user-defined)
static def create_color_instance(name, instance, is_builtin)
return _class(name, _class.TYPE_COLOR, instance, is_builtin)
end
# Create a symbol entry for a variable
static def create_variable(name, is_builtin)
return _class(name, _class.TYPE_VARIABLE, nil, is_builtin)
end
# Create a symbol entry for a sequence
static def create_sequence(name, is_builtin)
return _class(name, _class.TYPE_SEQUENCE, nil, is_builtin)
end
# Create a symbol entry for a template
static def create_template(name, is_builtin)
return _class(name, _class.TYPE_TEMPLATE, nil, is_builtin)
end
end
# Mock engine class for parameter validation during transpilation
class MockEngine
var time_ms
def init()
self.time_ms = 0
end
def get_strip_length()
return 30 # Default strip length for validation
end
end
# Enhanced symbol table class for holistic symbol management and caching
#@ solidify:SymbolTable,weak
class SymbolTable
var entries # Map of name -> SymbolEntry
var mock_engine # MockEngine for validation
def init()
self.entries = {}
self.mock_engine = animation_dsl.MockEngine()
end
# Dynamically detect and cache symbol type when first encountered
def _detect_and_cache_symbol(name)
if self.entries.contains(name)
return self.entries[name] # Already cached
end
try
import introspect
# Check for named colors first (from animation_dsl.named_colors)
if animation_dsl.named_colors.contains(name)
var entry = animation_dsl._symbol_entry.create_color_instance(name, nil, true) # true = is_builtin
self.entries[name] = entry
return entry
end
# Check for special built-in functions like 'log'
if name == "log"
var entry = animation_dsl._symbol_entry.create_user_function("log", true) # true = is_builtin
self.entries[name] = entry
return entry
end
# Check for user functions (they might not be in animation module directly)
if animation.is_user_function(name)
var entry = animation_dsl._symbol_entry.create_user_function(name, true)
self.entries[name] = entry
return entry
end
# Check for math functions (they are in animation._math, not directly in animation)
if introspect.contains(animation._math, name)
var entry = animation_dsl._symbol_entry.create_math_function(name, true)
self.entries[name] = entry
return entry
end
# Check if it exists in animation module
if introspect.contains(animation, name)
var obj = animation.(name)
var obj_type = type(obj)
# Detect palette objects (bytes() instances)
if isinstance(obj, bytes)
var entry = animation_dsl._symbol_entry.create_palette_constant(name, obj, true)
self.entries[name] = entry
return entry
end
# Detect integer constants (like LINEAR, SINE, COSINE, etc.)
if obj_type == "int"
var entry = animation_dsl._symbol_entry.create_constant(name, obj, true)
self.entries[name] = entry
return entry
end
# Detect constructors (functions/classes that create instances)
if obj_type == "function" || obj_type == "class"
try
var instance = obj(self.mock_engine)
if isinstance(instance, animation.color_provider)
# Color providers are a subclass of value providers, check them first
var entry = animation_dsl._symbol_entry.create_color_constructor(name, instance, true)
self.entries[name] = entry
return entry
elif isinstance(instance, animation.value_provider)
var entry = animation_dsl._symbol_entry.create_value_provider_constructor(name, instance, true)
self.entries[name] = entry
return entry
elif isinstance(instance, animation.animation)
var entry = animation_dsl._symbol_entry.create_animation_constructor(name, instance, true)
self.entries[name] = entry
return entry
end
except .. as e, msg
# If instance creation fails, it might still be a valid function
# but not a constructor we can validate
end
end
end
# If not found in animation module, return nil (will be handled as user-defined)
return nil
except .. as e, msg
# If detection fails, return nil
return nil
end
end
# Add a symbol entry to the table (with conflict detection) - returns the entry
def add(name, entry)
# First check if there's a built-in symbol with this name
var builtin_entry = self._detect_and_cache_symbol(name)
if builtin_entry != nil && builtin_entry.type != entry.type
raise "symbol_redefinition_error", f"Cannot define '{name}' as {entry.type_to_string()} - it conflicts with built-in {builtin_entry.type_to_string()}"
end
# Check existing user-defined symbols
var existing = self.entries.find(name)
if existing != nil
# Check if it's the same type
if existing.type != entry.type
raise "symbol_redefinition_error", f"Cannot redefine symbol '{name}' as {entry.type_to_string()} - it's already defined as {existing.type_to_string()}"
end
# If same type, allow update (for cases like reassignment)
end
self.entries[name] = entry
return entry
end
# Check if a symbol exists (with dynamic detection)
def contains(name)
if self.entries.contains(name)
return true
end
# Try to detect and cache it
var entry = self._detect_and_cache_symbol(name)
return entry != nil
end
# Get a symbol entry (with dynamic detection)
def get(name)
var entry = self.entries.find(name)
if entry != nil
return entry
end
# Try to detect and cache it
return self._detect_and_cache_symbol(name)
end
# Get symbol reference for code generation (with dynamic detection)
def get_reference(name)
# Try to get from cache or detect dynamically (includes named colors)
var entry = self.get(name)
if entry != nil
# For builtin color entries, return the actual color value directly
if entry.is_builtin && entry.type == animation_dsl._symbol_entry.TYPE_COLOR
return animation_dsl.named_colors[name]
end
return entry.get_reference()
end
# Default to user-defined format
return f"{name}_"
end
# Check if symbol exists (including named colors, with dynamic detection)
def symbol_exists(name)
# Use proper discovery through _detect_and_cache_symbol via contains()
return self.contains(name)
end
# Create and register a palette instance symbol (user-defined)
def create_palette(name, instance)
var entry = animation_dsl._symbol_entry.create_palette_instance(name, instance, false)
return self.add(name, entry)
end
# Create and register a color instance symbol (user-defined)
def create_color(name, instance)
var entry = animation_dsl._symbol_entry.create_color_instance(name, instance, false)
return self.add(name, entry)
end
# Create and register an animation instance symbol (user-defined)
def create_animation(name, instance)
var entry = animation_dsl._symbol_entry.create_animation_instance(name, instance, false)
return self.add(name, entry)
end
# Create and register a value provider instance symbol (user-defined)
def create_value_provider(name, instance)
var entry = animation_dsl._symbol_entry.create_value_provider_instance(name, instance, false)
return self.add(name, entry)
end
# Create and register a variable symbol (user-defined)
def create_variable(name)
var entry = animation_dsl._symbol_entry.create_variable(name, false)
return self.add(name, entry)
end
# Create and register a sequence symbol (user-defined)
def create_sequence(name)
var entry = animation_dsl._symbol_entry.create_sequence(name, false)
return self.add(name, entry)
end
# Create and register a template symbol (user-defined)
def create_template(name, param_types)
var entry = animation_dsl._symbol_entry.create_template(name, false)
entry.set_param_types(param_types != nil ? param_types : {})
return self.add(name, entry)
end
# Register a user function (detected at runtime)
def register_user_function(name)
if !self.contains(name)
var entry = animation_dsl._symbol_entry.create_user_function(name, false)
self.add(name, entry)
end
end
# Generic create function that can specify name/type/instance/builtin directly
def create_generic(name, typ, instance, is_builtin)
var entry = animation_dsl._symbol_entry(name, typ, instance, is_builtin != nil ? is_builtin : false)
return self.add(name, entry)
end
# Get the type of a symbol
def get_type(name)
var entry = self.get(name)
return entry != nil ? entry.type_to_string() : nil
end
# Check if symbol takes arguments
def takes_args(name)
var entry = self.get(name)
return entry != nil ? entry.takes_args : false
end
# Check if symbol takes positional arguments
def takes_positional_args(name)
var entry = self.get(name)
return entry != nil ? entry.takes_positional_args() : false
end
# Check if symbol takes named arguments
def takes_named_args(name)
var entry = self.get(name)
return entry != nil ? entry.takes_named_args() : false
end
# Get instance for validation
def get_instance(name)
var entry = self.get(name)
return entry != nil ? entry.instance : nil
end
# Check if symbol is dangerous (creates new instances when called)
def is_dangerous(name)
var entry = self.get(name)
return entry != nil ? entry.is_dangerous_call() : false
end
# Helper method to get named color value (uses proper discovery)
def _get_named_color_value(color_name)
var entry = self.get(color_name) # This will trigger _detect_and_cache_symbol if needed
if entry != nil && entry.is_builtin && entry.type == animation_dsl._symbol_entry.TYPE_COLOR
return animation_dsl.named_colors[color_name]
end
return "0xFFFFFFFF" # Default fallback
end
# Debug method to list all symbols
def list_symbols()
var result = []
for name : self.entries.keys()
var entry = self.entries[name]
result.push(f"{name}: {entry.type_to_string()}")
end
return result
end
end
# Return module exports
return {
"_symbol_entry": SymbolEntry,
"_symbol_table": SymbolTable,
"MockEngine": MockEngine
}

View File

@ -27,12 +27,12 @@ class Token
static var statement_keywords = [
"strip", "set", "color", "palette", "animation",
"sequence", "function", "zone", "on", "run", "template", "param", "import"
"sequence", "function", "zone", "on", "run", "template", "param", "import", "berry"
]
static var keywords = [
# Configuration keywords
"strip", "set", "import",
"strip", "set", "import", "berry",
# Definition keywords
"color", "palette", "animation", "sequence", "function", "zone", "template", "param", "type",

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,6 @@ class BreatheColorProvider : animation.oscillator_value
# Handle parameter changes - no need to sync oscillator min/max since they're fixed
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
# Only handle curve_factor changes for oscillator form
if name == "curve_factor"
# For curve_factor = 1, use pure cosine
@ -95,10 +94,10 @@ class BreatheColorProvider : animation.oscillator_value
var green = (current_base_color >> 8) & 0xFF
var blue = current_base_color & 0xFF
# Apply brightness scaling
red = (red * brightness) / 255
green = (green * brightness) / 255
blue = (blue * brightness) / 255
# Apply brightness scaling using tasmota.scale_uint
red = tasmota.scale_uint(red, 0, 255, 0, brightness)
green = tasmota.scale_uint(green, 0, 255, 0, brightness)
blue = tasmota.scale_uint(blue, 0, 255, 0, brightness)
# Reconstruct color
return (alpha << 24) | (red << 16) | (green << 8) | blue

View File

@ -198,21 +198,10 @@ class ColorCycleColorProvider : animation.color_provider
return self._get_color_at_index(color_index)
end
# String representation of the provider
def tostring()
try
var mode = self.cycle_period == 0 ? "manual" : "auto"
var palette_size = self._get_palette_size()
return f"ColorCycleColorProvider(palette_size={palette_size}, cycle_period={self.cycle_period}, mode={mode}, current_index={self.current_index})"
except ..
return "ColorCycleColorProvider(uninitialized)"
end
return f"ColorCycleColorProvider(palette_size={self._get_palette_size()}, cycle_period={self.cycle_period}, mode={self.cycle_period ? 'manual' :: 'auto'}, current_index={self.current_index})"
end
end
return {'color_cycle': ColorCycleColorProvider}
return {'color_cycle': ColorCycleColorProvider}

View File

@ -0,0 +1,50 @@
# IterationNumberProvider - ValueProvider that returns current sequence iteration number
#
# This provider returns the current iteration number (0-based) for the innermost
# sequence context, or nil if not called within a sequence.
#
# The iteration number is tracked by the animation engine using a stack-based
# approach to handle nested sequences properly.
#
# Usage:
# set iteration = iteration_number()
# animation pulse = pulsating_animation(color=red, period=2s)
# pulse.opacity = iteration * 50 + 100 # Brightness increases with each iteration
#
# In sequences:
# sequence demo {
# repeat 5 times {
# play pulse for 1s
# # iteration will be 0, 1, 2, 3, 4 for each repeat
# }
# }
#@ solidify:IterationNumberProvider,weak
class IterationNumberProvider : animation.value_provider
# Static parameter definitions (no parameters needed)
static var PARAMS = {}
# Produce the current iteration number from the animation engine
#
# @param name: string - Parameter name being requested (ignored)
# @param time_ms: int - Current time in milliseconds (ignored)
# @return int|nil - Current iteration number (0-based) or nil if not in sequence
def produce_value(name, time_ms)
# Get the current iteration number from the engine's sequence stack
return self.engine.get_current_iteration_number()
end
# String representation for debugging
#
# @return string - Human-readable description of the provider
def tostring()
var current_iteration = self.engine.get_current_iteration_number()
if current_iteration != nil
return f"IterationNumberProvider(current: {current_iteration})"
else
return "IterationNumberProvider(not_in_sequence)"
end
end
end
return {'iteration_number': IterationNumberProvider}

View File

@ -137,12 +137,13 @@ class OscillatorValueProvider : animation.value_provider
elif form == animation.EASE_IN
# Quadratic ease-in: starts slow, accelerates
var t = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 255) # 0..255
var eased = (t * t) / 255 # t^2 scaled back to 0..255
var eased = tasmota.scale_int(t * t, 0, 255 * 255, 0, 255) # t^2 scaled back to 0..255
self.value = tasmota.scale_int(eased, 0, 255, min_value, max_value)
elif form == animation.EASE_OUT
# Quadratic ease-out: starts fast, decelerates
var t = tasmota.scale_uint(past_with_phase, 0, duration - 1, 0, 255) # 0..255
var eased = 255 - ((255 - t) * (255 - t)) / 255 # 1 - (1-t)^2 scaled to 0..255
var inv_t = 255 - t
var eased = 255 - tasmota.scale_int(inv_t * inv_t, 0, 255 * 255, 0, 255) # 1 - (1-t)^2 scaled to 0..255
self.value = tasmota.scale_int(eased, 0, 255, min_value, max_value)
elif form == animation.ELASTIC
# Elastic easing: overshoots and oscillates like a spring
@ -157,12 +158,12 @@ class OscillatorValueProvider : animation.value_provider
var decay = tasmota.scale_uint(255 - t, 0, 255, 255, 32) # Exponential decay approximation
var freq_angle = tasmota.scale_uint(t, 0, 255, 0, 32767 * 6) # High frequency oscillation
var oscillation = tasmota.sine_int(freq_angle % 32767) # -4096 to 4096
var elastic_offset = (oscillation * decay) / 4096 # Scale oscillation by decay
var elastic_offset = tasmota.scale_int(oscillation * decay, -4096 * 255, 4096 * 255, -255, 255) # Scale oscillation by decay
var base_progress = tasmota.scale_int(t, 0, 255, 0, max_value - min_value)
self.value = min_value + base_progress + elastic_offset
# Clamp to reasonable bounds to prevent extreme overshoots
var value_range = max_value - min_value
var max_overshoot = value_range / 4 # Allow 25% overshoot
var max_overshoot = tasmota.scale_int(value_range, 0, 4, 0, 1) # Allow 25% overshoot
if self.value > max_value + max_overshoot self.value = max_value + max_overshoot end
if self.value < min_value - max_overshoot self.value = min_value - max_overshoot end
end
@ -174,15 +175,18 @@ class OscillatorValueProvider : animation.value_provider
# Simplified bounce with 3 segments for better behavior
if t < 128 # First big bounce (0-50% of time)
var segment_t = tasmota.scale_uint(t, 0, 127, 0, 255)
bounced_t = 255 - ((255 - segment_t) * (255 - segment_t)) / 255 # Ease-out curve
var inv_segment = 255 - segment_t
bounced_t = 255 - tasmota.scale_int(inv_segment * inv_segment, 0, 255 * 255, 0, 255) # Ease-out curve
elif t < 192 # Second smaller bounce (50-75% of time)
var segment_t = tasmota.scale_uint(t - 128, 0, 63, 0, 255)
var bounce_val = 255 - ((255 - segment_t) * (255 - segment_t)) / 255
bounced_t = (bounce_val * 128) / 255 # Scale to 50% height
var inv_segment = 255 - segment_t
var bounce_val = 255 - tasmota.scale_int(inv_segment * inv_segment, 0, 255 * 255, 0, 255)
bounced_t = tasmota.scale_int(bounce_val, 0, 255, 0, 128) # Scale to 50% height
else # Final settle (75-100% of time)
var segment_t = tasmota.scale_uint(t - 192, 0, 63, 0, 255)
var bounce_val = 255 - ((255 - segment_t) * (255 - segment_t)) / 255
bounced_t = 255 - ((255 - bounce_val) * 64) / 255 # Settle towards full value
var inv_segment = 255 - segment_t
var bounce_val = 255 - tasmota.scale_int(inv_segment * inv_segment, 0, 255 * 255, 0, 255)
bounced_t = 255 - tasmota.scale_int(255 - bounce_val, 0, 255, 0, 64) # Settle towards full value
end
self.value = tasmota.scale_int(bounced_t, 0, 255, min_value, max_value)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,323 @@
# DSL Berry Code Blocks Test Suite
# Tests for berry code block functionality in SimpleDSLTranspiler
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota" lib/libesp32/berry_animation/src/tests/dsl_berry_code_blocks_test.be
import animation
import animation_dsl
import string
# Test basic berry code block with triple braces
def test_basic_berry_block_triple_braces()
print("Testing basic berry code block with triple braces...")
var dsl_source = 'berry """\n' +
'var test_var = 42\n' +
'print("Hello from berry block")\n' +
'"""\n' +
'color red_custom = 0xFF0000\n'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "# Berry code block") >= 0, "Should have berry code block comment")
assert(string.find(berry_code, "var test_var = 42") >= 0, "Should include berry code verbatim")
assert(string.find(berry_code, 'print("Hello from berry block")') >= 0, "Should include print statement")
assert(string.find(berry_code, "# End berry code block") >= 0, "Should have end comment")
assert(string.find(berry_code, "var red_custom_ = 0xFFFF0000") >= 0, "Should continue with DSL after berry block")
print("✓ Basic berry code block (triple braces) test passed")
return true
end
# Test basic berry code block with braces inside
def test_basic_berry_block_with_braces()
print("Testing basic berry code block with braces inside...")
var dsl_source = "berry '''\n" +
"var test_var = 100\n" +
"if test_var > 50\n" +
" print('Value is greater than 50')\n" +
"end\n" +
"'''\n" +
"color blue_custom = 0x0000FF\n"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "# Berry code block") >= 0, "Should have berry code block comment")
assert(string.find(berry_code, "var test_var = 100") >= 0, "Should include berry code verbatim")
assert(string.find(berry_code, "print('Value is greater than 50')") >= 0, "Should include print statement")
assert(string.find(berry_code, "# End berry code block") >= 0, "Should have end comment")
print("✓ Basic berry code block (single quotes) test passed")
return true
end
# Test multiple berry code blocks
def test_multiple_berry_blocks()
print("Testing multiple berry code blocks...")
var dsl_source = 'berry """\n' +
'var global_var = 50\n' +
'def helper_function(x)\n' +
' return x * 2\n' +
'end\n' +
'"""\n' +
'color green_custom = 0x00FF00\n' +
"berry '''\n" +
"var result = helper_function(global_var)\n" +
"print('Result:', result)\n" +
"'''\n" +
"set col = 0xFF8800"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
# Check first berry block
var first_block_pos = string.find(berry_code, "var global_var = 50")
assert(first_block_pos >= 0, "Should include first berry block")
# Check second berry block
var second_block_pos = string.find(berry_code, "var result = helper_function(global_var)")
assert(second_block_pos >= 0, "Should include second berry block")
assert(second_block_pos > first_block_pos, "Second block should come after first block")
# Check DSL continues to work
assert(string.find(berry_code, "var green_custom_ = 0xFF00FF00") >= 0, "Should process DSL after berry blocks")
print("✓ Multiple berry code blocks test passed")
return true
end
# Test berry code block with complex content
def test_complex_berry_content()
print("Testing berry code block with complex content...")
var dsl_source = 'berry """\n' +
'import math\n' +
'import string\n' +
'\n' +
'# Complex function with multiple features\n' +
'def calculate_dynamic_value(base, factor)\n' +
' var result = math.sin(factor) * base\n' +
' return int(result + 100)\n' +
'end\n' +
'\n' +
'var config = {\n' +
' "brightness": 200,\n' +
' "speed": 1.5\n' +
'}\n' +
'\n' +
'print("Complex berry block initialized")\n' +
'"""\n' +
'color purple_custom = 0x800080\n' +
'animation pulse = pulsating_animation(color=purple_custom, period=2s)\n' +
'run pulse'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "import math") >= 0, "Should include import statements")
assert(string.find(berry_code, "import string") >= 0, "Should include multiple imports")
assert(string.find(berry_code, "def calculate_dynamic_value(base, factor)") >= 0, "Should include function definition")
assert(string.find(berry_code, 'var config = {') >= 0, "Should include complex data structures")
assert(string.find(berry_code, '"brightness": 200,') >= 0, "Should preserve map syntax")
assert(string.find(berry_code, 'print("Complex berry block initialized")') >= 0, "Should include print statement")
print("✓ Complex berry content test passed")
return true
end
# Test berry code block interacting with DSL objects
def test_berry_dsl_interaction()
print("Testing berry code block interaction with DSL objects...")
var dsl_source = 'color red_custom = 0xFF0000\n' +
'animation test_anim = pulsating_animation(color=red_custom, period=3s)\n' +
'berry """\n' +
'# Modify DSL-generated animation\n' +
'test_anim_.opacity = 150\n' +
'test_anim_.priority = 10\n' +
'print("Animation modified via berry code")\n' +
'"""\n' +
'run test_anim'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "var test_anim_ = animation.pulsating_animation(engine)") >= 0, "Should create animation")
assert(string.find(berry_code, "test_anim_.opacity = 150") >= 0, "Should modify animation opacity")
assert(string.find(berry_code, "test_anim_.priority = 10") >= 0, "Should modify animation priority")
assert(string.find(berry_code, 'print("Animation modified via berry code")') >= 0, "Should include print statement")
print("✓ Berry-DSL interaction test passed")
return true
end
# Test berry code block with inline comments
def test_berry_block_with_inline_comment()
print("Testing berry code block with inline comment...")
var dsl_source = 'berry """ # This is an inline comment\n' +
'var test_value = 123 # Berry code block # This is an inline comment\n' +
'"""\n' +
'color yellow_custom = 0xFFFF00'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "# Berry code block # This is an inline comment") >= 0, "Should preserve inline comment")
assert(string.find(berry_code, "var test_value = 123") >= 0, "Should include berry code")
print("✓ Berry block with inline comment test passed")
return true
end
# Test error handling - missing string after berry keyword
def test_error_missing_string()
print("Testing error handling for missing string after berry keyword...")
var dsl_source = 'berry\n' +
'print("This should cause an error")\n' +
'color test_color = 0xFF0000'
try
var berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should raise compilation error")
except "dsl_compilation_error"
# ok
end
print("✓ Error handling (missing string) test passed")
return true
end
# Test error handling - invalid token after berry keyword
def test_error_invalid_token()
print("Testing error handling for invalid token after berry keyword...")
var dsl_source = 'berry 123\n' +
'color test_color = 0xFF0000'
try
var berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should raise compilation error")
except "dsl_compilation_error"
# ok
end
print("✓ Error handling (invalid token) test passed")
return true
end
# Test execution of berry code blocks
def test_berry_block_execution()
print("Testing execution of berry code blocks...")
var dsl_source = 'berry """\n' +
'var execution_test = "Berry code executed successfully"\n' +
'print(execution_test)\n' +
'"""\n' +
'color cyan_custom = 0x00FFFF\n' +
'animation my_anim = solid(color=cyan_custom)'
'run my_anim'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Test execution (this will actually run the berry code)
try
animation_dsl.execute(dsl_source)
print("✓ Berry block execution test passed")
return true
except .. as e, m
print("✗ Berry block execution failed:", e, m)
return false
end
end
# Test berry code blocks with multiline strings and complex syntax
def test_multiline_complex_syntax()
print("Testing berry code blocks with multiline strings and complex syntax...")
var dsl_source = 'berry """\n' +
'# Test multiline strings and complex syntax\n' +
'var multiline_string = "This is a\\n" +\n' +
' "multiline string\\n" +\n' +
' "with multiple lines"\n' +
'\n' +
'def complex_function(a, b, c)\n' +
' if a > b\n' +
' return a + c\n' +
' else\n' +
' return b + c\n' +
' end\n' +
'end\n' +
'\n' +
'var result = complex_function(10, 5, 3)\n' +
'print("Complex function result:", result)\n' +
'"""\n' +
'color magenta_custom = 0xFF00FF\n'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, 'var multiline_string = "This is a\\n"') >= 0, "Should preserve multiline string syntax")
assert(string.find(berry_code, "def complex_function(a, b, c)") >= 0, "Should include function definition")
assert(string.find(berry_code, "if a > b") >= 0, "Should include conditional logic")
assert(string.find(berry_code, "var result = complex_function(10, 5, 3)") >= 0, "Should include function call")
print("✓ Multiline complex syntax test passed")
return true
end
# Run all tests
def run_all_berry_block_tests()
print("=== DSL Berry Code Blocks Test Suite ===")
print("")
var tests = [
test_basic_berry_block_triple_braces,
test_basic_berry_block_with_braces,
test_multiple_berry_blocks,
test_complex_berry_content,
test_berry_dsl_interaction,
test_berry_block_with_inline_comment,
test_error_missing_string,
test_error_invalid_token,
test_berry_block_execution,
test_multiline_complex_syntax
]
var passed = 0
var total = size(tests)
for test_func : tests
try
if test_func()
passed += 1
end
except .. as e, m
print("✗ Test failed with exception:", e, m)
end
print("")
end
print("=== Berry Code Blocks Test Results ===")
print(f"Passed: {passed}/{total}")
if passed == total
print("All berry code block tests passed! ✓")
return true
else
print("Some berry code block tests failed! ✗")
raise "test_failed"
end
end
# Run the tests
return run_all_berry_block_tests()

View File

@ -0,0 +1,313 @@
# DSL Berry Code Blocks Integration Test Suite
# Tests for real-world integration scenarios with berry code blocks
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota" lib/libesp32/berry_animation/src/tests/dsl_berry_integration_test.be
import animation
import animation_dsl
import string
# Test berry code blocks with mathematical calculations
def test_mathematical_integration()
print("Testing berry code blocks with mathematical calculations...")
var dsl_source = 'berry """\n' +
'import math\n' +
'def calculate_period(base_period, frequency)\n' +
' return int(base_period / frequency)\n' +
'end\n' +
'def calculate_opacity(brightness_percent)\n' +
' return int(brightness_percent * 2.55)\n' +
'end\n' +
'"""\n' +
'color wave_color = 0x0080FF\n' +
'animation wave1 = pulsating_animation(color=wave_color, period=2s)\n' +
'animation wave2 = pulsating_animation(color=wave_color, period=3s)\n' +
'berry """\n' +
'wave1_.period = calculate_period(4000, 2.0) # 2000ms\n' +
'wave1_.opacity = calculate_opacity(80) # 204\n' +
'wave2_.period = calculate_period(6000, 1.5) # 4000ms\n' +
'wave2_.opacity = calculate_opacity(60) # 153\n' +
'"""\n' +
'run wave1\n' +
'run wave2'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Verify mathematical functions are included
assert(string.find(berry_code, "def calculate_period(base_period, frequency)") >= 0, "Should include period calculation function")
assert(string.find(berry_code, "def calculate_opacity(brightness_percent)") >= 0, "Should include opacity calculation function")
# Verify function calls are included
assert(string.find(berry_code, "wave1_.period = calculate_period(4000, 2.0)") >= 0, "Should include period calculation call")
assert(string.find(berry_code, "wave1_.opacity = calculate_opacity(80)") >= 0, "Should include opacity calculation call")
# Test execution
try
animation_dsl.execute(dsl_source)
print("✓ Mathematical integration test passed")
return true
except .. as e
print("✗ Mathematical integration test failed:", e)
return false
end
end
# Test berry code blocks with configuration management
def test_configuration_management()
print("Testing berry code blocks with configuration management...")
var dsl_source = 'berry """\n' +
'# Global configuration object\n' +
'var config = {\n' +
' "brightness": 200,\n' +
' "speed_multiplier": 1.5,\n' +
' "color_intensity": 0.8,\n' +
' "debug": true\n' +
'}\n' +
'def log_debug(message)\n' +
' if config["debug"]\n' +
' print("[DEBUG]", message)\n' +
' end\n' +
'end\n' +
'def apply_config_to_animation(anim, name)\n' +
' anim.opacity = config["brightness"]\n' +
' log_debug("Applied config to " + name)\n' +
'end\n' +
'"""\n' +
'color fire_red = 0xFF4500\n' +
'color ocean_blue = 0x006994\n' +
'animation fire_anim = pulsating_animation(color=fire_red, period=2s)\n' +
'animation ocean_anim = pulsating_animation(color=ocean_blue, period=3s)\n' +
'berry """\n' +
'apply_config_to_animation(fire_anim_, "fire_animation")\n' +
'apply_config_to_animation(ocean_anim_, "ocean_animation")\n' +
'log_debug("Configuration applied to all animations")\n' +
'"""\n' +
'run fire_anim\n' +
'run ocean_anim'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Verify configuration object is included
assert(string.find(berry_code, 'var config = {') >= 0, "Should include config object")
assert(string.find(berry_code, '"brightness": 200,') >= 0, "Should include brightness setting")
# Verify helper functions are included
assert(string.find(berry_code, "def log_debug(message)") >= 0, "Should include debug function")
assert(string.find(berry_code, "def apply_config_to_animation(anim, name)") >= 0, "Should include config application function")
# Test execution
try
animation_dsl.execute(dsl_source)
print("✓ Configuration management test passed")
return true
except .. as e
print("✗ Configuration management test failed:", e)
return false
end
end
# Test berry code blocks with dynamic animation creation
def test_dynamic_animation_creation()
print("Testing berry code blocks with dynamic animation creation...")
var dsl_source = 'berry """\n' +
'var animation_counter = 0\n' +
'def create_numbered_animation(base_color, period_ms)\n' +
' animation_counter += 1\n' +
' var anim = animation.pulsating_animation(engine)\n' +
' anim.color = base_color\n' +
' anim.period = period_ms\n' +
' anim.priority = animation_counter\n' +
' engine.add(anim)\n' +
' print("Created animation #" + str(animation_counter))\n' +
' return anim\n' +
'end\n' +
'"""\n' +
'color red_custom = 0xFF0000\n' +
'color green_custom = 0x00FF00\n' +
'color blue_custom = 0x0000FF\n' +
'berry """\n' +
'create_numbered_animation(0xFFFF0000, 2000) # Red\n' +
'create_numbered_animation(0xFF00FF00, 2500) # Green\n' +
'create_numbered_animation(0xFF0000FF, 3000) # Blue\n' +
'print("Created " + str(animation_counter) + " dynamic animations")\n' +
'"""'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Verify dynamic creation function is included
assert(string.find(berry_code, "def create_numbered_animation(base_color, period_ms)") >= 0, "Should include dynamic creation function")
assert(string.find(berry_code, "var anim = animation.pulsating_animation(engine)") >= 0, "Should create animations dynamically")
assert(string.find(berry_code, "engine.add(anim)") >= 0, "Should add animations to engine")
# Test execution
try
animation_dsl.execute(dsl_source)
print("✓ Dynamic animation creation test passed")
return true
except .. as e
print("✗ Dynamic animation creation test failed:", e)
return false
end
end
# Test berry code blocks with state management
def test_state_management()
print("Testing berry code blocks with state management...")
var dsl_source = 'berry """\n' +
'# State management system\n' +
'var state = {\n' +
' "current_mode": "normal",\n' +
' "brightness_level": 255,\n' +
' "animation_count": 0\n' +
'}\n' +
'def set_mode(mode)\n' +
' state["current_mode"] = mode\n' +
' print("Mode changed to:", mode)\n' +
'end\n' +
'def get_brightness_for_mode()\n' +
' if state["current_mode"] == "dim"\n' +
' return 100\n' +
' elif state["current_mode"] == "bright"\n' +
' return 255\n' +
' else\n' +
' return 180\n' +
' end\n' +
'end\n' +
'"""\n' +
'color status_color = 0x00FFFF\n' +
'animation status_anim = pulsating_animation(color=status_color, period=2s)\n' +
'berry """\n' +
'set_mode("bright")\n' +
'status_anim_.opacity = get_brightness_for_mode()\n' +
'state["animation_count"] = 1\n' +
'print("State:", state)\n' +
'"""\n' +
'run status_anim'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Verify state management is included
assert(string.find(berry_code, 'var state = {') >= 0, "Should include state object")
assert(string.find(berry_code, "def set_mode(mode)") >= 0, "Should include mode setter")
assert(string.find(berry_code, "def get_brightness_for_mode()") >= 0, "Should include brightness getter")
# Test execution
try
animation_dsl.execute(dsl_source)
print("✓ State management test passed")
return true
except .. as e
print("✗ State management test failed:", e)
return false
end
end
# Test berry code blocks with error handling
def test_error_handling_integration()
print("Testing berry code blocks with error handling...")
var dsl_source = 'berry """\n' +
'def safe_divide(a, b)\n' +
' try\n' +
' return a / b\n' +
' except .. as e\n' +
' print("Division error:", e)\n' +
' return 1.0\n' +
' end\n' +
'end\n' +
'def safe_set_period(anim, period)\n' +
' try\n' +
' if period > 0\n' +
' anim.period = period\n' +
' print("Period set to:", period)\n' +
' else\n' +
' print("Invalid period, using default")\n' +
' anim.period = 2000\n' +
' end\n' +
' except .. as e\n' +
' print("Error setting period:", e)\n' +
' end\n' +
'end\n' +
'"""\n' +
'color safe_color = 0xFF8000\n' +
'animation safe_anim = pulsating_animation(color=safe_color, period=1s)\n' +
'berry """\n' +
'var calculated_period = safe_divide(4000, 2) # Should work\n' +
'safe_set_period(safe_anim_, int(calculated_period * 1000))\n' +
'"""\n' +
'run safe_anim'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Verify error handling functions are included
assert(string.find(berry_code, "def safe_divide(a, b)") >= 0, "Should include safe division function")
assert(string.find(berry_code, "def safe_set_period(anim, period)") >= 0, "Should include safe period setter")
assert(string.find(berry_code, "try") >= 0, "Should include try-catch blocks")
# Test execution
try
animation_dsl.execute(dsl_source)
print("✓ Error handling integration test passed")
return true
except .. as e
print("✗ Error handling integration test failed:", e)
return false
end
end
# Run all integration tests
def run_all_berry_integration_tests()
print("=== DSL Berry Code Blocks Integration Test Suite ===")
print("")
var tests = [
test_mathematical_integration,
test_configuration_management,
test_dynamic_animation_creation,
test_state_management,
test_error_handling_integration
]
var passed = 0
var total = size(tests)
for test_func : tests
try
if test_func()
passed += 1
end
except .. as e
print("✗ Test failed with exception:", e)
end
print("")
end
print("=== Berry Integration Test Results ===")
print(f"Passed: {passed}/{total}")
if passed == total
print("All berry integration tests passed! ✓")
return true
else
print("Some berry integration tests failed! ✗")
return false
end
end
# Run the tests
return run_all_berry_integration_tests()

View File

@ -366,8 +366,8 @@ def test_reserved_name_validation()
"color red_custom = 0x800000",
"color smooth_custom = 0x808080",
# Easing function names are now valid as user-defined names
"animation smooth = solid(color=blue)",
"animation linear = solid(color=green)"
"animation smooth2 = solid(color=blue)",
"animation linear2 = solid(color=green)"
]
for dsl_input : valid_name_tests

View File

@ -0,0 +1,216 @@
# DSL Lexer Triple Quotes Test Suite
# Tests for triple-quoted string tokenization in DSLLexer
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota" lib/libesp32/berry_animation/src/tests/dsl_lexer_triple_quotes_test.be
import animation_dsl
import string
# Test basic triple-quoted string tokenization with double quotes
def test_triple_quotes_double()
print("Testing triple-quoted string tokenization (triple quotes)...")
var source = 'berry """\nHello World\n"""'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
assert(size(tokens) >= 3, "Should have at least 3 tokens (berry, string, EOF)")
assert(tokens[0].type == animation_dsl.Token.KEYWORD, "First token should be KEYWORD")
assert(tokens[0].value == "berry", "First token should be 'berry'")
assert(tokens[1].type == animation_dsl.Token.STRING, "Second token should be STRING")
assert(tokens[1].value == "\nHello World\n", "String content should be preserved")
print("✓ Triple-quoted string (double quotes) tokenization test passed")
return true
end
# Test basic triple-quoted string tokenization with single quotes
def test_triple_quotes_single()
print("Testing triple-quoted string tokenization (single quotes)...")
var source = "berry '''\nHello World\n'''"
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
assert(size(tokens) >= 3, "Should have at least 3 tokens (berry, string, EOF)")
assert(tokens[0].type == animation_dsl.Token.KEYWORD, "First token should be KEYWORD")
assert(tokens[0].value == "berry", "First token should be 'berry'")
assert(tokens[1].type == animation_dsl.Token.STRING, "Second token should be STRING")
assert(tokens[1].value == "\nHello World\n", "String content should be preserved")
print("✓ Triple-quoted string (single quotes) tokenization test passed")
return true
end
# Test multiline triple-quoted string
def test_multiline_triple_quotes()
print("Testing multiline triple-quoted string tokenization...")
var source = 'berry """\nLine 1\nLine 2\nLine 3\n"""'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
assert(size(tokens) >= 3, "Should have at least 3 tokens")
assert(tokens[1].type == animation_dsl.Token.STRING, "Second token should be STRING")
var expected_content = "\nLine 1\nLine 2\nLine 3\n"
assert(tokens[1].value == expected_content, f"String content should be '{expected_content}', got '{tokens[1].value}'")
print("✓ Multiline triple-quoted string tokenization test passed")
return true
end
# Test triple-quoted string with embedded quotes
def test_embedded_quotes()
print("Testing triple-quoted string with embedded quotes...")
var source = 'berry """\nprint("Hello")\nvar x = \'world\'\n"""'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
assert(size(tokens) >= 3, "Should have at least 3 tokens")
assert(tokens[1].type == animation_dsl.Token.STRING, "Second token should be STRING")
var expected_content = '\nprint("Hello")\nvar x = \'world\'\n'
assert(tokens[1].value == expected_content, f"String content should preserve embedded quotes")
print("✓ Embedded quotes in triple-quoted string test passed")
return true
end
# Test empty triple-quoted string
def test_empty_triple_quotes()
print("Testing empty triple-quoted string...")
var source = 'berry """"""'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
assert(size(tokens) >= 3, "Should have at least 3 tokens")
assert(tokens[1].type == animation_dsl.Token.STRING, "Second token should be STRING")
assert(tokens[1].value == "", "Empty string should have empty value")
print("✓ Empty triple-quoted string test passed")
return true
end
# Test unterminated triple-quoted string (error case)
def test_unterminated_triple_quotes()
print("Testing unterminated triple-quoted string...")
var source = 'berry """\nUnterminated string'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
# Should generate an error token
var has_error = false
for token : tokens
if token.type == animation_dsl.Token.ERROR
has_error = true
break
end
end
assert(has_error, "Should generate an ERROR token for unterminated string")
print("✓ Unterminated triple-quoted string test passed")
return true
end
# Test triple-quoted string with complex content
def test_complex_content()
print("Testing triple-quoted string with complex content...")
var source = 'berry """\n' +
'import math\n' +
'def func(x)\n' +
' return x * 2\n' +
'end\n' +
'var result = func(21)\n' +
'print("Result:", result)\n' +
'"""'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
assert(size(tokens) >= 3, "Should have at least 3 tokens")
assert(tokens[1].type == animation_dsl.Token.STRING, "Second token should be STRING")
var content = tokens[1].value
assert(string.find(content, "import math") >= 0, "Should contain import statement")
assert(string.find(content, "def func(x)") >= 0, "Should contain function definition")
assert(string.find(content, "return x * 2") >= 0, "Should contain function body")
assert(string.find(content, 'print("Result:", result)') >= 0, "Should contain print statement")
print("✓ Complex content triple-quoted string test passed")
return true
end
# Test mixed quote types (should not interfere)
def test_mixed_quote_types()
print("Testing that triple quotes don't interfere with regular quotes...")
var source = 'color red = 0xFF0000\nberry """\ntest\n"""\nvar x = "normal string"'
var lexer = animation_dsl.DSLLexer(source)
var tokens = lexer.tokenize()
# Find the normal string token
var normal_string_found = false
for token : tokens
if token.type == animation_dsl.Token.STRING && token.value == "normal string"
normal_string_found = true
break
end
end
assert(normal_string_found, "Should still tokenize normal strings correctly")
print("✓ Mixed quote types test passed")
return true
end
# Run all lexer triple quotes tests
def run_all_lexer_triple_quotes_tests()
print("=== DSL Lexer Triple Quotes Test Suite ===")
print("")
var tests = [
test_triple_quotes_double,
test_triple_quotes_single,
test_multiline_triple_quotes,
test_embedded_quotes,
test_empty_triple_quotes,
test_unterminated_triple_quotes,
test_complex_content,
test_mixed_quote_types
]
var passed = 0
var total = size(tests)
for test_func : tests
try
if test_func()
passed += 1
end
except .. as e, m
print("✗ Test failed with exception:", e, m)
end
print("")
end
print("=== Lexer Triple Quotes Test Results ===")
print(f"Passed: {passed}/{total}")
if passed == total
print("All lexer triple quotes tests passed! ✓")
return true
else
print("Some lexer triple quotes tests failed! ✗")
raise "test_failed"
end
end
# Run the tests
return run_all_lexer_triple_quotes_tests()

View File

@ -214,7 +214,7 @@ class DSLParameterValidationTest
end
# Check that the generated code contains the expected object reference
if string.find(berry_code, "self.resolve(red_eye_, 'pos')") == -1
if string.find(berry_code, "animation.resolve(red_eye_, 'pos')") == -1
raise "generation_error", "Generated code should contain object property reference"
end
end
@ -336,7 +336,7 @@ class DSLParameterValidationTest
end
# Check that the generated code contains the expected computed expression
if string.find(berry_code, "self.resolve(strip_len_) - self.resolve(red_eye_, 'pos')") == -1
if string.find(berry_code, "animation.resolve(strip_len_) - animation.resolve(red_eye_, 'pos')") == -1
raise "generation_error", "Generated code should contain computed object property reference"
end
end
@ -383,7 +383,7 @@ class DSLParameterValidationTest
return true
else
print("✗ Some DSL parameter validation tests failed!")
return false
raise "test_failed"
end
end
end

View File

@ -1,5 +1,5 @@
# DSL Reset/Restart Test Suite
# Tests for reset and restart functionality in sequences
# DSL Restart Test Suite
# Tests for restart functionality in sequences
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota def log(x) print(x) end" lib/libesp32/berry_animation/src/tests/dsl_reset_restart_test.be
@ -8,32 +8,6 @@ import animation
import animation_dsl
import string
# Test basic reset functionality
def test_reset_basic()
print("Testing basic reset functionality...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=2s)\n" +
"animation test_anim = solid(color=red)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset osc_val\n" +
" play test_anim for 1s\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for reset")
assert(string.find(berry_code, "animation.triangle(engine)") >= 0, "Should generate triangle oscillator")
assert(string.find(berry_code, "push_closure_step") >= 0, "Should generate closure step for reset")
assert(string.find(berry_code, "osc_val_.start(engine.time_ms)") >= 0, "Should call start() method")
print("✓ Basic reset test passed")
return true
end
# Test basic restart functionality
def test_restart_basic()
print("Testing basic restart functionality...")
@ -60,9 +34,9 @@ def test_restart_basic()
return true
end
# Test reset/restart with different value provider types
def test_reset_restart_different_providers()
print("Testing reset/restart with different value provider types...")
# Test restart with different value provider types
def test_restart_different_providers()
print("Testing restart with different value provider types...")
var dsl_source = "set triangle_val = triangle(min_value=0, max_value=29, duration=5s)\n" +
"set cosine_val = cosine_osc(min_value=0, max_value=29, duration=5s)\n" +
@ -71,11 +45,11 @@ def test_reset_restart_different_providers()
"\n" +
"sequence demo {\n" +
" play test_anim for 500ms\n" +
" reset triangle_val\n" +
" restart triangle_val\n" +
" wait 200ms\n" +
" restart cosine_val\n" +
" wait 200ms\n" +
" reset sine_val\n" +
" restart sine_val\n" +
" play test_anim for 500ms\n" +
"}\n" +
"\n" +
@ -84,11 +58,11 @@ def test_reset_restart_different_providers()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for multiple providers")
assert(string.find(berry_code, "triangle_val_.start(engine.time_ms)") >= 0, "Should reset triangle")
assert(string.find(berry_code, "triangle_val_.start(engine.time_ms)") >= 0, "Should restart triangle")
assert(string.find(berry_code, "cosine_val_.start(engine.time_ms)") >= 0, "Should restart cosine")
assert(string.find(berry_code, "sine_val_.start(engine.time_ms)") >= 0, "Should reset sine")
assert(string.find(berry_code, "sine_val_.start(engine.time_ms)") >= 0, "Should restart sine")
# Count the number of closure steps - should be 3 (one for each reset/restart)
# Count the number of closure steps - should be 3 (one for each restart)
var closure_count = 0
var pos = 0
while true
@ -97,15 +71,15 @@ def test_reset_restart_different_providers()
closure_count += 1
pos += 1
end
assert(closure_count == 3, f"Should have 3 closure steps for reset/restart, found {closure_count}")
assert(closure_count == 3, f"Should have 3 closure steps for estart, found {closure_count}")
print("✓ Different providers test passed")
return true
end
# Test reset/restart with animations
def test_reset_restart_animations()
print("Testing reset/restart with animations...")
# Test restart with animations
def test_restart_animations()
print("Testing restart with animations...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=2s)\n" +
"animation pulse_anim = pulsating_animation(color=red, period=3s)\n" +
@ -113,7 +87,7 @@ def test_reset_restart_animations()
"\n" +
"sequence demo {\n" +
" play pulse_anim for 1s\n" +
" reset pulse_anim\n" +
" restart pulse_anim\n" +
" play solid_anim for 1s\n" +
" restart solid_anim\n" +
" play pulse_anim for 1s\n" +
@ -123,11 +97,11 @@ def test_reset_restart_animations()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for animation reset/restart")
assert(string.find(berry_code, "pulse_anim_.start(engine.time_ms)") >= 0, "Should reset pulse animation")
assert(berry_code != nil, "Should generate Berry code for animation restart")
assert(string.find(berry_code, "pulse_anim_.start(engine.time_ms)") >= 0, "Should restart pulse animation")
assert(string.find(berry_code, "solid_anim_.start(engine.time_ms)") >= 0, "Should restart solid animation")
# Count the number of closure steps - should be 2 (one for each reset/restart)
# Count the number of closure steps - should be 2 (one for each restart)
var closure_count = 0
var pos = 0
while true
@ -136,15 +110,15 @@ def test_reset_restart_animations()
closure_count += 1
pos += 1
end
assert(closure_count == 2, f"Should have 2 closure steps for animation reset/restart, found {closure_count}")
assert(closure_count == 2, f"Should have 2 closure steps for animation restart, found {closure_count}")
print("✓ Animation reset/restart test passed")
print("✓ Animation restart test passed")
return true
end
# Test reset/restart in repeat blocks
def test_reset_restart_in_repeat()
print("Testing reset/restart in repeat blocks...")
# Test restart in repeat blocks
def test_restart_in_repeat()
print("Testing restart in repeat blocks...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=1s)\n" +
"animation test_anim = solid(color=yellow)\n" +
@ -152,7 +126,7 @@ def test_reset_restart_in_repeat()
"sequence demo {\n" +
" repeat 3 times {\n" +
" play test_anim for 500ms\n" +
" reset osc_val\n" +
" restart osc_val\n" +
" wait 200ms\n" +
" }\n" +
"}\n" +
@ -161,11 +135,11 @@ def test_reset_restart_in_repeat()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code for reset in repeat")
assert(berry_code != nil, "Should generate Berry code for restart in repeat")
assert(string.find(berry_code, "push_repeat_subsequence") >= 0, "Should generate repeat block")
assert(string.find(berry_code, "osc_val_.start(engine.time_ms)") >= 0, "Should reset in repeat block")
assert(string.find(berry_code, "osc_val_.start(engine.time_ms)") >= 0, "Should restart in repeat block")
print("✓ Reset/restart in repeat test passed")
print("✓ Restart in repeat test passed")
return true
end
@ -177,7 +151,7 @@ def test_error_undefined_provider()
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset undefined_provider\n" +
" restart undefined_provider\n" +
"}\n" +
"\n" +
"run demo"
@ -203,7 +177,7 @@ def test_error_non_value_provider()
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset my_color\n" +
" restart my_color\n" +
"}\n" +
"\n" +
"run demo"
@ -226,9 +200,10 @@ def test_error_animation_not_provider()
var dsl_source = "animation my_anim = solid(color=blue)\n" +
"\n" +
"set myvar = 2\n"
"sequence demo {\n" +
" play my_anim for 1s\n" +
" restart my_anim\n" +
" restart myvar\n" +
"}\n" +
"\n" +
"run demo"
@ -254,7 +229,7 @@ def test_error_variable_not_provider()
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset my_var\n" +
" restart my_var\n" +
"}\n" +
"\n" +
"run demo"
@ -271,11 +246,11 @@ def test_error_variable_not_provider()
return true
end
# Test complex scenario with multiple resets/restarts
# Test complex scenario with multiple restarts
def test_complex_scenario()
print("Testing complex scenario with multiple resets/restarts...")
print("Testing complex scenario with multiple restarts...")
var dsl_source = "# Complex cylon eye with reset functionality\n" +
var dsl_source = "# Complex cylon eye with restart functionality\n" +
"set strip_len = strip_length()\n" +
"palette eye_palette = [ red, yellow, green, violet ]\n" +
"color eye_color = color_cycle(palette=eye_palette, cycle_period=0)\n" +
@ -293,7 +268,7 @@ def test_complex_scenario()
"sequence cylon_eye {\n" +
" play red_eye for 3s\n" +
" red_eye.pos = triangle_val\n" +
" reset triangle_val\n" +
" restart triangle_val\n" +
" play red_eye for 3s\n" +
" red_eye.pos = cosine_val\n" +
" restart cosine_val\n" +
@ -305,10 +280,10 @@ def test_complex_scenario()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile complex scenario")
assert(string.find(berry_code, "triangle_val_.start(engine.time_ms)") >= 0, "Should reset triangle_val")
assert(string.find(berry_code, "triangle_val_.start(engine.time_ms)") >= 0, "Should restart triangle_val")
assert(string.find(berry_code, "cosine_val_.start(engine.time_ms)") >= 0, "Should restart cosine_val")
# Should have multiple closure steps: 2 assignments + 2 resets + 1 color advance = 5 total
# Should have multiple closure steps: 2 assignments + 2 restarts + 1 color advance = 5 total
var closure_count = 0
var pos = 0
while true
@ -323,16 +298,16 @@ def test_complex_scenario()
return true
end
# Test that reset/restart works with comments
def test_reset_restart_with_comments()
print("Testing reset/restart with comments...")
# Test that restart works with comments
def test_restart_with_comments()
print("Testing restart with comments...")
var dsl_source = "set osc_val = triangle(min_value=0, max_value=10, duration=2s) # Triangle oscillator\n" +
"animation test_anim = solid(color=red)\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 1s\n" +
" reset osc_val # Reset the oscillator\n" +
" restart osc_val # Restart the oscillator\n" +
" restart osc_val # Restart it again\n" +
" play test_anim for 1s\n" +
"}\n" +
@ -342,10 +317,10 @@ def test_reset_restart_with_comments()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code with comments")
assert(string.find(berry_code, "# Reset the oscillator") >= 0, "Should preserve reset comment")
assert(string.find(berry_code, "# Restart the oscillator") >= 0, "Should preserve restart comment")
assert(string.find(berry_code, "# Restart it again") >= 0, "Should preserve restart comment")
# Should have 2 closure steps for reset and restart
# Should have 2 closure steps for restart
var closure_count = 0
var pos = 0
while true
@ -356,42 +331,40 @@ def test_reset_restart_with_comments()
end
assert(closure_count == 2, f"Should have 2 closure steps, found {closure_count}")
print("✓ Reset/restart with comments test passed")
print("✓ Restart with comments test passed")
return true
end
# Run all tests
def run_all_reset_restart_tests()
print("Starting DSL Reset/Restart Tests...")
def run_all_restart_tests()
print("Starting DSL Restart Tests...")
test_reset_basic()
test_restart_basic()
test_reset_restart_different_providers()
test_reset_restart_in_repeat()
test_restart_different_providers()
test_restart_in_repeat()
test_error_undefined_provider()
test_error_non_value_provider()
test_error_animation_not_provider()
test_error_variable_not_provider()
test_complex_scenario()
test_reset_restart_with_comments()
test_restart_with_comments()
print("\n🎉 All DSL Reset/Restart tests passed!")
print("\n🎉 All DSL Restart tests passed!")
return true
end
# Execute tests
run_all_reset_restart_tests()
run_all_restart_tests()
return {
"run_all_reset_restart_tests": run_all_reset_restart_tests,
"test_reset_basic": test_reset_basic,
"run_all_estart_tests": run_all_restart_tests,
"test_restart_basic": test_restart_basic,
"test_reset_restart_different_providers": test_reset_restart_different_providers,
"test_reset_restart_in_repeat": test_reset_restart_in_repeat,
"test_restart_different_providers": test_restart_different_providers,
"test_restart_in_repeat": test_restart_in_repeat,
"test_error_undefined_provider": test_error_undefined_provider,
"test_error_non_value_provider": test_error_non_value_provider,
"test_error_animation_not_provider": test_error_animation_not_provider,
"test_error_variable_not_provider": test_error_variable_not_provider,
"test_complex_scenario": test_complex_scenario,
"test_reset_restart_with_comments": test_reset_restart_with_comments
"test_restart_with_comments": test_restart_with_comments
}

View File

@ -603,15 +603,14 @@ def test_error_handling()
# Expected behavior
end
# Test undefined references - simplified transpiler uses runtime resolution
# Test undefined references - should raise exception
var undefined_ref_dsl = "animation test = undefined_pattern"
try
var berry_code = animation_dsl.compile(undefined_ref_dsl)
# Simplified transpiler uses runtime resolution, so this should compile
assert(berry_code != nil, "Should compile with runtime resolution")
assert(false, "Should have raised exception for undefined identifier")
except "dsl_compilation_error" as e, msg
assert(false, "Should not raise exception for undefined references: " + msg)
# Expected behavior - undefined identifiers should raise exceptions
end
print("✓ Error handling test passed")
@ -1114,6 +1113,141 @@ def test_invalid_sequence_commands()
return true
end
# Test template-only transpilation
def test_template_only_transpilation()
print("Testing template-only transpilation...")
# Test single template definition
var single_template_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" param duration\n" +
" param brightness type number\n" +
" \n" +
" animation pulse = pulsating_animation(\n" +
" color=base_color\n" +
" period=duration\n" +
" )\n" +
" pulse.opacity = brightness\n" +
" run pulse\n" +
"}"
var single_code = animation_dsl.compile(single_template_dsl)
assert(single_code != nil, "Should compile single template")
# Should NOT contain engine initialization
assert(string.find(single_code, "var engine = animation.init_strip()") < 0, "Should NOT generate engine initialization for template-only file")
# Should NOT contain engine.run()
assert(string.find(single_code, "engine.run()") < 0, "Should NOT generate engine.run() for template-only file")
# Should contain template function definition
assert(string.find(single_code, "def pulse_effect_template(engine, base_color_, duration_, brightness_)") >= 0, "Should generate template function")
# Should contain function registration
assert(string.find(single_code, "animation.register_user_function('pulse_effect', pulse_effect_template)") >= 0, "Should register template function")
# Test multiple templates
var multiple_templates_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" param duration\n" +
" \n" +
" animation pulse = pulsating_animation(\n" +
" color=base_color\n" +
" period=duration\n" +
" )\n" +
" run pulse\n" +
"}\n" +
"\n" +
"template blink_red {\n" +
" param speed\n" +
" \n" +
" animation blink = pulsating_animation(\n" +
" color=red\n" +
" period=speed\n" +
" )\n" +
" \n" +
" run blink\n" +
"}"
var multiple_code = animation_dsl.compile(multiple_templates_dsl)
assert(multiple_code != nil, "Should compile multiple templates")
# Should NOT contain engine initialization or run
assert(string.find(multiple_code, "var engine = animation.init_strip()") < 0, "Should NOT generate engine initialization for multiple templates")
assert(string.find(multiple_code, "engine.run()") < 0, "Should NOT generate engine.run() for multiple templates")
# Should contain both template functions
assert(string.find(multiple_code, "def pulse_effect_template(") >= 0, "Should generate first template function")
assert(string.find(multiple_code, "def blink_red_template(") >= 0, "Should generate second template function")
# Should contain both registrations
assert(string.find(multiple_code, "animation.register_user_function('pulse_effect'") >= 0, "Should register first template")
assert(string.find(multiple_code, "animation.register_user_function('blink_red'") >= 0, "Should register second template")
print("✓ Template-only transpilation test passed")
return true
end
# Test mixed template and DSL transpilation
def test_mixed_template_dsl_transpilation()
print("Testing mixed template and DSL transpilation...")
# Test template with regular DSL (should generate engine initialization and run)
var mixed_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" param duration\n" +
" \n" +
" animation pulse = pulsating_animation(\n" +
" color=base_color\n" +
" period=duration\n" +
" )\n" +
" run pulse\n" +
"}\n" +
"\n" +
"color my_red = 0xFF0000\n" +
"animation test_anim = solid(color=my_red)\n" +
"run test_anim"
var mixed_code = animation_dsl.compile(mixed_dsl)
assert(mixed_code != nil, "Should compile mixed template and DSL")
# Should contain engine initialization because of non-template DSL
assert(string.find(mixed_code, "var engine = animation.init_strip()") >= 0, "Should generate engine initialization for mixed content")
# Should contain engine.run() because of run statement
assert(string.find(mixed_code, "engine.run()") >= 0, "Should generate engine.run() for mixed content")
# Should contain template function
assert(string.find(mixed_code, "def pulse_effect_template(") >= 0, "Should generate template function")
# Should contain regular DSL elements
assert(string.find(mixed_code, "var my_red_ = 0xFFFF0000") >= 0, "Should generate color definition")
assert(string.find(mixed_code, "var test_anim_ = animation.solid(engine)") >= 0, "Should generate animation definition")
# Test template with property assignment (should generate engine initialization)
var template_with_property_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" \n" +
" animation pulse = pulsating_animation(color=base_color, period=2s)\n" +
" run pulse\n" +
"}\n" +
"\n" +
"animation test_anim = solid(color=red)\n" +
"test_anim.opacity = 128"
var property_code = animation_dsl.compile(template_with_property_dsl)
assert(property_code != nil, "Should compile template with property assignment")
# Should generate engine initialization because of property assignment
assert(string.find(property_code, "var engine = animation.init_strip()") >= 0, "Should generate engine initialization for property assignment")
# Should NOT generate engine.run() because no run statement
assert(string.find(property_code, "engine.run()") < 0, "Should NOT generate engine.run() without run statement")
print("✓ Mixed template and DSL transpilation test passed")
return true
end
# Run all tests
def run_dsl_transpiler_tests()
print("=== DSL Transpiler Test Suite ===")
@ -1141,7 +1275,9 @@ def run_dsl_transpiler_tests()
test_easing_keywords,
test_animation_type_checking,
test_color_type_checking,
test_invalid_sequence_commands
test_invalid_sequence_commands,
test_template_only_transpilation,
test_mixed_template_dsl_transpilation
]
var passed = 0

View File

@ -0,0 +1,188 @@
# DSL Undefined Identifier Test
# Tests that undefined identifiers in function calls are properly caught and reported
#
# This test covers the fix for process_primary_expression() where undefined identifiers
# in function call contexts should raise appropriate error messages.
import animation
import animation_dsl
import string
# Test class to verify undefined identifier error handling
class DSLUndefinedIdentifierTest
var test_results
def init()
self.test_results = []
end
# Helper method to run a test case
def run_test(test_name, test_func)
try
test_func()
self.test_results.push(f"✓ {test_name}")
return true
except .. as e, msg
self.test_results.push(f"✗ {test_name}: {msg}")
return false
end
end
# Test undefined function in animation definition
# Note: This will be caught by animation factory validation, not our new check
def test_undefined_function_in_animation()
var dsl_code =
"animation test = undefined_function(color=red)\n"
"run test"
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
raise "compilation_error", "Should have failed with undefined function error"
except "dsl_compilation_error" as e, msg
# This will be caught by animation factory validation
if string.find(msg, "Animation factory function 'undefined_function' does not exist") == -1
raise "wrong_error", f"Expected animation factory error, got: {msg}"
end
end
end
# Test undefined function in color definition
# Note: This will be caught by color provider validation, not our new check
def test_undefined_function_in_color()
var dsl_code =
"color test_color = undefined_color_provider(period=2s)\n"
"animation test = solid(color=test_color)\n"
"run test"
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
raise "compilation_error", "Should have failed with undefined function error"
except "dsl_compilation_error" as e, msg
# This will be caught by color provider validation
if string.find(msg, "Color provider factory") == -1 && string.find(msg, "does not exist") == -1
raise "wrong_error", f"Expected color provider factory error, got: {msg}"
end
end
end
# Test undefined function in property assignment
def test_undefined_function_in_property()
var dsl_code =
"animation test = solid(color=red)\n"
"test.opacity = undefined_value_provider(min_value=0, max_value=255)\n"
"run test"
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
raise "compilation_error", "Should have failed with undefined function error"
except "dsl_compilation_error" as e, msg
if string.find(msg, "Unknown function or identifier 'undefined_value_provider'") == -1
raise "wrong_error", f"Expected undefined function error, got: {msg}"
end
end
end
# Test undefined function in computed expression
def test_undefined_function_in_computed_expression()
var dsl_code =
"animation test = solid(color=red)\n"
"test.opacity = undefined_function() + 100\n"
"run test"
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
raise "compilation_error", "Should have failed with undefined function error"
except "dsl_compilation_error" as e, msg
if string.find(msg, "Unknown function or identifier 'undefined_function'") == -1
raise "wrong_error", f"Expected undefined function error, got: {msg}"
end
end
end
# Test that valid functions still work (regression test)
def test_valid_functions_still_work()
var dsl_code =
"set osc = triangle(min_value=50, max_value=255, duration=2s)\n"
"animation test = solid(color=red)\n"
"test.opacity = osc\n"
"run test"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Valid functions should compile successfully"
end
# Check that the generated code contains expected elements
if string.find(berry_code, "animation.triangle") == -1
raise "generation_error", "Generated code should contain animation.triangle"
end
end
# Test undefined identifier in regular context (not function call)
def test_undefined_identifier_regular_context()
var dsl_code =
"animation test = solid(color=undefined_color)\n"
"run test"
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
raise "compilation_error", "Should have failed with undefined identifier error"
except "dsl_compilation_error" as e, msg
if string.find(msg, "Unknown identifier 'undefined_color'") == -1
raise "wrong_error", f"Expected undefined identifier error, got: {msg}"
end
end
end
# Run all tests
def run_all_tests()
print("=== DSL Undefined Identifier Test Suite ===")
var tests = [
["Undefined function in animation definition", / -> self.test_undefined_function_in_animation()],
["Undefined function in color definition", / -> self.test_undefined_function_in_color()],
["Undefined function in property assignment", / -> self.test_undefined_function_in_property()],
["Undefined function in computed expression", / -> self.test_undefined_function_in_computed_expression()],
["Valid functions still work (regression)", / -> self.test_valid_functions_still_work()],
["Undefined identifier in regular context", / -> self.test_undefined_identifier_regular_context()]
]
var passed = 0
var total = size(tests)
for test_info : tests
var test_name = test_info[0]
var test_func = test_info[1]
try
if self.run_test(test_name, test_func)
passed += 1
end
except .. as error_type, error_message
print(f"✗ Test crashed: {error_type} - {error_message}")
end
end
print(f"\n=== Test Results: {passed}/{total} passed ===")
for result : self.test_results
print(result)
end
return passed == total
end
end
# Run the tests
var test_suite = DSLUndefinedIdentifierTest()
var success = test_suite.run_all_tests()
if success
print("\n🎉 All undefined identifier tests passed!")
else
print("\n❌ Some undefined identifier tests failed!")
raise "test_failed"
end
return success

View File

@ -165,93 +165,103 @@ class DSLValueProviderValidationTest
end
end
# Test strip_length arithmetic expression (should be wrapped with animation.resolve)
# Test strip_length arithmetic expression (should fail compilation due to dangerous pattern)
def test_strip_length_arithmetic_expression()
var dsl_code = "set strip_len3 = (strip_length() + 1) / 2"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "strip_length arithmetic expression should compile successfully"
var compilation_failed = false
var error_message = ""
try
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
compilation_failed = true
end
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
# Check that it generates closure with animation.resolve wrapper
if string.find(berry_code, "create_closure_value") == -1
raise "generation_error", "Arithmetic expression should create closure"
if !compilation_failed
raise "validation_error", "strip_length in arithmetic expression should cause compilation to fail due to dangerous pattern"
end
# Most importantly, check that strip_length is wrapped with animation.resolve
if string.find(berry_code, "animation.resolve(animation.strip_length(engine))") == -1
raise "generation_error", "strip_length in arithmetic should be wrapped with animation.resolve()"
end
# Check the complete expected pattern
var expected_pattern = "def (engine) return (animation.resolve(animation.strip_length(engine)) + 1) / 2 end"
if string.find(berry_code, expected_pattern) == -1
raise "generation_error", f"Generated code should contain expected pattern: {expected_pattern}"
# Check that the error message mentions the dangerous pattern
if string.find(error_message, "cannot be used in computed expressions") == -1
raise "error_message_error", f"Error message should mention computed expressions restriction, got: {error_message}"
end
end
# Test complex strip_length arithmetic
# Test complex strip_length arithmetic (should fail compilation due to dangerous pattern)
def test_strip_length_complex_arithmetic()
var dsl_code = "set complex = (strip_length() + 5) * 2 - strip_length() / 4"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "Complex strip_length arithmetic should compile successfully"
end
var compilation_failed = false
var error_message = ""
# Check that both strip_length calls are wrapped with animation.resolve
var resolve_count = 0
var pos = 0
while true
var found_pos = string.find(berry_code, "animation.resolve(animation.strip_length(engine))", pos)
if found_pos == -1
break
try
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
compilation_failed = true
end
resolve_count += 1
pos = found_pos + 1
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if resolve_count != 2
raise "generation_error", f"Expected 2 animation.resolve() calls for strip_length, found {resolve_count}"
if !compilation_failed
raise "validation_error", "Complex strip_length arithmetic should cause compilation to fail due to dangerous pattern"
end
# Check that the error message mentions the dangerous pattern
if string.find(error_message, "cannot be used in computed expressions") == -1
raise "error_message_error", f"Error message should mention computed expressions restriction, got: {error_message}"
end
end
# Test mixed user variables and strip_length
# Test mixed user variables and strip_length (should fail due to dangerous pattern)
def test_mixed_variables_and_strip_length()
var dsl_code = "set val1 = 10\nset mixed = val1 + strip_length() * 2"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "Mixed variables and strip_length should compile successfully"
var compilation_failed = false
var error_message = ""
try
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
compilation_failed = true
end
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
# Check that both val1_ and strip_length are properly resolved
if string.find(berry_code, "animation.resolve(val1_)") == -1
raise "generation_error", "User variable val1_ should be wrapped with animation.resolve()"
if !compilation_failed
raise "validation_error", "Mixed variables and strip_length should cause compilation to fail due to dangerous pattern"
end
if string.find(berry_code, "animation.resolve(animation.strip_length(engine))") == -1
raise "generation_error", "strip_length should be wrapped with animation.resolve()"
# Check that the error message mentions the dangerous pattern
if string.find(error_message, "cannot be used in computed expressions") == -1
raise "error_message_error", f"Error message should mention computed expressions restriction, got: {error_message}"
end
end
# Test strip_length in property assignment
# Test strip_length in property assignment (currently allowed due to anonymous function wrapper)
def test_strip_length_in_property_assignment()
var dsl_code = "animation test = solid(color=red)\ntest.opacity = strip_length() / 2"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "strip_length in property assignment should compile successfully"
raise "compilation_error", "strip_length in property assignment should compile (anonymous function wrapper bypasses dangerous pattern detection)"
end
# Check that property assignment creates closure with resolved strip_length
if string.find(berry_code, "test_.opacity = animation.create_closure_value") == -1
raise "generation_error", "Property assignment with arithmetic should create closure"
# Check that it generates an anonymous function wrapper
if string.find(berry_code, "def (engine)") == -1
raise "generation_error", "Property assignment should generate anonymous function wrapper"
end
if string.find(berry_code, "animation.resolve(animation.strip_length(engine))") == -1
raise "generation_error", "strip_length in property assignment should be wrapped with animation.resolve()"
if string.find(berry_code, "animation.strip_length(engine)") == -1
raise "generation_error", "Anonymous function should contain strip_length call"
end
end
@ -275,74 +285,61 @@ class DSLValueProviderValidationTest
end
end
# Test edge case: strip_length with parentheses
# Test edge case: strip_length with parentheses (should fail due to dangerous pattern)
def test_strip_length_with_parentheses()
var dsl_code = "set result = (strip_length()) * 2"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "strip_length with parentheses should compile successfully"
end
var compilation_failed = false
var error_message = ""
# Check that strip_length is still properly resolved even with extra parentheses
if string.find(berry_code, "animation.resolve(animation.strip_length(engine))") == -1
raise "generation_error", "strip_length with parentheses should be wrapped with animation.resolve()"
end
end
# Test edge case: multiple strip_length calls in same expression
def test_multiple_strip_length_calls()
var dsl_code = "set ratio = strip_length() / (strip_length() + 10)"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "Multiple strip_length calls should compile successfully"
end
# Count the number of animation.resolve(animation.strip_length(engine)) calls
var resolve_count = 0
var pos = 0
while true
var found_pos = string.find(berry_code, "animation.resolve(animation.strip_length(engine))", pos)
if found_pos == -1
break
try
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
compilation_failed = true
end
resolve_count += 1
pos = found_pos + 1
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if resolve_count != 2
raise "generation_error", f"Expected 2 resolved strip_length calls, found {resolve_count}"
if !compilation_failed
raise "validation_error", "strip_length with parentheses in arithmetic should cause compilation to fail due to dangerous pattern"
end
# Check that the error message mentions the dangerous pattern
if string.find(error_message, "cannot be used in computed expressions") == -1
raise "error_message_error", f"Error message should mention computed expressions restriction, got: {error_message}"
end
end
# Test edge case: strip_length in nested expressions
# Test edge case: strip_length in nested expressions (should fail due to dangerous pattern)
def test_strip_length_nested_expressions()
var dsl_code = "set nested = ((strip_length() + 1) * 2) / (strip_length() - 1)"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "Nested strip_length expressions should compile successfully"
end
var compilation_failed = false
var error_message = ""
# Both strip_length calls should be resolved
var resolve_count = 0
var pos = 0
while true
var found_pos = string.find(berry_code, "animation.resolve(animation.strip_length(engine))", pos)
if found_pos == -1
break
try
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
compilation_failed = true
end
resolve_count += 1
pos = found_pos + 1
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if resolve_count != 2
raise "generation_error", f"Expected 2 resolved strip_length calls in nested expression, found {resolve_count}"
if !compilation_failed
raise "validation_error", "Nested strip_length expressions should cause compilation to fail due to dangerous pattern"
end
# Check that the error message mentions the dangerous pattern
if string.find(error_message, "cannot be used in computed expressions") == -1
raise "error_message_error", f"Error message should mention computed expressions restriction, got: {error_message}"
end
end
# Test that the fix doesn't affect strip_length when not in arithmetic
# Test that strip_length works in non-arithmetic contexts (direct assignment without closure)
def test_strip_length_non_arithmetic_contexts()
var dsl_code = "animation test = solid(color=red)\ntest.opacity = strip_length()"
@ -351,14 +348,57 @@ class DSLValueProviderValidationTest
raise "compilation_error", "strip_length in non-arithmetic context should compile successfully"
end
# In non-arithmetic context, strip_length should be used directly without resolve
if string.find(berry_code, "test_.opacity = animation.strip_length(engine)") == -1
raise "generation_error", "strip_length in non-arithmetic context should be used directly"
# In property assignment context, strip_length should be assigned directly without closure wrapping
# since it's a value provider instance without additional computation
if string.find(berry_code, "def (engine)") != -1
raise "generation_error", "strip_length in property assignment should NOT use anonymous function wrapper when used alone"
end
# Should not contain animation.resolve for this case
if string.find(berry_code, "animation.resolve(animation.strip_length(engine))") != -1
raise "generation_error", "strip_length in non-arithmetic context should not be wrapped with resolve"
if string.find(berry_code, "animation.strip_length(engine)") == -1
raise "generation_error", "Should contain direct strip_length call"
end
# Should not contain animation.resolve since it's not in a computed expression
if string.find(berry_code, "animation.resolve") != -1
raise "generation_error", "strip_length should not be wrapped with resolve in this context"
end
end
# Test the safe pattern: separate strip_length() call from computation
def test_strip_length_safe_pattern()
var dsl_code = "set strip_len = strip_length()\nset computed = (strip_len + 1) / 2"
var berry_code = animation_dsl.compile(dsl_code)
if berry_code == nil
raise "compilation_error", "Safe strip_length pattern should compile successfully"
end
# Check that strip_len is created as a direct call
if string.find(berry_code, "var strip_len_ = animation.strip_length(engine)") == -1
raise "generation_error", "strip_len should be created as direct call"
end
# Check that computed uses closure with animation.resolve for the variable
if string.find(berry_code, "create_closure_value") == -1
raise "generation_error", "Computed expression should create closure"
end
if string.find(berry_code, "animation.resolve(strip_len_)") == -1
raise "generation_error", "User variable strip_len_ should be wrapped with animation.resolve()"
end
# Should NOT contain direct strip_length calls in the computed expression
var computed_line_start = string.find(berry_code, "var computed_ = ")
if computed_line_start != -1
var computed_line_end = string.find(berry_code, "\n", computed_line_start)
if computed_line_end == -1
computed_line_end = size(berry_code)
end
var computed_line = berry_code[computed_line_start..computed_line_end-1]
if string.find(computed_line, "animation.strip_length(engine)") != -1
raise "generation_error", "Computed expression should not contain direct strip_length calls"
end
end
end
@ -382,9 +422,9 @@ class DSLValueProviderValidationTest
["Strip Length in Property Assignment", / -> self.test_strip_length_in_property_assignment()],
["No Regression with Regular Expressions", / -> self.test_no_regression_with_regular_expressions()],
["Strip Length with Parentheses", / -> self.test_strip_length_with_parentheses()],
["Multiple Strip Length Calls", / -> self.test_multiple_strip_length_calls()],
["Strip Length Nested Expressions", / -> self.test_strip_length_nested_expressions()],
["Strip Length Non-Arithmetic Contexts", / -> self.test_strip_length_non_arithmetic_contexts()]
["Strip Length Non-Arithmetic Contexts", / -> self.test_strip_length_non_arithmetic_contexts()],
["Strip Length Safe Pattern", / -> self.test_strip_length_safe_pattern()]
]
for test : tests

View File

@ -130,7 +130,8 @@ def test_dsl_event_compilation()
"# strip length 30 # TEMPORARILY DISABLED\n"
"color custom_red = 0xFF0000\n"
"on button_press: solid(custom_red)\n"
"run solid(custom_red)"
"animation anim = solid(color=custom_red)"
"run anim"
var compiled_code = animation_dsl.compile(dsl_code)
@ -145,8 +146,10 @@ def test_dsl_event_with_parameters()
var dsl_code =
"# strip length 30 # TEMPORARILY DISABLED\n"
"color custom_blue = 0x0000FF\n"
"color custom_red = 0xFF0000\n"
"on timer(5s): solid(custom_blue)\n"
"run solid(custom_blue)"
"animation anim = solid(color=custom_red)"
"run anim"
var compiled_code = animation_dsl.compile(dsl_code)

View File

@ -46,21 +46,17 @@ def test_undefined_variable_exception()
print("Testing undefined variable exception behavior...")
var dsl_code = "animation test = solid(color=undefined_var)"
var berry_code = animation_dsl.compile(dsl_code)
assert(berry_code != nil, "Should compile DSL code")
# Check that undefined variables use direct underscore notation
import string
assert(string.find(berry_code, "test_.color = undefined_var_") >= 0, "Should use undefined_var_ directly for undefined variable")
# Verify the generated code fails to compile (due to undefined variable)
# This is better than the old behavior - we catch undefined variables at compile time!
# The new transpiler behavior is to catch undefined variables at DSL compile time
# This is better than the old behavior - we catch errors earlier!
try
var compiled_code = compile(berry_code)
assert(false, "Should have failed to compile due to undefined variable")
except .. as e, msg
print(f"✓ Correctly failed to compile due to undefined variable: {e}")
var berry_code = animation_dsl.compile(dsl_code)
assert(false, "Should have failed to compile DSL due to undefined variable")
except "dsl_compilation_error" as e, msg
print(f"✓ Correctly failed to compile DSL due to undefined variable: {e}")
# Verify the error message mentions the undefined variable
import string
assert(string.find(msg, "undefined_var") >= 0, "Error message should mention undefined_var")
end
print("✓ Undefined variable exception test passed")

View File

@ -589,7 +589,8 @@ def test_alternative_syntax_integration()
" 0xFFFF00\n" +
"]\n" +
"\n" +
"animation fire_anim = rich_palette_animation(palette=fire_colors, cycle_period=3s)\n" +
"color rich_palette2 = color_cycle(palette=fire_colors, cycle_period=3s)\n"
"animation fire_anim = solid(color=rich_palette2)\n" +
"\n" +
"run fire_anim\n"
@ -599,7 +600,8 @@ def test_alternative_syntax_integration()
# Verify the generated code contains the expected elements
import string
assert(string.find(berry_code, "var fire_colors_ = bytes(") != -1, "Should contain palette definition")
assert(string.find(berry_code, "rich_palette_animation(engine)") != -1, "Should contain animation creation")
assert(string.find(berry_code, "color_cycle(engine)") != -1, "Should contain value provider creation")
assert(string.find(berry_code, "solid(engine)") != -1, "Should contain animation creation")
assert(string.find(berry_code, "fire_colors_") != -1, "Should reference the palette")
print("✓ Alternative syntax integration test passed")

View File

@ -36,13 +36,13 @@ def test_basic_symbol_registration()
return true
end
# Test forward reference resolution
def test_forward_reference_resolution()
print("Testing forward reference resolution...")
# Test proper symbol ordering (no forward references)
def test_proper_symbol_ordering()
print("Testing proper symbol ordering...")
# DSL with forward reference: animation uses color defined later
var dsl_source = "animation fire_pattern = solid(color=custom_red)\n" +
"color custom_red = 0xFF0000"
# DSL with proper ordering: color defined before animation uses it
var dsl_source = "color custom_red = 0xFF0000\n" +
"animation fire_pattern = solid(color=custom_red)"
var lexer = animation_dsl.DSLLexer(dsl_source)
var tokens = lexer.tokenize()
@ -50,20 +50,20 @@ def test_forward_reference_resolution()
var berry_code = transpiler.transpile()
# Should resolve the forward reference successfully
assert(berry_code != nil, "Should compile with forward reference")
assert(!transpiler.has_errors(), "Should resolve forward reference without errors")
# Should compile successfully with proper ordering
assert(berry_code != nil, "Should compile with proper symbol ordering")
assert(!transpiler.has_errors(), "Should have no errors with proper ordering")
# Check generated code contains both definitions (with underscore suffix)
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should define custom_red color")
assert(string.find(berry_code, "var fire_pattern_ = animation.solid(engine)") >= 0, "Should define fire animation")
assert(string.find(berry_code, "fire_pattern_.color = custom_red_") >= 0, "Should reference custom_red")
print("✓ Forward reference resolution test passed")
print("✓ Proper symbol ordering test passed")
return true
end
# Test undefined reference handling (simplified transpiler uses runtime resolution)
# Test undefined reference handling (should fail at transpile time)
def test_undefined_reference_handling()
print("Testing undefined reference handling...")
@ -76,22 +76,13 @@ def test_undefined_reference_handling()
var berry_code = transpiler.transpile()
# New behavior: transpiler generates direct reference to undefined_color_
assert(berry_code != nil, "Should compile with direct reference")
assert(!transpiler.has_errors(), "Should have no compile-time errors")
# Should detect undefined reference at transpile time
assert(transpiler.has_errors(), "Should detect undefined reference error")
# Check that direct reference is generated (since undefined_color doesn't exist in animation module)
assert(string.find(berry_code, "undefined_color_") >= 0, "Should generate runtime resolution")
# With new behavior, Berry compilation will fail due to undefined variable
# This is actually better than runtime errors as it catches issues earlier
try
var compiled_code = compile(berry_code)
assert(false, "Should fail to compile due to undefined variable")
except .. as e, msg
print(f"✓ Correctly caught undefined variable at compile time: {e}")
assert(string.find(str(msg), "undefined_color_") >= 0, "Error should mention undefined variable")
end
# Check that error message mentions the undefined symbol
var error_report = transpiler.get_error_report()
assert(string.find(error_report, "undefined_color") >= 0, "Error should mention undefined_color")
assert(string.find(error_report, "Unknown identifier") >= 0, "Should be an unknown identifier error")
print("✓ Undefined reference handling test passed")
return true
@ -147,14 +138,14 @@ def test_definition_generation()
return true
end
# Test complex forward references
def test_complex_forward_references()
print("Testing complex forward references...")
# Test complex symbol dependencies with proper ordering
def test_complex_symbol_dependencies()
print("Testing complex symbol dependencies...")
# Complex DSL with multiple forward references
var dsl_source = "animation complex_anim = pulsating_animation(color=primary_color, period=3000)\n" +
# Complex DSL with proper symbol ordering (no forward references)
var dsl_source = "color primary_color = 0xFF8000\n" +
"animation complex_anim = pulsating_animation(color=primary_color, period=3000)\n" +
"animation gradient_pattern = solid(color=primary_color)\n" +
"color primary_color = 0xFF8000\n" +
"sequence demo {\n" +
" play complex_anim for 5s\n" +
"}\n" +
@ -166,9 +157,9 @@ def test_complex_forward_references()
var berry_code = transpiler.transpile()
# Should resolve all forward references
assert(berry_code != nil, "Should compile complex forward references")
assert(!transpiler.has_errors(), "Should resolve all forward references")
# Should compile successfully with proper ordering
assert(berry_code != nil, "Should compile complex dependencies")
assert(!transpiler.has_errors(), "Should have no errors with proper ordering")
# Check all definitions are present (with underscore suffix)
assert(string.find(berry_code, "var primary_color_") >= 0, "Should define primary color")
@ -176,7 +167,7 @@ def test_complex_forward_references()
assert(string.find(berry_code, "var complex_anim_") >= 0, "Should define complex animation")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence")
print("✓ Complex forward references test passed")
print("✓ Complex symbol dependencies test passed")
return true
end
@ -186,11 +177,11 @@ def run_symbol_registry_tests()
var tests = [
test_basic_symbol_registration,
test_forward_reference_resolution,
test_proper_symbol_ordering,
test_undefined_reference_handling,
test_builtin_reference_handling,
test_definition_generation,
test_complex_forward_references
test_complex_symbol_dependencies
]
var passed = 0

View File

@ -0,0 +1,357 @@
# Symbol Table Test Suite
# Tests for the SymbolEntry and SymbolTable classes
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota def log(x) print(x) end" lib/libesp32/berry_animation/src/tests/symbol_table_test.be
import animation
import animation_dsl
import string
# Test SymbolEntry creation and basic properties
def test_symbol_entry_creation()
print("Testing SymbolEntry creation...")
# Test palette constant entry
var palette_entry = animation_dsl._symbol_entry.create_palette_constant("PALETTE_RAINBOW", nil, true)
assert(palette_entry.name == "PALETTE_RAINBOW", "Should set name correctly")
assert(palette_entry.type == "palette_constant", "Should set type correctly")
assert(palette_entry.is_builtin == true, "Should be marked as builtin")
assert(palette_entry.takes_args == false, "Palette constants don't take args")
assert(palette_entry.is_dangerous_call() == false, "Palette constants are not dangerous")
# Test animation constructor entry
var anim_entry = animation_dsl._symbol_entry.create_animation_constructor("solid", nil, true)
assert(anim_entry.type == "animation_constructor", "Should be animation constructor")
assert(anim_entry.takes_named_args() == true, "Animation constructors take named args")
assert(anim_entry.is_dangerous_call() == true, "Animation constructors are dangerous")
# Test user function entry
var func_entry = animation_dsl._symbol_entry.create_user_function("my_func", false)
assert(func_entry.type == "user_function", "Should be user function")
assert(func_entry.takes_positional_args() == true, "User functions take positional args")
assert(func_entry.is_builtin == false, "Should not be builtin")
print("✓ SymbolEntry creation test passed")
return true
end
# Test SymbolEntry reference generation
def test_symbol_entry_references()
print("Testing SymbolEntry reference generation...")
# Test builtin reference
var builtin_entry = animation_dsl._symbol_entry.create_animation_constructor("solid", nil, true)
assert(builtin_entry.get_reference() == "animation.solid", "Should generate builtin reference")
# Test math function reference
var math_entry = animation_dsl._symbol_entry.create_math_function("max", true)
assert(math_entry.get_reference() == "animation._math.max", "Should generate math function reference")
# Test user-defined reference
var user_entry = animation_dsl._symbol_entry.create_animation_instance("my_anim", nil, false)
assert(user_entry.get_reference() == "my_anim_", "Should generate user reference with underscore")
print("✓ SymbolEntry reference generation test passed")
return true
end
# Test SymbolEntry argument detection
def test_symbol_entry_argument_detection()
print("Testing SymbolEntry argument detection...")
# Test different types and their argument characteristics
var tests = [
["palette_constant", false, "none"],
["math_function", true, "positional"],
["user_function", true, "positional"],
["value_provider_constructor", true, "named"],
["animation_constructor", true, "named"],
["color_constructor", true, "named"],
["variable", false, "none"],
["sequence", false, "none"]
]
for test : tests
var typ = test[0]
var expected_takes_args = test[1]
var expected_arg_type = test[2]
var entry = animation_dsl._symbol_entry("test_symbol", typ, nil, false)
assert(entry.takes_args == expected_takes_args, f"Type {typ} should have takes_args={expected_takes_args}")
assert(entry.arg_type == expected_arg_type, f"Type {typ} should have arg_type={expected_arg_type}")
end
print("✓ SymbolEntry argument detection test passed")
return true
end
# Test SymbolEntry danger detection
def test_symbol_entry_danger_detection()
print("Testing SymbolEntry danger detection...")
# Test dangerous types (constructors)
var dangerous_types = ["value_provider_constructor", "animation_constructor", "color_constructor"]
for typ : dangerous_types
var entry = animation_dsl._symbol_entry("test", typ, nil, true)
assert(entry.is_dangerous_call() == true, f"Type {typ} should be dangerous")
end
# Test safe types
var safe_types = ["palette_constant", "math_function", "user_function", "variable", "sequence"]
for typ : safe_types
var entry = animation_dsl._symbol_entry("test", typ, nil, true)
assert(entry.is_dangerous_call() == false, f"Type {typ} should be safe")
end
print("✓ SymbolEntry danger detection test passed")
return true
end
# Test SymbolTable basic operations
def test_symbol_table_basic_operations()
print("Testing SymbolTable basic operations...")
var table = animation_dsl._symbol_table()
# Test adding a symbol
var entry = animation_dsl._symbol_entry.create_variable("test_var", false)
var added_entry = table.add("test_var", entry)
assert(added_entry == entry, "Should return the added entry")
# Test checking if symbol exists
assert(table.contains("test_var") == true, "Should contain added symbol")
assert(table.contains("nonexistent") == false, "Should not contain nonexistent symbol")
# Test getting symbol
var retrieved = table.get("test_var")
assert(retrieved == entry, "Should retrieve the same entry")
# Test getting reference
var ref = table.get_reference("test_var")
assert(ref == "test_var_", "Should generate correct reference")
print("✓ SymbolTable basic operations test passed")
return true
end
# Test SymbolTable dynamic detection
def test_symbol_table_dynamic_detection()
print("Testing SymbolTable dynamic detection...")
var table = animation_dsl._symbol_table()
# Test detection of built-in animation constructor
assert(table.contains("solid") == true, "Should detect built-in solid function")
var solid_entry = table.get("solid")
assert(solid_entry != nil, "Should get solid entry")
assert(solid_entry.type == "animation_constructor", "Should detect as animation constructor")
assert(solid_entry.is_builtin == true, "Should be marked as builtin")
# Test detection of built-in math function
assert(table.contains("max") == true, "Should detect built-in max function")
var max_entry = table.get("max")
assert(max_entry != nil, "Should get max entry")
assert(max_entry.type == "math_function", "Should detect as math function")
# Test detection of built-in palette
assert(table.contains("PALETTE_RAINBOW") == true, "Should detect built-in palette")
var palette_entry = table.get("PALETTE_RAINBOW")
assert(palette_entry != nil, "Should get palette entry")
assert(palette_entry.type == "palette_constant", "Should detect as palette constant")
print("✓ SymbolTable dynamic detection test passed")
return true
end
# Test SymbolTable conflict detection
def test_symbol_table_conflict_detection()
print("Testing SymbolTable conflict detection...")
var table = animation_dsl._symbol_table()
# Try to redefine a built-in symbol with different type
var user_entry = animation_dsl._symbol_entry.create_variable("solid", false)
try
table.add("solid", user_entry)
assert(false, "Should have thrown conflict error")
except "symbol_redefinition_error" as e, msg
assert(string.find(str(msg), "solid") >= 0, "Error should mention symbol name")
assert(string.find(str(msg), "animation_constructor") >= 0, "Error should mention built-in type")
end
# Test that same type redefinition is allowed
var anim_entry1 = animation_dsl._symbol_entry.create_animation_instance("my_anim", nil, false)
var anim_entry2 = animation_dsl._symbol_entry.create_animation_instance("my_anim", nil, false)
table.add("my_anim", anim_entry1)
table.add("my_anim", anim_entry2) # Should not throw
print("✓ SymbolTable conflict detection test passed")
return true
end
# Test SymbolTable creation methods
def test_symbol_table_creation_methods()
print("Testing SymbolTable creation methods...")
var table = animation_dsl._symbol_table()
# Test create_palette
var palette_entry = table.create_palette("my_palette", nil)
assert(palette_entry.type == "palette", "Should create palette entry")
assert(table.contains("my_palette") == true, "Should add to table")
# Test create_color
var color_entry = table.create_color("my_color", nil)
assert(color_entry.type == "color", "Should create color entry")
assert(table.contains("my_color") == true, "Should add to table")
# Test create_animation
var anim_entry = table.create_animation("my_anim", nil)
assert(anim_entry.type == "animation", "Should create animation entry")
assert(table.contains("my_anim") == true, "Should add to table")
# Test create_value_provider
var vp_entry = table.create_value_provider("my_vp", nil)
assert(vp_entry.type == "value_provider", "Should create value provider entry")
assert(table.contains("my_vp") == true, "Should add to table")
# Test create_variable
var var_entry = table.create_variable("my_var")
assert(var_entry.type == "variable", "Should create variable entry")
assert(table.contains("my_var") == true, "Should add to table")
# Test create_sequence
var seq_entry = table.create_sequence("my_seq")
assert(seq_entry.type == "sequence", "Should create sequence entry")
assert(table.contains("my_seq") == true, "Should add to table")
# Test create_template
var template_entry = table.create_template("my_template", {"param1": "int", "param2": "string"})
assert(template_entry.type == "template", "Should create template entry")
assert(template_entry.get_param_types()["param1"] == "int", "Should set parameter types")
assert(table.contains("my_template") == true, "Should add to table")
print("✓ SymbolTable creation methods test passed")
return true
end
# Test SymbolTable named color handling
def test_symbol_table_named_colors()
print("Testing SymbolTable named color handling...")
var table = animation_dsl._symbol_table()
# Test named color detection
assert(table.symbol_exists("red") == true, "Should recognize named color 'red'")
assert(table.symbol_exists("blue") == true, "Should recognize named color 'blue'")
assert(table.symbol_exists("nonexistent_color") == false, "Should not recognize invalid color")
# Test named color reference generation
var red_ref = table.get_reference("red")
assert(red_ref == "0xFFFF0000", "Should generate correct hex value for red")
var blue_ref = table.get_reference("blue")
assert(blue_ref == "0xFF0000FF", "Should generate correct hex value for blue")
print("✓ SymbolTable named color handling test passed")
return true
end
# Test SymbolTable utility methods
def test_symbol_table_utility_methods()
print("Testing SymbolTable utility methods...")
var table = animation_dsl._symbol_table()
# Add some test symbols
table.create_animation("test_anim", nil)
table.create_variable("test_var")
# Test get_type
assert(table.get_type("test_anim") == "animation", "Should return correct type")
assert(table.get_type("solid") == "animation_constructor", "Should return builtin type")
assert(table.get_type("nonexistent") == nil, "Should return nil for nonexistent")
# Test takes_args
assert(table.takes_args("solid") == true, "Solid should take args")
assert(table.takes_args("test_var") == false, "Variables don't take args")
# Test takes_named_args
assert(table.takes_named_args("solid") == true, "Solid takes named args")
assert(table.takes_named_args("max") == false, "Max takes positional args")
# Test takes_positional_args
assert(table.takes_positional_args("max") == true, "Max takes positional args")
assert(table.takes_positional_args("solid") == false, "Solid doesn't take positional args")
# Test is_dangerous
assert(table.is_dangerous("solid") == true, "Solid is dangerous (constructor)")
assert(table.is_dangerous("max") == false, "Max is not dangerous")
assert(table.is_dangerous("test_var") == false, "Variables are not dangerous")
print("✓ SymbolTable utility methods test passed")
return true
end
# Test MockEngine functionality
def test_mock_engine()
print("Testing MockEngine functionality...")
var mock = animation_dsl.MockEngine()
assert(mock.time_ms == 0, "Should initialize time to 0")
assert(mock.get_strip_length() == 30, "Should return default strip length")
print("✓ MockEngine test passed")
return true
end
# Run all symbol table tests
def run_symbol_table_tests()
print("=== Symbol Table Test Suite ===")
var tests = [
test_symbol_entry_creation,
test_symbol_entry_references,
test_symbol_entry_argument_detection,
test_symbol_entry_danger_detection,
test_symbol_table_basic_operations,
test_symbol_table_dynamic_detection,
test_symbol_table_conflict_detection,
test_symbol_table_creation_methods,
test_symbol_table_named_colors,
test_symbol_table_utility_methods,
test_mock_engine
]
var passed = 0
var total = size(tests)
for test_func : tests
try
if test_func()
passed += 1
else
print("✗ Test failed")
end
except .. as error_type, error_message
print("✗ Test crashed: " + str(error_type) + " - " + str(error_message))
end
print("") # Add spacing between tests
end
print("=== Results: " + str(passed) + "/" + str(total) + " tests passed ===")
if passed == total
print("🎉 All symbol table tests passed!")
return true
else
print("❌ Some symbol table tests failed")
raise "test_failed"
end
end
# Auto-run tests when file is executed
run_symbol_table_tests()

View File

@ -8,6 +8,7 @@ import tasmota
# Import the animation module
import animation
import user_functions
# Define a function to run a test file
def run_test_file(file_path)
@ -56,8 +57,8 @@ def run_all_tests()
# Animation effect tests
"lib/libesp32/berry_animation/src/tests/filled_animation_test.be",
"lib/libesp32/berry_animation/src/tests/pulse_animation_test.be",
"lib/libesp32/berry_animation/src/tests/breathe_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/pulse_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/breathe_animation_test.be",
"lib/libesp32/berry_animation/src/tests/color_cycle_animation_test.be",
"lib/libesp32/berry_animation/src/tests/color_cycle_bytes_test.be", # Tests ColorCycleColorProvider with bytes palette
"lib/libesp32/berry_animation/src/tests/color_cycle_palette_size_test.be", # Tests ColorCycleColorProvider palette_size read-only parameter
@ -70,17 +71,17 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/beacon_animation_test.be",
"lib/libesp32/berry_animation/src/tests/gradient_animation_test.be",
"lib/libesp32/berry_animation/src/tests/noise_animation_test.be",
"lib/libesp32/berry_animation/src/tests/plasma_animation_test.be",
"lib/libesp32/berry_animation/src/tests/sparkle_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/plasma_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/sparkle_animation_test.be",
"lib/libesp32/berry_animation/src/tests/wave_animation_test.be",
"lib/libesp32/berry_animation/src/tests/palette_pattern_animation_test.be",
# Motion effects tests
"lib/libesp32/berry_animation/src/tests/shift_animation_test.be",
"lib/libesp32/berry_animation/src/tests/bounce_animation_test.be",
"lib/libesp32/berry_animation/src/tests/scale_animation_test.be",
"lib/libesp32/berry_animation/src/tests/jitter_animation_test.be",
"lib/libesp32/berry_animation/src/tests/motion_effects_test.be",
# "lib/libesp32/berry_animation/src/tests/shift_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/bounce_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/scale_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/jitter_animation_test.be",
# "lib/libesp32/berry_animation/src/tests/motion_effects_test.be",
# Color and parameter tests
"lib/libesp32/berry_animation/src/tests/crenel_position_color_test.be",
@ -117,9 +118,14 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/palette_dsl_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_parameter_validation_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_value_provider_validation_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_undefined_identifier_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_newline_syntax_test.be",
"lib/libesp32/berry_animation/src/tests/test_math_method_transpilation.be",
"lib/libesp32/berry_animation/src/tests/test_user_functions_in_computed_parameters.be",
"lib/libesp32/berry_animation/src/tests/dsl_berry_code_blocks_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_lexer_triple_quotes_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_berry_integration_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_restart_test.be",
# Event system tests
"lib/libesp32/berry_animation/src/tests/event_system_test.be"

View File

@ -111,7 +111,8 @@ def test_is_math_method_function()
# Test mathematical methods
var math_methods = ["min", "max", "abs", "round", "sqrt", "scale", "sin", "cos"]
for method : math_methods
if !transpiler.is_math_method(method)
var entry = transpiler.symbol_table.get(method)
if entry == nil || entry.type != animation_dsl._symbol_entry.TYPE_MATH_FUNCTION
print(f" ❌ {method} should be detected as a math method")
return false
else
@ -122,7 +123,8 @@ def test_is_math_method_function()
# Test non-mathematical methods
var non_math_methods = ["pulsating_animation", "solid", "color_cycle", "unknown_method"]
for method : non_math_methods
if transpiler.is_math_method(method)
var entry = transpiler.symbol_table.get(method)
if entry != nil && entry.type == animation_dsl._symbol_entry.TYPE_MATH_FUNCTION
print(f" ❌ {method} should NOT be detected as a math method")
return false
else

View File

@ -31,13 +31,13 @@ def test_transpilation_case(dsl_code, expected_user_function, test_name)
print(f" Generated code:\n{generated_code}")
# Check that user function is called with self.engine
var expected_call = f"animation.get_user_function('{expected_user_function}')(self.engine"
# Check that user function is called with engine parameter
var expected_call = f"animation.get_user_function('{expected_user_function}')(engine"
if string.find(generated_code, expected_call) < 0
print(f" ❌ Expected to find '{expected_call}' in generated code")
return false
else
print(f" ✅ Found user function call with self.engine: '{expected_call}'")
print(f" ✅ Found user function call with engine: '{expected_call}'")
end
# Verify the code compiles
@ -87,6 +87,7 @@ def test_user_function_in_computed_parameter()
# Test case 1: Simple user function in computed parameter
var dsl_code1 =
"import user_functions\n"
"set strip_len = strip_length()\n"
"animation test = pulsating_animation(color=red, period=2s)\n"
"test.opacity = rand_demo()\n"
@ -99,9 +100,10 @@ def test_user_function_in_computed_parameter()
# Test case 2: User function with mathematical functions
var dsl_code2 =
"import user_functions\n"
"set strip_len = strip_length()\n"
"animation test = solid(color=red)\n"
"test.brightness = max(100, rand_demo())\n"
"test.opacity = max(100, rand_demo())\n"
"run test"
var result2 = test_transpilation_case(dsl_code2, "rand_demo", "User function with mathematical functions")
@ -111,9 +113,10 @@ def test_user_function_in_computed_parameter()
# Test case 3: User function in arithmetic expressions
var dsl_code3 =
"import user_functions\n"
"set strip_len = strip_length()\n"
"animation test = solid(color=green)\n"
"test.position = abs(rand_demo() - 128) + 64\n"
"test.opacity = abs(rand_demo() - 128) + 64\n"
"run test"
var result3 = test_transpilation_case(dsl_code3, "rand_demo", "User function in arithmetic expressions")
@ -136,7 +139,8 @@ if test1_result && test2_result
print("\n🎉 All tests passed!")
print("✅ User functions are correctly detected")
print("✅ User functions work correctly in computed parameters")
print("✅ User functions are called with self.engine in closure context")
print("✅ User functions are called with engine in closure context")
else
print("\n❌ Some tests failed!")
raise "test_failed"
end

View File

@ -33,7 +33,7 @@ def test_user_function_in_computed_parameters()
var dsl_code =
"animation random_base = solid(color=blue, priority=10)\n"
"random_base.opacity = user.rand_demo()\n"
"random_base.opacity = rand_demo()\n"
"run random_base"
try
@ -57,7 +57,7 @@ def test_user_function_with_math()
var dsl_code =
"animation random_bounded = solid(color=orange, priority=8)\n"
"random_bounded.opacity = max(50, min(255, user.rand_demo() + 100))\n"
"random_bounded.opacity = max(50, min(255, rand_demo() + 100))\n"
"run random_bounded"
try
@ -83,7 +83,7 @@ def test_user_function_in_arithmetic()
var dsl_code =
"animation random_variation = solid(color=purple, priority=15)\n"
"random_variation.opacity = abs(user.rand_demo() - 128) + 64\n"
"random_variation.opacity = abs(rand_demo() - 128) + 64\n"
"run random_variation"
try
@ -109,7 +109,7 @@ def test_complex_user_function_expressions()
var dsl_code =
"animation random_complex = solid(color=white, priority=20)\n"
"random_complex.opacity = round((user.rand_demo() + 128) / 2 + abs(user.rand_demo() - 100))\n"
"random_complex.opacity = round((rand_demo() + 128) / 2 + abs(rand_demo() - 100))\n"
"run random_complex"
try
@ -140,8 +140,8 @@ def test_generated_code_validity()
var dsl_code =
"animation random_multi = solid(color=cyan, priority=12)\n"
"random_multi.opacity = user.rand_demo()\n"
"random_multi.duration = max(100, user.rand_demo())\n"
"random_multi.opacity = rand_demo()\n"
"random_multi.duration = max(100, rand_demo())\n"
"run random_multi"
try