Class Formula
This is the authoritative representation of a formula. The text a user typed is a DISPLAY ARTEFACT only: it is regenerated from the opcodes by toSource() and nothing parses a stored string at runtime.
Code layout. The array is a flat opcode stream, see Op. The only instruction with an operand is Op.OP_CONST, which occupies two slots: the opcode followed by the index of its value in the parallel constant arrays. There is no constant pool and no de-duplication; the k-th OP_CONST in the stream refers to the k-th constant, which is what lets the wire format write constants inline in code order without an index byte.
Constants are never negative. A leading minus is a unary operator that compiles to Op.OP_NEG, so every constant is a non-negative value stored as the same integer / decimal-digit-count / decimal triple that a simple GUnit value uses. That reuses the existing transport encoding and sidesteps the sign-of-zero problem that the triple has for values such as -0.5, whose integer part cannot carry the sign.
Stack bound. The evaluation stack depth grows with parenthesis NESTING only, never with argument count, because variadic min() and max() are folded pairwise. Each nesting level costs TWO slots, so with the parenthesis depth limited to 10 the worst legal formula peaks at 2*MAX_NEST+1, i.e. 21, and MAX_STACK_DEPTH is derived from the same constant with margin over that. The exact peak of a given formula is computed once at construction and available from getMaxStackDepth(), and a formula exceeding the bound is rejected rather than left to overflow at evaluation time.
NaN. Op.OP_MIN2 and Op.OP_MAX2 are implemented as a plain compare rather than as Math.min / Math.max, so that Java and JavaScript cannot disagree about -0 and NaN. The consequence, deliberate: a NaN as the RIGHT operand loses the comparison and is discarded, while a NaN as the LEFT operand survives. Either way a surviving non-finite value is clamped to 0 once at the end of the evaluation, so NaN never escapes.
- Author:
- Christopher Mindus
Field Summary
FieldsConstructor Summary
ConstructorsMethod Summary
Modifier and TypeMethodDescriptionbooleanChecks if two compiled formulas are the same program.doubleevaluate(double[] scaleTable) Evaluates this formula against a prepared scale table.int[]getCode()Gets a copy of the opcode stream.intGets the number of constants.intgetConstantDec(int index) Gets the decimal part of a constant, for the transport encoding.intgetConstantDecimalDigits(int index) Gets the decimal digit count of a constant, for the transport encoding.intgetConstantInt(int index) Gets the integer part of a constant, for the transport encoding.double[]Gets the constant values as doubles.intGets the number of opcodes, which is what the wire format writes first so that the reader can preallocate.intGets the peak evaluation stack depth of this program.longGets the bit mask of the unit types used by this formula, bitut-1per unit.inthashCode()Returns the hash code of this formula.static FormulaCompiles a formula from source text.static StringtoDecimalString(int valueInt, int valueDec, int valueDecDigits) Formats a constant triple exactly the way a simpleGUnitvalue formats itself, so that a value written as a formula and the same value written as a simple unit render identically.Returns the opcode stream in mnemonic form, for debugging and for the test harness.toSource()Re-emits the formula as normalised source text: single spaces around binary operators, none after a unit suffix, none inside parentheses and none after an argument comma.toString()Returns the normalised source text of this formula.booleanusesUnit(int ut) Checks whether this formula uses a given unit type.booleanusesUnitRange(int utFrom, int utTo) Checks whether this formula uses any unit type in an inclusive range.
Field Details
MAX_STACK_DEPTH
public static final int MAX_STACK_DEPTHThe fixed evaluation stack size. Never grown, never allocated per instruction. The client mirrors this with a singleFloat64Array(MAX_STACK_DEPTH)allocated once outside the evaluation function, so that a layout pass allocates nothing.DERIVED from
Parser.MAX_NEST, never hardcoded. Each nesting level of a binary operator costs TWO stack slots, not one: in(1+2*X)both the1and the2remain live whileXevaluates. The measured worst case is2*MAX_NEST+1, so ten legal parentheses want 21 slots; the+4is margin, which also covers the extra slot aclampat the deepest point takes.The two constants were once chosen independently -- a stack of 20 against a believed bound of
nest+2-- and disagreed, so a formula obeying the ten-parenthesis rule could be refused for exceeding a stack limit that rule says nothing about. Linking them is what stops that recurring. The parenthesis limit is the human-facing rule and is the one that stays put.- See Also:
Constructor Details
Formula
public Formula(int[] code, int codeLength, int[] constInt, int[] constDec, int[] constDecDigits, int constCount) Creates a compiled formula from an opcode stream and its constants, as read back from a transaction or produced by theParser.The arrays are copied, so the caller may reuse its buffers. The stream is verified: an unknown opcode, a constant index out of range, a stack underflow, a stack depth beyond
MAX_STACK_DEPTHor a program not leaving exactly one value on the stack are all rejected here. That is what makes a wire desync fail loudly instead of evaluating to a plausible wrong number.- Parameters:
code- The opcode stream.codeLength- The used length ofcode.constInt- The integer part of each constant.constDec- The decimal part of each constant.constDecDigits- The decimal digit count of each constant.constCount- The used length of the three constant arrays.- Throws:
IllegalArgumentException- If the opcode stream is not a valid program.
Method Details
parse
Compiles a formula from source text.- Parameters:
src- The formula source, exactly as the author wrote it.- Returns:
- The compiled formula.
- Throws:
FormulaException- On any syntax or semantic error, carrying the offset and length of the offending span insrc.
getCode
public int[] getCode()Gets a copy of the opcode stream.The returned array is a COPY. It used to be the internal array, handed out rather than copied so the evaluator could walk it without a method call per instruction. That was safe only while every caller was ours. A
Formulais immutable and is shared and cached across the property tree, so a caller writing into the array it was handed would silently change the value of EVERY component using that formula, with no exception and no log line. Oncecom.iizix.prop.gunitis exported the callers are no longer all ours, and a javadoc sentence is not an access control.Nothing pays for the copy on the layout path, because nothing on the layout path calls this:
evaluate(double[])walks the array in place. The remaining callers run once per parse, once per transport, or on an explicit user action.- Returns:
- A copy of the opcode stream.
getConstants
public double[] getConstants()Gets the constant values as doubles.The returned array is a COPY, for the same reason as
getCode().- Returns:
- A copy of the constant values.
getConstantCount
public int getConstantCount()Gets the number of constants.- Returns:
- The constant count.
getConstantInt
public int getConstantInt(int index) Gets the integer part of a constant, for the transport encoding.- Parameters:
index- The constant index.- Returns:
- The integer part, always non-negative.
getConstantDec
public int getConstantDec(int index) Gets the decimal part of a constant, for the transport encoding.- Parameters:
index- The constant index.- Returns:
- The decimal part as a plain integer of
getConstantDecimalDigits(int)digits.
getConstantDecimalDigits
public int getConstantDecimalDigits(int index) Gets the decimal digit count of a constant, for the transport encoding.- Parameters:
index- The constant index.- Returns:
- The digit count, 0 to 7.
getInstructionCount
public int getInstructionCount()Gets the number of opcodes, which is what the wire format writes first so that the reader can preallocate.- Returns:
- The instruction count.
getMaxStackDepth
public int getMaxStackDepth()Gets the peak evaluation stack depth of this program.- Returns:
- The depth, never above
MAX_STACK_DEPTH.
usesUnit
public boolean usesUnit(int ut) Checks whether this formula uses a given unit type.- Parameters:
ut- The unit type, 1 toUnitType.UNIT_COUNT.- Returns:
trueif a scale opcode for that unit is present.
usesUnitRange
public boolean usesUnitRange(int utFrom, int utTo) Checks whether this formula uses any unit type in an inclusive range.- Parameters:
utFrom- The first unit type of the range.utTo- The last unit type of the range.- Returns:
trueif any scale opcode in the range is present.
getUsedUnits
public long getUsedUnits()Gets the bit mask of the unit types used by this formula, bitut-1per unit.- Returns:
- The mask.
evaluate
public double evaluate(double[] scaleTable) Evaluates this formula against a prepared scale table. THE EVALUATOR.A loop over a switch and a fixed stack. The stack never grows and is never indexed out of bounds, because the parser computes the peak depth of the program at compile time and rejects anything above
MAX_STACK_DEPTH; the depth grows with parenthesis nesting only, never with argument count, since variadicmin()andmax()are folded pairwise.Op.OP_MIN2andOp.OP_MAX2are a plain compare rather thanMath.min/Math.max: faster, and it avoids having to replicate JavaScript's-0and NaN rules exactly in Java. The consequence is that a NaN as the right operand loses the comparison and is discarded rather than propagating. A non-finite result is clamped to 0 once, at the end, so NaN never escapes either way.It lives here rather than in
GUnitso that the opcode array never leaves the object. It readscodeandconstValuedirectly, which is whygetCode()andgetConstants()can afford to copy: the hot path does not go through them.- Parameters:
scaleTable- The scale table, seeUnitType.- Returns:
- The value, non-finite results clamped to zero.
toSource
Re-emits the formula as normalised source text: single spaces around binary operators, none after a unit suffix, none inside parentheses and none after an argument comma.This is a decompiler, which buys a free correctness check: source to opcodes to source to opcodes must reach a fixed point. If the reformatted text does not re-parse to identical opcodes then either the parser or this method is wrong, and every formula ever written is a test case for it.
Two normalisations are worth knowing about, both of which re-parse to identical opcodes and both of which keep the parenthesis nesting at or below the original:
- A chain of folded
MIN2orMAX2instructions is re-emitted as one variadic call, somin(min(a,b),c)prints asmin(a,b,c). - A
MIN2immediately consumed by aMAX2prints asclamp, sinceval hi MIN2 lo MAX2is exactly whatclamp(lo,val,hi)emits. A hand-writtenmax(min(a,b),c)therefore prints asclamp(c,a,b); the two compile to the same opcodes by construction, so this is a spelling change and not a semantic one.
Parentheses. The opcode array contains none at all -- they are a source syntax artefact that vanishes at parse -- so this is not a removal pass. It emits the MINIMUM set needed for the text to re-parse to the identical tree, and every redundant one the author typed disappears automatically, however deeply nested. A child is parenthesised when its precedence is LOWER than the parent's, or when it is EQUAL and the child is the RIGHT operand of a left-associative operator. That second clause is the associativity trap:
a+(b+c)must KEEP its parentheses, becausea+b+cre-parses as(a+b)+cand in IEEE arithmetic those are different operations. The same holds fora-(b-c)anda/(b/c).- Returns:
- The normalised source text.
- A chain of folded
toDecimalString
Formats a constant triple exactly the way a simpleGUnitvalue formats itself, so that a value written as a formula and the same value written as a simple unit render identically.- Parameters:
valueInt- The integer part.valueDec- The decimal part.valueDecDigits- The number of decimal digits.- Returns:
- The decimal text, e.g.
"6.0123457".
equals
hashCode
toString
toOpcodeString
Returns the opcode stream in mnemonic form, for debugging and for the test harness.- Returns:
- E.g.
"CONST 10, SCALE %, CONST 5, ADD".