Skip to content

Instantly share code, notes, and snippets.

@kanaka
Created December 8, 2012 18:40
Show Gist options
  • Select an option

  • Save kanaka/4241273 to your computer and use it in GitHub Desktop.

Select an option

Save kanaka/4241273 to your computer and use it in GitHub Desktop.
--- /dev/null 2012-11-30 09:18:52.404117001 -0600
+++ src/cljs/cljs/analyzer_macros.clj 2012-12-08 11:13:03.441968192 -0600
@@ -0,0 +1,4 @@
+(ns cljs.analyzer-macros)
+
+(defmacro disallowing-recur [& body]
+ `(binding [cljs.analyzer/*recur-frames* (cons nil cljs.analyzer/*recur-frames*)] ~@body))
--- /dev/null 2012-11-30 09:18:52.404117001 -0600
+++ src/cljs/cljs/compiler_macros.clj 2012-11-17 13:40:55.000000000 -0600
@@ -0,0 +1,8 @@
+(ns cljs.compiler-macros)
+
+(defmacro emit-wrap [env & body]
+ `(let [env# ~env]
+ (when (= :return (:context env#)) (cljs.compiler/emits "return "))
+ ~@body
+ (when-not (= :expr (:context env#)) (cljs.compiler/emitln ";"))))
+
--- src/clj/cljs/analyzer.clj 2012-12-08 11:13:03.441968192 -0600
+++ src/cljs/cljs/analyzer.cljs 2012-12-08 11:39:19.068577065 -0600
@@ -6,14 +6,26 @@
; the terms of this license.
; You must not remove this notice, or any other, from this software.
-(set! *warn-on-reflection* true)
+;; (set! *warn-on-reflection* true)
(ns cljs.analyzer
(:refer-clojure :exclude [macroexpand-1])
- (:require [clojure.java.io :as io]
+ (:require ;; [clojure.java.io :as io]
[clojure.string :as string]
- [cljs.tagged-literals :as tags])
- (:import java.lang.StringBuilder))
+ ;;[cljs.tagged-literals :as tags]
+ )
+ (:use-macros [cljs.analyzer-macros :only [disallowing-recur]])
+ ;;(:import java.lang.StringBuilder)
+ )
+
+;; Stubs just to make it work
+(declare ^:dynamic *out*)
+(declare ^:dynamic *ns*)
+
+;;(defn create-ns [ns-sym] nil)
+(defn create-ns [ns-sym] #_(.log js/console ns-sym)
+ (js/eval (str "try { " ns-sym "; } catch (e) { " ns-sym " = {}; }")))
+
(declare resolve-var)
(declare resolve-existing-var)
@@ -28,8 +40,12 @@
(def ^:dynamic *reader-ns-name* (gensym))
(def ^:dynamic *reader-ns* (create-ns *reader-ns-name*))
-(defonce namespaces (atom '{cljs.core {:name cljs.core}
- cljs.user {:name cljs.user}}))
+;; (defonce namespaces (atom '{cljs.core {:name cljs.core}
+;; cljs.user {:name cljs.user}}))
+;; (def namespaces (atom '{cljs.core {:name cljs.core}
+;; cljs.user {:name cljs.user}}))
+;; "refer" it from somehwere that it will be from the start
+(set! cljs.analyzer/namespaces cljs.core/namespaces)
(defn reset-namespaces! []
(reset! namespaces
@@ -63,29 +79,29 @@
(load *cljs-macros-path*)
(load-file *cljs-macros-path*))))
-(defmacro with-core-macros
- [path & body]
- `(do
- (when (not= *cljs-macros-path* ~path)
- (reset! -cljs-macros-loaded false))
- (binding [*cljs-macros-path* ~path]
- ~@body)))
-
-(defmacro with-core-macros-file
- [path & body]
- `(do
- (when (not= *cljs-macros-path* ~path)
- (reset! -cljs-macros-loaded false))
- (binding [*cljs-macros-path* ~path
- *cljs-macros-is-classpath* false]
- ~@body)))
+;;(defmacro with-core-macros
+;; [path & body]
+;; `(do
+;; (when (not= *cljs-macros-path* ~path)
+;; (reset! -cljs-macros-loaded false))
+;; (binding [*cljs-macros-path* ~path]
+;; ~@body)))
+;;
+;;(defmacro with-core-macros-file
+;; [path & body]
+;; `(do
+;; (when (not= *cljs-macros-path* ~path)
+;; (reset! -cljs-macros-loaded false))
+;; (binding [*cljs-macros-path* ~path
+;; *cljs-macros-is-classpath* false]
+;; ~@body)))
(defn empty-env []
{:ns (@namespaces *cljs-ns*) :context :statement :locals {}})
-(defmacro ^:private debug-prn
- [& args]
- `(.println System/err (str ~@args)))
+;;(defmacro ^:private debug-prn
+;; [& args]
+;; `(.println System/err (str ~@args)))
(defn warning [env s]
(binding [*out* *err*]
@@ -128,7 +144,7 @@
{:name (symbol (str full-ns) (str (name sym)))
:ns full-ns}))
- (.contains s ".")
+ (>= (.indexOf s ".") 0)
(let [idx (.indexOf s ".")
prefix (symbol (subs s 0 idx))
suffix (subs s (inc idx))
@@ -173,7 +189,7 @@
ns (if (= "clojure.core" ns) "cljs.core" ns)]
{:name (symbol (str (resolve-ns-alias env ns)) (name sym))})
- (.contains s ".")
+ (>= (.indexOf s ".") 0)
(let [idx (.indexOf s ".")
prefix (symbol (subs s 0 idx))
suffix (subs s idx)
@@ -213,8 +229,8 @@
(def ^:dynamic *recur-frames* nil)
(def ^:dynamic *loop-lets* nil)
-(defmacro disallowing-recur [& body]
- `(binding [*recur-frames* (cons nil *recur-frames*)] ~@body))
+;;(defmacro disallowing-recur [& body]
+;; `(binding [*recur-frames* (cons nil *recur-frames*)] ~@body))
(defn analyze-keyword
[env sym]
@@ -472,7 +488,7 @@
bindings (seq (partition 2 bindings))]
(if-let [[name init] (first bindings)]
(do
- (assert (not (or (namespace name) (.contains (str name) "."))) (str "Invalid local name: " name))
+ (assert (not (or (namespace name) (>= (.indexOf (str name) ".") 0))) (str "Invalid local name: " name))
(let [init-expr (analyze env init)
be {:name name
:init init-expr
@@ -589,12 +605,12 @@
(declare analyze-file)
-(defn analyze-deps [deps]
- (doseq [dep deps]
- (when-not (:defs (@namespaces dep))
- (let [relpath (ns->relpath dep)]
- (when (io/resource relpath)
- (analyze-file relpath))))))
+;; (defn analyze-deps [deps]
+;; (doseq [dep deps]
+;; (when-not (:defs (@namespaces dep))
+;; (let [relpath (ns->relpath dep)]
+;; (when (io/resource relpath)
+;; (analyze-file relpath))))))
(defmethod parse 'ns
[_ env [_ name & args :as form] _]
@@ -666,7 +682,9 @@
(apply merge-with merge m (map (spec-parsers k) libs)))
{} (remove (fn [[r]] (= r :refer-clojure)) args))]
(when (seq @deps)
- (analyze-deps @deps))
+ ;; (analyze-deps @deps)
+ (println "**** Skipping analyze-deps ****")
+ )
(set! *cljs-ns* name)
(load-core)
(doseq [nsym (concat (vals requires-macros) (vals uses-macros))]
@@ -846,25 +864,19 @@
(assoc ret :op :var :info lb)
(assoc ret :op :var :info (resolve-existing-var env sym)))))
+(defn is-macro? [sym]
+ (let [var (resolve-existing-var (empty-env) sym)
+ ns (:ns var)
+ name (symbol (name (:name var)))]
+ (get-in @namespaces [:macros ns name])))
+
(defn get-expander [sym env]
- (let [mvar
- (when-not (or (-> env :locals sym) ;locals hide macros
- (and (or (-> env :ns :excludes sym)
- (get-in @namespaces [(-> env :ns :name) :excludes sym]))
- (not (or (-> env :ns :uses-macros sym)
- (get-in @namespaces [(-> env :ns :name) :uses-macros sym])))))
- (if-let [nstr (namespace sym)]
- (when-let [ns (cond
- (= "clojure.core" nstr) (find-ns 'cljs.core)
- (.contains nstr ".") (find-ns (symbol nstr))
- :else
- (-> env :ns :requires-macros (get (symbol nstr))))]
- (.findInternedVar ^clojure.lang.Namespace ns (symbol (name sym))))
- (if-let [nsym (-> env :ns :uses-macros sym)]
- (.findInternedVar ^clojure.lang.Namespace (find-ns nsym) sym)
- (.findInternedVar ^clojure.lang.Namespace (find-ns 'cljs.core) sym))))]
- (when (and mvar (.isMacro ^clojure.lang.Var mvar))
- @mvar)))
+ (let [var (resolve-existing-var (empty-env) sym)
+ ns (:ns var)
+ name (symbol (name (:name var)))]
+ ;(println "// get-expander:" sym ns name)
+ (when (is-macro? sym)
+ (js/eval (str ns "." name)))))
(defn macroexpand-1 [env form]
(let [op (first form)]
@@ -944,10 +956,10 @@
facilitate code walking without knowing the details of the op set."
([env form] (analyze env form nil))
([env form name]
- (let [form (if (instance? clojure.lang.LazySeq form)
+ (let [form (if (instance? cljs.core.LazySeq form)
(or (seq form) ())
form)]
- (load-core)
+ ;;(load-core)
(cond
(symbol? form) (analyze-symbol env form)
(and (seq? form) (seq form)) (analyze-seq env form name)
@@ -957,19 +969,19 @@
(keyword? form) (analyze-keyword env form)
:else {:op :constant :env env :form form}))))
-(defn analyze-file
- [^String f]
- (let [res (if (re-find #"^file://" f) (java.net.URL. f) (io/resource f))]
- (assert res (str "Can't find " f " in classpath"))
- (binding [*cljs-ns* 'cljs.user
- *cljs-file* (.getPath ^java.net.URL res)
- *ns* *reader-ns*]
- (with-open [r (io/reader res)]
- (let [env (empty-env)
- pbr (clojure.lang.LineNumberingPushbackReader. r)
- eof (Object.)]
- (loop [r (read pbr false eof false)]
- (let [env (assoc env :ns (get-namespace *cljs-ns*))]
- (when-not (identical? eof r)
- (analyze env r)
- (recur (read pbr false eof false))))))))))
+;; (defn analyze-file
+;; [^String f]
+;; (let [res (if (re-find #"^file://" f) (java.net.URL. f) (io/resource f))]
+;; (assert res (str "Can't find " f " in classpath"))
+;; (binding [*cljs-ns* 'cljs.user
+;; *cljs-file* (.getPath ^java.net.URL res)
+;; *ns* *reader-ns*]
+;; (with-open [r (io/reader res)]
+;; (let [env (empty-env)
+;; pbr (clojure.lang.LineNumberingPushbackReader. r)
+;; eof (Object.)]
+;; (loop [r (read pbr false eof false)]
+;; (let [env (assoc env :ns (get-namespace *cljs-ns*))]
+;; (when-not (identical? eof r)
+;; (analyze env r)
+;; (recur (read pbr false eof false))))))))))
diff --git a/src/clj/cljs/compiler.clj b/src/clj/cljs/compiler.clj
index 4f5e1e4..965b2ed 100644
--- a/src/clj/cljs/compiler.clj
+++ b/src/clj/cljs/compiler.clj
@@ -778,6 +778,15 @@
(ana/analyze-file "cljs/core.cljs"))
~@body))
+(defn ns-snap [ns]
+ (let [nss1 (dissoc (get @ana/namespaces ns) :requires-macros)
+ nss2 (read-string (pr-str (update-in nss1
+ [:defs] dissoc '/)))]
+ (apply str
+ (emit (ana/analyze (ana/empty-env)
+ (list 'swap! 'cljs.core/namespaces 'assoc (list 'quote ns) (list 'quote nss2)))))))
+
+
(defn compile-file* [src dest]
(with-core-cljs
(with-open [out ^java.io.Writer (io/make-writer dest {})]
@@ -797,10 +806,13 @@
(if (= (:op ast) :ns)
(recur (rest forms) (:name ast) (merge (:uses ast) (:requires ast)))
(recur (rest forms) ns-name deps))))
- {:ns (or ns-name 'cljs.user)
- :provides [ns-name]
- :requires (if (= ns-name 'cljs.core) (set (vals deps)) (conj (set (vals deps)) 'cljs.core))
- :file dest}))))))
+ (do
+ (println "\n// Analyzer namespace snapshot:")
+ (ns-snap ns-name)
+ {:ns (or ns-name 'cljs.user)
+ :provides [ns-name]
+ :requires (if (= ns-name 'cljs.core) (set (vals deps)) (conj (set (vals deps)) 'cljs.core))
+ :file dest})))))))
(defn requires-compilation?
"Return true if the src file requires compilation."
diff --git a/src/cljs/cljs/core.cljs b/src/cljs/cljs/core.cljs
index 3bb58ef..c4eb5a6 100644
--- a/src/cljs/cljs/core.cljs
+++ b/src/cljs/cljs/core.cljs
@@ -1453,6 +1453,7 @@ reduces them without incurring seq initialization"
([x] (cond
(symbol? x) (. x (substring 2 (alength x)))
(keyword? x) (str* ":" (. x (substring 2 (alength x))))
+ (regexp? x) (.-source x)
(nil? x) ""
:else (. x (toString))))
([x & ys]
@@ -7210,3 +7211,92 @@ reduces them without incurring seq initialization"
(-hash [this]
(goog.string/hashCode (pr-str this))))
+
+;;;;;;;;;;;;;;;;;; PushbackReader ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defprotocol PushbackReader
+ (read-char [reader] "Returns the next char from the Reader,
+nil if the end of stream has been reached")
+ (unread [reader ch] "Push back a single character on to the stream"))
+
+; Using two atoms is less idomatic, but saves the repeat overhead of map creation
+(deftype StringPushbackReader [s index-atom buffer-atom]
+ PushbackReader
+ (read-char [reader]
+ (if (empty? @buffer-atom)
+ (let [idx @index-atom]
+ (swap! index-atom inc)
+ (aget s idx))
+ (let [buf @buffer-atom]
+ (swap! buffer-atom rest)
+ (first buf))))
+ (unread [reader ch] (swap! buffer-atom #(cons ch %))))
+
+(defn push-back-reader [s]
+ "Creates a StringPushbackReader from a given string"
+ (StringPushbackReader. s (atom 0) (atom nil)))
+
+;;;;;;;;;;;;;;;;;; Namespace/Vars/Macro hackery ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def namespaces (atom '{cljs.core {:name cljs.core}
+ cljs.user {:name cljs.user}}))
+
+(defn setMacro [sym]
+ (let [ns (symbol (or (namespace sym)
+ (try cljs.analyzer/*cljs-ns*
+ (catch js/Error e 'cljs.core))))
+ name (symbol (name sym))]
+ (swap! namespaces assoc-in [:macros ns name] true))
+ nil)
+
+(def
+
+ ^{:doc "Like defn, but the resulting function name is declared as a
+ macro and will be used as a macro by the compiler when it is
+ called."
+ :arglists '([name doc-string? attr-map? [params*] body]
+ [name doc-string? attr-map? ([params*] body)+ attr-map?])
+ :added "1.0"}
+ defmacro (fn [&form &env
+ name & args]
+ (let [prefix (loop [p (list name) args args]
+ (let [f (first args)]
+ (if (string? f)
+ (recur (cons f p) (next args))
+ (if (map? f)
+ (recur (cons f p) (next args))
+ p))))
+ fdecl (loop [fd args]
+ (if (string? (first fd))
+ (recur (next fd))
+ (if (map? (first fd))
+ (recur (next fd))
+ fd)))
+ fdecl (if (vector? (first fdecl))
+ (list fdecl)
+ fdecl)
+ add-implicit-args (fn [fd]
+ (let [args (first fd)]
+ (cons (vec (cons '&form (cons '&env args))) (next fd))))
+ add-args (fn [acc ds]
+ (if (nil? ds)
+ acc
+ (let [d (first ds)]
+ (if (map? d)
+ (conj acc d)
+ (recur (conj acc (add-implicit-args d)) (next ds))))))
+ fdecl (seq (add-args [] fdecl))
+ decl (loop [p prefix d fdecl]
+ (if p
+ (recur (next p) (cons (first p) d))
+ d))]
+ (list 'do
+ (list 'def (first decl) (cons `fn* (first (rest decl))))
+ #_(cons `defn decl)
+ #_(list '. (list 'var name) '(setMacro))
+ (list 'cljs.core/setMacro (list 'quote name))
+ #_(list 'var name)))))
+
+#_(. (var defmacro) (setMacro))
+(setMacro 'cljs.core/defmacro)
+
diff --git a/src/cljs/cljs/reader.cljs b/src/cljs/cljs/reader.cljs
index 36c1592..0ec9dcd 100644
--- a/src/cljs/cljs/reader.cljs
+++ b/src/cljs/cljs/reader.cljs
@@ -7,29 +7,8 @@
; You must not remove this notice, or any other, from this software.
(ns cljs.reader
- (:require [goog.string :as gstring]))
-
-(defprotocol PushbackReader
- (read-char [reader] "Returns the next char from the Reader,
-nil if the end of stream has been reached")
- (unread [reader ch] "Push back a single character on to the stream"))
-
-; Using two atoms is less idomatic, but saves the repeat overhead of map creation
-(deftype StringPushbackReader [s index-atom buffer-atom]
- PushbackReader
- (read-char [reader]
- (if (empty? @buffer-atom)
- (let [idx @index-atom]
- (swap! index-atom inc)
- (aget s idx))
- (let [buf @buffer-atom]
- (swap! buffer-atom rest)
- (first buf))))
- (unread [reader ch] (swap! buffer-atom #(cons ch %))))
-
-(defn push-back-reader [s]
- "Creates a StringPushbackReader from a given string"
- (StringPushbackReader. s (atom 0) (atom nil)))
+ (:require [goog.string :as gstring]
+ [cljs.analyzer :as ana]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; predicates
@@ -368,6 +347,136 @@ nil if the end of stream has been reached")
(with-meta o (merge (meta o) m))
(reader-error rdr "Metadata can only be applied to IWithMetas")))))
+(def UNQUOTE :__thisInternalKeywordRepresentsUnquoteToTheReader__)
+(def UNQUOTE-SPICING :__thisInternalKeywordRepresentsUnquoteSplicingToTheReader__)
+
+(declare syntaxQuote)
+(def ^:dynamic *gensym-env* (atom nil))
+
+(defn isUnquote? [form]
+ (and (satisfies? ISeq form) (= (first form) UNQUOTE)))
+
+(defn isUnquoteSplicing? [form]
+ (and (satisfies? ISeq form) (= (first form) UNQUOTE-SPLICING)))
+
+(defn sqExpandList [sq]
+ (doall
+ (for [item sq]
+ (cond
+ (isUnquote? item)
+ (list 'list (second item))
+
+ (isUnquoteSplicing? item)
+ (second item)
+
+ :else
+ (list 'list (syntaxQuote item))
+ ))))
+
+(defn syntaxQuote [form]
+ (cond
+ ;; (Compiler.isSpecial(form))
+ (get ana/specials form)
+ (list 'quote form)
+
+ ;; (form instanceof Symbol)
+ (symbol? form)
+ (let [sym form
+ name (name sym)
+ ns (namespace sym)
+ var (ana/resolve-existing-var (ana/empty-env) sym)]
+ (cond
+ ;; no namespace and name ends with #
+ (and (not ns) (= "#" (last name)))
+ (let [new-name (subs name 0 (- (count name) 1))
+ gmap @*gensym-env*]
+ (when (not gmap)
+ (reader-error nil "Gensym literal not in syntax-quote"))
+ (let [gs (or (get gmap sym)
+ (gensym (str new-name "__auto__")))]
+ (swap! *gensym-env* assoc sym gs)
+ (list 'quote gs)))
+
+ ;; no namespace and name ends with .
+ (and (not ns) (= "." (last name)))
+ (let [new-name (subs name 0 (- (count name) 1))
+ new-var (ana/resolve-existing-var
+ (ana/empty-env) (symbol new-name))]
+ (list 'quote (:name new-var)))
+
+ ;; no namespace and name begins with .
+ (and (not ns) (= "." (first name)))
+ (list 'quote sym)
+
+ ;; resolve symbol
+ :else
+ (list 'quote
+ (:name
+ (cljs.analyzer/resolve-existing-var (cljs.analyzer/empty-env) sym)))))
+
+ ;; (isUnquote(form))
+ (isUnquote? form)
+ (second form)
+
+ ;; (isUnquoteSplicing(form))
+ (isUnquoteSplicing? form)
+ (reader-error rdr "Reader ~@ splice not in list")
+
+ ;; (form instanceof IPersistentCollection)
+ (satisfies? ICollection form)
+ (cond
+ (satisfies? IRecord form)
+ form
+
+ (satisfies? IMap form)
+ (list 'apply 'hash-map (list 'seq (cons 'concat (sqExpandList (apply concat (seq form))))))
+
+ (satisfies? IVector form)
+ (list 'apply 'vector (list 'seq (cons 'concat (sqExpandList form))))
+
+ (satisfies? ISet form)
+ (list 'apply 'hash-set (list 'seq (cons 'concat (sqExpandList (seq form)))))
+
+ (or (satisfies? ISeq form) (satisfies? IList form))
+ (if-let [sq (seq form)]
+ (list 'seq (cons 'concat (sqExpandList sq)))
+ (cons 'list nil))
+
+ :else
+ (reader-error rdr "Unknown Collection type"))
+
+ ;; (form instanceof Keyword || form instanceof Number ||
+ ;; form instanceof Character || form instanceof String)
+ (or (keyword? form) (number? form) (string? form))
+ form
+
+ :else
+ (list 'quote form)
+ ))
+
+(defn read-syntax-quote
+ [rdr _]
+ (binding [*gensym-env* (atom {})]
+ (let [form (read rdr true nil true)]
+ (syntaxQuote form))))
+
+(defn read-unquote
+ [rdr _]
+ (let [ch (read-char rdr)]
+ (cond
+ (= nil ch)
+ (reader-error rdr "EOF while reading character")
+
+ (= "@" ch)
+ (let [o (read rdr true nil true)]
+ (list UNQUOTE-SPLICING o))
+
+ :else
+ (do
+ (unread rdr ch)
+ (let [o (read rdr true nil true)]
+ (list UNQUOTE o))))))
+
(defn read-set
[rdr _]
(set (read-delimited-list "}" rdr true)))
@@ -389,8 +498,8 @@ nil if the end of stream has been reached")
(identical? c \') (wrapping-reader 'quote)
(identical? c \@) (wrapping-reader 'deref)
(identical? c \^) read-meta
- (identical? c \`) not-implemented
- (identical? c \~) not-implemented
+ (identical? c \`) read-syntax-quote
+ (identical? c \~) read-unquote
(identical? c \() read-list
(identical? c \)) read-unmatched-delimiter
(identical? c \[) read-vector
diff --git a/src/cljs/goog.js b/src/cljs/goog.js
new file mode 100644
index 0000000..9be0a56
--- /dev/null
+++ b/src/cljs/goog.js
@@ -0,0 +1,37 @@
+// Simple module wrapper around Google Closure Library
+// goog/ directory should be in the same directory as this file
+
+goog = exports;
+path_ = require("path");
+googDir_ = path_.join(path_.dirname(module.filename), "goog");
+
+rawLoaded_ = {};
+rawLoad_ = function(file) {
+ var path = path_.resolve(googDir_, file);
+ //console.log("rawLoad_ file:", file, "path:", path);
+
+ if (rawLoaded_[path]) { return; }
+ rawLoaded_[path] = true;
+
+ var contents = require('fs').readFileSync(path);
+ // TODO: cljs.nodejscli needs require, but this is gross
+ global.require = require;
+ process.binding('evals').NodeScript.
+ runInThisContext.call(global, contents, file);
+};
+
+rawLoad_('base.js');
+goog.global = goog.window = global.top = global;
+rawLoad_('deps.js');
+
+// Override goog.require script loader/evaluator
+goog.writeScriptTag_ = function(file) {
+ try {
+ rawLoad_(file);
+ } catch (exc) {
+ console.error('Could not goog.require("' + file + '")\n' + exc.stack);
+ process.exit(1);
+ }
+ return false;
+};
+
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment