diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b81424e..cbfb150 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,23 +16,18 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [18.x, 20.x, 22.x] + node-version: [24.x, latest] + + runs-on: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm i - run: npm run build --if-present - run: npm test - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./coverage/lcov.info diff --git a/.gitignore b/.gitignore index 33e9ba1..9172ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,6 @@ yarn.lock *~ +dist-test/ +debug_ts.mjs +reference/ diff --git a/go/expr.go b/go/expr.go new file mode 100644 index 0000000..43fb258 --- /dev/null +++ b/go/expr.go @@ -0,0 +1,1598 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +// Package expr provides a Pratt-parser expression plugin for the jsonic +// JSON parser. It supports infix, prefix, suffix, ternary and paren +// operators with configurable precedence. +// +// Expressions are encoded as LISP-style S-expressions using arrays/slices. +// The operator source string is the first element of the slice. +package expr + +import ( + jsonic "github.com/jsonicjs/jsonic/go" +) + +// OpDef defines an operator for the expression parser. +type OpDef struct { + Src interface{} // string or []string (for ternary) + OSrc string + CSrc string + Left int + Right int + Prefix bool + Suffix bool + Infix bool + Ternary bool + Paren bool + Preval interface{} + Use interface{} +} + +// Op is the full operator description available during parsing and evaluation. +type Op struct { + Name string + Src string + Left int + Right int + Prefix bool + Suffix bool + Infix bool + Ternary bool + Paren bool + Terms int + Tkn string + Tin int + OSrc string + CSrc string + OTkn string + OTin int + CTkn string + CTin int + Preval PrevalDef + Use interface{} +} + +// PrevalDef specifies paren-preval options. +type PrevalDef struct { + Active bool + Required bool + Allow []string +} + +// ExprOptions configures the Expr plugin. +type ExprOptions struct { + Op map[string]*OpDef + Evaluate func(rule *jsonic.Rule, ctx *jsonic.Context, op *Op, terms []interface{}) interface{} +} + +// _unfilled is a sentinel value for pre-allocated but unfilled expression slots. +// Go slices don't have JS-like reference semantics for append, so we +// pre-allocate expression slices to their final length and fill slots +// as child results arrive. +var _unfilled interface{} = &struct{ x int }{-1} + +func isUnfilled(v interface{}) bool { return v == _unfilled } + +// isOp checks if a node is an expression (slice starting with *Op). +func isOp(node interface{}) bool { + if sl, ok := node.([]interface{}); ok && len(sl) > 0 { + _, isOp := sl[0].(*Op) + return isOp + } + return false +} + +// isExprOp checks if a node's op is an infix/prefix/suffix expression +// (not a ternary or paren, which are structural and shouldn't be drilled into). +func isExprOp(node interface{}) bool { + if sl, ok := node.([]interface{}); ok && len(sl) > 0 { + if op, ok := sl[0].(*Op); ok { + return !op.Ternary && !op.Paren + } + } + return false +} + +// fillNextSlot walks the expression tree depth-first and fills +// the deepest unfilled (_unfilled sentinel) slot with val. +// Returns true if a slot was filled. +func fillNextSlot(node []interface{}, val interface{}) bool { + if len(node) == 0 { + return false + } + op, ok := node[0].(*Op) + if !ok { + return false + } + // Check children first (depth-first) to fill innermost incomplete expr. + for i := 1; i <= op.Terms && i < len(node); i++ { + if sub, ok := node[i].([]interface{}); ok && len(sub) > 0 { + if _, subOp := sub[0].(*Op); subOp { + if fillNextSlot(sub, val) { + return true + } + } + } + } + // Then check this node's own slots. + for i := 1; i <= op.Terms && i < len(node); i++ { + if isUnfilled(node[i]) { + node[i] = val + return true + } + } + return false +} + +// makeExpr creates a pre-allocated expression slice [op, term1, term2, ...] +// with unfilled slots marked by _unfilled sentinel. +func makeExpr(op *Op, terms ...interface{}) []interface{} { + n := op.Terms + 1 + expr := make([]interface{}, n) + expr[0] = op + for i := 1; i < n; i++ { + if i-1 < len(terms) { + expr[i] = terms[i-1] + } else { + expr[i] = _unfilled + } + } + return expr +} + +// Expr is the expression parser plugin for jsonic. +func Expr(j *jsonic.Jsonic, opts map[string]interface{}) { + eopts := resolveOptions(opts) + allOps := makeAllOps(j, eopts) + + // Build lookup maps. + infixByTin := make(map[int]*Op) + prefixByTin := make(map[int]*Op) + suffixByTin := make(map[int]*Op) + parenOpenByTin := make(map[int]*Op) + parenCloseByTin := make(map[int]*Op) + ternaryByTin := make(map[int]*Op) + ternaryCloseByTin := make(map[int]*Op) + + for _, op := range allOps { + if op.Infix { + infixByTin[op.Tin] = op + } + if op.Prefix { + prefixByTin[op.Tin] = op + } + if op.Suffix { + suffixByTin[op.Tin] = op + } + if op.Paren { + parenOpenByTin[op.OTin] = op + parenCloseByTin[op.CTin] = op + } + if op.Ternary { + ternaryByTin[op.Tin] = op + ternaryCloseByTin[op.CTin] = op + } + } + + collectTins := func(m map[int]*Op) []int { + var tins []int + for t := range m { + tins = append(tins, t) + } + return tins + } + + PREFIX := collectTins(prefixByTin) + INFIX := collectTins(infixByTin) + SUFFIX := collectTins(suffixByTin) + OP := collectTins(parenOpenByTin) + CP := collectTins(parenCloseByTin) + TERN0 := collectTins(ternaryByTin) + TERN1 := collectTins(ternaryCloseByTin) + + hasPrefix := len(PREFIX) > 0 + hasInfix := len(INFIX) > 0 + hasSuffix := len(SUFFIX) > 0 + hasParen := len(OP) > 0 + hasTernary := len(TERN0) > 0 + + // Check if any paren op has preval active. + hasPreval := false + for _, op := range allOps { + if op.Paren && op.Preval.Active { + hasPreval = true + break + } + } + + mkS := func(tins []int) [][]int { return [][]int{tins} } + + // === VAL rule modifications === + j.Rule("val", func(rs *jsonic.RuleSpec) { + // Prefix operator: backtrack and push to 'expr'. + if hasPrefix { + rs.Open = append([]*jsonic.AltSpec{{ + S: mkS(PREFIX), + B: 1, + P: "expr", + N: map[string]int{"expr_prefix": 1, "expr_suffix": 0}, + G: "expr,prefix", + }}, rs.Open...) + } + + // Preval: value followed by paren open (e.g., foo(1,2)). + if hasPreval { + valTinsLocal := j.TokenSet("VAL") + rs.Open = append([]*jsonic.AltSpec{{ + S: [][]int{valTinsLocal, OP}, + B: 1, + P: "expr", + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + pdef := parenOpenByTin[r.O1.Tin] + if pdef == nil || !pdef.Preval.Active { + return false + } + if len(pdef.Preval.Allow) > 0 { + val, _ := r.O0.ResolveVal().(string) + for _, a := range pdef.Preval.Allow { + if a == val { + return true + } + } + return false + } + return true + }, + U: map[string]interface{}{"paren_preval": true}, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + r.Node = r.O0.ResolveVal() + }, + G: "expr,paren,preval", + }}, rs.Open...) + } + + // Block pair detection when inside ternary and the colon + // is a ternary close token (e.g., `1?2:3` — the `2:` should + // NOT be treated as a key-value pair). + if hasTernary { + rs.Open = append([]*jsonic.AltSpec{{ + S: [][]int{j.TokenSet("VAL"), TERN1}, + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_ternary"] > 0 + }, + G: "expr,ternary,block-pair", + }}, rs.Open...) + } + + // Paren open: backtrack and push to 'expr'. + if hasParen { + rs.Open = append([]*jsonic.AltSpec{{ + S: mkS(OP), + B: 1, + P: "expr", + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + pdef := parenOpenByTin[r.O0.Tin] + return !pdef.Preval.Required + }, + G: "expr,paren", + }}, rs.Open...) + } + + // Infix after value: backtrack, replace with 'expr' (only when NOT inside an expr). + if hasInfix { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(INFIX), + B: 1, + N: map[string]int{"expr_prefix": 0, "expr_suffix": 0}, + RF: func(r *jsonic.Rule, ctx *jsonic.Context) string { + if r.N["expr"] < 1 { + return "expr" + } + return "" + }, + G: "expr,infix", + }}, rs.Close...) + } + + // Suffix after value: backtrack, replace with 'expr' (only when NOT inside an expr). + if hasSuffix { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(SUFFIX), + B: 1, + N: map[string]int{"expr_prefix": 0, "expr_suffix": 1}, + RF: func(r *jsonic.Rule, ctx *jsonic.Context) string { + if r.N["expr"] < 1 { + return "expr" + } + return "" + }, + G: "expr,suffix", + }}, rs.Close...) + } + + // Ternary first separator. + if hasTernary { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(TERN0), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr"] < 1 + }, + R: "ternary", + G: "expr,ternary", + }}, rs.Close...) + + // Ternary close: backtrack so ternary rule can consume it. + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(TERN1), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_ternary"] > 0 + }, + G: "expr,ternary,close", + }}, rs.Close...) + } + + // Paren close propagation. + if hasParen { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(CP), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_paren"] > 0 + }, + G: "expr,paren-close", + }}, rs.Close...) + } + + // Prevent implicit list inside expression (comma). + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS([]int{jsonic.TinCA}), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return (r.D == 1 && (r.N["expr"] >= 1 || r.N["expr_ternary"] >= 1)) || + (r.N["expr_ternary"] >= 1 && r.N["expr_paren"] >= 1) + }, + G: "expr,imp,comma", + }}, rs.Close...) + + // Prevent implicit list inside expression (space). + valTins := j.TokenSet("VAL") + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(valTins), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return (r.D == 1 && (r.N["expr"] >= 1 || r.N["expr_ternary"] >= 1)) || + (r.N["expr_ternary"] >= 1 && r.N["expr_paren"] >= 1) + }, + G: "expr,imp,space", + }}, rs.Close...) + }) + + // === LIST rule modifications === + j.Rule("list", func(rs *jsonic.RuleSpec) { + rs.BO = append(rs.BO, func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.Prev == nil || r.Prev == jsonic.NoRule || r.Prev.U["implist"] == nil { + r.N["expr"] = 0 + r.N["expr_prefix"] = 0 + r.N["expr_suffix"] = 0 + r.N["expr_paren"] = 0 + r.N["expr_ternary"] = 0 + } + }) + if hasParen { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(CP), + BF: func(r *jsonic.Rule, ctx *jsonic.Context) int { + if r.C0.Tin == jsonic.TinCS && r.N["expr_paren"] < 1 { + return 0 + } + return 1 + }, + G: "expr,paren,list", + }}, rs.Close...) + // Propagate implicit list node to enclosing paren. + // Go slice append may reallocate, making paren.Child.Node + // (which points to the original val) stale. + rs.AC = append(rs.AC, func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.N["expr_paren"] > 0 && r.Parent != nil && r.Parent != jsonic.NoRule && r.Parent.Name == "paren" { + r.Parent.Node = r.Node + } + }) + } + }) + + // === MAP rule modifications === + j.Rule("map", func(rs *jsonic.RuleSpec) { + rs.BO = append(rs.BO, func(r *jsonic.Rule, ctx *jsonic.Context) { + r.N["expr"] = 0 + r.N["expr_prefix"] = 0 + r.N["expr_suffix"] = 0 + r.N["expr_paren"] = 0 + r.N["expr_ternary"] = 0 + }) + if hasParen { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(CP), + BF: func(r *jsonic.Rule, ctx *jsonic.Context) int { + if r.C0.Tin == jsonic.TinCB && r.N["expr_paren"] < 1 { + return 0 + } + return 1 + }, + G: "expr,paren,map", + }}, rs.Close...) + } + }) + + // === PAIR rule modifications === + j.Rule("pair", func(rs *jsonic.RuleSpec) { + if hasParen { + rs.Close = append([]*jsonic.AltSpec{{ + S: mkS(CP), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_paren"] > 0 || r.N["pk"] > 0 + }, + G: "expr,paren,pair", + }}, rs.Close...) + } + }) + + // === ELEM rule modifications === + j.Rule("elem", func(rs *jsonic.RuleSpec) { + if hasParen { + // Close implicit list within parens when ')' is seen. + rs.Close = append([]*jsonic.AltSpec{ + { + S: mkS(CP), + B: 1, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_paren"] > 0 + }, + G: "expr,paren,elem,close", + }, + // Following elem is a paren expression. + { + S: mkS(OP), + B: 1, + R: "elem", + G: "expr,paren,elem,open", + }, + }, rs.Close...) + // Propagate elem node to enclosing paren after close. + // Go slice append may reallocate, making earlier + // references to the list stale. + rs.AC = append(rs.AC, func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.N["expr_paren"] > 0 { + // Walk parent chain to find paren rule. + for p := r.Parent; p != nil && p != jsonic.NoRule; p = p.Parent { + if p.Name == "paren" { + p.Node = r.Node + break + } + } + } + }) + } + }) + + // === EXPR rule === + exprSpec := &jsonic.RuleSpec{Name: "expr"} + + exprOpen := make([]*jsonic.AltSpec, 0) + + // Paren open inside expression: push to 'paren' rule (not 'val'). + // The 'paren' rule consumes '(' and pushes to 'val', breaking the + // val→expr→val backtrack loop. + if hasParen { + exprOpen = append(exprOpen, &jsonic.AltSpec{ + S: mkS(OP), + P: "paren", + B: 1, + G: "expr,paren,open", + }) + } + + // Prefix operator. + if hasPrefix { + exprOpen = append(exprOpen, &jsonic.AltSpec{ + S: mkS(PREFIX), + P: "val", + N: map[string]int{"expr": 1, "dlist": 1, "dmap": 1}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_prefix"] > 0 + }, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + op := prefixByTin[r.O0.Tin] + if isOp(r.Parent.Node) && isExprOp(r.Parent.Node) { + r.Node = prattify(r.Parent.Node, op) + r.Parent.Node = r.Node // sync after potential reallocation + } else { + r.Node = prior(r, r.Parent, op) + } + }, + G: "expr,prefix", + }) + } + + // Infix operator. + if hasInfix { + exprOpen = append(exprOpen, &jsonic.AltSpec{ + S: mkS(INFIX), + P: "val", + N: map[string]int{"expr": 1, "expr_prefix": 0, "dlist": 1, "dmap": 1}, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + op := infixByTin[r.O0.Tin] + prev := r.Prev + parent := r.Parent + + if isOp(parent.Node) && isExprOp(parent.Node) { + r.Node = prattify(parent.Node, op) + parent.Node = r.Node // sync after potential reallocation + } else if isOp(prev.Node) { + r.Node = prattify(prev.Node, op) + r.Parent = prev + prev.Node = r.Node // sync after potential reallocation + } else { + r.Node = prior(r, prev, op) + } + }, + G: "expr,infix", + }) + } + + // Suffix operator. + if hasSuffix { + exprOpen = append(exprOpen, &jsonic.AltSpec{ + S: mkS(SUFFIX), + N: map[string]int{"expr": 1, "expr_prefix": 0, "dlist": 1, "dmap": 1}, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + op := suffixByTin[r.O0.Tin] + prev := r.Prev + if isOp(prev.Node) { + r.Node = prattifySuffix(prev.Node, op) + } else { + r.Node = prior(r, prev, op) + } + }, + G: "expr,suffix", + }) + } + + exprSpec.Open = exprOpen + + // expr.BC: attach child result to incomplete expression. + // Uses fillNextSlot to find the deepest unfilled slot and fill it. + // This avoids Go slice append issues and works with the Go parser's + // replacement-chain result extraction. + exprSpec.BC = []jsonic.StateAction{ + func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.Child == nil || r.Child == jsonic.NoRule { + return + } + // Paren child: paren.AC already propagated the result. + if r.Child.Name == "paren" { + return + } + childNode := r.Child.Node + if jsonic.IsUndefined(childNode) { + childNode = nil + } + + if sl, ok := r.Node.([]interface{}); ok && len(sl) > 0 { + if _, isOpV := sl[0].(*Op); isOpV { + fillNextSlot(sl, childNode) + } + } + }, + } + + // expr.Close alternates. + exprClose := make([]*jsonic.AltSpec, 0) + + // After paren child (paren rule completed). + if hasParen { + exprClose = append(exprClose, &jsonic.AltSpec{ + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.Child != nil && r.Child != jsonic.NoRule && r.Child.Name == "paren" + }, + N: map[string]int{"expr": 0}, + G: "expr,paren,end", + }) + } + + // More infix (not during prefix). + if hasInfix { + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(INFIX), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_prefix"] < 1 + }, + B: 1, + R: "expr", + G: "expr,infix,more", + }) + // Infix seen during prefix: just end and backtrack. + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(INFIX), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_prefix"] > 0 + }, + B: 1, + G: "expr,infix,prefix-end", + }) + } + + // More suffix (not during prefix). + if hasSuffix { + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(SUFFIX), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_prefix"] < 1 + }, + B: 1, + R: "expr", + G: "expr,suffix,more", + }) + } + + // Paren close inside expression. + if hasParen { + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(CP), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_paren"] > 0 + }, + B: 1, + G: "expr,paren,close", + }) + } + + // Ternary start. + if hasTernary { + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(TERN0), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["expr_prefix"] < 1 + }, + B: 1, + R: "ternary", + G: "expr,ternary", + }) + } + + // Implicit list at top level (comma). + valTins := j.TokenSet("VAL") + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS([]int{jsonic.TinCA}), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.D <= 0 + }, + N: map[string]int{"expr": 0}, + R: "elem", + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + node := r.Node + if isOp(node) { + node = cleanExpr(node.([]interface{})) + } + r.Parent.Node = []interface{}{node} + r.Node = r.Parent.Node + }, + G: "expr,comma,list,top", + }) + + // Implicit list at top level (space). + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(valTins), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.D <= 0 + }, + N: map[string]int{"expr": 0}, + B: 1, + R: "elem", + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + node := r.Node + if isOp(node) { + node = cleanExpr(node.([]interface{})) + } + r.Parent.Node = []interface{}{node} + r.Node = r.Parent.Node + }, + G: "expr,space,list,top", + }) + + // Implicit list inside paren (comma). + // When expr finishes inside a paren (expr_paren > 0) and sees a + // comma, wrap the expression in a list on the paren node and + // replace with elem to process subsequent items. + implicitListAction := func(r *jsonic.Rule, ctx *jsonic.Context) { + // Find enclosing paren rule in the stack. + var paren *jsonic.Rule + for rI := ctx.RSI - 1; rI >= 0; rI-- { + if ctx.RS[rI].Name == "paren" { + paren = ctx.RS[rI] + break + } + } + if paren == nil { + return + } + node := r.Node + if isOp(node) { + node = cleanExpr(node.([]interface{})) + } + // If paren already has a list node, append to it. + // Otherwise create a new list. + if sl, ok := paren.Node.([]interface{}); ok && len(sl) > 0 { + if _, isOpV := sl[0].(*Op); !isOpV { + // It's a plain list, append. + paren.Node = append(sl, node) + r.Node = paren.Node + return + } + } + paren.Node = []interface{}{node} + r.Node = paren.Node + } + if hasParen { + // Only fire when there's no existing list/elem handling + // the implicit list. Walk the parent chain to check if + // there's an elem/list between this expr and the paren. + isFirstImplicitInParen := func(r *jsonic.Rule) bool { + if r.N["expr_paren"] < 1 || r.N["pk"] >= 1 { + return false + } + for p := r.Parent; p != nil && p != jsonic.NoRule; p = p.Parent { + if p.Name == "elem" || p.Name == "list" { + return false // existing list machinery handles it + } + if p.Name == "paren" { + return true // reached paren without finding elem/list + } + } + return true + } + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS([]int{jsonic.TinCA}), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return isFirstImplicitInParen(r) + }, + N: map[string]int{"expr": 0, "expr_prefix": 0, "expr_suffix": 0}, + R: "elem", + A: implicitListAction, + G: "expr,paren,imp,comma", + }) + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS(valTins), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return isFirstImplicitInParen(r) && r.N["expr_suffix"] < 1 + }, + N: map[string]int{"expr": 0, "expr_prefix": 0, "expr_suffix": 0}, + B: 1, + R: "elem", + A: implicitListAction, + G: "expr,paren,imp,space", + }) + } + + // Implicit list (comma, not top). + exprClose = append(exprClose, &jsonic.AltSpec{ + S: mkS([]int{jsonic.TinCA}), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] < 1 + }, + N: map[string]int{"expr": 0}, + B: 1, + G: "expr,list,imp,comma", + }) + + // Implicit list (space, not top). + exprClose = append(exprClose, &jsonic.AltSpec{ + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] < 1 && r.N["expr_suffix"] < 1 + }, + N: map[string]int{"expr": 0}, + G: "expr,list,imp,space", + }) + + exprSpec.Close = exprClose + + // AC: propagate result and evaluate. + exprSpec.AC = []jsonic.StateAction{ + // Propagate expr result to the val it replaced (r.Prev). + // This ensures parent rules (elem, paren) see the expression + // result via their Child.Node, not the stale pre-replacement value. + func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.Prev != nil && r.Prev != jsonic.NoRule { + r.Prev.Node = r.Node + } + }, + // Evaluate if evaluator provided. + func(r *jsonic.Rule, ctx *jsonic.Context) { + if eopts.Evaluate != nil { + if isOp(r.Node) { + r.Node = evaluation(r, ctx, r.Node, eopts.Evaluate) + // Also update Prev to reflect evaluated result. + if r.Prev != nil && r.Prev != jsonic.NoRule { + r.Prev.Node = r.Node + } + } + } + }, + } + + j.RSM()["expr"] = exprSpec + + // === PAREN rule === + // Intermediary rule that consumes '(' and pushes to val. + // This breaks the val→expr→val backtrack loop. + if hasParen { + parenSpec := &jsonic.RuleSpec{Name: "paren"} + + parenSpec.BO = []jsonic.StateAction{ + func(r *jsonic.Rule, ctx *jsonic.Context) { + // Allow implicits inside parens. + r.N["dmap"] = 0 + r.N["dlist"] = 0 + r.N["pk"] = 0 + }, + } + + parenSpec.Open = []*jsonic.AltSpec{ + // Empty parens: () + { + S: func() [][]int { return [][]int{OP, CP} }(), + B: 1, + G: "expr,paren,empty", + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + oOp := parenOpenByTin[r.O0.Tin] + cOp := parenCloseByTin[r.O1.Tin] + return oOp != nil && cOp != nil && oOp.Name == cOp.Name + }, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + pop := parenOpenByTin[r.O0.Tin] + pd := "expr_paren_depth_" + pop.Name + r.U[pd] = 1 + r.N[pd] = 1 + r.Node = jsonic.Undefined + }, + }, + // Normal paren open: consumes '(' and pushes to val. + { + S: mkS(OP), + P: "val", + N: map[string]int{ + "expr_paren": 1, + "expr": 0, + "expr_prefix": 0, + "expr_suffix": 0, + }, + G: "expr,paren,open", + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + pop := parenOpenByTin[r.O0.Tin] + pd := "expr_paren_depth_" + pop.Name + r.U[pd] = 1 + r.N[pd] = 1 + r.Node = jsonic.Undefined + }, + }, + } + + parenSpec.Close = []*jsonic.AltSpec{ + { + S: mkS(CP), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + cop := parenCloseByTin[r.C0.Tin] + if cop == nil { + return false + } + pd := "expr_paren_depth_" + cop.Name + _, ok := r.N[pd] + return ok && r.N[pd] > 0 + }, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + // Construct completed paren expression. + cop := parenCloseByTin[r.C0.Tin] + pop := parenOpenByTin[cop.OTin] + if pop == nil { + // Lookup by matching name. + for _, op := range allOps { + if op.Paren && op.Name == cop.Name { + pop = op + break + } + } + } + if pop == nil { + return + } + + val := r.Node + + // Build paren expression node. + result := []interface{}{pop} + + // Inject function name if preval is active. + if r.Parent != nil && r.Parent != jsonic.NoRule && + r.Parent.Parent != nil && r.Parent.Parent != jsonic.NoRule && + r.Parent.Parent.U["paren_preval"] == true && + r.Parent.Parent.Node != nil { + result = append(result, r.Parent.Parent.Node) + } + + if !jsonic.IsUndefined(val) { + result = append(result, val) + } + + r.Node = result + }, + G: "expr,paren,close", + }, + } + + parenSpec.BC = []jsonic.StateAction{ + func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.Child == nil || r.Child == jsonic.NoRule { + return + } + childNode := r.Child.Node + if jsonic.IsUndefined(childNode) { + return + } + if jsonic.IsUndefined(r.Node) { + r.Node = childNode + } else if isOp(childNode) { + // Don't overwrite if paren.Node is already a plain list + // (set by implicit list handling in elem/ternary). + if !isOp(r.Node) { + if sl, ok := r.Node.([]interface{}); ok && len(sl) > 0 { + return // keep the implicit list + } + } + r.Node = childNode + } + }, + } + + parenSpec.AC = []jsonic.StateAction{ + func(r *jsonic.Rule, ctx *jsonic.Context) { + // Propagate paren result to parent. + r.Parent.Node = r.Node + if r.Parent.Parent != nil && r.Parent.Parent != jsonic.NoRule { + r.Parent.Parent.Node = r.Node + } + }, + } + + j.RSM()["paren"] = parenSpec + } + + // === TERNARY rule === + if hasTernary { + ternarySpec := &jsonic.RuleSpec{Name: "ternary"} + + ternarySpec.Open = []*jsonic.AltSpec{ + { + S: mkS(TERN0), + P: "val", + N: map[string]int{"expr_ternary": 1, "dlist": 1, "dmap": 1, "expr": 0, "expr_prefix": 0, "expr_suffix": 0}, + A: func(r *jsonic.Rule, ctx *jsonic.Context) { + op := ternaryByTin[r.O0.Tin] + prev := r.Prev + prevNode := prev.Node + if isOp(prevNode) { + prevNode = dupExpr(prevNode.([]interface{})) + } + r.Node = makeExpr(op, prevNode) // [op, cond, _unfilled, _unfilled] + prev.Node = r.Node + }, + G: "expr,ternary,open", + }, + } + + ternarySpec.BC = []jsonic.StateAction{ + func(r *jsonic.Rule, ctx *jsonic.Context) { + if r.Child == nil || r.Child == jsonic.NoRule { + return + } + childNode := r.Child.Node + if jsonic.IsUndefined(childNode) { + childNode = nil + } + if sl, ok := r.Node.([]interface{}); ok { + step, _ := r.U["ternary_step"].(int) + if step == 0 { + fillNextSlot(sl, childNode) + r.U["ternary_step"] = 1 + } else if step == 1 { + fillNextSlot(sl, childNode) + r.U["ternary_step"] = 2 + } else if step == 2 { + // Final slot filled when ternary ends + // (e.g., inside an existing elem/list). + fillNextSlot(sl, childNode) + } + } + }, + } + + // Condition for implicit list after ternary completes. + // Only fire when ternary is the FIRST expression — i.e., not already + // inside an elem/list that handles implicit list continuation. + implicitTernaryCond := func(r *jsonic.Rule) bool { + step, _ := r.U["ternary_step"].(int) + if step != 2 || r.N["pk"] >= 1 { + return false + } + if r.D == 0 { + // Top-level: check no elem/list parent exists. + for p := r.Parent; p != nil && p != jsonic.NoRule; p = p.Parent { + if p.Name == "elem" || p.Name == "list" { + return false + } + } + return true + } + if r.N["expr_paren"] >= 1 { + // Inside paren: check no elem/list between ternary and paren. + for p := r.Parent; p != nil && p != jsonic.NoRule; p = p.Parent { + if p.Name == "elem" || p.Name == "list" { + return false + } + if p.Name == "paren" { + return true + } + } + return true + } + return false + } + + // Action to wrap ternary result as first element of implicit list. + implicitTernaryAction := func(r *jsonic.Rule, ctx *jsonic.Context) { + // Fill the last slot with child node. + if r.Child != nil && r.Child != jsonic.NoRule { + childNode := r.Child.Node + if jsonic.IsUndefined(childNode) { + childNode = nil + } + if sl, ok := r.Node.([]interface{}); ok { + fillNextSlot(sl, childNode) + } + } + // Wrap the completed ternary node as the first element of a list. + ternaryNode := r.Node + if isOp(ternaryNode) { + ternaryNode = cleanExpr(ternaryNode.([]interface{})) + } + listNode := []interface{}{ternaryNode} + + // If inside a paren, store the list on paren.Node directly + // (same approach as implicitListAction for expr). + if r.N["expr_paren"] >= 1 { + for rI := ctx.RSI - 1; rI >= 0; rI-- { + if ctx.RS[rI].Name == "paren" { + ctx.RS[rI].Node = listNode + break + } + } + } + r.Node = listNode + } + + ternarySpec.Close = []*jsonic.AltSpec{ + // Second separator (e.g. ':'). + { + S: mkS(TERN1), + P: "val", + N: map[string]int{"expr": 0, "expr_prefix": 0, "expr_suffix": 0}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + step, _ := r.U["ternary_step"].(int) + return step == 1 + }, + G: "expr,ternary,sep2", + }, + + // Implicit list after ternary (comma): 1?2:3,b → [[?,1,2,3],"b"] + { + S: mkS([]int{jsonic.TinCA}), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return implicitTernaryCond(r) + }, + R: "elem", + A: implicitTernaryAction, + G: "expr,ternary,list,imp,comma", + }, + + // Paren close after ternary: backtrack so paren can consume it. + { + S: mkS(CP), + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + step, _ := r.U["ternary_step"].(int) + return step == 2 && r.N["expr_paren"] >= 1 + }, + B: 1, + G: "expr,ternary,paren,close", + }, + + // Implicit list after ternary (space): 1?2:3 b → [[?,1,2,3],"b"] + { + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return implicitTernaryCond(r) && ctx.T0.Tin != jsonic.TinZZ + }, + R: "elem", + A: implicitTernaryAction, + G: "expr,ternary,list,imp,space", + }, + + // End of ternary (deeper depth, or no more tokens). + { + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + step, _ := r.U["ternary_step"].(int) + return step == 2 + }, + G: "expr,ternary,end", + }, + } + + ternarySpec.AC = []jsonic.StateAction{ + func(r *jsonic.Rule, ctx *jsonic.Context) { + if eopts.Evaluate != nil { + if isOp(r.Node) { + r.Node = evaluation(r, ctx, r.Node, eopts.Evaluate) + } + } + }, + } + + j.RSM()["ternary"] = ternarySpec + } +} + +// prior converts a prior rule's node into the start of a new expression. +// Uses pre-allocated expression slices with unfilled sentinel slots. +func prior(rule *jsonic.Rule, priorRule *jsonic.Rule, op *Op) []interface{} { + priorNode := priorRule.Node + if isOp(priorNode) { + priorNode = dupExpr(priorNode.([]interface{})) + } + + var expr []interface{} + if op.Prefix { + expr = makeExpr(op) // [op, _unfilled] + } else { + expr = makeExpr(op, priorNode) // [op, priorNode, _unfilled] + } + priorRule.Node = expr + rule.Parent = priorRule + return expr +} + +// prattify integrates a new operator into the expression tree +// according to operator precedence (Pratt algorithm). +// Always returns the outermost expression (for Go parser compatibility). +func prattify(exprNode interface{}, op *Op) []interface{} { + expr, ok := exprNode.([]interface{}) + if !ok || len(expr) == 0 { + return makeExpr(op, exprNode) + } + + exprOp, isOpV := expr[0].(*Op) + if !isOpV { + return makeExpr(op, exprNode) + } + + // Paren expressions are complete units — never drill into them. + if exprOp.Paren { + return makeExpr(op, dupExpr(expr)) + } + + if op.Infix { + // op is lower or equal precedence: wrap entire expression. + if exprOp.Suffix || op.Left <= exprOp.Right { + return wrapExpr(expr, op) + } + + // op is higher: drill into last term. Create inner expression. + end := exprOp.Terms + if end < len(expr) { + if isOp(expr[end]) { + subExpr := expr[end].([]interface{}) + subOp := subExpr[0].(*Op) + if subOp.Right < op.Left { + expr[end] = prattify(subExpr, op) + return expr + } + } + // Create pre-allocated inner expression with old value as first term. + expr[end] = makeExpr(op, expr[end]) + return expr + } + return expr + } + + if op.Prefix { + end := exprOp.Terms + if end < len(expr) { + expr[end] = makeExpr(op) // [op, _unfilled] + return expr + } + return expr + } + + if op.Suffix { + return prattifySuffix(exprNode, op) + } + + return expr +} + +// wrapExpr wraps an existing expression with a new operator. +// Reuses the slice in-place: [new_op, dup(old), _unfilled, ...] +func wrapExpr(expr []interface{}, op *Op) []interface{} { + oldCopy := dupExpr(expr) + needed := op.Terms + 1 + // Ensure slice is long enough. + for len(expr) < needed { + expr = append(expr, _unfilled) + } + expr[0] = op + expr[1] = oldCopy + // Clear remaining slots and truncate extras. + for i := 2; i < needed; i++ { + expr[i] = _unfilled + } + if len(expr) > needed { + // Clear excess slots to avoid stale data. + for i := needed; i < len(expr); i++ { + expr[i] = nil + } + expr = expr[:needed] + } + return expr +} + +// prattifySuffix integrates a suffix operator into the expression tree. +func prattifySuffix(node interface{}, op *Op) []interface{} { + expr, ok := node.([]interface{}) + if !ok { + return makeExpr(op, node) + } + + exprOp, isOpV := expr[0].(*Op) + if !isOpV { + return makeExpr(op, node) + } + + if !exprOp.Suffix && exprOp.Right <= op.Left { + end := exprOp.Terms + if end < len(expr) { + lastTerm := expr[end] + // Drill into prefix. + if subExpr, ok := lastTerm.([]interface{}); ok && len(subExpr) > 0 { + if subOp, isSub := subExpr[0].(*Op); isSub && subOp.Prefix && subOp.Right < op.Left { + prattifySuffix(subExpr, op) + return expr + } + } + expr[end] = makeExpr(op, lastTerm) + return expr + } + } + + // Wrap entire expression. + return wrapExpr(expr, op) +} + +// cleanExpr removes _unfilled sentinels from an expression tree. +func cleanExpr(expr []interface{}) []interface{} { + out := make([]interface{}, 0, len(expr)) + for _, el := range expr { + if isUnfilled(el) { + continue + } + if sub, ok := el.([]interface{}); ok && isOp(sub) { + out = append(out, cleanExpr(sub)) + } else { + out = append(out, el) + } + } + return out +} + +func dupExpr(expr []interface{}) []interface{} { + out := make([]interface{}, len(expr)) + copy(out, expr) + return out +} + +// Parse is a convenience function. +func Parse(src string, opts ...map[string]interface{}) (interface{}, error) { + j := MakeJsonic(opts...) + return j.Parse(src) +} + +// MakeJsonic creates a jsonic instance configured with the Expr plugin. +func MakeJsonic(opts ...map[string]interface{}) *jsonic.Jsonic { + j := jsonic.Make() + var pluginOpts map[string]interface{} + if len(opts) > 0 { + pluginOpts = opts[0] + } + j.Use(Expr, pluginOpts) + return j +} + +func resolveOptions(opts map[string]interface{}) *ExprOptions { + eopts := &ExprOptions{Op: make(map[string]*OpDef)} + if opts == nil { + addDefaultOps(eopts) + return eopts + } + if opRaw, ok := opts["op"]; ok { + if opMap, ok := opRaw.(map[string]interface{}); ok { + for name, defRaw := range opMap { + if defRaw == nil { + eopts.Op[name] = nil + continue + } + if defMap, ok := defRaw.(map[string]interface{}); ok { + od := &OpDef{} + if v, ok := defMap["src"]; ok { + od.Src = v + } + if v, ok := defMap["osrc"].(string); ok { + od.OSrc = v + } + if v, ok := defMap["csrc"].(string); ok { + od.CSrc = v + } + if v, ok := defMap["left"].(float64); ok { + od.Left = int(v) + } else if v, ok := defMap["left"].(int); ok { + od.Left = v + } + if v, ok := defMap["right"].(float64); ok { + od.Right = int(v) + } else if v, ok := defMap["right"].(int); ok { + od.Right = v + } + if v, ok := defMap["prefix"].(bool); ok { + od.Prefix = v + } + if v, ok := defMap["suffix"].(bool); ok { + od.Suffix = v + } + if v, ok := defMap["infix"].(bool); ok { + od.Infix = v + } + if v, ok := defMap["ternary"].(bool); ok { + od.Ternary = v + } + if v, ok := defMap["paren"].(bool); ok { + od.Paren = v + } + if v, ok := defMap["preval"]; ok { + od.Preval = v + } + if v, ok := defMap["use"]; ok { + od.Use = v + } + eopts.Op[name] = od + } + } + } + } + if evalRaw, ok := opts["evaluate"]; ok { + if evalFn, ok := evalRaw.(func(*jsonic.Rule, *jsonic.Context, *Op, []interface{}) interface{}); ok { + eopts.Evaluate = evalFn + } + } + addDefaultOps(eopts) + return eopts +} + +func addDefaultOps(eopts *ExprOptions) { + defaults := map[string]*OpDef{ + "positive": {Prefix: true, Right: 14000, Src: "+"}, + "negative": {Prefix: true, Right: 14000, Src: "-"}, + "addition": {Infix: true, Left: 140, Right: 150, Src: "+"}, + "subtraction": {Infix: true, Left: 140, Right: 150, Src: "-"}, + "multiplication": {Infix: true, Left: 160, Right: 170, Src: "*"}, + "division": {Infix: true, Left: 160, Right: 170, Src: "/"}, + "remainder": {Infix: true, Left: 160, Right: 170, Src: "%"}, + "plain": {Paren: true, OSrc: "(", CSrc: ")"}, + } + for name, def := range defaults { + if _, exists := eopts.Op[name]; !exists { + eopts.Op[name] = def + } + } +} + +func makeAllOps(j *jsonic.Jsonic, eopts *ExprOptions) []*Op { + // Track registered tins by source string to share between operators + // (e.g., "+" is both prefix "positive" and infix "addition"). + // FixedTokens is a map[string]Tin, so only one tin per source string. + srcTins := make(map[string]int) // src → tin + + getOrCreateTin := func(name, src string) int { + if src == "" { + return j.Token(name) + } + if tin, ok := srcTins[src]; ok { + return tin + } + // Reuse existing fixed token tin if src matches a built-in token + // (e.g., ":" is TinCL, "[" is TinOS). This prevents overriding + // jsonic's built-in token types when operators share syntax. + if existingTin, ok := jsonic.FixedTokens[src]; ok { + srcTins[src] = int(existingTin) + return int(existingTin) + } + tin := j.Token(name, src) + srcTins[src] = tin + return tin + } + + var ops []*Op + for name, def := range eopts.Op { + if def == nil { + continue + } + op := &Op{ + Name: name, Left: def.Left, Right: def.Right, + Prefix: def.Prefix, Suffix: def.Suffix, Infix: def.Infix, + Ternary: def.Ternary, Paren: def.Paren, Use: def.Use, + } + if def.Infix { + op.Terms = 2 + } else if def.Ternary { + op.Terms = 3 + } else { + op.Terms = 1 + } + if def.Paren { + op.OSrc = def.OSrc + op.CSrc = def.CSrc + op.Name = name + "-paren" + op.OTkn = "#E_" + name + "_o" + op.CTkn = "#E_" + name + "_c" + op.OTin = getOrCreateTin(op.OTkn, op.OSrc) + op.CTin = getOrCreateTin(op.CTkn, op.CSrc) + if def.Preval != nil { + switch pv := def.Preval.(type) { + case bool: + op.Preval.Active = pv + case map[string]interface{}: + if v, ok := pv["active"].(bool); ok { + op.Preval.Active = v + } else { + // Default: active=true when preval object is specified + op.Preval.Active = true + } + if v, ok := pv["required"].(bool); ok { + op.Preval.Required = v + } + if v, ok := pv["allow"].([]interface{}); ok { + for _, a := range v { + if s, ok := a.(string); ok { + op.Preval.Allow = append(op.Preval.Allow, s) + } + } + } + if v, ok := pv["allow"].([]string); ok { + op.Preval.Allow = v + } + case PrevalDef: + op.Preval = pv + } + } + } else if def.Ternary { + op.Name = name + "-ternary" + if src, ok := def.Src.([]interface{}); ok && len(src) >= 2 { + op.Src = src[0].(string) + op.CSrc = src[1].(string) + } + op.Tkn = "#E_" + name + op.Tin = getOrCreateTin(op.Tkn, op.Src) + op.CTkn = "#E_" + name + "_c" + op.CTin = getOrCreateTin(op.CTkn, op.CSrc) + } else { + srcStr := "" + if s, ok := def.Src.(string); ok { + srcStr = s + } + op.Src = srcStr + kind := "infix" + if def.Prefix { + kind = "prefix" + } else if def.Suffix { + kind = "suffix" + } + op.Name = name + "-" + kind + op.Tkn = "#E_" + name + op.Tin = getOrCreateTin(op.Tkn, srcStr) + } + ops = append(ops, op) + } + return ops +} + +// Evaluation recursively evaluates an expression tree. +func Evaluation( + rule *jsonic.Rule, ctx *jsonic.Context, node interface{}, + resolve func(*jsonic.Rule, *jsonic.Context, *Op, []interface{}) interface{}, +) interface{} { + return evaluation(rule, ctx, node, resolve) +} + +func evaluation( + rule *jsonic.Rule, ctx *jsonic.Context, node interface{}, + resolve func(*jsonic.Rule, *jsonic.Context, *Op, []interface{}) interface{}, +) interface{} { + expr, isSlice := node.([]interface{}) + if !isSlice || len(expr) == 0 { + return node + } + op, isOpV := expr[0].(*Op) + if !isOpV { + result := make([]interface{}, len(expr)) + for i, el := range expr { + result[i] = evaluation(rule, ctx, el, resolve) + } + return result + } + terms := make([]interface{}, 0, len(expr)-1) + for _, sub := range expr[1:] { + if isUnfilled(sub) { + continue + } + terms = append(terms, evaluation(rule, ctx, sub, resolve)) + } + return resolve(rule, ctx, op, terms) +} + +// Simplify converts an expression tree with *Op nodes into plain +// arrays/maps with string operator names. +func Simplify(node interface{}) interface{} { + switch v := node.(type) { + case []interface{}: + if len(v) == 0 { + return v + } + if op, isOpV := v[0].(*Op); isOpV { + result := make([]interface{}, 0, len(v)) + src := op.Src + if op.Paren { + src = op.OSrc + } + result = append(result, src) + for _, el := range v[1:] { + if isUnfilled(el) { + continue + } + s := Simplify(el) + if s != nil { + result = append(result, s) + } + } + return result + } + result := make([]interface{}, len(v)) + for i, el := range v { + result[i] = Simplify(el) + } + return result + case map[string]interface{}: + result := make(map[string]interface{}) + for k, val := range v { + result[k] = Simplify(val) + } + return result + default: + return node + } +} diff --git a/go/expr_test.go b/go/expr_test.go new file mode 100644 index 0000000..beffeff --- /dev/null +++ b/go/expr_test.go @@ -0,0 +1,850 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +package expr + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + jsonic "github.com/jsonicjs/jsonic/go" +) + +// specEntry holds one line from a TSV spec file. +type specEntry struct { + input string + expected interface{} +} + +// loadSpec reads a TSV spec file and returns parsed entries. +func loadSpec(t *testing.T, name string) []specEntry { + t.Helper() + + // Find spec dir relative to this test file. + _, filename, _, _ := runtime.Caller(0) + specDir := filepath.Join(filepath.Dir(filename), "..", "test", "spec") + specPath := filepath.Join(specDir, name) + + f, err := os.Open(specPath) + if err != nil { + t.Fatalf("failed to open spec file %s: %v", specPath, err) + } + defer f.Close() + + var entries []specEntry + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "\t", 2) + if len(parts) != 2 { + continue + } + var expected interface{} + if err := json.Unmarshal([]byte(parts[1]), &expected); err != nil { + t.Fatalf("failed to parse expected JSON in %s: %q: %v", name, parts[1], err) + } + entries = append(entries, specEntry{input: parts[0], expected: expected}) + } + if err := scanner.Err(); err != nil { + t.Fatalf("error reading spec file %s: %v", name, err) + } + return entries +} + +// simplifyAndNormalize converts the parse result to simplified form +// and normalizes it to match JSON-parsed expected values. +func simplifyAndNormalize(node interface{}) interface{} { + simplified := Simplify(node) + // Round-trip through JSON to normalize types (float64 for numbers, etc.) + b, err := json.Marshal(simplified) + if err != nil { + return simplified + } + var normalized interface{} + if err := json.Unmarshal(b, &normalized); err != nil { + return simplified + } + return normalized +} + +// runSpec runs all entries from a TSV spec file against a jsonic instance. +func runSpec(t *testing.T, specName string, j *jsonic.Jsonic) { + t.Helper() + entries := loadSpec(t, specName) + for _, e := range entries { + t.Run(e.input, func(t *testing.T) { + result, err := j.Parse(e.input) + if err != nil { + t.Fatalf("parse error for %q: %v", e.input, err) + } + got := simplifyAndNormalize(result) + if !reflect.DeepEqual(got, e.expected) { + gotJSON, _ := json.Marshal(got) + expJSON, _ := json.Marshal(e.expected) + t.Errorf("input: %q\n got: %s\n want: %s", e.input, gotJSON, expJSON) + } + }) + } +} + +func makeExprJsonic(opOpts ...map[string]interface{}) *jsonic.Jsonic { + j := jsonic.Make() + var opts map[string]interface{} + if len(opOpts) > 0 { + opts = opOpts[0] + } + j.Use(Expr, opts) + return j +} + +func TestSpecHappy(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "happy.tsv", j) +} + +func TestSpecBinary(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "binary.tsv", j) +} + +func TestSpecStructure(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "structure.tsv", j) +} + +func TestSpecUnaryPrefixBasic(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "unary-prefix-basic.tsv", j) +} + +func TestSpecUnaryPrefixEdge(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "at": map[string]interface{}{ + "prefix": true, "right": 15000, "src": "@", + }, + "tight": map[string]interface{}{ + "infix": true, "left": 120000, "right": 130000, "src": "~", + }, + }, + }) + runSpec(t, "unary-prefix-edge.tsv", j) +} + +func TestSpecUnarySuffixBasic(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "left": 15000, "src": "!", + }, + "question": map[string]interface{}{ + "suffix": true, "left": 13000, "src": "?", + }, + }, + }) + runSpec(t, "unary-suffix-basic.tsv", j) +} + +func TestSpecUnarySuffixEdge(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "left": 15000, "src": "!", + }, + "question": map[string]interface{}{ + "suffix": true, "left": 13000, "src": "?", + }, + "tight": map[string]interface{}{ + "infix": true, "left": 120000, "right": 130000, "src": "~", + }, + }, + }) + runSpec(t, "unary-suffix-edge.tsv", j) +} + +func TestSpecUnarySuffixStructure(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "left": 15000, "src": "!", + }, + "question": map[string]interface{}{ + "suffix": true, "left": 13000, "src": "?", + }, + }, + }) + runSpec(t, "unary-suffix-structure.tsv", j) +} + +func TestSpecUnarySuffixPrefix(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "left": 15000, "src": "!", + }, + "question": map[string]interface{}{ + "suffix": true, "left": 13000, "src": "?", + }, + }, + }) + runSpec(t, "unary-suffix-prefix.tsv", j) +} + +func TestSpecUnarySuffixParen(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "left": 15000, "src": "!", + }, + "question": map[string]interface{}{ + "suffix": true, "left": 13000, "src": "?", + }, + }, + }) + runSpec(t, "unary-suffix-paren.tsv", j) +} + +func TestSpecParenBasic(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-basic.tsv", j) +} + +func TestSpecImplicitListTopBasic(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "implicit-list-top-basic.tsv", j) +} + +func TestSpecTernaryBasic(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "src": "!", "left": 15000, + }, + "ternary": map[string]interface{}{ + "ternary": true, "src": []interface{}{"?", ":"}, + }, + }, + }) + runSpec(t, "ternary-basic.tsv", j) +} + +func TestTernaryBasicImplicitList(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "src": "!", "left": 15000, + }, + "ternary": map[string]interface{}{ + "ternary": true, "src": []interface{}{"?", ":"}, + }, + }, + }) + + tests := []struct { + input string + expected interface{} + }{ + // Top-level implicit lists with ternary. + {"a 1?2:3", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}, + {"1?2:3 b", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + {"a 1?2:3 b", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + {"a,1?2:3", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}, + {"1?2:3,b", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + {"a,1?2:3,b", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + // Inside parens. + {"(a 1?2:3)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}}, + {"(1?2:3 b)", []interface{}{"(", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"(a 1?2:3 b)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"(a,1?2:3)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}}, + {"(1?2:3,b)", []interface{}{"(", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"(a,1?2:3,b)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := j.Parse(tt.input) + if err != nil { + t.Fatalf("parse error for %q: %v", tt.input, err) + } + got := simplifyAndNormalize(result) + if !reflect.DeepEqual(got, tt.expected) { + gotJSON, _ := json.Marshal(got) + expJSON, _ := json.Marshal(tt.expected) + t.Errorf("got: %s\nwant: %s", gotJSON, expJSON) + } + }) + } +} + +func TestSpecJSONBase(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "json-base.tsv", j) +} + +func TestSpecParenImplicitMap(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-implicit-map.tsv", j) +} + +func TestSpecJsonicBase(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "jsonic-base.tsv", j) +} + +func TestSpecImplicitListTopParen(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "implicit-list-top-paren.tsv", j) +} + +func TestSpecParenImplicitList(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-implicit-list.tsv", j) +} + +func TestSpecMapImplicitListParen(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "map-implicit-list-paren.tsv", j) +} + +func TestSpecParenListImplicitStructureComma(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-list-implicit-structure-comma.tsv", j) +} + +func TestSpecParenListImplicitStructureSpace(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-list-implicit-structure-space.tsv", j) +} + +func TestSpecParenMapImplicitStructureComma(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-map-implicit-structure-comma.tsv", j) +} + +func TestSpecParenMapImplicitStructureSpace(t *testing.T) { + j := makeExprJsonic() + runSpec(t, "paren-map-implicit-structure-space.tsv", j) +} + +func TestSpecAddInfix(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "foo": map[string]interface{}{ + "infix": true, "left": 180, "right": 190, "src": "foo", + }, + }, + }) + runSpec(t, "add-infix.tsv", j) +} + +// TestSimplify verifies the Simplify function. +func TestSimplify(t *testing.T) { + op := &Op{Name: "addition-infix", Src: "+", Infix: true} + expr := []interface{}{op, 1.0, 2.0} + got := Simplify(expr) + + expected := []interface{}{"+", 1.0, 2.0} + if !reflect.DeepEqual(got, expected) { + t.Errorf("Simplify: got %v, want %v", got, expected) + } +} + +// TestEvaluation verifies basic evaluation. +func TestEvaluation(t *testing.T) { + mathResolve := func(r *jsonic.Rule, ctx *jsonic.Context, op *Op, terms []interface{}) interface{} { + switch op.Name { + case "addition-infix": + return toFloat(terms[0]) + toFloat(terms[1]) + case "subtraction-infix": + return toFloat(terms[0]) - toFloat(terms[1]) + case "multiplication-infix": + return toFloat(terms[0]) * toFloat(terms[1]) + case "negative-prefix": + return -1 * toFloat(terms[0]) + case "positive-prefix": + return toFloat(terms[0]) + case "plain-paren": + if len(terms) > 0 { + return terms[0] + } + return nil + default: + return nil + } + } + + j := jsonic.Make() + j.Use(Expr, nil) + + tests := []struct { + input string + expected float64 + }{ + {"1+2", 3}, + {"1+2+3", 6}, + {"1*2+3", 5}, + {"1+2*3", 7}, + {"(1+2)*3", 9}, + {"3*(1+2)", 9}, + {"(1)", 1}, + {"(1+2)", 3}, + {"3+(1+2)", 6}, + {"(1+2)+3", 6}, + {"111+222", 333}, + {"(111+222)", 333}, + {"111+(222)", 333}, + {"(111)+222", 333}, + {"(111)+(222)", 333}, + {"(1+2)*4", 12}, + {"1+(2*4)", 9}, + {"((1+2)*4)", 12}, + {"(1+(2*4))", 9}, + {"((114))", 114}, + {"(((115)))", 115}, + {"1-3", -2}, + {"-1", -1}, + {"+1", 1}, + {"1+(-3)", -2}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := j.Parse(tt.input) + if err != nil { + t.Fatalf("parse error: %v", err) + } + val := Evaluation(nil, nil, result, mathResolve) + if got := toFloat(val); got != tt.expected { + t.Errorf("got %v, want %v", got, tt.expected) + } + }) + } +} + +func toFloat(v interface{}) float64 { + switch n := v.(type) { + case float64: + return n + case int: + return float64(n) + case int64: + return float64(n) + default: + return 0 + } +} + +// TestParseConvenience tests the Parse convenience function. +func TestParseConvenience(t *testing.T) { + result, err := Parse("1+2") + if err != nil { + t.Fatalf("Parse error: %v", err) + } + got := simplifyAndNormalize(result) + expected := []interface{}{"+", float64(1), float64(2)} + expectedJSON, _ := json.Marshal(expected) + gotJSON, _ := json.Marshal(got) + if string(gotJSON) != string(expectedJSON) { + t.Errorf("got %s, want %s", gotJSON, expectedJSON) + } + _ = fmt.Sprintf("") // use fmt +} + +// TestEvaluateSets verifies set union/intersection evaluation with custom operators. +func TestEvaluateSets(t *testing.T) { + setResolve := func(r *jsonic.Rule, ctx *jsonic.Context, op *Op, terms []interface{}) interface{} { + switch op.Name { + case "plain-paren": + if len(terms) > 0 { + return terms[0] + } + return nil + case "union-infix": + a := toIntSlice(terms[0]) + b := toIntSlice(terms[1]) + seen := make(map[int]bool) + var result []int + for _, v := range a { + if !seen[v] { + seen[v] = true + result = append(result, v) + } + } + for _, v := range b { + if !seen[v] { + seen[v] = true + result = append(result, v) + } + } + sortInts(result) + return intsToInterface(result) + case "intersection-infix": + a := toIntSlice(terms[0]) + b := toIntSlice(terms[1]) + setA := make(map[int]bool) + for _, v := range a { + setA[v] = true + } + var result []int + seen := make(map[int]bool) + for _, v := range b { + if setA[v] && !seen[v] { + seen[v] = true + result = append(result, v) + } + } + sortInts(result) + return intsToInterface(result) + default: + return []interface{}{} + } + } + + j := jsonic.Make() + j.Use(Expr, map[string]interface{}{ + "op": map[string]interface{}{ + "union": map[string]interface{}{ + "infix": true, "src": "U", "left": 140, "right": 150, + }, + "intersection": map[string]interface{}{ + "infix": true, "src": "N", "left": 140, "right": 150, + }, + }, + }) + + tests := []struct { + input string + expected []int + }{ + {"[1]U[2]", []int{1, 2}}, + {"[1,3]U[1,2]", []int{1, 2, 3}}, + {"[1,3]N[1,2]", []int{1}}, + {"[1,3]N[2]", []int{}}, + {"[1,3]N[2,1]", []int{1}}, + {"[1,3]N[2]U[1,2]", []int{1, 2}}, + {"[1,3]N([2]U[1,2])", []int{1}}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := j.Parse(tt.input) + if err != nil { + t.Fatalf("parse error for %q: %v", tt.input, err) + } + val := Evaluation(nil, nil, result, setResolve) + got := toIntSlice(val) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("got %v, want %v", got, tt.expected) + } + }) + } +} + +func toIntSlice(v interface{}) []int { + switch s := v.(type) { + case []interface{}: + result := make([]int, 0, len(s)) + for _, el := range s { + result = append(result, int(toFloat(el))) + } + return result + case []int: + return s + default: + return []int{} + } +} + +func intsToInterface(nums []int) []interface{} { + result := make([]interface{}, len(nums)) + for i, n := range nums { + result[i] = float64(n) + } + return result +} + +func sortInts(a []int) { + for i := 0; i < len(a); i++ { + for j := i + 1; j < len(a); j++ { + if a[j] < a[i] { + a[i], a[j] = a[j], a[i] + } + } + } +} + +// TestExampleDotpath verifies custom dot-path operator with evaluation. +func TestExampleDotpath(t *testing.T) { + // Go's makeAllOps appends "-infix"/"-prefix" to the user-provided name, + // so "dot" becomes "dot-infix" and "dot-prefix" respectively. + dotResolve := func(r *jsonic.Rule, ctx *jsonic.Context, op *Op, terms []interface{}) interface{} { + switch op.Name { + case "dot-infix": + parts := make([]string, len(terms)) + for i, term := range terms { + parts[i] = fmt.Sprintf("%v", term) + } + return strings.Join(parts, "/") + case "dotpre-prefix": + return "/" + fmt.Sprintf("%v", terms[0]) + case "plain-paren": + if len(terms) > 0 { + return terms[0] + } + return nil + case "positive-prefix": + return terms[0] + case "addition-infix": + return toFloat(terms[0]) + toFloat(terms[1]) + default: + return nil + } + } + + j := jsonic.Make() + j.Use(Expr, map[string]interface{}{ + "op": map[string]interface{}{ + "dot": map[string]interface{}{ + "src": ".", "infix": true, "left": 15000000, "right": 14000000, + }, + "dotpre": map[string]interface{}{ + "src": ".", "prefix": true, "right": 14000000, + }, + }, + }) + + tests := []struct { + input string + expected interface{} + }{ + {"a.b", "a/b"}, + {"a.b.c", "a/b/c"}, + {"a.b.c.d", "a/b/c/d"}, + {".a", "/a"}, + {".a.b", "/a/b"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := j.Parse(tt.input) + if err != nil { + t.Fatalf("parse error: %v", err) + } + val := Evaluation(nil, nil, result, dotResolve) + if val != tt.expected { + t.Errorf("got %v, want %v", val, tt.expected) + } + }) + } +} + +func TestSpecPrevalBasic(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "angle": map[string]interface{}{ + "osrc": "<", "csrc": ">", "paren": true, + "preval": map[string]interface{}{"active": true}, + }, + }, + }) + runSpec(t, "paren-preval-basic.tsv", j) +} + +func TestSpecPrevalOverload(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "factorial": map[string]interface{}{ + "suffix": true, "left": 15000, "src": "!", + }, + "square": map[string]interface{}{ + "osrc": "[", "csrc": "]", "paren": true, + "preval": map[string]interface{}{"required": true}, + }, + "brace": map[string]interface{}{ + "osrc": "{", "csrc": "}", "paren": true, + "preval": map[string]interface{}{"required": true}, + }, + }, + }) + runSpec(t, "paren-preval-overload.tsv", j) +} + +func TestSpecPrevalImplicit(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "plain": map[string]interface{}{ + "paren": true, "osrc": "(", "csrc": ")", + "preval": map[string]interface{}{"active": true}, + }, + }, + }) + runSpec(t, "paren-preval-implicit.tsv", j) +} + +func TestSpecAddParen(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "angle": map[string]interface{}{ + "paren": true, "osrc": "<", "csrc": ">", + }, + }, + }) + runSpec(t, "add-paren.tsv", j) +} + +func TestTernaryMany(t *testing.T) { + // Two ternary operators. + j0 := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "foo": map[string]interface{}{ + "ternary": true, + "src": []interface{}{"?", ":"}, + }, + "bar": map[string]interface{}{ + "ternary": true, + "src": []interface{}{"QQ", "CC"}, + }, + }, + }) + + tests0 := []struct { + input string + expected interface{} + }{ + {"a:1", map[string]interface{}{"a": float64(1)}}, + {"1?2:3", []interface{}{"?", float64(1), float64(2), float64(3)}}, + {"1QQ2CC3", []interface{}{"QQ", float64(1), float64(2), float64(3)}}, + {"1QQ2?4:5CC3", []interface{}{"QQ", float64(1), []interface{}{"?", float64(2), float64(4), float64(5)}, float64(3)}}, + {"1?2QQ4CC5:3", []interface{}{"?", float64(1), []interface{}{"QQ", float64(2), float64(4), float64(5)}, float64(3)}}, + } + + for _, tt := range tests0 { + t.Run("j0/"+tt.input, func(t *testing.T) { + result, err := j0.Parse(tt.input) + if err != nil { + t.Fatalf("parse error: %v", err) + } + got := simplifyAndNormalize(result) + if !reflect.DeepEqual(got, tt.expected) { + gotJSON, _ := json.Marshal(got) + expJSON, _ := json.Marshal(tt.expected) + t.Errorf("got: %s\nwant: %s", gotJSON, expJSON) + } + }) + } + + // Three ternary operators. + j1 := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "foo": map[string]interface{}{ + "ternary": true, + "src": []interface{}{"?", ":"}, + }, + "bar": map[string]interface{}{ + "ternary": true, + "src": []interface{}{"QQ", "CC"}, + }, + "zed": map[string]interface{}{ + "ternary": true, + "src": []interface{}{"%%", "@@"}, + }, + }, + }) + + tests1 := []struct { + input string + expected interface{} + }{ + {"a:1", map[string]interface{}{"a": float64(1)}}, + {"1?2:3", []interface{}{"?", float64(1), float64(2), float64(3)}}, + {"1QQ2CC3", []interface{}{"QQ", float64(1), float64(2), float64(3)}}, + {"1%%2@@3", []interface{}{"%%", float64(1), float64(2), float64(3)}}, + } + + for _, tt := range tests1 { + t.Run("j1/"+tt.input, func(t *testing.T) { + result, err := j1.Parse(tt.input) + if err != nil { + t.Fatalf("parse error: %v", err) + } + got := simplifyAndNormalize(result) + if !reflect.DeepEqual(got, tt.expected) { + gotJSON, _ := json.Marshal(got) + expJSON, _ := json.Marshal(tt.expected) + t.Errorf("got: %s\nwant: %s", gotJSON, expJSON) + } + }) + } +} + +func TestTernaryParenPreval(t *testing.T) { + j := makeExprJsonic(map[string]interface{}{ + "op": map[string]interface{}{ + "ternary": map[string]interface{}{ + "ternary": true, + "src": []interface{}{"?", ":"}, + }, + "plain": map[string]interface{}{ + "paren": true, "osrc": "(", "csrc": ")", + "preval": map[string]interface{}{}, + }, + }, + }) + + tests := []struct { + input string + expected interface{} + }{ + {"a:1", map[string]interface{}{"a": float64(1)}}, + {"1?2:3", []interface{}{"?", float64(1), float64(2), float64(3)}}, + + {"a 1?2:3", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}, + {"a 1?2:3 b", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + {"1?2:3 b", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + {"1?2:3,b", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + + {"a,1?2:3", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}, + {"a,1?2:3,b", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}, + + {"(1?2:3)", []interface{}{"(", []interface{}{"?", float64(1), float64(2), float64(3)}}}, + {"(1?2:3 b)", []interface{}{"(", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"(1?2:3,b)", []interface{}{"(", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"(a 1?2:3)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}}, + {"(a 1?2:3 b)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + + {"(a,1?2:3)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}}, + {"(a,1?2:3,b)", []interface{}{"(", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + + {"foo(a 1?2:3)", []interface{}{"(", "foo", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}}, + {"foo(1?2:3 b)", []interface{}{"(", "foo", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"foo(a 1?2:3 b)", []interface{}{"(", "foo", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + + {"foo(a,1?2:3)", []interface{}{"(", "foo", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}}}}, + {"foo(1?2:3,b)", []interface{}{"(", "foo", []interface{}{[]interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + {"foo(a,1?2:3,b)", []interface{}{"(", "foo", []interface{}{"a", []interface{}{"?", float64(1), float64(2), float64(3)}, "b"}}}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result, err := j.Parse(tt.input) + if err != nil { + t.Fatalf("parse error for %q: %v", tt.input, err) + } + got := simplifyAndNormalize(result) + if !reflect.DeepEqual(got, tt.expected) { + gotJSON, _ := json.Marshal(got) + expJSON, _ := json.Marshal(tt.expected) + t.Errorf("got: %s\nwant: %s", gotJSON, expJSON) + } + }) + } +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..37e13b8 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,5 @@ +module github.com/jsonicjs/expr/go + +go 1.24.7 + +require github.com/jsonicjs/jsonic/go v0.1.6 diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..4ea3f83 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,2 @@ +github.com/jsonicjs/jsonic/go v0.1.6 h1:oUw4vxCK6tqa7SGN87vjCtx3sCpeHXdqfl25hx5LKP0= +github.com/jsonicjs/jsonic/go v0.1.6/go.mod h1:ObNKlCG7esWoi4AHCpdgkILvPINV8bpvkbCd4llGGUg= diff --git a/package.json b/package.json index 7e6c41c..c5b8838 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,6 @@ "typescript": "^5.7.3" }, "peerDependencies": { - "jsonic": ">=2" + "jsonic": ">=2.20.1" } } diff --git a/test/gen-extra.mjs b/test/gen-extra.mjs new file mode 100644 index 0000000..51f56e7 --- /dev/null +++ b/test/gen-extra.mjs @@ -0,0 +1,47 @@ +import Jsonic from '@jsonic/jsonic-next' +import { Expr } from '../dist/expr.js' +import { writeFileSync } from 'fs' + +function gen(name, inputs, j) { + const lines = [`# ${name}\n# input\texpected_output`] + for (const input of inputs) { + try { + const result = j(input) + lines.push(`${input}\t${JSON.stringify(result)}`) + } catch (e) { + console.error(`ERROR: ${name}: ${input}: ${e.message}`) + } + } + writeFileSync(`test/spec/${name}.tsv`, lines.join('\n') + '\n') + console.log(`Wrote ${name}.tsv (${inputs.length} cases)`) +} + +const je_overload = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + square: { osrc: '[', csrc: ']', paren: true, preval: { required: true } }, + brace: { osrc: '{', csrc: '}', paren: true, preval: { required: true } } + } +}) +const jo = (s) => je_overload(s) +gen('paren-preval-overload', [ + '[1]', 'a[1]', '[a[1]]', 'a:[1]', 'a:b[1]', 'a:[b[1]]', + '{a:[1]}', '{a:b[1]}', '{a:[b[1]]}', + '-[1]+2', '-a[1]+2', '-[a[1]]+2', + '2+[1]', '2+a[1]', '2+[a[1]]', + '2+{a:[1]}', '2+{a:b[1]}', '2+{a:[b[1]]}', + 'a[b[1]]', 'a[b[c[1]]]', 'a[b[c[d[1]]]]', + 'a{1}', 'a{b{1}}', 'a{b{c{1}}}', + 'a{1+2}', 'a{b{1+2}}', 'a{b{c{1+2}}}', + 'a{{x:1}}', 'a{{x:1,y:2}}', +], jo) + +const je_impl = Jsonic.make().use(Expr, { + op: { plain: { preval: true } } +}) +const ji = (s) => je_impl(s) +gen('paren-preval-implicit', [ + 'foo,(1,a)', 'foo,(1+2,a)', 'foo,(1+2+3,a)', +], ji) + +console.log('Done!') diff --git a/test/gen-new-specs.mjs b/test/gen-new-specs.mjs new file mode 100644 index 0000000..a6fce3b --- /dev/null +++ b/test/gen-new-specs.mjs @@ -0,0 +1,184 @@ +import Jsonic from '@jsonic/jsonic-next' +import { Expr } from '../dist/expr.js' +import { writeFileSync } from 'fs' +const { omap } = Jsonic.util + +const C = (x) => JSON.parse(JSON.stringify(x)) +const S = (x, seen) => ( + seen = seen ?? new WeakSet(), + seen?.has(x) ? '[CIRCLE]' : ( + (x && 'object' === typeof x ? seen?.add(x) : null), + (x && Array.isArray(x)) ? + (0 === x.length ? x : [ + x[0].src || S(x[0], seen), + ...(1 < x.length ? (x.slice(1).map((t) => S(t, seen))) : [])] + .filter(t => undefined !== t)) : + (null != x && 'object' === typeof (x) ? omap(x, ([n, v]) => [n, S(v, seen)]) : x))) +const mj = (je) => (s) => C(S(je(s))) + +function gen(name, inputs, j) { + const lines = [`# ${name}\n# input\texpected_output`] + for (const input of inputs) { + try { + const result = j(input) + lines.push(`${input}\t${JSON.stringify(result)}`) + } catch (e) { + console.error(`ERROR: ${name}: ${input}: ${e.message}`) + } + } + writeFileSync(`test/spec/${name}.tsv`, lines.join('\n') + '\n') + console.log(`Wrote ${name}.tsv (${inputs.length} cases)`) +} + +const je = Jsonic.make().use(Expr) +const j = mj(je) + +gen('implicit-list-top-paren', [ + '(1,2)', '(1+2,3)', '(1+2+3,4)', '(1+2+3+4,5)', + '(1 2)', '(1+2 3)', '(1+2+3 4)', '(1+2+3+4 5)', + '(1,2,11)', '(1+2,3,11)', '(1+2+3,4,11)', '(1+2+3+4,5,11)', + '(1 2 11)', '(1+2 3 11)', '(1+2+3 4 11)', '(1+2+3+4 5 11)', + '(22,1,2,11)', '(22,1+2,3,11)', '(22,1+2+3,4,11)', '(22,1+2+3+4,5,11)', + '(22 1 2 11)', '(22 1+2 3 11)', '(22 1+2+3 4 11)', '(22 1+2+3+4 5 11)', + '([true,false],1,2,11)', '([true,false],1+2,3,11)', '([true,false],1+2+3,4,11)', '([true,false],1+2+3+4,5,11)', + '([true,false] 1 2 11)', '([true,false] 1+2 3 11)', '([true,false] 1+2+3 4 11)', '([true,false] 1+2+3+4 5 11)', + '([true,false],1,2,{x:11,y:22})', '([true,false],1+2,3,{x:11,y:22})', '([true,false],1+2+3,4,{x:11,y:22})', '([true,false],1+2+3+4,5,{x:11,y:22})', + '([true,false] 1 2 {x:11,y:22})', '([true,false] 1+2 3 {x:11,y:22})', '([true,false] 1+2+3 4 {x:11,y:22})', '([true,false] 1+2+3+4 5 {x:11,y:22})', + '(1+2,3+4)', '(1+2,3+4,5+6)', '(1+2 3+4)', '(1+2 3+4 5+6)', +], j) + +gen('paren-implicit-list', [ + '(a)', '(a,b)', '(a,b,c)', '(a,b,c,d)', + '(1,2)', '(1+2,3)', '(1+2+3,4)', '(1+2+3+4,5)', + '(1+2,3,4)', '(1+2,3+4,5)', '(1+2,3+4,5+6)', + '(a b)', '(a b c)', + '(1+2 3)', '(1+2 3 4)', '(1+2 3+4 5)', '(1+2 3+4 5+6)', + 'foo(1,a)', 'foo,(1,a)', 'foo (1,a)', +], j) + +gen('paren-implicit-map', [ + '(a:1,b:2)', '(a:1 b:2)', '(a:1,b:2,c:3)', '(a:1 b:2 c:3)', + '(a:1+2,b:3)', '(a:1+2,b:3,c:4)', '(a:1+2,b:3+4,c:5)', '(a:1+2,b:3+4,c:5+6)', + '(a:1+2 b:3)', '(a:1+2 b:3 c:4)', '(a:1+2 b:3+4 c:5)', '(a:1+2 b:3+4 c:5+6)', +], j) + +gen('map-implicit-list-paren', [ + 'a:(1,2),b:0', 'a:(1+2,3),b:0', 'a:(1+2+3,4),b:0', 'a:(1+2+3+4,5),b:0', + 'a:(1 2),b:0', 'a:(1+2 3),b:0', 'a:(1+2+3 4),b:0', 'a:(1+2+3+4 5),b:0', + 'a:(1,2,11),b:0', 'a:(1+2,3,11),b:0', 'a:(1+2+3,4,11),b:0', 'a:(1+2+3+4,5,11),b:0', + 'a:(1 2 11),b:0', 'a:(1+2 3 11),b:0', 'a:(1+2+3 4 11),b:0', 'a:(1+2+3+4 5 11),b:0', + '{a:(1,2),b:0}', '{a:(1+2,3),b:0}', '{a:(1+2+3,4),b:0}', '{a:(1+2+3+4,5),b:0}', + '{a:(1 2),b:0}', '{a:(1+2 3),b:0}', '{a:(1+2+3 4),b:0}', '{a:(1+2+3+4 5),b:0}', + '{a:(1+2,3+4)}', '{a:(1+2,3+4,5+6)}', '{a:(1+2 3+4)}', '{a:(1+2 3+4 5+6)}', +], j) + +gen('paren-map-implicit-structure-comma', [ + '{a:(1)}', '{a:(1,2)}', '{a:(1,2,3)}', + '{a:(1),b:9}', '{a:(1,2),b:9}', '{a:(1,2,3),b:9}', + '{a:(1),b:9,c:8}', '{a:(1,2),b:9,c:8}', '{a:(1,2,3),b:9,c:8}', + '{a:(1),b:(9)}', '{a:(1,2),b:(9)}', '{a:(1,2,3),b:(9)}', + '{a:(1),b:(8,9)}', '{a:(1,2),b:(8,9)}', '{a:(1,2,3),b:(8,9)}', + '{d:0,a:(1)}', '{d:0,a:(1,2)}', '{d:0,a:(1,2,3)}', + '{d:0,a:(1),b:9}', '{d:0,a:(1,2),b:9}', '{d:0,a:(1,2,3),b:9}', + '{d:0,a:(1),b:(8,9)}', '{d:0,a:(1,2),b:(8,9)}', '{d:0,a:(1,2,3),b:(8,9)}', + 'a:(1)', 'a:(1,2)', 'a:(1,2,3)', + 'a:(1),b:9', 'a:(1,2),b:9', 'a:(1,2,3),b:9', + 'a:(1),b:(8,9)', 'a:(1,2),b:(8,9)', 'a:(1,2,3),b:(8,9)', + 'd:0,a:(1)', 'd:0,a:(1,2)', 'd:0,a:(1,2,3)', + 'd:0,a:(1),b:9', 'd:0,a:(1,2),b:9', 'd:0,a:(1,2,3),b:9', + 'd:0,a:(1),b:(8,9)', 'd:0,a:(1,2),b:(8,9)', 'd:0,a:(1,2,3),b:(8,9)', +], j) + +gen('paren-map-implicit-structure-space', [ + '{a:(1)}', '{a:(1 2)}', '{a:(1 2 3)}', + '{a:(1) b:9}', '{a:(1 2) b:9}', '{a:(1 2 3) b:9}', + '{a:(1) b:9 c:8}', '{a:(1 2) b:9 c:8}', '{a:(1 2 3) b:9 c:8}', + '{a:(1) b:(9)}', '{a:(1 2) b:(9)}', '{a:(1 2 3) b:(9)}', + '{a:(1) b:(8 9)}', '{a:(1 2) b:(8 9)}', '{a:(1 2 3) b:(8 9)}', + '{d:0,a:(1)}', '{d:0,a:(1 2)}', '{d:0,a:(1 2 3)}', + '{d:0,a:(1) b:9}', '{d:0,a:(1 2) b:9}', '{d:0,a:(1 2 3) b:9}', + '{d:0,a:(1) b:(8 9)}', '{d:0,a:(1 2) b:(8 9)}', '{d:0,a:(1 2 3) b:(8 9)}', + 'a:(1)', 'a:(1 2)', 'a:(1 2 3)', + 'a:(1) b:9', 'a:(1 2) b:9', 'a:(1 2 3) b:9', + 'a:(1) b:(8 9)', 'a:(1 2) b:(8 9)', 'a:(1 2 3) b:(8 9)', + 'd:0,a:(1)', 'd:0,a:(1 2)', 'd:0,a:(1 2 3)', + 'd:0,a:(1) b:9', 'd:0,a:(1 2) b:9', 'd:0,a:(1 2 3) b:9', + 'd:0,a:(1) b:(8 9)', 'd:0,a:(1 2) b:(8 9)', 'd:0,a:(1 2 3) b:(8 9)', +], j) + +gen('paren-list-implicit-structure-comma', [ + '[(1)]', '[(1,2)]', '[(1,2,3)]', + '[(1),9]', '[(1,2),9]', '[(1,2,3),9]', + '[(1),(9)]', '[(1,2),(9)]', '[(1,2,3),(9)]', '[(1),(9),(8)]', + '[(1),(8,9)]', '[(1,2),(8,9)]', '[(1,2,3),(8,9)]', + '[0,(1)]', '[0,(1,2)]', '[0,(1,2,3)]', + '[0,(1),9]', '[0,(1,2),9]', '[0,(1,2,3),9]', + '[0,(1),(9)]', '[0,(1,2),(9)]', '[0,(1,2,3),(9)]', + '[0,(1),(8,9)]', '[0,(1,2),(8,9)]', '[0,(1,2,3),(8,9)]', + '(1)', '(1,2)', '(1,2,3)', + '(1),9', '(1,2),9', '(1,2,3),9', + '(1),(9)', '(1,2),(9)', '(1,2,3),(9)', '(1),(9),(8)', + '(1),(8,9)', '(1,2),(8,9)', '(1,2,3),(8,9)', + '0,(1)', '0,(1,2)', '0,(1,2,3)', + '0,(1),9', '0,(1,2),9', '0,(1,2,3),9', + '0,(1),(9)', '0,(1,2),(9)', '0,(1,2,3),(9)', + '0,(1),(8,9)', '0,(1,2),(8,9)', '0,(1,2,3),(8,9)', +], j) + +gen('paren-list-implicit-structure-space', [ + '[(1)]', '[(1 2)]', '[(1 2 3)]', + '[(1) 9]', '[(1 2) 9]', '[(1 2 3) 9]', + '[(1) (9)]', '[(1 2) (9)]', '[(1 2 3) (9)]', '[(1) (9) (8)]', + '[(1) (8,9)]', '[(1 2) (8,9)]', '[(1 2 3) (8,9)]', + '[0 (1)]', '[0 (1 2)]', '[0 (1 2 3)]', + '[0 (1) 9]', '[0 (1 2) 9]', '[0 (1 2 3) 9]', + '[0 (1) (9)]', '[0 (1 2) (9)]', '[0 (1 2 3) (9)]', + '[0 (1) (8 9)]', '[0 (1 2) (8 9)]', '[0 (1 2 3) (8 9)]', + '(1)', '(1 2)', '(1 2 3)', + '(1) 9', '(1 2) 9', '(1 2 3) 9', + '(1) (9)', '(1 2) (9)', '(1 2 3) (9)', '(1) (9) (8)', + '(1) (8 9)', '(1 2) (8 9)', '(1 2 3) (8 9)', + '0 (1)', '0 (1 2)', '0 (1 2 3)', + '0 (1) 9', '0 (1 2) 9', '0 (1 2 3) 9', + '0 (1) (9)', '0 (1 2) (9)', '0 (1 2 3) (9)', + '0 (1) (8 9)', '0 (1 2) (8 9)', '0 (1 2 3) (8 9)', +], j) + +gen('jsonic-base', ['1 "a" true', 'x:1 y:"a" z:true'], j) + +const je_foo = Jsonic.make().use(Expr, { op: { foo: { infix: true, left: 180, right: 190, src: 'foo' } } }) +gen('add-infix', ['1 foo 2'], mj(je_foo)) + +const je_angle = Jsonic.make().use(Expr, { op: { angle: { paren: true, osrc: '<', csrc: '>' } } }) +gen('add-paren', ['<1>', '<<1>>', '(<1>)', '<(1)>', '1*(2+3)', '1*<2+3>'], mj(je_angle)) + +const je_preval = Jsonic.make().use(Expr, { op: { angle: { osrc: '<', csrc: '>', paren: true, preval: { active: true } } } }) +gen('paren-preval-basic', [ + 'B', 'a:b', 'a:b', + '<1>', '1<2>', 'a:<1>', 'a:1<2>', + '9+<1>', '9+1<2>', '<1>+9', '1<2>+9', +], mj(je_preval)) + +const je_overload = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + square: { osrc: '[', csrc: ']', paren: true, preval: { required: true } }, + brace: { osrc: '{', csrc: '}', paren: true, preval: { required: true } } + } +}) +gen('paren-preval-overload', [ + '[1]', 'a[1]', '[a[1]]', 'a:[1]', 'a:b[1]', 'a:[b[1]]', + '{a:[1]}', '{a:b[1]}', '{a:[b[1]]}', + '-[1]+2', '-a[1]+2', '-[a[1]]+2', + '2+[1]', '2+a[1]', '2+[a[1]]', + '2+{a:[1]}', '2+{a:b[1]}', '2+{a:[b[1]]}', + 'a[b[1]]', 'a[b[c[1]]]', 'a[b[c[d[1]]]]', + 'a{1}', 'a{b{1}}', 'a{b{c{1}}}', + 'a{1+2}', 'a{b{1+2}}', 'a{b{c{1+2}}}', + 'a{{x:1}}', 'a{{x:1,y:2}}', +], mj(je_overload)) + +const je_impl = Jsonic.make().use(Expr, { op: { plain: { preval: true } } }) +gen('paren-preval-implicit', ['foo,(1,a)', 'foo,(1+2,a)', 'foo,(1+2+3,a)'], mj(je_impl)) + +console.log('Done!') diff --git a/test/gen-spec.js b/test/gen-spec.js new file mode 100644 index 0000000..86e15a9 --- /dev/null +++ b/test/gen-spec.js @@ -0,0 +1,351 @@ +// Generate TSV spec files from parser output +const { Jsonic, util } = require('jsonic') +const { Expr } = require('..') +const fs = require('fs') +const path = require('path') + +const { omap } = util + +const C = (x) => JSON.parse(JSON.stringify(x)) +const S = (x, seen) => ( + seen = seen ?? new WeakSet(), + seen?.has(x) ? '[CIRCLE]' : ( + (x && 'object' === typeof x ? seen?.add(x) : null), + (x && Array.isArray(x)) ? + (0 === x.length ? x : [ + x[0].src || S(x[0], seen), + ...(1 < x.length ? (x.slice(1).map((t) => S(t, seen))) : [])] + .filter(t => undefined !== t)) : + (null != x && 'object' === typeof (x) ? omap(x, ([n, v]) => [n, S(v, seen)]) : x))) + +const mj = (je) => (s, m) => C(S(je(s, m))) + +function writeTsv(filename, header, entries) { + const lines = [`# ${header}`, '# input\texpected_output'] + for (const [input, output] of entries) { + lines.push(`${input}\t${JSON.stringify(output)}`) + } + const specDir = path.join(__dirname, 'spec') + fs.mkdirSync(specDir, { recursive: true }) + fs.writeFileSync(path.join(specDir, filename), lines.join('\n') + '\n') + console.log(`Wrote ${filename}: ${entries.length} entries`) +} + + +// === happy === +{ + const j = mj(Jsonic.make().use(Expr)) + writeTsv('happy.tsv', 'Happy path basic tests - default Expr config', [ + ['1+2', j('1+2')], + ['-1+2', j('-1+2')], + ]) +} + +// === binary === +{ + const j = mj(Jsonic.make().use(Expr)) + const entries = [] + const cases = [ + '1+2', '1*2', + '1*2+3', '1+2*3', '1*2*3', + '1+2+3+4', + '1*2+3+4', '1+2*3+4', '1+2+3*4', + '1+2*3*4', '1*2+3*4', '1*2*3+4', + '1*2*3*4', + '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', + '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', '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', + '1*2*3*4*5', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('binary.tsv', 'Binary infix operator tests - default Expr config', entries) +} + +// === structure === +{ + const j = mj(Jsonic.make().use(Expr)) + writeTsv('structure.tsv', 'Expression structure tests - default Expr config', [ + ['a:1+2', j('a:1+2')], + ['a:1+2,b:3+4', j('a:1+2,b:3+4')], + ['[1+2]', j('[1+2]')], + ['[1+2,3+4]', j('[1+2,3+4]')], + ['{a:[1+2]}', j('{a:[1+2]}')], + ]) +} + +// === unary-prefix-basic === +{ + const j = mj(Jsonic.make().use(Expr)) + const entries = [] + const cases = [ + '-1', '- 1', '+1', '+ 1', + '--1', '---1', '++1', '+++1', + '-+1', '+-1', + '--+1', '-+-1', '+--1', + '-++1', '++-1', + '-z', '- z', '+z', '+ z', + '--z', '---z', '++z', '+++z', + '-+z', '+-z', + '--+z', '-+-z', '+--z', + '-++z', '++-z', + '1+2', '-1+2', '--1+2', + '-1+-2', '1+-2', '1++2', '-1++2', + '-1+2+3', '-1+-2+3', '-1+-2+-3', '-1+2+-3', + '1+2+3', '1+-2+3', '1+-2+-3', '1+2+-3', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-prefix-basic.tsv', 'Unary prefix operator tests - default Expr config', entries) +} + +// === paren-basic === +{ + const j = mj(Jsonic.make().use(Expr)) + const entries = [] + const cases = [ + '100+200', '(100)', '(100)+200', '100+(200)', + '(1+2)', '(1+2+3)', '(1+2+3+4)', + '((1))', '(((1)))', '((((1))))', + '(1+2)+3', '1+(2+3)', + '((1+2))+3', '1+((2+3))', + '(1)+2+3', + '100+200+300', '100+(200)+300', + '1+2+(3)', '1+(2)+(3)', '(1)+2+(3)', '(1)+(2)+3', '(1)+(2)+(3)', + '(1+2)*3', '1*(2+3)', + '(a)', '("a")', '([])', '([a])', '([a,b])', '([a b])', + '([a,b,c])', '([a b c])', + '({})', '({a:1})', '({a:1,b:2})', '({a:1 b:2})', + '({a:1,b:2,c:3})', '({a:1 b:2 c:3})', + '(a:1)', + '()', '(),()', '(),(),()', + '() ()', '() () ()', + '[()]', '[(),()]', '[(),(),()]', + '[() ()]', '[() () ()]', + '{a:()}', '{a:(),b:()}', '{a:(),b:(),c:()}', + '{a:() b:()}', '{a:() b:() c:()}', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('paren-basic.tsv', 'Parenthesis tests - default Expr config', entries) +} + +// === implicit-list-top-basic === +{ + const j = mj(Jsonic.make().use(Expr)) + const entries = [] + const cases = [ + '1,2', '1+2,3', '1+2+3,4', '1+2+3+4,5', + '1 2', '1+2 3', '1+2+3 4', '1+2+3+4 5', + '1,2,11', '1+2,3,11', '1+2+3,4,11', '1+2+3+4,5,11', + '1 2 11', '1+2 3 11', '1+2+3 4 11', '1+2+3+4 5 11', + '22,1,2,11', '22,1+2,3,11', '22,1+2+3,4,11', + '22,1+2+3+4,5,11', + '22 1 2 11', '22 1+2 3 11', '22 1+2+3 4 11', + '22 1+2+3+4 5 11', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('implicit-list-top-basic.tsv', 'Implicit list at top level - default Expr config', entries) +} + +// === unary-suffix-basic === (requires custom config) +{ + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '1!', '1 !', '1!!', '1!!!', + 'z!', 'z !', + '1?', '1 ?', '1??', '1???', + '1+2!', '1!+2', '1!+2!', + '1+2!!', '1!!+2', '1!!+2!!', + '1+2?', '1?+2', '1?+2?', + '1+2??', '1??+2', '1??+2??', + '0+1+2!', '0+1!+2', '0+1!+2!', + '0!+1!+2!', '0!+1!+2', '0!+1+2!', '0!+1+2', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-suffix-basic.tsv', 'Unary suffix operator tests - config:suffix', entries) +} + +// === unary-suffix-edge === (requires custom config) +{ + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + tight: { infix: true, left: 120000, right: 130000, src: '~' }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '1!', '1!!', '1!!!', + '1!?', '1?!', '1!??', '1??!', + '1?!!', '1!!?', '1?!?', '1!?!', + '1!+2', '1+2!', '1!+2!', + '1!+2+3', '1+2!+3', '1!+2!+3', + '1!+2+3!', '1+2!+3!', '1!+2!+3!', + '1!~2', '1~2!', '1!~2!', + '1!~2+3', '1~2!+3', '1!~2!+3', + '1!~2~3', '1~2!~3', '1!~2!~3', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-suffix-edge.tsv', 'Unary suffix edge cases - config:suffix-tight', entries) +} + +// === unary-suffix-structure === +{ + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '1!,2!', '1!,2!,3!', '1!,2!,3!,4!', + '1! 2!', '1! 2! 3!', '1! 2! 3! 4!', + '[1!,2!]', '[1!,2!,3!]', '[1!,2!,3!,4!]', + '[1! 2!]', '[1! 2! 3!]', '[1! 2! 3! 4!]', + 'a:1!', 'a:1!,b:2!', 'a:1!,b:2!,c:3!', 'a:1!,b:2!,c:3!,d:4!', + 'a:1! b:2!', 'a:1! b:2! c:3!', 'a:1! b:2! c:3!,d:4!', + '{a:1!}', '{a:1!,b:2!}', '{a:1!,b:2!,c:3!}', '{a:1!,b:2!,c:3!,d:4!}', + '{a:1! b:2!}', '{a:1! b:2! c:3!}', '{a:1! b:2! c:3! d:4!}', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-suffix-structure.tsv', 'Unary suffix in structures - config:suffix', entries) +} + +// === unary-suffix-prefix === +{ + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '-1!', '--1!', '-1!!', '--1!!', + '-1!+2', '--1!+2', '---1!+2', + '-1?', '--1?', '-1??', '--1??', + '-1!?', '-1!?!', + '-1?+2', '--1?+2', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-suffix-prefix.tsv', 'Combined suffix and prefix - config:suffix', entries) +} + +// === unary-prefix-edge === +{ + const je = Jsonic.make().use(Expr, { + op: { + at: { prefix: true, right: 15000, src: '@' }, + tight: { infix: true, left: 120000, right: 130000, src: '~' }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '@1', '@@1', '@@@1', + '-@1', '@-1', '--@1', '@--1', + '@@-1', '-@@1', '-@-1', '@-@1', + '@1+2', '1+@2', '@1+@2', + '@1+2+3', '1+@2+3', '@1+@2+3', + '@1+2+@3', '1+@2+@3', '@1+@2+@3', + '@1~2', '1~@2', '@1~@2', + '@1~2+3', '1~@2+3', '@1~@2+3', + '@1~2~3', '1~@2~3', '@1~@2~3', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-prefix-edge.tsv', 'Unary prefix edge cases - config:prefix-tight', entries) +} + +// === ternary-basic === +{ + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, src: '!', left: 15000 }, + ternary: { ternary: true, src: ['?', ':'] }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '1?2:3', + '1?2: 3?4:5', '1?4:5 ?2:3', + '1? 2?4:5 :3', + '0+1?2:3', + '0+1?2: 3?4:5', '0+1?4:5 ?2:3', + '0+1? 2?4:5 :3', + '1?0+2:3', + '1?2:0+3', + '0+1?0+2:0+3', + '-1?2:3', + '1!?2:3', + '-1!?2:3', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('ternary-basic.tsv', 'Ternary operator tests - config:ternary', entries) +} + +// === json-base === +{ + const j = mj(Jsonic.make().use(Expr)) + const entries = [] + const scalars = [ + ['1', j('1')], + ['"a"', j('"a"')], + ['true', j('true')], + ] + entries.push(...scalars) + writeTsv('json-base.tsv', 'JSON base compatibility - default Expr config', entries) +} + +// === paren-suffix === +{ + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + const entries = [] + const cases = [ + '(0!+1!+2!)', '(0!+1!+2)', '(0!+1+2!)', '(0!+1+2)', + ] + for (const c of cases) { + entries.push([c, j(c)]) + } + writeTsv('unary-suffix-paren.tsv', 'Suffix operators inside parens - config:suffix', entries) +} + +console.log('Done generating spec files!') diff --git a/test/spec-util.ts b/test/spec-util.ts new file mode 100644 index 0000000..7f3dcf4 --- /dev/null +++ b/test/spec-util.ts @@ -0,0 +1,37 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +import * as fs from 'fs' +import * as path from 'path' + + +export type SpecEntry = { + input: string + expected: any +} + +export function loadSpec(name: string): SpecEntry[] { + // Resolve spec files relative to the project root test/spec directory, + // since compiled tests run from dist-test/ but specs live in test/spec/. + const rootDir = path.resolve(__dirname, '..') + const specPath = path.join(rootDir, 'test', 'spec', name) + const content = fs.readFileSync(specPath, 'utf8') + const entries: SpecEntry[] = [] + + for (const line of content.split('\n')) { + const trimmed = line.trim() + if (trimmed === '' || trimmed.startsWith('#')) continue + + const tabIdx = trimmed.indexOf('\t') + if (tabIdx === -1) continue + + const input = trimmed.substring(0, tabIdx) + const expectedJson = trimmed.substring(tabIdx + 1) + + entries.push({ + input, + expected: JSON.parse(expectedJson), + }) + } + + return entries +} diff --git a/test/spec.test.ts b/test/spec.test.ts new file mode 100644 index 0000000..0159532 --- /dev/null +++ b/test/spec.test.ts @@ -0,0 +1,288 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +import { describe, test, beforeEach } from 'node:test' +import { expect } from '@hapi/code' + +import { Jsonic, util } from 'jsonic' + +import { + Expr, +} from '..' + +import { loadSpec } from './spec-util' + + +const { omap } = util + +const C = (x: any) => JSON.parse(JSON.stringify(x)) + +const S = (x: any, seen?: WeakSet): any => ( + seen = seen ?? new WeakSet(), + seen?.has(x) ? '[CIRCLE]' : ( + (x && 'object' === typeof x ? seen?.add(x) : null), + (x && Array.isArray(x)) ? + (0 === x.length ? x : [ + x[0].src || S(x[0], seen), + ...(1 < x.length ? (x.slice(1).map((t: any) => S(t, seen))) : [])] + .filter(t => undefined !== t)) : + (null != x && 'object' === typeof (x) ? omap(x, ([n, v]) => [n, S(v, seen)]) : x))) + +const mj = + (je: Jsonic) => (s: string, m?: any) => C(S(je(s, m))) + +const _mo_ = 'equal' + + +function runSpec(specName: string, j: (s: string) => any) { + const entries = loadSpec(specName) + for (const entry of entries) { + expect(j(entry.input))[_mo_](entry.expected) + } +} + + +describe('spec', () => { + + beforeEach(() => { + global.console = require('console') + }) + + + test('happy', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('happy.tsv', j) + }) + + + test('binary', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('binary.tsv', j) + }) + + + test('structure', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('structure.tsv', j) + }) + + + test('unary-prefix-basic', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('unary-prefix-basic.tsv', j) + }) + + + test('unary-prefix-edge', () => { + const je = Jsonic.make().use(Expr, { + op: { + at: { prefix: true, right: 15000, src: '@' }, + tight: { infix: true, left: 120_000, right: 130_000, src: '~' }, + } + }) + const j = mj(je) + runSpec('unary-prefix-edge.tsv', j) + }) + + + test('unary-suffix-basic', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + runSpec('unary-suffix-basic.tsv', j) + }) + + + test('unary-suffix-edge', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + tight: { infix: true, left: 120_000, right: 130_000, src: '~' }, + } + }) + const j = mj(je) + runSpec('unary-suffix-edge.tsv', j) + }) + + + test('unary-suffix-structure', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + runSpec('unary-suffix-structure.tsv', j) + }) + + + test('unary-suffix-prefix', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + runSpec('unary-suffix-prefix.tsv', j) + }) + + + test('unary-suffix-paren', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + question: { suffix: true, left: 13000, src: '?' }, + } + }) + const j = mj(je) + runSpec('unary-suffix-paren.tsv', j) + }) + + + test('paren-basic', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-basic.tsv', j) + }) + + + test('implicit-list-top-basic', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('implicit-list-top-basic.tsv', j) + }) + + + test('ternary-basic', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, src: '!', left: 15000 }, + ternary: { ternary: true, src: ['?', ':'] }, + } + }) + const j = mj(je) + runSpec('ternary-basic.tsv', j) + }) + + + test('json-base', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('json-base.tsv', j) + }) + + + test('implicit-list-top-paren', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('implicit-list-top-paren.tsv', j) + }) + + + test('paren-implicit-list', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-implicit-list.tsv', j) + }) + + + test('paren-implicit-map', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-implicit-map.tsv', j) + }) + + + test('map-implicit-list-paren', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('map-implicit-list-paren.tsv', j) + }) + + + test('paren-map-implicit-structure-comma', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-map-implicit-structure-comma.tsv', j) + }) + + + test('paren-map-implicit-structure-space', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-map-implicit-structure-space.tsv', j) + }) + + + test('paren-list-implicit-structure-comma', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-list-implicit-structure-comma.tsv', j) + }) + + + test('paren-list-implicit-structure-space', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('paren-list-implicit-structure-space.tsv', j) + }) + + + test('jsonic-base', () => { + const j = mj(Jsonic.make().use(Expr)) + runSpec('jsonic-base.tsv', j) + }) + + + test('add-infix', () => { + const je = Jsonic.make().use(Expr, { + op: { + foo: { infix: true, left: 180, right: 190, src: 'foo' }, + } + }) + const j = mj(je) + runSpec('add-infix.tsv', j) + }) + + + test('add-paren', () => { + const je = Jsonic.make().use(Expr, { + op: { + angle: { paren: true, osrc: '<', csrc: '>' }, + } + }) + const j = mj(je) + runSpec('add-paren.tsv', j) + }) + + + test('paren-preval-basic', () => { + const je = Jsonic.make().use(Expr, { + op: { + angle: { osrc: '<', csrc: '>', paren: true, preval: { active: true } }, + } + }) + const j = mj(je) + runSpec('paren-preval-basic.tsv', j) + }) + + + test('paren-preval-overload', () => { + const je = Jsonic.make().use(Expr, { + op: { + factorial: { suffix: true, left: 15000, src: '!' }, + square: { osrc: '[', csrc: ']', paren: true, preval: { required: true } }, + brace: { osrc: '{', csrc: '}', paren: true, preval: { required: true } }, + } + }) + const j = mj(je) + runSpec('paren-preval-overload.tsv', j) + }) + + + test('paren-preval-implicit', () => { + const je = Jsonic.make().use(Expr, { + op: { + plain: { preval: true }, + } + }) + const j = mj(je) + runSpec('paren-preval-implicit.tsv', j) + }) + +}) diff --git a/test/spec/add-infix.tsv b/test/spec/add-infix.tsv new file mode 100644 index 0000000..8ec4bd7 --- /dev/null +++ b/test/spec/add-infix.tsv @@ -0,0 +1,3 @@ +# add-infix +# input expected_output +1 foo 2 ["foo",1,2] diff --git a/test/spec/add-paren.tsv b/test/spec/add-paren.tsv new file mode 100644 index 0000000..2445dbe --- /dev/null +++ b/test/spec/add-paren.tsv @@ -0,0 +1,8 @@ +# add-paren +# input expected_output +<1> ["<",1] +<<1>> ["<",["<",1]] +(<1>) ["(",["<",1]] +<(1)> ["<",["(",1]] +1*(2+3) ["*",1,["(",["+",2,3]]] +1*<2+3> ["*",1,["<",["+",2,3]]] diff --git a/test/spec/binary.tsv b/test/spec/binary.tsv new file mode 100644 index 0000000..9c5699f --- /dev/null +++ b/test/spec/binary.tsv @@ -0,0 +1,31 @@ +# Binary infix operator tests - default Expr config +# input expected_output +1+2 ["+",1,2] +1*2 ["*",1,2] +1*2+3 ["+",["*",1,2],3] +1+2*3 ["+",1,["*",2,3]] +1*2*3 ["*",["*",1,2],3] +1+2+3+4 ["+",["+",["+",1,2],3],4] +1*2+3+4 ["+",["+",["*",1,2],3],4] +1+2*3+4 ["+",["+",1,["*",2,3]],4] +1+2+3*4 ["+",["+",1,2],["*",3,4]] +1+2*3*4 ["+",1,["*",["*",2,3],4]] +1*2+3*4 ["+",["*",1,2],["*",3,4]] +1*2*3+4 ["+",["*",["*",1,2],3],4] +1*2*3*4 ["*",["*",["*",1,2],3],4] +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 ["+",["+",["+",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]] +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 ["+",["+",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] +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 ["+",["*",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] +1*2*3*4*5 ["*",["*",["*",["*",1,2],3],4],5] diff --git a/test/spec/happy.tsv b/test/spec/happy.tsv new file mode 100644 index 0000000..4160553 --- /dev/null +++ b/test/spec/happy.tsv @@ -0,0 +1,4 @@ +# Happy path basic tests - default Expr config +# input expected_output +1+2 ["+",1,2] +-1+2 ["+",["-",1],2] diff --git a/test/spec/implicit-list-top-basic.tsv b/test/spec/implicit-list-top-basic.tsv new file mode 100644 index 0000000..82ff29a --- /dev/null +++ b/test/spec/implicit-list-top-basic.tsv @@ -0,0 +1,26 @@ +# Implicit list at top level - default Expr config +# input expected_output +1,2 [1,2] +1+2,3 [["+",1,2],3] +1+2+3,4 [["+",["+",1,2],3],4] +1+2+3+4,5 [["+",["+",["+",1,2],3],4],5] +1 2 [1,2] +1+2 3 [["+",1,2],3] +1+2+3 4 [["+",["+",1,2],3],4] +1+2+3+4 5 [["+",["+",["+",1,2],3],4],5] +1,2,11 [1,2,11] +1+2,3,11 [["+",1,2],3,11] +1+2+3,4,11 [["+",["+",1,2],3],4,11] +1+2+3+4,5,11 [["+",["+",["+",1,2],3],4],5,11] +1 2 11 [1,2,11] +1+2 3 11 [["+",1,2],3,11] +1+2+3 4 11 [["+",["+",1,2],3],4,11] +1+2+3+4 5 11 [["+",["+",["+",1,2],3],4],5,11] +22,1,2,11 [22,1,2,11] +22,1+2,3,11 [22,["+",1,2],3,11] +22,1+2+3,4,11 [22,["+",["+",1,2],3],4,11] +22,1+2+3+4,5,11 [22,["+",["+",["+",1,2],3],4],5,11] +22 1 2 11 [22,1,2,11] +22 1+2 3 11 [22,["+",1,2],3,11] +22 1+2+3 4 11 [22,["+",["+",1,2],3],4,11] +22 1+2+3+4 5 11 [22,["+",["+",["+",1,2],3],4],5,11] diff --git a/test/spec/implicit-list-top-paren.tsv b/test/spec/implicit-list-top-paren.tsv new file mode 100644 index 0000000..ca2e1e1 --- /dev/null +++ b/test/spec/implicit-list-top-paren.tsv @@ -0,0 +1,46 @@ +# implicit-list-top-paren +# input expected_output +(1,2) ["(",[1,2]] +(1+2,3) ["(",[["+",1,2],3]] +(1+2+3,4) ["(",[["+",["+",1,2],3],4]] +(1+2+3+4,5) ["(",[["+",["+",["+",1,2],3],4],5]] +(1 2) ["(",[1,2]] +(1+2 3) ["(",[["+",1,2],3]] +(1+2+3 4) ["(",[["+",["+",1,2],3],4]] +(1+2+3+4 5) ["(",[["+",["+",["+",1,2],3],4],5]] +(1,2,11) ["(",[1,2,11]] +(1+2,3,11) ["(",[["+",1,2],3,11]] +(1+2+3,4,11) ["(",[["+",["+",1,2],3],4,11]] +(1+2+3+4,5,11) ["(",[["+",["+",["+",1,2],3],4],5,11]] +(1 2 11) ["(",[1,2,11]] +(1+2 3 11) ["(",[["+",1,2],3,11]] +(1+2+3 4 11) ["(",[["+",["+",1,2],3],4,11]] +(1+2+3+4 5 11) ["(",[["+",["+",["+",1,2],3],4],5,11]] +(22,1,2,11) ["(",[22,1,2,11]] +(22,1+2,3,11) ["(",[22,["+",1,2],3,11]] +(22,1+2+3,4,11) ["(",[22,["+",["+",1,2],3],4,11]] +(22,1+2+3+4,5,11) ["(",[22,["+",["+",["+",1,2],3],4],5,11]] +(22 1 2 11) ["(",[22,1,2,11]] +(22 1+2 3 11) ["(",[22,["+",1,2],3,11]] +(22 1+2+3 4 11) ["(",[22,["+",["+",1,2],3],4,11]] +(22 1+2+3+4 5 11) ["(",[22,["+",["+",["+",1,2],3],4],5,11]] +([true,false],1,2,11) ["(",[[true,false],1,2,11]] +([true,false],1+2,3,11) ["(",[[true,false],["+",1,2],3,11]] +([true,false],1+2+3,4,11) ["(",[[true,false],["+",["+",1,2],3],4,11]] +([true,false],1+2+3+4,5,11) ["(",[[true,false],["+",["+",["+",1,2],3],4],5,11]] +([true,false] 1 2 11) ["(",[[true,false],1,2,11]] +([true,false] 1+2 3 11) ["(",[[true,false],["+",1,2],3,11]] +([true,false] 1+2+3 4 11) ["(",[[true,false],["+",["+",1,2],3],4,11]] +([true,false] 1+2+3+4 5 11) ["(",[[true,false],["+",["+",["+",1,2],3],4],5,11]] +([true,false],1,2,{x:11,y:22}) ["(",[[true,false],1,2,{"x":11,"y":22}]] +([true,false],1+2,3,{x:11,y:22}) ["(",[[true,false],["+",1,2],3,{"x":11,"y":22}]] +([true,false],1+2+3,4,{x:11,y:22}) ["(",[[true,false],["+",["+",1,2],3],4,{"x":11,"y":22}]] +([true,false],1+2+3+4,5,{x:11,y:22}) ["(",[[true,false],["+",["+",["+",1,2],3],4],5,{"x":11,"y":22}]] +([true,false] 1 2 {x:11,y:22}) ["(",[[true,false],1,2,{"x":11,"y":22}]] +([true,false] 1+2 3 {x:11,y:22}) ["(",[[true,false],["+",1,2],3,{"x":11,"y":22}]] +([true,false] 1+2+3 4 {x:11,y:22}) ["(",[[true,false],["+",["+",1,2],3],4,{"x":11,"y":22}]] +([true,false] 1+2+3+4 5 {x:11,y:22}) ["(",[[true,false],["+",["+",["+",1,2],3],4],5,{"x":11,"y":22}]] +(1+2,3+4) ["(",[["+",1,2],["+",3,4]]] +(1+2,3+4,5+6) ["(",[["+",1,2],["+",3,4],["+",5,6]]] +(1+2 3+4) ["(",[["+",1,2],["+",3,4]]] +(1+2 3+4 5+6) ["(",[["+",1,2],["+",3,4],["+",5,6]]] diff --git a/test/spec/json-base.tsv b/test/spec/json-base.tsv new file mode 100644 index 0000000..52be938 --- /dev/null +++ b/test/spec/json-base.tsv @@ -0,0 +1,5 @@ +# JSON base compatibility - default Expr config +# input expected_output +1 1 +"a" "a" +true true diff --git a/test/spec/jsonic-base.tsv b/test/spec/jsonic-base.tsv new file mode 100644 index 0000000..e66a15f --- /dev/null +++ b/test/spec/jsonic-base.tsv @@ -0,0 +1,4 @@ +# jsonic-base +# input expected_output +1 "a" true [1,"a",true] +x:1 y:"a" z:true {"x":1,"y":"a","z":true} diff --git a/test/spec/map-implicit-list-paren.tsv b/test/spec/map-implicit-list-paren.tsv new file mode 100644 index 0000000..d5383cc --- /dev/null +++ b/test/spec/map-implicit-list-paren.tsv @@ -0,0 +1,30 @@ +# map-implicit-list-paren +# input expected_output +a:(1,2),b:0 {"a":["(",[1,2]],"b":0} +a:(1+2,3),b:0 {"a":["(",[["+",1,2],3]],"b":0} +a:(1+2+3,4),b:0 {"a":["(",[["+",["+",1,2],3],4]],"b":0} +a:(1+2+3+4,5),b:0 {"a":["(",[["+",["+",["+",1,2],3],4],5]],"b":0} +a:(1 2),b:0 {"a":["(",[1,2]],"b":0} +a:(1+2 3),b:0 {"a":["(",[["+",1,2],3]],"b":0} +a:(1+2+3 4),b:0 {"a":["(",[["+",["+",1,2],3],4]],"b":0} +a:(1+2+3+4 5),b:0 {"a":["(",[["+",["+",["+",1,2],3],4],5]],"b":0} +a:(1,2,11),b:0 {"a":["(",[1,2,11]],"b":0} +a:(1+2,3,11),b:0 {"a":["(",[["+",1,2],3,11]],"b":0} +a:(1+2+3,4,11),b:0 {"a":["(",[["+",["+",1,2],3],4,11]],"b":0} +a:(1+2+3+4,5,11),b:0 {"a":["(",[["+",["+",["+",1,2],3],4],5,11]],"b":0} +a:(1 2 11),b:0 {"a":["(",[1,2,11]],"b":0} +a:(1+2 3 11),b:0 {"a":["(",[["+",1,2],3,11]],"b":0} +a:(1+2+3 4 11),b:0 {"a":["(",[["+",["+",1,2],3],4,11]],"b":0} +a:(1+2+3+4 5 11),b:0 {"a":["(",[["+",["+",["+",1,2],3],4],5,11]],"b":0} +{a:(1,2),b:0} {"a":["(",[1,2]],"b":0} +{a:(1+2,3),b:0} {"a":["(",[["+",1,2],3]],"b":0} +{a:(1+2+3,4),b:0} {"a":["(",[["+",["+",1,2],3],4]],"b":0} +{a:(1+2+3+4,5),b:0} {"a":["(",[["+",["+",["+",1,2],3],4],5]],"b":0} +{a:(1 2),b:0} {"a":["(",[1,2]],"b":0} +{a:(1+2 3),b:0} {"a":["(",[["+",1,2],3]],"b":0} +{a:(1+2+3 4),b:0} {"a":["(",[["+",["+",1,2],3],4]],"b":0} +{a:(1+2+3+4 5),b:0} {"a":["(",[["+",["+",["+",1,2],3],4],5]],"b":0} +{a:(1+2,3+4)} {"a":["(",[["+",1,2],["+",3,4]]]} +{a:(1+2,3+4,5+6)} {"a":["(",[["+",1,2],["+",3,4],["+",5,6]]]} +{a:(1+2 3+4)} {"a":["(",[["+",1,2],["+",3,4]]]} +{a:(1+2 3+4 5+6)} {"a":["(",[["+",1,2],["+",3,4],["+",5,6]]]} diff --git a/test/spec/paren-basic.tsv b/test/spec/paren-basic.tsv new file mode 100644 index 0000000..d2333aa --- /dev/null +++ b/test/spec/paren-basic.tsv @@ -0,0 +1,56 @@ +# Parenthesis tests - default Expr config +# input expected_output +100+200 ["+",100,200] +(100) ["(",100] +(100)+200 ["+",["(",100],200] +100+(200) ["+",100,["(",200]] +(1+2) ["(",["+",1,2]] +(1+2+3) ["(",["+",["+",1,2],3]] +(1+2+3+4) ["(",["+",["+",["+",1,2],3],4]] +((1)) ["(",["(",1]] +(((1))) ["(",["(",["(",1]]] +((((1)))) ["(",["(",["(",["(",1]]]] +(1+2)+3 ["+",["(",["+",1,2]],3] +1+(2+3) ["+",1,["(",["+",2,3]]] +((1+2))+3 ["+",["(",["(",["+",1,2]]],3] +1+((2+3)) ["+",1,["(",["(",["+",2,3]]]] +(1)+2+3 ["+",["+",["(",1],2],3] +100+200+300 ["+",["+",100,200],300] +100+(200)+300 ["+",["+",100,["(",200]],300] +1+2+(3) ["+",["+",1,2],["(",3]] +1+(2)+(3) ["+",["+",1,["(",2]],["(",3]] +(1)+2+(3) ["+",["+",["(",1],2],["(",3]] +(1)+(2)+3 ["+",["+",["(",1],["(",2]],3] +(1)+(2)+(3) ["+",["+",["(",1],["(",2]],["(",3]] +(1+2)*3 ["*",["(",["+",1,2]],3] +1*(2+3) ["*",1,["(",["+",2,3]]] +(a) ["(","a"] +("a") ["(","a"] +([]) ["(",[]] +([a]) ["(",["a"]] +([a,b]) ["(",["a","b"]] +([a b]) ["(",["a","b"]] +([a,b,c]) ["(",["a","b","c"]] +([a b c]) ["(",["a","b","c"]] +({}) ["(",{}] +({a:1}) ["(",{"a":1}] +({a:1,b:2}) ["(",{"a":1,"b":2}] +({a:1 b:2}) ["(",{"a":1,"b":2}] +({a:1,b:2,c:3}) ["(",{"a":1,"b":2,"c":3}] +({a:1 b:2 c:3}) ["(",{"a":1,"b":2,"c":3}] +(a:1) ["(",{"a":1}] +() ["("] +(),() [["("],["("]] +(),(),() [["("],["("],["("]] +() () [["("],["("]] +() () () [["("],["("],["("]] +[()] [["("]] +[(),()] [["("],["("]] +[(),(),()] [["("],["("],["("]] +[() ()] [["("],["("]] +[() () ()] [["("],["("],["("]] +{a:()} {"a":["("]} +{a:(),b:()} {"a":["("],"b":["("]} +{a:(),b:(),c:()} {"a":["("],"b":["("],"c":["("]} +{a:() b:()} {"a":["("],"b":["("]} +{a:() b:() c:()} {"a":["("],"b":["("],"c":["("]} diff --git a/test/spec/paren-implicit-list.tsv b/test/spec/paren-implicit-list.tsv new file mode 100644 index 0000000..53f2134 --- /dev/null +++ b/test/spec/paren-implicit-list.tsv @@ -0,0 +1,22 @@ +# paren-implicit-list +# input expected_output +(a) ["(","a"] +(a,b) ["(",["a","b"]] +(a,b,c) ["(",["a","b","c"]] +(a,b,c,d) ["(",["a","b","c","d"]] +(1,2) ["(",[1,2]] +(1+2,3) ["(",[["+",1,2],3]] +(1+2+3,4) ["(",[["+",["+",1,2],3],4]] +(1+2+3+4,5) ["(",[["+",["+",["+",1,2],3],4],5]] +(1+2,3,4) ["(",[["+",1,2],3,4]] +(1+2,3+4,5) ["(",[["+",1,2],["+",3,4],5]] +(1+2,3+4,5+6) ["(",[["+",1,2],["+",3,4],["+",5,6]]] +(a b) ["(",["a","b"]] +(a b c) ["(",["a","b","c"]] +(1+2 3) ["(",[["+",1,2],3]] +(1+2 3 4) ["(",[["+",1,2],3,4]] +(1+2 3+4 5) ["(",[["+",1,2],["+",3,4],5]] +(1+2 3+4 5+6) ["(",[["+",1,2],["+",3,4],["+",5,6]]] +foo(1,a) ["foo",["(",[1,"a"]]] +foo,(1,a) ["foo",["(",[1,"a"]]] +foo (1,a) ["foo",["(",[1,"a"]]] diff --git a/test/spec/paren-implicit-map.tsv b/test/spec/paren-implicit-map.tsv new file mode 100644 index 0000000..76bad7b --- /dev/null +++ b/test/spec/paren-implicit-map.tsv @@ -0,0 +1,14 @@ +# paren-implicit-map +# input expected_output +(a:1,b:2) ["(",{"a":1,"b":2}] +(a:1 b:2) ["(",{"a":1,"b":2}] +(a:1,b:2,c:3) ["(",{"a":1,"b":2,"c":3}] +(a:1 b:2 c:3) ["(",{"a":1,"b":2,"c":3}] +(a:1+2,b:3) ["(",{"a":["+",1,2],"b":3}] +(a:1+2,b:3,c:4) ["(",{"a":["+",1,2],"b":3,"c":4}] +(a:1+2,b:3+4,c:5) ["(",{"a":["+",1,2],"b":["+",3,4],"c":5}] +(a:1+2,b:3+4,c:5+6) ["(",{"a":["+",1,2],"b":["+",3,4],"c":["+",5,6]}] +(a:1+2 b:3) ["(",{"a":["+",1,2],"b":3}] +(a:1+2 b:3 c:4) ["(",{"a":["+",1,2],"b":3,"c":4}] +(a:1+2 b:3+4 c:5) ["(",{"a":["+",1,2],"b":["+",3,4],"c":5}] +(a:1+2 b:3+4 c:5+6) ["(",{"a":["+",1,2],"b":["+",3,4],"c":["+",5,6]}] diff --git a/test/spec/paren-list-implicit-structure-comma.tsv b/test/spec/paren-list-implicit-structure-comma.tsv new file mode 100644 index 0000000..3ac50bc --- /dev/null +++ b/test/spec/paren-list-implicit-structure-comma.tsv @@ -0,0 +1,52 @@ +# paren-list-implicit-structure-comma +# input expected_output +[(1)] [["(",1]] +[(1,2)] [["(",[1,2]]] +[(1,2,3)] [["(",[1,2,3]]] +[(1),9] [["(",1],9] +[(1,2),9] [["(",[1,2]],9] +[(1,2,3),9] [["(",[1,2,3]],9] +[(1),(9)] [["(",1],["(",9]] +[(1,2),(9)] [["(",[1,2]],["(",9]] +[(1,2,3),(9)] [["(",[1,2,3]],["(",9]] +[(1),(9),(8)] [["(",1],["(",9],["(",8]] +[(1),(8,9)] [["(",1],["(",[8,9]]] +[(1,2),(8,9)] [["(",[1,2]],["(",[8,9]]] +[(1,2,3),(8,9)] [["(",[1,2,3]],["(",[8,9]]] +[0,(1)] [0,["(",1]] +[0,(1,2)] [0,["(",[1,2]]] +[0,(1,2,3)] [0,["(",[1,2,3]]] +[0,(1),9] [0,["(",1],9] +[0,(1,2),9] [0,["(",[1,2]],9] +[0,(1,2,3),9] [0,["(",[1,2,3]],9] +[0,(1),(9)] [0,["(",1],["(",9]] +[0,(1,2),(9)] [0,["(",[1,2]],["(",9]] +[0,(1,2,3),(9)] [0,["(",[1,2,3]],["(",9]] +[0,(1),(8,9)] [0,["(",1],["(",[8,9]]] +[0,(1,2),(8,9)] [0,["(",[1,2]],["(",[8,9]]] +[0,(1,2,3),(8,9)] [0,["(",[1,2,3]],["(",[8,9]]] +(1) ["(",1] +(1,2) ["(",[1,2]] +(1,2,3) ["(",[1,2,3]] +(1),9 [["(",1],9] +(1,2),9 [["(",[1,2]],9] +(1,2,3),9 [["(",[1,2,3]],9] +(1),(9) [["(",1],["(",9]] +(1,2),(9) [["(",[1,2]],["(",9]] +(1,2,3),(9) [["(",[1,2,3]],["(",9]] +(1),(9),(8) [["(",1],["(",9],["(",8]] +(1),(8,9) [["(",1],["(",[8,9]]] +(1,2),(8,9) [["(",[1,2]],["(",[8,9]]] +(1,2,3),(8,9) [["(",[1,2,3]],["(",[8,9]]] +0,(1) [0,["(",1]] +0,(1,2) [0,["(",[1,2]]] +0,(1,2,3) [0,["(",[1,2,3]]] +0,(1),9 [0,["(",1],9] +0,(1,2),9 [0,["(",[1,2]],9] +0,(1,2,3),9 [0,["(",[1,2,3]],9] +0,(1),(9) [0,["(",1],["(",9]] +0,(1,2),(9) [0,["(",[1,2]],["(",9]] +0,(1,2,3),(9) [0,["(",[1,2,3]],["(",9]] +0,(1),(8,9) [0,["(",1],["(",[8,9]]] +0,(1,2),(8,9) [0,["(",[1,2]],["(",[8,9]]] +0,(1,2,3),(8,9) [0,["(",[1,2,3]],["(",[8,9]]] diff --git a/test/spec/paren-list-implicit-structure-space.tsv b/test/spec/paren-list-implicit-structure-space.tsv new file mode 100644 index 0000000..a2266f9 --- /dev/null +++ b/test/spec/paren-list-implicit-structure-space.tsv @@ -0,0 +1,52 @@ +# paren-list-implicit-structure-space +# input expected_output +[(1)] [["(",1]] +[(1 2)] [["(",[1,2]]] +[(1 2 3)] [["(",[1,2,3]]] +[(1) 9] [["(",1],9] +[(1 2) 9] [["(",[1,2]],9] +[(1 2 3) 9] [["(",[1,2,3]],9] +[(1) (9)] [["(",1],["(",9]] +[(1 2) (9)] [["(",[1,2]],["(",9]] +[(1 2 3) (9)] [["(",[1,2,3]],["(",9]] +[(1) (9) (8)] [["(",1],["(",9],["(",8]] +[(1) (8,9)] [["(",1],["(",[8,9]]] +[(1 2) (8,9)] [["(",[1,2]],["(",[8,9]]] +[(1 2 3) (8,9)] [["(",[1,2,3]],["(",[8,9]]] +[0 (1)] [0,["(",1]] +[0 (1 2)] [0,["(",[1,2]]] +[0 (1 2 3)] [0,["(",[1,2,3]]] +[0 (1) 9] [0,["(",1],9] +[0 (1 2) 9] [0,["(",[1,2]],9] +[0 (1 2 3) 9] [0,["(",[1,2,3]],9] +[0 (1) (9)] [0,["(",1],["(",9]] +[0 (1 2) (9)] [0,["(",[1,2]],["(",9]] +[0 (1 2 3) (9)] [0,["(",[1,2,3]],["(",9]] +[0 (1) (8 9)] [0,["(",1],["(",[8,9]]] +[0 (1 2) (8 9)] [0,["(",[1,2]],["(",[8,9]]] +[0 (1 2 3) (8 9)] [0,["(",[1,2,3]],["(",[8,9]]] +(1) ["(",1] +(1 2) ["(",[1,2]] +(1 2 3) ["(",[1,2,3]] +(1) 9 [["(",1],9] +(1 2) 9 [["(",[1,2]],9] +(1 2 3) 9 [["(",[1,2,3]],9] +(1) (9) [["(",1],["(",9]] +(1 2) (9) [["(",[1,2]],["(",9]] +(1 2 3) (9) [["(",[1,2,3]],["(",9]] +(1) (9) (8) [["(",1],["(",9],["(",8]] +(1) (8 9) [["(",1],["(",[8,9]]] +(1 2) (8 9) [["(",[1,2]],["(",[8,9]]] +(1 2 3) (8 9) [["(",[1,2,3]],["(",[8,9]]] +0 (1) [0,["(",1]] +0 (1 2) [0,["(",[1,2]]] +0 (1 2 3) [0,["(",[1,2,3]]] +0 (1) 9 [0,["(",1],9] +0 (1 2) 9 [0,["(",[1,2]],9] +0 (1 2 3) 9 [0,["(",[1,2,3]],9] +0 (1) (9) [0,["(",1],["(",9]] +0 (1 2) (9) [0,["(",[1,2]],["(",9]] +0 (1 2 3) (9) [0,["(",[1,2,3]],["(",9]] +0 (1) (8 9) [0,["(",1],["(",[8,9]]] +0 (1 2) (8 9) [0,["(",[1,2]],["(",[8,9]]] +0 (1 2 3) (8 9) [0,["(",[1,2,3]],["(",[8,9]]] diff --git a/test/spec/paren-map-implicit-structure-comma.tsv b/test/spec/paren-map-implicit-structure-comma.tsv new file mode 100644 index 0000000..c408d09 --- /dev/null +++ b/test/spec/paren-map-implicit-structure-comma.tsv @@ -0,0 +1,44 @@ +# paren-map-implicit-structure-comma +# input expected_output +{a:(1)} {"a":["(",1]} +{a:(1,2)} {"a":["(",[1,2]]} +{a:(1,2,3)} {"a":["(",[1,2,3]]} +{a:(1),b:9} {"a":["(",1],"b":9} +{a:(1,2),b:9} {"a":["(",[1,2]],"b":9} +{a:(1,2,3),b:9} {"a":["(",[1,2,3]],"b":9} +{a:(1),b:9,c:8} {"a":["(",1],"b":9,"c":8} +{a:(1,2),b:9,c:8} {"a":["(",[1,2]],"b":9,"c":8} +{a:(1,2,3),b:9,c:8} {"a":["(",[1,2,3]],"b":9,"c":8} +{a:(1),b:(9)} {"a":["(",1],"b":["(",9]} +{a:(1,2),b:(9)} {"a":["(",[1,2]],"b":["(",9]} +{a:(1,2,3),b:(9)} {"a":["(",[1,2,3]],"b":["(",9]} +{a:(1),b:(8,9)} {"a":["(",1],"b":["(",[8,9]]} +{a:(1,2),b:(8,9)} {"a":["(",[1,2]],"b":["(",[8,9]]} +{a:(1,2,3),b:(8,9)} {"a":["(",[1,2,3]],"b":["(",[8,9]]} +{d:0,a:(1)} {"d":0,"a":["(",1]} +{d:0,a:(1,2)} {"d":0,"a":["(",[1,2]]} +{d:0,a:(1,2,3)} {"d":0,"a":["(",[1,2,3]]} +{d:0,a:(1),b:9} {"d":0,"a":["(",1],"b":9} +{d:0,a:(1,2),b:9} {"d":0,"a":["(",[1,2]],"b":9} +{d:0,a:(1,2,3),b:9} {"d":0,"a":["(",[1,2,3]],"b":9} +{d:0,a:(1),b:(8,9)} {"d":0,"a":["(",1],"b":["(",[8,9]]} +{d:0,a:(1,2),b:(8,9)} {"d":0,"a":["(",[1,2]],"b":["(",[8,9]]} +{d:0,a:(1,2,3),b:(8,9)} {"d":0,"a":["(",[1,2,3]],"b":["(",[8,9]]} +a:(1) {"a":["(",1]} +a:(1,2) {"a":["(",[1,2]]} +a:(1,2,3) {"a":["(",[1,2,3]]} +a:(1),b:9 {"a":["(",1],"b":9} +a:(1,2),b:9 {"a":["(",[1,2]],"b":9} +a:(1,2,3),b:9 {"a":["(",[1,2,3]],"b":9} +a:(1),b:(8,9) {"a":["(",1],"b":["(",[8,9]]} +a:(1,2),b:(8,9) {"a":["(",[1,2]],"b":["(",[8,9]]} +a:(1,2,3),b:(8,9) {"a":["(",[1,2,3]],"b":["(",[8,9]]} +d:0,a:(1) {"d":0,"a":["(",1]} +d:0,a:(1,2) {"d":0,"a":["(",[1,2]]} +d:0,a:(1,2,3) {"d":0,"a":["(",[1,2,3]]} +d:0,a:(1),b:9 {"d":0,"a":["(",1],"b":9} +d:0,a:(1,2),b:9 {"d":0,"a":["(",[1,2]],"b":9} +d:0,a:(1,2,3),b:9 {"d":0,"a":["(",[1,2,3]],"b":9} +d:0,a:(1),b:(8,9) {"d":0,"a":["(",1],"b":["(",[8,9]]} +d:0,a:(1,2),b:(8,9) {"d":0,"a":["(",[1,2]],"b":["(",[8,9]]} +d:0,a:(1,2,3),b:(8,9) {"d":0,"a":["(",[1,2,3]],"b":["(",[8,9]]} diff --git a/test/spec/paren-map-implicit-structure-space.tsv b/test/spec/paren-map-implicit-structure-space.tsv new file mode 100644 index 0000000..17c225b --- /dev/null +++ b/test/spec/paren-map-implicit-structure-space.tsv @@ -0,0 +1,44 @@ +# paren-map-implicit-structure-space +# input expected_output +{a:(1)} {"a":["(",1]} +{a:(1 2)} {"a":["(",[1,2]]} +{a:(1 2 3)} {"a":["(",[1,2,3]]} +{a:(1) b:9} {"a":["(",1],"b":9} +{a:(1 2) b:9} {"a":["(",[1,2]],"b":9} +{a:(1 2 3) b:9} {"a":["(",[1,2,3]],"b":9} +{a:(1) b:9 c:8} {"a":["(",1],"b":9,"c":8} +{a:(1 2) b:9 c:8} {"a":["(",[1,2]],"b":9,"c":8} +{a:(1 2 3) b:9 c:8} {"a":["(",[1,2,3]],"b":9,"c":8} +{a:(1) b:(9)} {"a":["(",1],"b":["(",9]} +{a:(1 2) b:(9)} {"a":["(",[1,2]],"b":["(",9]} +{a:(1 2 3) b:(9)} {"a":["(",[1,2,3]],"b":["(",9]} +{a:(1) b:(8 9)} {"a":["(",1],"b":["(",[8,9]]} +{a:(1 2) b:(8 9)} {"a":["(",[1,2]],"b":["(",[8,9]]} +{a:(1 2 3) b:(8 9)} {"a":["(",[1,2,3]],"b":["(",[8,9]]} +{d:0,a:(1)} {"d":0,"a":["(",1]} +{d:0,a:(1 2)} {"d":0,"a":["(",[1,2]]} +{d:0,a:(1 2 3)} {"d":0,"a":["(",[1,2,3]]} +{d:0,a:(1) b:9} {"d":0,"a":["(",1],"b":9} +{d:0,a:(1 2) b:9} {"d":0,"a":["(",[1,2]],"b":9} +{d:0,a:(1 2 3) b:9} {"d":0,"a":["(",[1,2,3]],"b":9} +{d:0,a:(1) b:(8 9)} {"d":0,"a":["(",1],"b":["(",[8,9]]} +{d:0,a:(1 2) b:(8 9)} {"d":0,"a":["(",[1,2]],"b":["(",[8,9]]} +{d:0,a:(1 2 3) b:(8 9)} {"d":0,"a":["(",[1,2,3]],"b":["(",[8,9]]} +a:(1) {"a":["(",1]} +a:(1 2) {"a":["(",[1,2]]} +a:(1 2 3) {"a":["(",[1,2,3]]} +a:(1) b:9 {"a":["(",1],"b":9} +a:(1 2) b:9 {"a":["(",[1,2]],"b":9} +a:(1 2 3) b:9 {"a":["(",[1,2,3]],"b":9} +a:(1) b:(8 9) {"a":["(",1],"b":["(",[8,9]]} +a:(1 2) b:(8 9) {"a":["(",[1,2]],"b":["(",[8,9]]} +a:(1 2 3) b:(8 9) {"a":["(",[1,2,3]],"b":["(",[8,9]]} +d:0,a:(1) {"d":0,"a":["(",1]} +d:0,a:(1 2) {"d":0,"a":["(",[1,2]]} +d:0,a:(1 2 3) {"d":0,"a":["(",[1,2,3]]} +d:0,a:(1) b:9 {"d":0,"a":["(",1],"b":9} +d:0,a:(1 2) b:9 {"d":0,"a":["(",[1,2]],"b":9} +d:0,a:(1 2 3) b:9 {"d":0,"a":["(",[1,2,3]],"b":9} +d:0,a:(1) b:(8 9) {"d":0,"a":["(",1],"b":["(",[8,9]]} +d:0,a:(1 2) b:(8 9) {"d":0,"a":["(",[1,2]],"b":["(",[8,9]]} +d:0,a:(1 2 3) b:(8 9) {"d":0,"a":["(",[1,2,3]],"b":["(",[8,9]]} diff --git a/test/spec/paren-preval-basic.tsv b/test/spec/paren-preval-basic.tsv new file mode 100644 index 0000000..52dab5b --- /dev/null +++ b/test/spec/paren-preval-basic.tsv @@ -0,0 +1,13 @@ +# paren-preval-basic +# input expected_output +B ["<","B","C"] +a:b {"a":["<","b","c"]} +a:b {"a":["<","b",["c","d"]]} +<1> ["<",1] +1<2> ["<",1,2] +a:<1> {"a":["<",1]} +a:1<2> {"a":["<",1,2]} +9+<1> ["+",9,["<",1]] +9+1<2> ["+",9,["<",1,2]] +<1>+9 ["+",["<",1],9] +1<2>+9 ["+",["<",1,2],9] diff --git a/test/spec/paren-preval-implicit.tsv b/test/spec/paren-preval-implicit.tsv new file mode 100644 index 0000000..7bb9692 --- /dev/null +++ b/test/spec/paren-preval-implicit.tsv @@ -0,0 +1,5 @@ +# paren-preval-implicit +# input expected_output +foo,(1,a) ["foo",["(",[1,"a"]]] +foo,(1+2,a) ["foo",["(",[["+",1,2],"a"]]] +foo,(1+2+3,a) ["foo",["(",[["+",["+",1,2],3],"a"]]] diff --git a/test/spec/paren-preval-overload.tsv b/test/spec/paren-preval-overload.tsv new file mode 100644 index 0000000..d828d4a --- /dev/null +++ b/test/spec/paren-preval-overload.tsv @@ -0,0 +1,31 @@ +# paren-preval-overload +# input expected_output +[1] [1] +a[1] ["[","a",1] +[a[1]] [["[","a",1]] +a:[1] {"a":[1]} +a:b[1] {"a":["[","b",1]} +a:[b[1]] {"a":[["[","b",1]]} +{a:[1]} {"a":[1]} +{a:b[1]} {"a":["[","b",1]} +{a:[b[1]]} {"a":[["[","b",1]]} +-[1]+2 ["+",["-",[1]],2] +-a[1]+2 ["+",["-",["[","a",1]],2] +-[a[1]]+2 ["+",["-",[["[","a",1]]],2] +2+[1] ["+",2,[1]] +2+a[1] ["+",2,["[","a",1]] +2+[a[1]] ["+",2,[["[","a",1]]] +2+{a:[1]} ["+",2,{"a":[1]}] +2+{a:b[1]} ["+",2,{"a":["[","b",1]}] +2+{a:[b[1]]} ["+",2,{"a":[["[","b",1]]}] +a[b[1]] ["[","a",["[","b",1]] +a[b[c[1]]] ["[","a",["[","b",["[","c",1]]] +a[b[c[d[1]]]] ["[","a",["[","b",["[","c",["[","d",1]]]] +a{1} ["{","a",1] +a{b{1}} ["{","a",["{","b",1]] +a{b{c{1}}} ["{","a",["{","b",["{","c",1]]] +a{1+2} ["{","a",["+",1,2]] +a{b{1+2}} ["{","a",["{","b",["+",1,2]]] +a{b{c{1+2}}} ["{","a",["{","b",["{","c",["+",1,2]]]] +a{{x:1}} ["{","a",{"x":1}] +a{{x:1,y:2}} ["{","a",{"x":1,"y":2}] diff --git a/test/spec/structure.tsv b/test/spec/structure.tsv new file mode 100644 index 0000000..20caea4 --- /dev/null +++ b/test/spec/structure.tsv @@ -0,0 +1,7 @@ +# Expression structure tests - default Expr config +# input expected_output +a:1+2 {"a":["+",1,2]} +a:1+2,b:3+4 {"a":["+",1,2],"b":["+",3,4]} +[1+2] [["+",1,2]] +[1+2,3+4] [["+",1,2],["+",3,4]] +{a:[1+2]} {"a":[["+",1,2]]} diff --git a/test/spec/ternary-basic.tsv b/test/spec/ternary-basic.tsv new file mode 100644 index 0000000..e3793cf --- /dev/null +++ b/test/spec/ternary-basic.tsv @@ -0,0 +1,16 @@ +# Ternary operator tests - config:ternary +# input expected_output +1?2:3 ["?",1,2,3] +1?2: 3?4:5 ["?",1,2,["?",3,4,5]] +1?4:5 ?2:3 ["?",1,4,["?",5,2,3]] +1? 2?4:5 :3 ["?",1,["?",2,4,5],3] +0+1?2:3 ["?",["+",0,1],2,3] +0+1?2: 3?4:5 ["?",["+",0,1],2,["?",3,4,5]] +0+1?4:5 ?2:3 ["?",["+",0,1],4,["?",5,2,3]] +0+1? 2?4:5 :3 ["?",["+",0,1],["?",2,4,5],3] +1?0+2:3 ["?",1,["+",0,2],3] +1?2:0+3 ["?",1,2,["+",0,3]] +0+1?0+2:0+3 ["?",["+",0,1],["+",0,2],["+",0,3]] +-1?2:3 ["?",["-",1],2,3] +1!?2:3 ["?",["!",1],2,3] +-1!?2:3 ["?",["-",["!",1]],2,3] diff --git a/test/spec/unary-prefix-basic.tsv b/test/spec/unary-prefix-basic.tsv new file mode 100644 index 0000000..41a5ea6 --- /dev/null +++ b/test/spec/unary-prefix-basic.tsv @@ -0,0 +1,47 @@ +# Unary prefix operator tests - default Expr config +# input expected_output +-1 ["-",1] +- 1 ["-",1] ++1 ["+",1] ++ 1 ["+",1] +--1 ["-",["-",1]] +---1 ["-",["-",["-",1]]] +++1 ["+",["+",1]] ++++1 ["+",["+",["+",1]]] +-+1 ["-",["+",1]] ++-1 ["+",["-",1]] +--+1 ["-",["-",["+",1]]] +-+-1 ["-",["+",["-",1]]] ++--1 ["+",["-",["-",1]]] +-++1 ["-",["+",["+",1]]] +++-1 ["+",["+",["-",1]]] +-z ["-","z"] +- z ["-","z"] ++z ["+","z"] ++ z ["+","z"] +--z ["-",["-","z"]] +---z ["-",["-",["-","z"]]] +++z ["+",["+","z"]] ++++z ["+",["+",["+","z"]]] +-+z ["-",["+","z"]] ++-z ["+",["-","z"]] +--+z ["-",["-",["+","z"]]] +-+-z ["-",["+",["-","z"]]] ++--z ["+",["-",["-","z"]]] +-++z ["-",["+",["+","z"]]] +++-z ["+",["+",["-","z"]]] +1+2 ["+",1,2] +-1+2 ["+",["-",1],2] +--1+2 ["+",["-",["-",1]],2] +-1+-2 ["+",["-",1],["-",2]] +1+-2 ["+",1,["-",2]] +1++2 ["+",1,["+",2]] +-1++2 ["+",["-",1],["+",2]] +-1+2+3 ["+",["+",["-",1],2],3] +-1+-2+3 ["+",["+",["-",1],["-",2]],3] +-1+-2+-3 ["+",["+",["-",1],["-",2]],["-",3]] +-1+2+-3 ["+",["+",["-",1],2],["-",3]] +1+2+3 ["+",["+",1,2],3] +1+-2+3 ["+",["+",1,["-",2]],3] +1+-2+-3 ["+",["+",1,["-",2]],["-",3]] +1+2+-3 ["+",["+",1,2],["-",3]] diff --git a/test/spec/unary-prefix-edge.tsv b/test/spec/unary-prefix-edge.tsv new file mode 100644 index 0000000..ae04e9a --- /dev/null +++ b/test/spec/unary-prefix-edge.tsv @@ -0,0 +1,31 @@ +# Unary prefix edge cases - config:prefix-tight +# input expected_output +@1 ["@",1] +@@1 ["@",["@",1]] +@@@1 ["@",["@",["@",1]]] +-@1 ["-",["@",1]] +@-1 ["@",["-",1]] +--@1 ["-",["-",["@",1]]] +@--1 ["@",["-",["-",1]]] +@@-1 ["@",["@",["-",1]]] +-@@1 ["-",["@",["@",1]]] +-@-1 ["-",["@",["-",1]]] +@-@1 ["@",["-",["@",1]]] +@1+2 ["+",["@",1],2] +1+@2 ["+",1,["@",2]] +@1+@2 ["+",["@",1],["@",2]] +@1+2+3 ["+",["+",["@",1],2],3] +1+@2+3 ["+",["+",1,["@",2]],3] +@1+@2+3 ["+",["+",["@",1],["@",2]],3] +@1+2+@3 ["+",["+",["@",1],2],["@",3]] +1+@2+@3 ["+",["+",1,["@",2]],["@",3]] +@1+@2+@3 ["+",["+",["@",1],["@",2]],["@",3]] +@1~2 ["@",["~",1,2]] +1~@2 ["~",1,["@",2]] +@1~@2 ["@",["~",1,["@",2]]] +@1~2+3 ["+",["@",["~",1,2]],3] +1~@2+3 ["+",["~",1,["@",2]],3] +@1~@2+3 ["+",["@",["~",1,["@",2]]],3] +@1~2~3 ["@",["~",["~",1,2],3]] +1~@2~3 ["~",["~",1,["@",2]],3] +@1~@2~3 ["@",["~",["~",1,["@",2]],3]] diff --git a/test/spec/unary-suffix-basic.tsv b/test/spec/unary-suffix-basic.tsv new file mode 100644 index 0000000..d1f3781 --- /dev/null +++ b/test/spec/unary-suffix-basic.tsv @@ -0,0 +1,31 @@ +# Unary suffix operator tests - config:suffix +# input expected_output +1! ["!",1] +1 ! ["!",1] +1!! ["!",["!",1]] +1!!! ["!",["!",["!",1]]] +z! ["!","z"] +z ! ["!","z"] +1? ["?",1] +1 ? ["?",1] +1?? ["?",["?",1]] +1??? ["?",["?",["?",1]]] +1+2! ["+",1,["!",2]] +1!+2 ["+",["!",1],2] +1!+2! ["+",["!",1],["!",2]] +1+2!! ["+",1,["!",["!",2]]] +1!!+2 ["+",["!",["!",1]],2] +1!!+2!! ["+",["!",["!",1]],["!",["!",2]]] +1+2? ["+",1,["?",2]] +1?+2 ["+",["?",1],2] +1?+2? ["+",["?",1],["?",2]] +1+2?? ["+",1,["?",["?",2]]] +1??+2 ["+",["?",["?",1]],2] +1??+2?? ["+",["?",["?",1]],["?",["?",2]]] +0+1+2! ["+",["+",0,1],["!",2]] +0+1!+2 ["+",["+",0,["!",1]],2] +0+1!+2! ["+",["+",0,["!",1]],["!",2]] +0!+1!+2! ["+",["+",["!",0],["!",1]],["!",2]] +0!+1!+2 ["+",["+",["!",0],["!",1]],2] +0!+1+2! ["+",["+",["!",0],1],["!",2]] +0!+1+2 ["+",["+",["!",0],1],2] diff --git a/test/spec/unary-suffix-edge.tsv b/test/spec/unary-suffix-edge.tsv new file mode 100644 index 0000000..6774ca9 --- /dev/null +++ b/test/spec/unary-suffix-edge.tsv @@ -0,0 +1,31 @@ +# Unary suffix edge cases - config:suffix-tight +# input expected_output +1! ["!",1] +1!! ["!",["!",1]] +1!!! ["!",["!",["!",1]]] +1!? ["?",["!",1]] +1?! ["!",["?",1]] +1!?? ["?",["?",["!",1]]] +1??! ["!",["?",["?",1]]] +1?!! ["!",["!",["?",1]]] +1!!? ["?",["!",["!",1]]] +1?!? ["?",["!",["?",1]]] +1!?! ["!",["?",["!",1]]] +1!+2 ["+",["!",1],2] +1+2! ["+",1,["!",2]] +1!+2! ["+",["!",1],["!",2]] +1!+2+3 ["+",["+",["!",1],2],3] +1+2!+3 ["+",["+",1,["!",2]],3] +1!+2!+3 ["+",["+",["!",1],["!",2]],3] +1!+2+3! ["+",["+",["!",1],2],["!",3]] +1+2!+3! ["+",["+",1,["!",2]],["!",3]] +1!+2!+3! ["+",["+",["!",1],["!",2]],["!",3]] +1!~2 ["~",["!",1],2] +1~2! ["!",["~",1,2]] +1!~2! ["!",["~",["!",1],2]] +1!~2+3 ["+",["~",["!",1],2],3] +1~2!+3 ["+",["!",["~",1,2]],3] +1!~2!+3 ["+",["!",["~",["!",1],2]],3] +1!~2~3 ["~",["~",["!",1],2],3] +1~2!~3 ["~",["!",["~",1,2]],3] +1!~2!~3 ["~",["!",["~",["!",1],2]],3] diff --git a/test/spec/unary-suffix-paren.tsv b/test/spec/unary-suffix-paren.tsv new file mode 100644 index 0000000..f03e242 --- /dev/null +++ b/test/spec/unary-suffix-paren.tsv @@ -0,0 +1,6 @@ +# Suffix operators inside parens - config:suffix +# input expected_output +(0!+1!+2!) ["(",["+",["+",["!",0],["!",1]],["!",2]]] +(0!+1!+2) ["(",["+",["+",["!",0],["!",1]],2]] +(0!+1+2!) ["(",["+",["+",["!",0],1],["!",2]]] +(0!+1+2) ["(",["+",["+",["!",0],1],2]] diff --git a/test/spec/unary-suffix-prefix.tsv b/test/spec/unary-suffix-prefix.tsv new file mode 100644 index 0000000..b058105 --- /dev/null +++ b/test/spec/unary-suffix-prefix.tsv @@ -0,0 +1,17 @@ +# Combined suffix and prefix - config:suffix +# input expected_output +-1! ["-",["!",1]] +--1! ["-",["-",["!",1]]] +-1!! ["-",["!",["!",1]]] +--1!! ["-",["-",["!",["!",1]]]] +-1!+2 ["+",["-",["!",1]],2] +--1!+2 ["+",["-",["-",["!",1]]],2] +---1!+2 ["+",["-",["-",["-",["!",1]]]],2] +-1? ["?",["-",1]] +--1? ["?",["-",["-",1]]] +-1?? ["?",["?",["-",1]]] +--1?? ["?",["?",["-",["-",1]]]] +-1!? ["?",["-",["!",1]]] +-1!?! ["!",["?",["-",["!",1]]]] +-1?+2 ["+",["?",["-",1]],2] +--1?+2 ["+",["?",["-",["-",1]]],2] diff --git a/test/spec/unary-suffix-structure.tsv b/test/spec/unary-suffix-structure.tsv new file mode 100644 index 0000000..e6497fe --- /dev/null +++ b/test/spec/unary-suffix-structure.tsv @@ -0,0 +1,28 @@ +# Unary suffix in structures - config:suffix +# input expected_output +1!,2! [["!",1],["!",2]] +1!,2!,3! [["!",1],["!",2],["!",3]] +1!,2!,3!,4! [["!",1],["!",2],["!",3],["!",4]] +1! 2! [["!",1],["!",2]] +1! 2! 3! [["!",1],["!",2],["!",3]] +1! 2! 3! 4! [["!",1],["!",2],["!",3],["!",4]] +[1!,2!] [["!",1],["!",2]] +[1!,2!,3!] [["!",1],["!",2],["!",3]] +[1!,2!,3!,4!] [["!",1],["!",2],["!",3],["!",4]] +[1! 2!] [["!",1],["!",2]] +[1! 2! 3!] [["!",1],["!",2],["!",3]] +[1! 2! 3! 4!] [["!",1],["!",2],["!",3],["!",4]] +a:1! {"a":["!",1]} +a:1!,b:2! {"a":["!",1],"b":["!",2]} +a:1!,b:2!,c:3! {"a":["!",1],"b":["!",2],"c":["!",3]} +a:1!,b:2!,c:3!,d:4! {"a":["!",1],"b":["!",2],"c":["!",3],"d":["!",4]} +a:1! b:2! {"a":["!",1],"b":["!",2]} +a:1! b:2! c:3! {"a":["!",1],"b":["!",2],"c":["!",3]} +a:1! b:2! c:3!,d:4! {"a":["!",1],"b":["!",2],"c":["!",3],"d":["!",4]} +{a:1!} {"a":["!",1]} +{a:1!,b:2!} {"a":["!",1],"b":["!",2]} +{a:1!,b:2!,c:3!} {"a":["!",1],"b":["!",2],"c":["!",3]} +{a:1!,b:2!,c:3!,d:4!} {"a":["!",1],"b":["!",2],"c":["!",3],"d":["!",4]} +{a:1! b:2!} {"a":["!",1],"b":["!",2]} +{a:1! b:2! c:3!} {"a":["!",1],"b":["!",2],"c":["!",3]} +{a:1! b:2! c:3! d:4!} {"a":["!",1],"b":["!",2],"c":["!",3],"d":["!",4]}