Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/Lexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ enum TokenType: String {

case callOperator = "CallOperator"
case additiveBinaryOperator = "AdditiveBinaryOperator"
case concatBinaryOperator = "ConcatBinaryOperator"
case multiplicativeBinaryOperator = "MultiplicativeBinaryOperator"
case comparisonBinaryOperator = "ComparisonBinaryOperator"
case unaryOperator = "UnaryOperator"
Expand Down Expand Up @@ -115,6 +116,7 @@ let orderedMappingTable: [(String, TokenType)] = [
(">", .comparisonBinaryOperator),
("+", .additiveBinaryOperator),
("-", .additiveBinaryOperator),
("~", .concatBinaryOperator),
("*", .multiplicativeBinaryOperator),
("/", .multiplicativeBinaryOperator),
("%", .multiplicativeBinaryOperator),
Expand Down
15 changes: 13 additions & 2 deletions Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ func parse(tokens: [Token]) throws -> Program {
return left
}

func parseAdditiveExpression() throws -> Expression {
func parseConcatExpression() throws -> Expression {
var left = try parseMultiplicativeExpression()
while typeof(.additiveBinaryOperator) {
while typeof(.concatBinaryOperator) {
let operation = tokens[current]
current += 1
let right = try parseMultiplicativeExpression()
Expand All @@ -296,6 +296,17 @@ func parse(tokens: [Token]) throws -> Program {
return left
}

func parseAdditiveExpression() throws -> Expression {
var left = try parseConcatExpression()
while typeof(.additiveBinaryOperator) {
let operation = tokens[current]
current += 1
let right = try parseConcatExpression()
left = BinaryExpression(operation: operation, left: left, right: right)
}
return left
}

func parseComparisonExpression() throws -> Expression {
var left = try parseAdditiveExpression()
while typeof(.comparisonBinaryOperator) || typeof(.in) || typeof(.notIn)
Expand Down
67 changes: 67 additions & 0 deletions Sources/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,29 @@ struct Interpreter {
return StringValue(value: "")
}

// Tilde operator with undefined/null (converts to empty string)
if node.operation.value == "~" {
let leftValue: String
if let leftString = left as? StringValue {
leftValue = leftString.value
} else if left is UndefinedValue {
leftValue = ""
} else {
leftValue = try stringify(left, whitespaceControl: true)
}

let rightValue: String
if let rightString = right as? StringValue {
rightValue = rightString.value
} else if right is UndefinedValue {
rightValue = ""
} else {
rightValue = try stringify(right, whitespaceControl: true)
}

return StringValue(value: leftValue + rightValue)
}

// Math operations with undefined/null
if ["-", "*", "/", "%"].contains(node.operation.value) {
return NumericValue(value: 0)
Expand Down Expand Up @@ -771,6 +794,16 @@ struct Interpreter {
throw JinjaError.runtime("Unsupported right operand type for string concatenation")
}
return StringValue(value: left.value + rightValue)
case "~":
let rightValue: String
if let rightString = right as? StringValue {
rightValue = rightString.value
} else if right is UndefinedValue {
rightValue = ""
} else {
rightValue = try stringify(right, whitespaceControl: true)
}
return StringValue(value: left.value + rightValue)
case "in":
if let right = right as? StringValue {
return BooleanValue(value: right.value.contains(left.value))
Expand Down Expand Up @@ -821,6 +854,16 @@ struct Interpreter {
} else {
throw JinjaError.runtime("Unsupported left operand type for string concatenation")
}
} else if node.operation.value == "~" {
let leftValue: String
if let leftString = left as? StringValue {
leftValue = leftString.value
} else if left is UndefinedValue {
leftValue = ""
} else {
leftValue = try stringify(left, whitespaceControl: true)
}
return StringValue(value: leftValue + right.value)
}
}
if let left = left as? StringValue, let right = right as? ObjectValue {
Expand All @@ -835,6 +878,30 @@ struct Interpreter {
)
}
}

// Handle tilde operator for any type combination
if node.operation.value == "~" {
let leftValue: String
if let leftString = left as? StringValue {
leftValue = leftString.value
} else if left is UndefinedValue {
leftValue = ""
} else {
leftValue = try stringify(left, whitespaceControl: true)
}

let rightValue: String
if let rightString = right as? StringValue {
rightValue = rightString.value
} else if right is UndefinedValue {
rightValue = ""
} else {
rightValue = try stringify(right, whitespaceControl: true)
}

return StringValue(value: leftValue + rightValue)
}

throw JinjaError.syntax(
"Unknown operator '\(node.operation.value)' between \(type(of:left)) and \(type(of:right))"
)
Expand Down
34 changes: 34 additions & 0 deletions Tests/LexerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ final class LexerTests: XCTestCase {
// Binary expressions
"BINOP_EXPR": "{{ 1 % 2 }}{{ 1 < 2 }}{{ 1 > 2 }}{{ 1 >= 2 }}{{ 2 <= 2 }}{{ 2 == 2 }}{{ 2 != 3 }}{{ 2 + 3 }}",

// Tilde operator tests
"TILDE_CONCAT": "{{ 'Hello' ~ ' ' ~ 'World' }}",
"TILDE_MIXED": "{{ 'Count: ' ~ 42 ~ ' items' }}",
"TILDE_BOOL": "{{ true ~ ' is ' ~ false }}",

// Strings
"STRINGS": "{{ 'Bye' }}{{ bos_token + '[INST] ' }}",
"STRINGS_1": "|{{ \"test\" }}|{{ \"a\" + 'b' + \"c\" }}|{{ '\"' + \"'\" }}|{{ '\\'' }}|{{ \"\\\"\" }}|",
Expand Down Expand Up @@ -706,6 +711,35 @@ final class LexerTests: XCTestCase {
Token(value: "}}", type: .closeExpression),
],

// Tilde operator tests
"TILDE_CONCAT": [
Token(value: "{{", type: .openExpression),
Token(value: "Hello", type: .stringLiteral),
Token(value: "~", type: .concatBinaryOperator),
Token(value: " ", type: .stringLiteral),
Token(value: "~", type: .concatBinaryOperator),
Token(value: "World", type: .stringLiteral),
Token(value: "}}", type: .closeExpression),
],
"TILDE_MIXED": [
Token(value: "{{", type: .openExpression),
Token(value: "Count: ", type: .stringLiteral),
Token(value: "~", type: .concatBinaryOperator),
Token(value: "42", type: .numericLiteral),
Token(value: "~", type: .concatBinaryOperator),
Token(value: " items", type: .stringLiteral),
Token(value: "}}", type: .closeExpression),
],
"TILDE_BOOL": [
Token(value: "{{", type: .openExpression),
Token(value: "true", type: .booleanLiteral),
Token(value: "~", type: .concatBinaryOperator),
Token(value: " is ", type: .stringLiteral),
Token(value: "~", type: .concatBinaryOperator),
Token(value: "false", type: .booleanLiteral),
Token(value: "}}", type: .closeExpression),
],

// Strings
"STRINGS": [
Token(value: "{{", type: .openExpression),
Expand Down