-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- Concepts - a short overview of compilers and how Froley fits in.
- Token Type Definitions - how to define token types.
- Scanner Command Reference - Scanner coding reference .
- Parser Command Reference - Parser coding reference.
A .froley file is used to define token types and write the logic for the Scanner and Parser. Name it with the name of the language you're defining - for example, Simple.froley for a language called "Simple".
A .froley file is comprised of three sections: [tokens](Token Type Definitions), [scanner](Scanner Command Reference), and [parser](Parser Command Reference).
There are four types of comments in Froley:
# Single-line comment
#{ Multi-line comment }#
==== Single-line "HR" comment
---- Single-line "HR" comment
Taken from the Simple-Interpreter example.
--------------------------------------------------------------------------------
tokens
--------------------------------------------------------------------------------
tokens begins an unnamed group of token type definitions.
Note that ---- begins a single-line comment so the "HR" bars are for visual formatting only.
EOL(end of line)
IDENTIFIER identifier [content]
NUMBER number [content]
STRING string [content]
Sets of token type names, symbols, and optional [attributes]. Symbols can't have any spaces
as part of them unless you declare the token types using the NAME(symbol) variation.
[content] is a built-in attribute type that causes created tokens to automatically have
a copy of the Scanner's scan buffer as the token's content string.
--------------------------------------------------------------------------------
tokens Symbols
--------------------------------------------------------------------------------
SYMBOL_BANG !
SYMBOL_CARET ^
SYMBOL_CLOSE_PAREN ) [structural]
SYMBOL_EQUALS =
SYMBOL_MINUS -
SYMBOL_OPEN_PAREN (
SYMBOL_PLUS +
SYMBOL_SLASH /
SYMBOL_STAR *
A named token-type group for symbols. The group name can be used to match scan buffer content with token symbols en masse.
--------------------------------------------------------------------------------
tokens Keywords
--------------------------------------------------------------------------------
KEYWORD_PRINTLN println
A named group for keywords. Keywords should be kept separate from symbols because of different scanning mechanisms used. For example, if the Scanner scans ++ it does not need to read further characters to classify ++ as an increment operator. On the other hand if else is scanned, it cannot be classified as the else operator because the next characters might be where for a variable named elsewhere. Keyword matching must first read a complete ID and then check for keywords.
main
--------------------------------------------------------------------------------
scanner
--------------------------------------------------------------------------------
- main
consume [ \r\t]* # whitespace
if (consume('\n')) produce EOL
# single-line comment
if consume("#" [^\n]*) restart
if (not hasAnother) halt
markPosition
match
produceAny Symbols
endMatch
scan_id_or_keyword
scan_number
scan_string
syntaxError
-
mainis the topmost routine so it becomes the start routine. - The start routine will be repeatedly called until a
haltcommand is executed. The general approach is to produce one new Token each "pass". -
consumecan operate on characters, strings, or patterns. If the specified sequence can be read from the input, that sequence is discarded andconsumereturnstrue. Otherwise nothing is read (no partial matches) andconsumereturnsfalse. - Patterns are a simple form of RegEx comprised of the following pattern elements:
- Curly braces
{...}can be used to unambiguously enclose an entire pattern sequence. - Single and double-quote strings are literals that must be matched exactly.
-
[...]defines a set of single characters, any single one of which can be read to match that pattern element. -
[^...]matches any single character except those listed - "NOT these". -
[A-Z]matches any single character betweenAandZ. Mix and match:[A-Za-z_]etc. - Following a pattern element with
*,+, or?allows it to occur zero or more times, one or more times, or optionally (at most once), respectively.
- Curly braces
-
restartabandons the current pass and restarts at the start routine. -
hasAnotherreturnstrueas long as there are any characters waiting to be read from the input. -
markPositionuses the current line and column of input the next time a Token is created. - A standard
match(also calledmatch input) uses an efficient scan table technique to read the longest match among its constituentproduceAnyand individualcasecomparisons. In this case any symbols of theSymbolstoken group is matched then a corresponding Token is produced.produceing a Token automatically adds it to the output queue and restarts the Scanner at the start routine. -
scan_id_or_keyword,scan_number, andscan_stringare all routine calls. Each routine produces a Token if it can, so if the routine returns (instead of the Scanner restarting) then it was unable to scan a token. - The final command is
syntaxError, which generates an "Unexpected input" error.
scan_id_or_keyword
- scan_id_or_keyword
if (not scan([_a-zA-Z][_a-zA-Z0-9]*)) return
match buffer
produceAny Keywords
others: produce IDENTIFIER
endMatch
- Routines cannot accept any parameters or return any values. However global variables (not shown here) can be used to transmit information across routine calls.
-
scanis likeconsumeexcept instead of discarding the matching input, the characters are stored in the internal scanbuffer. So ifscan(...)returns true then a matching set of characters is stored inbuffer. -
match bufferattempts to match the characters in the scan buffer against the listed symbols and cases. The entire buffer must be matched to count as a match. If there are no matches then theothersblock is executed if defined. - Per standard approach, keywords should be handled by first reading a complete identifier and then attempting to match it with a defined keyword.
- The
IDENTIFIERtoken type is defined with the attribute[content], so a newIDENTIFIERtoken will automatically get a copy of the scan buffer as its.contentstring.
scan_number
- scan_number
if (not hasAnother) return
if (scan [0-9])
scan_integer
if (scan '.') scan_integer
produce NUMBER
elseIf (scan '.')
scan_integer
produce NUMBER
else
return
endIf
scan_number could be simpler if we weren't filtering out underscore (_) characters from the input. Numbers such as 123_456 are tokenized as 123456.
scan_integer, scan_string
- scan_integer
while hasAnother
if (not scan([0-9]) and not consume('_'*)) return
endWhile
return
- scan_string
if (not consume('"')) return
while (hasAnother and not nextIs('\n'))
ch = read
if (ch == '"') produce STRING
elseIf (ch == '\\' and hasAnother) ch = read
collect ch
endWhile
syntaxError "Unterminated string."
Whereas the Scanner produces tokens onto a queue, with each token being independent of every other token, the Parser produces Cmd nodes onto a stack. Each command node produced removes zero or more nodes from the stack as its properties and then is itself added to the stack. The stack should have a single node on it - the root of the AST - when the start routine returns.
--------------------------------------------------------------------------------
parser
--------------------------------------------------------------------------------
- statements
consume_eols
beginList
while (hasAnother)
on "println"
arg_list
create Println(args)
elseOthers
expression
endOn
consume_eols
endWhile
produceList Statements
-
consume_eolsis a call to another routine (not built-in). -
beginListmarks the current top of the node stack. WhenproduceListis later called, all nodes since become part of an extendedCmdListarbitrarily calledStatements. -
hasAnotherreturns true as long as there is another unread Token available. The input tokens are automatically set up to come from the Scanner's output. -
on "println"behaves likeconsume("println")with the additional behavior of making that consumed Token the current token stored with the next created or produced Cmd node. - Tokens can be consume'd or scan'd using either their "symbol" or their TOKEN_NAME.
- In the Scanner,
producecreates a Token and then restarts the Scanner. In the Parser,producecreates a new command node and returns from the current routine. -
createis likeproduceexcept the current routine continues instead of being return'd from. - New extended
Cmdnode types are created automatically when used in acreateorproducecommand.create Println(args)says: "Extend a new node type calledPrintlnwith a single propertyargs:Cmd. Instantiating aPrintlnpops one node off the internal stack and stores it asargs. The newPrintlnnode is then pushed on the stack."
consume_eols, arg_list
- consume_eols
while (consume(EOL)) noAction
- arg_list
beginList
while (hasAnother and not nextIs(EOL))
expression
endWhile
produceList Args
expression, assign
- expression
assign
- assign [rightBinary]
on "=" -> Assign
- A
[rightBinary]routine facilitates the construction of right-associative operator subtrees. If the input tokens were [a=b=c], the resulting subtree here would beAssign( Access(a), Assign(Access(b),Access(c)) ), reflecting the order of operations(a = (b = c)). - Any "operation shorthand" routine (rightBinary, binary, preUnary, postUnary) implicitly pulls its higher-precedence operands from the next routine below it, so
assign [rightBinary]gets its operands by callingadd_subtract. - Cmd node type
Assignwill automatically be defined and will extend built-in base typeBinary. -
->is shorthand forproduce. - Only
on-produce(e.g.on ... -> NodeType) statements can be used in operator shorthand routines. - An operator shorthand routine automatically returns a single node from the next routine down if no
onmatches are found.
Binary Operators
- add_subtract [binary]
on "+" -> Add
on "-" -> Subtract
- multiply_divide [binary]
on "*" -> Multiply
on "/" -> Divide
- power [binary]
on "^" -> Power
- A
[binary]routine is a left-associative binary operator shorthand routine. Input tokens [a+b+c] would result in the subtreeAdd( Add(Access(a),Access(b)), Access(c) ), reflecting the order of operations((a + b) + c). Node typesAdd,Subtract,Multiply,Divide, andPowerwould automatically be defined as extendedBinarynodes.
Unary Operators
- factorial [postUnary]
on "!" -> Factorial
- negate [preUnary]
on "-" -> Negate
- The two routines above would parse input tokens [
-5!] into the subtreeFactorial( Negate(Number(5)) ).
term
- term
on '(' expression ')': return
on IDENTIFIER -> Access(name=content)
on NUMBER -> Number(value=content:Real)
on STRING -> LiteralString(value=content)
syntaxError
- Parens (
()) control the order of parsing but do not need to create any additional structure. - Here we see
on '('with two additional parts:expressionand). Only the first part of a multi-partonis checked as part of the condition and the remainder become expected parts. For example:
Equivalent statements for on '(' expression ')': return
#{
on '('
expression
mustConsume ')'
return
endOn
}#
- Node properties (like
argsinPrintln(args)) normally have the default typeCmd. - Properties may also be declared as any extended node type (
Println(args:Args)) or as one of the five built-in typesString,Real,Integer,Character, andLogical. - If a node type property is assigned the value
contentthen the string.contentof the current token (set by the enclosingon) is passed as the argument. A property assignedcontentdefaults to typeStringbut can be explicitly declared as one of the four other built-in primitive types.
Additional Nodes
# This routine is not called but its commands cause additional necessary node defs to be generated
- additional_nodes
create ReadVar(name:String)
create WriteVar(name:String,new_value)
- As seen previously, a command such as
Access(name=content)creates an Access class with aname:Stringproperty, with the string value's taken from the current token's content. - Consider here the additional functionality of declaring
ReadVar(name:String)without the=contentvalue assignment. In this case a node is popped off the internal stack andto->Stringis called on it. ThereforeWriteVar(name:String,new_value)pops twoCmdnodes off the stack.new_valueis assigned a reference the most recent node andnameis assigned the older node.to->String.