<commit_before>module.exports = function(ast, file) {
  let result = file
  for (let i = ast.body.length - 1; i >= 0; i--) {
    let node = ast.body[i]
    if (node.type == "GrammarDeclaration")
      result = result.slice(0, node.start) + compileGrammar(node, file) + result.slice(node.end)
  }
  return result
}

let WS = "_whitespace_"

class Graph {
  constructor(grammar) {
    this.nodes = Object.create(null)
    this.curRule = null
    this.grammar = grammar
    this.rules = Object.create(null)
    this.first = null
    for (let i = 0; i < grammar.rules.length; i++) {
      let rule = grammar.rules[i]
      this.rules[rule.id.name] = {ast: rule, start: this.node(rule.id.name)}
      if (this.first == null && !rule.lexical) this.first = rule.id.name
    }
    if (grammar.whitespace)
      this.rules[WS] = {ast: grammar.whitespace, start: this.node(WS)}

    for (let n in this.rules) {
      let {ast, start} = this.rules[n]
      this.withRule(ast, () => {
        let end = this.node(null, "end")
        generateExpr(start, end, ast.expr, this)
        this.edge(end, null, new NullMatch(ast), [new ReturnEffect])
      })
    }
  }

  node(base, suffix) {
    let label = (base || this.curRule.id.name) + (suffix ? "_" + suffix : "")
    for (let i = 0;; i++) {
      let cur = i ? label + "_" + i : label
      if (!(cur in this.nodes)) {
        this.nodes[cur] = []
        return cur
      }
    }
  }

  edge(from, to, match, effects) {
    let edge = new Edge(to, match, effects)
    this.nodes[from].push(edge)
    return edge
  }

  withRule(rule, f) {
    let prevRule = this.curRule
    this.curRule = rule
    f()
    this.curRule = prevRule
  }

  gc() {
    let reached = Object.create(null), work = []

    function reach(node) {
      if (node in reached) return
      reached[node] = true
      work.push(node)
    }
    reach(this.rules[this.first].start)

    while (work.length) {
      let next = this.nodes[work.pop()]
      for (let i = 0; i < next.length; i++) {
        let edge = next[i]
        if (edge.to) reach(edge.to)
        for (let j = 0; j < edge.effects.length; j++)
          if (edge.effects[j] instanceof CallEffect)
            reach(edge.effects[j].returnTo)
      }
    }

    for (let n in this.nodes) if (!(n in reached)) delete this.nodes[n]
  }
}

class Edge {
  constructor(to, match, effects) {
    this.to = to
    this.match = match
    this.effects = effects || []
  }

  toString(from) {
    let effects = this.effects.length ? " " + this.effects.join(" ") : ""
    return `${from} -> ${this.to || "NULL"}[label=${JSON.stringify(this.match.toString() + effects)}]`
  }
}

class Match {
  constructor(ast) { this.ast = ast }
}

class StringMatch extends Match {
  constructor(ast, string) {
    super(ast)
    this.string = string
  }

  toString() { return JSON.stringify(this.string) }
}

class RangeMatch extends Match {
  constructor(ast, from, to) {
    super(ast)
    this.from = from
    this.to = to
  }

  toString() { return JSON.stringify(this.from) + "-" + JSON.stringify(this.to) }
}

class AnyMatch extends Match {
  toString() { return "_" }
}

class NullMatch extends Match {
  toString() { return "ø" }
}

class SeqMatch extends Match {
  constructor(ast, matches) {
    super(ast)
    this.matches = matches
  }

  toString() { return this.matches.join(" ") }

  static create(left, right) {
    if (left instanceof NullMatch) return right
    if (right instanceof NullMatch) return left
    let matches = []
    if (left instanceof SeqMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    let last = matches[matches.length - 1]
    if (right instanceof StringMatch && last instanceof StringMatch)
      matches[matches.length - 1] = new StringMatch(last.ast, last.value + right.value)
    else if (right instanceof SeqMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    if (matches.length == 1) return matches[0]
    else return new SeqMatch(left.ast, matches)
  }
}

class ChoiceMatch extends Match {
  constructor(ast, matches) {
    super(ast)
    this.matches = matches
  }

  toString() { return "(" + this.matches.join(" | ") + ")" }

  static create(left, right) {
    let matches = []
    if (left instanceof ChoiceMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    if (right instanceof ChoiceMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    return new ChoiceMatch(left.ast, matches)
  }
}

class RepeatMatch extends Match {
  constructor(ast, match) {
    super(ast)
    this.match = match
  }

  toString() { return this.match.toString() + "*" }
}

class CallEffect {
  constructor(rule, returnTo) {
    this.rule = rule
    this.returnTo = returnTo
  }

  eq(other) {
    return other instanceof CallEffect && other.rule == this.rule && other.returnTo == this.returnTo
  }

  toString() { return `call ${this.rule} -> ${this.returnTo}` }
}

class ReturnEffect {
  constructor() {}

  eq(other) { return other instanceof ReturnEffect }

  toString() { return "return" }
}

function generateExpr(start, end, expr, graph) {
  let t = expr.type
  if (t == "CharacterRange") {
    graph.edge(start, end, new RangeMatch(expr, expr.from, expr.to))
  } else if (t == "StringMatch") {
    graph.edge(start, end, new StringMatch(expr, expr.value))
  } else if (t == "AnyMatch") {
    graph.edge(start, end, new AnyMatch(expr))
  } else if (t == "RuleIdentifier") {
    let rule = graph.rules[expr.id.name]
    if (!rule) throw new SyntaxError(`No rule '${expr.id.name}' defined`)
    graph.edge(start, rule.start, new NullMatch(expr), [new CallEffect(expr.id.name, end)])
  } else if (t == "RepeatedMatch") {
    if (expr.kind == "*") {
      graph.edge(start, end, new NullMatch(expr))
      generateExpr(start, start, expr.expr, graph)
    } else if (expr.kind == "+") {
      generateExpr(start, end, expr.expr, graph)
      generateExpr(end, end, expr.expr, graph)
    } else if (expr.kind == "?") {
      graph.edge(start, end, new NullMatch(expr))
      generateExpr(start, end, expr.expr, graph)
    }
  } else if (t == "LookaheadMatch") {
    throw new Error("not supporting lookahead yet")
  } else if (t == "SequenceMatch") {
    for (let i = 0; i < expr.exprs.length; i++) {
      if (i && !graph.curRule.lexical && graph.rules[WS]) {
        let afterWS = graph.node()
        graph.edge(start, graph.rules[WS].start, new NullMatch(expr), [new CallEffect(WS, afterWS)])
        start = afterWS
      }
      let to = i == expr.exprs.length - 1 ? end : graph.node()
      generateExpr(start, to, expr.exprs[i], graph)
      start = to
    }
  } else if (t == "ChoiceMatch") {
    for (let i = 0; i < expr.exprs.length; i++)
      generateExpr(start, end, expr.exprs[i], graph)
  } else {
    throw new Error("Unrecognized AST node type " + t)
  }
}

function simplifySequence(graph, node, edges) {
  for (let i = 0; i < edges.length; i++) {
    let first = edges[i], next
    if (first.to == node || !first.to || (next = graph.nodes[first.to]).length != 1) continue
    let second = next[0], end = second.to, effects
    if (end == first.to) continue
    // If second is a return edge
    if (!end) for (let j = first.effects.length - 1; j >= 0; j--) {
      if (first.effects[j] instanceof CallEffect) {
      // Matching call found, wire directly to return address, remove call/return effects
        end = first.effects[j].returnTo
        effects = first.effects.slice(0, j).concat(first.effects.slice(j + 1))
          .concat(second.effects.filter(e => !(e instanceof ReturnEffect)))
      }
    }
    if (!effects) effects = first.effects.concat(second.effects)
    edges[i] = new Edge(end, SeqMatch.create(first.match, second.match), effects)
    return true
  }
  return false
}

function sameEffect(edge1, edge2) {
  let e1 = edge1.effects, e2 = edge2.effects
  if (e1.length != e2.length) return false
  for (let i = 0; i < e1.length; i++)
    if (!e1[i].eq(e2[i])) return false
  return true
}

function simplifyChoice(graph, node, edges) {
  for (let i = 0; i < edges.length; i++) {
    let edge = edges[i], set
    if (edge.match instanceof NullMatch) continue
    for (let j = i + 1; j < edges.length; j++) {
      let other = edges[j]
      if (other.to == edge.to && sameEffect(edge, other) && !(other.match instanceof NullMatch))
        (set || (set = [edge])).push(other)
    }
    if (set) {
      let match = set[0].match
      for (let j = 1; j < set.length; j++)
        match = ChoiceMatch.create(match, set[j].match)
      graph.nodes[node] = edges.filter(e => set.indexOf(e) == -1).concat(new Edge(edge.to, match, edge.effects))
      return true
    }
  }
  return false
}

function simplifyRepeat(graph, node, edges) {
  let cycleIndex, cycleEdge
  for (let i = 0; i < edges.length; i++) {
    let edge = edges[i]
    if (edge.to == node) {
      if (cycleEdge) return false
      cycleIndex = i
      cycleEdge = edge
    }
  }
  if (!cycleEdge || cycleEdge.effects.length) return false
  let newNode = graph.node(node, "split")
  graph.nodes[newNode] = edges.slice(0, cycleIndex).concat(edges.slice(cycleIndex + 1))
  graph.nodes[node] = [new Edge(newNode, new RepeatMatch(cycleEdge.match.ast, cycleEdge.match), cycleEdge.effects)]
  return true
}

// Look for simplification possibilities around the given node, return
// true if anything was done
function simplifyWith(graph, simplifiers) {
  let changed = false
  for (let node in graph.nodes) {
    let edges = graph.nodes[node]
    for (let i = 0; i < simplifiers.length; i++) if (simplifiers[i](graph, node, edges)) {
      changed = true
      break
    }
  }
  return changed
}

function simplify(graph) {
  while (simplifyWith(graph, [simplifySequence, simplifyChoice, simplifyRepeat])) {}
}

function printGraph(graph) {
  let output = "digraph {\n"
  for (let node in graph.nodes) {
    let edges = graph.nodes[node]
    for (let i = 0; i < edges.length; i++)
      output += "  " + edges[i].toString(node) + ";\n"
  }
  return output + "}\n"
}

function compileGrammar(grammar, file) {
  let graph = new Graph(grammar)
  simplify(graph)
  graph.gc()
  console.log(printGraph(graph))
  return "FIXME"
}
<commit_msg>Simplify match representation<commit_after>module.exports = function(ast, file) {
  let result = file
  for (let i = ast.body.length - 1; i >= 0; i--) {
    let node = ast.body[i]
    if (node.type == "GrammarDeclaration")
      result = result.slice(0, node.start) + compileGrammar(node, file) + result.slice(node.end)
  }
  return result
}

let WS = "_whitespace_"

class Graph {
  constructor(grammar) {
    this.nodes = Object.create(null)
    this.curRule = null
    this.grammar = grammar
    this.rules = Object.create(null)
    this.first = null
    for (let i = 0; i < grammar.rules.length; i++) {
      let rule = grammar.rules[i]
      this.rules[rule.id.name] = {ast: rule, start: this.node(rule.id.name)}
      if (this.first == null && !rule.lexical) this.first = rule.id.name
    }
    if (grammar.whitespace)
      this.rules[WS] = {ast: grammar.whitespace, start: this.node(WS)}

    for (let n in this.rules) {
      let {ast, start} = this.rules[n]
      this.withRule(ast, () => {
        let end = this.node(null, "end")
        generateExpr(start, end, ast.expr, this)
        this.edge(end, null, null, [new ReturnEffect])
      })
    }
  }

  node(base, suffix) {
    let label = (base || this.curRule.id.name) + (suffix ? "_" + suffix : "")
    for (let i = 0;; i++) {
      let cur = i ? label + "_" + i : label
      if (!(cur in this.nodes)) {
        this.nodes[cur] = []
        return cur
      }
    }
  }

  edge(from, to, match, effects) {
    let edge = new Edge(to, match || nullMatch, effects)
    this.nodes[from].push(edge)
    return edge
  }

  withRule(rule, f) {
    let prevRule = this.curRule
    this.curRule = rule
    f()
    this.curRule = prevRule
  }

  gc() {
    let reached = Object.create(null), work = []

    function reach(node) {
      if (node in reached) return
      reached[node] = true
      work.push(node)
    }
    reach(this.rules[this.first].start)

    while (work.length) {
      let next = this.nodes[work.pop()]
      for (let i = 0; i < next.length; i++) {
        let edge = next[i]
        if (edge.to) reach(edge.to)
        for (let j = 0; j < edge.effects.length; j++)
          if (edge.effects[j] instanceof CallEffect)
            reach(edge.effects[j].returnTo)
      }
    }

    for (let n in this.nodes) if (!(n in reached)) delete this.nodes[n]
  }
}

class Edge {
  constructor(to, match, effects) {
    this.to = to
    this.match = match
    this.effects = effects || []
  }

  toString(from) {
    let effects = this.effects.length ? " " + this.effects.join(" ") : ""
    return `${from} -> ${this.to || "NULL"}[label=${JSON.stringify(this.match.toString() + effects)}]`
  }
}

class StringMatch {
  constructor(string) {
    this.string = string
  }

  toString() { return JSON.stringify(this.string) }
}

class RangeMatch {
  constructor(from, to) {
    this.from = from
    this.to = to
  }

  toString() { return JSON.stringify(this.from) + "-" + JSON.stringify(this.to) }
}

const anyMatch = new class AnyMatch {
  toString() { return "_" }
}

const nullMatch = new class NullMatch {
  toString() { return "ø" }
}

class SeqMatch {
  constructor(matches) {
    this.matches = matches
  }

  toString() { return this.matches.join(" ") }

  static create(left, right) {
    if (left == nullMatch) return right
    if (right == nullMatch) return left
    let matches = []
    if (left instanceof SeqMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    let last = matches[matches.length - 1]
    if (right instanceof StringMatch && last instanceof StringMatch)
      matches[matches.length - 1] = new StringMatch(last.value + right.value)
    else if (right instanceof SeqMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    if (matches.length == 1) return matches[0]
    else return new SeqMatch(matches)
  }
}

class ChoiceMatch {
  constructor(matches) {
    this.matches = matches
  }

  toString() { return "(" + this.matches.join(" | ") + ")" }

  static create(left, right) {
    let matches = []
    if (left instanceof ChoiceMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    if (right instanceof ChoiceMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    return new ChoiceMatch(matches)
  }
}

class RepeatMatch {
  constructor(match) {
    this.match = match
  }

  toString() { return this.match.toString() + "*" }
}

class CallEffect {
  constructor(rule, returnTo) {
    this.rule = rule
    this.returnTo = returnTo
  }

  eq(other) {
    return other instanceof CallEffect && other.rule == this.rule && other.returnTo == this.returnTo
  }

  toString() { return `call ${this.rule} -> ${this.returnTo}` }
}

class ReturnEffect {
  constructor() {}

  eq(other) { return other instanceof ReturnEffect }

  toString() { return "return" }
}

function generateExpr(start, end, expr, graph) {
  let t = expr.type
  if (t == "CharacterRange") {
    graph.edge(start, end, new RangeMatch(expr.from, expr.to))
  } else if (t == "StringMatch") {
    graph.edge(start, end, new StringMatch(expr.value))
  } else if (t == "AnyMatch") {
    graph.edge(start, end, anyMatch)
  } else if (t == "RuleIdentifier") {
    let rule = graph.rules[expr.id.name]
    if (!rule) throw new SyntaxError(`No rule '${expr.id.name}' defined`)
    graph.edge(start, rule.start, null, [new CallEffect(expr.id.name, end)])
  } else if (t == "RepeatedMatch") {
    if (expr.kind == "*") {
      graph.edge(start, end)
      generateExpr(start, start, expr.expr, graph)
    } else if (expr.kind == "+") {
      generateExpr(start, end, expr.expr, graph)
      generateExpr(end, end, expr.expr, graph)
    } else if (expr.kind == "?") {
      graph.edge(start, end)
      generateExpr(start, end, expr.expr, graph)
    }
  } else if (t == "LookaheadMatch") {
    throw new Error("not supporting lookahead yet")
  } else if (t == "SequenceMatch") {
    for (let i = 0; i < expr.exprs.length; i++) {
      if (i && !graph.curRule.lexical && graph.rules[WS]) {
        let afterWS = graph.node()
        graph.edge(start, graph.rules[WS].start, null, [new CallEffect(WS, afterWS)])
        start = afterWS
      }
      let to = i == expr.exprs.length - 1 ? end : graph.node()
      generateExpr(start, to, expr.exprs[i], graph)
      start = to
    }
  } else if (t == "ChoiceMatch") {
    for (let i = 0; i < expr.exprs.length; i++)
      generateExpr(start, end, expr.exprs[i], graph)
  } else {
    throw new Error("Unrecognized AST node type " + t)
  }
}

function simplifySequence(graph, node, edges) {
  for (let i = 0; i < edges.length; i++) {
    let first = edges[i], next
    if (first.to == node || !first.to || (next = graph.nodes[first.to]).length != 1) continue
    let second = next[0], end = second.to, effects
    if (end == first.to) continue
    // If second is a return edge
    if (!end) for (let j = first.effects.length - 1; j >= 0; j--) {
      if (first.effects[j] instanceof CallEffect) {
      // Matching call found, wire directly to return address, remove call/return effects
        end = first.effects[j].returnTo
        effects = first.effects.slice(0, j).concat(first.effects.slice(j + 1))
          .concat(second.effects.filter(e => !(e instanceof ReturnEffect)))
      }
    }
    if (!effects) effects = first.effects.concat(second.effects)
    edges[i] = new Edge(end, SeqMatch.create(first.match, second.match), effects)
    return true
  }
  return false
}

function sameEffect(edge1, edge2) {
  let e1 = edge1.effects, e2 = edge2.effects
  if (e1.length != e2.length) return false
  for (let i = 0; i < e1.length; i++)
    if (!e1[i].eq(e2[i])) return false
  return true
}

function simplifyChoice(graph, node, edges) {
  for (let i = 0; i < edges.length; i++) {
    let edge = edges[i], set
    if (edge.match == nullMatch) continue
    for (let j = i + 1; j < edges.length; j++) {
      let other = edges[j]
      if (other.to == edge.to && sameEffect(edge, other) && other.match != nullMatch)
        (set || (set = [edge])).push(other)
    }
    if (set) {
      let match = set[0].match
      for (let j = 1; j < set.length; j++)
        match = ChoiceMatch.create(match, set[j].match)
      graph.nodes[node] = edges.filter(e => set.indexOf(e) == -1).concat(new Edge(edge.to, match, edge.effects))
      return true
    }
  }
  return false
}

function simplifyRepeat(graph, node, edges) {
  let cycleIndex, cycleEdge
  for (let i = 0; i < edges.length; i++) {
    let edge = edges[i]
    if (edge.to == node) {
      if (cycleEdge) return false
      cycleIndex = i
      cycleEdge = edge
    }
  }
  if (!cycleEdge || cycleEdge.effects.length) return false
  let newNode = graph.node(node, "split")
  graph.nodes[newNode] = edges.slice(0, cycleIndex).concat(edges.slice(cycleIndex + 1))
  graph.nodes[node] = [new Edge(newNode, new RepeatMatch(cycleEdge.match), cycleEdge.effects)]
  return true
}

// Look for simplification possibilities around the given node, return
// true if anything was done
function simplifyWith(graph, simplifiers) {
  let changed = false
  for (let node in graph.nodes) {
    let edges = graph.nodes[node]
    for (let i = 0; i < simplifiers.length; i++) if (simplifiers[i](graph, node, edges)) {
      changed = true
      break
    }
  }
  return changed
}

function simplify(graph) {
  while (simplifyWith(graph, [simplifySequence, simplifyChoice, simplifyRepeat])) {}
}

function printGraph(graph) {
  let output = "digraph {\n"
  for (let node in graph.nodes) {
    let edges = graph.nodes[node]
    for (let i = 0; i < edges.length; i++)
      output += "  " + edges[i].toString(node) + ";\n"
  }
  return output + "}\n"
}

function compileGrammar(grammar, file) {
  let graph = new Graph(grammar)
  simplify(graph)
  graph.gc()
  console.log(printGraph(graph))
  return "FIXME"
}
<|endoftext|>


<commit_before>import createNamespace from './namespace';
import getIdentifier from './util/getIdentifier';
import CodeGenerator from './util/codeGenerator';

export default function compile(ast, namespace = createNamespace()) {
  console.log(JSON.stringify(ast, null, 2));
  // Create compiler state.
  let state = { ast, namespace };
  state.resolveType = resolveType.bind(null, state);
  state.resolveBlock = resolveBlock.bind(null, state);
  // Resolve each block - all blocks will be compiled then.
  // However, if a circular reference occurs, a stack overflow will happen.
  // It can be resolved by sacrificing some functions to resolve other
  // datatypes using proxy objects instead of direct function references.
  // But such use case won't happen, so I'll do it someday later.
  // TODO Fix stack overflow.
  for (let key in ast) {
    resolveBlock(state, key);
  }
}

export function assert(expected, received) {
  if (expected !== received) {
    throw new Error(
      `Assertion error: Expected ${expected}, but got ${received}`);
  }
}

function resolveType(state, type, parentGenerics) {
  let resolvedType = type;
  if (type.generic === true) resolvedType = parentGenerics[type.name];
  if (Array.isArray(resolvedType)) {
    // Namespaces are hard to handle. Nevertheless, we need to implement them
    // to implement enums.
    // If an array is provided, we need to resolve AST / namespace in order,
    // returning valid object with that name.
    // resolveBlock should be able to distinguish local scope and
    // global scope, allowing to use global namespace if local namespace
    // doesn't have the requested entry.
    return resolvedType.reduce((prev, typeVal, i) => {
      // Start from root; narrow down to right entry. Repeat until the end.
      // parentGenerics should be same all the time.
      let resolvedTypeVal = typeVal;
      if (typeVal.generic === true) {
        resolvedTypeVal = parentGenerics[typeVal.name];
      }
      let astKey = getIdentifier({ name: resolvedTypeVal.name },
        resolvedTypeVal.generics && resolvedTypeVal.generics.map(() => '_'));
      let block = resolveBlock(prev, resolvedTypeVal.name,
        resolvedTypeVal.generics, parentGenerics);
      if (i === resolvedType.length - 1) {
        // Done! Directly return the block.
        return block;
      } else {
        // Descend...
        return {
          // Store 'root' object to use in compiling mode; compilers have to
          // refer root namespace directly.
          root: prev.root || prev,
          namespace: block.namespace,
          ast: prev.ast[astKey],
        };
      }
    }, state);
  }
  return resolveBlock(state, resolvedType.name,
    resolvedType.generics, parentGenerics);
}

function resolveBlock(state, name, generics, parentGenerics) {
  const { ast, namespace } = state;
  let genericsData = generics;
  if (generics != null && parentGenerics != null) {
    genericsData = generics.map(v => v.generic ? parentGenerics[v.name] : v);
  }
  let key = getIdentifier({ name }, genericsData);
  let astBlock = ast[key];
  // If 'generics' is provided and the astBlock is missing, compile against
  // the generics template.
  if (generics != null && astBlock == null) {
    let template = resolveBlock(state,
      getIdentifier({ name }, generics.map(() => '_')));
    if (template == null) throw new Error(`${key} is not defined`);
    // Swap the astBlock to the template and continue.
    astBlock = template;
  } else if (astBlock == null && namespace[key] == null) {
    throw new Error(`${key} is not defined`);
  }
  // If the block is already compiled, skip it.
  if (namespace[key] != null) return namespace[key];
  // 'Lock' the output object to avoid stack overflow. Any other functions
  // meeting this locked object will use proxy objects instead.
  namespace[key] = { name: key, locked: true, namespace: {} };
  // If 'generics' is not defined and the block uses generics, return a
  // function that compiles the block using generics.
  if (generics == null && astBlock.generics != null) {
    namespace[key] = (state, generics, namespace) => {
      // Since the generics variable is already processed by parentGenerics,
      // we can just call compileBlock with correct generics. Done!
      return compileBlock(state, astBlock, generics, namespace);
    };
    namespace[key].ast = astBlock;
    return namespace[key];
  }
  // Otherwise, just compile it!
  let result = compileBlock(state.root || state, astBlock, genericsData,
    namespace[key].namespace);
  result.name = key;
  result.ast = astBlock.ast || astBlock;
  // If the AST has namespace definition, move previous namespace definition
  // in locked object onto the result object.
  if (astBlock.namespace != null) result.namespace = namespace[key].namespace;
  namespace[key] = result;
  return result;
}

// Assume that everything is compiled at this moment.
function compileBlock(state, astBlock, generics, namespace) {
  if (typeof astBlock === 'function') {
    return astBlock(state, generics, namespace);
  }
  if (astBlock.type === 'struct') {
    return compileStruct(state, astBlock, generics);
  }
  if (astBlock.type === 'enum') {
    return compileEnum(state, astBlock, generics, namespace);
  }
  throw new Error('Unknown type ' + astBlock.type);
}

function compileStruct(state, ast, generics) {
  let codeGen = new CodeGenerator(state);
  // TODO We can directly reference functions; but since that's complicated,
  // just use indirect reference now
  function writeEntry(key, value) {
    if (value.jsConst) {
      codeGen.pushDecode(`#value#[${key}] = ${JSON.stringify(value.value)};`);
    } else if (value.const) {
      let type = resolveType(state, value.type, generics);
      let valueStr = JSON.stringify(value.value);
      codeGen.pushTypeEncode(valueStr, type);
      codeGen.pushTypeDecode('assertValue', type, true);
      // TODO Actually assert the value
      // decodeCode.push(`assert(${valueStr}, ${ref}.decode(dataView));`);
    } else {
      let type = resolveType(state, value, generics);
      // If the type is nullable, read a byte to check if the data exists.
      if (value.nullable) {
        // TODO We can compact the size by putting 8 null flags onto
        // single byte.
        let u8 = resolveType(state, { name: 'u8' });
        let fieldName = 'nullCheck' + (Math.random() * 100000 | 0);
        codeGen.pushEncode(
          `var ${fieldName} = #value#[${key}] == null ? 1 : 0;`);
        codeGen.pushType(fieldName, u8, true);
        codeGen.push(`if (${fieldName} !== 0) {`);
        codeGen.pushType(`#value#[${key}]`, type);
        codeGen.push('}');
      } else {
        codeGen.pushType(`#value#[${key}]`, type);
      }
    }
  }
  switch (ast.subType) {
    case 'object': {
      codeGen.pushDecode('#value# = {};');
      ast.keys.forEach(key => {
        // If key is an object, process it separately since it is a const
        // value, not an actual value.
        if (typeof key === 'object' && key.const) {
          writeEntry(null, key);
        } else {
          writeEntry(`"${key}"`, ast.values[key]);
        }
      });
      break;
    }
    case 'array': {
      codeGen.pushDecode('#value# = [];');
      let pos = 0;
      ast.keys.forEach(key => {
        if (key.const) {
          writeEntry(null, key);
        } else {
          writeEntry(pos++, key);
        }
      });
      break;
    }
    case 'empty': {
      codeGen.pushDecode('#value# = {};');
      break;
    }
  }
  return codeGen.compile();
}

function compileEnum(state, ast, generics, namespace) {
  // Create a code generator, then loop for every entry in the entries list,
  // compile them into switch loop.
  let codeGen = new CodeGenerator(state);
  let typeRef = JSON.stringify(ast.typeTarget);
  if (ast.subType === 'array') typeRef = '0';
  // We have to build encode / decode routine separately - they can't be shared.
  // TODO Support nulls? Although it's not necessary at all, but it'd be good
  // if we can support it.
  // Read the type object.
  let varName = 'enumType' + (Math.random() * 100000 | 0);
  let varOut = 'enumData' + (Math.random() * 100000 | 0);
  let typeType = resolveType(state, ast.typeType, generics);
  let localState = { root: state, namespace, ast: ast.namespace };
  codeGen.push(`var ${varOut};`);
  codeGen.pushEncode(`${varOut} = #value#;`);
  codeGen.pushTypeDecode(varName, typeType, true);
  codeGen.pushEncode(`switch (#value#[${typeRef}]) {`);
  codeGen.pushDecode(`switch (${varName}) {`);
  // Now, insert case clauses using for loop.
  ast.entries.forEach(([key, valueName]) => {
    let keyStr = JSON.stringify(key);
    let valueNameStr = JSON.stringify(valueName);
    let type = resolveType(localState, { name: valueName }, generics);
    if (ast.subType === 'array' && type.ast.keys[0].jsConst) {
      valueNameStr = JSON.stringify(type.ast.keys[0].value);
    } else if (type.ast.values != null &&
      type.ast.values[ast.typeTarget] != null
    ) {
      valueNameStr = JSON.stringify(type.ast.values[ast.typeTarget].value);
    }
    codeGen.pushEncode(`case ${valueNameStr}:`);
    codeGen.pushDecode(`case ${keyStr}:`);
    // Slice header if const is not specified at front.
    if (ast.subType === 'array' && !type.ast.keys[0].jsConst) {
      codeGen.pushEncode(`${varOut} = #value#.slice(0);`);
    }
    // Encode the type; this is already done in decoder.
    codeGen.pushTypeEncode(keyStr, typeType);
    // Now, encode / decode the value.
    // If the value is array, we have to increment each key. This is
    // not possible yet, so we'll just use temporary variable to store the
    // result, then concat with the old array.
    codeGen.pushType(varOut, type);
    if (ast.subType === 'array') {
      if (!type.ast.keys[0].jsConst) {
        codeGen.pushDecode(`${varOut}.unshift(${valueNameStr});`);
      }
    } else if (type.ast.values == null ||
      type.ast.values[ast.typeTarget] == null
    ) {
      codeGen.pushDecode(`${varOut}[${typeRef}] = ${valueNameStr};`);
    }
    codeGen.push('break;');
  });
  codeGen.push('}');
  codeGen.pushDecode(`#value# = ${varOut};`);
  return codeGen.compile();
}
<commit_msg>Implement nullable compression<commit_after>import createNamespace from './namespace';
import getIdentifier from './util/getIdentifier';
import CodeGenerator from './util/codeGenerator';

export default function compile(ast, namespace = createNamespace()) {
  console.log(JSON.stringify(ast, null, 2));
  // Create compiler state.
  let state = { ast, namespace };
  state.resolveType = resolveType.bind(null, state);
  state.resolveBlock = resolveBlock.bind(null, state);
  // Resolve each block - all blocks will be compiled then.
  // However, if a circular reference occurs, a stack overflow will happen.
  // It can be resolved by sacrificing some functions to resolve other
  // datatypes using proxy objects instead of direct function references.
  // But such use case won't happen, so I'll do it someday later.
  // TODO Fix stack overflow.
  for (let key in ast) {
    resolveBlock(state, key);
  }
}

export function assert(expected, received) {
  if (expected !== received) {
    throw new Error(
      `Assertion error: Expected ${expected}, but got ${received}`);
  }
}

function resolveType(state, type, parentGenerics) {
  let resolvedType = type;
  if (type.generic === true) resolvedType = parentGenerics[type.name];
  if (Array.isArray(resolvedType)) {
    // Namespaces are hard to handle. Nevertheless, we need to implement them
    // to implement enums.
    // If an array is provided, we need to resolve AST / namespace in order,
    // returning valid object with that name.
    // resolveBlock should be able to distinguish local scope and
    // global scope, allowing to use global namespace if local namespace
    // doesn't have the requested entry.
    return resolvedType.reduce((prev, typeVal, i) => {
      // Start from root; narrow down to right entry. Repeat until the end.
      // parentGenerics should be same all the time.
      let resolvedTypeVal = typeVal;
      if (typeVal.generic === true) {
        resolvedTypeVal = parentGenerics[typeVal.name];
      }
      let astKey = getIdentifier({ name: resolvedTypeVal.name },
        resolvedTypeVal.generics && resolvedTypeVal.generics.map(() => '_'));
      let block = resolveBlock(prev, resolvedTypeVal.name,
        resolvedTypeVal.generics, parentGenerics);
      if (i === resolvedType.length - 1) {
        // Done! Directly return the block.
        return block;
      } else {
        // Descend...
        return {
          // Store 'root' object to use in compiling mode; compilers have to
          // refer root namespace directly.
          root: prev.root || prev,
          namespace: block.namespace,
          ast: prev.ast[astKey],
        };
      }
    }, state);
  }
  return resolveBlock(state, resolvedType.name,
    resolvedType.generics, parentGenerics);
}

function resolveBlock(state, name, generics, parentGenerics) {
  const { ast, namespace } = state;
  let genericsData = generics;
  if (generics != null && parentGenerics != null) {
    genericsData = generics.map(v => v.generic ? parentGenerics[v.name] : v);
  }
  let key = getIdentifier({ name }, genericsData);
  let astBlock = ast[key];
  // If 'generics' is provided and the astBlock is missing, compile against
  // the generics template.
  if (generics != null && astBlock == null) {
    let template = resolveBlock(state,
      getIdentifier({ name }, generics.map(() => '_')));
    if (template == null) throw new Error(`${key} is not defined`);
    // Swap the astBlock to the template and continue.
    astBlock = template;
  } else if (astBlock == null && namespace[key] == null) {
    throw new Error(`${key} is not defined`);
  }
  // If the block is already compiled, skip it.
  if (namespace[key] != null) return namespace[key];
  // 'Lock' the output object to avoid stack overflow. Any other functions
  // meeting this locked object will use proxy objects instead.
  namespace[key] = { name: key, locked: true, namespace: {} };
  // If 'generics' is not defined and the block uses generics, return a
  // function that compiles the block using generics.
  if (generics == null && astBlock.generics != null) {
    namespace[key] = (state, generics, namespace) => {
      // Since the generics variable is already processed by parentGenerics,
      // we can just call compileBlock with correct generics. Done!
      return compileBlock(state, astBlock, generics, namespace);
    };
    namespace[key].ast = astBlock;
    return namespace[key];
  }
  // Otherwise, just compile it!
  let result = compileBlock(state.root || state, astBlock, genericsData,
    namespace[key].namespace);
  result.name = key;
  result.ast = astBlock.ast || astBlock;
  // If the AST has namespace definition, move previous namespace definition
  // in locked object onto the result object.
  if (astBlock.namespace != null) result.namespace = namespace[key].namespace;
  namespace[key] = result;
  return result;
}

// Assume that everything is compiled at this moment.
function compileBlock(state, astBlock, generics, namespace) {
  if (typeof astBlock === 'function') {
    return astBlock(state, generics, namespace);
  }
  if (astBlock.type === 'struct') {
    return compileStruct(state, astBlock, generics);
  }
  if (astBlock.type === 'enum') {
    return compileEnum(state, astBlock, generics, namespace);
  }
  throw new Error('Unknown type ' + astBlock.type);
}

function compileStruct(state, ast, generics) {
  let codeGen = new CodeGenerator(state);
  let nullableCount = 0;
  let nullFieldName = 'nullCheck' + (Math.random() * 100000 | 0);
  function writeNullable(key, value) {
    if (value.nullable) {
      let bytePos = (nullableCount / 8) | 0;
      let fieldName = nullFieldName + bytePos;
      if (nullableCount % 8 === 0) {
        let u8 = resolveType(state, { name: 'u8' });
        codeGen.pushTypeDecode(fieldName, u8, true);
        if (bytePos > 0) {
          codeGen.pushTypeEncode(nullFieldName + (bytePos - 1), u8);
        }
        codeGen.pushEncode(`var ${fieldName} = 0;`);
      }
      let shiftPos = 1 << (nullableCount % 8);
      codeGen.pushEncode(
        `${fieldName} |= #value#[${key}] != null ? ${shiftPos} : 0;`);
      nullableCount++;
    }
  }
  function finalizeNullable() {
    if (nullableCount > 0) {
      let bytePos = (nullableCount / 8) | 0;
      let u8 = resolveType(state, { name: 'u8' });
      codeGen.pushTypeEncode(nullFieldName + bytePos, u8);
    }
    nullableCount = 0;
  }
  function writeEntry(key, value) {
    if (value.jsConst) {
      codeGen.pushDecode(`#value#[${key}] = ${JSON.stringify(value.value)};`);
    } else if (value.const) {
      let type = resolveType(state, value.type, generics);
      let valueStr = JSON.stringify(value.value);
      codeGen.pushTypeEncode(valueStr, type);
      codeGen.pushTypeDecode('assertValue', type, true);
      // TODO Actually assert the value
      // decodeCode.push(`assert(${valueStr}, ${ref}.decode(dataView));`);
    } else {
      let type = resolveType(state, value, generics);
      // If the type is nullable, read a byte to check if the data exists.
      if (value.nullable) {
        let bytePos = (nullableCount / 8) | 0;
        let flagName = nullFieldName + bytePos;
        let shiftPos = 1 << (nullableCount % 8);
        codeGen.push(`if ((${flagName} & ${shiftPos}) !== 0) {`);
        codeGen.pushType(`#value#[${key}]`, type);
        codeGen.push('}');
        nullableCount++;
      } else {
        codeGen.pushType(`#value#[${key}]`, type);
      }
    }
  }
  switch (ast.subType) {
    case 'object': {
      codeGen.pushDecode('#value# = {};');
      ast.keys.forEach(key => {
        if (typeof key === 'string') {
          writeNullable(`"${key}"`, ast.values[key]);
        }
      });
      finalizeNullable();
      ast.keys.forEach(key => {
        // If key is an object, process it separately since it is a const
        // value, not an actual value.
        if (typeof key === 'object' && key.const) {
          writeEntry(null, key);
        } else {
          writeEntry(`"${key}"`, ast.values[key]);
        }
      });
      break;
    }
    case 'array': {
      codeGen.pushDecode('#value# = [];');
      let pos = 0;
      ast.keys.forEach(key => {
        if (key.const) {
          writeEntry(null, key);
        } else {
          writeEntry(pos++, key);
        }
      });
      break;
    }
    case 'empty': {
      codeGen.pushDecode('#value# = {};');
      break;
    }
  }
  return codeGen.compile();
}

function compileEnum(state, ast, generics, namespace) {
  // Create a code generator, then loop for every entry in the entries list,
  // compile them into switch loop.
  let codeGen = new CodeGenerator(state);
  let typeRef = JSON.stringify(ast.typeTarget);
  if (ast.subType === 'array') typeRef = '0';
  // We have to build encode / decode routine separately - they can't be shared.
  // TODO Support nulls? Although it's not necessary at all, but it'd be good
  // if we can support it.
  // Read the type object.
  let varName = 'enumType' + (Math.random() * 100000 | 0);
  let varOut = 'enumData' + (Math.random() * 100000 | 0);
  let typeType = resolveType(state, ast.typeType, generics);
  let localState = { root: state, namespace, ast: ast.namespace };
  codeGen.push(`var ${varOut};`);
  codeGen.pushEncode(`${varOut} = #value#;`);
  codeGen.pushTypeDecode(varName, typeType, true);
  codeGen.pushEncode(`switch (#value#[${typeRef}]) {`);
  codeGen.pushDecode(`switch (${varName}) {`);
  // Now, insert case clauses using for loop.
  ast.entries.forEach(([key, valueName]) => {
    let keyStr = JSON.stringify(key);
    let valueNameStr = JSON.stringify(valueName);
    let type = resolveType(localState, { name: valueName }, generics);
    if (ast.subType === 'array' && type.ast.keys[0].jsConst) {
      valueNameStr = JSON.stringify(type.ast.keys[0].value);
    } else if (type.ast.values != null &&
      type.ast.values[ast.typeTarget] != null
    ) {
      valueNameStr = JSON.stringify(type.ast.values[ast.typeTarget].value);
    }
    codeGen.pushEncode(`case ${valueNameStr}:`);
    codeGen.pushDecode(`case ${keyStr}:`);
    // Slice header if const is not specified at front.
    if (ast.subType === 'array' && !type.ast.keys[0].jsConst) {
      codeGen.pushEncode(`${varOut} = #value#.slice(0);`);
    }
    // Encode the type; this is already done in decoder.
    codeGen.pushTypeEncode(keyStr, typeType);
    // Now, encode / decode the value.
    // If the value is array, we have to increment each key. This is
    // not possible yet, so we'll just use temporary variable to store the
    // result, then concat with the old array.
    codeGen.pushType(varOut, type);
    if (ast.subType === 'array') {
      if (!type.ast.keys[0].jsConst) {
        codeGen.pushDecode(`${varOut}.unshift(${valueNameStr});`);
      }
    } else if (type.ast.values == null ||
      type.ast.values[ast.typeTarget] == null
    ) {
      codeGen.pushDecode(`${varOut}[${typeRef}] = ${valueNameStr};`);
    }
    codeGen.push('break;');
  });
  codeGen.push('}');
  codeGen.pushDecode(`#value# = ${varOut};`);
  return codeGen.compile();
}
<|endoftext|>


<commit_before>module.exports = function(ast, file) {
  let result = file
  for (let i = ast.body.length - 1; i >= 0; i--) {
    let node = ast.body[i]
    if (node.type == "GrammarDeclaration")
      result = result.slice(0, node.start) + compileGrammar(node, file) + result.slice(node.end)
  }
  return result
}

class Graph {
  constructor(grammar) {
    this.nodes = Object.create(null)
    this.aliases = Object.create(this.nodes)
    this.curLabel = "-"
    this.rules = Object.create(null)
    this.grammar = grammar
    this.first = null
    for (let i = 0; i < grammar.rules.length; i++) {
      let rule = grammar.rules[i]
      this.rules[rule.id.name] = {ast: rule, start: null, end: null}
      if (this.first == null && !rule.lexical) this.first = rule.id.name
    }
    fillRule(this, this.rules[this.first])
  }

  node(suffix) {
    // FIXME make sure this is unique
    let label = this.curLabel + (suffix ? "_" + suffix : "")
    for (let i = 0;; i++) {
      let cur = i ? label + "_" + i : label
      if (!(cur in this.aliases)) return this.nodes[cur] = new Node(cur)
    }
  }

  edge(from, to, match, effects) {
    let edge = new Edge(from, to, match, effects)
    from.outgoing.push(edge)
    to.incoming.push(edge)
    return edge
  }

  merge(a, b) {
    this.aliases[b.label] = a
    delete this.nodes[b.label]
  }

  withLabels(label, f) {
    let prevLabel = this.curLabel
    this.curLabel = label
    f()
    this.curLabel = prevLabel
  }
}

class Node {
  constructor(label) {
    this.label = label
    this.incoming = []
    this.outgoing = []
  }

  toString() { return this.label }
}

class Edge {
  constructor(from, to, match, effects) {
    this.from = from
    this.to = to
    this.match = match
    this.effects = effects || []
  }

  rm() {
    this.from.outgoing.splice(this.from.outgoing.indexOf(this), 1)
    this.to.incoming.splice(this.to.incoming.indexOf(this), 1)
  }

  toString() {
    let effects = this.effects.length ? " " + this.effects.join(" ") : ""
    return `${this.from} -> ${this.to}[label=${JSON.stringify(this.match.toString() + effects)}]`
  }
}

class Match {
  constructor(ast) { this.ast = ast }
}

class StringMatch extends Match {
  constructor(ast, string) {
    super(ast)
    this.string = string
  }

  toString() { return JSON.stringify(this.string) }
}

class RangeMatch extends Match {
  constructor(ast, from, to) {
    super(ast)
    this.from = from
    this.to = to
  }

  toString() { return JSON.stringify(this.from) + "-" + JSON.stringify(this.to) }
}

class AnyMatch extends Match {
  toString() { return "_" }
}

class NullMatch extends Match {
  toString() { return "ø" }
}

class SeqMatch extends Match {
  constructor(ast, matches) {
    super(ast)
    this.matches = matches
  }

  toString() { return this.matches.join(" ") }

  static create(left, right) {
    if (left instanceof NullMatch) return right
    if (right instanceof NullMatch) return left
    let matches = []
    if (left instanceof SeqMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    let last = matches[matches.length - 1]
    if (right instanceof StringMatch && last instanceof StringMatch)
      matches[matches.length - 1] = new StringMatch(last.ast, last.value + right.value)
    else if (right instanceof SeqMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    if (matches.length == 1) return matches[0]
    else return new SeqMatch(left.ast, matches)
  }
}

class ChoiceMatch extends Match {
  constructor(ast, matches) {
    super(ast)
    this.matches = matches
  }

  toString() { return "(" + this.matches.join(" | ") + ")" }

  static create(left, right) {
    let matches = []
    if (left instanceof ChoiceMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    if (right instanceof ChoiceMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    return new ChoiceMatch(left.ast, matches)
  }
}

class CallEffect {
  constructor(rule, returnTo) {
    this.rule = rule
    this.returnTo = returnTo
  }

  eq(other) {
    return other instanceof CallEffect && other.rule == this.rule && other.returnTo == this.returnTo
  }

  toString() { return "call " + this.rule.id.name }
}

class ReturnEffect {
  constructor() {}

  eq(other) { return other instanceof ReturnEffect }

  toString() { return "return" }
}

function fillRule(graph, rule) {
  graph.withLabels(rule.ast.id.name, () => {
    let start = graph.node("start"), end = graph.node("end")
    rule.start = start.label; rule.end = end.label
    fillExpr(start, end, rule.ast.expr, graph)
  })
}

function fillExpr(start, end, expr, graph) {
  let t = expr.type
  if (t == "CharacterRange") {
    graph.edge(start, end, new RangeMatch(expr, expr.from, expr.to))
  } else if (t == "StringMatch") {
    graph.edge(start, end, new StringMatch(expr, expr.value))
  } else if (t == "AnyMatch") {
    graph.edge(start, end, new AnyMatch(expr))
  } else if (t == "RuleIdentifier") {
    let rule = graph.rules[expr.id.name]
    if (!rule.start) fillRule(graph, rule)
    graph.edge(start, graph.aliases[rule.start], new NullMatch(expr), [new CallEffect(rule.ast, end)])
    graph.edge(graph.aliases[rule.end], end, new NullMatch(expr), [new ReturnEffect])
  } else if (t == "RepeatedMatch") {
    if (expr.kind == "*") {
      graph.edge(start, end, new NullMatch(expr))
      fillExpr(start, start, expr.expr, graph)
    } else if (expr.kind == "+") {
      fillExpr(start, end, expr.expr, graph)
      fillExpr(end, end, expr.expr, graph)
    } else if (expr.kind == "?") {
      graph.edge(start, end, new NullMatch(expr))
      fillExpr(start, end, expr.expr, graph)
    }
  } else if (t == "LookaheadMatch") {
    throw new Error("not supporting lookahead yet")
  } else if (t == "SequenceMatch") {
    for (let i = 0; i < expr.exprs.length; i++) {
      let to = i == expr.exprs.length - 1 ? end : graph.node()
      fillExpr(start, to, expr.exprs[i], graph)
      start = to
    }
  } else if (t == "ChoiceMatch") {
    for (let i = 0; i < expr.exprs.length; i++)
      fillExpr(start, end, expr.exprs[i], graph)
  } else {
    throw new Error("Unrecognized AST node type " + t)
  }
}

function mergeEffects(a, b) {
  for (let i = 0; i < b.length; i++) {
    if (b[i] instanceof ReturnEffect) for (let j = a.length - 1; j >= 0; j--) {
      if (a[j] instanceof CallEffect)
        return mergeEffects(a.slice(0, j).concat(a.slice(j + 1)),
                            b.slice(0, i).concat(b.slice(i + 1)))
    }
  }
  return a.concat(b)
}

function simplifySequence(graph, node) {
  if (node.incoming.length != 1 || node.outgoing.length != 1) return false
  let inEdge = node.incoming[0], outEdge = node.outgoing[0]
  // FIXME handle context effects
  if (inEdge.from == node || outEdge.to == node) return false
  if (inEdge.effects.some(e => e instanceof ReturnEffect)) return false
  inEdge.rm(); outEdge.rm()
  graph.merge(inEdge.from, node)
  let effects = mergeEffects(inEdge.effects, outEdge.effects)
  graph.edge(inEdge.from, outEdge.to, SeqMatch.create(inEdge.match, outEdge.match), effects)
  return true
}

function sameEffect(edge1, edge2) {
  let e1 = edge1.effects, e2 = edge2.effects
  if (e1.length != e2.length) return false
  for (let i = 0; i < e1.length; i++)
    if (!e1[i].eq(e2[i])) return false
  return true
}

function simplifyChoice(graph, node) {
  if (node.outgoing.length < 2) return false
  let first = node.outgoing[0]
  for (let i = 1; i < node.outgoing.length; i++) {
    let edge = node.outgoing[i]
    if (edge.to != first.to || !sameEffect(edge, first)) return false
  }
  let match = first.match
  first.rm()
  while (node.outgoing.length) {
    match = ChoiceMatch.create(match, node.outgoing[0].match)
    node.outgoing[0].rm()
  }
  graph.edge(node, first.to, match, first.effects)
  return true
}

function simplifyRepeat(graph, node) {
  // FIXME
}

// Look for simplification possibilities around the given node, return
// true if anything was done
function simplifyWith(graph, simplifiers) {
  let changed = false
  for (let n in graph.nodes) {
    let node = graph.nodes[n]
    for (let i = 0; i < simplifiers.length; i++) if (simplifiers[i](graph, node)) {
      changed = true
      break
    }
  }
  return changed
}

function simplify(graph) {
  while (simplifyWith(graph, [simplifySequence, simplifyChoice, simplifyRepeat])) {}
}

function printGraph(graph) {
  let output = "digraph {\n"
  for (let n in graph.nodes) {
    let node = graph.nodes[n]
    for (let i = 0; i < node.outgoing.length; i++)
      output += "  " + node.outgoing[i].toString() + ";\n"
  }
  return output + "}\n"
}

function compileGrammar(grammar, file) {
  let graph = new Graph(grammar)
  simplify(graph)
  console.log(printGraph(graph))
  return "FIXME"
}
<commit_msg>Simplify repeat operations<commit_after>module.exports = function(ast, file) {
  let result = file
  for (let i = ast.body.length - 1; i >= 0; i--) {
    let node = ast.body[i]
    if (node.type == "GrammarDeclaration")
      result = result.slice(0, node.start) + compileGrammar(node, file) + result.slice(node.end)
  }
  return result
}

class Graph {
  constructor(grammar) {
    this.nodes = Object.create(null)
    this.aliases = Object.create(this.nodes)
    this.curLabel = "-"
    this.rules = Object.create(null)
    this.grammar = grammar
    this.first = null
    for (let i = 0; i < grammar.rules.length; i++) {
      let rule = grammar.rules[i]
      this.rules[rule.id.name] = {ast: rule, start: null, end: null}
      if (this.first == null && !rule.lexical) this.first = rule.id.name
    }
    fillRule(this, this.rules[this.first])
  }

  node(suffix, label) {
    if (!label) label = this.curLabel + (suffix ? "_" + suffix : "")
    for (let i = 0;; i++) {
      let cur = i ? label + "_" + i : label
      if (!(cur in this.aliases)) return this.nodes[cur] = new Node(cur)
    }
  }

  edge(from, to, match, effects) {
    let edge = new Edge(from, to, match, effects)
    from.outgoing.push(edge)
    to.incoming.push(edge)
    return edge
  }

  merge(a, b) {
    this.aliases[b.label] = a
    delete this.nodes[b.label]
  }

  withLabels(label, f) {
    let prevLabel = this.curLabel
    this.curLabel = label
    f()
    this.curLabel = prevLabel
  }
}

class Node {
  constructor(label) {
    this.label = label
    this.incoming = []
    this.outgoing = []
  }

  toString() { return this.label }
}

class Edge {
  constructor(from, to, match, effects) {
    this.from = from
    this.to = to
    this.match = match
    this.effects = effects || []
  }

  rm() {
    this.from.outgoing.splice(this.from.outgoing.indexOf(this), 1)
    this.to.incoming.splice(this.to.incoming.indexOf(this), 1)
  }

  toString() {
    let effects = this.effects.length ? " " + this.effects.join(" ") : ""
    return `${this.from} -> ${this.to}[label=${JSON.stringify(this.match.toString() + effects)}]`
  }
}

class Match {
  constructor(ast) { this.ast = ast }
}

class StringMatch extends Match {
  constructor(ast, string) {
    super(ast)
    this.string = string
  }

  toString() { return JSON.stringify(this.string) }
}

class RangeMatch extends Match {
  constructor(ast, from, to) {
    super(ast)
    this.from = from
    this.to = to
  }

  toString() { return JSON.stringify(this.from) + "-" + JSON.stringify(this.to) }
}

class AnyMatch extends Match {
  toString() { return "_" }
}

class NullMatch extends Match {
  toString() { return "ø" }
}

class SeqMatch extends Match {
  constructor(ast, matches) {
    super(ast)
    this.matches = matches
  }

  toString() { return this.matches.join(" ") }

  static create(left, right) {
    if (left instanceof NullMatch) return right
    if (right instanceof NullMatch) return left
    let matches = []
    if (left instanceof SeqMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    let last = matches[matches.length - 1]
    if (right instanceof StringMatch && last instanceof StringMatch)
      matches[matches.length - 1] = new StringMatch(last.ast, last.value + right.value)
    else if (right instanceof SeqMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    if (matches.length == 1) return matches[0]
    else return new SeqMatch(left.ast, matches)
  }
}

class ChoiceMatch extends Match {
  constructor(ast, matches) {
    super(ast)
    this.matches = matches
  }

  toString() { return "(" + this.matches.join(" | ") + ")" }

  static create(left, right) {
    let matches = []
    if (left instanceof ChoiceMatch) matches = matches.concat(left.matches)
    else matches.push(left)
    if (right instanceof ChoiceMatch) matches = matches.concat(right.matches)
    else matches.push(right)
    return new ChoiceMatch(left.ast, matches)
  }
}

class RepeatMatch extends Match {
  constructor(ast, match) {
    super(ast)
    this.match = match
  }

  toString() { return this.match.toString() + "*" }
}

class CallEffect {
  constructor(rule, returnTo) {
    this.rule = rule
    this.returnTo = returnTo
  }

  eq(other) {
    return other instanceof CallEffect && other.rule == this.rule && other.returnTo == this.returnTo
  }

  toString() { return "call " + this.rule.id.name }
}

class ReturnEffect {
  constructor() {}

  eq(other) { return other instanceof ReturnEffect }

  toString() { return "return" }
}

function fillRule(graph, rule) {
  graph.withLabels(rule.ast.id.name, () => {
    let start = graph.node("start"), end = graph.node("end")
    rule.start = start.label; rule.end = end.label
    fillExpr(start, end, rule.ast.expr, graph)
  })
}

function fillExpr(start, end, expr, graph) {
  let t = expr.type
  if (t == "CharacterRange") {
    graph.edge(start, end, new RangeMatch(expr, expr.from, expr.to))
  } else if (t == "StringMatch") {
    graph.edge(start, end, new StringMatch(expr, expr.value))
  } else if (t == "AnyMatch") {
    graph.edge(start, end, new AnyMatch(expr))
  } else if (t == "RuleIdentifier") {
    let rule = graph.rules[expr.id.name]
    if (!rule.start) fillRule(graph, rule)
    graph.edge(start, graph.aliases[rule.start], new NullMatch(expr), [new CallEffect(rule.ast, end)])
    graph.edge(graph.aliases[rule.end], end, new NullMatch(expr), [new ReturnEffect])
  } else if (t == "RepeatedMatch") {
    if (expr.kind == "*") {
      graph.edge(start, end, new NullMatch(expr))
      fillExpr(start, start, expr.expr, graph)
    } else if (expr.kind == "+") {
      fillExpr(start, end, expr.expr, graph)
      fillExpr(end, end, expr.expr, graph)
    } else if (expr.kind == "?") {
      graph.edge(start, end, new NullMatch(expr))
      fillExpr(start, end, expr.expr, graph)
    }
  } else if (t == "LookaheadMatch") {
    throw new Error("not supporting lookahead yet")
  } else if (t == "SequenceMatch") {
    for (let i = 0; i < expr.exprs.length; i++) {
      let to = i == expr.exprs.length - 1 ? end : graph.node()
      fillExpr(start, to, expr.exprs[i], graph)
      start = to
    }
  } else if (t == "ChoiceMatch") {
    for (let i = 0; i < expr.exprs.length; i++)
      fillExpr(start, end, expr.exprs[i], graph)
  } else {
    throw new Error("Unrecognized AST node type " + t)
  }
}

function mergeEffects(a, b) {
  for (let i = 0; i < b.length; i++) {
    if (b[i] instanceof ReturnEffect) for (let j = a.length - 1; j >= 0; j--) {
      if (a[j] instanceof CallEffect)
        return mergeEffects(a.slice(0, j).concat(a.slice(j + 1)),
                            b.slice(0, i).concat(b.slice(i + 1)))
    }
  }
  return a.concat(b)
}

function simplifySequence(graph, node) {
  if (node.incoming.length != 1 || node.outgoing.length != 1) return false
  let inEdge = node.incoming[0], outEdge = node.outgoing[0]
  if (inEdge.from == node || outEdge.to == node || inEdge.effects.length || outEdge.effects.length)
    return false
  inEdge.rm(); outEdge.rm()
  graph.merge(inEdge.from, node)
  graph.edge(inEdge.from, outEdge.to, SeqMatch.create(inEdge.match, outEdge.match))
  return true
}

function sameEffect(edge1, edge2) {
  let e1 = edge1.effects, e2 = edge2.effects
  if (e1.length != e2.length) return false
  for (let i = 0; i < e1.length; i++)
    if (!e1[i].eq(e2[i])) return false
  return true
}

function simplifyChoice(graph, node) {
  if (node.outgoing.length < 2) return false
  let first = node.outgoing[0]
  for (let i = 1; i < node.outgoing.length; i++) {
    let edge = node.outgoing[i]
    if (edge.to != first.to || !sameEffect(edge, first)) return false
  }
  let match = first.match
  first.rm()
  while (node.outgoing.length) {
    match = ChoiceMatch.create(match, node.outgoing[0].match)
    node.outgoing[0].rm()
  }
  graph.edge(node, first.to, match, first.effects)
  return true
}

function simplifyRepeat(graph, node) {
  let cycleIndex, cycleEdge
  for (let i = 0; i < node.outgoing.length; i++) {
    let edge = node.outgoing[i]
    if (edge.to == node) {
      if (cycleEdge) return false
      cycleIndex = i
      cycleEdge = edge
    }
  }
  if (!cycleEdge) return false
  let newNode = graph.node(null, node.label + "_split")
  cycleEdge.rm()
  while (node.outgoing.length) {
    let edge = node.outgoing[0]
    edge.rm()
    graph.edge(newNode, edge.to, edge.match, edge.effects)
  }
  graph.edge(node, newNode, new RepeatMatch(cycleEdge.match.ast, cycleEdge.match), cycleEdge.effects)
  return true
}

// Look for simplification possibilities around the given node, return
// true if anything was done
function simplifyWith(graph, simplifiers) {
  let changed = false
  for (let n in graph.nodes) {
    let node = graph.nodes[n]
    for (let i = 0; i < simplifiers.length; i++) if (simplifiers[i](graph, node)) {
      changed = true
      break
    }
  }
  return changed
}

function simplify(graph) {
  while (simplifyWith(graph, [simplifySequence, simplifyChoice, simplifyRepeat])) {}
}

function printGraph(graph) {
  let output = "digraph {\n"
  for (let n in graph.nodes) {
    let node = graph.nodes[n]
    for (let i = 0; i < node.outgoing.length; i++)
      output += "  " + node.outgoing[i].toString() + ";\n"
  }
  return output + "}\n"
}

function compileGrammar(grammar, file) {
  let graph = new Graph(grammar)
  simplify(graph)
  console.log(printGraph(graph))
  return "FIXME"
}
<|endoftext|>


<commit_before>const logger = require('../../../logger');

const { FileModel } = require('../model');
const { userModel } = require('../../user/model');
const RoleModel = require('../../role/model');
const { sortRoles } = require('../../role/utils/rolesHelper');
const { submissionModel: Submission, homeworkModel: Homework } = require('../../homework/model');
const { equal: equalIds } = require('../../../helper/compare').ObjectId;

const getFile = (id) => FileModel
	.findOne({ _id: id })
	.populate('owner')
	.lean()
	.exec();

const checkTeamPermission = async ({ user, file, permission }) => {
	let teamRoles;
	let sortedTeamRoles;
	const roleIndex = {};

	try {
		teamRoles = await RoleModel.find({ name: /^team/ }).lean().exec();
		teamRoles.forEach((role) => { roleIndex[role._id] = role; });
		sortedTeamRoles = sortRoles(teamRoles);
	} catch (error) {
		logger.error(error);
		return Promise.reject();
	}

	return new Promise((resolve, reject) => {
		const { role } = user;
		const { permissions } = file;
		let rolePermissions;

		let rolesToTest = [role];
		while (rolesToTest.length > 0 && rolePermissions === undefined) {
			const roleId = rolesToTest.pop().toString();
			rolePermissions = permissions.find((perm) => equalIds(perm.refId, roleId));
			rolesToTest = rolesToTest.concat(roleIndex[roleId].roles || []);
		}

		const { role: creatorRole } = file.owner.userIds
			.find((_) => equalIds(_.userId, file.permissions[0].refId));

		const findRole = (roleId) => (roles) => roles
			.findIndex((r) => equalIds(r._id, roleId)) > -1;

		const userPos = sortedTeamRoles.findIndex(findRole(role));
		const creatorPos = sortedTeamRoles.findIndex(findRole(creatorRole));

		if (userPos > creatorPos || rolePermissions[permission]) {
			resolve(true);
		}
		reject();
	});
};

const checkMemberStatus = ({ file, user }) => {
	const { owner: { userIds, teacherIds, substitutionIds } } = file;
	const finder = (obj) => user.equals(obj.userId || obj);

	return [userIds, teacherIds, substitutionIds]
		.reduce((result, list) => result || (list && list.find(finder)), false);
};

const checkPermissions = (permission) => async (user, file) => {
	const fileObject = await getFile(file);
	if (fileObject === undefined || fileObject === null) {
		throw new Error('File does not exist.', { user, file, permission });
	}
	const {
		permissions,
		refOwnerModel,
		owner: { _id: owner },
	} = fileObject;

	// return always true for owner of file
	if (equalIds(user, owner)) {
		return Promise.resolve(true);
	}

	const userPermissions = permissions
		.find((perm) => perm.refId && perm.refId.toString() === user.toString());

	if (userPermissions && userPermissions[permission]) {
		return Promise.resolve(true);
	}

	const submission = await Submission.findOne({ fileIds: fileObject._id }).lean().exec();
	if (refOwnerModel === 'course' || submission) {
		const userObject = await userModel.findOne({ _id: user }).populate('roles').lean().exec();
		const isStudent = userObject.roles.find((role) => role.name === 'student');
		let courseFile = fileObject;
		let homework;
		if (submission) {
			homework = await Homework.findOne({ _id: submission.homeworkId }).populate('courseId').lean().exec();
			courseFile = { ...fileObject, owner: homework.courseId || {} };
		}
		const isMember = checkMemberStatus({ file: courseFile, user });
		if (isMember) {
			if (homework && homework.publicSubmissions) {
				return Promise.resolve(true);
			}
			if (isStudent) {
				const rolePermissions = permissions.find(
					(perm) => perm.refId && equalIds(perm.refId, isStudent._id),
				);
				return rolePermissions[permission] ? Promise.resolve(true) : Promise.reject();
			}
			return Promise.resolve(true);
		}
		return Promise.reject();
	}

	const homework = await Homework.findOne({ fileIds: fileObject._id }).lean().exec();
	if (homework && !homework.private) {
		// TODO: Check if user as access to homework
		// let courseFile = fileObject;
		// courseFile = { ...fileObject, owner: homework.courseId || {} };
		// const isMember = checkMemberStatus({ file: courseFile, user });
		// if isMember??

		return Promise.resolve(true);
	}

	const isMember = checkMemberStatus({ file: fileObject, user });

	// User is no member of team or course
	// and file has no explicit user permissions (sharednetz files)
	if (!isMember && !userPermissions) {
		return Promise.reject();
	}

	return checkTeamPermission({
		permission,
		file: fileObject,
		user: isMember,
	});
};

module.exports = {
	checkPermissions,
	canWrite: checkPermissions('write'),
	canRead: checkPermissions('read'),
	canCreate: checkPermissions('create'),
	canDelete: checkPermissions('delete'),
};
<commit_msg>added check<commit_after>const logger = require('../../../logger');

const { FileModel } = require('../model');
const { userModel } = require('../../user/model');
const RoleModel = require('../../role/model');
const { sortRoles } = require('../../role/utils/rolesHelper');
const { submissionModel: Submission, homeworkModel: Homework } = require('../../homework/model');
const { equal: equalIds } = require('../../../helper/compare').ObjectId;

const getFile = (id) => FileModel
	.findOne({ _id: id })
	.populate('owner')
	.lean()
	.exec();

const checkTeamPermission = async ({ user, file, permission }) => {
	let teamRoles;
	let sortedTeamRoles;
	const roleIndex = {};

	try {
		teamRoles = await RoleModel.find({ name: /^team/ }).lean().exec();
		teamRoles.forEach((role) => { roleIndex[role._id] = role; });
		sortedTeamRoles = sortRoles(teamRoles);
	} catch (error) {
		logger.error(error);
		return Promise.reject();
	}

	return new Promise((resolve, reject) => {
		const { role } = user;
		const { permissions } = file;
		let rolePermissions;

		let rolesToTest = [role];
		while (rolesToTest.length > 0 && rolePermissions === undefined) {
			const roleId = rolesToTest.pop().toString();
			rolePermissions = permissions.find((perm) => equalIds(perm.refId, roleId));
			rolesToTest = rolesToTest.concat(roleIndex[roleId].roles || []);
		}

		const { role: creatorRole } = file.owner.userIds
			.find((_) => equalIds(_.userId, file.permissions[0].refId));

		const findRole = (roleId) => (roles) => roles
			.findIndex((r) => equalIds(r._id, roleId)) > -1;

		const userPos = sortedTeamRoles.findIndex(findRole(role));
		const creatorPos = sortedTeamRoles.findIndex(findRole(creatorRole));

		if (userPos > creatorPos || rolePermissions[permission]) {
			resolve(true);
		}
		reject();
	});
};

const checkMemberStatus = ({ file, user }) => {
	const { owner: { userIds, teacherIds, substitutionIds } } = file;
	const finder = (obj) => user.equals(obj.userId || obj);

	return [userIds, teacherIds, substitutionIds]
		.reduce((result, list) => result || (list && list.find(finder)), false);
};

const checkPermissions = (permission) => async (user, file) => {
	const fileObject = await getFile(file);
	if (fileObject === undefined || fileObject === null) {
		throw new Error('File does not exist.', { user, file, permission });
	}
	const {
		permissions,
		refOwnerModel,
		owner: { _id: owner },
	} = fileObject;

	// return always true for owner of file
	if (equalIds(user, owner)) {
		return Promise.resolve(true);
	}

	const userPermissions = permissions
		.find((perm) => perm.refId && perm.refId.toString() === user.toString());

	if (userPermissions && userPermissions[permission]) {
		return Promise.resolve(true);
	}

	const submission = await Submission.findOne({ fileIds: fileObject._id }).lean().exec();
	if (refOwnerModel === 'course' || submission) {
		const userObject = await userModel.findOne({ _id: user }).populate('roles').lean().exec();
		const isStudent = userObject.roles.find((role) => role.name === 'student');
		let courseFile = fileObject;
		let homework;
		if (submission) {
			homework = await Homework.findOne({ _id: submission.homeworkId }).populate('courseId').lean().exec();
			courseFile = { ...fileObject, owner: homework.courseId || {} };
		}
		const isMember = checkMemberStatus({ file: courseFile, user });
		if (isMember) {
			if (homework && homework.publicSubmissions) {
				return Promise.resolve(true);
			}
			if (isStudent) {
				const rolePermissions = permissions.find(
					(perm) => perm.refId && equalIds(perm.refId, isStudent._id),
				);
				return rolePermissions[permission] ? Promise.resolve(true) : Promise.reject();
			}
			return Promise.resolve(true);
		}
		return Promise.reject();
	}

	const homework = await Homework.findOne({ fileIds: fileObject._id }).populate('courseId').lean().exec();
	if (homework && !homework.private) {
		// let courseFile = fileObject;
		let courseFile = fileObject;
		courseFile = { ...fileObject, owner: homework.courseId || {} };
		const isMember = checkMemberStatus({ file: courseFile, user });
		if (isMember) return Promise.resolve(true);

		return Promise.resolve(false);
	}

	const isMember = checkMemberStatus({ file: fileObject, user });

	// User is no member of team or course
	// and file has no explicit user permissions (sharednetz files)
	if (!isMember && !userPermissions) {
		return Promise.reject();
	}

	return checkTeamPermission({
		permission,
		file: fileObject,
		user: isMember,
	});
};

module.exports = {
	checkPermissions,
	canWrite: checkPermissions('write'),
	canRead: checkPermissions('read'),
	canCreate: checkPermissions('create'),
	canDelete: checkPermissions('delete'),
};
<|endoftext|>


<commit_before>/*
 * Class representing the Capability only
 * Prepare the editor, save, update
 */

var Capability = new Class({
	Implements: [Options, Events],
	type: 'capability',
	options: {
		version: {},
		//slug: null,
		//name: null,
		//description: null,
		//creator: null,
		//managers: [],
		//developers: [],
		//public_permission: 2,
		//group_permission: 2,
		description_el: {element: 'capability_description'},
		//switch_description_id: '',
		update_el: 'update',
		version_create_el: 'version_create',
		try_in_browser_el: 'try_in_browser',
		//edit_url: '',
		//update_url: '',
		//version_create_url: '',
		is_dependency: false // was the Capability loaded as dependency?
	},
	/*
	 * Method: initialize
	 * @attribute object options:
	 *
	 * initialize Version and inside of that chosen Editor
	 * assign actions to the buttons
	 */
	initialize: function(options) {
		this.setOptions(options)
		this.initializeVersion();
		this.instantiateEditors();

		this.listenToEvents();
		if (!this.options.is_dependency) {
			// TODO: this should probably be moved to the UI.Editor.js or however this file will be called
			this.initializeEditorSwitches();
		}

		this.data = {};
		if (!this.options.is_dependency) {
			this.data[this.type+'_name'] = this.options.name;
			this.data[this.type+'_description'] = this.options.description;
		}
		this.data[this.type+'_slug'] = this.options.slug;
		// #TODO: remove these - it's just to switch the buttons all the time
		// this.afterVersionChanged();
		// this.afterDataChanged();
	},
	/*
	 * Method: instantiateEditors
	 */
	instantiateEditors: function() {
		// do not create an editor for the description if loaded as dependency
		if (! this.options.is_dependency) {
			this.description_el = new Editor(this.options.description_el).hide();
			fd.editors.push(this.description_el);
		}
	},
	/*
	 * Method: initializeEditorSwitches
	 */
	initializeEditorSwitches: function() {
		// TODO: this should be done with the event broadcast
		// 		 to not bother about new dependencies added
		$$('.UI_File_Listing li').each(function(file_el) {
			file_el.switch_mode_on = function() {
				$$('.UI_File_Selected').each(function(el) {
					el.switch_mode_off();
				});
				this.removeClass('UI_File_Normal')
					.addClass('UI_File_Selected')
			};
			file_el.switch_mode_off = function() {
				this.removeClass('UI_File_Selected')
					.addClass('UI_File_Normal')
			}
		});
		$$('.UI_File_Listing li a').addEvent('click', function() {
			this.getParent('li').switch_mode_on();
		});
		// "there is no spoon" if dependency
		if (!this.options.dependency_id) {
			this.switch_description_el = $(this.options.switch_description_id);
			if (this.switch_description_el) {
				this.switch_description_el.addEvent('click', this.switchToDescription.bind(this));
			}
		}
	},
	/*
	 * Method: switchToDescription
	 */
	switchToDescription: function() {
		fd.hideEditors();
		this.description_el.show();
	},
	createAfterBounds: function() {
		this.boundAfterDataChanged = this.afterDataChanged.bind(this);
		this.boundAfterVersionChanged = this.afterVersionChanged.bind(this);
	},
	/*
	 * Method: listenToEvents
	 */
	listenToEvents: function() {
		this.createAfterBounds();
		this.version.addEvent('change', this.boundAfterVersionChanged);
		this.description_el.addEvent('change', this.boundAfterDataChanged);
		// one may try even not edited data
		this.try_in_browser_el = $(this.options.try_in_browser_el)
		if (this.try_in_browser_el) {
			this.try_in_browser_el.addEvent('click', function(e) {
				e.stop();
				this.try_in_browser();
			}.bind(this));
		}
	},
	/*
	 * Method: afterVersionChanged
	 * assign version_create with the $('version_create') click event
	 */
	afterDataChanged: function() {
		$(this.options.update_el).addEvent('click', function(e) {
			e.stop();
			this.update();
		}.bind(this));
		this.description_el.removeEvent('change', this.boundAfterDataChanged);
	},
	/*
	 * Method: afterVersionChanged
	 * assign version_create with the $('version_create') click event
	 */
	afterVersionChanged: function() {
		// TODO: discover if change was actually an undo and there is
		//       no change to the original content
		$(this.options.version_create_el).addEvent('click', function(e) {
			e.stop();
			this.version_create();
		}.bind(this));
		this.version.removeEvent('change', this.boundAfterVersionChanged);
	},
	/*
	 * Method: initializeVersion
	 * assigns CapVersion to this.version
	 */
	initializeVersion: function() {
		this.version = new CapVersion(this.options.version);
	},
	/*
	 * Method: update
	 * Prepare data and send Request to the back-end
	 */
	update: function() {
		var data = this.prepareData();
		new Request.JSON({
			url: this.options.update_url,
			data: data,
			method: 'post',
			onSuccess: function(response) {
				// display notification from response
				fd.message.alert('Success', response.message);
			}
		}).send();
	},
	/*
	 * Method: version_create
	 * Prepare data and send Request - create a new version
	 */
	version_create: function(data) {
		var data = $pick(data, this.version.prepareData());
		new Request.JSON({
			url: this.options.version_create_url,
			data: data,
			method: 'post',
			onSuccess: this.afterVersionCreated.bind(this)
		}).send();
	},
	afterVersionCreated: function(response) {
		window.location.href = response.version_absolute_url;
	},
	/*
	 * Method: try_in_browser
	 * Prepare Capability using saved content and install temporary in the browser
	 */
	try_in_browser: function() {
		var data = this.getFullData();
		fd.warning.alert('Not implemented','try_in_browser');
		console.log(data);
	},
	/*
	 * Method: getContent
	 * Wrapper for getting content from the Editor
	 */
	getContent: function() {
		return this.version.getContent();
	},
	/*
	 * Method: getVersionName
	 * Wrapper for getting Version name from options
	 */
	getVersionName: function() {
		return this.version.getName();
	},
	/*
	 * Method: prepareData
	 * Take all jetpack available data and return
	 */
	prepareData: function() {
		this.updateFromDOM();
		return this.data;
	},
	/*
	 * Method: updateFromDOM
	 * get all jetpack editable fields from DOM and set parameters in model
	 */
	updateFromDOM: function() {
		// here update metadata from its editors
		this.data[this.type+'_description'] = this.description_el.getContent();
	},
	/*
	 * Method: getFullData
	 * get all data to save Jetpack and Version models
	 */
	getFullData: function() {
		var data = $H(this.prepareData());
		data.extend(this.version.prepareData());
		return data.getClean();
	}

});

/*
 * Class representing the Version only
 * Prepare the editor, save, update
 */
var CapVersion = new Class({
	Implements: [Options, Events],
	type: 'capability',
	options: {
		//commited_by: null,
		//name: null,
		//description: null,
		//content: null,
		//status: null,
		//is_base: null,
		name_el: 'version_name',
		// TODO: move to new Editor
		description_el: {element: 'version_description'},
		content_el: {element: 'version_content'},
		update_el: 'update',
		set_as_base_el: 'set_as_base',
		edit_url: '',
		update_url: '',
		set_as_base_url: '',
		is_dependency: false
	},
	/*
	 * Method: initialize
	 * instantiate Editor
	 */
	initialize: function(options) {
		this.setOptions(options);
		this.instantiateEditors();
		this.listenToEvents();
		this.initializeEditorSwitches();

		// this.data is everything we send to the backend
		this.data = $H({
			version_content: this.options.content,
			version_name: this.options.name,
			version_description: this.options.description,
		});
	},
	/*
	 * Method: instantiateEditors
	 */
	instantiateEditors: function() {
		this.content_el = new Editor(this.options.content_el);
		fd.editors.push(this.content_el);
		this.name_el = $(this.options.name_el);
		this.description_el = new Editor(this.options.description_el).hide();
		fd.editors.push(this.description_el);
	},
	/*
	 * Method: initializeEditorSwitches
	 */
	initializeEditorSwitches: function() {
		this.switch_content_el = $(this.options.switch_content_id);
		this.switch_description_el = $(this.options.switch_description_id);
		if (this.switch_content_el) {
			this.switch_content_el.addEvent('click', this.switchToContent.bind(this));
		}
		if (this.switch_description_el) {
			this.switch_description_el.addEvent('click', this.switchToDescription.bind(this));
		}
	},
	/*
	 * Method: switchToContent
	 */
	switchToContent: function(e) {
		e.stop();
		fd.hideEditors();
		this.content_el.show();
	},
	/*
	 * Method: switchToDescription
	 */
	switchToDescription: function(e) {
		e.stop();
		fd.hideEditors();
		this.description_el.show();
	},
	/*
	 * Method: listenToCapabilityEvents
	 */
	listenToEvents: function() {
		this.changed = false;
		this.boundAfterDataChanged = this.afterDataChanged.bind(this);
		this.description_el.addEvent('change', this.boundAfterDataChanged);
		this.content_el.addEvent('change', this.boundAfterDataChanged);
	},
	afterDataChanged: function() {
		// TODO: discover if change was actually an undo and there is
		//       no change to the original content
		this.changed = true;
		$(this.options.update_el).addEvent('click', function(e) {
			e.stop();
			this.update();
		}.bind(this));
		this.content_el.removeEvent('change', this.boundAfterDataChanged);
		this.description_el.removeEvent('change', this.boundAfterDataChanged);
		this.set_as_base_el = $(this.options.set_as_base_el);
		this.set_as_base_el.addEvent('click', function(e) {
			e.stop();
			this.setAsBase();
		}.bind(this));
		this.fireEvent('change');
	},
	/*
	 * Method: setAsBase
	 * set current version as Base
	 */
	setAsBase: function() {
		new Request.JSON({
			url: this.options.set_as_base_url,
			method: 'post',
			onSuccess: function(response) {
				fd.message.alert('Success', response.message);
				this.set_as_base_el.set('text', 'Base version')
			}.bind(this)
		}).send()
	},
	/*
	 * Method: update
	 * get current data and send Request to the backend
	 */
	update: function() {
		var data = this.prepareData();
		// prevent from updating a version with different name
		if (data.version_name && data.version_name != this.options.name) {
			return window[this.type].version_create(data);
		}
		new Request.JSON({
			url: this.options.update_url,
			data: data,
			method: 'post',
			onSuccess: function(response) {
				fd.message.alert('Success', response.message);
			}
		}).send();
	},
	/*
	 * Method: getContent
	 * Wrapper for getting content from the Editor
	 */
	getContent: function() {
		return this.content_el.getContent();
	},
	/*
	 * Method: getName
	 * Wrapper for getting Version name from options
	 */
	getName: function() {
		return this.options.name
	},
	/*
	 * Method: prepareData
	 * Prepare all version specific available data
	 */
	prepareData: function() {
		this.updateFromDOM();
		return this.data;
	},
	/*
	 * Method: updateFromDOM
	 * get all version editable fields from DOM and set parameters in model
	 */
	updateFromDOM: function() {
		this.data.version_name = this.name_el.get('value');
		this.data.version_description = this.description_el.getContent();
		this.data.version_content = this.content_el.getContent();
	}
});

<commit_msg>better function usage in switchers<commit_after>/*
 * Class representing the Capability only
 * Prepare the editor, save, update
 */

var Capability = new Class({
	Implements: [Options, Events],
	type: 'capability',
	options: {
		version: {},
		//slug: null,
		//name: null,
		//description: null,
		//creator: null,
		//managers: [],
		//developers: [],
		//public_permission: 2,
		//group_permission: 2,
		description_el: {element: 'capability_description'},
		//switch_description_id: '',
		update_el: 'update',
		version_create_el: 'version_create',
		try_in_browser_el: 'try_in_browser',
		//edit_url: '',
		//update_url: '',
		//version_create_url: '',
		is_dependency: false // was the Capability loaded as dependency?
	},
	/*
	 * Method: initialize
	 * @attribute object options:
	 *
	 * initialize Version and inside of that chosen Editor
	 * assign actions to the buttons
	 */
	initialize: function(options) {
		this.setOptions(options)
		this.initializeVersion();
		this.instantiateEditors();

		this.listenToEvents();
		if (!this.options.is_dependency) {
			// TODO: this should probably be moved to the UI.Editor.js or however this file will be called
			this.initializeEditorSwitches();
		}

		this.data = {};
		if (!this.options.is_dependency) {
			this.data[this.type+'_name'] = this.options.name;
			this.data[this.type+'_description'] = this.options.description;
		}
		this.data[this.type+'_slug'] = this.options.slug;
		// #TODO: remove these - it's just to switch the buttons all the time
		// this.afterVersionChanged();
		// this.afterDataChanged();
	},
	/*
	 * Method: instantiateEditors
	 */
	instantiateEditors: function() {
		// do not create an editor for the description if loaded as dependency
		if (! this.options.is_dependency) {
			this.description_el = new Editor(this.options.description_el).hide();
			fd.editors.push(this.description_el);
		}
	},
	/*
	 * Method: initializeEditorSwitches
	 */
	initializeEditorSwitches: function() {
		// TODO: this should be done with the event broadcast
		// 		 to not bother about new dependencies added
		var switch_mode_on  = function() {
			$$('.UI_File_Selected').each(function(el) {
				el.switch_mode_off();
			});
			this.removeClass('UI_File_Normal')
				.addClass('UI_File_Selected')
		};
		var switch_mode_off = function() {
			this.removeClass('UI_File_Selected')
				.addClass('UI_File_Normal')
		}
		$$('.UI_File_Listing li').each(function(file_el) {
			file_el.switch_mode_on = switch_mode_on;
			file_el.switch_mode_off = switch_mode_off;
		});
		$$('.UI_File_Listing li a').addEvent('click', function() {
			this.getParent('li').switch_mode_on();
		});

		this.switch_description_el = $(this.options.switch_description_id);
		if (this.switch_description_el) {
			this.switch_description_el.addEvent('click', this.switchToDescription.bind(this));
		}
	},
	/*
	 * Method: switchToDescription
	 */
	switchToDescription: function() {
		fd.hideEditors();
		this.description_el.show();
	},
	createAfterBounds: function() {
		this.boundAfterDataChanged = this.afterDataChanged.bind(this);
		this.boundAfterVersionChanged = this.afterVersionChanged.bind(this);
	},
	/*
	 * Method: listenToEvents
	 */
	listenToEvents: function() {
		this.createAfterBounds();
		this.version.addEvent('change', this.boundAfterVersionChanged);
		this.description_el.addEvent('change', this.boundAfterDataChanged);
		// one may try even not edited data
		this.try_in_browser_el = $(this.options.try_in_browser_el)
		if (this.try_in_browser_el) {
			this.try_in_browser_el.addEvent('click', function(e) {
				e.stop();
				this.try_in_browser();
			}.bind(this));
		}
	},
	/*
	 * Method: afterVersionChanged
	 * assign version_create with the $('version_create') click event
	 */
	afterDataChanged: function() {
		$(this.options.update_el).addEvent('click', function(e) {
			e.stop();
			this.update();
		}.bind(this));
		this.description_el.removeEvent('change', this.boundAfterDataChanged);
	},
	/*
	 * Method: afterVersionChanged
	 * assign version_create with the $('version_create') click event
	 */
	afterVersionChanged: function() {
		// TODO: discover if change was actually an undo and there is
		//       no change to the original content
		$(this.options.version_create_el).addEvent('click', function(e) {
			e.stop();
			this.version_create();
		}.bind(this));
		this.version.removeEvent('change', this.boundAfterVersionChanged);
	},
	/*
	 * Method: initializeVersion
	 * assigns CapVersion to this.version
	 */
	initializeVersion: function() {
		this.version = new CapVersion(this.options.version);
	},
	/*
	 * Method: update
	 * Prepare data and send Request to the back-end
	 */
	update: function() {
		var data = this.prepareData();
		new Request.JSON({
			url: this.options.update_url,
			data: data,
			method: 'post',
			onSuccess: function(response) {
				// display notification from response
				fd.message.alert('Success', response.message);
			}
		}).send();
	},
	/*
	 * Method: version_create
	 * Prepare data and send Request - create a new version
	 */
	version_create: function(data) {
		var data = $pick(data, this.version.prepareData());
		new Request.JSON({
			url: this.options.version_create_url,
			data: data,
			method: 'post',
			onSuccess: this.afterVersionCreated.bind(this)
		}).send();
	},
	afterVersionCreated: function(response) {
		window.location.href = response.version_absolute_url;
	},
	/*
	 * Method: try_in_browser
	 * Prepare Capability using saved content and install temporary in the browser
	 */
	try_in_browser: function() {
		var data = this.getFullData();
		fd.warning.alert('Not implemented','try_in_browser');
		console.log(data);
	},
	/*
	 * Method: getContent
	 * Wrapper for getting content from the Editor
	 */
	getContent: function() {
		return this.version.getContent();
	},
	/*
	 * Method: getVersionName
	 * Wrapper for getting Version name from options
	 */
	getVersionName: function() {
		return this.version.getName();
	},
	/*
	 * Method: prepareData
	 * Take all jetpack available data and return
	 */
	prepareData: function() {
		this.updateFromDOM();
		return this.data;
	},
	/*
	 * Method: updateFromDOM
	 * get all jetpack editable fields from DOM and set parameters in model
	 */
	updateFromDOM: function() {
		// here update metadata from its editors
		this.data[this.type+'_description'] = this.description_el.getContent();
	},
	/*
	 * Method: getFullData
	 * get all data to save Jetpack and Version models
	 */
	getFullData: function() {
		var data = $H(this.prepareData());
		data.extend(this.version.prepareData());
		return data.getClean();
	}

});

/*
 * Class representing the Version only
 * Prepare the editor, save, update
 */
var CapVersion = new Class({
	Implements: [Options, Events],
	type: 'capability',
	options: {
		//commited_by: null,
		//name: null,
		//description: null,
		//content: null,
		//status: null,
		//is_base: null,
		name_el: 'version_name',
		// TODO: move to new Editor
		description_el: {element: 'version_description'},
		content_el: {element: 'version_content'},
		update_el: 'update',
		set_as_base_el: 'set_as_base',
		edit_url: '',
		update_url: '',
		set_as_base_url: '',
		is_dependency: false
	},
	/*
	 * Method: initialize
	 * instantiate Editor
	 */
	initialize: function(options) {
		this.setOptions(options);
		this.instantiateEditors();
		this.listenToEvents();
		this.initializeEditorSwitches();

		// this.data is everything we send to the backend
		this.data = $H({
			version_content: this.options.content,
			version_name: this.options.name,
			version_description: this.options.description,
		});
	},
	/*
	 * Method: instantiateEditors
	 */
	instantiateEditors: function() {
		this.content_el = new Editor(this.options.content_el);
		fd.editors.push(this.content_el);
		this.name_el = $(this.options.name_el);
		this.description_el = new Editor(this.options.description_el).hide();
		fd.editors.push(this.description_el);
	},
	/*
	 * Method: initializeEditorSwitches
	 */
	initializeEditorSwitches: function() {
		this.switch_content_el = $(this.options.switch_content_id);
		this.switch_description_el = $(this.options.switch_description_id);
		if (this.switch_content_el) {
			this.switch_content_el.addEvent('click', this.switchToContent.bind(this));
		}
		if (this.switch_description_el) {
			this.switch_description_el.addEvent('click', this.switchToDescription.bind(this));
		}
	},
	/*
	 * Method: switchToContent
	 */
	switchToContent: function(e) {
		e.stop();
		fd.hideEditors();
		this.content_el.show();
	},
	/*
	 * Method: switchToDescription
	 */
	switchToDescription: function(e) {
		e.stop();
		fd.hideEditors();
		this.description_el.show();
	},
	/*
	 * Method: listenToCapabilityEvents
	 */
	listenToEvents: function() {
		this.changed = false;
		this.boundAfterDataChanged = this.afterDataChanged.bind(this);
		this.description_el.addEvent('change', this.boundAfterDataChanged);
		this.content_el.addEvent('change', this.boundAfterDataChanged);
	},
	afterDataChanged: function() {
		// TODO: discover if change was actually an undo and there is
		//       no change to the original content
		this.changed = true;
		$(this.options.update_el).addEvent('click', function(e) {
			e.stop();
			this.update();
		}.bind(this));
		this.content_el.removeEvent('change', this.boundAfterDataChanged);
		this.description_el.removeEvent('change', this.boundAfterDataChanged);
		this.set_as_base_el = $(this.options.set_as_base_el);
		this.set_as_base_el.addEvent('click', function(e) {
			e.stop();
			this.setAsBase();
		}.bind(this));
		this.fireEvent('change');
	},
	/*
	 * Method: setAsBase
	 * set current version as Base
	 */
	setAsBase: function() {
		new Request.JSON({
			url: this.options.set_as_base_url,
			method: 'post',
			onSuccess: function(response) {
				fd.message.alert('Success', response.message);
				this.set_as_base_el.set('text', 'Base version')
			}.bind(this)
		}).send()
	},
	/*
	 * Method: update
	 * get current data and send Request to the backend
	 */
	update: function() {
		var data = this.prepareData();
		// prevent from updating a version with different name
		if (data.version_name && data.version_name != this.options.name) {
			return window[this.type].version_create(data);
		}
		new Request.JSON({
			url: this.options.update_url,
			data: data,
			method: 'post',
			onSuccess: function(response) {
				fd.message.alert('Success', response.message);
			}
		}).send();
	},
	/*
	 * Method: getContent
	 * Wrapper for getting content from the Editor
	 */
	getContent: function() {
		return this.content_el.getContent();
	},
	/*
	 * Method: getName
	 * Wrapper for getting Version name from options
	 */
	getName: function() {
		return this.options.name
	},
	/*
	 * Method: prepareData
	 * Prepare all version specific available data
	 */
	prepareData: function() {
		this.updateFromDOM();
		return this.data;
	},
	/*
	 * Method: updateFromDOM
	 * get all version editable fields from DOM and set parameters in model
	 */
	updateFromDOM: function() {
		this.data.version_name = this.name_el.get('value');
		this.data.version_description = this.description_el.getContent();
		this.data.version_content = this.content_el.getContent();
	}
});

<|endoftext|>