Repository: benhoyt/goawk
Pull request #16 (2018-12-01 14:03:23)

Support Go-defined native functions


Patch:
diff --git a/README.md b/README.md
index fb97ff0..ef1d3c3 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ The intention is for GoAWK to conform to `awk`'s behavior and to the [POSIX AWK
 
 Additional features GoAWK has over AWK:
 
-* It's embeddable in your Go programs!
+* It's embeddable in your Go programs! You can even call custom Go functions from your AWK scripts.
 * I/O-bound AWK scripts (which is most of them) are significantly faster than `awk`, and on a par with `gawk` and `mawk`.
 * The parser supports `'single-quoted strings'` in addition to `"double-quoted strings"`, primarily to make Windows one-liners easier (the Windows `cmd.exe` shell uses `"` as the quote character).
 
diff --git a/benchmarks.txt b/benchmarks.txt
index ddd2fd5..64b5b90 100644
--- a/benchmarks.txt
+++ b/benchmarks.txt
@@ -1,165 +1,170 @@
 goos: darwin
 goarch: amd64
 pkg: github.com/benhoyt/goawk/interp
-BenchmarkGlobalVars-8             	 2000000	       652 ns/op
-BenchmarkGlobalVars-8             	 2000000	       652 ns/op
-BenchmarkGlobalVars-8             	 2000000	       650 ns/op
-BenchmarkGlobalVars-8             	 2000000	       651 ns/op
-BenchmarkGlobalVars-8             	 2000000	       652 ns/op
-BenchmarkLocalVars-8              	 2000000	       668 ns/op
-BenchmarkLocalVars-8              	 2000000	       652 ns/op
-BenchmarkLocalVars-8              	 2000000	       660 ns/op
-BenchmarkLocalVars-8              	 2000000	       649 ns/op
-BenchmarkLocalVars-8              	 2000000	       644 ns/op
-BenchmarkIncrDecr-8               	 2000000	       735 ns/op
-BenchmarkIncrDecr-8               	 2000000	       734 ns/op
-BenchmarkIncrDecr-8               	 2000000	       726 ns/op
-BenchmarkIncrDecr-8               	 2000000	       735 ns/op
-BenchmarkIncrDecr-8               	 2000000	       741 ns/op
-BenchmarkSimpleBuiltins-8         	 1000000	      1282 ns/op
-BenchmarkSimpleBuiltins-8         	 1000000	      1331 ns/op
-BenchmarkSimpleBuiltins-8         	 1000000	      1271 ns/op
-BenchmarkSimpleBuiltins-8         	 1000000	      1289 ns/op
-BenchmarkSimpleBuiltins-8         	 1000000	      1290 ns/op
-BenchmarkBuiltinMatch-8           	  500000	      3060 ns/op
-BenchmarkBuiltinMatch-8           	  500000	      3057 ns/op
-BenchmarkBuiltinMatch-8           	  500000	      3030 ns/op
-BenchmarkBuiltinMatch-8           	  500000	      3080 ns/op
-BenchmarkBuiltinMatch-8           	  500000	      3048 ns/op
-BenchmarkBuiltinLength-8          	 2000000	      1072 ns/op
-BenchmarkBuiltinLength-8          	 1000000	      1008 ns/op
-BenchmarkBuiltinLength-8          	 1000000	      1050 ns/op
-BenchmarkBuiltinLength-8          	 1000000	      1075 ns/op
-BenchmarkBuiltinLength-8          	 2000000	       913 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1652 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1515 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1672 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1568 ns/op
-BenchmarkBuiltinIndex-8           	 1000000	      1596 ns/op
-BenchmarkBuiltinSubstr-8          	 1000000	      1551 ns/op
-BenchmarkBuiltinSubstr-8          	 1000000	      1675 ns/op
-BenchmarkBuiltinSubstr-8          	 1000000	      1587 ns/op
-BenchmarkBuiltinSubstr-8          	 1000000	      1618 ns/op
-BenchmarkBuiltinSubstr-8          	 1000000	      1583 ns/op
-BenchmarkBuiltinSplitSpace-8      	   20000	     87261 ns/op
-BenchmarkBuiltinSplitSpace-8      	   20000	     87465 ns/op
-BenchmarkBuiltinSplitSpace-8      	   20000	     89976 ns/op
-BenchmarkBuiltinSplitSpace-8      	   20000	     87045 ns/op
-BenchmarkBuiltinSplitSpace-8      	   20000	     85917 ns/op
-BenchmarkBuiltinSplitRegex-8      	   10000	    134739 ns/op
-BenchmarkBuiltinSplitRegex-8      	   10000	    134200 ns/op
-BenchmarkBuiltinSplitRegex-8      	   10000	    134237 ns/op
-BenchmarkBuiltinSplitRegex-8      	   10000	    133984 ns/op
-BenchmarkBuiltinSplitRegex-8      	   10000	    134695 ns/op
-BenchmarkBuiltinSub-8             	  100000	     13567 ns/op
-BenchmarkBuiltinSub-8             	  100000	     13891 ns/op
-BenchmarkBuiltinSub-8             	  100000	     13577 ns/op
-BenchmarkBuiltinSub-8             	  100000	     13588 ns/op
-BenchmarkBuiltinSub-8             	  100000	     13612 ns/op
-BenchmarkBuiltinSubAmpersand-8    	  100000	     13841 ns/op
+BenchmarkGlobalVars-8             	 2000000	       658 ns/op
+BenchmarkGlobalVars-8             	 2000000	       658 ns/op
+BenchmarkGlobalVars-8             	 2000000	       657 ns/op
+BenchmarkGlobalVars-8             	 2000000	       659 ns/op
+BenchmarkGlobalVars-8             	 2000000	       664 ns/op
+BenchmarkLocalVars-8              	 2000000	       619 ns/op
+BenchmarkLocalVars-8              	 2000000	       630 ns/op
+BenchmarkLocalVars-8              	 2000000	       640 ns/op
+BenchmarkLocalVars-8              	 2000000	       618 ns/op
+BenchmarkLocalVars-8              	 2000000	       625 ns/op
+BenchmarkIncrDecr-8               	 2000000	       755 ns/op
+BenchmarkIncrDecr-8               	 2000000	       744 ns/op
+BenchmarkIncrDecr-8               	 2000000	       746 ns/op
+BenchmarkIncrDecr-8               	 2000000	       740 ns/op
+BenchmarkIncrDecr-8               	 2000000	       739 ns/op
+BenchmarkSimpleBuiltins-8         	 1000000	      1340 ns/op
+BenchmarkSimpleBuiltins-8         	 1000000	      1301 ns/op
+BenchmarkSimpleBuiltins-8         	 1000000	      1305 ns/op
+BenchmarkSimpleBuiltins-8         	 1000000	      1328 ns/op
+BenchmarkSimpleBuiltins-8         	 1000000	      1295 ns/op
+BenchmarkBuiltinMatch-8           	  500000	      3099 ns/op
+BenchmarkBuiltinMatch-8           	  500000	      3137 ns/op
+BenchmarkBuiltinMatch-8           	  500000	      3110 ns/op
+BenchmarkBuiltinMatch-8           	  500000	      3175 ns/op
+BenchmarkBuiltinMatch-8           	  500000	      3153 ns/op
+BenchmarkBuiltinLength-8          	 1000000	      1035 ns/op
+BenchmarkBuiltinLength-8          	 2000000	      1023 ns/op
+BenchmarkBuiltinLength-8          	 2000000	       983 ns/op
+BenchmarkBuiltinLength-8          	 1000000	      1084 ns/op
+BenchmarkBuiltinLength-8          	 1000000	      1019 ns/op
+BenchmarkBuiltinIndex-8           	 1000000	      1613 ns/op
+BenchmarkBuiltinIndex-8           	 1000000	      1586 ns/op
+BenchmarkBuiltinIndex-8           	 1000000	      1650 ns/op
+BenchmarkBuiltinIndex-8           	 1000000	      1578 ns/op
+BenchmarkBuiltinIndex-8           	 1000000	      1581 ns/op
+BenchmarkBuiltinSubstr-8          	 1000000	      1592 ns/op
+BenchmarkBuiltinSubstr-8          	 1000000	      1576 ns/op
+BenchmarkBuiltinSubstr-8          	 1000000	      1585 ns/op
+BenchmarkBuiltinSubstr-8          	 1000000	      1612 ns/op
+BenchmarkBuiltinSubstr-8          	 1000000	      1645 ns/op
+BenchmarkBuiltinSplitSpace-8      	   20000	     87979 ns/op
+BenchmarkBuiltinSplitSpace-8      	   20000	     89665 ns/op
+BenchmarkBuiltinSplitSpace-8      	   20000	     88859 ns/op
+BenchmarkBuiltinSplitSpace-8      	   20000	     87480 ns/op
+BenchmarkBuiltinSplitSpace-8      	   20000	     87303 ns/op
+BenchmarkBuiltinSplitRegex-8      	   10000	    133917 ns/op
+BenchmarkBuiltinSplitRegex-8      	   10000	    136324 ns/op
+BenchmarkBuiltinSplitRegex-8      	   10000	    132426 ns/op
+BenchmarkBuiltinSplitRegex-8      	   10000	    134832 ns/op
+BenchmarkBuiltinSplitRegex-8      	   10000	    133654 ns/op
+BenchmarkBuiltinSub-8             	  100000	     13705 ns/op
+BenchmarkBuiltinSub-8             	  100000	     13455 ns/op
+BenchmarkBuiltinSub-8             	  100000	     13545 ns/op
+BenchmarkBuiltinSub-8             	  100000	     13480 ns/op
+BenchmarkBuiltinSub-8             	  100000	     13642 ns/op
+BenchmarkBuiltinSubAmpersand-8    	  100000	     13471 ns/op
+BenchmarkBuiltinSubAmpersand-8    	  100000	     13539 ns/op
 BenchmarkBuiltinSubAmpersand-8    	  100000	     13535 ns/op
-BenchmarkBuiltinSubAmpersand-8    	  100000	     13819 ns/op
-BenchmarkBuiltinSubAmpersand-8    	  100000	     13598 ns/op
-BenchmarkBuiltinSubAmpersand-8    	  100000	     13511 ns/op
-BenchmarkBuiltinGsub-8            	  100000	     16165 ns/op
-BenchmarkBuiltinGsub-8            	  100000	     16376 ns/op
-BenchmarkBuiltinGsub-8            	  100000	     16247 ns/op
-BenchmarkBuiltinGsub-8            	  100000	     16050 ns/op
-BenchmarkBuiltinGsub-8            	  100000	     16093 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	  100000	     16145 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	  100000	     15977 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	  100000	     16014 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	  100000	     16073 ns/op
-BenchmarkBuiltinGsubAmpersand-8   	  100000	     15972 ns/op
-BenchmarkBuiltinSprintf-8         	  200000	     10020 ns/op
-BenchmarkBuiltinSprintf-8         	  200000	     10100 ns/op
+BenchmarkBuiltinSubAmpersand-8    	  100000	     13716 ns/op
+BenchmarkBuiltinSubAmpersand-8    	  100000	     13920 ns/op
+BenchmarkBuiltinGsub-8            	  100000	     16567 ns/op
+BenchmarkBuiltinGsub-8            	  100000	     16374 ns/op
+BenchmarkBuiltinGsub-8            	  100000	     16191 ns/op
+BenchmarkBuiltinGsub-8            	  100000	     16331 ns/op
+BenchmarkBuiltinGsub-8            	  100000	     16287 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	  100000	     15927 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	  100000	     15933 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	  100000	     16140 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	  100000	     15872 ns/op
+BenchmarkBuiltinGsubAmpersand-8   	  100000	     16044 ns/op
+BenchmarkBuiltinSprintf-8         	  200000	     10224 ns/op
 BenchmarkBuiltinSprintf-8         	  200000	     10119 ns/op
-BenchmarkBuiltinSprintf-8         	  200000	     10064 ns/op
-BenchmarkBuiltinSprintf-8         	  200000	     10025 ns/op
-BenchmarkRecursiveFunc-8          	  100000	     16892 ns/op
-BenchmarkRecursiveFunc-8          	  100000	     17137 ns/op
-BenchmarkRecursiveFunc-8          	  100000	     17047 ns/op
-BenchmarkRecursiveFunc-8          	  100000	     17041 ns/op
-BenchmarkRecursiveFunc-8          	  100000	     16936 ns/op
-BenchmarkFuncCall-8               	  500000	      3106 ns/op
-BenchmarkFuncCall-8               	  500000	      3161 ns/op
-BenchmarkFuncCall-8               	  500000	      3129 ns/op
-BenchmarkFuncCall-8               	  500000	      3147 ns/op
-BenchmarkFuncCall-8               	  500000	      3132 ns/op
-BenchmarkForLoop-8                	  300000	      5874 ns/op
-BenchmarkForLoop-8                	  300000	      5858 ns/op
-BenchmarkForLoop-8                	  300000	      5876 ns/op
-BenchmarkForLoop-8                	  300000	      5838 ns/op
-BenchmarkForLoop-8                	  200000	      5897 ns/op
-BenchmarkForInLoop-8              	 1000000	      2003 ns/op
-BenchmarkForInLoop-8              	 1000000	      2022 ns/op
-BenchmarkForInLoop-8              	 1000000	      1965 ns/op
-BenchmarkForInLoop-8              	 1000000	      1978 ns/op
-BenchmarkForInLoop-8              	 1000000	      1974 ns/op
-BenchmarkIfStatement-8            	 5000000	       270 ns/op
-BenchmarkIfStatement-8            	 5000000	       274 ns/op
-BenchmarkIfStatement-8            	 5000000	       276 ns/op
-BenchmarkIfStatement-8            	 5000000	       280 ns/op
-BenchmarkIfStatement-8            	 5000000	       271 ns/op
+BenchmarkBuiltinSprintf-8         	  200000	     10131 ns/op
+BenchmarkBuiltinSprintf-8         	  200000	     10185 ns/op
+BenchmarkBuiltinSprintf-8         	  200000	     10198 ns/op
+BenchmarkRecursiveFunc-8          	  100000	     17225 ns/op
+BenchmarkRecursiveFunc-8          	  100000	     17132 ns/op
+BenchmarkRecursiveFunc-8          	  100000	     17682 ns/op
+BenchmarkRecursiveFunc-8          	  100000	     17068 ns/op
+BenchmarkRecursiveFunc-8          	  100000	     17243 ns/op
+BenchmarkFuncCall-8               	  500000	      3193 ns/op
+BenchmarkFuncCall-8               	  500000	      3172 ns/op
+BenchmarkFuncCall-8               	  500000	      3191 ns/op
+BenchmarkFuncCall-8               	  500000	      3150 ns/op
+BenchmarkFuncCall-8               	  500000	      3156 ns/op
+BenchmarkNativeFunc-8             	  200000	     10142 ns/op
+BenchmarkNativeFunc-8             	  200000	     10175 ns/op
+BenchmarkNativeFunc-8             	  200000	     10171 ns/op
+BenchmarkNativeFunc-8             	  200000	     10205 ns/op
+BenchmarkNativeFunc-8             	  200000	     10158 ns/op
+BenchmarkForLoop-8                	  300000	      5916 ns/op
+BenchmarkForLoop-8                	  300000	      5885 ns/op
+BenchmarkForLoop-8                	  300000	      5873 ns/op
+BenchmarkForLoop-8                	  200000	      6021 ns/op
+BenchmarkForLoop-8                	  200000	      5942 ns/op
+BenchmarkForInLoop-8              	 1000000	      2005 ns/op
+BenchmarkForInLoop-8              	 1000000	      2083 ns/op
+BenchmarkForInLoop-8              	 1000000	      2019 ns/op
+BenchmarkForInLoop-8              	 1000000	      2073 ns/op
+BenchmarkForInLoop-8              	 1000000	      2109 ns/op
+BenchmarkIfStatement-8            	 5000000	       277 ns/op
+BenchmarkIfStatement-8            	 5000000	       277 ns/op
+BenchmarkIfStatement-8            	 5000000	       286 ns/op
+BenchmarkIfStatement-8            	 5000000	       272 ns/op
+BenchmarkIfStatement-8            	 5000000	       282 ns/op
 BenchmarkCondExpr-8               	 5000000	       268 ns/op
+BenchmarkCondExpr-8               	 5000000	       275 ns/op
+BenchmarkCondExpr-8               	 5000000	       276 ns/op
+BenchmarkCondExpr-8               	 5000000	       275 ns/op
 BenchmarkCondExpr-8               	 5000000	       267 ns/op
-BenchmarkCondExpr-8               	 5000000	       263 ns/op
-BenchmarkCondExpr-8               	 5000000	       270 ns/op
-BenchmarkCondExpr-8               	 5000000	       270 ns/op
-BenchmarkSimplePattern-8          	10000000	       186 ns/op
+BenchmarkSimplePattern-8          	10000000	       184 ns/op
 BenchmarkSimplePattern-8          	10000000	       191 ns/op
-BenchmarkSimplePattern-8          	10000000	       186 ns/op
-BenchmarkSimplePattern-8          	10000000	       193 ns/op
-BenchmarkSimplePattern-8          	10000000	       187 ns/op
-BenchmarkGetField-8               	 3000000	       506 ns/op
+BenchmarkSimplePattern-8          	10000000	       190 ns/op
+BenchmarkSimplePattern-8          	10000000	       197 ns/op
+BenchmarkSimplePattern-8          	10000000	       185 ns/op
+BenchmarkGetField-8               	 3000000	       510 ns/op
+BenchmarkGetField-8               	 3000000	       508 ns/op
+BenchmarkGetField-8               	 3000000	       508 ns/op
+BenchmarkGetField-8               	 3000000	       516 ns/op
 BenchmarkGetField-8               	 3000000	       512 ns/op
-BenchmarkGetField-8               	 3000000	       504 ns/op
-BenchmarkGetField-8               	 3000000	       505 ns/op
-BenchmarkGetField-8               	 3000000	       514 ns/op
-BenchmarkSetField-8               	 2000000	       972 ns/op
-BenchmarkSetField-8               	 2000000	       977 ns/op
-BenchmarkSetField-8               	 2000000	       970 ns/op
-BenchmarkSetField-8               	 2000000	       969 ns/op
-BenchmarkSetField-8               	 2000000	       970 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1374 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1395 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1407 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1389 ns/op
-BenchmarkRegexMatch-8             	 1000000	      1398 ns/op
-BenchmarkBinaryOperators-8        	 2000000	       824 ns/op
-BenchmarkBinaryOperators-8        	 2000000	       797 ns/op
-BenchmarkBinaryOperators-8        	 2000000	       823 ns/op
-BenchmarkBinaryOperators-8        	 2000000	       819 ns/op
-BenchmarkBinaryOperators-8        	 2000000	       802 ns/op
+BenchmarkSetField-8               	 2000000	      1000 ns/op
+BenchmarkSetField-8               	 2000000	       994 ns/op
+BenchmarkSetField-8               	 2000000	       987 ns/op
+BenchmarkSetField-8               	 2000000	       998 ns/op
+BenchmarkSetField-8               	 2000000	       987 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1474 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1468 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1531 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1457 ns/op
+BenchmarkRegexMatch-8             	 1000000	      1439 ns/op
+BenchmarkBinaryOperators-8        	 2000000	       818 ns/op
+BenchmarkBinaryOperators-8        	 2000000	       822 ns/op
+BenchmarkBinaryOperators-8        	 2000000	       829 ns/op
+BenchmarkBinaryOperators-8        	 2000000	       807 ns/op
+BenchmarkBinaryOperators-8        	 2000000	       825 ns/op
+BenchmarkConcatSmall-8            	 2000000	       848 ns/op
+BenchmarkConcatSmall-8            	 2000000	       866 ns/op
+BenchmarkConcatSmall-8            	 2000000	       851 ns/op
 BenchmarkConcatSmall-8            	 2000000	       858 ns/op
-BenchmarkConcatSmall-8            	 2000000	       861 ns/op
-BenchmarkConcatSmall-8            	 2000000	       858 ns/op
-BenchmarkConcatSmall-8            	 2000000	       857 ns/op
-BenchmarkConcatSmall-8            	 2000000	       862 ns/op
-BenchmarkConcatLarge-8            	     200	   6516560 ns/op
-BenchmarkConcatLarge-8            	     200	   6524422 ns/op
-BenchmarkConcatLarge-8            	     200	   6575319 ns/op
-BenchmarkConcatLarge-8            	     200	   6653210 ns/op
-BenchmarkConcatLarge-8            	     200	   6677386 ns/op
-BenchmarkComparisons-8            	 3000000	       409 ns/op
-BenchmarkComparisons-8            	 3000000	       405 ns/op
-BenchmarkComparisons-8            	 3000000	       404 ns/op
-BenchmarkComparisons-8            	 3000000	       402 ns/op
-BenchmarkComparisons-8            	 3000000	       404 ns/op
-BenchmarkArrayOperations-8        	 2000000	       989 ns/op
-BenchmarkArrayOperations-8        	 2000000	       982 ns/op
-BenchmarkArrayOperations-8        	 2000000	       966 ns/op
-BenchmarkArrayOperations-8        	 2000000	       974 ns/op
-BenchmarkArrayOperations-8        	 1000000	      1000 ns/op
+BenchmarkConcatSmall-8            	 2000000	       859 ns/op
+BenchmarkConcatLarge-8            	     200	   6819492 ns/op
+BenchmarkConcatLarge-8            	     200	   6710580 ns/op
+BenchmarkConcatLarge-8            	     200	   6747375 ns/op
+BenchmarkConcatLarge-8            	     200	   6898919 ns/op
+BenchmarkConcatLarge-8            	     200	   6715454 ns/op
+BenchmarkComparisons-8            	 3000000	       412 ns/op
+BenchmarkComparisons-8            	 3000000	       416 ns/op
+BenchmarkComparisons-8            	 3000000	       420 ns/op
+BenchmarkComparisons-8            	 3000000	       415 ns/op
+BenchmarkComparisons-8            	 3000000	       416 ns/op
+BenchmarkArrayOperations-8        	 1000000	      1058 ns/op
+BenchmarkArrayOperations-8        	 1000000	      1037 ns/op
+BenchmarkArrayOperations-8        	 1000000	      1043 ns/op
+BenchmarkArrayOperations-8        	 1000000	      1038 ns/op
+BenchmarkArrayOperations-8        	 1000000	      1051 ns/op
+BenchmarkAssign-8                 	 3000000	       540 ns/op
 BenchmarkAssign-8                 	 3000000	       529 ns/op
-BenchmarkAssign-8                 	 3000000	       530 ns/op
-BenchmarkAssign-8                 	 3000000	       527 ns/op
+BenchmarkAssign-8                 	 3000000	       545 ns/op
+BenchmarkAssign-8                 	 3000000	       525 ns/op
 BenchmarkAssign-8                 	 3000000	       525 ns/op
-BenchmarkAssign-8                 	 3000000	       535 ns/op
-BenchmarkAugAssign-8              	  500000	      2063 ns/op
-BenchmarkAugAssign-8              	 1000000	      2076 ns/op
-BenchmarkAugAssign-8              	 1000000	      2093 ns/op
-BenchmarkAugAssign-8              	 1000000	      2080 ns/op
-BenchmarkAugAssign-8              	 1000000	      2074 ns/op
+BenchmarkAugAssign-8              	 1000000	      2072 ns/op
+BenchmarkAugAssign-8              	 1000000	      2043 ns/op
+BenchmarkAugAssign-8              	 1000000	      2066 ns/op
+BenchmarkAugAssign-8              	 1000000	      2050 ns/op
+BenchmarkAugAssign-8              	 1000000	      2043 ns/op
 PASS
-ok  	github.com/benhoyt/goawk/interp	328.185s
+ok  	github.com/benhoyt/goawk/interp	334.364s
diff --git a/goawk.go b/goawk.go
index b7f5700..f5aa744 100644
--- a/goawk.go
+++ b/goawk.go
@@ -44,7 +44,7 @@ import (
 )
 
 const (
-	version = "v1.0.1"
+	version = "v1.1.0"
 )
 
 func main() {
diff --git a/internal/ast/ast.go b/internal/ast/ast.go
index 094ab95..90bffbb 100644
--- a/internal/ast/ast.go
+++ b/internal/ast/ast.go
@@ -265,9 +265,10 @@ func (e *CallExpr) String() string {
 // resolved function index used by the interpreter; Name is the
 // original name used by String().
 type UserCallExpr struct {
-	Index int
-	Name  string
-	Args  []Expr
+	Native bool // false = AWK-defined function, true = native Go func
+	Index  int
+	Name   string
+	Args   []Expr
 }
 
 func (e *UserCallExpr) String() string {
diff --git a/interp/functions.go b/interp/functions.go
index b59f024..7a8d1d0 100644
--- a/interp/functions.go
+++ b/interp/functions.go
@@ -9,6 +9,8 @@ import (
 	"io"
 	"math"
 	"os/exec"
+	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 	"syscall"
@@ -245,7 +247,7 @@ func (p *interp) callBuiltin(op Token, argExprs []Expr) (value, error) {
 }
 
 // Call user-defined function with given index and arguments, return
-// return value (or null value if it doesn't return anything)
+// its return value (or null value if it doesn't return anything)
 func (p *interp) callUser(index int, args []Expr) (value, error) {
 	f := p.program.Functions[index]
 
@@ -303,6 +305,235 @@ func (p *interp) callUser(index int, args []Expr) (value, error) {
 	return value{}, nil
 }
 
+// Call native-defined function with given name and arguments, return
+// return value (or null value if it doesn't return anything).
+func (p *interp) callNative(index int, args []Expr) (value, error) {
+	f := p.nativeFuncs[index]
+	minIn := len(f.in) // Mininum number of args we should pass
+	var variadicType reflect.Type
+	if f.isVariadic {
+		variadicType = f.in[len(f.in)-1].Elem()
+		minIn--
+	}
+
+	// Build list of args to pass to function
+	values := make([]reflect.Value, 0, len(f.in))
+	for i, arg := range args {
+		a, err := p.eval(arg)
+		if err != nil {
+			return value{}, err
+		}
+		var argType reflect.Type
+		if !f.isVariadic || i < len(f.in)-1 {
+			argType = f.in[i]
+		} else {
+			// Final arg(s) when calling a variadic are all of this type
+			argType = variadicType
+		}
+		values = append(values, p.toNative(a, argType))
+	}
+	// Use zero value for any unspecified args
+	for i := len(args); i < minIn; i++ {
+		values = append(values, reflect.Zero(f.in[i]))
+	}
+
+	// Call Go function, determine return value
+	outs := f.value.Call(values)
+	switch len(outs) {
+	case 0:
+		// No return value, return null value to AWK
+		return value{}, nil
+	case 1:
+		// Single return value
+		return fromNative(outs[0]), nil
+	case 2:
+		// Two-valued return of (scalar, error)
+		if !outs[1].IsNil() {
+			return value{}, outs[1].Interface().(error)
+		}
+		return fromNative(outs[0]), nil
+	default:
+		// Should never happen (checked at parse time)
+		panic(fmt.Sprintf("unexpected number of return values: %d", len(outs)))
+	}
+}
+
+// Convert from an AWK value to a native Go value
+func (p *interp) toNative(v value, typ reflect.Type) reflect.Value {
+	switch typ.Kind() {
+	case reflect.Bool:
+		return reflect.ValueOf(v.boolean())
+	case reflect.Int:
+		return reflect.ValueOf(int(v.num()))
+	case reflect.Int8:
+		return reflect.ValueOf(int8(v.num()))
+	case reflect.Int16:
+		return reflect.ValueOf(int16(v.num()))
+	case reflect.Int32:
+		return reflect.ValueOf(int32(v.num()))
+	case reflect.Int64:
+		return reflect.ValueOf(int64(v.num()))
+	case reflect.Uint:
+		return reflect.ValueOf(uint(v.num()))
+	case reflect.Uint8:
+		return reflect.ValueOf(uint8(v.num()))
+	case reflect.Uint16:
+		return reflect.ValueOf(uint16(v.num()))
+	case reflect.Uint32:
+		return reflect.ValueOf(uint32(v.num()))
+	case reflect.Uint64:
+		return reflect.ValueOf(uint64(v.num()))
+	case reflect.Float32:
+		return reflect.ValueOf(float32(v.num()))
+	case reflect.Float64:
+		return reflect.ValueOf(v.num())
+	case reflect.String:
+		return reflect.ValueOf(p.toString(v))
+	case reflect.Slice:
+		if typ.Elem().Kind() != reflect.Uint8 {
+			// Shouldn't happen: prevented at parse time
+			panic(fmt.Sprintf("unexpected argument slice: %s", typ.Elem().Kind()))
+		}
+		return reflect.ValueOf([]byte(p.toString(v)))
+	default:
+		// Shouldn't happen: prevented at parse time
+		panic(fmt.Sprintf("unexpected argument type: %s", typ.Kind()))
+	}
+}
+
+// Convert from a native Go value to an AWK value
+func fromNative(v reflect.Value) value {
+	switch v.Kind() {
+	case reflect.Bool:
+		return boolean(v.Bool())
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return num(float64(v.Int()))
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return num(float64(v.Uint()))
+	case reflect.Float32, reflect.Float64:
+		return num(v.Float())
+	case reflect.String:
+		return str(v.String())
+	case reflect.Slice:
+		if b, ok := v.Interface().([]byte); ok {
+			return str(string(b))
+		}
+		// Shouldn't happen: prevented at parse time
+		panic(fmt.Sprintf("unexpected return slice: %s", v.Type().Elem().Kind()))
+	default:
+		// Shouldn't happen: prevented at parse time
+		panic(fmt.Sprintf("unexpected return type: %s", v.Kind()))
+	}
+}
+
+// Used for caching native function type information on init
+type nativeFunc struct {
+	isVariadic bool
+	in         []reflect.Type
+	value      reflect.Value
+}
+
+// Check and initialize native functions
+func (p *interp) initNativeFuncs(funcs map[string]interface{}) error {
+	for name, f := range funcs {
+		err := checkNativeFunc(name, f)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Sort functions by name, then use those indexes to build slice
+	// (this has to match how the parser sets the indexes).
+	names := make([]string, 0, len(funcs))
+	for name := range funcs {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	p.nativeFuncs = make([]nativeFunc, len(names))
+	for i, name := range names {
+		f := funcs[name]
+		typ := reflect.TypeOf(f)
+		in := make([]reflect.Type, typ.NumIn())
+		for j := 0; j < len(in); j++ {
+			in[j] = typ.In(j)
+		}
+		p.nativeFuncs[i] = nativeFunc{
+			isVariadic: typ.IsVariadic(),
+			in:         in,
+			value:      reflect.ValueOf(f),
+		}
+	}
+	return nil
+}
+
+// Got this trick from the Go stdlib text/template source
+var errorType = reflect.TypeOf((*error)(nil)).Elem()
+
+// Check that native function with given name is okay to call from
+// AWK, return a *interp.Error if not. This checks that f is actually
+// a function, and that it's parameter and return types are good.
+func checkNativeFunc(name string, f interface{}) error {
+	if KeywordToken(name) != ILLEGAL {
+		return newError("can't use keyword %q as native function name", name)
+	}
+
+	typ := reflect.TypeOf(f)
+	if typ.Kind() != reflect.Func {
+		return newError("native function %q is not a function", name)
+	}
+	for i := 0; i < typ.NumIn(); i++ {
+		param := typ.In(i)
+		if typ.IsVariadic() && i == typ.NumIn()-1 {
+			param = param.Elem()
+		}
+		if !validNativeType(param) {
+			return newError("native function %q param %d is not int or string", name, i)
+		}
+	}
+
+	switch typ.NumOut() {
+	case 0:
+		// No return value is fine
+	case 1:
+		// Single scalar return value is fine
+		if !validNativeType(typ.Out(0)) {
+			return newError("native function %q return value is not int or string", name)
+		}
+	case 2:
+		// Returning (scalar, error) is handled too
+		if !validNativeType(typ.Out(0)) {
+			return newError("native function %q first return value is not int or string", name)
+		}
+		if typ.Out(1) != errorType {
+			return newError("native function %q second return value is not an error", name)
+		}
+	default:
+		return newError("native function %q returns more than two values", name)
+	}
+	return nil
+}
+
+// Return true if typ is a valid parameter or return type.
+func validNativeType(typ reflect.Type) bool {
+	switch typ.Kind() {
+	case reflect.Bool:
+		return true
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return true
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return true
+	case reflect.Float32, reflect.Float64:
+		return true
+	case reflect.String:
+		return true
+	case reflect.Slice:
+		// Only allow []byte (convert to string in AWK)
+		return typ.Elem().Kind() == reflect.Uint8
+	default:
+		return false
+	}
+}
+
 // Guts of the split() function
 func (p *interp) split(s string, scope VarScope, index int, fs string) (int, error) {
 	var parts []string
diff --git a/interp/interp.go b/interp/interp.go
index 930ddaa..d4a02b2 100644
--- a/interp/interp.go
+++ b/interp/interp.go
@@ -82,6 +82,7 @@ type interp struct {
 	arrays      []map[string]value
 	localArrays [][]int
 	callDepth   int
+	nativeFuncs []nativeFunc
 
 	// File, line, and field handling
 	filename    string
@@ -152,6 +153,30 @@ type Config struct {
 	// the program (useful for setting FS and other built-in
 	// variables, for example []string{"FS", ",", "OFS", ","}).
 	Vars []string
+
+	// Map of named Go functions to allow calling from AWK. You need
+	// to pass this same map to the parser.ParseProgram config.
+	//
+	// Functions can have any number of parameters, and variadic
+	// functions are supported. Functions can have no return values,
+	// one return value, or two return values (result, error). In the
+	// two-value case, if the function returns a non-nil error,
+	// program execution will stop and ExecProgram will return that
+	// error.
+	//
+	// Apart from the error return value, the types supported are
+	// bool, integer and floating point types (excluding complex),
+	// and string types (string or []byte).
+	//
+	// It's not an error to call a Go function from AWK with fewer
+	// arguments than it has parameters in Go. In this case, the zero
+	// value will be used for any additional parameters. However, it
+	// is a parse error to call a non-variadic function from AWK with
+	// more arguments than it has parameters in Go.
+	//
+	// Functions defined with the "function" keyword in AWK code
+	// take precedence over functions in Funcs.
+	Funcs map[string]interface{}
 }
 
 // ExecProgram executes the parsed program using the given interpreter
@@ -186,6 +211,10 @@ func ExecProgram(program *Program, config *Config) (int, error) {
 	p.outputFieldSep = " "
 	p.outputRecordSep = "\n"
 	p.subscriptSep = "\x1c"
+	err := p.initNativeFuncs(config.Funcs)
+	if err != nil {
+		return 0, err
+	}
 
 	// Setup ARGV and other variables from config
 	argvIndex := program.Arrays["ARGV"]
@@ -225,7 +254,7 @@ func ExecProgram(program *Program, config *Config) (int, error) {
 	defer p.closeAll()
 
 	// Execute the program! BEGIN, then pattern/actions, then END
-	err := p.execBeginEnd(program.Begin)
+	err = p.execBeginEnd(program.Begin)
 	if err != nil && err != errExit {
 		return 0, err
 	}
@@ -755,8 +784,12 @@ func (p *interp) eval(expr Expr) (value, error) {
 		return boolean(ok), nil
 
 	case *UserCallExpr:
-		// Call user-defined function
-		return p.callUser(e.Index, e.Args)
+		// Call user-defined or native Go function
+		if e.Native {
+			return p.callNative(e.Index, e.Args)
+		} else {
+			return p.callUser(e.Index, e.Args)
+		}
 
 	case *GetlineExpr:
 		// Getline: read line from input
diff --git a/lexer/lexer.go b/lexer/lexer.go
index 3ee995e..e18ea8b 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -106,8 +106,8 @@ func (l *Lexer) scan() (Position, Token, string) {
 			l.next()
 		}
 		name := string(l.src[start : l.offset-1])
-		tok, isKeyword := keywordTokens[name]
-		if !isKeyword {
+		tok := KeywordToken(name)
+		if tok == ILLEGAL {
 			tok = NAME
 			val = name
 		}
diff --git a/lexer/token.go b/lexer/token.go
index f77a315..f1ec804 100644
--- a/lexer/token.go
+++ b/lexer/token.go
@@ -149,6 +149,12 @@ var keywordTokens = map[string]Token{
 	"toupper": F_TOUPPER,
 }
 
+// KeywordToken returns the token associated with the given keyword
+// string, or ILLEGAL if given name is not a keyword.
+func KeywordToken(name string) Token {
+	return keywordTokens[name]
+}
+
 var tokenNames = map[Token]string{
 	ILLEGAL: "<illegal>",
 	EOF:     "EOF",
diff --git a/parser/parser.go b/parser/parser.go
index c5940b3..537ce68 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -36,8 +36,13 @@ func (e *ParseError) Error() string {
 type ParserConfig struct {
 	// Enable printing of type information
 	DebugTypes bool
+
 	// io.Writer to print type information on (for example, os.Stderr)
 	DebugWriter io.Writer
+
+	// Map of named Go functions to allow calling from AWK. See docs
+	// on interp.Config.Funcs for details.
+	Funcs map[string]interface{}
 }
 
 // ParseProgram parses an entire AWK program, returning the *Program
@@ -59,6 +64,7 @@ func ParseProgram(src []byte, config *ParserConfig) (prog *Program, err error) {
 	if config != nil {
 		p.debugTypes = config.DebugTypes
 		p.debugWriter = config.DebugWriter
+		p.nativeFuncs = config.Funcs
 	}
 	p.initResolve()
 	p.next() // initialize p.tok
@@ -115,8 +121,9 @@ type parser struct {
 	multiExprs map[*MultiExpr]Position        // tracks comma-separated expressions
 
 	// Function tracking
-	functions map[string]int // map of function name to index
-	userCalls []userCall     // record calls so we can resolve them later
+	functions   map[string]int // map of function name to index
+	userCalls   []userCall     // record calls so we can resolve them later
+	nativeFuncs map[string]interface{}
 
 	// Configuration and debugging
 	debugTypes  bool      // show variable types for debugging
@@ -967,7 +974,7 @@ func (p *parser) userCall(name string, pos Position) *UserCallExpr {
 		i++
 	}
 	p.expect(RPAREN)
-	call := &UserCallExpr{-1, name, args} // index is resolved later
+	call := &UserCallExpr{false, -1, name, args} // index is resolved later
 	p.recordUserCall(call, pos)
 	return call
 }
diff --git a/parser/resolve.go b/parser/resolve.go
index 601135e..3b01e2c 100644
--- a/parser/resolve.go
+++ b/parser/resolve.go
@@ -4,6 +4,7 @@ package parser
 
 import (
 	"fmt"
+	"reflect"
 	"sort"
 
 	. "github.com/benhoyt/goawk/internal/ast"
@@ -116,10 +117,32 @@ func (p *parser) recordUserCall(call *UserCallExpr, pos Position) {
 // ensures functions called have actually been defined, and that
 // they're not being called with too many arguments.
 func (p *parser) resolveUserCalls(prog *Program) {
+	// Number the native funcs (order by name to get consistent order)
+	nativeNames := make([]string, 0, len(p.nativeFuncs))
+	for name := range p.nativeFuncs {
+		nativeNames = append(nativeNames, name)
+	}
+	sort.Strings(nativeNames)
+	nativeIndexes := make(map[string]int, len(nativeNames))
+	for i, name := range nativeNames {
+		nativeIndexes[name] = i
+	}
+
 	for _, c := range p.userCalls {
+		// AWK-defined functions take precedence over native Go funcs
 		index, ok := p.functions[c.call.Name]
 		if !ok {
-			panic(&ParseError{c.pos, fmt.Sprintf("undefined function %q", c.call.Name)})
+			f, haveNative := p.nativeFuncs[c.call.Name]
+			if !haveNative {
+				panic(&ParseError{c.pos, fmt.Sprintf("undefined function %q", c.call.Name)})
+			}
+			typ := reflect.TypeOf(f)
+			if !typ.IsVariadic() && len(c.call.Args) > typ.NumIn() {
+				panic(&ParseError{c.pos, fmt.Sprintf("%q called with more arguments than declared", c.call.Name)})
+			}
+			c.call.Native = true
+			c.call.Index = nativeIndexes[c.call.Name]
+			continue
 		}
 		function := prog.Functions[index]
 		if len(c.call.Args) > len(function.Params) {
@@ -165,9 +188,9 @@ func (p *parser) varRef(name string, pos Position) *VarExpr {
 	scope, funcName := p.getScope(name)
 	expr := &VarExpr{scope, 0, name}
 	p.varRefs = append(p.varRefs, varRef{funcName, expr, false, pos})
-	typ := p.varTypes[funcName][name].typ
-	if typ == typeUnknown {
-		p.varTypes[funcName][name] = typeInfo{typeScalar, expr, scope, 0, "", 0}
+	info := p.varTypes[funcName][name]
+	if info.typ == typeUnknown {
+		p.varTypes[funcName][name] = typeInfo{typeScalar, expr, scope, 0, info.callName, 0}
 	}
 	return expr
 }
@@ -181,9 +204,9 @@ func (p *parser) arrayRef(name string, pos Position) *ArrayExpr {
 	}
 	expr := &ArrayExpr{scope, 0, name}
 	p.arrayRefs = append(p.arrayRefs, arrayRef{funcName, expr, pos})
-	typ := p.varTypes[funcName][name].typ
-	if typ == typeUnknown {
-		p.varTypes[funcName][name] = typeInfo{typeArray, nil, scope, 0, "", 0}
+	info := p.varTypes[funcName][name]
+	if info.typ == typeUnknown {
+		p.varTypes[funcName][name] = typeInfo{typeArray, nil, scope, 0, info.callName, 0}
 	}
 	return expr
 }
@@ -311,6 +334,25 @@ func (p *parser) resolveVars(prog *Program) {
 
 	// Check that variables passed to functions are the correct type
 	for _, c := range p.userCalls {
+		// Check native function calls
+		if c.call.Native {
+			for _, arg := range c.call.Args {
+				varExpr, ok := arg.(*VarExpr)
+				if !ok {
+					// Non-variable expression, must be scalar
+					continue
+				}
+				funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
+				info := p.varTypes[funcName][varExpr.Name]
+				if info.typ == typeArray {
+					message := fmt.Sprintf("can't pass array %q to native function", varExpr.Name)
+					panic(&ParseError{c.pos, message})
+				}
+			}
+			continue
+		}
+
+		// Check AWK function calls
 		function := prog.Functions[c.call.Index]
 		for i, arg := range c.call.Args {
 			varExpr, ok := arg.(*VarExpr)
@@ -321,15 +363,7 @@ func (p *parser) resolveVars(prog *Program) {
 				}
 				continue
 			}
-			// If this VarExpr refers to a local, use caller name to
-			// look up in varTypes, otherwise use "" (meaning global).
-			funcName := ""
-			for _, p := range prog.Functions[p.functions[c.inFunc]].Params {
-				if varExpr.Name == p {
-					funcName = c.inFunc
-					break
-				}
-			}
+			funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
 			info := p.varTypes[funcName][varExpr.Name]
 			if info.typ == typeArray && !function.Arrays[i] {
 				message := fmt.Sprintf("can't pass array %q as scalar param", varExpr.Name)
@@ -366,6 +400,20 @@ func (p *parser) resolveVars(prog *Program) {
 	}
 }
 
+// If name refers to a local (in function inFunc), return that
+// function's name, otherwise return "" (meaning global).
+func (p *parser) getVarFuncName(prog *Program, name, inFunc string) string {
+	if inFunc == "" {
+		return ""
+	}
+	for _, param := range prog.Functions[p.functions[inFunc]].Params {
+		if name == param {
+			return inFunc
+		}
+	}
+	return ""
+}
+
 func (p *parser) multiExpr(exprs []Expr, pos Position) Expr {
 	expr := &MultiExpr{exprs}
 	p.multiExprs[expr] = pos


Test patch:
diff --git a/interp/example_test.go b/interp/example_test.go
index 0583dd7..3e8b10b 100644
--- a/interp/example_test.go
+++ b/interp/example_test.go
@@ -8,12 +8,13 @@ package interp_test
 import (
 	"bytes"
 	"fmt"
+	"strings"
 
 	"github.com/benhoyt/goawk/interp"
 	"github.com/benhoyt/goawk/parser"
 )
 
-func Example_simple() {
+func Example() {
 	input := bytes.NewReader([]byte("foo bar\n\nbaz buz"))
 	err := interp.Exec("$0 { print $1 }", " ", input, nil)
 	if err != nil {
@@ -61,3 +62,35 @@ func Example_program() {
 	// 2:ab
 	// 3:abc
 }
+
+func Example_funcs() {
+	src := `BEGIN { print sum(), sum(1), sum(2, 3, 4), repeat("xyz", 3) }`
+
+	parserConfig := &parser.ParserConfig{
+		Funcs: map[string]interface{}{
+			"sum": func(args ...float64) float64 {
+				sum := 0.0
+				for _, a := range args {
+					sum += a
+				}
+				return sum
+			},
+			"repeat": strings.Repeat,
+		},
+	}
+	prog, err := parser.ParseProgram([]byte(src), parserConfig)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	interpConfig := &interp.Config{
+		Funcs: parserConfig.Funcs,
+	}
+	_, err = interp.ExecProgram(prog, interpConfig)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	// Output:
+	// 0 1 9 xyzxyzxyz
+}
diff --git a/interp/interp_test.go b/interp/interp_test.go
index cb0fecb..e355fb5 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -596,44 +596,274 @@ BEGIN { early() }
 
 		// Then test it in GoAWK
 		t.Run(testName, func(t *testing.T) {
-			prog, err := parser.ParseProgram([]byte(test.src), nil)
-			if err != nil {
-				if test.err != "" {
-					if err.Error() == test.err {
-						return
-					}
-					t.Fatalf("expected error %q, got %q", test.err, err.Error())
-				}
-				t.Fatal(err)
+			testGoAWK(t, test.src, test.in, test.out, test.err, nil)
+		})
+	}
+	_ = os.Remove("out")
+}
+
+func testGoAWK(t *testing.T, src, in, out, errStr string, funcs map[string]interface{}) {
+	parserConfig := &parser.ParserConfig{
+		Funcs: funcs,
+	}
+	prog, err := parser.ParseProgram([]byte(src), parserConfig)
+	if err != nil {
+		if errStr != "" {
+			if err.Error() == errStr {
+				return
 			}
-			outBuf := &bytes.Buffer{}
-			errBuf := &bytes.Buffer{}
-			config := &interp.Config{
-				Stdin:  strings.NewReader(test.in),
-				Output: outBuf,
-				Error:  errBuf,
+			t.Fatalf("expected error %q, got %q", errStr, err.Error())
+		}
+		t.Fatal(err)
+	}
+	outBuf := &bytes.Buffer{}
+	errBuf := &bytes.Buffer{}
+	config := &interp.Config{
+		Stdin:  strings.NewReader(in),
+		Output: outBuf,
+		Error:  errBuf,
+		Funcs:  funcs,
+	}
+	_, err = interp.ExecProgram(prog, config)
+	if err != nil {
+		if errStr != "" {
+			if err.Error() == errStr {
+				return
 			}
-			_, err = interp.ExecProgram(prog, config)
-			if err != nil {
-				if test.err != "" {
-					if err.Error() == test.err {
-						return
+			t.Fatalf("expected error %q, got %q", errStr, err.Error())
+		}
+		t.Fatal(err)
+	}
+	if errStr != "" {
+		t.Fatalf(`expected error %q, got ""`, errStr)
+	}
+	normalized := normalizeNewlines(outBuf.String() + errBuf.String())
+	if normalized != out {
+		t.Fatalf("expected %q, got %q", out, normalized)
+	}
+}
+
+func TestNative(t *testing.T) {
+	tests := []struct {
+		src   string
+		in    string
+		out   string
+		err   string
+		funcs map[string]interface{}
+	}{
+		{`BEGIN { print foo() }`, "", "", `parse error at 1:15: undefined function "foo"`,
+			nil},
+		{`BEGIN { print foo() }`, "", "\n", "",
+			map[string]interface{}{
+				"foo": func() {},
+			}},
+		{`BEGIN { print foo() }`, "", "FOO\n", "",
+			map[string]interface{}{
+				"foo": func() string { return "FOO" },
+			}},
+		{`BEGIN { print foo() }`, "", "BYTES\n", "",
+			map[string]interface{}{
+				"foo": func() []byte { return []byte("BYTES") },
+			}},
+		{`BEGIN { print repeat("xy", 5) }`, "", "xyxyxyxyxy\n", "",
+			map[string]interface{}{
+				"repeat": func(s string, n int) string { return strings.Repeat(s, n) },
+			}},
+		{`BEGIN { print repeat("xy", 5) }`, "", "xyxyxyxyxy\n", "",
+			map[string]interface{}{
+				"repeat": func(s string, n int) string { return strings.Repeat(s, n) },
+			}},
+		{`
+BEGIN {
+	print r0()
+	print r1(), r1(5)
+	print r2(), r2(5)
+}`, "", "\n0 25\n0 25\n", "",
+			map[string]interface{}{
+				"r0": func() {},
+				"r1": func(n int) int { return n * n },
+				"r2": func(n int) (int, error) {
+					return n * n, nil
+				},
+			}},
+		{`
+BEGIN {
+	print r2()
+}`, "", "", "NATIVE ERROR",
+			map[string]interface{}{
+				"r2": func(n int) (int, error) {
+					return n * n, fmt.Errorf("NATIVE ERROR")
+				},
+			}},
+		{`
+BEGIN {
+	print
+	print bool(), bool(0), bool(1), bool(""), bool("0"), bool("x")
+	print i(), i(42), i(-5), i(3.75), i(-3.75)
+	print i8(), i8(42), i8(-5.6), i8(127), i8(128), i8(255), i8(256)
+	print i16(), i16(42), i16(-5.6), i16(32767), i16(32768), i16(65535), i16(65536)
+	print i32(), i32(42), i32(-5.6), i32(2147483647), i32(2147483648), i32(4294967295), i32(4294967296)
+	print i64(), i64(42), i64(-5.6), i64(2147483647000), i64(-2147483647000)
+	print u(), u(42), u(-1)
+	print u8(), u8(42), u8(-5.6), u8(127), u8(128), u8(255), u8(256)
+	print u16(), u16(42), u16(-1), u16(65535), u16(65536)
+	print u32(), u32(42), u32(-1), u32(4294967295), u32(4294967296)
+	print u64(), u64(42), u64(-1), u64(4294967296), u64(2147483647000)
+	print s() "." s("") "." s("Foo bar") "." s(1234)
+	print b() "." b("") "." b("Foo bar") "." b(1234)
+}`, "", `
+0 0 1 0 1 1
+0 42 -5 3 -3
+0 42 -5 127 -128 -1 0
+0 42 -5 32767 -32768 -1 0
+0 42 -5 2147483647 -2147483648 -2147483648 -2147483648
+0 42 -5 2147483647000 -2147483647000
+0 42 1.84467e+19
+0 42 251 127 128 255 0
+0 42 65535 65535 0
+0 42 4294967295 4294967295 0
+0 42 1.84467e+19 4294967296 2147483647000
+..Foo bar.1234
+..Foo bar.1234
+`, "",
+			map[string]interface{}{
+				"bool": func(b bool) bool { return b },
+				"i":    func(n int) int { return n },
+				"i8":   func(n int8) int8 { return n },
+				"i16":  func(n int16) int16 { return n },
+				"i32":  func(n int32) int32 { return n },
+				"i64":  func(n int64) int64 { return n },
+				"u":    func(n uint) uint { return n },
+				"u8":   func(n uint8) uint8 { return n },
+				"u16":  func(n uint16) uint16 { return n },
+				"u32":  func(n uint32) uint32 { return n },
+				"u64":  func(n uint64) uint64 { return n },
+				"b":    func(b []byte) []byte { return b },
+				"s":    func(s string) string { return s },
+			}},
+		{`
+BEGIN {
+	print
+	print sum(), sum(1), sum(2, 3), sum(4, 5, 6, 7, 8)
+	print fmt_ints()
+	print fmt_ints("%5d")
+	print fmt_ints("%5d", 123)
+	print fmt_ints("%d %d", 123, 456)
+	print fmt_ints("%d %d %d", 123, 456, 789)
+}`, "", `
+0 1 5 30
+
+%!d(MISSING)
+  123
+123 456
+123 456 789
+`, "",
+			map[string]interface{}{
+				"sum": func(args ...int) int {
+					sum := 0
+					for _, a := range args {
+						sum += a
 					}
-					t.Fatalf("expected error %q, got %q", test.err, err.Error())
-				}
-				t.Fatal(err)
-			}
-			if test.err != "" {
-				t.Fatalf(`expected error %q, got ""`, test.err)
-			}
-			out := outBuf.String() + errBuf.String()
-			normalized := normalizeNewlines(out)
-			if normalized != test.out {
-				t.Fatalf("expected %q, got %q", test.out, normalized)
-			}
+					return sum
+				},
+				"fmt_ints": func(s string, args ...int) string {
+					fmtArgs := make([]interface{}, len(args))
+					for i, a := range args {
+						fmtArgs[i] = a
+					}
+					return fmt.Sprintf(s, fmtArgs...)
+				},
+			}},
+		{`BEGIN { 0 }`, "", "", `native function "f" is not a function`,
+			map[string]interface{}{
+				"f": 0,
+			}},
+		{`BEGIN { 1 }`, "", "", `native function "g" param 0 is not int or string`,
+			map[string]interface{}{
+				"g": func(s complex64) {},
+			}},
+		{`BEGIN { 2 }`, "", "", `native function "g" param 2 is not int or string`,
+			map[string]interface{}{
+				"g": func(x, y int, s []int, t string) {},
+			}},
+		{`BEGIN { 3 }`, "", "", `native function "h" param 0 is not int or string`,
+			map[string]interface{}{
+				"h": func(a ...map[string]int) {},
+			}},
+		{`BEGIN { 4 }`, "", "", `native function "h" param 1 is not int or string`,
+			map[string]interface{}{
+				"h": func(x int, a ...complex64) {},
+			}},
+		{`BEGIN { 5 }`, "", "", `native function "r" return value is not int or string`,
+			map[string]interface{}{
+				"r": func() map[string]int { return nil },
+			}},
+		{`BEGIN { 6 }`, "", "", `native function "r" first return value is not int or string`,
+			map[string]interface{}{
+				"r": func() (map[string]int, error) { return nil, nil },
+			}},
+		{`BEGIN { 7 }`, "", "", `native function "r" second return value is not an error`,
+			map[string]interface{}{
+				"r": func() (int, int) { return 0, 0 },
+			}},
+		{`BEGIN { 8 }`, "", "", `native function "r" returns more than two values`,
+			map[string]interface{}{
+				"r": func() (int, error, int) { return 0, nil, 0 },
+			}},
+		{`BEGIN { print f(), f(1, 2) }`, "", "", `parse error at 1:20: "f" called with more arguments than declared`,
+			map[string]interface{}{
+				"f": func(n int) {},
+			}},
+		{`BEGIN { print split("x y", a) }`, "", "", `can't use keyword "split" as native function name`,
+			map[string]interface{}{
+				"split": func() {},
+			}},
+		{`
+function foo(n) { return n * 2 }
+BEGIN { print foo(42) }
+`, "", "84\n", "", map[string]interface{}{
+			"foo": func(n int) int { return n / 2 },
+		}},
+		{`BEGIN { x=3; print foo(x) }`, "", "9\n", ``,
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+		{`
+function bar(n) { return foo(n) }
+BEGIN { x=4; y=5; print foo(x), bar(y) }
+`, "", "16 25\n", ``,
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+		{`BEGIN { a["x"]=1; print foo(a) }`, "", "",
+			`parse error at 1:25: can't pass array "a" to native function`,
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+		{`BEGIN { x["x"]=1; print f(x) }  function f(a) { return foo(a) }`, "", "",
+			`parse error at 1:25: can't pass array "x" as scalar param`,
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+		{`function f(a) { return foo(a) }  BEGIN { x["x"]=1; print f(x) }`, "", "",
+			`parse error at 1:58: can't pass array "x" as scalar param`,
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+		{`BEGIN { x["x"]=1; print f(x["x"]) }  function f(a) { return foo(a) }`, "", "1\n", "",
+			map[string]interface{}{
+				"foo": func(n int) int { return n * n },
+			}},
+	}
+	for _, test := range tests {
+		testName := test.src
+		if len(testName) > 70 {
+			testName = testName[:70]
+		}
+		t.Run(testName, func(t *testing.T) {
+			testGoAWK(t, test.src, test.in, test.out, test.err, test.funcs)
 		})
 	}
-	_ = os.Remove("out")
 }
 
 func TestConfigVarsCorrect(t *testing.T) {
@@ -654,10 +884,15 @@ func TestConfigVarsCorrect(t *testing.T) {
 	}
 }
 
-func benchmarkProgram(b *testing.B, input, expected, srcFormat string, args ...interface{}) {
+func benchmarkProgram(b *testing.B, funcs map[string]interface{},
+	input, expected, srcFormat string, args ...interface{},
+) {
 	b.StopTimer()
 	src := fmt.Sprintf(srcFormat, args...)
-	prog, err := parser.ParseProgram([]byte(src), nil)
+	parserConfig := &parser.ParserConfig{
+		Funcs: funcs,
+	}
+	prog, err := parser.ParseProgram([]byte(src), parserConfig)
 	if err != nil {
 		b.Fatalf("error parsing %s: %v", b.Name(), err)
 	}
@@ -666,6 +901,7 @@ func benchmarkProgram(b *testing.B, input, expected, srcFormat string, args ...i
 		Stdin:  strings.NewReader(input),
 		Output: outBuf,
 		Error:  ioutil.Discard,
+		Funcs:  funcs,
 	}
 	b.StartTimer()
 	_, err = interp.ExecProgram(prog, config)
@@ -683,7 +919,7 @@ func benchmarkProgram(b *testing.B, input, expected, srcFormat string, args ...i
 }
 
 func BenchmarkGlobalVars(b *testing.B) {
-	benchmarkProgram(b, "", "a 1", `
+	benchmarkProgram(b, nil, "", "a 1", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	x = 1; y = "a"; t = x; x = y; y = t
@@ -698,7 +934,7 @@ BEGIN {
 }
 
 func BenchmarkLocalVars(b *testing.B) {
-	benchmarkProgram(b, "", "b 2", `
+	benchmarkProgram(b, nil, "", "b 2", `
 function f(i, x, y, t) {
   for (i = 0; i < %d; i++) {
   	x = 2; y = "b"; t = x; x = y; y = t
@@ -717,7 +953,7 @@ BEGIN {
 }
 
 func BenchmarkIncrDecr(b *testing.B) {
-	benchmarkProgram(b, "", "0 10", `
+	benchmarkProgram(b, nil, "", "0 10", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	x++; x++; x++; x++; x++; x++; x++; x++; x++; x++
@@ -730,7 +966,7 @@ BEGIN {
 }
 
 func BenchmarkSimpleBuiltins(b *testing.B) {
-	benchmarkProgram(b, "", "", `
+	benchmarkProgram(b, nil, "", "", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	sin(0); cos(0); exp(0); log(1); sqrt(2); int("x");
@@ -744,7 +980,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinMatch(b *testing.B) {
-	benchmarkProgram(b, "", "21", `
+	benchmarkProgram(b, nil, "", "21", `
 BEGIN {
   s = "The quick brown fox jumps over the lazy dog"
   for (i = 0; i < %d; i++) {
@@ -760,7 +996,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinLength(b *testing.B) {
-	benchmarkProgram(b, "", "134", `
+	benchmarkProgram(b, nil, "", "134", `
 BEGIN {
   s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
   for (i = 0; i < %d; i++) {
@@ -776,7 +1012,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinIndex(b *testing.B) {
-	benchmarkProgram(b, "", "134", `
+	benchmarkProgram(b, nil, "", "134", `
 BEGIN {
   s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog!?!"
   for (i = 0; i < %d; i++) {
@@ -792,7 +1028,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinSubstr(b *testing.B) {
-	benchmarkProgram(b, "", " brown fox", `
+	benchmarkProgram(b, nil, "", " brown fox", `
 BEGIN {
   s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog!?!"
   for (i = 0; i < %d; i++) {
@@ -808,7 +1044,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinSplitSpace(b *testing.B) {
-	benchmarkProgram(b, "", "27", `
+	benchmarkProgram(b, nil, "", "27", `
 BEGIN {
   s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog!?!"
   for (i = 0; i < %d; i++) {
@@ -825,7 +1061,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinSplitRegex(b *testing.B) {
-	benchmarkProgram(b, "", "22", `
+	benchmarkProgram(b, nil, "", "22", `
 BEGIN {
   s = "a fox ab fax abc fix a fox ab fax abc fix a fox ab fax abc fix a fox ab fax abc fix a fox ab fax abc fix a fox ab fax abc fix a fox ab fax abc fix"
   for (i = 0; i < %d; i++) {
@@ -842,7 +1078,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinSub(b *testing.B) {
-	benchmarkProgram(b, "", "1 164", `
+	benchmarkProgram(b, nil, "", "1 164", `
 BEGIN {
   for (i = 0; i < %d; i++) {
     s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
@@ -858,7 +1094,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinSubAmpersand(b *testing.B) {
-	benchmarkProgram(b, "", "1 164", `
+	benchmarkProgram(b, nil, "", "1 164", `
 BEGIN {
   for (i = 0; i < %d; i++) {
     s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
@@ -874,7 +1110,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinGsub(b *testing.B) {
-	benchmarkProgram(b, "", "3 224", `
+	benchmarkProgram(b, nil, "", "3 224", `
 BEGIN {
   for (i = 0; i < %d; i++) {
     s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
@@ -890,7 +1126,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinGsubAmpersand(b *testing.B) {
-	benchmarkProgram(b, "", "3 224", `
+	benchmarkProgram(b, nil, "", "3 224", `
 BEGIN {
   for (i = 0; i < %d; i++) {
     s = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog."
@@ -906,7 +1142,7 @@ BEGIN {
 }
 
 func BenchmarkBuiltinSprintf(b *testing.B) {
-	benchmarkProgram(b, "", "A 123   foo 3.14", `
+	benchmarkProgram(b, nil, "", "A 123   foo 3.14", `
 BEGIN {
   x = "foo"
   y = 3.14159
@@ -923,7 +1159,7 @@ BEGIN {
 }
 
 func BenchmarkRecursiveFunc(b *testing.B) {
-	benchmarkProgram(b, "", "55", `
+	benchmarkProgram(b, nil, "", "55", `
 function fib(n) {
   if (n <= 2) {
     return 1
@@ -941,7 +1177,7 @@ BEGIN {
 }
 
 func BenchmarkFuncCall(b *testing.B) {
-	benchmarkProgram(b, "", "75", `
+	benchmarkProgram(b, nil, "", "75", `
 function add(a, b) {
   return a + b
 }
@@ -959,8 +1195,26 @@ BEGIN {
 `, b.N)
 }
 
+func BenchmarkNativeFunc(b *testing.B) {
+	funcs := map[string]interface{}{
+		"add": func(a, b float64) float64 { return a + b },
+	}
+	benchmarkProgram(b, funcs, "", "75", `
+BEGIN {
+  for (i = 0; i < %d; i++) {
+    sum = add(0, add(1, add(2, add(3, add(4, 5)))))
+    sum = add(sum, add(1, add(2, add(3, add(4, 5)))))
+    sum = add(sum, add(1, add(2, add(3, add(4, 5)))))
+    sum = add(sum, add(1, add(2, add(3, add(4, 5)))))
+    sum = add(sum, add(1, add(2, add(3, add(4, 5)))))
+  }
+  print sum
+}
+`, b.N)
+}
+
 func BenchmarkForLoop(b *testing.B) {
-	benchmarkProgram(b, "", "", `
+	benchmarkProgram(b, nil, "", "", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	for (j = 0; j < 100; j++);
@@ -970,7 +1224,7 @@ BEGIN {
 }
 
 func BenchmarkForInLoop(b *testing.B) {
-	benchmarkProgram(b, "", "", `
+	benchmarkProgram(b, nil, "", "", `
 BEGIN {
   for (j = 0; j < 100; j++) {
   	a[j] = j
@@ -983,7 +1237,7 @@ BEGIN {
 }
 
 func BenchmarkIfStatement(b *testing.B) {
-	benchmarkProgram(b, "", "0", `
+	benchmarkProgram(b, nil, "", "0", `
 BEGIN {
   c = 1
   d = 0
@@ -1001,7 +1255,7 @@ BEGIN {
 }
 
 func BenchmarkCondExpr(b *testing.B) {
-	benchmarkProgram(b, "", "0", `
+	benchmarkProgram(b, nil, "", "0", `
 BEGIN {
   c = 1
   d = 0
@@ -1033,7 +1287,7 @@ func BenchmarkSimplePattern(b *testing.B) {
 	}
 	input := strings.Join(inputLines, "\n")
 	expected := strings.Join(expectedLines, "\n")
-	benchmarkProgram(b, input, expected, "$0")
+	benchmarkProgram(b, nil, input, expected, "$0")
 }
 
 func BenchmarkGetField(b *testing.B) {
@@ -1046,11 +1300,11 @@ func BenchmarkGetField(b *testing.B) {
 	}
 	input := strings.Join(inputLines, "\n")
 	expected := strings.Join(expectedLines, "\n")
-	benchmarkProgram(b, input, expected, "{ print $1, $3 }")
+	benchmarkProgram(b, nil, input, expected, "{ print $1, $3 }")
 }
 
 func BenchmarkSetField(b *testing.B) {
-	benchmarkProgram(b, "1 2 3", "one 2 three", `
+	benchmarkProgram(b, nil, "1 2 3", "one 2 three", `
 {
   for (i = 0; i < %d; i++) {
     $1 = "one"; $3 = "three"
@@ -1067,7 +1321,7 @@ END {
 }
 
 func BenchmarkRegexMatch(b *testing.B) {
-	benchmarkProgram(b, "", "1", `
+	benchmarkProgram(b, nil, "", "1", `
 BEGIN {
   s = "The quick brown fox jumps over the lazy dog"
   for (i = 0; i < %d; i++) {
@@ -1083,7 +1337,7 @@ BEGIN {
 }
 
 func BenchmarkBinaryOperators(b *testing.B) {
-	benchmarkProgram(b, "", "5.0293", `
+	benchmarkProgram(b, nil, "", "5.0293", `
 BEGIN {
   for (i = 0; i < %d; i++) {
     res = (1+2*3/4^5) + (1+2*3/4^5) + (1+2*3/4^5) + (1+2*3/4^5) + (1+2*3/4^5)
@@ -1095,7 +1349,7 @@ BEGIN {
 
 func BenchmarkConcatSmall(b *testing.B) {
 	b.StopTimer()
-	benchmarkProgram(b, "", "100", `
+	benchmarkProgram(b, nil, "", "100", `
 BEGIN {
   x = "0123456789"
   for (i = 0; i < %d; i++) {
@@ -1108,7 +1362,7 @@ BEGIN {
 
 func BenchmarkConcatLarge(b *testing.B) {
 	b.StopTimer()
-	benchmarkProgram(b, "", "1000000", `
+	benchmarkProgram(b, nil, "", "1000000", `
 BEGIN {
   x = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
   for (i = 0; i < %d; i++) {
@@ -1124,7 +1378,7 @@ BEGIN {
 
 func BenchmarkComparisons(b *testing.B) {
 	b.StopTimer()
-	benchmarkProgram(b, "", "1", `
+	benchmarkProgram(b, nil, "", "1", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	x = ((((((1 < 2) <= 3) > 4) >= 5) == 6) != 7)
@@ -1138,7 +1392,7 @@ BEGIN {
 
 func BenchmarkArrayOperations(b *testing.B) {
 	b.StopTimer()
-	benchmarkProgram(b, "", "243", `
+	benchmarkProgram(b, nil, "", "243", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	a[0] = 1
@@ -1155,7 +1409,7 @@ BEGIN {
 
 func BenchmarkAssign(b *testing.B) {
 	b.StopTimer()
-	benchmarkProgram(b, "", "0 1 2 3 4", `
+	benchmarkProgram(b, nil, "", "0 1 2 3 4", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	v=0; w=1; x=2; y=3; z=4
@@ -1171,7 +1425,7 @@ BEGIN {
 
 func BenchmarkAugAssign(b *testing.B) {
 	b.StopTimer()
-	benchmarkProgram(b, "", "5 -9 729 32 3.0536 2", `
+	benchmarkProgram(b, nil, "", "5 -9 729 32 3.0536 2", `
 BEGIN {
   for (i = 0; i < %d; i++) {
   	a = 0; b = 1; c = 3; d = 1024; e = 2; f = 14
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index e7e2908..00efc6e 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -148,6 +148,27 @@ func TestHadSpace(t *testing.T) {
 	}
 }
 
+func TestKeywordToken(t *testing.T) {
+	tests := []struct {
+		name string
+		tok  Token
+	}{
+		{"print", PRINT},
+		{"split", F_SPLIT},
+		{"BEGIN", BEGIN},
+		{"foo", ILLEGAL},
+		{"GoAWK", ILLEGAL},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			tok := KeywordToken(test.name)
+			if tok != test.tok {
+				t.Errorf("expected %v, got %v", test.tok, tok)
+			}
+		})
+	}
+}
+
 func TestAllTokens(t *testing.T) {
 	input := "# comment line\n" +
 		"+ += && = : , -- /\n/= $ == >= > >> ++ { [ < ( #\n" +