execute method

  1. @override
  2. @override
JinjaValue execute(
  1. Context ctx
)
override

Executes the statement within the given ctx and returns the result as a JinjaValue.

Implementation

@override
@override
JinjaValue execute(Context ctx) {
  // Handle slice expressions first, as they are a special case of computed access
  if (computed && property is SliceExpression) {
    final slice = property as SliceExpression;
    final startVal = slice.start?.execute(ctx);
    final stopVal = slice.stop?.execute(ctx);
    final stepVal = slice.step?.execute(ctx);

    final obj = object.execute(ctx);

    int? start = (startVal != null && !startVal.isNone)
        ? startVal.asInt
        : null;
    int? stop = (stopVal != null && !stopVal.isNone) ? stopVal.asInt : null;
    int step = (stepVal != null && !stepVal.isNone) ? stepVal.asInt : 1;

    if (obj is JinjaList || obj is JinjaTuple) {
      final List<JinjaValue> itemsList = obj is JinjaList
          ? obj.items
          : (obj as JinjaTuple).items;
      int len = itemsList.length;
      start ??= step > 0 ? 0 : len - 1;
      stop ??= step > 0 ? len : -1;

      if (start < 0) start += len;
      if (stop < 0 && slice.stop != null) {
        stop += len; // Only adjust if explicit negative
      }

      // Clamp
      if (step > 0) {
        if (start < 0) start = 0;
        if (stop > len) stop = len;
      } else {
        if (start >= len) start = len - 1;
        if (stop < -1) stop = -1;
      }

      final res = <JinjaValue>[];
      if (step > 0) {
        for (int i = start; i < stop; i += step) {
          if (i >= 0 && i < len) res.add(itemsList[i]);
        }
      } else if (step < 0) {
        for (int i = start; i > stop; i += step) {
          if (i >= 0 && i < len) res.add(itemsList[i]);
        }
      }
      return obj is JinjaList ? JinjaList(res) : JinjaTuple(res);
    }
    if (obj is JinjaStringValue) {
      final s = obj.value;
      int len = s.length;
      start ??= step > 0 ? 0 : len - 1;
      stop ??= step > 0 ? len : -1;

      if (start < 0) start += len;
      if (stop < 0 && slice.stop != null) stop += len;

      // Clamp
      if (step > 0) {
        if (start < 0) start = 0;
        if (stop > len) stop = len;
      } else {
        if (start >= len) start = len - 1;
        if (stop < -1) stop = -1;
      }

      if (step == 1) {
        if (start >= stop) return const JinjaStringValue(JinjaString([]));
        return JinjaStringValue(s.substring(start, stop));
      }

      final parts = <JinjaStringPart>[];
      if (step > 0) {
        for (int i = start; i < stop; i += step) {
          if (i >= 0 && i < len) parts.addAll(s[i].parts);
        }
      } else if (step < 0) {
        for (int i = start; i > stop; i += step) {
          if (i >= 0 && i < len) parts.addAll(s[i].parts);
        }
      }
      return JinjaStringValue(JinjaString(parts));
    }

    return const JinjaUndefined();
  }

  if (computed || property is IntegerLiteral) {
    // Bracket notation: obj[expr], or an index after a dot: obj.0
    final obj = object.execute(ctx);
    final prop = property.execute(ctx);
    if (!computed && prop.asInt < 0) {
      throw Exception('Static member property cannot be negative');
    }

    if (obj is JinjaMap) {
      // Bracket access: check keys FIRST
      if (obj.items.containsKey(prop)) {
        return obj.items[prop]!;
      }

      // Special case for 'env': NO methods allowed even via bracket access if it's the env object
      if (obj.isEnv) {
        return const JinjaUndefined();
      }

      // Then check if the key is a string that matches a method
      if (prop is JinjaStringValue) {
        final method = resolveMember(obj, prop.toString());
        if (method != null) return method;
      }
      return const JinjaUndefined();
    } else if (obj is JinjaList || obj is JinjaTuple) {
      // Corrected: use prop.asInt directly for indexing
      if (prop.isNumeric) {
        final items = obj is JinjaList
            ? obj.items
            : (obj as JinjaTuple).items;
        int idx = prop.asInt;
        if (idx < 0) idx += items.length;
        if (idx >= 0 && idx < items.length) {
          return items[idx];
        }
      }
      return const JinjaUndefined();
    }
    // String index?
    if (obj is JinjaStringValue) {
      if (prop is JinjaInteger) {
        int idx = prop.value;
        String s = obj.value.toString();
        if (idx < 0) idx += s.length;
        if (idx >= 0 && idx < s.length) {
          return JinjaStringValue.fromString(s[idx]);
        }
      }
    }
    // For other types, bracket access behaves like dot access for methods if prop is string
    if (prop is JinjaStringValue) {
      final method = resolveMember(obj, prop.toString());
      if (method != null) return method;
    }
    return const JinjaUndefined();
  } else {
    // Dot notation: obj.prop
    // prop IS Identifier
    if (property is! Identifier) {
      throw Exception('Member property must be identifier for dot access');
    }
    final propName = (property as Identifier).name;
    final obj = object.execute(ctx);

    // Priority: 1. Attributes/Methods, 2. Map keys, 3. Special cases (length)
    final method = resolveMember(obj, propName);
    if (method != null) return method;

    if (obj is JinjaMap) {
      final key = JinjaStringValue.fromString(propName);
      if (obj.asJinjaMap.containsKey(key)) {
        return obj.asJinjaMap[key]!;
      }
    }

    // 3. Fallback: length for sequences (if not a method)
    if (propName == 'length') {
      if (obj is JinjaList) return JinjaInteger(obj.items.length);
      if (obj is JinjaTuple) return JinjaInteger(obj.items.length);
      if (obj is JinjaStringValue) return JinjaInteger(obj.value.length);
    }

    // Special Case: llama.cpp tests expect 'env' to NOT have methods
    if (obj is JinjaMap && obj.name == 'env') {
      return const JinjaUndefined();
    }

    return const JinjaUndefined();
  }
}