FGI - Re: UDR: Authoritative Feature List / Grammar

  • From: Ben Kolera <ben.kolera@xxxxxxxxx>
  • To: fgi@xxxxxxxxxxxxx
  • Date: Fri, 4 Sep 2020 07:16:43 +1000

Doing a LL(2) parser? I know its not as "clean" as an LL(1), but if 
necessary, are they that much harder - or have I missed the point entirly?

Yes you have missed the point. I gave you counterexamples of how this
look ahead hurts modularity of the parsers and even very real bugs
that happen from extending such a language. None of it is impossible
to get working, of course, but it's unnecessarily harder than it needs
to be. Especially not where there are easy syntactic alternatives that
remove the ambiguities that need further context. Doing further
context is something that you want to do only when you really really
need it, not as the default choice. I really don't think that it's
worth it here.


But it's okay. Go nuts. I'm really trying to help make things simpler
and easier to manage. But if that's not what we're actually after,
that's fine. I'm out of energy on this.

On Thu, 3 Sep 2020 at 23:36, duluxoz <duluxoz@xxxxxxxxx> wrote:

I do not think there is a flaw (critical or otherwise) in my mental
model (but will be happy to be correct if there is). EBNF is a language
used to describe the syntax of a context-free grammar of a language. A
LL(1) parser is a Left-to-Right Read, Left-to-Right Derivation,
1-Character-Look-Ahead parser. A LL(1) parser should be able to take an
EBNF grammar and, well, parse it.
There's nothing to say that a multi-layer grammar cannot be specified in
EBNF form, and thus LL(1) parsers should (when written correctly) be
able to parse a multi-layer grammar when they written in EBNF form.

Aside from that (because its not really relevant to the discussion, I
believe), our "dice-roll language" has either number-expressions, the
Die-Code-D code, or flags (die-code-modifers), separated by operands -
where there is no operand then we assume '+' (or '*' for 'NdX' ie
'N*dX'), and where there is no N we assume '+1' (or '*1'). With addition
of parenthesis and die-groups to help with precedence, there should be
any ambiguity - remembering that the Grammar.md document is not up-to-date.

OK so, with the first issue, what's wrong with:

Doing a LL(2) parser? I know its not as "clean" as an LL(1), but if
necessary, are they that much harder - or have I missed the point entirly?

I'm pretty sure that I can put things in EBNF form (ie update the
Grammar.md document) to allow for an LL(1) parser anyway. I'll put in an
element for the Die-Code-D 'd' that takes precedence over the
Exponentials ('^','**') but after the Parentheses ('()'). See also the
rest of this post.

Issue 2:

Then let's use 'b' for bust, as I suggested as a possible solution
earlier, if we were going down this route. :-)

Moving on to a different (sub-)topic.

Let's see if I can articulate my (initial and on-going) thought patterns:

A sub-die-roll is a die-roll (ie NdX+MdY+Z, etc) and a set of
flags/options forming a tuple (possible the wrong term) ie [D,F], where
D=NdX+MdY+Z, etc, and F is the set of flags/options (ie
'!','k','dl','s',etc). It is possible for F={}.

(I'm using "[","]" instead of the traditional "{","}" for the tuple(s)
to avoid confusion with the die-group operators "{","}".)

A full-die-roll would then consist of the global-tuple (guple) [ { [D,F]
, [D,F]* },G] where G is the set of group-flags/options (ie
,'k','<N','s',etc) and '*' is as in regex ie '*' =='zero or more'.

In writing this I now realise that G is *not* a subset of F (as is
currently written in the Die-Codes.md document), but instead:

  * F={'','!','a','dh','dl','h','k','kh','kl','m','mt','o','p','rr','s'}
  * G={'','a','b','c','dh','dl','f','k','kh','kl','m','mt','o','p','r',rr'
    ,'s','<','>','='}

Note: The tokens ('<N','>N','=N') are allowed as part of F when they are
paired with (proceeded by) ('!','h','o','rr'), in which case '=N' and be
shortened to 'N'.

Note that all of the Target Number, Crit, Crit-Fail, Bust, and Raise
Codes (the "Target Codes"=={'b','c','f','r','<','>','='}) can only
appear in G; the "Explode Codes"== {'!','h'} can only appear in F, and
the "Common Codes" (the "Keep Codes"=={'dh','dl','k','kh','kl'}, the
"Sort Codes"=={'a','s'}), the "Match Codes"=={'m','mt'}, and the "Reroll
Codes"== {'o','rr'}) can appear in both F and G.

It should be obvious what to do when encountering a code depending upon
context ie when a Common Code appears in F it is applied to the
corresponding D; when it appears in G it applies to all of the dice in
all of the encapsulated tuples.

Also, the following codes are mutually exclusive within a tuple (ie only
one instance of each code can appear in each F) and within the guple
(within G):

  * The Explode Codes == {'!','h'}
  * The Keep Codes == {'dh','dl','k','kh','kl'}
  * The Sort Codes == {'a','s'}
  * The Match Codes == {'m','mt'}
  * The Reroll Codes == {'o','rr'}
  * The Target Number Codes == {'<','>','='} - as a Target Number, not
    as the compare point.
  * The Fail Codes == {'b','f}'

For completeness, we also have:

  * The Pool Code == {'p'}.
  * The Crit Code =={'c'}.
  * The Raise Code=={'r'}.
  * The Target Codes =={'Crit Code','Fail Codes','Raise Code','Target
    Number Codes'}
  * The Common Codes=={'Keep Codes','Sort Codes','Match Codes','Reroll
    Codes'}

QUESTION: How should we treat the Pool Code:

  * Appearing only in F (ie as an element of the Common Codes)?
  * Appearing only in G?
  * Appearing in both?
  * What does each of these mean in terms of a multi-tuple guple?

ACTION: We obviously need to change the Possible_Die-Code.md and
Die-Code_EBNF_Grammar_Definition.md documents. I'll do so once we agree
on what we're discussing.

Once we've settled upon the above (or whatever):

The UDR, or at least the back-end to the UDR (I think that's what you're
referring to as the API), is/should:

 1. Take a guple, and
     1. Manipulate the D-compart of the encapsulated tuple(s) according
        to the relevant Flags in the F-part of the tuple(s).
     2. Feed the resulting "raw" die-rolls into the FG-(die)engine.
     3. Explode where applicable, which means re-feeding into the FG-engine.
     4. Manipulate those die-roll-results according the relevant Flags.
     5. Manipulate the group-results according to the Flags in the
        G-part of the guple.
     6. Present the final manipulations in:
         1. The Chat Box.
         2. Any OOB constructed required (most likely as a simple
            integer or set of integers, as required by the Ruleset).
         3. Any further functions as required by individual Rulesets
            (most likely as a simple integer or set of integers, as
            required by the Ruleset).

The front-ends of the UDR, which obviously feed into the back-end,
should (each):

 1. Take a die code from the ChatBox.
 2. This obviously is the Parser/Lexer, either as a single
    fpParserLexer() or as a fpParser()-fpLexer() pair (or whatever).
 3. Take a Drag-&-Drop from the V-Dice to the ChatBox.
 4. Take a Drag-&-Drop from the V-Dice to the Tower.
 5. Take a Button/Diefield-Click from somewhere (eg Character Sheet).
 6. Take a Drag-&-Drop from elsewhere (eg Character Sheet) to the Chatbox.
 7. Take a Drag-&-Drop from elsewhere (eg Character Sheet) to the Tower.

All front-ends need to modify the guple by including any Modifier (the
value of the first Die-Mod Box) and including any Flags set by the
Die-Mod Boxes. If there is a conflict (ie the Flag is already set) then
the Flags from Die-Mod Boxes should be ignored (not the case with the
Die-Modifer ie the value from the first Die-Mod Box).

That's what I think the UDR should do. I also think the various parts of
the UDR should be created in the following order:

 1. Back-end
     1. Arithmetic operands ie Number-Expressions
     2. The Die-Code_D ie get 'NdX+MdY+Z' working/passed through to the
        FG-Engine, etc
     3. Explode Codes
     4. Sort Codes
     5. Keep Codes
     6. Reroll Codes
     7. Target Codes: Target Number Codes, Raise Code, Crit Code,Fail Codes
     8. Match Codes
 2. Die-Code Front-End
 3. V-Dice To Chatbox Front-End
 4. V-Dice To Tower Front-End
 5. Click Button/Diefield Front-End
 6. Sheet-Drag To Chatbox Front-End
 7. Sheet-Drag To Tower Front-End

Thoughts?

I think this will allow up to achieve our goals of building a really
solid UDR, and also allow us to do the UDRF easier as well, plus make
the it relative easy to hard-code character sheets, etc.

If we get an agreement then I'll write up new versions of the Grammar
and Die-Code documents, which together will form our "spec?" - and if
there is any further discrepancy then we can discus it and resolve it.

Again, thoughts, yay, nay?

(I'd like everyone to give us their input on this, & not just leave it
to Ben and I - even if its just a "Looks good/yay".)

On 03/09/2020 19:28, Ben Kolera wrote:
Good that we agree on the targets and successes being a top level
construct. It's really the only thing that makes sense. Awesome.

1st Issue
There is a critical flaw in your mental model of ebnf and LL(1)
parsers. I'll have a think about how I can reword things and make the
examples more concrete than my last email. You're not properly taking
into account the ambiguities that arise in such grammars around
optional elements and you're thinking like a human rather than as a
computer. :) We'll have to shelve the discussion about modifiers till
we can reconcile this or till we ditch the idea of doing arithmetic
(at least in the DieSides position).

2nd Issue
Wouldn't we be better off simple coding that into the Ruleset?
Yes, probably, but we'd also be better off not implementing arithmetic
in dice codes if we're happy doing stuff in the ruleset too. We don't
need to implement crits (they are pretty meaningless unless
interpreted by the ruleset anyway so the ruleset may as well do all of
that interpretation) or any arithmetic because that's all trivially
done by the ruleset too. The only time arithmetic would be actually
necessary to implement in the UDR is if we had some crazy system where
numbers of dice/sides/keep/targets/successes were random and dependent
on rolls of dice (roll20 doesn't do this, in fact, it's pretty garbage
and easy to confuse). What's the thought process for some things being
in and some things being out (other than roll20 does the ones
currently in our docs)?

To me, it feels like we're chasing features in the UDR before thinking
properly about the api between the UDR and the rulesets. Instead of
chasing maximal roller functionality, I instead propose that we get it
at feature parity to what we use in our games and start playing around
with how the ruleset on top will interact with the UDR as a proper
API. I think that if we start making that API more concrete we'll have
a better idea of where to draw the line of what the UDR needs to do
and what can be the ruleset's responsibility. The die code rolls are
only a part of the API and we should really consider all of it when
defining UDR scope.

----

Okay, lets have a try at this EBNF / LL(1) example. I'm going to
simplify the grammar so that we can walk through a parser:

DieCode = NumberExpr, "d", "NumberExpr", ["!"], [GlobalModifier]
GlobalModifier = ("+"|"-"), Number
NumberExpr = Number, ["+", NumberExpr]
Number = { Digit }
Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;

So with an EBNF written grammar (otherwise we should not use ebnf!)
we're explicitly talking about LL(1) grammars/parsers [1] where all
the context that the parser has in any given rule is the leftmost
character. A parser at any given point has one or more rules to try
and match that character to. All it can do is give a thumbs up to that
character and move onto the next step (or the end of the rule and
returning the result) or error.

You're right in thinking that with 5d12!+5kt7s5, the global modifier
gets into the right place because the exploding code disambiguates
things. But, that code is optional!

Lets walk through what happens with 5d5+5.

1. We start at DieCode. Our only rule that we can match is NumberExpr.
Recurse into NumberExpr
2. At number expression, our first rule is Number, so let's go there.
3. This has to match 1 or more digits, so lets recurse
4. We have a set of possible rules, so let's go through in order, we
try 0,1,2,3,4 and eventually match 5. This is a primitive, awesome!
Return "5".
5. We're back in Number, but when we try getting another digit that
parser fails so we're done with the number. Return 5.
6. We're back in NumberExpr, and the next character is "d" not "+" but
that's okay because that rule was optional. We return 5.
7. We're back in DieCode. Our current char is "d" and our only rule to
match is "d". Phew!, Move onto another number expression.
8. Now we have another number to match, recurse, then recurse down to match 
5.
9. We now have a plus to match and we're in Number expression with an
optional "+" token, okay, lets go!
10. We then match another 5 and return to die code.
11. We don't have any characters left, so we ignore the optional "!"
and Global modifier tokens.
12. Return (5)d(5+5)

There's nothing in ebnf that allows for negative look aheads to say an
ebnf rule like: Give me an optional "+",NumberExpr so long as it isn't
followed by any of the tokens following our number expression

Because this is a losing game. Even if we can invent a negative
lookahead type thing and not actually write ebnf anymore, we'd have to
make (even in this simple grammar) a rule like:
NumberExpr = Number, ["+",NumberExpr,~("!"|EOF)]

But then we add in keep to our grammar, and we have to remember to add
"k" to that set. Then we add crits, rerolls, etc and have to do it all
over again. It also means that our NumberExpression is not modular
anymore. Let's go through the thought process of adding a negative
lookahead in with keep and target and successes:

DieCode = NumberExpr, "d", NumberExpr, ["!"],[Keep][Target][Success]
Keep = "k", NumberExpr
Target = "t", NumberExpr
Success = "s", NumberExpr
NumberExpr = Number, ["+",NumberExpr,~("!"|"k"|"t"|"s"|EOF)]

Then we yell and scream at wtf "5d12!k1+2" "5d12!k1+2t" etc are all
failing to parse. Then we have to make a special NumberExpression
specially for DieSides and we die on the inside a little and wonder
why we didn't just write the global modifier as a lexically distinct
token from a continuing number expression so we don't have to hack
these lookaheads in (like I suggested).

This is why the existing lexer has so many super gross and confusing
look aheads in it (like this:
https://github.com/Dulux-Oz/FGI/blob/master/DORCoreMin/UDR/Scripts/lsUDRDieManager.lua#L778).
It took me so long to properly understand the existing parser because
we just played the negative lookahead whack-a-mole game instead of
refining the grammar. And it does lots of silly and unexpected things
even though it tries to do parens and addition.

/die (5)d(10)
<parser fails>
/die 5d(5+5)
Rolls no /rdice and returns a flat 10
/die 5d6+5
Parses that as a global modifier (so it's negative lookahead for the
modifier works)
/die 5d6!kt7+5s5
Parses this as (5d6!kt7s5)+5

These lookaheads and the behaviour of the last result is what I mean
when I say that it is *way* too much work to get the current thing to
the full grammar as wanted. Teasing apart the ball of mud of these
very weird anti-modular lookaheads but also having some other parsing
rules be completely context-less to what has been prior is just not a
good thing.

Hopefully this clears things up!

[1] https://en.wikipedia.org/wiki/LL_grammar


On Thu, 3 Sep 2020 at 14:25, duluxoz <duluxoz@xxxxxxxxx> wrote:
Authoritative Source:
It's up to us, really, as the Project Doco is subject to
improvement/updates just like everything else. The UDR Grammar document
was written for the first draft of the UDR, and I don't think I updated
it when I updated the  Possible Die Codes document and set up the
Project - in fact, I'm sure I didn't.

So, of the two, the Possible Die Codes document is more up-to-date, so
that's the one *I'd* be using as Authoritative - and so the Grammar
document, which was used as a guide for the first Parser/Lexer, should
be updated so that it matches what's being done in the new Parser/Lexer.

So follow the Possible Die Codes and we'll update the Grammar - this
should resolve a lot of the issues @Ben raised - plus there's some
notes/thoughts below.

5d12!kt7s5+2d4!kt3s5:

I don't thinks this is legal. Target numbers (& therefore successes) 
should only apply to final rolls, not sub rolls, and thus should be the 
last set of die-code-elements, and so we should return this as an error 
(as we should do with all "illegal rolls"). It's a bit of a contrived 
example (although a good one, because it highlights the issue) because I 
can't think of *any* game system where you'd want a roll like that - but 
please chip in people and tell me I'm wrong.

A similar (legal) roll, without any ambiguity, would be:

({5d12!k}+{2d4!k})t3s5 = 5d12!k+2d4!kt3s5

Anything in the Die Boxes would/should apply to the entire roll - there's 
no other way to treat it without going insane - and yes, that means that 
at the moment we can't set up a Deadlands Classic Weapon Melee Roll 
(DLCWMR: eg 3d10!k+2d6!t5s5) because there's no way to specify that a Keep 
Flag should only apply to one of the die-sets. - at the moment if you set 
up the Die Boxes to Explode, Keep=5, Success=5, and then dropped 3d10 & 
2d6 (or put in /roll 3d10+2d6) what you'd get would be:

(3d10!+2d6!)kt5s5

How do we fix this? I'm not sure, but a DLCWMR is the most complex roll 
I've ever come across, so maybe we don't need to, as DLCMWRs should be 
coded into the Character Sheet.

I think the best way to handle things (via dice code) is to "error" 
anything that is ambiguous.

Happy to have a more detailed (group) discussion around this and any other 
complex rolls people know about.

1st Issue:

Dice should have precedence over addition, multiplication, etc, so d5+5 = 
(d5)+5, and if we want d(5+5) then that's what we need to write. I thought 
I'd implied that in the Precedence section of the one of the docos, but I 
mustn't have had. a "d" should go right after "{}" and "()" in precedence 
order.

We don't need and (extra) syntax because we already have it ie "()".

The second example also sort of "goes away" because of the statement 
above: "Target numbers (& therefore successes) should only apply to final 
rolls... and thus should be the last set of die-code-elements". 
5d12!kt7s5+2 (without the "|") should be interpreted as 5d12!kt7s(5+2) - 
if we want to add a modifier to the roll it need to go *before* the "k", 
etc (or use "()"):

5d12+2!kt7s5 or 5d12!+2kt7s5

I think a good "rule" is to enforce all die rolls and modifiers come 
before all keeps, crits, target numbers, etc - thoughts everyone?

2nd Issue:

If you bust the strength part of a DLCWMR then the whole roll is a bust 
(you're not strong enough to do any damage - you've got to do some damage 
with the strength part to get any damage from the weapon part - & you 
can't go bust on a non-strength-part damage roll).

Do we need an extra die code for this? All rolls is Deadlands which we 
"k1" also (have the potential to) bust - at least as I can recall (jump in 
guys and let me know if I'm wrong). Wouldn't we be better off simple 
coding that into the Ruleset?

However, if we do need a new code then let's use "b" by itself; we don't 
need "bN" because N is a quick calculation based off of MdX ie 
N=ceil(M/2+0.5).

Open Qs:

As I said, the Grammar was to help me to the initial Parser/Lexer. I'm OK 
if we don't update it, but I like we should (as it should form part of the 
final doco). And I have no problem with it being a hybrid- or two-stage 
grammar (if that's what's required).


On 02/09/2020 09:26, Ben Kolera wrote:

Heya,

What's the authoritative source of what we want to do with the UDR? I
ask because the Grammar [1] and the Possible die codes [2] disagree.
Are we intending to keep the grammar up to date (the Grammar already
doesn't line up to develop) and correct as we go (like actually make
it proper ebnf, lol) or will the code and tests become the doco and
examples of the die codes that we support?

Possible_Die-Codes.md suggests this an example:

{4d8+3d12,5d20+3,5d10+1}>20

But if you look at the grammar the only things that can be part of
expressions are number literals, so the above case is not something
that is allowed by the grammar.

Taking a step back from all the documentation though, as users of our
two current games we want to be able to do:

3d6!+5d10!k  (This lines up to a sabre damage roll with the strength
and weapon damage smooshed into one).

The current rewrite went down the route of treating DicePools as the
whole set of {NumDice,Sides,Exploding,Keep,Target,Raises} (it doesn't
do rerolls or crits yet) and it works out poorly when trying to add
these things together. I mean, what should the result of

5d12!kt7s5+2d4!kt3s5

be? Is it just adding the successes of both groups together? Is the
entire roll a success or a failure if one reaches its target and the
other fails? If you have a modifier in the modifier box in the UI,
does it apply to both groups or does it add extra successes to the
final result? It is really messy and suggests that this needs more
thought and precision.

I don't think that these have any clear answers in the context of our
game (and maybe not clear answers in general), so I'd prefer it if we
disallowed adding anything that has a target/raises from the grammar
but while still allowing the sabre roll that would be handy for
deadlands.

What I suggest is that the Target and Raises syntax can only apply at
the top level something like this (I've ignored functions and
Multiplication and Exponents here because I don't want to write out
the nested precedence, lol):

DieCode = DiceGroupExpr,[GlobalModifier],[Target],[Crit],[SortCode]
GlobalModifier=("+"|"-"), NumberLit
Target=("t"|"<"|">"),NumberExpr,[("s"|"r"),NumberExpr]
SortCode= "s", ("a" | "d")
DiceGroupExpr=DiceGroup,["+",DiceGroupExpr]
DiceGroup= DicePool | DicePools
DicePools = "{", { DicePool }, "}"
DicePool = [NumberExpr], "d", DieSides, [Exploding], [Keep], [Reroll]
<snip>

This seem to work in my head except for two issues:

1st Issue:
The global modifier is ambiguous! In spirit, it's supposed to handle
cases like these: "5d12!k+2t7s5" and "d100-60" but because we allow
arbitrary numeric expression in the diesides there's nothing in the
grammar that distinguishes "5d(5+5)" from "(5d5)+5". We need to
disambiguate this somehow. Some ideas:
    - Always forcing dice pools into a grouping even if single pooled
(kinda ugly) "{5d5}+5"
    - Putting some syntactic distinguisher before the global modifier
like "5d5|+5". Kinda looks weird with keep dice though "5d12!kt7s5|+2"
(is that clear that it'll add the +2 before doing the target number
calc? "5d12!k|+5t7s5" just looks weird so if we thought the ordering
is important we may need a better distinguisher than "|")

2nd Issue:
I want to add some syntax to the grammar that marks a pool with the
fact that it will bust if there are a majority of 1s in the pool.
However, this would have to go on the dicepools and with the proposed
grammar above that's still part of arithmetic so we need to figure out
what it means to do arithmetic on potentially busted pools is (5 +
bust = 5, or 5 + bust = 5?). I think that it's good to ground this in
deadlands. What happens if you bust a strength check on a weapon
damage roll? Do you still get the weapon damage or do you do a big fat
0 damage? Or do strength checks for hand to hand damage not bust? My
intuition is that a bust is an annihilator[3] (i.e X + bust = bust and
bust + X = bust, func(bust) = bust) that's akin to the expression
throwing an exception.

--- end issues ----

This is the problem with striving towards the kitchen sink! Trying to
figure out ways to make sure all of the features work precisely when
combined becomes harder. Being perfectionistic and wanting a kitchen
sink but being imprecise and doing funky things is a very sad state to
be in and I absolutely don't want the UDR ending up that way on my
watch. Precision is key with such a core piece of things, so we either
have something simple that covers our usecases in our games precisely
or we have to go through these thought exercises trying to hammer out
precisely what we mean and want in all these cases that we don't have
a concrete use case for.

---

Summarising, the proposed action points for this email are:
- Targets & Raises at top level only
- Need to distinguish the global modifier syntactically. Just going to
roll with a code terminating "|",("+","-"),NumberLit for now but we
can change that later if we want
- Crits go on the top level (they are basically a second target number)
- Busts go on the pool and (X <opr> bust = bust, bust <opr> X = bust
and func(bust) = bust).

Open Questions are:
- Are we planning to output a proper EBNF at the end of the UDR work
or is a test suite and the way that the parser is defined enough (the
nice thing about LPeg style parsers is that they read a lot like ebnf
anyway)? My vote would be that the parser & tests will be enough and
are better because they are machine checked. If we were generating a
parser from the ebnf this would be different, but we aren't so the
docs have the great risk of getting stale (the current ones are stale
and also contain a handful of errors).

I won't be doing any work on UDR today but when I get back to this
probably tomorrow or Friday I won't be blocked if folk haven't had the
time for a response yet so dw. We need to move in the direction of the
proposals anyway (unless we're cutting features, ofc!) so it feels
like the main things we'll need to discuss here are finer bikeshedding
type stuff anyway. I can just move as though those action points are
decided and I can adjust as we discuss the colour of the bikeshed. :)

Cheers,
Ben

[1] 
https://github.com/Dulux-Oz/FGI/blob/master/Support_Files/Die-Code_EBNF_Grammar_Definition.md
[2] 
https://github.com/Dulux-Oz/FGI/blob/master/Support_Files/Possible_Die-Codes.md
[3] https://en.wikipedia.org/wiki/Annihilator_(ring_theory)

--
Peregrine IT Signature

*Matthew J BLACK*
    M.Inf.Tech.(Data Comms)
    MBA
    B.Sc.
    MACS (Snr), CP, IP3P

When you want it done /right/ ‒ the first time!

Phone:  +61 4 0411 0089
Email:  matthew@xxxxxxxxxxxxxxx <mailto:matthew@xxxxxxxxxxxxxxx>
Web:    www.peregrineit.net <http://www.peregrineit.net>

View Matthew J BLACK's profile on LinkedIn
<http://au.linkedin.com/in/mjblack>

This Email is intended only for the addressee.  Its use is limited to
that intended by the author at the time and it is not to be distributed
without the author’s consent.  You must not use or disclose the contents
of this Email, or add the sender’s Email address to any database, list
or mailing list unless you are expressly authorised to do so.  Unless
otherwise stated, Peregrine I.T. Pty Ltd accepts no liability for the
contents of this Email except where subsequently confirmed in
writing.  The opinions expressed in this Email are those of the author
and do not necessarily represent the views of Peregrine I.T. Pty
Ltd.  This Email is confidential and may be subject to a claim of legal
privilege.

If you have received this Email in error, please notify the author and
delete this message immediately.




--
Peregrine IT Signature

*Matthew J BLACK*
   M.Inf.Tech.(Data Comms)
   MBA
   B.Sc.
   MACS (Snr), CP, IP3P

When you want it done /right/ ‒ the first time!

Phone:  +61 4 0411 0089
Email:  matthew@xxxxxxxxxxxxxxx <mailto:matthew@xxxxxxxxxxxxxxx>
Web:    www.peregrineit.net <http://www.peregrineit.net>

View Matthew J BLACK's profile on LinkedIn
<http://au.linkedin.com/in/mjblack>

This Email is intended only for the addressee.  Its use is limited to
that intended by the author at the time and it is not to be distributed
without the author’s consent.  You must not use or disclose the contents
of this Email, or add the sender’s Email address to any database, list
or mailing list unless you are expressly authorised to do so.  Unless
otherwise stated, Peregrine I.T. Pty Ltd accepts no liability for the
contents of this Email except where subsequently confirmed in
writing.  The opinions expressed in this Email are those of the author
and do not necessarily represent the views of Peregrine I.T. Pty
Ltd.  This Email is confidential and may be subject to a claim of legal
privilege.

If you have received this Email in error, please notify the author and
delete this message immediately.





Other related posts: