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 Dec 29, 2024
1 parent 9d79438 commit 16375f3
Show file tree
Hide file tree
Showing 8 changed files with 1,183 additions and 275 deletions.
27 changes: 23 additions & 4 deletions Sources/Ast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct TupleLiteral: Literal {
}

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

struct Set: Statement {
Expand All @@ -59,14 +59,13 @@ 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]
}

struct MemberExpression: Expression {
Expand Down Expand Up @@ -124,3 +123,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
}
}
144 changes: 80 additions & 64 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,25 @@ 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
},
"iterable": { args in
args[0] is ArrayValue || args[0] is StringValue
return args[0] is ArrayValue || args[0] is StringValue
},
"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 +87,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,61 +136,53 @@ 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 {
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 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 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[1] as? NumericValue)?.value as? Int
guard let arg2 = args[2] as? NumericValue, let int2 = arg2.value as? Int 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))
}
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]:
var object: [String: any RuntimeValue] = [:]

for (key, value) in dictionary {
object[key] = StringValue(value: value)
object[key] = try self.convertToRuntimeValues(input: value)
}

return ObjectValue(value: object)
case is NullValue:
return NullValue()
Expand All @@ -176,12 +197,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 +211,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
4 changes: 4 additions & 0 deletions Sources/Lexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ enum TokenType: String {
case and = "And"
case or = "Or"
case not = "Not"
case macro = "Macro"
case endMacro = "EndMacro"
}

struct Token: Equatable {
Expand All @@ -70,6 +72,8 @@ let keywords: [String: TokenType] = [
"and": .and,
"or": .or,
"not": .not,
"macro": .macro,
"endmacro": .endMacro,
// Literals
"true": .booleanLiteral,
"false": .booleanLiteral,
Expand Down
Loading

0 comments on commit 16375f3

Please sign in to comment.