Skip to content

evaluation is order dependent in local scope (missing attribute) #2072

Description

@ahkinsat

Bug: Lambda evaluation only computes last field when depending on local variables

Disclaimer: This issue is the result of human-AI collaboration after searching for the bug cause . I indeed experienced and reproduced the error and provided the code(partially the file is long and contains bunch of colors). The analysis down below was primarily generated by an LLM (DeepSeek) and is speculative - the author does not fully understand the codebase and this analysis may be incorrect. Please verify independently.

Description

When defining a lambda that creates a config object with multiple computed fields depending on local variables (defined in the same lambda), only the last field in declaration order appears in the output. Earlier fields are missing entirely.

This works correctly in global scope but fails in lambda/local scope.

Reproduction

# some types and schemas like Palette
...
# Base data structure (validated as correct YAML was indeed output)
everforest_src = {
	light = {
		fg = {...}, 
		bg = {
			hard = {...}, 
			..other variants
		}
	}, 
	dark = {..similar to light mode}
}  # Schema-validated theme, no errors

# ✅ WORKS - Global scope
_mode = "light"
_variant = "hard"
everforest_light_hard = {
    mode = _mode
    variant = _variant
    fg = everforest_src[mode].fg
    bg = everforest_src[mode].bg[variant]
}
# Result: {mode="light", variant="hard", fg=..., bg=...} - both fields present

# ❌ FAILS - Lambda scope (fg first, bg last)
get_palette2 = lambda _src, _mode, _variant -> {
    mode = _mode
    variant = _variant
    fg = everforest_src[mode].fg
    bg = everforest_src[mode].bg[variant]
}
everforest_light_hard2 = get_palette2(everforest_src, "light", "hard")
# Result: {mode="light", variant="hard", bg=...} - fg missing entirely

# ❌ FAILS - Lambda scope (bg first, fg last)
get_palette3 = lambda _src, _mode, _variant -> {
    mode = _mode
    variant = _variant
    bg = everforest_src[mode].bg[variant]
    fg = everforest_src[mode].fg
}
everforest_light_hard3 = get_palette3(everforest_src, "light", "hard")
# Result: {mode="light", variant="hard", fg=...} - bg missing entirely

# ❌ STILL FAILS - Even with intermediate variable
get_palette4 = lambda _src, _mode, _variant -> {
    mode = _mode
    variant = _variant
    mode_obj = _src[_mode]
    bg = mode_obj.bg[_variant]
    fg = mode_obj.fg
}
everforest_light_hard4 = get_palette4(everforest_src, "light", "hard")
# Result: Still only the last field appears (fg or bg depending on order, here bg missing)

Expected Behavior

All fields in the returned config should be present regardless of their declaration order. Both fg and bg should appear in all lambda examples.

Actual Behavior

Only the field that appears last in the lambda body appears in the output. The field(s) before it are completely missing.

Environment

  • KCL version: 0.12.3-linux-amd64 (installed via go install)
  • OS: Debian 13

Analysis Context

The issue was investigated by examining the KCL repository structure. Starting from compiler base(apprently empty), the investigation focused on the evaluator crate, particularly the interaction between scope handling (scope.rs), lazy evaluation (lazy.rs), and execution context (context.rs).

Speculative Analysis

Disclaimer: The following analysis was AI-generated and may be incorrect.(i don't fully understand this). It's provided as a starting point for maintainers who know the codebase.

The issue appears to stem from how KCL handles variables in lambda scopes versus global scope. In global scope, variables are properly evaluated through the lazy evaluation system. In lambda scope, variables seem to be tracked in two parallel systems that don't synchronize correctly.

When a lambda executes:

  1. Local variables (_mode, _variant) are stored in the current Scope via add_variable()
  2. Fields (fg, bg) have setters recorded in LazyEvalScope via emit_setters()
  3. When accessing variables during computation, lambda uses direct scope lookup (get_variable()) which bypasses lazy evaluation(calls undefined value instead of actually triggering evaluation)
  4. When constructing the final result, it uses get_value_from_lazy_scope() which only fully evaluates the last accessed field due to backtracking logic

This creates a race condition where earlier fields never get properly forced through the lazy evaluation system, causing them to be missing from the output.

The key files that may contain the issue:

  • scope.rs: Variable resolution and lambda scope handling
  • lazy.rs: Lazy evaluation and backtracking mechanism
  • context.rs: Lambda context management

Possible Impact

This affects any KCL code that uses lambdas to generate config objects with multiple computed fields - a common pattern for configuration generation, theming, and reusable logic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions