merge Cordial eval display formats
This commit is contained in:
commit
93bebbf2bf
|
|
@ -0,0 +1,781 @@
|
||||||
|
# Cordial Language Reference Guide
|
||||||
|
|
||||||
|
Cordial is a simple, expression-based programming language embedded in Swiftly. This guide documents every working feature.
|
||||||
|
|
||||||
|
## Evaluation Prefix
|
||||||
|
|
||||||
|
All evaluation in the editor uses the `/=` prefix:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 2 + 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
Cordial has five value types:
|
||||||
|
|
||||||
|
- **number**: floating-point numbers (internally f64). Displayed as integers when they have no fractional part.
|
||||||
|
- **bool**: `true` or `false`
|
||||||
|
- **str**: strings in double quotes, with escape sequences (`\n`, `\t`, `\\`, `\"`)
|
||||||
|
- **array**: ordered collections of values
|
||||||
|
- **void**: the value returned by statements with no explicit value
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
### Declaration
|
||||||
|
|
||||||
|
Use `let` to declare and initialize a variable:
|
||||||
|
|
||||||
|
```
|
||||||
|
let x = 5
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Annotations
|
||||||
|
|
||||||
|
Optionally annotate the type after the variable name:
|
||||||
|
|
||||||
|
```
|
||||||
|
let x: int = 3.7
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported type annotations:
|
||||||
|
- `int`: truncates numbers to integers
|
||||||
|
- `float`: accepts any number (no coercion)
|
||||||
|
- `bool`: converts 0 to `false`, 1 to `true`; rejects other numbers
|
||||||
|
- `str`: only accepts strings
|
||||||
|
|
||||||
|
```
|
||||||
|
let b: bool = 1
|
||||||
|
/= b
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assignment
|
||||||
|
|
||||||
|
Reassign an existing variable without `let`:
|
||||||
|
|
||||||
|
```
|
||||||
|
let x = 5
|
||||||
|
x = 10
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operators
|
||||||
|
|
||||||
|
### Arithmetic
|
||||||
|
|
||||||
|
- `+`: addition or string concatenation
|
||||||
|
- `-`: subtraction
|
||||||
|
- `*`: multiplication
|
||||||
|
- `/`: division (errors on division by zero)
|
||||||
|
- `%`: modulo
|
||||||
|
- `^`: exponentiation (right-associative)
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 2 ^ 3 ^ 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
512
|
||||||
|
```
|
||||||
|
|
||||||
|
(Evaluated as 2 ^ (3 ^ 2) = 2 ^ 9 = 512)
|
||||||
|
|
||||||
|
### Comparison
|
||||||
|
|
||||||
|
All comparison operators return booleans:
|
||||||
|
|
||||||
|
- `<`: less than
|
||||||
|
- `>`: greater than
|
||||||
|
- `<=`: less than or equal
|
||||||
|
- `>=`: greater than or equal
|
||||||
|
- `==`: equality
|
||||||
|
- `!=`: inequality
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 5 > 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logical
|
||||||
|
|
||||||
|
- `&&`: logical AND (short-circuits)
|
||||||
|
- `||`: logical OR (short-circuits)
|
||||||
|
- `!`: logical NOT
|
||||||
|
|
||||||
|
```
|
||||||
|
/= true && false
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
false
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= !true
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operator Precedence
|
||||||
|
|
||||||
|
From lowest to highest:
|
||||||
|
1. `||`
|
||||||
|
2. `&&`
|
||||||
|
3. `==`, `!=`, `<`, `>`, `<=`, `>=`
|
||||||
|
4. `+`, `-`
|
||||||
|
5. `*`, `/`, `%`
|
||||||
|
6. `^` (right-associative)
|
||||||
|
7. unary `-`, `!`
|
||||||
|
8. function calls, array literals
|
||||||
|
|
||||||
|
## Strings
|
||||||
|
|
||||||
|
String literals use double quotes:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
hello
|
||||||
|
```
|
||||||
|
|
||||||
|
Escape sequences:
|
||||||
|
- `\n`: newline
|
||||||
|
- `\t`: tab
|
||||||
|
- `\\`: backslash
|
||||||
|
- `\"`: double quote
|
||||||
|
|
||||||
|
### String Concatenation
|
||||||
|
|
||||||
|
Strings concatenate with the `+` operator. Numbers and booleans are automatically converted:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= "value: " + 42
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
value: 42
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 100 + " items"
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
100 items
|
||||||
|
```
|
||||||
|
|
||||||
|
## Arrays
|
||||||
|
|
||||||
|
### Literals
|
||||||
|
|
||||||
|
Create arrays with square brackets:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= [1, 2, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[1, 2, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
Arrays can contain mixed types:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= [1, "two", true]
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[1, "two", true]
|
||||||
|
```
|
||||||
|
|
||||||
|
Empty arrays:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= []
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Indexing
|
||||||
|
|
||||||
|
Access elements by index (zero-based):
|
||||||
|
|
||||||
|
```
|
||||||
|
let arr = [10, 20, 30]
|
||||||
|
/= arr[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
20
|
||||||
|
```
|
||||||
|
|
||||||
|
Negative indices count from the end:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= [10, 20, 30][-1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
30
|
||||||
|
```
|
||||||
|
|
||||||
|
Out-of-bounds access returns an error.
|
||||||
|
|
||||||
|
### String Indexing
|
||||||
|
|
||||||
|
Strings support the same index syntax, returning individual characters:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= "hello"[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
h
|
||||||
|
```
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
Define functions with the `fn` keyword:
|
||||||
|
|
||||||
|
```
|
||||||
|
fn add(a, b) {
|
||||||
|
a + b
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The last expression in the function body is returned implicitly. An explicit `return` statement is also available for early exit.
|
||||||
|
|
||||||
|
```
|
||||||
|
fn square(x) {
|
||||||
|
x * x
|
||||||
|
}
|
||||||
|
/= square(5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
25
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
Function parameters are local to the function. The function saves and restores the variable scope:
|
||||||
|
|
||||||
|
```
|
||||||
|
let x = 10
|
||||||
|
fn modify() {
|
||||||
|
x = 5
|
||||||
|
}
|
||||||
|
modify()
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
10
|
||||||
|
```
|
||||||
|
|
||||||
|
(x inside the function is a separate variable; the outer x is unchanged)
|
||||||
|
|
||||||
|
### Recursion
|
||||||
|
|
||||||
|
Functions can call themselves recursively, up to a maximum call depth of 256:
|
||||||
|
|
||||||
|
```
|
||||||
|
fn fib(n) {
|
||||||
|
let a = 0
|
||||||
|
let b = 1
|
||||||
|
let i = 0
|
||||||
|
while (i < n) {
|
||||||
|
let tmp = b
|
||||||
|
b = a + b
|
||||||
|
a = tmp
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
a
|
||||||
|
}
|
||||||
|
/= fib(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
55
|
||||||
|
```
|
||||||
|
|
||||||
|
## Control Flow
|
||||||
|
|
||||||
|
### While Loops
|
||||||
|
|
||||||
|
Loop while a condition is truthy:
|
||||||
|
|
||||||
|
```
|
||||||
|
let i = 0
|
||||||
|
let sum = 0
|
||||||
|
while (i < 10) {
|
||||||
|
sum = sum + i
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
/= sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
45
|
||||||
|
```
|
||||||
|
|
||||||
|
Parentheses around the condition are optional (but recommended for clarity).
|
||||||
|
|
||||||
|
Loop safety: while loops are limited to 10,000 iterations to prevent infinite loops.
|
||||||
|
|
||||||
|
### If/Else
|
||||||
|
|
||||||
|
Conditional branching with optional else clause:
|
||||||
|
|
||||||
|
```
|
||||||
|
let x = 10
|
||||||
|
if x > 5 {
|
||||||
|
x = 1
|
||||||
|
} else {
|
||||||
|
x = 0
|
||||||
|
}
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
1
|
||||||
|
```
|
||||||
|
|
||||||
|
Parentheses around the condition are optional. Else-if chaining works:
|
||||||
|
|
||||||
|
```
|
||||||
|
let grade = 85
|
||||||
|
let letter = "F"
|
||||||
|
if grade >= 90 {
|
||||||
|
letter = "A"
|
||||||
|
} else if grade >= 80 {
|
||||||
|
letter = "B"
|
||||||
|
} else if grade >= 70 {
|
||||||
|
letter = "C"
|
||||||
|
}
|
||||||
|
/= letter
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
B
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Loops
|
||||||
|
|
||||||
|
Iterate over arrays or ranges:
|
||||||
|
|
||||||
|
```
|
||||||
|
let sum = 0
|
||||||
|
for x in [1, 2, 3, 4, 5] {
|
||||||
|
sum = sum + x
|
||||||
|
}
|
||||||
|
/= sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
15
|
||||||
|
```
|
||||||
|
|
||||||
|
Using a range expression (`start..end`, exclusive of end):
|
||||||
|
|
||||||
|
```
|
||||||
|
let sum = 0
|
||||||
|
for i in 0..5 {
|
||||||
|
sum = sum + i
|
||||||
|
}
|
||||||
|
/= sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
10
|
||||||
|
```
|
||||||
|
|
||||||
|
The `range(start, end)` builtin function also works:
|
||||||
|
|
||||||
|
```
|
||||||
|
let sum = 0
|
||||||
|
for i in range(1, 6) {
|
||||||
|
sum = sum + i
|
||||||
|
}
|
||||||
|
/= sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
15
|
||||||
|
```
|
||||||
|
|
||||||
|
For loops are limited to 10,000 iterations.
|
||||||
|
|
||||||
|
### Return Statements
|
||||||
|
|
||||||
|
Use `return` for early exit from functions:
|
||||||
|
|
||||||
|
```
|
||||||
|
fn abs_val(x) {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
x
|
||||||
|
}
|
||||||
|
/= abs_val(-7)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
7
|
||||||
|
```
|
||||||
|
|
||||||
|
Bare `return` (with no value) returns `false`.
|
||||||
|
|
||||||
|
## Builtin Functions
|
||||||
|
|
||||||
|
### Math Functions
|
||||||
|
|
||||||
|
All math functions accept a single number and return a number:
|
||||||
|
|
||||||
|
- `sin(x)`: sine
|
||||||
|
- `cos(x)`: cosine
|
||||||
|
- `tan(x)`: tangent
|
||||||
|
- `asin(x)`: arc sine
|
||||||
|
- `acos(x)`: arc cosine
|
||||||
|
- `atan(x)`: arc tangent
|
||||||
|
- `sqrt(x)`: square root
|
||||||
|
- `abs(x)`: absolute value
|
||||||
|
- `floor(x)`: round down
|
||||||
|
- `ceil(x)`: round up
|
||||||
|
- `round(x)`: round to nearest integer
|
||||||
|
- `ln(x)`: natural logarithm
|
||||||
|
- `log(x)`: base-10 logarithm
|
||||||
|
|
||||||
|
```
|
||||||
|
/= sqrt(16)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
4
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= abs(-5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
5
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= floor(3.7)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Range Function
|
||||||
|
|
||||||
|
`range(start, end)` returns an array of numbers from start to end (exclusive):
|
||||||
|
|
||||||
|
```
|
||||||
|
/= range(0, 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[0, 1, 2, 3, 4]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Length Function
|
||||||
|
|
||||||
|
`len()` returns the length of a string or array:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= len("hello")
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
5
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= len([1, 2, 3])
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Truthiness
|
||||||
|
|
||||||
|
In boolean contexts (conditions, logical operators), values are truthy or falsy:
|
||||||
|
|
||||||
|
- **bool**: `true` is truthy, `false` is falsy
|
||||||
|
- **number**: 0 is falsy, all other numbers are truthy
|
||||||
|
- **string**: empty string is falsy, all other strings are truthy
|
||||||
|
- **array**: empty array is falsy, all other arrays are truthy
|
||||||
|
- **void**: falsy
|
||||||
|
- **error**: falsy
|
||||||
|
|
||||||
|
Truthiness matters for `if`, `while`, `for` conditions and `&&`/`||` operators.
|
||||||
|
|
||||||
|
## Ranges
|
||||||
|
|
||||||
|
The `..` operator creates an array from start to end (exclusive):
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 0..5
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
[0, 1, 2, 3, 4]
|
||||||
|
```
|
||||||
|
|
||||||
|
Ranges are primarily useful with `for` loops and can be stored in variables.
|
||||||
|
|
||||||
|
## Negative Numbers
|
||||||
|
|
||||||
|
Negative number literals are recognized in appropriate contexts:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= -5
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
-5
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 10 + -3
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
7
|
||||||
|
```
|
||||||
|
|
||||||
|
The unary minus operator also works:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= -(5 + 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
-8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Errors are propagated as error values and displayed with an `error:` prefix:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= undefined_var
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
error: undefined variable 'undefined_var'
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/= 1 / 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
error: division by zero
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple statements execute in sequence, and errors in one statement don't prevent subsequent statements from executing.
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
Single-line comments use `//`:
|
||||||
|
|
||||||
|
```
|
||||||
|
// This is a comment
|
||||||
|
let x = 5
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Comments can appear at the end of lines too.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Multi-step Calculation
|
||||||
|
|
||||||
|
```
|
||||||
|
let principal = 1000
|
||||||
|
let rate = 0.05
|
||||||
|
let years = 10
|
||||||
|
let amount = principal * (1 + rate) ^ years
|
||||||
|
/= amount
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
1628.89462382...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fibonacci
|
||||||
|
|
||||||
|
```
|
||||||
|
fn fib(n) {
|
||||||
|
let a = 0
|
||||||
|
let b = 1
|
||||||
|
let i = 0
|
||||||
|
while (i < n) {
|
||||||
|
let tmp = b
|
||||||
|
b = a + b
|
||||||
|
a = tmp
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
a
|
||||||
|
}
|
||||||
|
/= fib(15)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
610
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Building
|
||||||
|
|
||||||
|
```
|
||||||
|
let greeting = "Hello"
|
||||||
|
let name = "World"
|
||||||
|
/= greeting + ", " + name + "!"
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
Hello, World!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Display Formats
|
||||||
|
|
||||||
|
### Inline (default)
|
||||||
|
|
||||||
|
The standard `/=` prefix displays the result as a single value on the right edge of the editor:
|
||||||
|
|
||||||
|
```
|
||||||
|
let x = 42
|
||||||
|
/= x
|
||||||
|
```
|
||||||
|
|
||||||
|
Result appears as: `→ 42`
|
||||||
|
|
||||||
|
Arrays display inline:
|
||||||
|
|
||||||
|
```
|
||||||
|
/= [1, 2, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: `→ [1, 2, 3]`
|
||||||
|
|
||||||
|
### Table (`/=|`)
|
||||||
|
|
||||||
|
The `/=|` prefix renders a 2D array (array of arrays) as a visual table overlay:
|
||||||
|
|
||||||
|
```
|
||||||
|
let data = [["Name", "Age"], ["Alice", 30], ["Bob", 25]]
|
||||||
|
/=| data
|
||||||
|
```
|
||||||
|
|
||||||
|
The first row is treated as the header (rendered bold). Each subsequent row becomes a table row. Non-array values fall back to inline display.
|
||||||
|
|
||||||
|
Works with any 2D array:
|
||||||
|
|
||||||
|
```
|
||||||
|
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
||||||
|
/=| matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tree (`/=\`)
|
||||||
|
|
||||||
|
The `/=\` prefix renders nested data as an indented tree overlay:
|
||||||
|
|
||||||
|
```
|
||||||
|
let tree = [1, [2, 3], [4, [5, 6]]]
|
||||||
|
/=\ tree
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested arrays are expanded with tree-drawing characters showing depth. Leaf values display inline. The root shows the total element count.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- **No array mutation**: Arrays cannot be modified after creation (no `arr[i] = val`)
|
||||||
|
- **No mutable references**: Variable reassignment creates new bindings in the current scope
|
||||||
|
- **Maximum call depth**: 256 function calls deep
|
||||||
|
- **Maximum loop iterations**: 10,000 iterations per while/for loop
|
||||||
|
- **No user-defined types or structs**
|
||||||
|
- **No imports or modules**
|
||||||
|
- **No pattern matching or destructuring**
|
||||||
|
- **No closures or lambdas**
|
||||||
|
|
@ -6,6 +6,7 @@ use crate::interp;
|
||||||
pub struct EvalResult {
|
pub struct EvalResult {
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
pub result: String,
|
pub result: String,
|
||||||
|
pub format: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
|
@ -36,14 +37,23 @@ pub fn evaluate_document(text: &str) -> DocumentResult {
|
||||||
|
|
||||||
let interp_results = interp::interpret_document(&lines);
|
let interp_results = interp::interpret_document(&lines);
|
||||||
for ir in interp_results {
|
for ir in interp_results {
|
||||||
|
let fmt = match ir.format {
|
||||||
|
interp::EvalFormat::Inline => "inline",
|
||||||
|
interp::EvalFormat::Table => "table",
|
||||||
|
interp::EvalFormat::Tree => "tree",
|
||||||
|
};
|
||||||
match ir.value {
|
match ir.value {
|
||||||
Some(interp::Value::Error(e)) => {
|
Some(interp::Value::Error(e)) => {
|
||||||
errors.push(EvalError { line: ir.line, error: e });
|
errors.push(EvalError { line: ir.line, error: e });
|
||||||
}
|
}
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
let s = v.display();
|
let s = match ir.format {
|
||||||
|
interp::EvalFormat::Table => value_to_table_json(&v),
|
||||||
|
interp::EvalFormat::Tree => value_to_tree_json(&v),
|
||||||
|
interp::EvalFormat::Inline => v.display(),
|
||||||
|
};
|
||||||
if !s.is_empty() {
|
if !s.is_empty() {
|
||||||
results.push(EvalResult { line: ir.line, result: s });
|
results.push(EvalResult { line: ir.line, result: s, format: fmt.to_string() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
|
|
@ -53,6 +63,41 @@ pub fn evaluate_document(text: &str) -> DocumentResult {
|
||||||
DocumentResult { results, errors }
|
DocumentResult { results, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn value_to_table_json(val: &interp::Value) -> String {
|
||||||
|
match val {
|
||||||
|
interp::Value::Array(rows) => {
|
||||||
|
let table: Vec<Vec<String>> = rows.iter().map(|row| {
|
||||||
|
match row {
|
||||||
|
interp::Value::Array(cols) => cols.iter().map(|c| c.display()).collect(),
|
||||||
|
other => vec![other.display()],
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
serde_json::to_string(&table).unwrap_or_else(|_| val.display())
|
||||||
|
}
|
||||||
|
_ => val.display(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_tree_json(val: &interp::Value) -> String {
|
||||||
|
fn to_json(v: &interp::Value) -> serde_json::Value {
|
||||||
|
match v {
|
||||||
|
interp::Value::Array(items) => {
|
||||||
|
serde_json::Value::Array(items.iter().map(|i| to_json(i)).collect())
|
||||||
|
}
|
||||||
|
interp::Value::Number(n) => {
|
||||||
|
serde_json::Value::Number(
|
||||||
|
serde_json::Number::from_f64(*n)
|
||||||
|
.unwrap_or_else(|| serde_json::Number::from(0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
interp::Value::Bool(b) => serde_json::Value::Bool(*b),
|
||||||
|
interp::Value::Str(s) => serde_json::Value::String(s.clone()),
|
||||||
|
other => serde_json::Value::String(other.display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::to_string(&to_json(val)).unwrap_or_else(|_| val.display())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn evaluate_line(text: &str) -> Result<String, String> {
|
pub fn evaluate_line(text: &str) -> Result<String, String> {
|
||||||
let mut interp = interp::Interpreter::new();
|
let mut interp = interp::Interpreter::new();
|
||||||
match interp.eval_expr_str(text) {
|
match interp.eval_expr_str(text) {
|
||||||
|
|
@ -251,4 +296,49 @@ mod tests {
|
||||||
assert_eq!(result.results.len(), 1);
|
assert_eq!(result.results.len(), 1);
|
||||||
assert_eq!(result.results[0].result, "7");
|
assert_eq!(result.results[0].result, "7");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_table_format() {
|
||||||
|
let doc = "let data = [[\"Name\", \"Age\"], [\"Alice\", 30], [\"Bob\", 25]]\n/=| data";
|
||||||
|
let result = evaluate_document(doc);
|
||||||
|
assert_eq!(result.results.len(), 1);
|
||||||
|
assert_eq!(result.results[0].format, "table");
|
||||||
|
let parsed: Vec<Vec<String>> = serde_json::from_str(&result.results[0].result).unwrap();
|
||||||
|
assert_eq!(parsed.len(), 3);
|
||||||
|
assert_eq!(parsed[0], vec!["Name", "Age"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_tree_format() {
|
||||||
|
let doc = "let tree = [1, [2, 3], [4, [5]]]\n/=\\ tree";
|
||||||
|
let result = evaluate_document(doc);
|
||||||
|
assert_eq!(result.results.len(), 1);
|
||||||
|
assert_eq!(result.results[0].format, "tree");
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&result.results[0].result).unwrap();
|
||||||
|
assert!(parsed.is_array());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_inline_format_default() {
|
||||||
|
let doc = "let x = 42\n/= x";
|
||||||
|
let result = evaluate_document(doc);
|
||||||
|
assert_eq!(result.results[0].format, "inline");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_table_flat_array() {
|
||||||
|
let doc = "/=| [1, 2, 3]";
|
||||||
|
let result = evaluate_document(doc);
|
||||||
|
assert_eq!(result.results.len(), 1);
|
||||||
|
assert_eq!(result.results[0].format, "table");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_document_json_has_format() {
|
||||||
|
let doc = "let x = 42\n/= x\n/=| [[1, 2], [3, 4]]";
|
||||||
|
let result = evaluate_document(doc);
|
||||||
|
let json = serde_json::to_string(&result).unwrap();
|
||||||
|
assert!(json.contains("\"format\":\"inline\""));
|
||||||
|
assert!(json.contains("\"format\":\"table\""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1158,6 +1158,14 @@ fn apply_type_annotation(val: &Value, ann: Option<&str>) -> Result<Value, String
|
||||||
pub struct InterpResult {
|
pub struct InterpResult {
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
pub value: Option<Value>,
|
pub value: Option<Value>,
|
||||||
|
pub format: EvalFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum EvalFormat {
|
||||||
|
Inline,
|
||||||
|
Table,
|
||||||
|
Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
|
pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
|
||||||
|
|
@ -1174,7 +1182,6 @@ pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
|
||||||
|
|
||||||
for &(idx, content, is_eval) in lines {
|
for &(idx, content, is_eval) in lines {
|
||||||
if is_eval {
|
if is_eval {
|
||||||
// flush any accumulated block first
|
|
||||||
if !block_acc.is_empty() {
|
if !block_acc.is_empty() {
|
||||||
let block_text = block_acc.join("\n");
|
let block_text = block_acc.join("\n");
|
||||||
block_acc.clear();
|
block_acc.clear();
|
||||||
|
|
@ -1182,20 +1189,27 @@ pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
|
||||||
match interp.exec_line(&block_text) {
|
match interp.exec_line(&block_text) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
results.push(InterpResult { line: idx, value: Some(Value::Error(e)) });
|
results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format: EvalFormat::Inline });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let expr = content.trim().strip_prefix("/=").unwrap_or("").trim();
|
let trimmed = content.trim();
|
||||||
|
let (format, expr) = if let Some(rest) = trimmed.strip_prefix("/=|") {
|
||||||
|
(EvalFormat::Table, rest.trim())
|
||||||
|
} else if let Some(rest) = trimmed.strip_prefix("/=\\") {
|
||||||
|
(EvalFormat::Tree, rest.trim())
|
||||||
|
} else {
|
||||||
|
(EvalFormat::Inline, trimmed.strip_prefix("/=").unwrap_or("").trim())
|
||||||
|
};
|
||||||
if expr.is_empty() {
|
if expr.is_empty() {
|
||||||
results.push(InterpResult { line: idx, value: Some(Value::Error("empty expression".into())) });
|
results.push(InterpResult { line: idx, value: Some(Value::Error("empty expression".into())), format });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match interp.eval_expr_str(expr) {
|
match interp.eval_expr_str(expr) {
|
||||||
Ok(val) => results.push(InterpResult { line: idx, value: Some(val) }),
|
Ok(val) => results.push(InterpResult { line: idx, value: Some(val), format }),
|
||||||
Err(e) => results.push(InterpResult { line: idx, value: Some(Value::Error(e)) }),
|
Err(e) => results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format }),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let trimmed = content.trim();
|
let trimmed = content.trim();
|
||||||
|
|
@ -1211,17 +1225,15 @@ pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
|
||||||
block_acc.clear();
|
block_acc.clear();
|
||||||
brace_depth = 0;
|
brace_depth = 0;
|
||||||
if let Err(e) = interp.exec_line(&block_text) {
|
if let Err(e) = interp.exec_line(&block_text) {
|
||||||
results.push(InterpResult { line: idx, value: Some(Value::Error(e)) });
|
results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format: EvalFormat::Inline });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if opens > closes {
|
} else if opens > closes {
|
||||||
// starting a block
|
|
||||||
block_acc.push(trimmed.to_string());
|
block_acc.push(trimmed.to_string());
|
||||||
brace_depth = opens - closes;
|
brace_depth = opens - closes;
|
||||||
} else {
|
} else {
|
||||||
// single-line cordial statement
|
|
||||||
if let Err(e) = interp.exec_line(trimmed) {
|
if let Err(e) = interp.exec_line(trimmed) {
|
||||||
results.push(InterpResult { line: idx, value: Some(Value::Error(e)) });
|
results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format: EvalFormat::Inline });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published var evalResults: [Int: String] = [:]
|
@Published var evalResults: [Int: EvalEntry] = [:]
|
||||||
@Published var noteList: [NoteInfo] = []
|
@Published var noteList: [NoteInfo] = []
|
||||||
@Published var currentNoteID: UUID
|
@Published var currentNoteID: UUID
|
||||||
@Published var selectedNoteIDs: Set<UUID> = []
|
@Published var selectedNoteIDs: Set<UUID> = []
|
||||||
|
|
|
||||||
|
|
@ -2538,20 +2538,18 @@ class LineNumberTextView: NSTextView {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let entry = evalResults[lineNumber - 1] {
|
if let entry = evalResults[lineNumber - 1] {
|
||||||
let displayText: String
|
|
||||||
switch entry.format {
|
switch entry.format {
|
||||||
case .table:
|
case .table:
|
||||||
displayText = "\u{2192} [T] \(entry.result.prefix(50))"
|
drawTableResult(entry.result, at: y, origin: origin, resultAttrs: resultAttrs)
|
||||||
case .tree:
|
case .tree:
|
||||||
displayText = "\u{2192} [R] \(entry.result.prefix(50))"
|
drawTreeResult(entry.result, at: y, origin: origin, resultAttrs: resultAttrs)
|
||||||
case .inline:
|
case .inline:
|
||||||
displayText = "\u{2192} \(entry.result)"
|
let resultStr = NSAttributedString(string: "\u{2192} \(entry.result)", attributes: resultAttrs)
|
||||||
}
|
|
||||||
let resultStr = NSAttributedString(string: displayText, attributes: resultAttrs)
|
|
||||||
let size = resultStr.size()
|
let size = resultStr.size()
|
||||||
let rightEdge = visibleRect.maxX
|
let rightEdge = visibleRect.maxX
|
||||||
resultStr.draw(at: NSPoint(x: rightEdge - size.width - 8, y: y))
|
resultStr.draw(at: NSPoint(x: rightEdge - size.width - 8, y: y))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lineNumber += 1
|
lineNumber += 1
|
||||||
charIndex = NSMaxRange(lineRange)
|
charIndex = NSMaxRange(lineRange)
|
||||||
|
|
@ -2562,8 +2560,9 @@ class LineNumberTextView: NSTextView {
|
||||||
|
|
||||||
private func drawTableResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) {
|
private func drawTableResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) {
|
||||||
guard let data = json.data(using: .utf8),
|
guard let data = json.data(using: .utf8),
|
||||||
let rows = try? JSONSerialization.jsonObject(with: data) as? [[Any]] else {
|
let parsed = try? JSONSerialization.jsonObject(with: data),
|
||||||
let fallback = NSAttributedString(string: "\u{2192} \(json)", attributes: resultAttrs)
|
let rows = parsed as? [[Any]] else {
|
||||||
|
let fallback = NSAttributedString(string: "\u{2192} \(json.prefix(40))", attributes: resultAttrs)
|
||||||
let size = fallback.size()
|
let size = fallback.size()
|
||||||
fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y))
|
fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y))
|
||||||
return
|
return
|
||||||
|
|
@ -2607,7 +2606,7 @@ class LineNumberTextView: NSTextView {
|
||||||
let tableHeight = rowHeight * CGFloat(stringRows.count) + CGFloat(stringRows.count + 1)
|
let tableHeight = rowHeight * CGFloat(stringRows.count) + CGFloat(stringRows.count + 1)
|
||||||
|
|
||||||
let rightEdge = visibleRect.maxX
|
let rightEdge = visibleRect.maxX
|
||||||
let tableX = rightEdge - tableWidth - 8
|
let tableX = rightEdge - tableWidth - 12
|
||||||
let tableY = y
|
let tableY = y
|
||||||
|
|
||||||
let tableRect = NSRect(x: tableX, y: tableY, width: tableWidth, height: tableHeight)
|
let tableRect = NSRect(x: tableX, y: tableY, width: tableWidth, height: tableHeight)
|
||||||
|
|
@ -2618,7 +2617,7 @@ class LineNumberTextView: NSTextView {
|
||||||
path.lineWidth = 0.5
|
path.lineWidth = 0.5
|
||||||
path.stroke()
|
path.stroke()
|
||||||
|
|
||||||
var cy = tableY + 1
|
var cy = tableY + 2
|
||||||
for (ri, row) in stringRows.enumerated() {
|
for (ri, row) in stringRows.enumerated() {
|
||||||
let attrs = ri == 0 ? headerAttrs : cellAttrs
|
let attrs = ri == 0 ? headerAttrs : cellAttrs
|
||||||
var cx = tableX + cellPad
|
var cx = tableX + cellPad
|
||||||
|
|
@ -2632,8 +2631,8 @@ class LineNumberTextView: NSTextView {
|
||||||
if ri == 0 {
|
if ri == 0 {
|
||||||
borderColor.setStroke()
|
borderColor.setStroke()
|
||||||
let linePath = NSBezierPath()
|
let linePath = NSBezierPath()
|
||||||
linePath.move(to: NSPoint(x: tableX + 2, y: cy))
|
linePath.move(to: NSPoint(x: tableX + 2, y: cy - 1))
|
||||||
linePath.line(to: NSPoint(x: tableX + tableWidth - 2, y: cy))
|
linePath.line(to: NSPoint(x: tableX + tableWidth - 2, y: cy - 1))
|
||||||
linePath.lineWidth = 0.5
|
linePath.lineWidth = 0.5
|
||||||
linePath.stroke()
|
linePath.stroke()
|
||||||
}
|
}
|
||||||
|
|
@ -2643,7 +2642,7 @@ class LineNumberTextView: NSTextView {
|
||||||
private func drawTreeResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) {
|
private func drawTreeResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) {
|
||||||
guard let data = json.data(using: .utf8),
|
guard let data = json.data(using: .utf8),
|
||||||
let root = try? JSONSerialization.jsonObject(with: data) else {
|
let root = try? JSONSerialization.jsonObject(with: data) else {
|
||||||
let fallback = NSAttributedString(string: "\u{2192} \(json)", attributes: resultAttrs)
|
let fallback = NSAttributedString(string: "\u{2192} \(json.prefix(40))", attributes: resultAttrs)
|
||||||
let size = fallback.size()
|
let size = fallback.size()
|
||||||
fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y))
|
fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y))
|
||||||
return
|
return
|
||||||
|
|
@ -2664,12 +2663,11 @@ class LineNumberTextView: NSTextView {
|
||||||
func walk(_ node: Any, depth: Int) {
|
func walk(_ node: Any, depth: Int) {
|
||||||
if let arr = node as? [Any] {
|
if let arr = node as? [Any] {
|
||||||
for (i, item) in arr.enumerated() {
|
for (i, item) in arr.enumerated() {
|
||||||
if item is [Any] {
|
|
||||||
let prefix = i == arr.count - 1 ? "\u{2514}" : "\u{251C}"
|
let prefix = i == arr.count - 1 ? "\u{2514}" : "\u{251C}"
|
||||||
|
if item is [Any] {
|
||||||
lines.append(("\(prefix) [\(((item as? [Any])?.count ?? 0))]", depth))
|
lines.append(("\(prefix) [\(((item as? [Any])?.count ?? 0))]", depth))
|
||||||
walk(item, depth: depth + 1)
|
walk(item, depth: depth + 1)
|
||||||
} else {
|
} else {
|
||||||
let prefix = i == arr.count - 1 ? "\u{2514}" : "\u{251C}"
|
|
||||||
lines.append(("\(prefix) \(item)", depth))
|
lines.append(("\(prefix) \(item)", depth))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,17 @@ struct NoteInfo: Identifiable {
|
||||||
var lastModified: Date
|
var lastModified: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EvalFormat: String {
|
||||||
|
case inline
|
||||||
|
case table
|
||||||
|
case tree
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EvalEntry {
|
||||||
|
let result: String
|
||||||
|
let format: EvalFormat
|
||||||
|
}
|
||||||
|
|
||||||
class RustBridge {
|
class RustBridge {
|
||||||
static let shared = RustBridge()
|
static let shared = RustBridge()
|
||||||
|
|
||||||
|
|
@ -41,7 +52,7 @@ class RustBridge {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluate(_ id: UUID) -> [Int: String] {
|
func evaluate(_ id: UUID) -> [Int: EvalEntry] {
|
||||||
guard let ptr = docs[id] else { return [:] }
|
guard let ptr = docs[id] else { return [:] }
|
||||||
guard let cstr = swiftly_doc_evaluate(ptr) else { return [:] }
|
guard let cstr = swiftly_doc_evaluate(ptr) else { return [:] }
|
||||||
let json = String(cString: cstr)
|
let json = String(cString: cstr)
|
||||||
|
|
@ -118,14 +129,15 @@ class RustBridge {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseEvalJSON(_ json: String) -> [Int: String] {
|
private func parseEvalJSON(_ json: String) -> [Int: EvalEntry] {
|
||||||
guard let data = json.data(using: .utf8) else { return [:] }
|
guard let data = json.data(using: .utf8) else { return [:] }
|
||||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
||||||
guard let results = obj["results"] as? [[String: Any]] else { return [:] }
|
guard let results = obj["results"] as? [[String: Any]] else { return [:] }
|
||||||
var dict: [Int: String] = [:]
|
var dict: [Int: EvalEntry] = [:]
|
||||||
for item in results {
|
for item in results {
|
||||||
if let line = item["line"] as? Int, let result = item["result"] as? String {
|
if let line = item["line"] as? Int, let result = item["result"] as? String {
|
||||||
dict[line] = result
|
let fmt = EvalFormat(rawValue: item["format"] as? String ?? "inline") ?? .inline
|
||||||
|
dict[line] = EvalEntry(result: result, format: fmt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dict
|
return dict
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue