Class Formula

java.lang.Object
com.iizix.prop.gunit.Formula

public final class Formula extends Object
A compiled unit formula: the opcode array plus its constants. Immutable.

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

    Fields
    Modifier and Type
    Field
    Description
    static final int
    The fixed evaluation stack size.
  • Constructor Summary

    Constructors
    Constructor
    Description
    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 the Parser.
  • Method Summary

    Modifier and Type
    Method
    Description
    boolean
    Checks if two compiled formulas are the same program.
    double
    evaluate(double[] scaleTable)
    Evaluates this formula against a prepared scale table.
    int[]
    Gets a copy of the opcode stream.
    int
    Gets the number of constants.
    int
    getConstantDec(int index)
    Gets the decimal part of a constant, for the transport encoding.
    int
    Gets the decimal digit count of a constant, for the transport encoding.
    int
    getConstantInt(int index)
    Gets the integer part of a constant, for the transport encoding.
    double[]
    Gets the constant values as doubles.
    int
    Gets the number of opcodes, which is what the wire format writes first so that the reader can preallocate.
    int
    Gets the peak evaluation stack depth of this program.
    long
    Gets the bit mask of the unit types used by this formula, bit ut-1 per unit.
    int
    Returns the hash code of this formula.
    static Formula
    Compiles a formula from source text.
    static String
    toDecimalString(int valueInt, int valueDec, int valueDecDigits)
    Formats a constant triple exactly the way a simple GUnit value 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.
    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.
    Returns the normalised source text of this formula.
    boolean
    usesUnit(int ut)
    Checks whether this formula uses a given unit type.
    boolean
    usesUnitRange(int utFrom, int utTo)
    Checks whether this formula uses any unit type in an inclusive range.

    Methods inherited from class Object

    clone, finalize, getClass, notify, notifyAll, wait, wait, wait
  • Field Details

    • MAX_STACK_DEPTH

      public static final int MAX_STACK_DEPTH
      The fixed evaluation stack size. Never grown, never allocated per instruction. The client mirrors this with a single Float64Array(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 the 1 and the 2 remain live while X evaluates. The measured worst case is 2*MAX_NEST+1, so ten legal parentheses want 21 slots; the +4 is margin, which also covers the extra slot a clamp at 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 the Parser.

      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_DEPTH or 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 of code.
      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

      public static Formula parse(String src) throws FormulaException
      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 in src.
    • 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 Formula is 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. Once com.iizix.prop.gunit is 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 to UnitType.UNIT_COUNT.
      Returns:
      true if 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:
      true if any scale opcode in the range is present.
    • getUsedUnits

      public long getUsedUnits()
      Gets the bit mask of the unit types used by this formula, bit ut-1 per 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 variadic min() and max() are folded pairwise.

      Op.OP_MIN2 and Op.OP_MAX2 are a plain compare rather than Math.min/Math.max: faster, and it avoids having to replicate JavaScript's -0 and 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 GUnit so that the opcode array never leaves the object. It reads code and constValue directly, which is why getCode() and getConstants() can afford to copy: the hot path does not go through them.

      Parameters:
      scaleTable - The scale table, see UnitType.
      Returns:
      The value, non-finite results clamped to zero.
    • toSource

      public String 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 MIN2 or MAX2 instructions is re-emitted as one variadic call, so min(min(a,b),c) prints as min(a,b,c).
      • A MIN2 immediately consumed by a MAX2 prints as clamp, since val hi MIN2 lo MAX2 is exactly what clamp(lo,val,hi) emits. A hand-written max(min(a,b),c) therefore prints as clamp(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, because a+b+c re-parses as (a+b)+c and in IEEE arithmetic those are different operations. The same holds for a-(b-c) and a/(b/c).

      Returns:
      The normalised source text.
    • toDecimalString

      public static String toDecimalString(int valueInt, int valueDec, int valueDecDigits)
      Formats a constant triple exactly the way a simple GUnit value 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

      public boolean equals(Object o)
      Checks if two compiled formulas are the same program.
      Overrides:
      equals in class Object
      Parameters:
      o - Another object. If null or not a Formula, they are not equal.
      Returns:
      true if the opcodes and the constants are identical.
    • hashCode

      public int hashCode()
      Returns the hash code of this formula.
      Overrides:
      hashCode in class Object
      Returns:
      The hash code.
    • toString

      public String toString()
      Returns the normalised source text of this formula.
      Overrides:
      toString in class Object
      Returns:
      A string representation of this class instance.
    • toOpcodeString

      public String toOpcodeString()
      Returns the opcode stream in mnemonic form, for debugging and for the test harness.
      Returns:
      E.g. "CONST 10, SCALE %, CONST 5, ADD".