Spin2 Interpreter

124678

Comments

  • Evanh, here is that routine now:
    '
    '
    ' CLKSET(clkmode,clkfreq)
    '
    clkset_		mov	w,x			'get freq into w
    
    		setq	#2-1			'get stack top into x
    		rdlong	x,--ptra[2]		'get mode into y
    
    clkset_init	rdlong	z,#@clkmode_hub		'get current clkmode to avoid PPPP = %1111 clock glitch
    		andn	z,#%11			'switch to 20MHz while preserving current pll and crystal settings
    		hubset	z
    
    		test	y,#%10		wz	'if xtal or pll then switch to 20MHz for ~10ms
    	if_nz	mov	z,y			'..while new pll and crystal settings take effect
    	if_nz	andn	z,#%11
    	if_nz	hubset	z
    	if_nz	waitx	##20_000_000/100
    
    		hubset	y			'set final mode
    
    		wrlong	w,#@clkfreq_hub		'update clkfreq
    	_ret_	wrlong	y,#@clkmode_hub		'update clkmode
    
  • Nice, looks spot on.

    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • Here is the current Spin2 interpreter.

    It was challenging to work out all the details for multiple return values. It complicated the call and return process significantly, but will allow a reduction in user code complexity.

    I moved the call code to hub and now free registers range from 0..$15F, with $1E0..$1EF being local variable conduit for calls and in-line PASM, for which $1F0..$1FF are also available.

  • Ah, following that clkmode_hub reference I should point out there was a previous attempt to form a convention of what addresses to reserve for these parameters. See https://forums.parallax.com/discussion/comment/1466520/#Comment_1466520 and https://forums.parallax.com/discussion/comment/1466702/#Comment_1466702

    Here's my default source I place at the beginning now:
    DAT						'not Spin code
    ORG	0					'longword addressing at 0
    '******************************************************************************
    '  COG Variables  (Auto-copied to cogram on COGINIT #%0x_xxxx,#0 after program load)
    '******************************************************************************
    msec		jmp     #_diaginit
    
    
    FIT	4	'DON'T TOUCH THIS! - more than four preceding variables stops assembly with error
    ORGF	4	'DON'T TOUCH THIS! - fills unused memory to align with fixed reserved parameters
    '--------------------------------------------------------
    '***  Boot-loader can fill all four of the following  ***
    '--------------------------------------------------------
    spare1		long	0			'hubRAM addr $010 - unused reserved parameter
    clk_freq	long	22_000_000		'hubRAM addr $014 - sysclock frequency, integer frequency in hertz
    clk_mode	long	0			'hubRAM addr $018 - clock mode config word, used directly in HUBSET
    asyn_baud	long	115_200			'hubRAM addr $01c - comport baud rate, integer baud in hertz
    '--------------------------------------------------------
    
    
    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • cgraceycgracey Posts: 11,486
    edited 2019-07-26 - 11:19:47
    The interpreter has everything it needs now, minus the special smart pin instructions which will each be easily added with a word vector, its hub program code, and a table entry into the compiler. All the necessities and infrastructure are in place.

    On the next silicon, all the instructions which affect the pins use bits 10..6 of the pin number to add extra pins onto the operation (up to 31, for 32 total). I changed PINW(pin, value) to work this way, so that any set of states can be output to a contiguous group of pins on a 32-bit port (OUTA or OUTB). Also PINR(pin) returns 1..32 bits of data, depending on bits 10..6 of 'pin'. So, all pin operations are 1..32 pins in size, any offset, but cannot cross 32-bit ports. Instead, they wrap around within the base pin's port. That's what the PASM pin instructions do, as well.

    Hub long[0] is 'clkfreq', hub long[1] is 'clkmode' and hub longs[2..15] are free for the user at runtime, though they stored the startup code on boot. This means hub $00008..$0003F is free for users.

    Cog registers $000..$162 are free for user programs. You can load callable PASM programs into that memory or have code which runs from interrupts in the background, or some combination. This is going to allow a single cog to do a lot of things, like maintain a whole raft of PWM outputs with data from an array in Spin. That memory also gets used by inline PASM code within Spin methods.

    The interpreter is now 3,784 bytes or 946 longs. With the smart pin command additions it will hit 4KB. So, $1000..$7FFFF will be available for user program and data.

    In the morning, I will get back to work on the compiler, so I can finally get this thing breathing sometime soon.
  • cgracey wrote: »
    Hub long[0] is 'clkfreq', hub long[1] is 'clkmode' and hub longs[2..15] are free for the user at runtime, though they stored the startup code on boot. This means hub $00008..$0003F is free for user use.
    Could we change that to hub long[$18] for 'clkmode' and hub long[$14] for 'clkfreq'? Those are what Tachyon, p2gcc, micropython, and fastspin are using.

    One advantage is that those values can be modified by boot loaders while still allowing a few instructions to execute at address 0 (e.g. coginit).

    Another is just plain compatibility with Tachyon (the originator of the standard) and the other programs which have adopted it.

    Regards,
    Eric

  • Fantastic news, Chip.
    cgracey wrote: »

    In the morning, I will get back to work on the compiler, so I can finally get this thing breathing sometime soon.

    Hopefully you might find a moment for "breathing" too, between these monster tasks. The weekend is upon us!

  • ersmith wrote: »
    cgracey wrote: »
    Hub long[0] is 'clkfreq', hub long[1] is 'clkmode' and hub longs[2..15] are free for the user at runtime, though they stored the startup code on boot. This means hub $00008..$0003F is free for user use.
    Could we change that to hub long[$18] for 'clkmode' and hub long[$14] for 'clkfreq'? Those are what Tachyon, p2gcc, micropython, and fastspin are using.

    One advantage is that those values can be modified by boot loaders while still allowing a few instructions to execute at address 0 (e.g. coginit).

    Another is just plain compatibility with Tachyon (the originator of the standard) and the other programs which have adopted it.

    Regards,
    Eric

    I was actually thinking of moving them to $40 and $44 to leave a clean 64 bytes of hub space for the user.
  • cgracey wrote: »
    ersmith wrote: »
    cgracey wrote: »
    Hub long[0] is 'clkfreq', hub long[1] is 'clkmode' and hub longs[2..15] are free for the user at runtime, though they stored the startup code on boot. This means hub $00008..$0003F is free for user use.
    Could we change that to hub long[$18] for 'clkmode' and hub long[$14] for 'clkfreq'? Those are what Tachyon, p2gcc, micropython, and fastspin are using.

    One advantage is that those values can be modified by boot loaders while still allowing a few instructions to execute at address 0 (e.g. coginit).

    Another is just plain compatibility with Tachyon (the originator of the standard) and the other programs which have adopted it.

    Regards,
    Eric

    I was actually thinking of moving them to $40 and $44 to leave a clean 64 bytes of hub space for the user.

    I'm not sure there'd be much advantage to that -- the user's initial boot code (setting a flag and either jumping to or coginit'ing the main program) will easily fit in 16 bytes. 64 bytes isn't enough to do very much more than that.

    I would be tempted to set aside some additional space for future configuration needs, something like:
    $00-$0f: initial boot sequence
    $10-$1f: clock mode settings (ala Tachyon)
    $20-$3f: reserved for future use
    
  • One JMP as first instruction is it really. Nothing more nothing less. The three longwords after that can be the future reserved space.
    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • evanh wrote: »
    One JMP as first instruction is it really. Nothing more nothing less. The three longwords after that can be the future reserved space.

    I had it set up that way, but then changed it to save a long. It worked out that way in my code.
  • evanh wrote: »
    One JMP as first instruction is it really. Nothing more nothing less. The three longwords after that can be the future reserved space.

    The JMP is all that's strictly required, but it's often convenient to also set a flag that says "first boot". Alternately, a SETQ + COGINIT is a good way to restart the COG with a parameter.

    Anyway, a lot of programs are already using the Tachyon convention ($14 and $18). It may not be perfect, but it's good enough (there is some space at the beginning for boot code) and it works. I think we might as well keep it.
  • ersmith wrote: »
    evanh wrote: »
    One JMP as first instruction is it really. Nothing more nothing less. The three longwords after that can be the future reserved space.

    The JMP is all that's strictly required, but it's often convenient to also set a flag that says "first boot". Alternately, a SETQ + COGINIT is a good way to restart the COG with a parameter.

    Anyway, a lot of programs are already using the Tachyon convention ($14 and $18). It may not be perfect, but it's good enough (there is some space at the beginning for boot code) and it works. I think we might as well keep it.

    You guys can keep it, but I want to do something different.

    I just changed it so that the booter code clears $00000..$0003F that it occupied in the hub and clkfreq/clkmode are at $00040/$00044. This way, Spin programs start out with the first 64 hub bytes initialized to $00 and available for use. This is nice for any mailboxes. I think it's better to negotiate that stuff at run-time, but there was some one-time-use boot code that would be a shame to leave sitting around forever.
  • Chip,
    It's only for handover. After handover init it can be moved or ignored/overwritten. The critical function is the interoperability of that handover.

    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • This is needed to be sorted now so as to ensure best compatibility with configuring the system PLL/crystal.
    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • cgracey wrote: »
    ersmith wrote: »
    evanh wrote: »
    One JMP as first instruction is it really. Nothing more nothing less. The three longwords after that can be the future reserved space.

    The JMP is all that's strictly required, but it's often convenient to also set a flag that says "first boot". Alternately, a SETQ + COGINIT is a good way to restart the COG with a parameter.

    Anyway, a lot of programs are already using the Tachyon convention ($14 and $18). It may not be perfect, but it's good enough (there is some space at the beginning for boot code) and it works. I think we might as well keep it.

    You guys can keep it, but I want to do something different.

    I just changed it so that the booter code clears $00000..$0003F that it occupied in the hub and clkfreq/clkmode are at $00040/$00044. This way, Spin programs start out with the first 64 hub bytes initialized to $00 and available for use. This is nice for any mailboxes. I think it's better to negotiate that stuff at run-time, but there was some one-time-use boot code that would be a shame to leave sitting around forever.

    Why, Chip? Seriously, how much advantage is there to this scheme? It may be aesthetically somewhat more pleasing, but practically it makes little difference whether the reserved area is $00-$3f or $20-$5f (and there's still the matter that any PASM program that starts at 0 needs to have a JMP or COGINIT there).

    Breaking compatibility with practically everything else can be done, but I would suggest that it needs to have a compelling advantage over the status quo, and I don't see that here.

    It would be a real shame to have two competing and non-interoperable standards :(. Our developer base is small enough as it is...
  • One-time-use boot code is going to be at the very start of memory. I don't think it will ever need to run twice. It might as well clean up after itself (setq+wrlong from cog) and open up a nice little buffer. And it's very convenient if the buffer starts at $00000 and it goes to some 2^n-1 address.
  • cgracey wrote: »
    One-time-use boot code is going to be at the very start of memory. I don't think it will ever need to run twice. It might as well clean up after itself (setq+wrlong from cog) and open up a nice little buffer. And it's very convenient if the buffer starts at $00000 and it goes to some 2^n-1 address.

    Sorry to keep pestering about this, but what makes $00000 more convenient than $00010 or $00040? It's slightly easier to remember I suppose, but anyone messing around with direct HUB RAM access rather than traditional Spin variables should already be capable of looking at a memory map (and they'll have to do that anyway in order to know how much space they can use).
  • Since the current clock settings are needed to switch the clock again it would be very convenient if Spin keeps them at the same place ALL other languages for the P2 do.

    Hell it is hard enough to get P2-tools developer to agree on anything, but at least they agreed on this (and maybe some .h files for C)

    So PLEASE Chip agree too.

    Mike
    I am just another Code Monkey.
    A determined coder can write COBOL programs in any language. -- Author unknown.
    Press any key to continue, any other key to quit

    The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this post are to be interpreted as described in RFC 2119.
  • jmgjmg Posts: 13,775
    ersmith wrote: »
    Anyway, a lot of programs are already using the Tachyon convention ($14 and $18). It may not be perfect, but it's good enough (there is some space at the beginning for boot code) and it works. I think we might as well keep it.

    I think Tachyon does this in ROM, so that convention is cast in stone, which everyone else has followed ?
    Given the PLL is both a bit temperamental, and very flexible, it certainly is important to have ALL languages use a common convention, for CLOCK and MODE defines, as users WILL be mixing languages on P2.

    msrobots wrote: »
    Since the current clock settings are needed to switch the clock again it would be very convenient if Spin keeps them at the same place ALL other languages for the P2 do.
    Yup, agreed.

  • jmgjmg Posts: 13,775
    cgracey wrote: »
    Cog registers $000..$162 are free for user programs. You can load callable PASM programs into that memory or have code which runs from interrupts in the background, or some combination. This is going to allow a single cog to do a lot of things, like maintain a whole raft of PWM outputs with data from an array in Spin. That memory also gets used by inline PASM code within Spin methods.
    What about LUT memory ? Is that used by spin, or free for users ?

  • $00000 is better if you want to have a fast access table in HUB, right?
    No need to add in an offset or use ptrx...
    Prop Info and Apps: http://www.rayslogic.com/
  • cgracey wrote: »
    One-time-use boot code is going to be at the very start of memory. I don't think it will ever need to run twice. It might as well clean up after itself (setq+wrlong from cog) and open up a nice little buffer. And it's very convenient if the buffer starts at $00000 and it goes to some 2^n-1 address.
    There's no conflict with that idea. These special reserved locations are really only reserved for the boot-up code. After they've served the parameter passing purpose you don't have to keep them reserved.

    But they do need to be agreed upon. The earlier, the better.

    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • roglohrogloh Posts: 1,145
    edited 2019-07-27 - 01:47:38
    Hi Chip,

    Just noticed this line below in your interpreter. Does the fixed COG nature of this restart imply that if you ever want SPIN in your applications that you need to always start up in a SPIN operating mode from the very beginning?
    		setq	dbase_init	'restart cog 0 with interpreter
    		coginit	#$20,##launch_spin
    
    If you were to make it such that the COG selected for restart was just the current COG rather than fixed to COG 0, would that then allow SPIN to be started dynamically at runtime from another already booted (non-SPIN) environment? I'm sort of assuming here that there would already sufficient space set aside for the SPIN interpreter in hub memory at the regular location it would want to use (looks like it will be from 0-4k).

    The reason I ask this question is that imagine a situation where some other environment like Micropython or TAQOZ or something else might be running and the user wants to dynamically spawn some object as a SPIN executable "module" based on external input. It would be handy to be able to do this dynamically if it were possible somehow, rather than have to lock it in at compile time.

    There could be other ways to achieve this same result with SPIN always booting up first but they may require keeping a SPIN COG allocated and waiting for this request to run some other SPIN based COG.

    Was just wondering...

    Roger.


    UPDATE: Looking at it further, perhaps what I was thinking is still achievable if we simply do some of the VAR and stack initialisation currently done before the restart happens and jump directly to ##launch_spin ourselves...? The clock init stuff would likely have occurred anyway. Perhaps if the booter code just used a parameter that could tell whether this was the first boot or a later invocation that didn't necessarily need the clock adjusted it could be cleaner though and potentially more universal I guess.
  • roglohrogloh Posts: 1,145
    edited 2019-07-27 - 05:19:03
    Related to my post above another idea came to mind, Chip...

    If/when the SPIN COG's main routine ever finally "returns" and its stack points to the "stopcog" bytecode sequence that currently does a COGID and COGSTOP, it would be neat to be able to intercept this default behavior before the COG shuts down so we could return the SPIN method's "result" value back to the caller that invoked the SPIN object (with data left on the stack as the return or "exit" value for example). I am imagining this could enable other languages to start up SPIN code synchronously using the same COG that the caller uses and when the SPIN routine completes we get a chance to run special code that could restore the caller's COG state afterwards so it could continue on unaware that it lost its COG temporarily while it was running SPIN. Of course if more free COGs were available it could also just spawn a different COG for SPIN and optionally poll/wait for any result to be saved off somewhere before the SPIN COG completes and shuts down. Both methods are useful, but the synchronous one is nice to save a COG when things get tight and you don't want to worry about sitting around polling for results etc particularly if you have nothing else to do with the caller COG anyway and could potentiully allocate this COG to run the SPIN code, except that right now when it completes it shuts down and you wouldn't be able to continue.

    So we can just think of this like a program in one language/environment calling a SPIN function with all its setup data as arguments on the stack and then getting an optional result back to the caller before continuing on. We just need the ability to intercept the usual SPIN exit sequence for this to occur, so if there is enough space for storing a jump vector that could optionally jump elsewhere before that happens this would be good. Again I imagine we could patch the launch_spin code to return somewhere other than "stopcog" if this shutdown method is not officially supported by the interpreter on day one, but it would be nice if this flexibility was sort of baked in from the very beginning and might let SPIN interoperate with other languages in a standardised way rather than have lots of people try their own approach that may not be compatible with everyone else's solution. It may be better to have this exit routine address passed in by the compiler/tools/caller, so each type of caller can customize the COG exit/shutdown sequence appropriately to their needs. Eg, you could patch this object's arguments to exit to your custom C caller code, or to a custom Forth caller code or Python etc, doing whatever the caller language needs to collect it's results and optionally restore its state and continue. Basically then the same SPIN interpreter and COG code format could be used by all languages, but you just pass the exit routine address at startup time for the type of caller if you don't want the default COG shutdown behavior to occur first. Maybe this return address could be one of the first arguments passed on the stack, like you have already for pbase and vbase.

    Do you think this type of thing is possible/desirable? I haven't really considered all the ramifications here, and don't know whether you'd need to exit back to byte code or native hub exec code etc.
  • cgraceycgracey Posts: 11,486
    edited 2019-07-27 - 06:29:37
    rogloh wrote: »
    Related to my post above another idea came to mind, Chip...

    If/when the SPIN COG's main routine ever finally "returns" and its stack points to the "stopcog" bytecode sequence that currently does a COGID and COGSTOP, it would be neat to be able to intercept this default behavior before the COG shuts down so we could return the SPIN method's "result" value back to the caller that invoked the SPIN object (with data left on the stack as the return or "exit" value for example). I am imagining this could enable other languages to start up SPIN code synchronously using the same COG that the caller uses and when the SPIN routine completes we get a chance to run special code that could restore the caller's COG state afterwards so it could continue on unaware that it lost its COG temporarily while it was running SPIN. Of course if more free COGs were available it could also just spawn a different COG for SPIN and optionally poll/wait for any result to be saved off somewhere before the SPIN COG completes and shuts down. Both methods are useful, but the synchronous one is nice to save a COG when things get tight and you don't want to worry about sitting around polling for results etc particularly if you have nothing else to do with the caller COG anyway and could potentiully allocate this COG to run the SPIN code, except that right now when it completes it shuts down and you wouldn't be able to continue.

    So we can just think of this like a program in one language/environment calling a SPIN function with all its setup data as arguments on the stack and then getting an optional result back to the caller before continuing on. We just need the ability to intercept the usual SPIN exit sequence for this to occur, so if there is enough space for storing a jump vector that could optionally jump elsewhere before that happens this would be good. Again I imagine we could patch the launch_spin code to return somewhere other than "stopcog" if this shutdown method is not officially supported by the interpreter on day one, but it would be nice if this flexibility was sort of baked in from the very beginning and might let SPIN interoperate with other languages in a standardised way rather than have lots of people try their own approach that may not be compatible with everyone else's solution. It may be better to have this exit routine address passed in by the compiler/tools/caller, so each type of caller can customize the COG exit/shutdown sequence appropriately to their needs. Eg, you could patch this object's arguments to exit to your custom C caller code, or to a custom Forth caller code or Python etc, doing whatever the caller language needs to collect it's results and optionally restore its state and continue. Basically then the same SPIN interpreter and COG code format could be used by all languages, but you just pass the exit routine address at startup time for the type of caller if you don't want the default COG shutdown behavior to occur first. Maybe this return address could be one of the first arguments passed on the stack, like you have already for pbase and vbase.

    Do you think this type of thing is possible/desirable? I haven't really considered all the ramifications here, and don't know whether you'd need to exit back to byte code or native hub exec code etc.

    Rogloh, we could certainly make Spin language interoperability work like that. It never ocurred to me it could be done so easily. The cog just becomes fully available to the language system being called. It wouldn't even need to be restarted and the I/O registers could maintain their states.
  • roglohrogloh Posts: 1,145
    edited 2019-07-27 - 08:52:42
    Yeah it could be very handy. I was thinking of cases where you might have some high level software tool code written in SPIN2 that could be called from other environments. Like a compiler or disassembler function in SPIN2 that could write its output to a block of memory passed to it and a "status" return code etc. Lots of applications.

    Initially I thought it gets a bit trickier with IO but you may not need IO in every case anyway and as you say you could just restore the existing COG memory state without even restarting it which could preserve the IO, perhaps in a two step process involving LUT RAM. I definitely think it is worth some consideration. I'd like to think of SPIN2 also as a callable language and not necessarily as an overall environment that has to run first and control the entire P2's resources as such. It just needs the ability to differentiate between the initial boot if/when SPIN2 is the startup code, vs some later invocation by another language, as well as a way to return results and optionally giving control back to the original calling COG instead of full COG shutdown. Using its allocated stack for these arguments/return codes probably makes the most sense, and seems the most conventional.

    Of course this whole idea benefits most if the tools generating these SPIN object's runtime code can make it portable/relocatable, so that SPIN objects can be brought into RAM and called dynamically at arbitrary memory addresses in hub RAM and this means relative jumps mostly need to be used etc. Hopefully only the SPIN interpreter itself needs to have the fixed hub addresses preserved from 0-4kB, but the rest of the shareable SPIN code blocks can be generated and called in a way that essentially makes them location independent and dynamically loadable/callable. Having such a relocatable capability also helps support chaining etc where you can continually and dynamically load and execute SPIN code from external memory like Flash on demand as you need to call it, increasing the effective use of the available memory.
  • evanhevanh Posts: 7,502
    edited 2019-07-27 - 10:52:50
    Chip,
    I hope you are pondering what addresses for everyone to settle on for clock setting parameter passing. It can't be an each-to-their-own type situation because of the hardware flaw. Everyone has to be on the same page this time.

    Keep in mind that these addresses can happily be reused for other purposes after boot time. They don't have to be permanently reserved during run time. They just have to be post compile writeable addresses that the boot-loader or OS can fill at load time.

    "... peers into the actual workings of a quantum jump for the first time. The results
    reveal a surprising finding that contradicts Danish physicist Niels Bohr's established view
    —the jumps are neither abrupt nor as random as previously thought."
  • Great idea Roger. Love the ability to have any program be able to call spin for some desired code execution, and then have spin return to the calling program :smiley:
    Particularly, this allows small but slow code blocks to run when time is not a requirement. Then it would be easy to write little spin routines (objects) and if speed is a requirement then that particular object could be redone in pasm.
    My Prop boards: P8XBlade2, RamBlade, CpuBlade, TriBlade
    Prop OS (also see Sphinx, PropDos, PropCmd, Spinix)
    Website: www.clusos.com
    Prop Tools (Index) , Emulators (Index) , ZiCog (Z80)
  • Slowly this ship turns in the right direction. @ersmith is the fore runner, I fight with TAQOZ to get it cooperative (and finally @Peter Jakacki is helping me to do that, what a wonderful thing) Now getting Chip on board, YES the multi language P2.


    I am just another Code Monkey.
    A determined coder can write COBOL programs in any language. -- Author unknown.
    Press any key to continue, any other key to quit

    The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this post are to be interpreted as described in RFC 2119.
Sign In or Register to comment.