// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-FileCopyrightText: © 2000 Keith Packard // SPDX-License-Identifier: MIT AND HPND-sell-variant //! An expression from a configuration file, `FcExpr`, and its evaluation, //! `FcConfigEvaluate`. const std = @import("std"); const Allocator = std.mem.Allocator; const object = @import("object.zig"); const value = @import("value.zig"); const CharSet = @import("CharSet.zig"); const LangSet = @import("LangSet.zig"); const Pattern = @import("Pattern.zig"); const Object = object.Object; const Value = value.Value; const Expr = @This(); data: Data, /// Which pattern a ``, an `` or a `` refers to. pub const MatchKind = enum { /// The query, before matching. pattern, /// A font, while a match is being prepared. font, /// A font, as it is scanned. scan, }; pub const Op = enum { quest, @"or", @"and", eq, not_eq, contains, not_contains, less, less_eq, more, more_eq, plus, minus, times, divide, not, comma, floor, ceil, round, trunc, pub fn compare(op: Op) ?value.Compare { return switch (op) { .eq => .eq, .not_eq => .not_eq, .contains => .contains, .not_contains => .not_contains, .less => .less, .less_eq => .less_eq, .more => .more, .more_eq => .more_eq, else => null, }; } }; pub const Data = union(enum) { integer: i32, double: f64, string: []const u8, matrix: [4]*const Expr, range: value.Range, bool: value.Bool, charset: *const CharSet, langset: *const LangSet, nil, /// ``: the first value of an object, from the pattern being /// edited or, with `target="pattern"` in a font match, from the query. field: struct { object: Object, kind: ?MatchKind }, /// ``: a named constant, looked up for the object being tested /// or edited. constant: []const u8, op: struct { op: Op, left: *const Expr, right: ?*const Expr }, }; /// What an expression is evaluated against. pub const Context = struct { /// The pattern being edited. p: *const Pattern, /// The query, while a font is being prepared for it. p_pat: ?*const Pattern, /// The object of the test or edit the expression belongs to, which is /// what a `` is looked up for. object: Object, kind: MatchKind, /// Where any value the evaluation creates is allocated: the edited /// pattern's arena, so that it lives as long as the pattern. arena: Allocator, }; fn floorInt(d: f64) i32 { // fontconfig's FcDoubleFloor and friends truncate through an int. return lossyInt(@floor(d)); } fn lossyInt(d: f64) i32 { if (std.math.isNan(d)) return 0; return std.math.lossyCast(i32, d); } /// `FcConfigEvaluate`. pub fn evaluate(e: *const Expr, ctx: Context) Allocator.Error!Value { switch (e.data) { .integer => |i| return .{ .integer = i }, .double => |d| return .{ .double = d }, .string => |s| return .{ .string = s }, .matrix => |m| { const mtype: Value = .{ .matrix = .identity }; var d: [4]f64 = undefined; for (m, &d) |sub, *out| { const v = value.promote(try sub.evaluate(ctx), mtype, null); if (v != .double) return .void; out.* = v.double; } return .{ .matrix = .{ .xx = d[0], .xy = d[1], .yx = d[2], .yy = d[3] } }; }, .charset => |c| return .{ .charset = c }, .langset => |l| return .{ .langset = l }, .range => |r| return .{ .range = r }, .bool => |b| return .{ .bool = b }, .nil => return .void, .field => |f| { const want = f.kind; if (ctx.kind == .font and want == .pattern) { const pat = ctx.p_pat orelse return .void; return pat.get(f.object, 0) orelse .void; } else if (ctx.kind == .pattern and want == .font) { // fontconfig warns: a in a pattern // match has nothing to refer to. return .void; } return ctx.p.get(f.object, 0) orelse .void; }, .constant => |name| { if (object.constantFor(name, ctx.object)) |c| return .{ .integer = c.value }; return .void; }, .op => |o| return evaluateOp(o.op, o.left, o.right, ctx), } } fn evaluateOp(op: Op, left: *const Expr, right: ?*const Expr, ctx: Context) Allocator.Error!Value { switch (op) { .quest => { const cond = try left.evaluate(ctx); if (cond != .bool) return .void; // The branches are the two sides of the right operand, whatever // operator joins them. const r = right orelse return .void; const branches = switch (r.data) { .op => |b| b, else => return .void, }; if (cond.bool != .false) return branches.left.evaluate(ctx); const else_branch = branches.right orelse return .void; return else_branch.evaluate(ctx); }, .eq, .not_eq, .less, .less_eq, .more, .more_eq, .contains, .not_contains => { const vl = try left.evaluate(ctx); const vr = if (right) |r| try r.evaluate(ctx) else Value.void; return .{ .bool = .from(value.compare(vl, op.compare().?, vr, false)) }; }, .@"or", .@"and", .plus, .minus, .times, .divide => { const vl = try left.evaluate(ctx); const vr = if (right) |r| try r.evaluate(ctx) else Value.void; var buf1: value.PromotionBuffer = .{}; var buf2: value.PromotionBuffer = .{}; const a = value.promote(vl, vr, &buf1); const b = value.promote(vr, a, &buf2); if (a.typ() != b.typ()) return .void; switch (a) { .double => |x| { const y = b.double; const d: f64 = switch (op) { .plus => x + y, .minus => x - y, .times => x * y, .divide => x / y, else => return .void, }; // A whole result becomes an integer again. if (std.math.isFinite(d) and d == @trunc(d) and d >= std.math.minInt(i32) and d <= std.math.maxInt(i32)) return .{ .integer = @intFromFloat(d) }; return .{ .double = d }; }, .bool => |x| { const y = b.bool; return switch (op) { .@"or" => .{ .bool = .from(x != .false or y != .false) }, .@"and" => .{ .bool = .from(x != .false and y != .false) }, else => .void, }; }, .string => |x| { if (op != .plus) return .void; return .{ .string = try std.mem.concat(ctx.arena, u8, &.{ x, b.string }) }; }, .matrix => |x| { if (op != .times) return .void; return .{ .matrix = x.multiply(b.matrix) }; }, .charset => |x| { const p = try ctx.arena.create(CharSet); p.* = switch (op) { .plus => try CharSet.unionWith(ctx.arena, x.*, b.charset.*), .minus => try CharSet.subtract(ctx.arena, x.*, b.charset.*), else => return .void, }; return .{ .charset = p }; }, .langset => |x| { const p = try ctx.arena.create(LangSet); p.* = switch (op) { .plus => try LangSet.unionWith(ctx.arena, x, b.langset), .minus => try LangSet.subtract(ctx.arena, x, b.langset), else => return .void, }; return .{ .langset = p }; }, else => return .void, } }, .not => { const v = try left.evaluate(ctx); if (v != .bool) return .void; return .{ .bool = .from(v.bool == .false) }; }, .floor, .ceil, .round, .trunc => { const v = try left.evaluate(ctx); return switch (v) { .integer => v, .double => |d| .{ .integer = switch (op) { .floor => floorInt(d), .ceil => lossyInt(@ceil(d)), .round => floorInt(d + 0.5), .trunc => lossyInt(@trunc(d)), else => unreachable, } }, else => .void, }; }, // A comma is only ever taken apart by whoever holds the list. .comma => return .void, } } const testing = std.testing; test "arithmetic and promotion" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); var p: Pattern = .init(testing.allocator); defer p.deinit(); try p.addDouble(.pixelsize, 10.5); const two: Expr = .{ .data = .{ .integer = 2 } }; const field: Expr = .{ .data = .{ .field = .{ .object = .{ .builtin = .pixelsize }, .kind = null } } }; const times: Expr = .{ .data = .{ .op = .{ .op = .times, .left = &field, .right = &two } } }; const ctx: Context = .{ .p = &p, .p_pat = null, .object = .{ .builtin = .pixelsize }, .kind = .pattern, .arena = arena.allocator() }; try testing.expectEqual(Value{ .integer = 21 }, try times.evaluate(ctx)); const half: Expr = .{ .data = .{ .double = 0.25 } }; const plus: Expr = .{ .data = .{ .op = .{ .op = .plus, .left = &field, .right = &half } } }; try testing.expectEqual(Value{ .double = 10.75 }, try plus.evaluate(ctx)); const round: Expr = .{ .data = .{ .op = .{ .op = .round, .left = &plus, .right = null } } }; try testing.expectEqual(Value{ .integer = 11 }, try round.evaluate(ctx)); const bold: Expr = .{ .data = .{ .constant = "bold" } }; const wctx: Context = .{ .p = &p, .p_pat = null, .object = .{ .builtin = .weight }, .kind = .pattern, .arena = arena.allocator() }; try testing.expectEqual(Value{ .integer = 200 }, try bold.evaluate(wctx)); try testing.expectEqual(Value.void, try bold.evaluate(ctx)); } test "if" { var p: Pattern = .init(testing.allocator); defer p.deinit(); const t: Expr = .{ .data = .{ .bool = .true } }; const a: Expr = .{ .data = .{ .integer = 1 } }; const b: Expr = .{ .data = .{ .integer = 2 } }; const branches: Expr = .{ .data = .{ .op = .{ .op = .quest, .left = &a, .right = &b } } }; const quest: Expr = .{ .data = .{ .op = .{ .op = .quest, .left = &t, .right = &branches } } }; const ctx: Context = .{ .p = &p, .p_pat = null, .object = .{ .builtin = .size }, .kind = .pattern, .arena = p.allocator() }; try testing.expectEqual(Value{ .integer = 1 }, try quest.evaluate(ctx)); }