diff --git a/ast/Anchor.lua b/ast/Anchor.lua new file mode 100644 index 0000000..72b53b6 --- /dev/null +++ b/ast/Anchor.lua @@ -0,0 +1,35 @@ +local ast = require("ast") + +local resume_manager + +local Anchor +Anchor = ast.abstract.Node { + type = "anchor", + + name = nil, + + init = function(self, name) + self.name = name + self._list_anchors_cache = { [name] = true } + end, + + _hash = function(self) + return ("anchor<%q>"):format(self.name) + end, + + _format = function(self, ...) + return "#"..self.name + end, + + _eval = function(self, state) + if self:contains_resume_target(state) then + resume_manager:set_reached(state) + end + return Anchor:new(self.name) + end +} + +package.loaded[...] = Anchor +resume_manager = require("state.resume_manager") + +return Anchor diff --git a/ast/Block.lua b/ast/Block.lua index 76ea90f..670e1d5 100644 --- a/ast/Block.lua +++ b/ast/Block.lua @@ -1,6 +1,8 @@ local ast = require("ast") local Nil, Return, AutoCall, ArgumentTuple, Flush +local resume_manager = require("state.resume_manager") + local Block = ast.abstract.Node { type = "block", @@ -34,11 +36,11 @@ local Block = ast.abstract.Node { _eval = function(self, state) local r state.scope:push() - if self:resuming(state) then - local resuming = self:get_resume_data(state) + if self:contains_resume_target(state) then + local anchor = resume_manager:get(state) local resumed = false for _, e in ipairs(self.expressions) do - if e == resuming then resumed = true end + if e:contains_anchor(anchor) then resumed = true end if resumed then r = e:eval(state) if AutoCall:issub(r) then @@ -51,7 +53,6 @@ local Block = ast.abstract.Node { end else for _, e in ipairs(self.expressions) do - self:set_resume_data(state, e) r = e:eval(state) if AutoCall:issub(r) then r = r:call(state, ArgumentTuple:new()) diff --git a/ast/Function.lua b/ast/Function.lua index 63132b8..5a95b66 100644 --- a/ast/Function.lua +++ b/ast/Function.lua @@ -45,7 +45,7 @@ Function = Overloadable { state.scope:push() args:bind_parameter_tuple(state, self.parameters) - local exp = self.expression:eval_resumable(state) + local exp = self.expression:eval(state) state.scope:pop() diff --git a/ast/Resumable.lua b/ast/Resumable.lua deleted file mode 100644 index b9235a9..0000000 --- a/ast/Resumable.lua +++ /dev/null @@ -1,60 +0,0 @@ -local ast = require("ast") -local Table - -local resumable_manager - -local Resumable -Resumable = ast.abstract.Runtime { - type = "resumable", - - resuming = false, - - expression = nil, - scope = nil, - data = nil, - - init = function(self, state, expression, scope, data) - self.expression = expression - self.scope = scope - self.data = data or Table:new(state) - end, - - _format = function(self) - return "" - end, - - traverse = function(self, fn, ...) - fn(self.expression, ...) - fn(self.data, ...) - fn(self.scope, ...) - end, - - -- returns a copy with the data copied - capture = function(self, state) - return Resumable:new(state, self.expression, self.scope, self.data:copy(state)) - end, - - -- resume from this resumable - call = function(self, state, args) - assert(args.arity == 0, "Resumable! does not accept arguments") - - state.scope:push(self.scope) - - local resuming = self:capture(state) - resuming.resuming = true - resumable_manager:push(state, resuming) - local r = self.expression:eval(state) - resumable_manager:pop(state) - - state.scope:pop() - - return r - end, -} - -package.loaded[...] = Resumable -Table = ast.Table - -resumable_manager = require("state.resumable_manager") - -return Resumable diff --git a/ast/ResumeParentFunction.lua b/ast/ResumeParentFunction.lua deleted file mode 100644 index acae124..0000000 --- a/ast/ResumeParentFunction.lua +++ /dev/null @@ -1,44 +0,0 @@ --- intended to be wrapped in a Function, so that when resuming from the function, will keep resuming to where the function was called from --- used in Choices to resume back from where the event was flushed --- note: when resuming, the return value will be discarded, instead returning what the parent function will return - -local ast = require("ast") -local ArgumentTuple - -local resumable_manager - -local ResumeParentFunction = ast.abstract.Node { - type = "resume parent function", - - expression = nil, - - init = function(self, expression) - self.expression = expression - self.format_priority = expression.format_priority - end, - - _format = function(self, ...) - return self.expression:format(...) - end, - - traverse = function(self, fn, ...) - fn(self.expression, ...) - end, - - _eval = function(self, state) - if self:resuming(state) then - self.expression:eval(state) - return self:get_data(state):call(state, ArgumentTuple:new()) - else - self:set_data(state, resumable_manager:capture(state, 1)) - return self.expression:eval(state) - end - end -} - -package.loaded[...] = ResumeParentFunction -ArgumentTuple = ast.ArgumentTuple - -resumable_manager = require("state.resumable_manager") - -return ResumeParentFunction diff --git a/ast/abstract/Node.lua b/ast/abstract/Node.lua index 0df4919..bcf2475 100644 --- a/ast/abstract/Node.lua +++ b/ast/abstract/Node.lua @@ -12,8 +12,8 @@ local binser = require("lib.binser") local uuid = require("common").uuid -local State, Runtime -local resumable_manager +local State, Runtime, Call, Identifier, ArgumentTuple +local resume_manager local custom_call_identifier @@ -99,8 +99,8 @@ Node = class { end, -- prepare the AST after parsing and before evaluation - -- this behave like a cached :traverse through the AST, except this keeps track of the scope stack - -- i.e. when :prepare is called on a node, it should be in a similar scope stack context as will be when it will be evaluated + -- this behave like a cached :traverse through the AST, except this keeps track of the scope stack and preserve evaluation order + -- i.e. when :prepare is called on a node, it should be in a similar scope stack context and order as will be when it will be evaluated -- used to predefine exported variables and other compile-time variable handling -- note: the state here is a temporary state only used during the prepare step -- the actual preparation is done in _prepare @@ -122,6 +122,30 @@ Node = class { self:traverse(traverse.prepare, state) end, + -- returns a reversed list { [anchor] = true, ... } of the anchors contained in this node and its children + _list_anchors = function(self) + if not self._list_anchors_cache then + self._list_anchors_cache = {} + self:traverse(function(v) + for name in pairs(v:_list_anchors()) do + self._list_anchors_cache[name] = true + end + end) + end + return self._list_anchors_cache + end, + _list_anchors_cache = nil, + + -- returns true if the node or its children contains the anchor + contains_anchor = function(self, anchor) + return not not self:_list_anchors()[anchor.name] + end, + + -- returns true if we are currently trying to resume to an anchor target contained in the current node + contains_resume_target = function(self, state) + return resume_manager:resuming(state) and self:contains_anchor(resume_manager:get(state)) + end, + -- generate a list of translatable nodes that appear in this node -- should only be called on non-runtime nodes -- if a node is translatable, redefine this to add it to the table - note that it shouldn't call :traverse or :list_translatable on its children, as nested translations should not be needed @@ -131,26 +155,6 @@ Node = class { return t end, - -- same as eval, but make the evaluated expression as a resume boundary - -- i.e. if a checkpoint is defined somewhere in this eval, it will start back from this node eval when resuming - eval_resumable = function(self, state) - return resumable_manager:eval(state, self) - end, - -- set the current resume data for this node - -- (relevant inside :eval) - set_resume_data = function(self, state, data) - resumable_manager:set_data(state, self, data) - end, - -- get the current resume data for this node - get_resume_data = function(self, state) - return resumable_manager:get_data(state, self) - end, - -- returns true if the current node is in a resuming state - -- (relevant inside :eval) - resuming = function(self, state) - return resumable_manager:resuming(state, self) - end, - -- return result AST -- arg is a ArgumentTuple node (already evaluated) -- redefine if relevant @@ -276,11 +280,11 @@ Node = class { -- Thus, any require here that may require other Nodes shall be done here. This method is called in anselme.lua after everything else is required. _i_hate_cycles = function(self) local ast = require("ast") - custom_call_identifier = ast.Identifier:new("_!") - Runtime = ast.abstract.Runtime + Runtime, Call, Identifier, ArgumentTuple = ast.abstract.Runtime, ast.Call, ast.Identifier, ast.ArgumentTuple + custom_call_identifier = Identifier:new("_!") State = require("state.State") - resumable_manager = require("state.resumable_manager") + resume_manager = require("state.resume_manager") end, _debug_traverse = function(self, level) diff --git a/common/init.lua b/common/init.lua index 9802c35..a0e6ee1 100644 --- a/common/init.lua +++ b/common/init.lua @@ -35,8 +35,8 @@ local common = { { "!", 12 } }, infixes = { - { ";", 1 }, { "->", 1 }, - { "#", 2 }, + { ";", 1 }, + { "#", 2 }, { "->", 2 }, { "~>", 2 }, { "~", 4 }, { "~?", 4 }, { "|>", 5 }, { "&", 5 }, { "|", 5 }, { "==", 7 }, { "!=", 7 }, { ">=", 7 }, { "<=", 7 }, { "<", 7 }, { ">", 7 }, diff --git a/parser/expression/primary/anchor.lua b/parser/expression/primary/anchor.lua new file mode 100644 index 0000000..9247762 --- /dev/null +++ b/parser/expression/primary/anchor.lua @@ -0,0 +1,25 @@ +local primary = require("parser.expression.primary.primary") + +local identifier = require("parser.expression.primary.identifier") + +local ast = require("ast") +local Anchor = ast.Anchor + +return primary { + match = function(self, str) + if str:match("^#") then + return identifier:match(str:match("^#(.-)$")) + end + return false + end, + + parse = function(self, source, str) + local start_source = source:clone() + local rem = source:consume(str:match("^(#)(.-)$")) + + local ident + ident, rem = identifier:parse(source, rem) + + return Anchor:new(ident.name):set_source(start_source), rem + end +} diff --git a/parser/expression/primary/init.lua b/parser/expression/primary/init.lua index 370bb69..eae9843 100644 --- a/parser/expression/primary/init.lua +++ b/parser/expression/primary/init.lua @@ -12,6 +12,7 @@ local primaries = { r("function_definition"), r("symbol"), r("identifier"), + r("anchor"), r("block_identifier"), r("tuple"), r("struct"), diff --git a/parser/expression/secondary/infix/choice.lua b/parser/expression/secondary/infix/choice.lua index 5590eee..af3394e 100644 --- a/parser/expression/secondary/infix/choice.lua +++ b/parser/expression/secondary/infix/choice.lua @@ -3,7 +3,7 @@ local infix = require("parser.expression.secondary.infix.infix") local operator_priority = require("common").operator_priority local ast = require("ast") -local Call, Identifier, ArgumentTuple, ResumeParentFunction, ParameterTuple, Function = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.ResumeParentFunction, ast.ParameterTuple, ast.Function +local Call, Identifier, ArgumentTuple, ParameterTuple, Function = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.ParameterTuple, ast.Function return infix { operator = "|>", @@ -11,7 +11,7 @@ return infix { priority = operator_priority["_|>_"], build_ast = function(self, left, right) - right = Function:new(ParameterTuple:new(), ResumeParentFunction:new(right)) + right = Function:new(ParameterTuple:new(), right) return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right)) end } diff --git a/parser/expression/secondary/infix/resume.lua b/parser/expression/secondary/infix/resume.lua new file mode 100644 index 0000000..8d077a2 --- /dev/null +++ b/parser/expression/secondary/infix/resume.lua @@ -0,0 +1,9 @@ +local infix_quote_right = require("parser.expression.secondary.infix.infix_quote_right") + +local operator_priority = require("common").operator_priority + +return infix_quote_right { + operator = "~>", + identifier = "_~>_", + priority = operator_priority["_~>_"] +} diff --git a/parser/expression/secondary/init.lua b/parser/expression/secondary/init.lua index 784765b..e7426b2 100644 --- a/parser/expression/secondary/init.lua +++ b/parser/expression/secondary/init.lua @@ -8,10 +8,11 @@ local secondaries = { -- binary infix operators -- 1 r("infix.semicolon"), - r("infix.translate"), -- 2 r("infix.tuple"), r("infix.tag"), + r("infix.translate"), + r("infix.resume"), -- 4 r("infix.while"), r("infix.if"), diff --git a/state/ScopeStack.lua b/state/ScopeStack.lua index 90639c8..378a84c 100644 --- a/state/ScopeStack.lua +++ b/state/ScopeStack.lua @@ -76,7 +76,7 @@ local ScopeStack = class { define_overloadable = function(self, symbol, exp) return self.current:define_overloadable(self.state, symbol, exp) end, defined = function(self, identifier) return self.current:defined(self.state, identifier) end, defined_in_current = function(self, symbol) return self.current:defined_in_current(self.state, symbol) end, - set = function(self, identifier, exp) self.current:set(self.state, identifier, exp) end, + set = function(self, identifier, exp) self.current:set(self.state, identifier, exp) end, get = function(self, identifier) return self.current:get(self.state, identifier) end, depth = function(self) return self.current:depth() end, diff --git a/state/State.lua b/state/State.lua index e9846cf..225e582 100644 --- a/state/State.lua +++ b/state/State.lua @@ -5,7 +5,6 @@ local class = require("class") local ScopeStack = require("state.ScopeStack") local tag_manager = require("state.tag_manager") local event_manager = require("state.event_manager") -local resumable_manager = require("state.resumable_manager") local translation_manager = require("state.translation_manager") local uuid = require("common").uuid local parser = require("parser") @@ -25,14 +24,12 @@ State = class { self.scope = ScopeStack:new(self, branch_from) event_manager:reset(self) -- events are isolated per branch - resumable_manager:reset(self) -- resumable stack is isolated per branch -- create new empty state else self.scope = ScopeStack:new(self) event_manager:setup(self) tag_manager:setup(self) - resumable_manager:setup(self) translation_manager:setup(self) end end, diff --git a/state/event_manager.lua b/state/event_manager.lua index cf6dda9..9646c84 100644 --- a/state/event_manager.lua +++ b/state/event_manager.lua @@ -1,7 +1,7 @@ local class = require("class") local ast = require("ast") -local Nil, String, List, Identifier = ast.Nil, ast.String, ast.List, ast.Identifier +local Nil, String, List, Identifier, Boolean = ast.Nil, ast.String, ast.List, ast.Identifier, ast.Boolean -- list of event data local event_buffer_identifier = Identifier:new("_event_buffer") @@ -11,18 +11,26 @@ local event_buffer_symbol = event_buffer_identifier:to_symbol{ confined_to_branc local last_event_type_identifier = Identifier:new("_last_event_type") local last_event_type_symbol = last_event_type_identifier:to_symbol{ confined_to_branch = true } +-- indicate if the next flush should be ignored for the current buffered event +local discard_next_flush_identifier = Identifier:new("_discard_next_flush") +local discard_next_flush_symbol = discard_next_flush_identifier:to_symbol{ confined_to_branch = true } + return class { init = false, setup = function(self, state) state.scope:define(event_buffer_symbol, List:new(state)) state.scope:define(last_event_type_symbol, Nil:new()) + state.scope:define(discard_next_flush_symbol, Nil:new()) end, reset = function(self, state) state.scope:set(event_buffer_identifier, List:new(state)) state.scope:set(last_event_type_identifier, Nil:new()) + state.scope:set(discard_next_flush_identifier, Nil:new()) end, + -- write an event into the event buffer + -- will flush if an event of a different type is present in the buffer write = function(self, state, event) local current_type = state.scope:get(last_event_type_identifier):to_lua(state) if current_type ~= nil and current_type ~= event.type then @@ -31,22 +39,33 @@ return class { state.scope:set(last_event_type_identifier, String:new(event.type)) state.scope:get(event_buffer_identifier):insert(state, event) end, + -- same as :write, but the buffer will be discarded instead of yielded on the next flush + write_and_discard_on_flush = function(self, state, event) + self:write(state, event) + state.scope:set(discard_next_flush_identifier, Boolean:new(true)) + end, + -- flush the event buffer: build the event data and yield it flush = function(self, state) local last_type = state.scope:get(last_event_type_identifier):to_lua(state) if last_type then - local last_buffer = state.scope:get(event_buffer_identifier) - local event_president = last_buffer:get(state, 1) -- elected representative of all concerned events - -- yield event data - local data = event_president:build_event_data(state, last_buffer) - coroutine.yield(last_type, data) - -- clear room for the future - state.scope:set(last_event_type_identifier, Nil:new()) - state.scope:set(event_buffer_identifier, List:new(state)) - -- post callback - if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, data) end + local discard_next_flush = state.scope:get(discard_next_flush_identifier):truthy() + if discard_next_flush then + self:reset(state) + else + local last_buffer = state.scope:get(event_buffer_identifier) + local event_president = last_buffer:get(state, 1) -- elected representative of all concerned events + -- yield event data + local data = event_president:build_event_data(state, last_buffer) + coroutine.yield(last_type, data) + -- clear room for the future + self:reset(state) + -- post callback + if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, data) end + end end end, + -- keep flushing until nothing is left (a flush may re-fill the buffer during its execution) final_flush = function(self, state) while state.scope:get(last_event_type_identifier):to_lua(state) do self:flush(state) end end diff --git a/state/resumable_manager.lua b/state/resumable_manager.lua deleted file mode 100644 index 424be53..0000000 --- a/state/resumable_manager.lua +++ /dev/null @@ -1,70 +0,0 @@ -local class = require("class") - -local ast = require("ast") -local Resumable, Nil, List, Identifier - --- stack of resumable contexts -local resumable_stack_identifier, resumable_stack_symbol - -local resumable_manager = class { - init = false, - - setup = function(self, state) - state.scope:define(resumable_stack_symbol, List:new(state)) - self:push(state, Resumable:new(state, Nil:new(), state.scope:capture())) - end, - reset = function(self, state) - state.scope:set(resumable_stack_identifier, List:new(state)) - self:push(state, Resumable:new(state, Nil:new(), state.scope:capture())) - end, - - push = function(self, state, resumable) - local stack = state.scope:get(resumable_stack_identifier) - stack:insert(state, resumable) - end, - pop = function(self, state) - local stack = state.scope:get(resumable_stack_identifier) - stack:remove(state) - end, - _get = function(self, state) - return state.scope:get(resumable_stack_identifier):get(state, -1) - end, - - -- returns the Resumable object that resumes from this point - -- level indicate which function to resume: level=0 means resume the current function, level=1 the parent function (resume from the call to the current function in the parent function), etc. - capture = function(self, state, level) - level = level or 0 - return state.scope:get(resumable_stack_identifier):get(state, -1-level):capture(state) - end, - - eval = function(self, state, exp) - self:push(state, Resumable:new(state, exp, state.scope:capture())) - local r = exp:eval(state) - self:pop(state) - return r - end, - - set_data = function(self, state, node, data) - self:_get(state).data:set(state, node, data) - end, - get_data = function(self, state, node) - return self:_get(state).data:get(state, node) - end, - resuming = function(self, state, node) - local resumable = self:_get(state) - if node then - return resumable.resuming and resumable.data:has(state, node) - else - return resumable.resuming - end - end -} - -package.loaded[...] = resumable_manager - -Resumable, Nil, List, Identifier = ast.Resumable, ast.Nil, ast.List, ast.Identifier - -resumable_stack_identifier = Identifier:new("_resumable_stack") -resumable_stack_symbol = resumable_stack_identifier:to_symbol{ confined_to_branch = true } -- per-branch, global variables - -return resumable_manager diff --git a/state/resume_manager.lua b/state/resume_manager.lua new file mode 100644 index 0000000..259cd73 --- /dev/null +++ b/state/resume_manager.lua @@ -0,0 +1,44 @@ +local class = require("class") + +local ast = require("ast") +local Nil, Identifier, Anchor + +-- stack of resumable contexts +local resume_anchor_identifier, resume_anchor_symbol + +local resume_manager = class { + init = false, + + -- push a new resume context: all run code between this and the next push will try to resume to anchor + push = function(self, state, anchor) + assert(Anchor:is(anchor), "can only resume to an anchor target") -- well technically it wouldn't be hard to allow to resume to any node, but I feel like there's already enough stuff in Anselme that was done just because it could be done + state.scope:push_partial(resume_anchor_identifier) + state.scope:define(resume_anchor_symbol, anchor) + end, + -- pop the current resume context + pop = function(self, state) + state.scope:pop() + end, + + -- returns true if we are currently trying to resume to an anchor + resuming = function(self, state) + return state.scope:defined(resume_anchor_identifier) and not Nil:is(state.scope:get(resume_anchor_identifier)) + end, + -- returns the anchor we are trying to resume to + get = function(self, state) + return state.scope:get(resume_anchor_identifier) + end, + -- mark the anchor as reached and stop the resume + set_reached = function(self, state) + state.scope:set(resume_anchor_identifier, Nil:new()) + end, +} + +package.loaded[...] = resume_manager + +Nil, Identifier, Anchor = ast.Nil, ast.Identifier, ast.Anchor + +resume_anchor_identifier = Identifier:new("_resume_anchor") +resume_anchor_symbol = resume_anchor_identifier:to_symbol() + +return resume_manager diff --git a/stdlib/checkpoint.lua b/stdlib/checkpoint.lua index 605daac..bca3b43 100644 --- a/stdlib/checkpoint.lua +++ b/stdlib/checkpoint.lua @@ -1,10 +1,22 @@ -local resumable_manager = require("state.resumable_manager") +local ast = require("ast") +local ArgumentTuple = ast.ArgumentTuple + +local resume_manager = require("state.resume_manager") return { { - "new checkpoint", "(level::number=0)", - function(state, level) - return resumable_manager:capture(state, level.number) + "_~>_", "(anchor::anchor, quote)", + function(state, anchor, quote) + resume_manager:push(state, anchor) + local r = quote:call(state, ArgumentTuple:new()) + resume_manager:pop(state) + return r + end + }, + { + "_~>_", "(anchor::nil, quote)", + function(state, anchor, quote) + return quote:call(state, ArgumentTuple:new()) end } } diff --git a/stdlib/conditionals.lua b/stdlib/conditionals.lua index 37bc11b..75a341d 100644 --- a/stdlib/conditionals.lua +++ b/stdlib/conditionals.lua @@ -21,8 +21,9 @@ return { "_~_", "(condition, expression)", function(state, condition, expression) ensure_if_variable(state) if condition:truthy() then + local r = expression:call(state, ArgumentTuple:new()) set_if_variable(state, true) - return expression:call(state, ArgumentTuple:new()) + return r else set_if_variable(state, false) return Nil:new() @@ -36,8 +37,9 @@ return { if last_if_success(state) then return Nil:new() else + local r = expression:call(state, ArgumentTuple:new()) set_if_variable(state, true) - return expression:call(state, ArgumentTuple:new()) + return r end end }, diff --git a/stdlib/text.lua b/stdlib/text.lua index 061b30d..20cd362 100644 --- a/stdlib/text.lua +++ b/stdlib/text.lua @@ -1,5 +1,5 @@ local ast = require("ast") -local Nil, Choice, AttachBlock = ast.Nil, ast.Choice, ast.AttachBlock +local Nil, Choice, AttachBlock, ArgumentTuple = ast.Nil, ast.Choice, ast.AttachBlock, ast.ArgumentTuple local event_manager = require("state.event_manager") local translation_manager = require("state.translation_manager") @@ -19,7 +19,12 @@ return { { "_|>_", "(txt::text, fn)", function(state, text, func) - event_manager:write(state, Choice:new(text, func)) + if func:contains_resume_target(state) then + func:call(state, ArgumentTuple:new()) + event_manager:write_and_discard_on_flush(state, Choice:new(text, func)) + else + event_manager:write(state, Choice:new(text, func)) + end return Nil:new() end }, diff --git a/stdlib/type_check.lua b/stdlib/type_check.lua index ad6a436..faad07e 100644 --- a/stdlib/type_check.lua +++ b/stdlib/type_check.lua @@ -2,10 +2,12 @@ local ast = require("ast") local Boolean = ast.Boolean return { + { "nil", "(x)", function(state, x) return Boolean:new(x.type == "nil") end }, { "number", "(x)", function(state, x) return Boolean:new(x.type == "number") end }, { "string", "(x)", function(state, x) return Boolean:new(x.type == "string") end }, { "boolean", "(x)", function(state, x) return Boolean:new(x.type == "boolean") end }, { "symbol", "(x)", function(state, x) return Boolean:new(x.type == "symbol") end }, + { "anchor", "(x)", function(state, x) return Boolean:new(x.type == "anchor") end }, { "text", "(x)", function(state, x) return Boolean:new(x.type == "text") end },