diff --git a/.gitignore b/.gitignore index daf913b1b..11eff6993 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.o *.a *.so +.DS_Store # Folders _obj diff --git a/README.md b/README.md index 0c2c2031e..6dc834bf2 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,13 @@ git clone https://github.com/qiniu/text.git qiniupkg.com/text ## 社区资源 +### Embedded qlang (eql) + +* [eql](app/eql): 全称 [embedded qlang](app/eql),是类似 erubis/erb 的东西。结合 go generate 可很方便地让 Go 支持模板(不是 html template,是指泛型)。 + ### 为 Go package 导出 qlang module -* [qexport](app/qexport/README.md): 可为 Go 语言的标准库或者你自己写的 Go Package 自动导出相应的 qlang module。 +* [qexport](app/qexport): 可为 Go 语言的标准库或者你自己写的 Go Package 自动导出相应的 qlang module。 ### Qlang Modules diff --git a/README_QL.md b/README_QL.md index c7e2befa0..f3e929c49 100644 --- a/README_QL.md +++ b/README_QL.md @@ -680,12 +680,11 @@ func (p *Bar) GetX() int { ```go import ( - "reflect" "qlang.io/qlang.spec.v1" ) var Exports = map[string]interface{}{ - "Bar": qlang.NewType(reflect.TypeOf((*foo.Bar)(nil)).Elem()), + "Bar": qlang.StructOf((*foo.Bar)(nil)), } ``` diff --git a/app/eql/README.md b/app/eql/README.md new file mode 100644 index 000000000..20558918f --- /dev/null +++ b/app/eql/README.md @@ -0,0 +1,209 @@ +Embedded qlang (eql) +======== + +eql 全称 embedded qlang,是类似 erubis/erb 的东西。结合 go generate 可很方便地让 Go 支持模板(不是 html template,是指语言特性中的泛型)。 + +## eql 程序 + +命令行: + +``` +eql [-o ] [--key1=val1 --key2=val2 ...] +eql [-o ] [--key1=val1 --key2=val2 ...] +``` + +其中 + +* ``: 要解析的 eql 文件,也就是模板文件。 +* ``: 要解析的 template package,也就是整个目录是一个模板。 +* ``: 要生成的渲染后的文件。如果没有指定则为 stdout。 +* ``: 要生成的渲染后的目标目录。如果没有指定则为对 `` 进行渲染后的值。 +* `--key1=val1 --key2=val2 ...`: 渲染涉及到的模板变量的值。 + +## 样例 + +### 单文件模板 + +* [example.eql](example.eql): 展示 eql 语法的样例,下文将详细介绍。 + +### 目录模板 + +* [$set.v1](example/$set.v1): 以集合类为例展示如何构建一个 template package。 + + +## 语法 + +### 插入 qlang 代码 + +``` +<% + // 在此插入 qlang 代码 +%> +``` + +### 输出 qlang 表达式 + +``` +<%= qlang_expr %> +``` + +你可以理解为这只是插入 qlang 代码的一种简写手法。它等价于: + +``` +<% print(qlang_expr) %> +``` + +### 输出一个变量 + +``` +$var +``` + +你可以理解为这只是插入 qlang 代码的一种简写手法。它等价于: + +``` +<% print(var) %> +``` + +特别地,我们用 `$$` 表示普通字符 `$`。也就是说: + +``` +$$ +``` + +等价于: + +``` +<% print('$') %> +``` + +## 用 eql 实现 Go 对泛型的支持 + +我们举例说明。假设我们现在实现了一个 Go 的模板类,文件名为 `example.eql`,内容如下: + +```go +package eql_test + +import ( + <%= eql.imports(imports) %> + "encoding/binary" +) + +// ----------------------------------------------------------------------------- + +type $module string + +func (p $module) write(out $Writer, b []byte) { + + _, err := out.Write(b) + if err != nil { + panic(err) + } +} + +<% if Writer == "*bytes.Buffer" { %> +func (p $module) flush(out $Writer) { +} +<% } else { %> +func (p $module) flush(out $Writer) { + + err := out.Flush() + if err != nil { + panic(err) + } +} +<% } %> + +// ----------------------------------------------------------------------------- +``` + +这个模板里面,有 3 个模板变量: + +* `imports`: 需要额外引入的 package 列表,用 `,` 分隔。 +* `module`: 模板类的类名。 +* `Writer`: 模板类的用到的参数类型。 + +有了这个模板,我们就可以用如下命令生成具体的类: + +``` +eql example.eql -o example_bytes.go --imports=bytes --module=modbytes --Writer="*bytes.Buffer" +``` + +这会生成 example_bytes.go 文件,内容如下: + +```go +package eql_test + +import ( + "bytes" + "encoding/binary" +) + +// ----------------------------------------------------------------------------- + +type modbytes string + +func (p modbytes) write(out *bytes.Buffer, b []byte) { + + _, err := out.Write(b) + if err != nil { + panic(err) + } +} + +func (p modbytes) flush(out *bytes.Buffer) { +} + +// ----------------------------------------------------------------------------- +``` + +再试试换一个 Writer: + +``` +eql example.eql -o example_bufio.go --imports=bufio --module=modbufio --Writer="*bufio.Writer" +``` + +我们得到 example_bufio.go,内容如下: + +```go +package eql_test + +import ( + "bufio" + "encoding/binary" +) + +// ----------------------------------------------------------------------------- + +type modbufio string + +func (p modbufio) write(out *bufio.Writer, b []byte) { + + _, err := out.Write(b) + if err != nil { + panic(err) + } +} + +func (p modbufio) flush(out *bufio.Writer) { + + err := out.Flush() + if err != nil { + panic(err) + } +} + +// ----------------------------------------------------------------------------- +``` + +### 结合 go generate + +结合 go generate 工具,我们就可以很好地支持 Go 泛型了。 + +例如假设我们在 foo.go 里面引用了 Writer = `*bufio.Writer` 版本的实现,则只需要在 foo.go 文件中插入以下代码: + +```go +//go:generate eql example.eql -o example_bufio.go --imports=bufio --module=module --Writer=*bufio.Writer +``` + +如此,你只需要在 foo.go 所在的目录执行 go generate 就可以生成 example_bufio.go 文件了。 diff --git a/app/eql/eql_main.go b/app/eql/eql_main.go new file mode 100644 index 000000000..e94bf36dd --- /dev/null +++ b/app/eql/eql_main.go @@ -0,0 +1,189 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "qlang.io/exec.v2" + "qlang.io/qlang.v2/interpreter" + "qlang.io/qlang.v2/qlang" + "qlang.io/qlang/eql.v1" + + qall "qlang.io/qlang/qlang.all" +) + +// ----------------------------------------------------------------------------- + +const usage = ` +Usage: + eql [-o ] [--key1=value1 --key2=value2 ...] + eql [-o ] [--key1=value1 --key2=value2 ...] +` + +func main() { + + qall.InitSafe(false) + qlang.Import("", interpreter.Exports) + qlang.SetDumpCode(os.Getenv("QLANG_DUMPCODE")) + + libs := os.Getenv("QLANG_PATH") + if libs == "" { + libs = os.Getenv("HOME") + "/qlang" + } + + lang, err := qlang.New(qlang.InsertSemis) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + lang.SetLibs(libs) + + vars = lang.Context + eql.DefaultVars = vars + + paseFlags() + if source == "" { + fmt.Fprintln(os.Stderr, usage) + return + } + + fi, err := os.Stat(source) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-2) + } + + if fi.IsDir() { + if output == "" { + output = eql.Subst(source, vars) + if output == source { + panic(fmt.Sprintf("source `%s` doesn't have $var", source)) + } + } + global := lang.CopyVars() + genDir(lang, global, source, output) + } else { + genFile(lang, source, output) + } +} + +func genDir(lang *qlang.Qlang, global map[string]interface{}, source, output string) { + + err := os.MkdirAll(output, 0755) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(21) + } + + fis, err := ioutil.ReadDir(source) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(22) + } + + source += "/" + output += "/" + for _, fi := range fis { + name := fi.Name() + if fi.IsDir() { + genDir(lang, global, source+name, output+name) + } else if path.Ext(name) == ".eql" { + lang.ResetVars(global) + newname := name[:len(name)-4] + genFile(lang, source+name, output+newname) + } else { + copyFile(source+name, output+name, fi.Mode()) + } + } +} + +func copyFile(source, output string, perm os.FileMode) { + + b, err := ioutil.ReadFile(source) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(31) + } + + err = ioutil.WriteFile(output, b, perm) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(32) + } +} + +func genFile(lang *qlang.Qlang, source, output string) { + + b, err := ioutil.ReadFile(source) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + code, err := eql.Parse(string(b)) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + + if output != "" { + f, err := os.Create(output) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(3) + } + defer f.Close() + os.Stdout = f + } + + err = lang.SafeExec(code, source) + if err != nil { + os.Remove(output) + fmt.Fprintln(os.Stderr, err) + os.Exit(4) + } +} + +// ----------------------------------------------------------------------------- + +var ( + source string + output string + vars *exec.Context +) + +func paseFlags() { + + vars.SetVar("imports", "") + for i := 1; i < len(os.Args); i++ { + switch arg := os.Args[i]; arg { + case "-o": + if i+1 >= len(os.Args) { + fmt.Fprintln(os.Stderr, "ERROR: switch -o doesn't have parameters, please use -o ") + os.Exit(10) + } + output = os.Args[i+1] + i++ + default: + if strings.HasPrefix(arg, "--") { + kv := arg[2:] + pos := strings.Index(kv, "=") + if pos < 0 { + fmt.Fprintf(os.Stderr, "ERROR: invalid switch `%s`\n", arg) + os.Exit(11) + } + vars.SetVar(kv[:pos], kv[pos+1:]) + } else if arg[0] == '-' { + fmt.Fprintf(os.Stderr, "ERROR: unknown switch `%s`\n", arg) + os.Exit(12) + } else { + source = arg + } + } + } +} + +// ----------------------------------------------------------------------------- diff --git a/app/eql/example.eql b/app/eql/example.eql new file mode 100644 index 000000000..e569f034a --- /dev/null +++ b/app/eql/example.eql @@ -0,0 +1,39 @@ +<% + // + // eql example.eql -o example_bytes.go --imports=bytes --module=modbytes --Writer="*bytes.Buffer" + // eql example.eql -o example_bufio.go --imports=bufio --module=modbufio --Writer="*bufio.Writer" + // +%> +package eql_test + +import ( + <%= eql.imports(imports) %> + "encoding/binary" +) + +// ----------------------------------------------------------------------------- + +type $module string + +func (p $module) write(out $Writer, b []byte) { + + _, err := out.Write(b) + if err != nil { + panic(err) + } +} + +<% if Writer == "*bytes.Buffer" { %> +func (p $module) flush(out $Writer) { +} +<% } else { %> +func (p $module) flush(out $Writer) { + + err := out.Flush() + if err != nil { + panic(err) + } +} +<% } %> + +// ----------------------------------------------------------------------------- diff --git a/app/eql/example/$set.v1/README.md.eql b/app/eql/example/$set.v1/README.md.eql new file mode 100644 index 000000000..4d0884dac --- /dev/null +++ b/app/eql/example/$set.v1/README.md.eql @@ -0,0 +1,37 @@ +<% + // + // eql '$set.v1' --base=qlang.io/app/eql/example --set=uint32set --Set=Type --Item=uint32 + // eql '$set.v1' --base=qlang.io/app/eql/example --set=uintset --Set=Type --Item=uint + // + + if Item == "string" { + NewArgs, AddArgs, HasArgs = `"a", "b"`, `"c"`, `"d"` + } else { + NewArgs, AddArgs, HasArgs = `1, 2`, `3`, `4` + } + + base = eql.var("base", "") // if variable `base` is undefined, let it be "" + if base != "" { + base += "/" + } + package = base + set + ".v1" +%> +$package +====== + +$set.$Set is a set of $Item, implemented via `map[$Item]struct{}` for minimal memory consumption. + +Here is an example: + +```go +import ( + "$package" +) + +set := $set.New($NewArgs) +set.Add($AddArgs) + +if set.Has($HasArgs) { + println("set has item `$HasArgs`") +} +``` diff --git a/app/eql/example/$set.v1/set.go.eql b/app/eql/example/$set.v1/set.go.eql new file mode 100644 index 000000000..70cf1954e --- /dev/null +++ b/app/eql/example/$set.v1/set.go.eql @@ -0,0 +1,67 @@ +package $set + +// --------------------------------------------------------------------------- + +type empty struct{} + +// $Set is a set of $Item, implemented via map[$Item]struct{} for minimal memory consumption. +type $Set map[$Item]empty + +// New creates a $Set from a list of values. +func New(items ...$Item) $Set { + ss := $Set{} + ss.Insert(items...) + return ss +} + +// Add adds one item to the set. +func (s $Set) Add(item $Item) { + s[item] = empty{} +} + +// Insert adds items to the set. +func (s $Set) Insert(items ...$Item) { + for _, item := range items { + s[item] = empty{} + } +} + +// Delete removes all items from the set. +func (s $Set) Delete(items ...$Item) { + for _, item := range items { + delete(s, item) + } +} + +// Has returns true iff item is contained in the set. +func (s $Set) Has(item $Item) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true iff all items are contained in the set. +func (s $Set) HasAll(items ...$Item) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// IsSuperset returns true iff s1 is a superset of s2. +func (s $Set) IsSuperset(s2 $Set) bool { + for item := range s2 { + if !s.Has(item) { + return false + } + } + return true +} + +// Len returns the size of the set. +func (s $Set) Len() int { + return len(s) +} + +// --------------------------------------------------------------------------- diff --git a/app/eql/example/gen-set-types.sh b/app/eql/example/gen-set-types.sh new file mode 100755 index 000000000..625e5d0fa --- /dev/null +++ b/app/eql/example/gen-set-types.sh @@ -0,0 +1,2 @@ +eql '$set.v1' --base=qlang.io/app/eql/example --set=uint32set --Set=Type --Item=uint32 +eql '$set.v1' --base=qlang.io/app/eql/example --set=stringset --Set=Type --Item=string diff --git a/app/eql/example/stringset.v1/README.md b/app/eql/example/stringset.v1/README.md new file mode 100644 index 000000000..b65f7762a --- /dev/null +++ b/app/eql/example/stringset.v1/README.md @@ -0,0 +1,19 @@ +qlang.io/app/eql/example/stringset.v1 +====== + +stringset.Type is a set of string, implemented via `map[string]struct{}` for minimal memory consumption. + +Here is an example: + +```go +import ( + "qlang.io/app/eql/example/stringset.v1" +) + +set := stringset.New("a", "b") +set.Add("c") + +if set.Has("d") { + println("set has item `"d"`") +} +``` diff --git a/app/eql/example/stringset.v1/set.go b/app/eql/example/stringset.v1/set.go new file mode 100644 index 000000000..579f8f50d --- /dev/null +++ b/app/eql/example/stringset.v1/set.go @@ -0,0 +1,67 @@ +package stringset + +// --------------------------------------------------------------------------- + +type empty struct{} + +// Type is a set of string, implemented via map[string]struct{} for minimal memory consumption. +type Type map[string]empty + +// New creates a Type from a list of values. +func New(items ...string) Type { + ss := Type{} + ss.Insert(items...) + return ss +} + +// Add adds one item to the set. +func (s Type) Add(item string) { + s[item] = empty{} +} + +// Insert adds items to the set. +func (s Type) Insert(items ...string) { + for _, item := range items { + s[item] = empty{} + } +} + +// Delete removes all items from the set. +func (s Type) Delete(items ...string) { + for _, item := range items { + delete(s, item) + } +} + +// Has returns true iff item is contained in the set. +func (s Type) Has(item string) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true iff all items are contained in the set. +func (s Type) HasAll(items ...string) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// IsSuperset returns true iff s1 is a superset of s2. +func (s Type) IsSuperset(s2 Type) bool { + for item := range s2 { + if !s.Has(item) { + return false + } + } + return true +} + +// Len returns the size of the set. +func (s Type) Len() int { + return len(s) +} + +// --------------------------------------------------------------------------- diff --git a/app/eql/example/uint32set.v1/README.md b/app/eql/example/uint32set.v1/README.md new file mode 100644 index 000000000..cabdda1dc --- /dev/null +++ b/app/eql/example/uint32set.v1/README.md @@ -0,0 +1,19 @@ +qlang.io/app/eql/example/uint32set.v1 +====== + +uint32set.Type is a set of uint32, implemented via `map[uint32]struct{}` for minimal memory consumption. + +Here is an example: + +```go +import ( + "qlang.io/app/eql/example/uint32set.v1" +) + +set := uint32set.New(1, 2) +set.Add(3) + +if set.Has(4) { + println("set has item `4`") +} +``` diff --git a/app/eql/example/uint32set.v1/set.go b/app/eql/example/uint32set.v1/set.go new file mode 100644 index 000000000..97361d795 --- /dev/null +++ b/app/eql/example/uint32set.v1/set.go @@ -0,0 +1,67 @@ +package uint32set + +// --------------------------------------------------------------------------- + +type empty struct{} + +// Type is a set of uint32, implemented via map[uint32]struct{} for minimal memory consumption. +type Type map[uint32]empty + +// New creates a Type from a list of values. +func New(items ...uint32) Type { + ss := Type{} + ss.Insert(items...) + return ss +} + +// Add adds one item to the set. +func (s Type) Add(item uint32) { + s[item] = empty{} +} + +// Insert adds items to the set. +func (s Type) Insert(items ...uint32) { + for _, item := range items { + s[item] = empty{} + } +} + +// Delete removes all items from the set. +func (s Type) Delete(items ...uint32) { + for _, item := range items { + delete(s, item) + } +} + +// Has returns true iff item is contained in the set. +func (s Type) Has(item uint32) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true iff all items are contained in the set. +func (s Type) HasAll(items ...uint32) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// IsSuperset returns true iff s1 is a superset of s2. +func (s Type) IsSuperset(s2 Type) bool { + for item := range s2 { + if !s.Has(item) { + return false + } + } + return true +} + +// Len returns the size of the set. +func (s Type) Len() int { + return len(s) +} + +// --------------------------------------------------------------------------- diff --git a/app/qexport/README.md b/app/qexport/README.md index 22ec35bc2..8b392b214 100644 --- a/app/qexport/README.md +++ b/app/qexport/README.md @@ -41,11 +41,11 @@ export all package Export pacakge runtime: +* runtime.go ``` go package runtime import ( - "reflect" "runtime" "qlang.io/qlang.spec.v1" @@ -79,24 +79,34 @@ var Exports = map[string]interface{}{ "numCgoCall": runtime.NumCgoCall, "numGoroutine": runtime.NumGoroutine, "readMemStats": runtime.ReadMemStats, - "readTrace": runtime.ReadTrace, "setBlockProfileRate": runtime.SetBlockProfileRate, "setCPUProfileRate": runtime.SetCPUProfileRate, "setFinalizer": runtime.SetFinalizer, "stack": runtime.Stack, - "startTrace": runtime.StartTrace, - "stopTrace": runtime.StopTrace, "threadCreateProfile": runtime.ThreadCreateProfile, "unlockOSThread": runtime.UnlockOSThread, "version": runtime.Version, - "BlockProfileRecord": qlang.NewType(reflect.TypeOf((*runtime.BlockProfileRecord)(nil)).Elem()), - "Func": qlang.NewType(reflect.TypeOf((*runtime.Func)(nil)).Elem()), + "BlockProfileRecord": qlang.StructOf((*runtime.BlockProfileRecord)(nil)), + "Func": qlang.StructOf((*runtime.Func)(nil)), "funcForPC": runtime.FuncForPC, - "MemProfileRecord": qlang.NewType(reflect.TypeOf((*runtime.MemProfileRecord)(nil)).Elem()), - "MemStats": qlang.NewType(reflect.TypeOf((*runtime.MemStats)(nil)).Elem()), - "StackRecord": qlang.NewType(reflect.TypeOf((*runtime.StackRecord)(nil)).Elem()), + "MemProfileRecord": qlang.StructOf((*runtime.MemProfileRecord)(nil)), + "MemStats": qlang.StructOf((*runtime.MemStats)(nil)), + "StackRecord": qlang.StructOf((*runtime.StackRecord)(nil)), } ``` +* runtime-go15.go +``` go +// +build go1.5 + +package runtime +import "runtime" + +func init() { + Exports["readTrace"] = runtime.ReadTrace + Exports["startTrace"] = runtime.StartTrace + Exports["stopTrace"] = runtime.StopTrace +} +``` \ No newline at end of file diff --git a/app/qexport/api.go b/app/qexport/api.go new file mode 100644 index 000000000..28dc2a603 --- /dev/null +++ b/app/qexport/api.go @@ -0,0 +1,101 @@ +package main + +import ( + "bufio" + "os" + "path/filepath" + "regexp" + "strings" +) + +func apipath(base string) string { + return filepath.Join(os.Getenv("GOROOT"), "api", base) +} + +var sym = regexp.MustCompile(`^pkg (\S+)\s?(.*)?, (?:(var|func|type|const)) ([A-Z]\w*)`) + +type GoApi struct { + Keys map[string]bool + Ver string +} + +func LoadApi(ver string) (*GoApi, error) { + f, err := os.Open(apipath(ver + ".txt")) + if err != nil { + return nil, err + } + sc := bufio.NewScanner(f) + keys := make(map[string]bool) + for sc.Scan() { + l := sc.Text() + has := func(v string) bool { return strings.Contains(l, v) } + if has("interface, ") || has(", method (") { + continue + } + if m := sym.FindStringSubmatch(l); m != nil { + // 1 pkgname + // 2 os-arch-cgo + // 3 var|func|type|const + // 4 name + key := m[1] + "." + m[4] + keys[key] = true + } + } + return &GoApi{Ver: ver, Keys: keys}, nil +} + +type ApiCheck struct { + Base map[string]bool + Apis []*GoApi +} + +func NewApiCheck() *ApiCheck { + ac := &ApiCheck{} + ac.Base = make(map[string]bool) + return ac +} + +func (ac *ApiCheck) LoadBase(vers ...string) error { + for _, ver := range vers { + api, err := LoadApi(ver) + if err != nil { + return err + } + for k, v := range api.Keys { + ac.Base[k] = v + } + } + return nil +} + +func (ac *ApiCheck) LoadApi(vers ...string) error { + for _, ver := range vers { + api, err := LoadApi(ver) + if err != nil { + return err + } + for k, _ := range api.Keys { + if ac.Base[k] { + delete(api.Keys, k) + } + } + ac.Apis = append(ac.Apis, api) + } + return nil +} + +func (ac *ApiCheck) FincApis(name string) (vers []string) { + for _, api := range ac.Apis { + if api.Keys[name] { + vers = append(vers, api.Ver) + } + } + return +} + +func (ac *ApiCheck) ApiVers() (vers []string) { + for _, api := range ac.Apis { + vers = append(vers, api.Ver) + } + return +} diff --git a/app/qexport/main.go b/app/qexport/main.go index 281533cd9..8246ae585 100644 --- a/app/qexport/main.go +++ b/app/qexport/main.go @@ -45,6 +45,10 @@ func init() { flag.StringVar(&flagExportPath, "outpath", "./qlang", "optional set export root path") } +var ( + ac *ApiCheck +) + func main() { flag.Parse() args := flag.Args() @@ -59,6 +63,17 @@ func main() { setCustomContexts(flagCustomContext) } + //load ApiCheck + ac = NewApiCheck() + err := ac.LoadBase("go1", "go1.1", "go1.2", "go1.3", "go1.4") + if err != nil { + log.Println(err) + } + err = ac.LoadApi("go1.5", "go1.6", "go1.7") + if err != nil { + log.Println(err) + } + var outpath string if filepath.IsAbs(flagExportPath) { outpath = flagExportPath @@ -151,7 +166,22 @@ func export(pkg string, outpath string, skipOSArch bool) error { return } + checkVer := func(key string) (string, bool) { + vers := ac.FincApis(bp.ImportPath + "." + key) + if len(vers) > 0 { + return vers[0], true + } + return "", false + } + + // go ver map + verMap := make(map[string][]string) + outfv := func(ver string, k, v string) { + verMap[ver] = append(verMap[ver], fmt.Sprintf("Exports[%q]=%s", k, v)) + } + var hasTypeExport bool + verHasTypeExport := make(map[string]bool) //write exports outf(`// Exports is the export table of this module. @@ -169,7 +199,11 @@ var Exports = map[string]interface{}{ if isUint64Const(fn) { fn = "uint64(" + fn + ")" } - outf("\t%q:\t%s,\n", name, fn) + if vers, ok := checkVer(v); ok { + outfv(vers, name, fn) + } else { + outf("\t%q:\t%s,\n", name, fn) + } } } @@ -191,7 +225,10 @@ var Exports = map[string]interface{}{ name := v fn := pkgName + "." + v if isStructVar { - outf("\t%q:\t&%s,\n", name, fn) + fn = "&" + fn + } + if vers, ok := checkVer(v); ok { + outfv(vers, name, fn) } else { outf("\t%q:\t%s,\n", name, fn) } @@ -204,7 +241,11 @@ var Exports = map[string]interface{}{ for _, v := range keys { name := toLowerCaseStyle(v) fn := pkgName + "." + v - outf("\t%q:\t%s,\n", name, fn) + if vers, ok := checkVer(v); ok { + outfv(vers, name, fn) + } else { + outf("\t%q:\t%s,\n", name, fn) + } } } @@ -244,13 +285,21 @@ var Exports = map[string]interface{}{ } } fn := pkgName + "." + f - outf("\t%q:\t%s,\n", name, fn) + if vers, ok := checkVer(f); ok { + outfv(vers, name, fn) + } else { + outf("\t%q:\t%s,\n", name, fn) + } } for _, f := range funcsOther { name := toLowerCaseStyle(f) fn := pkgName + "." + f - outf("\t%q:\t%s,\n", name, fn) + if vers, ok := checkVer(f); ok { + outfv(vers, name, fn) + } else { + outf("\t%q:\t%s,\n", name, fn) + } } } } @@ -296,9 +345,17 @@ var Exports = map[string]interface{}{ } //export type, qlang.NewType(reflect.TypeOf((*http.Client)(nil)).Elem()) + //export type, qlang.StructOf((*strings.Reader)(nil)) if ast.IsExported(v) { - hasTypeExport = true - outf("\t%q:\tqlang.NewType(reflect.TypeOf((*%s.%s)(nil)).Elem()),\n", v, pkgName, v) + name := v + fn := fmt.Sprintf("qlang.StructOf((*%s.%s)(nil))", pkgName, v) + if vers, ok := checkVer(v); ok { + verHasTypeExport[vers] = true + outfv(vers, name, fn) + } else { + hasTypeExport = true + outf("\t%q:\t%s,\n", name, fn) + } } for _, f := range funcsNew { @@ -312,13 +369,21 @@ var Exports = map[string]interface{}{ } } fn := pkgName + "." + f - outf("\t%q:\t%s,\n", name, fn) + if vers, ok := checkVer(f); ok { + outfv(vers, name, fn) + } else { + outf("\t%q:\t%s,\n", name, fn) + } } for _, f := range funcsOther { name := toLowerCaseStyle(f) fn := pkgName + "." + f - outf("\t%q:\t%s,\n", name, fn) + if vers, ok := checkVer(f); ok { + outfv(vers, name, fn) + } else { + outf("\t%q:\t%s,\n", name, fn) + } } } } @@ -335,14 +400,15 @@ var Exports = map[string]interface{}{ //write package outHeadf("package %s\n", pkgName) - //write imports - outHeadf("import (\n") - outHeadf("\t%q\n", pkg) - if hasTypeExport { - outHeadf("\t\"reflect\"\n\n") - outHeadf("\t\"qlang.io/qlang.spec.v1\"\n") + if strings.Count(buf.String(), ",") > 1 { + //write imports + outHeadf("import (\n") + outHeadf("\t%q\n", pkg) + if hasTypeExport { + outHeadf("\n\t\"qlang.io/qlang.spec.v1\"\n") + } + outHeadf(")\n\n") } - outHeadf(")\n\n") // format data, err := format.Source(append(head.Bytes(), buf.Bytes()...)) @@ -364,6 +430,34 @@ var Exports = map[string]interface{}{ defer file.Close() file.Write(data) + // write version + for ver, lines := range verMap { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("// +build %s\n\n", ver)) + buf.WriteString(fmt.Sprintf("package %s\n\n", pkgName)) + if verHasTypeExport[ver] { + buf.WriteString("import (\n") + buf.WriteString(fmt.Sprintf("\t%q\n\n", bp.ImportPath)) + buf.WriteString(fmt.Sprintf("\t%q\n", "qlang.io/qlang.spec.v1")) + buf.WriteString(")\n") + } else { + buf.WriteString(fmt.Sprintf("import %q\n", bp.ImportPath)) + } + buf.WriteString("func init() {\n\t") + buf.WriteString(strings.Join(lines, "\n\t")) + buf.WriteString("\n}") + data, err := format.Source(buf.Bytes()) + if err != nil { + return err + } + file, err := os.Create(filepath.Join(root, pkgName+"-"+strings.Replace(ver, ".", "", 1)+".go")) + if err != nil { + return err + } + file.Write(data) + file.Close() + } + return nil } diff --git a/app/qlang.safe/main_safe.go b/app/qlang.safe/main_safe.go index 8531720ae..e22ab4164 100644 --- a/app/qlang.safe/main_safe.go +++ b/app/qlang.safe/main_safe.go @@ -1,78 +1,14 @@ package main import ( - "bufio" - "fmt" - "io/ioutil" - "strings" - "os" - - "qlang.io/qlang.v2/qlang" - qall "qlang.io/qlang/qlang.all" + "qlang.io/app/qshell.v1" ) // ----------------------------------------------------------------------------- func main() { - qall.InitSafe(true) - qlang.SetDumpCode(os.Getenv("QLANG_DUMPCODE")) - - libs := os.Getenv("QLANG_PATH") - if libs == "" { - libs = os.Getenv("HOME") + "/qlang" - } - - if len(os.Args) > 1 { - lang, err := qlang.New(qlang.InsertSemis) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - lang.SetLibs(libs) - fname := os.Args[1] - b, err := ioutil.ReadFile(fname) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(2) - } - err = lang.SafeExec(b, fname) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(3) - } - return - } - - qall.Copyright() - - var ret interface{} - qlang.SetOnPop(func(v interface{}) { - ret = v - }) - - lang, err := qlang.New(nil) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - lang.SetLibs(libs) - - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - line := strings.Trim(scanner.Text(), " \t\r\n") - if line == "" { - continue - } - ret = nil - err := lang.SafeEval(line) - if err != nil { - fmt.Fprintln(os.Stderr, err) - continue - } - fmt.Printf("> %v\n\n", ret) - } + qshell.Main(true) } // ----------------------------------------------------------------------------- - diff --git a/app/qlang/main.go b/app/qlang/main.go index 31e66fbde..d721ef599 100644 --- a/app/qlang/main.go +++ b/app/qlang/main.go @@ -1,176 +1,14 @@ package main import ( - "fmt" - "io" - "io/ioutil" - "os" - "strings" - - "qlang.io/qlang.v2/qlang" - "qlang.io/qlang/terminal" - - qspec "qlang.io/qlang.spec.v1" - qipt "qlang.io/qlang.v2/interpreter" - qall "qlang.io/qlang/qlang.all" -) - -var ( - historyFile = os.Getenv("HOME") + "/.qlang.history" + "qlang.io/app/qshell.v1" ) -func main() { - qall.InitSafe(false) - qlang.Import("", qipt.Exports) - qlang.SetDumpCode(os.Getenv("QLANG_DUMPCODE")) - - libs := os.Getenv("QLANG_PATH") - if libs == "" { - libs = os.Getenv("HOME") + "/qlang" - } - - lang, err := qlang.New(qlang.InsertSemis) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - lang.SetLibs(libs) - - // exec source - if len(os.Args) > 1 { - fname := os.Args[1] - b, err := ioutil.ReadFile(fname) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(2) - } - err = lang.SafeExec(b, fname) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(3) - } - return - } - - // interpreter - qall.Copyright() - - var ret interface{} - qlang.SetOnPop(func(v interface{}) { - ret = v - }) - - var tokener tokener - term := terminal.New(">>> ", "... ", tokener.ReadMore) - term.SetWordCompleter(func(line string, pos int) (head string, completions []string, tail string) { - return line[:pos], []string{" "}, line[pos:] - }) - - term.LoadHistroy(historyFile) // load/save histroy - defer term.SaveHistroy(historyFile) - - for { - expr, err := term.Scan() - if err != nil { - if err == terminal.ErrPromptAborted { - break - } else if err == io.EOF { - fmt.Println("^D") - break - } - fmt.Fprintln(os.Stderr, err) - continue - } - expr = strings.TrimSpace(expr) - if expr == "" { - continue - } - ret = qspec.Undefined - err = lang.SafeEval(expr) - if err != nil { - fmt.Fprintln(os.Stderr, err) - continue - } - if ret != qspec.Undefined { - fmt.Println(ret) - } - } -} - // ----------------------------------------------------------------------------- -type tokener struct { - level int - instr bool -} - -var dontReadMoreChars = "+-})];" -var puncts = "([=,*/%|&<>^.:" - -func readMore(line string) bool { - - n := len(line) - if n == 0 { - return false - } - - pos := strings.IndexByte(dontReadMoreChars, line[n-1]) - if pos == 0 || pos == 1 { - return n >= 2 && line[n-2] != dontReadMoreChars[pos] - } - return pos < 0 && strings.IndexByte(puncts, line[n-1]) >= 0 -} - -func findEnd(line string, c byte) int { - - for i := 0; i < len(line); i++ { - switch line[i] { - case c: - return i - case '\\': - i++ - } - } - return -1 -} - -func (p *tokener) ReadMore(expr string, line string) (string, bool) { // read more line check - - ret := expr + line + "\n" - for { - if p.instr { - pos := strings.IndexByte(line, '`') - if pos < 0 { - return ret, true - } - line = line[pos+1:] - p.instr = false - } +func main() { - pos := strings.IndexAny(line, "{}`'\"") - if pos < 0 { - if p.level != 0 { - return ret, true - } - line = strings.TrimRight(line, " \t") - return ret, readMore(line) - } - switch c := line[pos]; c { - case '{': - p.level++ - case '}': - p.level-- - case '`': - p.instr = true - default: - line = line[pos+1:] - pos = findEnd(line, c) - if pos < 0 { - return ret, p.level != 0 - } - } - line = line[pos+1:] - } + qshell.Main(false) } // ----------------------------------------------------------------------------- diff --git a/app/qshell.v1/qshell.go b/app/qshell.v1/qshell.go new file mode 100644 index 000000000..f7a05f46a --- /dev/null +++ b/app/qshell.v1/qshell.go @@ -0,0 +1,186 @@ +package qshell + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "qlang.io/qlang.v2/qlang" + "qlang.io/qlang/terminal" + + qspec "qlang.io/qlang.spec.v1" + qipt "qlang.io/qlang.v2/interpreter" + qall "qlang.io/qlang/qlang.all" +) + +var ( + historyFile = os.Getenv("HOME") + "/.qlang.history" +) + +// Main is the entry of qlang/qlang.safe shell +// +func Main(safeMode bool) { + + qall.InitSafe(safeMode) + qlang.Import("", qipt.Exports) + qlang.SetDumpCode(os.Getenv("QLANG_DUMPCODE")) + + libs := os.Getenv("QLANG_PATH") + if libs == "" { + libs = os.Getenv("HOME") + "/qlang" + } + + lang, err := qlang.New(qlang.InsertSemis) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + lang.SetLibs(libs) + + // exec source + // + if len(os.Args) > 1 { + fname := os.Args[1] + b, err := ioutil.ReadFile(fname) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + err = lang.SafeExec(b, fname) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(3) + } + return + } + + // interpreter + + qall.Copyright() + if safeMode { + fmt.Printf("Use Ctrl-D (i.e. EOF) to exit.\n\n") + } else { + fmt.Printf("Use exit() or Ctrl-D (i.e. EOF) to exit.\n\n") + } + + var ret interface{} + qlang.SetOnPop(func(v interface{}) { + ret = v + }) + + var tokener tokener + term := terminal.New(">>> ", "... ", tokener.ReadMore) + term.SetWordCompleter(func(line string, pos int) (head string, completions []string, tail string) { + return line[:pos], []string{" "}, line[pos:] + }) + + term.LoadHistroy(historyFile) // load/save histroy + defer term.SaveHistroy(historyFile) + + for { + expr, err := term.Scan() + if err != nil { + if err == terminal.ErrPromptAborted { + continue + } else if err == io.EOF { + fmt.Println("^D") + break + } + fmt.Fprintln(os.Stderr, err) + continue + } + expr = strings.TrimSpace(expr) + if expr == "" { + continue + } + ret = qspec.Undefined + err = lang.SafeEval(expr) + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue + } + if ret != qspec.Undefined { + fmt.Println(ret) + } + } +} + +// ----------------------------------------------------------------------------- + +type tokener struct { + level int + instr bool +} + +var dontReadMoreChars = "+-})];" +var puncts = "([=,*/%|&<>^.:" + +func readMore(line string) bool { + + n := len(line) + if n == 0 { + return false + } + + pos := strings.IndexByte(dontReadMoreChars, line[n-1]) + if pos == 0 || pos == 1 { + return n >= 2 && line[n-2] != dontReadMoreChars[pos] + } + return pos < 0 && strings.IndexByte(puncts, line[n-1]) >= 0 +} + +func findEnd(line string, c byte) int { + + for i := 0; i < len(line); i++ { + switch line[i] { + case c: + return i + case '\\': + i++ + } + } + return -1 +} + +func (p *tokener) ReadMore(expr string, line string) (string, bool) { // read more line check + + ret := expr + line + "\n" + for { + if p.instr { + pos := strings.IndexByte(line, '`') + if pos < 0 { + return ret, true + } + line = line[pos+1:] + p.instr = false + } + + pos := strings.IndexAny(line, "{}`'\"") + if pos < 0 { + if p.level != 0 { + return ret, true + } + line = strings.TrimRight(line, " \t") + return ret, readMore(line) + } + switch c := line[pos]; c { + case '{': + p.level++ + case '}': + p.level-- + case '`': + p.instr = true + default: + line = line[pos+1:] + pos = findEnd(line, c) + if pos < 0 { + return ret, p.level != 0 + } + } + line = line[pos+1:] + } +} + +// ----------------------------------------------------------------------------- diff --git a/exec.v2/code.go b/exec.v2/code.go index e7c803dfb..285adcc8f 100644 --- a/exec.v2/code.go +++ b/exec.v2/code.go @@ -205,13 +205,34 @@ func (p *Context) Exports() map[string]interface{} { return export } -// Vars returns all variables in executing context. +// Vars is deprecated. please use `CopyVars` method. // func (p *Context) Vars() map[string]interface{} { return p.vars } +// CopyVars copies and returns all variables in executing context. +// +func (p *Context) CopyVars() map[string]interface{} { + + vars := make(map[string]interface{}) + for k, v := range p.vars { + vars[k] = v + } + return vars +} + +// ResetVars resets all variables in executing context. +// +func (p *Context) ResetVars(vars map[string]interface{}) { + + p.vars = make(map[string]interface{}) + for k, v := range vars { + p.vars[k] = v + } +} + // Var returns a variable value in executing context. // func (p *Context) Var(name string) (v interface{}, ok bool) { diff --git a/qlang.spec.v1/spec.go b/qlang.spec.v1/spec.go index 2451bb4bf..c4c59729b 100644 --- a/qlang.spec.v1/spec.go +++ b/qlang.spec.v1/spec.go @@ -1,18 +1,31 @@ package qlang import ( - "errors" "fmt" "reflect" ) // ----------------------------------------------------------------------------- +type undefinedType int + +var undefinedBytes = []byte("\"```undefined```\"") + +func (p undefinedType) Error() string { + return "undefined" +} + +func (p undefinedType) MarshalJSON() ([]byte, error) { + return undefinedBytes, nil +} + var ( // Undefined is `undefined` in qlang. - Undefined = interface{}(errors.New("undefined")) + Undefined interface{} = undefinedType(0) ) +// ----------------------------------------------------------------------------- + // A Chan represents chan class in qlang. // type Chan struct { @@ -53,6 +66,13 @@ func NewType(t reflect.Type) *Type { return &Type{t: t} } +// StructOf returns a qlang builtin type object. +// +func StructOf(ptr interface{}) *Type { + + return &Type{t: reflect.TypeOf(ptr).Elem()} +} + // GoType returns the underlying go type. required by `qlang type` spec. // func (p *Type) GoType() reflect.Type { @@ -117,11 +137,18 @@ type TypeEx struct { Call interface{} } -// NewTypeEx returns a qlang builtin type object. +// NewTypeEx returns a qlang builtin type object with a cast function. +// +func NewTypeEx(t reflect.Type, cast interface{}) *TypeEx { + + return &TypeEx{t: t, Call: cast} +} + +// StructOfEx returns a qlang builtin type object with a cast function. // -func NewTypeEx(t reflect.Type, call interface{}) *TypeEx { +func StructOfEx(ptr interface{}, cast interface{}) *TypeEx { - return &TypeEx{t: t, Call: call} + return &TypeEx{t: reflect.TypeOf(ptr).Elem(), Call: cast} } // GoType returns the underlying go type. required by `qlang type` spec. @@ -195,7 +222,7 @@ func GoModuleName(table map[string]interface{}) (name string, ok bool) { return } -func getInitSafe(table map[string]interface{}) (initSafe func(mod Module), ok bool) { +func fetchInitSafe(table map[string]interface{}) (initSafe func(mod Module), ok bool) { vinitSafe, ok := table["_initSafe"] if !ok { @@ -205,6 +232,7 @@ func getInitSafe(table map[string]interface{}) (initSafe func(mod Module), ok bo if !ok { panic("invalid prototype of initSafe: must be `func initSafe(mod qlang.Module)`") } + delete(table, "_initSafe") return } @@ -324,7 +352,7 @@ func GoModuleList() []string { func Import(mod string, table map[string]interface{}) { if SafeMode { - if initSafe, ok := getInitSafe(table); ok { + if initSafe, ok := fetchInitSafe(table); ok { initSafe(Module{Exports: table}) } } diff --git a/qlang.v2/qlang/engine2.go b/qlang.v2/qlang/engine2.go index 19e1ff67a..8143a7c4a 100644 --- a/qlang.v2/qlang/engine2.go +++ b/qlang.v2/qlang/engine2.go @@ -82,7 +82,7 @@ type Qlang struct { func New(options *Options) (lang *Qlang, err error) { cl := qlangv2.New() - cl.Opts = (*interpreter.Options)(options) + cl.Opts = interpreter.InsertSemis stk := exec.NewStack() ctx := exec.NewContext() ctx.Stack = stk diff --git a/qlang.v2/qlang_test.go b/qlang.v2/qlang_test.go index 79fad1db0..256017695 100644 --- a/qlang.v2/qlang_test.go +++ b/qlang.v2/qlang_test.go @@ -133,7 +133,7 @@ func (p *AImportType) GetX() int { func init() { qlang.Import("foo", map[string]interface{}{ - "AImportType": qspec.NewType(reflect.TypeOf((*AImportType)(nil)).Elem()), + "AImportType": qspec.StructOf((*AImportType)(nil)), }) } diff --git a/qlang/builtin/exports.go b/qlang/builtin/exports.go index cbc81e22f..d9efe97b5 100644 --- a/qlang/builtin/exports.go +++ b/qlang/builtin/exports.go @@ -20,6 +20,7 @@ var exports = map[string]interface{}{ "mapOf": MapOf, "panic": Panic, "panicf": Panicf, + "print": fmt.Print, "printf": fmt.Printf, "println": fmt.Println, "fprintln": fmt.Fprintln, diff --git a/qlang/eql.v1/eql.go b/qlang/eql.v1/eql.go new file mode 100644 index 000000000..cb1a1f5d6 --- /dev/null +++ b/qlang/eql.v1/eql.go @@ -0,0 +1,243 @@ +package eql + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "strings" + "unicode" +) + +var ( + // ErrEndRequired is returned if eql script doesn't end by `%>`. + ErrEndRequired = errors.New("eql script requires `%>` to end") + + // ErrEndOfString is returned if string doesn't end. + ErrEndOfString = errors.New("string doesn't end") +) + +// ----------------------------------------------------------------------------- + +// Parse parses eql source into qlang code. +// +func Parse(source string) (code []byte, err error) { + + var b bytes.Buffer + for { + pos := strings.Index(source, "<%") + if pos < 0 { + err = parseText(&b, source) + break + } + if pos > 0 { + err = parseText(&b, source[:pos]) + if err != nil { + return + } + } + source, err = parseEql(&b, source[pos+2:]) + if err != nil { + return + } + } + code = b.Bytes() + return +} + +func parseText(b *bytes.Buffer, source string) (err error) { + + b.WriteString("printf(eql.subst(`") + for { + pos := strings.IndexByte(source, '`') + if pos < 0 { + b.WriteString(source) + break + } + b.WriteString(source[:pos]) + pos2 := pos + 1 + for ; pos2 < len(source); pos2++ { + if source[pos2] != '`' { + break + } + } + b.WriteString("` + \"") + b.WriteString(source[pos:pos2]) + b.WriteString("\" + `") + source = source[pos2:] + } + b.WriteString("`)); ") + return +} + +func parseEql(b *bytes.Buffer, source string) (ret string, err error) { + + fexpr := strings.HasPrefix(source, "=") + if fexpr { + b.WriteString("print(") + source = source[1:] + } + for { + pos := strings.IndexAny(source, "%\"`") + if pos < 0 { + return "", ErrEndRequired + } + if c := source[pos]; c == '%' { + if strings.HasPrefix(source[pos+1:], ">") { + ret = source[pos+2:] + b.WriteString(source[:pos]) + if fexpr { + b.WriteString("); ") + } else if strings.HasPrefix(ret, "\n") { + ret = ret[1:] + b.WriteString("\n") + } + return + } + b.WriteString(source[:pos+1]) + source = source[pos+1:] + } else { + n := findEnd(source[pos+1:], c) + if n < 0 { + return "", ErrEndOfString + } + n += pos + 2 + b.WriteString(source[:n]) + source = source[n:] + } + } +} + +func findEnd(line string, c byte) int { + + for i := 0; i < len(line); i++ { + switch line[i] { + case c: + return i + case '\\': + i++ + } + } + return -1 +} + +// ----------------------------------------------------------------------------- + +// Variables represent how to get value of a variable. +// +type Variables interface { + Var(name string) (v interface{}, ok bool) +} + +type mapVars map[string]interface{} +type mapStrings map[string]string + +func (vars mapVars) Var(name string) (v interface{}, ok bool) { + v, ok = vars[name] + return +} + +func (vars mapStrings) Var(name string) (v interface{}, ok bool) { + v, ok = vars[name] + return +} + +// ----------------------------------------------------------------------------- + +// DefaultVars is the default variables. +// +var DefaultVars Variables + +// Subst substs variables in text. +// +func Subst(text string, param ...interface{}) string { + + var b bytes.Buffer + var vars Variables + if len(param) == 0 { + vars = DefaultVars + } else { + switch v := param[0].(type) { + case map[string]interface{}: + vars = mapVars(v) + case map[string]string: + vars = mapStrings(v) + case Variables: + vars = v + default: + panic(fmt.Sprintf("eql.Subst: unsupported param type `%v`", reflect.TypeOf(param[0]))) + } + } + for { + pos := strings.IndexByte(text, '$') + if pos < 0 || pos+1 >= len(text) { + if b.Len() == 0 { + return text + } + b.WriteString(text) + break + } + switch c := text[pos+1]; { + case c == '$': + b.WriteString(text[:pos+1]) + text = text[pos+2:] + case (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'): + b.WriteString(text[:pos]) + pos1 := pos + 2 + n := strings.IndexFunc(text[pos1:], func(c rune) bool { + return !unicode.IsLetter(c) && !unicode.IsDigit(c) + }) + if n < 0 { + n = len(text) - pos1 + } + pos2 := pos1 + n + key := text[pos+1 : pos2] + val, ok := vars.Var(key) + if !ok { + panic("variable not found: " + key) + } + b.WriteString(fmt.Sprint(val)) + text = text[pos2:] + default: + b.WriteString(text[:pos+1]) + text = text[pos+1:] + } + } + return b.String() +} + +// ----------------------------------------------------------------------------- + +func imports(imports string) string { + + if imports == "" { + return "" + } + mods := strings.Split(imports, ",") + return "\"" + strings.Join(mods, "\"\n\t\"") + "\"" +} + +func getVar(name string, defval interface{}) interface{} { + + v, ok := DefaultVars.Var(name) + if !ok { + v = defval + } + return v +} + +// ----------------------------------------------------------------------------- + +// Exports is the export table of this module. +// +var Exports = map[string]interface{}{ + "parse": Parse, + "subst": Subst, + "var": getVar, + "imports": imports, + + "ErrEndRequired": ErrEndRequired, + "ErrEndOfString": ErrEndOfString, +} + +// ----------------------------------------------------------------------------- diff --git a/qlang/eql.v1/eql_test.go b/qlang/eql.v1/eql_test.go new file mode 100644 index 000000000..fd8f5ce86 --- /dev/null +++ b/qlang/eql.v1/eql_test.go @@ -0,0 +1,103 @@ +package eql + +import ( + "bytes" + "testing" +) + +// ----------------------------------------------------------------------------- + +const eqlTestCode = `<% + // + // eql example.eql -o example_bytes.go --imports=bytes --module=modbytes --Writer="*bytes.Buffer" + // eql example.eql -o example_bufio.go --imports=bufio --module=modbufio --Writer="*bufio.Writer" + // +%> +package eql_test + +import ( + <%= eql.imports(imports) %> + "encoding/binary" +) + +// ----------------------------------------------------------------------------- + +type $module string + +func (p $module) write(out $Writer, b []byte) { + + _, err := out.Write(b) + if err != nil { + panic(err) + } +} + +<% if Writer == "*bytes.Buffer" { %> +func (p $module) flush(out $Writer) { +} +<% } else { %> +func (p $module) flush(out $Writer) { + + err := out.Flush() + if err != nil { + panic(err) + } +} +<% } %> + +// ----------------------------------------------------------------------------- +` + +func TestEql(t *testing.T) { + + _, err := Parse(eqlTestCode) + if err != nil { + t.Fatal("Parse failed:", err) + } +} + +func TestSubst(t *testing.T) { + + out := Subst(`?$Writer!$`, map[string]interface{}{ + "Writer": "abc", + }) + if out != "?abc!$" { + t.Fatal("Subst failed:", out) + } + + out = Subst(`$Writer!$$`, map[string]interface{}{ + "Writer": "abc", + }) + if out != "abc!$" { + t.Fatal("Subst failed:", out) + } + + out = Subst(`$$$Writer!`, map[string]interface{}{ + "Writer": 123, + }) + if out != "$123!" { + t.Fatal("Subst failed:", out) + } + + out = Subst(`$$$Writer`, map[string]interface{}{ + "Writer": 123, + }) + if out != "$123" { + t.Fatal("Subst failed:", out) + } +} + +func TestParseText(t *testing.T) { + + var b bytes.Buffer + err := parseText(&b, "abc ``` def") + if err != nil { + t.Fatal("parseText failed:", err) + } + + if b.String() != "printf(eql.subst(`abc ` + \"```\" + ` def`)); " { + t.Fatal(b.String()) + } +} + +// ----------------------------------------------------------------------------- diff --git a/qlang/net/http/http.go b/qlang/net/http/http.go index 02a5f7e4c..c1dc7da1e 100644 --- a/qlang/net/http/http.go +++ b/qlang/net/http/http.go @@ -2,7 +2,6 @@ package http import ( "net/http" - "reflect" "qlang.io/qlang.spec.v1" ) @@ -55,12 +54,12 @@ var Exports = map[string]interface{}{ "DefaultClient": http.DefaultClient, "DefaultServeMux": http.DefaultServeMux, - "Client": qlang.NewType(reflect.TypeOf((*http.Client)(nil)).Elem()), - "Cookie": qlang.NewType(reflect.TypeOf((*http.Cookie)(nil)).Elem()), - "Header": qlang.NewType(reflect.TypeOf((*http.Header)(nil)).Elem()), - "Request": qlang.NewType(reflect.TypeOf((*http.Request)(nil)).Elem()), - "Response": qlang.NewType(reflect.TypeOf((*http.Response)(nil)).Elem()), - "Server": qlang.NewType(reflect.TypeOf((*http.Server)(nil)).Elem()), + "Client": qlang.StructOf((*http.Client)(nil)), + "Cookie": qlang.StructOf((*http.Cookie)(nil)), + "Header": qlang.StructOf((*http.Header)(nil)), + "Request": qlang.StructOf((*http.Request)(nil)), + "Response": qlang.StructOf((*http.Response)(nil)), + "Server": qlang.StructOf((*http.Server)(nil)), } // ----------------------------------------------------------------------------- diff --git a/qlang/os/os.go b/qlang/os/os.go index 24af3ca81..e3b833a3d 100644 --- a/qlang/os/os.go +++ b/qlang/os/os.go @@ -17,6 +17,7 @@ var Exports = map[string]interface{}{ "stdin": os.Stdin, "stderr": os.Stderr, "stdout": os.Stdout, + "getenv": os.Getenv, "open": os.Open, "create": os.Create, "exit": os.Exit, @@ -26,6 +27,7 @@ var Exports = map[string]interface{}{ func _initSafe(mod qlang.Module) { mod.Disable("open") + mod.Disable("getenv") mod.Exports["exit"] = SafeExit } @@ -33,7 +35,28 @@ func _initSafe(mod qlang.Module) { // func SafeExit(code int) { - panic(strconv.Itoa(code)) + panic("exit " + strconv.Itoa(code)) +} + +// ----------------------------------------------------------------------------- + +func exit() { + os.Exit(0) +} + +func safeExit() { + panic("exit") +} + +func _initSafe2(mod qlang.Module) { + mod.Exports["exit"] = safeExit +} + +// InlineExports is the export table of this module. +// +var InlineExports = map[string]interface{}{ + "exit": exit, + "_initSafe": _initSafe2, } // ----------------------------------------------------------------------------- diff --git a/qlang/qlang.all/all.go b/qlang/qlang.all/all.go index f3f5644bb..abf1e08cb 100644 --- a/qlang/qlang.all/all.go +++ b/qlang/qlang.all/all.go @@ -7,6 +7,7 @@ import ( "qlang.io/qlang/crypto/md5" "qlang.io/qlang/encoding/hex" "qlang.io/qlang/encoding/json" + "qlang.io/qlang/eql.v1" "qlang.io/qlang/errors" "qlang.io/qlang/io" "qlang.io/qlang/io/ioutil" @@ -53,8 +54,10 @@ func InitSafe(safeMode bool) { qlang.Import("hex", hex.Exports) qlang.Import("json", json.Exports) qlang.Import("errors", errors.Exports) + qlang.Import("eql", eql.Exports) qlang.Import("math", math.Exports) qlang.Import("os", os.Exports) + qlang.Import("", os.InlineExports) qlang.Import("path", path.Exports) qlang.Import("http", http.Exports) qlang.Import("reflect", reflect.Exports) diff --git a/qlang/strings/strings.go b/qlang/strings/strings.go index 4b35cb28e..6b8816f12 100644 --- a/qlang/strings/strings.go +++ b/qlang/strings/strings.go @@ -1,7 +1,6 @@ package strings import ( - "reflect" "strings" "qlang.io/qlang.spec.v1" @@ -10,8 +9,8 @@ import ( // ----------------------------------------------------------------------------- var ( - tyReader = qlang.NewType(reflect.TypeOf((*strings.Reader)(nil)).Elem()) - tyReplacer = qlang.NewType(reflect.TypeOf((*strings.Replacer)(nil)).Elem()) + tyReader = qlang.StructOf((*strings.Reader)(nil)) + tyReplacer = qlang.StructOf((*strings.Replacer)(nil)) ) // Exports is the export table of this module. diff --git a/qlang/terminal/terminal.go b/qlang/terminal/terminal.go index fb42a8024..6fa55173f 100644 --- a/qlang/terminal/terminal.go +++ b/qlang/terminal/terminal.go @@ -83,4 +83,6 @@ var Exports = map[string]interface{}{ "new": New, "supported": liner.TerminalSupported, "mode": liner.TerminalMode, + + "ErrPromptAborted": ErrPromptAborted, } diff --git a/qlang/version/version.go b/qlang/version/version.go index 65d16c8e8..56e6843d5 100644 --- a/qlang/version/version.go +++ b/qlang/version/version.go @@ -7,7 +7,7 @@ import ( "strings" ) -var version string = "develop" +var version = "develop" func init() { version = strings.TrimRight(version, " ") @@ -17,12 +17,15 @@ func init() { } } +// Copyright shows qlang copyright information. +// func Copyright() { fmt.Printf("Q-language - http://qlang.io, version qlang-%s %s/%s\n", version, runtime.GOOS, runtime.GOARCH) - fmt.Println("Copyright (C) 2015 Qiniu.com - Shanghai Qiniu Information Technologies Co., Ltd.\n") + fmt.Println("Copyright (C) 2015 Qiniu.com - Shanghai Qiniu Information Technologies Co., Ltd.") } +// Version returns qlang version. +// func Version() string { return version } - diff --git a/tutorial/calc/calc.ql b/tutorial/calc/calc.ql index fbafbd74a..4be0d2444 100644 --- a/tutorial/calc/calc.ql +++ b/tutorial/calc/calc.ql @@ -12,8 +12,6 @@ factor = (IDENT '(' doc %= ','/ARITY ')')/call ` -fntable = nil - Stack = class { fn _init() { @@ -104,27 +102,3 @@ fntable = { "$pushFloat": Stack.push, "$ARITY": Stack.push, } - -main { // 使用main关键字将主程序括起来,是为了避免其中用的局部变量比如 err 对其他函数造成影响 - - calc = new Calculator - engine, err = interpreter(calc, nil) - if err != nil { - fprintln(os.stderr, err) - return 1 - } - - scanner = bufio.scanner(os.stdin) - for scanner.scan() { - line = strings.trim(scanner.text(), " \t\r\n") - if line != "" { - err = engine.eval(line) - if err != nil { - fprintln(os.stderr, err) - } else { - printf("> %v\n\n", calc.ret()) - } - } - } -} - diff --git a/tutorial/calc/main.ql b/tutorial/calc/main.ql new file mode 100644 index 000000000..3dd0f4a7b --- /dev/null +++ b/tutorial/calc/main.ql @@ -0,0 +1,24 @@ +include "calc.ql" + +main { // 使用main关键字将主程序括起来,是为了避免其中用的局部变量比如 err 对其他函数造成影响 + + calc = new Calculator + engine, err = interpreter(calc, nil) + if err != nil { + fprintln(os.stderr, err) + return 1 + } + + scanner = bufio.scanner(os.stdin) + for scanner.scan() { + line = strings.trim(scanner.text(), " \t\r\n") + if line != "" { + err = engine.eval(line) + if err != nil { + fprintln(os.stderr, err) + } else { + printf("> %v\n\n", calc.ret()) + } + } + } +} diff --git a/tutorial/qlang/qlang.ql b/tutorial/qlang/qlang.ql index 129a1df40..1ae6a80e5 100644 --- a/tutorial/qlang/qlang.ql +++ b/tutorial/qlang/qlang.ql @@ -60,8 +60,6 @@ factor = '+' factor ` -fntable = nil - errReturn = newRuntimeError("return") Stack = class { diff --git a/tutorial/terminal/main.ql b/tutorial/terminal/main.ql new file mode 100644 index 000000000..999af5b86 --- /dev/null +++ b/tutorial/terminal/main.ql @@ -0,0 +1,45 @@ +include "../calc/calc.ql" + +main { // 使用main关键字将主程序括起来,是为了避免其中用的局部变量比如 err 对其他函数造成影响 + + calc = new Calculator + engine, err = interpreter(calc, nil) + if err != nil { + fprintln(os.stderr, err) + return 1 + } + + historyFile = os.getenv("HOME") + "/.qcalc.history" + term = terminal.new(">>> ", "... ", nil) + term.LoadHistroy(historyFile) + defer term.SaveHistroy(historyFile) + + println(`Q-Calculator - http://qlang.io, version 1.0.00 +Copyright (C) 2015 Qiniu.com - Shanghai Qiniu Information Technologies Co., Ltd. +Use Ctrl-D (i.e. EOF) to exit. +`) + + for { + expr, err = term.Scan() + if err != nil { + if err == terminal.ErrPromptAborted { + continue + } elif err == io.EOF { + println("^D") + break + } + fprintln(os.stderr, err) + continue + } + expr = strings.trimSpace(expr) + if expr == "" { + continue + } + err = engine.eval(expr) + if err != nil { + fprintln(os.stderr, err) + } else { + println(calc.ret()) + } + } +}