///|
fn expr_span(expr : @ast.Expr) -> @ast.Span {
match expr {
NoneLiteral(span) => span
BoolLiteral(_, span) => span
IntLiteral(_, span) => span
StringLiteral(_, span) => span
Identifier(_, span) => span
UnaryOp(span~, ..) => span
BinaryOp(span~, ..) => span
IfExpr(span~, ..) => span
Call(span~, ..) => span
Index(span~, ..) => span
Slice(span~, ..) => span
Dot(span~, ..) => span
ListExpr(span~, ..) => span
TupleExpr(span~, ..) => span
DictExpr(span~, ..) => span
ListComp(span~, ..) => span
DictComp(span~, ..) => span
}
}
///|
fn values_equal(a : @value.Value, b : @value.Value) -> Bool {
match (a, b) {
(None, None) => true
(Bool(x), Bool(y)) => x == y
(Int(x), Int(y)) => x == y
(String(x), String(y)) => x == y
(List(xs), List(ys)) => {
if xs.val.length() != ys.val.length() {
return false
}
for i = 0; i < xs.val.length(); i = i + 1 {
if not(values_equal(xs.val[i], ys.val[i])) {
return false
}
}
true
}
(Tuple(xs), Tuple(ys)) => {
if xs.length() != ys.length() {
return false
}
for i = 0; i < xs.length(); i = i + 1 {
if not(values_equal(xs[i], ys[i])) {
return false
}
}
true
}
(Dict(xs), Dict(ys)) => {
if xs.val.length() != ys.val.length() {
return false
}
for k, v in xs.val {
match ys.val.get(k) {
Some(v2) => if not(values_equal(v, v2)) { return false }
None => return false
}
}
true
}
_ => false
}
}
///|
/// Compare two values. Returns -1, 0, or 1. Raises TypeError for incomparable types.
fn values_compare(
a : @value.Value,
b : @value.Value,
span : @ast.Span,
) -> Int raise StarlarkError {
match (a, b) {
(Int(x), Int(y)) => if x < y { -1 } else if x > y { 1 } else { 0 }
(String(x), String(y)) => x.lexical_compare(y)
(Bool(x), Bool(y)) => {
let xi : Int64 = if x { 1L } else { 0L }
let yi : Int64 = if y { 1L } else { 0L }
if xi < yi {
-1
} else if xi > yi {
1
} else {
0
}
}
(List(xs), List(ys)) => {
let min_len = if xs.val.length() < ys.val.length() {
xs.val.length()
} else {
ys.val.length()
}
for i = 0; i < min_len; i = i + 1 {
let c = values_compare(xs.val[i], ys.val[i], span)
if c != 0 {
return c
}
}
if xs.val.length() < ys.val.length() {
-1
} else if xs.val.length() > ys.val.length() {
1
} else {
0
}
}
(Tuple(xs), Tuple(ys)) => {
let min_len = if xs.length() < ys.length() {
xs.length()
} else {
ys.length()
}
for i = 0; i < min_len; i = i + 1 {
let c = values_compare(xs[i], ys[i], span)
if c != 0 {
return c
}
}
if xs.length() < ys.length() {
-1
} else if xs.length() > ys.length() {
1
} else {
0
}
}
_ =>
raise StarlarkError::TypeError(
message="not supported between instances of '\{a.type_name()}' and '\{b.type_name()}'",
span~,
)
}
}
///|
/// Compute slice indices (start, stop, step) given optional values and length.
fn compute_slice(
start : @value.Value?,
stop : @value.Value?,
step : @value.Value?,
length : Int,
span : @ast.Span,
) -> (Int, Int, Int) raise StarlarkError {
let step_val : Int = match step {
Some(Int(n)) =>
if n == 0L {
raise StarlarkError::ValueError(
message="slice step cannot be zero",
span~,
)
} else {
n.to_int()
}
Some(None) => 1
None => 1
Some(other) =>
raise StarlarkError::TypeError(
message="slice indices must be integers or None, not '\{other.type_name()}'",
span~,
)
}
let default_start = if step_val > 0 { 0 } else { length - 1 }
let default_stop = if step_val > 0 { length } else { -1 }
let start_val : Int = match start {
Some(Int(n)) => {
let i = if n < 0L { n.to_int() + length } else { n.to_int() }
if step_val > 0 {
if i < 0 {
0
} else if i > length {
length
} else {
i
}
} else if i < -1 {
-1
} else if i >= length {
length - 1
} else {
i
}
}
Some(None) => default_start
None => default_start
Some(other) =>
raise StarlarkError::TypeError(
message="slice indices must be integers or None, not '\{other.type_name()}'",
span~,
)
}
let stop_val : Int = match stop {
Some(Int(n)) => {
let i = if n < 0L { n.to_int() + length } else { n.to_int() }
if step_val > 0 {
if i < 0 {
0
} else if i > length {
length
} else {
i
}
} else if i < -1 {
-1
} else if i >= length {
length - 1
} else {
i
}
}
Some(None) => default_stop
None => default_stop
Some(other) =>
raise StarlarkError::TypeError(
message="slice indices must be integers or None, not '\{other.type_name()}'",
span~,
)
}
(start_val, stop_val, step_val)
}
///|
/// Collect elements from a slice range.
fn slice_collect_values(
get : (Int) -> @value.Value,
start : Int,
stop : Int,
step : Int,
) -> Array[@value.Value] {
let result : Array[@value.Value] = []
if step > 0 {
for i = start; i < stop; i = i + step {
result.push(get(i))
}
} else {
for i = start; i > stop; i = i + step {
result.push(get(i))
}
}
result
}
///|
/// Check if a string contains a substring.
fn string_contains(haystack : String, needle : String) -> Bool {
if needle.length() == 0 {
return true
}
if needle.length() > haystack.length() {
return false
}
for i = 0; i <= haystack.length() - needle.length(); i = i + 1 {
let mut found = true
for j = 0; j < needle.length(); j = j + 1 {
if haystack[i + j] != needle[j] {
found = false
break
}
}
if found {
return true
}
}
false
}
///|
/// Repeat a string n times.
fn string_repeat(s : String, n : Int) -> String {
if n <= 0 {
return ""
}
let buf = StringBuilder::new()
for i = 0; i < n; i = i + 1 {
buf.write_string(s)
}
buf.to_string()
}
///|
/// Get a single character from a string as a string.
fn string_char_at(s : String, idx : Int) -> String {
let buf = StringBuilder::new()
buf.write_char(s[idx].to_int().unsafe_to_char())
buf.to_string()
}
///|
/// Slice a string using start, stop, step indices.
fn string_slice(s : String, start : Int, stop : Int, step : Int) -> String {
let buf = StringBuilder::new()
if step > 0 {
for i = start; i < stop; i = i + step {
buf.write_char(s[i].to_int().unsafe_to_char())
}
} else {
for i = start; i > stop; i = i + step {
buf.write_char(s[i].to_int().unsafe_to_char())
}
}
buf.to_string()
}
///|
pub fn eval_expr(
scope : Scope,
expr : @ast.Expr,
) -> @value.Value raise StarlarkError {
match expr {
NoneLiteral(_) => @value.Value::None
BoolLiteral(b, _) => @value.Value::Bool(b)
IntLiteral(n, _) => @value.Value::Int(n)
StringLiteral(s, _) => @value.Value::String(s)
Identifier(name, span) =>
match scope.get(name) {
Some(v) => v
None => raise StarlarkError::NameError(name~, span~)
}
UnaryOp(op~, operand~, span~) => eval_unary_op(scope, op, operand, span)
BinaryOp(left~, op~, right~, span~) =>
eval_binary_op(scope, left, op, right, span)
IfExpr(condition~, then_expr~, else_expr~, ..) => {
let cond = eval_expr(scope, condition)
if cond.is_truthy() {
eval_expr(scope, then_expr)
} else {
eval_expr(scope, else_expr)
}
}
ListExpr(elements~, ..) => {
let arr : Array[@value.Value] = []
for e in elements {
arr.push(eval_expr(scope, e))
}
@value.Value::List(Ref::new(arr))
}
TupleExpr(elements~, ..) => {
let arr : Array[@value.Value] = []
for e in elements {
arr.push(eval_expr(scope, e))
}
@value.Value::Tuple(FixedArray::from_array(arr))
}
DictExpr(entries~, ..) => {
let map : Map[String, @value.Value] = {}
for entry in entries {
let (key_expr, val_expr) = entry
let key = eval_expr(scope, key_expr)
let val = eval_expr(scope, val_expr)
match key {
String(s) => map.set(s, val)
_ =>
raise StarlarkError::TypeError(
message="dictionary keys must be strings, got '\{key.type_name()}'",
span=expr_span(key_expr),
)
}
}
@value.Value::Dict(Ref::new(map))
}
Index(object~, index~, span~) => eval_index(scope, object, index, span)
Slice(object~, start~, stop~, step~, span~) =>
eval_slice(scope, object, start, stop, step, span)
Call(func~, args~, span~) => eval_call(scope, func, args, span)
Dot(object~, field~, span~) => {
let obj = eval_expr(scope, object)
match obj {
String(s) =>
match string_methods().get(field) {
Some(m) =>
@value.Value::BuiltinFunction("str." + field, fn(pos, kw) raise {
m(s, pos, kw, span)
})
None =>
raise StarlarkError::TypeError(
message="'string' object has no attribute '\{field}'",
span~,
)
}
List(items) =>
match list_methods().get(field) {
Some(m) =>
@value.Value::BuiltinFunction("list." + field, fn(pos, kw) raise {
m(items, pos, kw, span)
})
None =>
raise StarlarkError::TypeError(
message="'list' object has no attribute '\{field}'",
span~,
)
}
Dict(map) =>
match dict_methods().get(field) {
Some(m) =>
@value.Value::BuiltinFunction("dict." + field, fn(pos, kw) raise {
m(map, pos, kw, span)
})
None =>
raise StarlarkError::TypeError(
message="'dict' object has no attribute '\{field}'",
span~,
)
}
_ =>
raise StarlarkError::TypeError(
message="'\{obj.type_name()}' object has no attribute '\{field}'",
span~,
)
}
}
ListComp(expr~, clauses~, span~) =>
eval_list_comp(scope, expr, clauses, span)
DictComp(key~, value~, clauses~, span~) =>
eval_dict_comp(scope, key, value, clauses, span)
}
}
///|
fn eval_unary_op(
scope : Scope,
op : @ast.UnaryOp,
operand : @ast.Expr,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
let val = eval_expr(scope, operand)
match op {
Negate =>
match val {
Int(n) => @value.Value::Int(-n)
_ =>
raise StarlarkError::TypeError(
message="bad operand type for unary -: '\{val.type_name()}'",
span~,
)
}
Pos =>
match val {
Int(n) => @value.Value::Int(n)
_ =>
raise StarlarkError::TypeError(
message="bad operand type for unary +: '\{val.type_name()}'",
span~,
)
}
BitNot =>
match val {
Int(n) => @value.Value::Int(-(n + 1L))
_ =>
raise StarlarkError::TypeError(
message="bad operand type for unary ~: '\{val.type_name()}'",
span~,
)
}
LogNot => @value.Value::Bool(not(val.is_truthy()))
}
}
///|
fn eval_binary_op(
scope : Scope,
left_expr : @ast.Expr,
op : @ast.BinaryOp,
right_expr : @ast.Expr,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
// Short-circuit for And/Or
match op {
And => {
let left = eval_expr(scope, left_expr)
if not(left.is_truthy()) {
return left
}
return eval_expr(scope, right_expr)
}
Or => {
let left = eval_expr(scope, left_expr)
if left.is_truthy() {
return left
}
return eval_expr(scope, right_expr)
}
_ => ()
}
let left = eval_expr(scope, left_expr)
let right = eval_expr(scope, right_expr)
match op {
Add => eval_add(left, right, span)
Sub => eval_arith(left, right, span, "-", fn(a, b) { a - b })
Mul => eval_mul(left, right, span)
Div =>
raise StarlarkError::TypeError(
message="unsupported binary operator: / (use // for integer division)",
span~,
)
FloorDiv =>
match (left, right) {
(Int(a), Int(b)) =>
if b == 0L {
raise StarlarkError::ValueError(
message="integer division by zero",
span~,
)
} else {
// Starlark floor division: rounds toward negative infinity
let q = a / b
// Adjust if signs differ and there's a remainder
if (a ^ b) < 0L && q * b != a {
@value.Value::Int(q - 1L)
} else {
@value.Value::Int(q)
}
}
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for //: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
Mod =>
match (left, right) {
(String(fmt), _) => {
let args : Array[@value.Value] = match right {
Tuple(items) => {
let arr : Array[@value.Value] = []
for i = 0; i < items.length(); i = i + 1 {
arr.push(items[i])
}
arr
}
_ => [right]
}
@value.Value::String(format_string(fmt, args, span))
}
(Int(a), Int(b)) =>
if b == 0L {
raise StarlarkError::ValueError(
message="integer modulo by zero",
span~,
)
} else {
// Starlark modulo: result has same sign as divisor
let r = a % b
if r != 0L && (r ^ b) < 0L {
@value.Value::Int(r + b)
} else {
@value.Value::Int(r)
}
}
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for %: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
// Comparison
Eq => @value.Value::Bool(values_equal(left, right))
NotEq => @value.Value::Bool(not(values_equal(left, right)))
Lt => @value.Value::Bool(values_compare(left, right, span) < 0)
Gt => @value.Value::Bool(values_compare(left, right, span) > 0)
LtEq => @value.Value::Bool(values_compare(left, right, span) <= 0)
GtEq => @value.Value::Bool(values_compare(left, right, span) >= 0)
// Membership
In => @value.Value::Bool(eval_membership(left, right, span))
NotIn => @value.Value::Bool(not(eval_membership(left, right, span)))
// Bitwise
BitAnd => eval_bitwise(left, right, span, "&", fn(a, b) { a & b })
BitOr => eval_bitwise(left, right, span, "|", fn(a, b) { a | b })
BitXor => eval_bitwise(left, right, span, "^", fn(a, b) { a ^ b })
LShift =>
eval_bitwise(left, right, span, "<<", fn(a, b) { a << b.to_int() })
RShift =>
eval_bitwise(left, right, span, ">>", fn(a, b) { a >> b.to_int() })
// And/Or handled above
And | Or => abort("unreachable: And/Or handled above")
}
}
///|
fn eval_add(
left : @value.Value,
right : @value.Value,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
match (left, right) {
(Int(a), Int(b)) => @value.Value::Int(a + b)
(String(a), String(b)) => @value.Value::String(a + b)
(List(a), List(b)) => {
let result : Array[@value.Value] = []
for item in a.val {
result.push(item)
}
for item in b.val {
result.push(item)
}
@value.Value::List(Ref::new(result))
}
(Tuple(a), Tuple(b)) => {
let result : Array[@value.Value] = []
for i = 0; i < a.length(); i = i + 1 {
result.push(a[i])
}
for i = 0; i < b.length(); i = i + 1 {
result.push(b[i])
}
@value.Value::Tuple(FixedArray::from_array(result))
}
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for +: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
}
///|
fn eval_arith(
left : @value.Value,
right : @value.Value,
span : @ast.Span,
op_str : String,
op : (Int64, Int64) -> Int64,
) -> @value.Value raise StarlarkError {
match (left, right) {
(Int(a), Int(b)) => @value.Value::Int(op(a, b))
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for \{op_str}: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
}
///|
fn eval_mul(
left : @value.Value,
right : @value.Value,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
match (left, right) {
(Int(a), Int(b)) => @value.Value::Int(a * b)
(String(s), Int(n)) => @value.Value::String(string_repeat(s, n.to_int()))
(Int(n), String(s)) => @value.Value::String(string_repeat(s, n.to_int()))
(List(items), Int(n)) => {
let result : Array[@value.Value] = []
for i = 0; i < n.to_int(); i = i + 1 {
for item in items.val {
result.push(item)
}
}
@value.Value::List(Ref::new(result))
}
(Int(n), List(items)) => {
let result : Array[@value.Value] = []
for i = 0; i < n.to_int(); i = i + 1 {
for item in items.val {
result.push(item)
}
}
@value.Value::List(Ref::new(result))
}
(Tuple(items), Int(n)) => {
let result : Array[@value.Value] = []
for i = 0; i < n.to_int(); i = i + 1 {
for j = 0; j < items.length(); j = j + 1 {
result.push(items[j])
}
}
@value.Value::Tuple(FixedArray::from_array(result))
}
(Int(n), Tuple(items)) => {
let result : Array[@value.Value] = []
for i = 0; i < n.to_int(); i = i + 1 {
for j = 0; j < items.length(); j = j + 1 {
result.push(items[j])
}
}
@value.Value::Tuple(FixedArray::from_array(result))
}
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for *: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
}
///|
fn eval_bitwise(
left : @value.Value,
right : @value.Value,
span : @ast.Span,
op_str : String,
op : (Int64, Int64) -> Int64,
) -> @value.Value raise StarlarkError {
match (left, right) {
(Int(a), Int(b)) => @value.Value::Int(op(a, b))
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for \{op_str}: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
}
///|
fn eval_membership(
element : @value.Value,
container : @value.Value,
span : @ast.Span,
) -> Bool raise StarlarkError {
match container {
List(items) => {
for item in items.val {
if values_equal(element, item) {
return true
}
}
false
}
Tuple(items) => {
for i = 0; i < items.length(); i = i + 1 {
if values_equal(element, items[i]) {
return true
}
}
false
}
Dict(map) =>
match element {
String(key) => map.val.contains(key)
_ =>
raise StarlarkError::TypeError(
message="dict membership test requires string key, got '\{element.type_name()}'",
span~,
)
}
String(haystack) =>
match element {
String(needle) => string_contains(haystack, needle)
_ =>
raise StarlarkError::TypeError(
message="'in ' requires string as left operand, not '\{element.type_name()}'",
span~,
)
}
_ =>
raise StarlarkError::TypeError(
message="argument of type '\{container.type_name()}' is not iterable",
span~,
)
}
}
///|
fn eval_index(
scope : Scope,
object_expr : @ast.Expr,
index_expr : @ast.Expr,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
let obj = eval_expr(scope, object_expr)
let idx = eval_expr(scope, index_expr)
match (obj, idx) {
(List(items), Int(n)) => {
let len = items.val.length()
let i = if n < 0L { n.to_int() + len } else { n.to_int() }
if i < 0 || i >= len {
raise StarlarkError::IndexError(
message="list index out of range",
span~,
)
}
items.val[i]
}
(Tuple(items), Int(n)) => {
let len = items.length()
let i = if n < 0L { n.to_int() + len } else { n.to_int() }
if i < 0 || i >= len {
raise StarlarkError::IndexError(
message="tuple index out of range",
span~,
)
}
items[i]
}
(String(s), Int(n)) => {
let len = s.length()
let i = if n < 0L { n.to_int() + len } else { n.to_int() }
if i < 0 || i >= len {
raise StarlarkError::IndexError(
message="string index out of range",
span~,
)
}
@value.Value::String(string_char_at(s, i))
}
(Dict(map), String(key)) =>
match map.val.get(key) {
Some(v) => v
None => raise StarlarkError::KeyError(key~, span~)
}
(Dict(_), _) =>
raise StarlarkError::TypeError(
message="dictionary key must be a string, got '\{idx.type_name()}'",
span~,
)
_ =>
raise StarlarkError::TypeError(
message="'\{obj.type_name()}' object is not subscriptable",
span~,
)
}
}
///|
fn eval_slice(
scope : Scope,
object_expr : @ast.Expr,
start_expr : @ast.Expr?,
stop_expr : @ast.Expr?,
step_expr : @ast.Expr?,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
let obj = eval_expr(scope, object_expr)
let start = match start_expr {
Some(e) => Some(eval_expr(scope, e))
None => None
}
let stop = match stop_expr {
Some(e) => Some(eval_expr(scope, e))
None => None
}
let step = match step_expr {
Some(e) => Some(eval_expr(scope, e))
None => None
}
match obj {
List(items) => {
let (s, e, st) = compute_slice(
start,
stop,
step,
items.val.length(),
span,
)
let result = slice_collect_values(fn(i) { items.val[i] }, s, e, st)
@value.Value::List(Ref::new(result))
}
Tuple(items) => {
let (s, e, st) = compute_slice(start, stop, step, items.length(), span)
let result = slice_collect_values(fn(i) { items[i] }, s, e, st)
@value.Value::Tuple(FixedArray::from_array(result))
}
String(str) => {
let (s, e, st) = compute_slice(start, stop, step, str.length(), span)
@value.Value::String(string_slice(str, s, e, st))
}
_ =>
raise StarlarkError::TypeError(
message="'\{obj.type_name()}' object is not subscriptable",
span~,
)
}
}
///|
fn eval_call_args(
scope : Scope,
args : Array[@ast.Argument],
span : @ast.Span,
) -> (Array[@value.Value], Map[String, @value.Value]) raise StarlarkError {
let pos_args : Array[@value.Value] = []
let kw_args : Map[String, @value.Value] = {}
for arg in args {
match arg {
Positional(expr) => pos_args.push(eval_expr(scope, expr))
Keyword(name, expr) => {
let value = eval_expr(scope, expr)
if kw_args.contains(name) {
raise StarlarkError::TypeError(
message="keyword argument '\{name}' repeated",
span~,
)
}
kw_args.set(name, value)
}
Star(expr) =>
// Unpack iterable into positional args
match eval_expr(scope, expr) {
List(items) =>
for item in items.val {
pos_args.push(item)
}
Tuple(items) =>
for i = 0; i < items.length(); i = i + 1 {
pos_args.push(items[i])
}
_ =>
raise StarlarkError::TypeError(
message="argument after * must be an iterable",
span~,
)
}
DoubleStar(expr) =>
match eval_expr(scope, expr) {
Dict(map) =>
for k, v in map.val {
if kw_args.contains(k) {
raise StarlarkError::TypeError(
message="keyword argument '\{k}' repeated",
span~,
)
}
kw_args.set(k, v)
}
_ =>
raise StarlarkError::TypeError(
message="argument after ** must be a dict",
span~,
)
}
}
}
(pos_args, kw_args)
}
///|
fn call_method(
obj : @value.Value,
field : String,
pos_args : Array[@value.Value],
kw_args : Map[String, @value.Value],
span : @ast.Span,
) -> @value.Value raise StarlarkError {
match obj {
String(s) =>
match string_methods().get(field) {
Some(m) => m(s, pos_args, kw_args, span)
None =>
raise StarlarkError::TypeError(
message="'string' object has no attribute '\{field}'",
span~,
)
}
List(items) =>
match list_methods().get(field) {
Some(m) => m(items, pos_args, kw_args, span)
None =>
raise StarlarkError::TypeError(
message="'list' object has no attribute '\{field}'",
span~,
)
}
Dict(map) =>
match dict_methods().get(field) {
Some(m) => m(map, pos_args, kw_args, span)
None =>
raise StarlarkError::TypeError(
message="'dict' object has no attribute '\{field}'",
span~,
)
}
_ =>
raise StarlarkError::TypeError(
message="'\{obj.type_name()}' object has no attribute '\{field}'",
span~,
)
}
}
///|
fn eval_call(
scope : Scope,
func_expr : @ast.Expr,
args : Array[@ast.Argument],
span : @ast.Span,
) -> @value.Value raise StarlarkError {
match func_expr {
Dot(object~, field~, span=dot_span) => {
// Fast path: method call — skip closure creation
let obj = eval_expr(scope, object)
let (pos_args, kw_args) = eval_call_args(scope, args, span)
call_method(obj, field, pos_args, kw_args, dot_span)
}
_ => {
// Regular call path
let func = eval_expr(scope, func_expr)
let (pos_args, kw_args) = eval_call_args(scope, args, span)
match func {
Function(fn_name, callable) =>
call_function(fn_name, callable, pos_args, kw_args, span)
BuiltinFunction(_name, builtin_fn) =>
builtin_fn(pos_args, kw_args) catch {
TypeError(_) as e => raise e
NameError(_) as e => raise e
ValueError(_) as e => raise e
IndexError(_) as e => raise e
KeyError(_) as e => raise e
e => raise StarlarkError::TypeError(message=e.to_string(), span~)
}
_ =>
raise StarlarkError::TypeError(
message="'\{func.type_name()}' object is not callable",
span~,
)
}
}
}
}
///|
fn call_function(
_name : String,
callable : (Array[@value.Value], Map[String, @value.Value]) -> @value.Value raise Error,
pos_args : Array[@value.Value],
kw_args : Map[String, @value.Value],
span : @ast.Span,
) -> @value.Value raise StarlarkError {
callable(pos_args, kw_args) catch {
TypeError(_) as e => raise e
NameError(_) as e => raise e
ValueError(_) as e => raise e
IndexError(_) as e => raise e
KeyError(_) as e => raise e
e => raise StarlarkError::TypeError(message=e.to_string(), span~)
}
}
///|
fn eval_list_comp(
scope : Scope,
expr : @ast.Expr,
clauses : Array[@ast.CompClause],
_span : @ast.Span,
) -> @value.Value raise StarlarkError {
let result : Array[@value.Value] = []
let comp_scope = Scope::new(parent=scope)
eval_comp_clauses(comp_scope, clauses, 0, () => {
result.push(eval_expr(comp_scope, expr))
})
@value.Value::List(Ref::new(result))
}
///|
fn eval_dict_comp(
scope : Scope,
key_expr : @ast.Expr,
value_expr : @ast.Expr,
clauses : Array[@ast.CompClause],
_span : @ast.Span,
) -> @value.Value raise StarlarkError {
let result : Map[String, @value.Value] = {}
let comp_scope = Scope::new(parent=scope)
eval_comp_clauses(comp_scope, clauses, 0, () => {
let key = eval_expr(comp_scope, key_expr)
let value = eval_expr(comp_scope, value_expr)
match key {
String(s) => result.set(s, value)
_ =>
raise StarlarkError::TypeError(
message="dictionary keys must be strings, got '\{key.type_name()}'",
span=expr_span(key_expr),
)
}
})
@value.Value::Dict(Ref::new(result))
}
///|
fn eval_comp_clauses(
scope : Scope,
clauses : Array[@ast.CompClause],
index : Int,
body : () -> Unit raise StarlarkError,
) -> Unit raise StarlarkError {
if index >= clauses.length() {
body()
return
}
match clauses[index] {
For(vars~, iterable~) => {
let iter_val = eval_expr(scope, iterable)
let items = iterable_to_array(iter_val, expr_span(iterable))
for item in items {
assign_target(scope, vars, item, expr_span(vars))
eval_comp_clauses(scope, clauses, index + 1, body)
}
}
If(condition) => {
let cond = eval_expr(scope, condition)
if cond.is_truthy() {
eval_comp_clauses(scope, clauses, index + 1, body)
}
}
}
}
///|
fn iterable_to_array(
value : @value.Value,
span : @ast.Span,
) -> Array[@value.Value] raise StarlarkError {
match value {
List(items) => {
let result : Array[@value.Value] = []
for item in items.val {
result.push(item)
}
result
}
Tuple(items) => {
let result : Array[@value.Value] = []
for i = 0; i < items.length(); i = i + 1 {
result.push(items[i])
}
result
}
Dict(map) => {
let result : Array[@value.Value] = []
for k, _ in map.val {
result.push(@value.Value::String(k))
}
result
}
String(s) => {
let result : Array[@value.Value] = []
for i = 0; i < s.length(); i = i + 1 {
result.push(@value.Value::String(string_char_at(s, i)))
}
result
}
_ =>
raise StarlarkError::TypeError(
message="'\{value.type_name()}' object is not iterable",
span~,
)
}
}
///|
/// Convert a value to an array of its elements, using a function name for error messages.
fn iterable_to_array_by_name(
value : @value.Value,
fname : String,
) -> Array[@value.Value] raise StarlarkError {
let span : @ast.Span = {
start: { file: "", line: 0, column: 0 },
end: { file: "", line: 0, column: 0 },
}
match value {
List(items) => {
let result : Array[@value.Value] = []
for item in items.val {
result.push(item)
}
result
}
Tuple(items) => {
let result : Array[@value.Value] = []
for i = 0; i < items.length(); i = i + 1 {
result.push(items[i])
}
result
}
Dict(map) => {
let result : Array[@value.Value] = []
for k, _ in map.val {
result.push(@value.Value::String(k))
}
result
}
String(s) => {
let result : Array[@value.Value] = []
for i = 0; i < s.length(); i = i + 1 {
result.push(@value.Value::String(string_char_at(s, i)))
}
result
}
_ =>
raise StarlarkError::TypeError(
message=fname +
"() argument must be an iterable, not '" +
value.type_name() +
"'",
span~,
)
}
}
///|
fn eval_index_assign(
obj : @value.Value,
idx : @value.Value,
value : @value.Value,
span : @ast.Span,
) -> Unit raise StarlarkError {
match (obj, idx) {
(List(items), Int(n)) => {
let len = items.val.length()
let i = if n < 0L { n.to_int() + len } else { n.to_int() }
if i < 0 || i >= len {
raise StarlarkError::IndexError(
message="list assignment index out of range",
span~,
)
}
items.val[i] = value
}
(Dict(map), String(key)) => map.val.set(key, value)
(Dict(_), _) =>
raise StarlarkError::TypeError(
message="dictionary key must be a string, got '\{idx.type_name()}'",
span~,
)
_ =>
raise StarlarkError::TypeError(
message="'\{obj.type_name()}' object does not support item assignment",
span~,
)
}
}
///|
fn assign_target(
scope : Scope,
target : @ast.Expr,
value : @value.Value,
span : @ast.Span,
) -> Unit raise StarlarkError {
match target {
Identifier(name, _) => scope.set(name, value)
TupleExpr(elements~, ..) => {
// Unpack value into the tuple elements
let items = match value {
List(items) => {
let result : Array[@value.Value] = []
for item in items.val {
result.push(item)
}
result
}
Tuple(items) => {
let result : Array[@value.Value] = []
for i = 0; i < items.length(); i = i + 1 {
result.push(items[i])
}
result
}
_ =>
raise StarlarkError::TypeError(
message="cannot unpack non-sequence '\{value.type_name()}'",
span~,
)
}
if items.length() != elements.length() {
raise StarlarkError::ValueError(
message="too many values to unpack (expected \{elements.length()}, got \{items.length()})",
span~,
)
}
for i = 0; i < elements.length(); i = i + 1 {
assign_target(scope, elements[i], items[i], span)
}
}
ListExpr(elements~, ..) => {
// Same as tuple unpacking
let items = match value {
List(items) => {
let result : Array[@value.Value] = []
for item in items.val {
result.push(item)
}
result
}
Tuple(items) => {
let result : Array[@value.Value] = []
for i = 0; i < items.length(); i = i + 1 {
result.push(items[i])
}
result
}
_ =>
raise StarlarkError::TypeError(
message="cannot unpack non-sequence '\{value.type_name()}'",
span~,
)
}
if items.length() != elements.length() {
raise StarlarkError::ValueError(
message="too many values to unpack (expected \{elements.length()}, got \{items.length()})",
span~,
)
}
for i = 0; i < elements.length(); i = i + 1 {
assign_target(scope, elements[i], items[i], span)
}
}
_ =>
raise StarlarkError::TypeError(
message="cannot assign to \{target.to_string()}",
span~,
)
}
}
///|
fn assign_op_to_binary_op(op : @ast.AssignOp) -> @ast.BinaryOp {
match op {
PlusEq => Add
MinusEq => Sub
StarEq => Mul
SlashEq => Div
FloorDivEq => FloorDiv
PercentEq => Mod
AmpEq => BitAnd
PipeEq => BitOr
CaretEq => BitXor
LShiftEq => LShift
RShiftEq => RShift
}
}
///|
pub fn eval_stmts(scope : Scope, stmts : Array[@ast.Stmt]) -> Unit raise Error {
for stmt in stmts {
eval_stmt(scope, stmt)
}
}
///|
fn eval_stmt(scope : Scope, stmt : @ast.Stmt) -> Unit raise Error {
match stmt {
Pass(_) => ()
Break(_) => raise BreakSignal::BreakSignal
Continue(_) => raise ContinueSignal::ContinueSignal
Expr(expr~, ..) => {
let _ = eval_expr(scope, expr)
}
Return(value~, ..) => {
let val = match value {
Some(expr) => eval_expr(scope, expr)
None => @value.Value::None
}
raise ReturnSignal::ReturnSignal(val)
}
Assign(target~, op~, value~, span~) =>
eval_assign(scope, target, op, value, span)
If(clauses~, else_body~, ..) => eval_if(scope, clauses, else_body)
For(vars~, iterable~, body~, span~) =>
eval_for(scope, vars, iterable, body, span)
Def(name~, params~, body~, span~) =>
eval_def(scope, name, params, body, span)
Load(mod=mod_name, bindings~, span~) =>
eval_load(scope, mod_name, bindings, span)
}
}
///|
fn eval_assign(
scope : Scope,
target : @ast.Expr,
op : @ast.AssignOp?,
value_expr : @ast.Expr,
span : @ast.Span,
) -> Unit raise StarlarkError {
let value = eval_expr(scope, value_expr)
match op {
None =>
// Plain assignment
match target {
Identifier(name, _) => scope.set(name, value)
TupleExpr(..) | ListExpr(..) =>
assign_target(scope, target, value, span)
Index(object~, index~, ..) => {
let idx_span = expr_span(target)
let obj = eval_expr(scope, object)
let idx = eval_expr(scope, index)
eval_index_assign(obj, idx, value, idx_span)
}
Dot(..) =>
raise StarlarkError::TypeError(
message="attribute assignment not supported",
span~,
)
_ =>
raise StarlarkError::TypeError(
message="cannot assign to expression",
span~,
)
}
Some(assign_op) =>
// Augmented assignment
match target {
Identifier(name, id_span) => {
let current = match scope.get(name) {
Some(v) => v
None => raise StarlarkError::NameError(name~, span=id_span)
}
// Special case: list += iterable extends the list in place
match (assign_op, current) {
(PlusEq, List(items)) => {
let new_items = iterable_to_array(value, span)
for item in new_items {
items.val.push(item)
}
}
_ => {
let bin_op = assign_op_to_binary_op(assign_op)
let result = eval_binary_op_values(current, bin_op, value, span)
scope.set(name, result)
}
}
}
Index(object~, index~, ..) => {
let idx_span = expr_span(target)
let obj = eval_expr(scope, object)
let idx = eval_expr(scope, index)
match (obj, idx) {
(List(items), Int(n)) => {
let len = items.val.length()
let i = if n < 0L { n.to_int() + len } else { n.to_int() }
if i < 0 || i >= len {
raise StarlarkError::IndexError(
message="list assignment index out of range",
span=idx_span,
)
}
let bin_op = assign_op_to_binary_op(assign_op)
let current = items.val[i]
let result = eval_binary_op_values(current, bin_op, value, span)
items.val[i] = result
}
(Dict(map), String(key)) => {
let current = match map.val.get(key) {
Some(v) => v
None => raise StarlarkError::KeyError(key~, span=idx_span)
}
let bin_op = assign_op_to_binary_op(assign_op)
let result = eval_binary_op_values(current, bin_op, value, span)
map.val.set(key, result)
}
_ =>
raise StarlarkError::TypeError(
message="'\{obj.type_name()}' object does not support item assignment",
span=idx_span,
)
}
}
_ =>
raise StarlarkError::TypeError(
message="cannot use augmented assignment with this target",
span~,
)
}
}
}
///|
fn eval_binary_op_values(
left : @value.Value,
op : @ast.BinaryOp,
right : @value.Value,
span : @ast.Span,
) -> @value.Value raise StarlarkError {
match op {
Add => eval_add(left, right, span)
Sub => eval_arith(left, right, span, "-", fn(a, b) { a - b })
Mul => eval_mul(left, right, span)
Div =>
raise StarlarkError::TypeError(
message="unsupported binary operator: / (use // for integer division)",
span~,
)
FloorDiv =>
match (left, right) {
(Int(a), Int(b)) =>
if b == 0L {
raise StarlarkError::ValueError(
message="integer division by zero",
span~,
)
} else {
let q = a / b
if (a ^ b) < 0L && q * b != a {
@value.Value::Int(q - 1L)
} else {
@value.Value::Int(q)
}
}
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for //: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
Mod =>
match (left, right) {
(String(fmt), _) => {
let args : Array[@value.Value] = match right {
Tuple(items) => {
let arr : Array[@value.Value] = []
for i = 0; i < items.length(); i = i + 1 {
arr.push(items[i])
}
arr
}
_ => [right]
}
@value.Value::String(format_string(fmt, args, span))
}
(Int(a), Int(b)) =>
if b == 0L {
raise StarlarkError::ValueError(
message="integer modulo by zero",
span~,
)
} else {
let r = a % b
if r != 0L && (r ^ b) < 0L {
@value.Value::Int(r + b)
} else {
@value.Value::Int(r)
}
}
_ =>
raise StarlarkError::TypeError(
message="unsupported operand type(s) for %: '\{left.type_name()}' and '\{right.type_name()}'",
span~,
)
}
Eq => @value.Value::Bool(values_equal(left, right))
NotEq => @value.Value::Bool(not(values_equal(left, right)))
Lt => @value.Value::Bool(values_compare(left, right, span) < 0)
Gt => @value.Value::Bool(values_compare(left, right, span) > 0)
LtEq => @value.Value::Bool(values_compare(left, right, span) <= 0)
GtEq => @value.Value::Bool(values_compare(left, right, span) >= 0)
In => @value.Value::Bool(eval_membership(left, right, span))
NotIn => @value.Value::Bool(not(eval_membership(left, right, span)))
BitAnd => eval_bitwise(left, right, span, "&", fn(a, b) { a & b })
BitOr => eval_bitwise(left, right, span, "|", fn(a, b) { a | b })
BitXor => eval_bitwise(left, right, span, "^", fn(a, b) { a ^ b })
LShift =>
eval_bitwise(left, right, span, "<<", fn(a, b) { a << b.to_int() })
RShift =>
eval_bitwise(left, right, span, ">>", fn(a, b) { a >> b.to_int() })
And => if not(left.is_truthy()) { left } else { right }
Or => if left.is_truthy() { left } else { right }
}
}
///|
fn eval_if(
scope : Scope,
clauses : Array[(@ast.Expr, Array[@ast.Stmt])],
else_body : Array[@ast.Stmt]?,
) -> Unit raise Error {
for clause in clauses {
let (condition, body) = clause
let cond = eval_expr(scope, condition)
if cond.is_truthy() {
eval_stmts(scope, body)
return
}
}
match else_body {
Some(body) => eval_stmts(scope, body)
None => ()
}
}
///|
fn eval_for(
scope : Scope,
vars : @ast.Expr,
iterable_expr : @ast.Expr,
body : Array[@ast.Stmt],
span : @ast.Span,
) -> Unit raise Error {
let iter_val = eval_expr(scope, iterable_expr)
let items = iterable_to_array(iter_val, span)
for item in items {
assign_target(scope, vars, item, span)
eval_stmts(scope, body) catch {
ContinueSignal::ContinueSignal => continue
BreakSignal::BreakSignal => break
other => raise other
}
}
}
///|
fn eval_def(
scope : Scope,
name : String,
params : Array[@ast.Parameter],
body : Array[@ast.Stmt],
span : @ast.Span,
) -> Unit raise StarlarkError {
// Evaluate default parameter values at definition time
let defaults : Array[@value.Value?] = []
for param in params {
match param.default {
Some(expr) => defaults.push(Some(eval_expr(scope, expr)))
None => defaults.push(None)
}
}
// Capture the current scope (closure)
let closure_scope = scope
let param_names : Array[String] = []
for param in params {
param_names.push(param.name)
}
let callable : (Array[@value.Value], Map[String, @value.Value]) -> @value.Value raise Error = fn(
pos_args,
kw_args,
) raise {
// Create a new child scope of the closure scope
let call_scope = Scope::new(parent=closure_scope)
// Bind arguments to parameter names
for i = 0; i < param_names.length(); i = i + 1 {
let param_name = param_names[i]
if i < pos_args.length() {
// Check if also given as keyword
if kw_args.contains(param_name) {
raise StarlarkError::TypeError(
message="\{name}() got multiple values for argument '\{param_name}'",
span~,
)
}
call_scope.set(param_name, pos_args[i])
} else {
// Check keyword args
match kw_args.get(param_name) {
Some(v) => call_scope.set(param_name, v)
None =>
// Use default if available
match defaults[i] {
Some(default_val) => call_scope.set(param_name, default_val)
None =>
raise StarlarkError::TypeError(
message="\{name}() missing required argument: '\{param_name}'",
span~,
)
}
}
}
}
// Check for too many positional args
if pos_args.length() > param_names.length() {
raise StarlarkError::TypeError(
message="\{name}() takes \{param_names.length()} positional argument(s) but \{pos_args.length()} were given",
span~,
)
}
// Check for unknown keyword args
for k, _ in kw_args {
let mut found = false
for pname in param_names {
if pname == k {
found = true
break
}
}
if not(found) {
raise StarlarkError::TypeError(
message="\{name}() got an unexpected keyword argument '\{k}'",
span~,
)
}
}
// Execute the body
eval_stmts(call_scope, body) catch {
ReturnSignal::ReturnSignal(val) => return val
other => raise other
}
@value.Value::None
}
scope.set(name, @value.Value::Function(name, callable))
}
///|
fn eval_load(
scope : Scope,
mod_name : String,
bindings : Array[(String, String)],
span : @ast.Span,
) -> Unit raise Error {
// 1. Get the environment
let env = match scope.get_env() {
Some(e) => e
None =>
raise StarlarkError::TypeError(
message="no module loader configured",
span~,
)
}
// 2. Check module cache
let exports = match env.module_cache.get(mod_name) {
Some(cached) => cached
None => {
// 3. Get the loader
let loader = match env.loader {
Some(l) => l
None =>
raise StarlarkError::TypeError(
message="no module loader configured",
span~,
)
}
// 4. Call loader to get source
let source = loader(mod_name)
// 5. Lex, parse, and evaluate the module in a fresh scope
let mod_scope = Scope::new(env~)
for name, val in env.predeclared {
mod_scope.set(name, val)
}
let tokens = @lexer.tokenize(source)
let stmts = @parser.parse(tokens)
eval_stmts(mod_scope, stmts)
// 6. Extract exported globals (names not starting with '_')
let exported : Map[String, @value.Value] = {}
for name, val in mod_scope.bindings {
if not(env.predeclared.contains(name)) {
exported.set(name, val)
}
}
// 7. Cache the exports
env.module_cache.set(mod_name, exported)
exported
}
}
// 8. Bind each requested name into the current scope
for binding in bindings {
let (local_name, remote_name) = binding
// Check for private names
if remote_name.length() > 0 && remote_name[0] == '_' {
raise StarlarkError::TypeError(
message="cannot load private name '\{remote_name}' from '\{mod_name}'",
span~,
)
}
match exports.get(remote_name) {
Some(val) => scope.set(local_name, val)
None =>
raise StarlarkError::TypeError(
message="module '\{mod_name}' has no symbol '\{remote_name}'",
span~,
)
}
}
}
///|
pub fn exec_module(scope : Scope, stmts : Array[@ast.Stmt]) -> Unit raise Error {
eval_stmts(scope, stmts)
}
///|
fn format_string(
fmt : String,
args : Array[@value.Value],
span : @ast.Span,
) -> String raise StarlarkError {
let buf = StringBuilder::new()
let mut arg_index = 0
let len = fmt.length()
let mut i = 0
while i < len {
let code = fmt[i].to_int()
if code == '%'.to_int() {
i = i + 1
if i >= len {
raise StarlarkError::ValueError(message="incomplete format", span~)
}
let verb = fmt[i].to_int()
if verb == '%'.to_int() {
buf.write_char('%')
} else if verb == 's'.to_int() {
if arg_index >= args.length() {
raise StarlarkError::ValueError(
message="not enough arguments for format string",
span~,
)
}
buf.write_string(value_to_str(args[arg_index]))
arg_index = arg_index + 1
} else if verb == 'r'.to_int() {
if arg_index >= args.length() {
raise StarlarkError::ValueError(
message="not enough arguments for format string",
span~,
)
}
buf.write_string(value_to_repr(args[arg_index]))
arg_index = arg_index + 1
} else if verb == 'd'.to_int() {
if arg_index >= args.length() {
raise StarlarkError::ValueError(
message="not enough arguments for format string",
span~,
)
}
match args[arg_index] {
Int(n) => buf.write_string(n.to_string())
_ =>
raise StarlarkError::TypeError(
message="%d format: a number is required, not \{args[arg_index].type_name()}",
span~,
)
}
arg_index = arg_index + 1
} else {
raise StarlarkError::ValueError(
message="unsupported format character '\{verb.unsafe_to_char()}'",
span~,
)
}
} else {
buf.write_char(code.unsafe_to_char())
}
i = i + 1
}
if arg_index != args.length() {
raise StarlarkError::ValueError(
message="not all arguments converted during string formatting",
span~,
)
}
buf.to_string()
}