Skip to content

Commit a60b6b1

Browse files
committed
Add functionality from TypeScript implementation
1 parent 9d79438 commit a60b6b1

9 files changed

+1869
-348
lines changed

Sources/Ast.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ struct TupleLiteral: Literal {
4141
}
4242

4343
struct ObjectLiteral: Literal {
44-
var value: [(Expression, Expression)]
44+
var value: [String: Expression]
4545
}
4646

4747
struct Set: Statement {
4848
var assignee: Expression
4949
var value: Expression
5050
}
5151

52-
struct If: Statement {
52+
struct If: Statement, Expression {
5353
var test: Expression
5454
var body: [Statement]
5555
var alternate: [Statement]
@@ -59,14 +59,14 @@ struct Identifier: Expression {
5959
var value: String
6060
}
6161

62-
protocol Loopvar {}
63-
extension Identifier: Loopvar {}
64-
extension TupleLiteral: Loopvar {}
62+
typealias Loopvar = Expression
6563

6664
struct For: Statement {
6765
var loopvar: Loopvar
6866
var iterable: Expression
6967
var body: [Statement]
68+
var defaultBlock: [Statement]
69+
var ifCondition: Expression?
7070
}
7171

7272
struct MemberExpression: Expression {
@@ -124,3 +124,23 @@ struct KeywordArgumentExpression: Expression {
124124
struct NullLiteral: Literal {
125125
var value: Any? = nil
126126
}
127+
128+
struct SelectExpression: Expression {
129+
var iterable: Expression
130+
var test: Expression
131+
}
132+
133+
struct Macro: Statement {
134+
var name: Identifier
135+
var args: [Expression]
136+
var body: [Statement]
137+
}
138+
139+
struct KeywordArgumentsValue: RuntimeValue {
140+
var value: [String: any RuntimeValue]
141+
var builtins: [String: any RuntimeValue] = [:]
142+
143+
func bool() -> Bool {
144+
!value.isEmpty
145+
}
146+
}

Sources/Environment.swift

Lines changed: 121 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,39 @@ class Environment {
1212

1313
var variables: [String: any RuntimeValue] = [
1414
"namespace": FunctionValue(value: { args, _ in
15-
if args.count == 0 {
15+
if args.isEmpty {
1616
return ObjectValue(value: [:])
1717
}
1818

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

23-
return args[0]
23+
return objectArg
2424
})
2525
]
2626

2727
var tests: [String: (any RuntimeValue...) throws -> Bool] = [
28-
"boolean": {
29-
args in
30-
args[0] is BooleanValue
28+
"boolean": { args in
29+
return args[0] is BooleanValue
3130
},
3231

33-
"callable": {
34-
args in
35-
args[0] is FunctionValue
32+
"callable": { args in
33+
return args[0] is FunctionValue
3634
},
3735

38-
"odd": {
39-
args in
40-
if let arg = args.first as? NumericValue {
41-
return arg.value as! Int % 2 != 0
36+
"odd": { args in
37+
if let arg = args.first as? NumericValue, let intValue = arg.value as? Int {
38+
return intValue % 2 != 0
4239
} else {
43-
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of:args.first))")
40+
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of: args.first))")
4441
}
4542
},
4643
"even": { args in
47-
if let arg = args.first as? NumericValue {
48-
return arg.value as! Int % 2 == 0
44+
if let arg = args.first as? NumericValue, let intValue = arg.value as? Int {
45+
return intValue % 2 == 0
4946
} else {
50-
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of:args.first))")
47+
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of: args.first))")
5148
}
5249
},
5350
"false": { args in
@@ -62,24 +59,28 @@ class Environment {
6259
}
6360
return false
6461
},
62+
"string": { args in
63+
return args[0] is StringValue
64+
},
6565
"number": { args in
66-
args[0] is NumericValue
66+
return args[0] is NumericValue
6767
},
6868
"integer": { args in
6969
if let arg = args[0] as? NumericValue {
7070
return arg.value is Int
7171
}
72-
7372
return false
7473
},
74+
"mapping": { args in
75+
return args[0] is ObjectValue
76+
},
7577
"iterable": { args in
76-
args[0] is ArrayValue || args[0] is StringValue
78+
return args[0] is ArrayValue || args[0] is StringValue || args[0] is ObjectValue
7779
},
7880
"lower": { args in
7981
if let arg = args[0] as? StringValue {
8082
return arg.value == arg.value.lowercased()
8183
}
82-
8384
return false
8485
},
8586
"upper": { args in
@@ -89,16 +90,47 @@ class Environment {
8990
return false
9091
},
9192
"none": { args in
92-
args[0] is NullValue
93+
return args[0] is NullValue
9394
},
9495
"defined": { args in
95-
!(args[0] is UndefinedValue)
96+
return !(args[0] is UndefinedValue)
9697
},
9798
"undefined": { args in
98-
args[0] is UndefinedValue
99+
return args[0] is UndefinedValue
99100
},
100-
"equalto": { _ in
101-
throw JinjaError.syntaxNotSupported("equalto")
101+
"equalto": { args in
102+
if args.count == 2 {
103+
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
104+
return left.value == right.value
105+
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
106+
let leftInt = left.value as? Int, let rightInt = right.value as? Int
107+
{
108+
return leftInt == rightInt
109+
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
110+
return left.value == right.value
111+
} else {
112+
return false
113+
}
114+
} else {
115+
return false
116+
}
117+
},
118+
"eq": { args in
119+
if args.count == 2 {
120+
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
121+
return left.value == right.value
122+
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
123+
let leftInt = left.value as? Int, let rightInt = right.value as? Int
124+
{
125+
return leftInt == rightInt
126+
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
127+
return left.value == right.value
128+
} else {
129+
return false
130+
}
131+
} else {
132+
return false
133+
}
102134
},
103135
]
104136

@@ -107,61 +139,91 @@ class Environment {
107139
}
108140

109141
func isFunction<T>(_ value: Any, functionType: T.Type) -> Bool {
110-
value is T
142+
return value is T
111143
}
112144

113145
func convertToRuntimeValues(input: Any) throws -> any RuntimeValue {
114146
switch input {
115147
case let value as Bool:
116148
return BooleanValue(value: value)
117-
case let values as [any Numeric]:
118-
var items: [any RuntimeValue] = []
119-
for value in values {
120-
try items.append(self.convertToRuntimeValues(input: value))
121-
}
122-
return ArrayValue(value: items)
123149
case let value as any Numeric:
124150
return NumericValue(value: value)
125151
case let value as String:
126152
return StringValue(value: value)
153+
case let data as Data:
154+
guard let string = String(data: data, encoding: .utf8) else {
155+
throw JinjaError.runtime("Failed to convert data to string")
156+
}
157+
return StringValue(value: string)
127158
case let fn as (String) throws -> Void:
128159
return FunctionValue { args, _ in
129-
var arg = ""
130-
switch args[0].value {
131-
case let value as String:
132-
arg = value
133-
case let value as Bool:
134-
arg = String(value)
135-
default:
136-
throw JinjaError.runtime("Unknown arg type:\(type(of: args[0].value))")
160+
guard let stringArg = args[0] as? StringValue else {
161+
throw JinjaError.runtime("Argument must be a StringValue")
137162
}
138-
139-
try fn(arg)
163+
try fn(stringArg.value)
140164
return NullValue()
141165
}
142166
case let fn as (Bool) throws -> Void:
143167
return FunctionValue { args, _ in
144-
try fn(args[0].value as! Bool)
168+
guard let boolArg = args[0] as? BooleanValue else {
169+
throw JinjaError.runtime("Argument must be a BooleanValue")
170+
}
171+
try fn(boolArg.value)
145172
return NullValue()
146173
}
147174
case let fn as (Int, Int?, Int) -> [Int]:
148175
return FunctionValue { args, _ in
149-
let result = fn(args[0].value as! Int, args[1].value as? Int, args[2].value as! Int)
176+
guard args.count > 0, let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
177+
throw JinjaError.runtime("First argument must be an Int")
178+
}
179+
var int1: Int? = nil
180+
if args.count > 1 {
181+
if let numericValue = args[1] as? NumericValue, let tempInt1 = numericValue.value as? Int {
182+
int1 = tempInt1
183+
} else {
184+
throw JinjaError.runtime("Second argument must be an Int or nil")
185+
}
186+
}
187+
var int2: Int = 1
188+
if args.count > 2 {
189+
if let numericValue = args[2] as? NumericValue, let tempInt2 = numericValue.value as? Int {
190+
int2 = tempInt2
191+
} else {
192+
throw JinjaError.runtime("Third argument must be an Int")
193+
}
194+
}
195+
let result = fn(int0, int1, int2)
150196
return try self.convertToRuntimeValues(input: result)
151197
}
152-
case let values as [Any]:
153-
var items: [any RuntimeValue] = []
154-
for value in values {
155-
try items.append(self.convertToRuntimeValues(input: value))
198+
case let fn as ([Int]) -> [Int]:
199+
return FunctionValue { args, _ in
200+
let intArgs = args.compactMap { ($0 as? NumericValue)?.value as? Int }
201+
guard intArgs.count == args.count else {
202+
throw JinjaError.runtime("Arguments to range must be Ints")
203+
}
204+
let result = fn(intArgs)
205+
return try self.convertToRuntimeValues(input: result)
156206
}
207+
case let fn as (Int, Int?, Int) -> [Int]:
208+
return FunctionValue { args, _ in
209+
guard let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
210+
throw JinjaError.runtime("First argument must be an Int")
211+
}
212+
let int1 = (args.count > 1) ? (args[1] as? NumericValue)?.value as? Int : nil
213+
guard let arg2 = args.last as? NumericValue, let int2 = arg2.value as? Int else {
214+
throw JinjaError.runtime("Last argument must be an Int")
215+
}
216+
let result = fn(int0, int1, int2)
217+
return try self.convertToRuntimeValues(input: result)
218+
}
219+
case let values as [Any]:
220+
let items = try values.map { try self.convertToRuntimeValues(input: $0) }
157221
return ArrayValue(value: items)
158-
case let dictionary as [String: String]:
222+
case let dictionary as [String: Any]:
159223
var object: [String: any RuntimeValue] = [:]
160-
161224
for (key, value) in dictionary {
162-
object[key] = StringValue(value: value)
225+
object[key] = try self.convertToRuntimeValues(input: value)
163226
}
164-
165227
return ObjectValue(value: object)
166228
case is NullValue:
167229
return NullValue()
@@ -176,12 +238,11 @@ class Environment {
176238
}
177239

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

183245
self.variables[name] = value
184-
185246
return value
186247
}
187248

@@ -191,25 +252,21 @@ class Environment {
191252
return value
192253
}
193254

194-
func resolve(name: String) throws -> Self {
195-
if self.variables.contains(where: { $0.0 == name }) {
255+
func resolve(name: String) throws -> Environment {
256+
if self.variables.keys.contains(name) {
196257
return self
197258
}
198259

199-
if let parent {
200-
return try parent.resolve(name: name) as! Self
260+
if let parent = self.parent {
261+
return try parent.resolve(name: name)
201262
}
202263

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

206267
func lookupVariable(name: String) -> any RuntimeValue {
207268
do {
208-
if let value = try self.resolve(name: name).variables[name] {
209-
return value
210-
} else {
211-
return UndefinedValue()
212-
}
269+
return try self.resolve(name: name).variables[name] ?? UndefinedValue()
213270
} catch {
214271
return UndefinedValue()
215272
}

Sources/Lexer.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ enum TokenType: String {
5050
case and = "And"
5151
case or = "Or"
5252
case not = "Not"
53+
case macro = "Macro"
54+
case endMacro = "EndMacro"
5355
}
5456

5557
struct Token: Equatable {
@@ -70,6 +72,8 @@ let keywords: [String: TokenType] = [
7072
"and": .and,
7173
"or": .or,
7274
"not": .not,
75+
"macro": .macro,
76+
"endmacro": .endMacro,
7377
// Literals
7478
"true": .booleanLiteral,
7579
"false": .booleanLiteral,
@@ -81,7 +85,7 @@ func isWord(char: String) -> Bool {
8185
}
8286

8387
func isInteger(char: String) -> Bool {
84-
char.range(of: #"[0-9]"#, options: .regularExpression) != nil
88+
char.range(of: #"^[0-9]+$"#, options: .regularExpression) != nil
8589
}
8690

8791
func isWhile(char: String) -> Bool {

0 commit comments

Comments
 (0)