Skip to content

Commit

Permalink
Add functionality from TypeScript implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
DePasqualeOrg committed Jan 1, 2025
1 parent 9d79438 commit 20d6621
Show file tree
Hide file tree
Showing 9 changed files with 2,007 additions and 381 deletions.
30 changes: 25 additions & 5 deletions Sources/Ast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ struct TupleLiteral: Literal {
}

struct ObjectLiteral: Literal {
var value: [(Expression, Expression)]
var value: [String: Expression]
}

struct Set: Statement {
var assignee: Expression
var value: Expression
}

struct If: Statement {
struct If: Statement, Expression {
var test: Expression
var body: [Statement]
var alternate: [Statement]
Expand All @@ -59,14 +59,14 @@ struct Identifier: Expression {
var value: String
}

protocol Loopvar {}
extension Identifier: Loopvar {}
extension TupleLiteral: Loopvar {}
typealias Loopvar = Expression

struct For: Statement {
var loopvar: Loopvar
var iterable: Expression
var body: [Statement]
var defaultBlock: [Statement]
var ifCondition: Expression?
}

struct MemberExpression: Expression {
Expand Down Expand Up @@ -124,3 +124,23 @@ struct KeywordArgumentExpression: Expression {
struct NullLiteral: Literal {
var value: Any? = nil
}

struct SelectExpression: Expression {
var iterable: Expression
var test: Expression
}

struct Macro: Statement {
var name: Identifier
var args: [Expression]
var body: [Statement]
}

struct KeywordArgumentsValue: RuntimeValue {
var value: [String: any RuntimeValue]
var builtins: [String: any RuntimeValue] = [:]

func bool() -> Bool {
!value.isEmpty
}
}
204 changes: 138 additions & 66 deletions Sources/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,39 @@ class Environment {

var variables: [String: any RuntimeValue] = [
"namespace": FunctionValue(value: { args, _ in
if args.count == 0 {
if args.isEmpty {
return ObjectValue(value: [:])
}

if args.count != 1 || !(args[0] is ObjectValue) {
guard args.count == 1, let objectArg = args[0] as? ObjectValue else {
throw JinjaError.runtime("`namespace` expects either zero arguments or a single object argument")
}

return args[0]
return objectArg
})
]

var tests: [String: (any RuntimeValue...) throws -> Bool] = [
"boolean": {
args in
args[0] is BooleanValue
"boolean": { args in
return args[0] is BooleanValue
},

"callable": {
args in
args[0] is FunctionValue
"callable": { args in
return args[0] is FunctionValue
},

"odd": {
args in
if let arg = args.first as? NumericValue {
return arg.value as! Int % 2 != 0
"odd": { args in
if let arg = args.first as? NumericValue, let intValue = arg.value as? Int {
return intValue % 2 != 0
} else {
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of:args.first))")
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of: args.first))")
}
},
"even": { args in
if let arg = args.first as? NumericValue {
return arg.value as! Int % 2 == 0
if let arg = args.first as? NumericValue, let intValue = arg.value as? Int {
return intValue % 2 == 0
} else {
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of:args.first))")
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of: args.first))")
}
},
"false": { args in
Expand All @@ -62,24 +59,28 @@ class Environment {
}
return false
},
"string": { args in
return args[0] is StringValue
},
"number": { args in
args[0] is NumericValue
return args[0] is NumericValue
},
"integer": { args in
if let arg = args[0] as? NumericValue {
return arg.value is Int
}

return false
},
"mapping": { args in
return args[0] is ObjectValue
},
"iterable": { args in
args[0] is ArrayValue || args[0] is StringValue
return args[0] is ArrayValue || args[0] is StringValue || args[0] is ObjectValue
},
"lower": { args in
if let arg = args[0] as? StringValue {
return arg.value == arg.value.lowercased()
}

return false
},
"upper": { args in
Expand All @@ -89,16 +90,47 @@ class Environment {
return false
},
"none": { args in
args[0] is NullValue
return args[0] is NullValue
},
"defined": { args in
!(args[0] is UndefinedValue)
return !(args[0] is UndefinedValue)
},
"undefined": { args in
args[0] is UndefinedValue
return args[0] is UndefinedValue
},
"equalto": { _ in
throw JinjaError.syntaxNotSupported("equalto")
"equalto": { args in
if args.count == 2 {
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
return left.value == right.value
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
let leftInt = left.value as? Int, let rightInt = right.value as? Int
{
return leftInt == rightInt
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
return left.value == right.value
} else {
return false
}
} else {
return false
}
},
"eq": { args in
if args.count == 2 {
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
return left.value == right.value
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
let leftInt = left.value as? Int, let rightInt = right.value as? Int
{
return leftInt == rightInt
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
return left.value == right.value
} else {
return false
}
} else {
return false
}
},
]

Expand All @@ -107,66 +139,111 @@ class Environment {
}

func isFunction<T>(_ value: Any, functionType: T.Type) -> Bool {
value is T
return value is T
}

func convertToRuntimeValues(input: Any) throws -> any RuntimeValue {
func convertToRuntimeValues(input: Any?) throws -> any RuntimeValue {
if input == nil {
return NullValue()
}
switch input {
case let value as Bool:
return BooleanValue(value: value)
case let values as [any Numeric]:
var items: [any RuntimeValue] = []
for value in values {
try items.append(self.convertToRuntimeValues(input: value))
}
return ArrayValue(value: items)
case let value as any Numeric:
return NumericValue(value: value)
case let value as String:
return StringValue(value: value)
case let data as Data:
guard let string = String(data: data, encoding: .utf8) else {
throw JinjaError.runtime("Failed to convert data to string")
}
return StringValue(value: string)
case let fn as (String) throws -> Void:
return FunctionValue { args, _ in
var arg = ""
switch args[0].value {
case let value as String:
arg = value
case let value as Bool:
arg = String(value)
default:
throw JinjaError.runtime("Unknown arg type:\(type(of: args[0].value))")
guard let stringArg = args[0] as? StringValue else {
throw JinjaError.runtime("Argument must be a StringValue")
}

try fn(arg)
try fn(stringArg.value)
return NullValue()
}
case let fn as (Bool) throws -> Void:
return FunctionValue { args, _ in
try fn(args[0].value as! Bool)
guard let boolArg = args[0] as? BooleanValue else {
throw JinjaError.runtime("Argument must be a BooleanValue")
}
try fn(boolArg.value)
return NullValue()
}
case let fn as (Int, Int?, Int) -> [Int]:
return FunctionValue { args, _ in
let result = fn(args[0].value as! Int, args[1].value as? Int, args[2].value as! Int)
guard args.count > 0, let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
throw JinjaError.runtime("First argument must be an Int")
}
var int1: Int? = nil
if args.count > 1 {
if let numericValue = args[1] as? NumericValue, let tempInt1 = numericValue.value as? Int {
int1 = tempInt1
} else {
throw JinjaError.runtime("Second argument must be an Int or nil")
}
}
var int2: Int = 1
if args.count > 2 {
if let numericValue = args[2] as? NumericValue, let tempInt2 = numericValue.value as? Int {
int2 = tempInt2
} else {
throw JinjaError.runtime("Third argument must be an Int")
}
}
let result = fn(int0, int1, int2)
return try self.convertToRuntimeValues(input: result)
}
case let values as [Any]:
var items: [any RuntimeValue] = []
for value in values {
try items.append(self.convertToRuntimeValues(input: value))
case let fn as ([Int]) -> [Int]:
return FunctionValue { args, _ in
let intArgs = args.compactMap { ($0 as? NumericValue)?.value as? Int }
guard intArgs.count == args.count else {
throw JinjaError.runtime("Arguments to range must be Ints")
}
let result = fn(intArgs)
return try self.convertToRuntimeValues(input: result)
}
case let fn as (Int, Int?, Int) -> [Int]:
return FunctionValue { args, _ in
guard let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
throw JinjaError.runtime("First argument must be an Int")
}
let int1 = (args.count > 1) ? (args[1] as? NumericValue)?.value as? Int : nil
guard let arg2 = args.last as? NumericValue, let int2 = arg2.value as? Int else {
throw JinjaError.runtime("Last argument must be an Int")
}
let result = fn(int0, int1, int2)
return try self.convertToRuntimeValues(input: result)
}
case let values as [Any]:
let items = try values.map { try self.convertToRuntimeValues(input: $0) }
return ArrayValue(value: items)
case let dictionary as [String: String]:
case let dictionary as [String: Any?]:
// Create ordered pairs from the dictionary, maintaining original order
let orderedPairs = Array(dictionary)
var object: [String: any RuntimeValue] = [:]
var keyOrder: [String] = []

for (key, value) in dictionary {
object[key] = StringValue(value: value)
// Convert values while maintaining order
for (key, value) in orderedPairs {
// Convert nil values to NullValue
object[key] = try self.convertToRuntimeValues(input: value)
keyOrder.append(key)
}

return ObjectValue(value: object)
// Use the original order from orderedPairs for keyOrder
return ObjectValue(value: object, keyOrder: keyOrder)

case is NullValue:
return NullValue()
default:
throw JinjaError.runtime("Cannot convert to runtime value: \(input) type:\(type(of: input))")
throw JinjaError.runtime(
"Cannot convert to runtime value: \(String(describing: input)) type:\(type(of: input))"
)
}
}

Expand All @@ -176,12 +253,11 @@ class Environment {
}

func declareVariable(name: String, value: any RuntimeValue) throws -> any RuntimeValue {
if self.variables.contains(where: { $0.0 == name }) {
if self.variables.keys.contains(name) {
throw JinjaError.syntax("Variable already declared: \(name)")
}

self.variables[name] = value

return value
}

Expand All @@ -191,25 +267,21 @@ class Environment {
return value
}

func resolve(name: String) throws -> Self {
if self.variables.contains(where: { $0.0 == name }) {
func resolve(name: String) throws -> Environment {
if self.variables.keys.contains(name) {
return self
}

if let parent {
return try parent.resolve(name: name) as! Self
if let parent = self.parent {
return try parent.resolve(name: name)
}

throw JinjaError.runtime("Unknown variable: \(name)")
}

func lookupVariable(name: String) -> any RuntimeValue {
do {
if let value = try self.resolve(name: name).variables[name] {
return value
} else {
return UndefinedValue()
}
return try self.resolve(name: name).variables[name] ?? UndefinedValue()
} catch {
return UndefinedValue()
}
Expand Down
Loading

0 comments on commit 20d6621

Please sign in to comment.