createId = ast => {
        const entries = Object.entries(ast)
          .filter(([key, value]) => typeof value != 'undefined' && !ignoreProps.includes(key))
          .sort(([a], [b]) => a < b ? -1 : 1)

        const isChildAst = ast.value?.type && typeof ast.value.col == 'number'
        if (isChildAst) {
          entries.push(['value', createId(ast.value)])
        } else if (Array.isArray(ast.value)) { // and, or
          const children = ast.value.map(createId)
          if (ast.type == 'and') { // and expr can be sorted
            children.sort((a, b) => a < b ? -1 : 1)
          }

          entries.push(['value', children])
        } else if (ast.value) {
          entries.push(['value', `'${ast.value}'`])
        }

        return `{${entries.map(([key, value]) => `${key}:${value}`).join(',')}}`
      }
These rules simplify the ast without changing the expression semantic.

These rules are applied from bottom to top