Skip to content

Commit 9fbf403

Browse files
committed
Allow value export of unsafe code by passing variable names to the native code (#70)
1 parent 5eba884 commit 9fbf403

3 files changed

Lines changed: 47 additions & 11 deletions

File tree

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,19 +418,27 @@ panic(err)
418418
```
419419

420420
### ⚠️ Unsafe
421-
It's possible to add native code directly to the output by using the *unsafe*-builtin. However, this should be avoided if possible as it can introduce unwanted side-effects. The builtin's first argument must be a string literal which contains the executable code. All other arguments can be of type *bool*, *int* or *string*. The transpiler parses the string literal and replaces all placeholders (e.g. {0}) with the corresponding positional argument. Then the code is added to the output. E.g.
421+
It's possible to add native code directly to the output by using the *unsafe*-builtin. However, this should be avoided if possible as it can introduce unwanted side-effects. The builtin's first argument must be a string literal which contains the executable code. All other arguments can be of type *bool*, *int* or *string*. The transpiler parses the string literal and replaces all placeholders (e.g. {0}) with the corresponding positional argument at transpile time.
422+
423+
To pass data into the native code, placeholders with either only the positional number (e.g. "{0}") or "i:" followed by the positional number (e.g. "{i:0}") should be used.
424+
425+
To get data out of the native code, placeholders with "o:" followed by the positional number must be used. **IMPORTANT**: The passed expression must be a variable.
426+
427+
In the following example, the variable *file* is passed as input to the native Batch code, while the variable *date* is passed as output argument. After the execution, *date* holds the value evaluated by the native code.
422428

423429
```golang
424430
file := "test.tsh"
431+
var date string
425432

426-
unsafe(`for %%U in ({0}) do (@echo %%~tU)`, file)
433+
unsafe(`for %%U in ({i:0}) do (set "{o:1}=%%~tU")`, file, date)
427434
```
428435

429436
Results in
430437

431438
```batch
432439
set "file_0=test.tsh"
433-
for %%U in (!file_0!) do (@echo %%~tU)
440+
set "date_0="
441+
for %%U in (!file_0!) do (set "date_0=%%~tU")
434442
```
435443

436444
## Caveats

parser/parser.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -689,14 +689,14 @@ func (p *Parser) evaluateBuiltInFunction(tokenType lexer.TokenType, keyword stri
689689
}
690690
nextToken := p.eat()
691691

692-
// Make sure after the print call comes a opening round bracket.
692+
// Make sure after the builtin call comes a opening round bracket.
693693
if nextToken.Type() != lexer.OPENING_ROUND_BRACKET {
694694
return nil, p.expectedError(`"("`, nextToken)
695695
}
696696
expressions := []Expression{}
697697
nextToken = p.peek()
698698

699-
// Evaluate arguments if it's a print call with arguments.
699+
// Evaluate arguments if it's a builtin call with arguments.
700700
if nextToken.Type() != lexer.CLOSING_ROUND_BRACKET {
701701
for {
702702
expr, err := p.evaluateExpression(ctx)
@@ -730,7 +730,7 @@ func (p *Parser) evaluateBuiltInFunction(tokenType lexer.TokenType, keyword stri
730730
}
731731
nextToken = p.eat()
732732

733-
// Make sure print call is terminated with a closing round bracket.
733+
// Make sure builtin call is terminated with a closing round bracket.
734734
if nextToken.Type() != lexer.CLOSING_ROUND_BRACKET {
735735
return nil, p.expectedError(`")"`, nextToken)
736736
}
@@ -3956,11 +3956,21 @@ func (p *Parser) evaluateUnsafe(ctx context) (Statement, error) {
39563956
if len(expressions) > 1 {
39573957
args = expressions[1:]
39583958
}
3959+
literalValue := literal.Value()
39593960

3960-
for _, arg := range args {
3961+
for i, arg := range args {
39613962
if t := arg.ValueType(); !t.IsBool() && !t.IsInt() && !t.IsString() {
39623963
return nil, p.expectedType(t, keywordToken, NewValueType(NewTypeBool(), false), NewValueType(NewTypeInt(), false), NewValueType(NewTypeString(), false))
39633964
}
3965+
3966+
// If output-placeholders exist, the provided expression must be a variable evaluation.
3967+
if strings.Count(literalValue, fmt.Sprintf("{o:%d}", i)) > 0 {
3968+
_, ok := arg.(VariableEvaluation)
3969+
3970+
if !ok {
3971+
return nil, p.atError(fmt.Sprintf("argument %d must be a variable", i+1), keywordToken)
3972+
}
3973+
}
39643974
}
39653975
return Unsafe{
39663976
code: literal,

transpiler/transpiler.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,30 @@ func (t *transpiler) evaluateUnsafe(unsafe parser.Unsafe) error {
235235
literal := unsafe.Code().Value()
236236

237237
for i, arg := range unsafe.Args() {
238-
result, err := t.evaluateExpression(arg, true)
238+
inputPlaceholder := fmt.Sprintf("{%d}", i)
239+
outputPlaceholder := fmt.Sprintf("{o:%d}", i)
239240

240-
if err != nil {
241-
return err
241+
// Replace all input-placeholders with regular placeholders as they are equivalent.
242+
literal = strings.ReplaceAll(literal, fmt.Sprintf("{i:%d}", i), fmt.Sprintf("{%d}", i))
243+
244+
// Count input- and output-placeholders to only evaluate what's really needed.
245+
inputsCount := strings.Count(literal, inputPlaceholder)
246+
outputsCount := strings.Count(literal, outputPlaceholder)
247+
248+
if outputsCount > 0 {
249+
variableEvaluation := arg.(parser.VariableEvaluation)
250+
251+
literal = strings.ReplaceAll(literal, outputPlaceholder, variableEvaluation.LayerName())
252+
}
253+
254+
if inputsCount > 0 {
255+
result, err := t.evaluateExpression(arg, true)
256+
257+
if err != nil {
258+
return err
259+
}
260+
literal = strings.ReplaceAll(literal, inputPlaceholder, result.firstValue())
242261
}
243-
literal = strings.ReplaceAll(literal, fmt.Sprintf("{%d}", i), result.firstValue())
244262
}
245263
return t.converter.Unsafe(literal)
246264
}

0 commit comments

Comments
 (0)