Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,26 @@ release is described in [docs/project/releasing.md](docs/project/releasing.md).

- **`ParseSources` re-analyzes only what an edit reaches when a client parses the same documents again.** From the second parse of a document set, the service answers from an incremental workspace held for it: unchanged documents keep their trees and diagnostics, and only the changed documents and those whose analysis read them are analyzed again. The answer is the one a fresh parse gives. On a 186-document model a re-parse after a one-line edit takes about 0.9 s instead of 1.7 s.

- **Recognising `this`, `that` and `isActive` no longer builds a qualified name.** Each read of a feature chain in a calculation or action body asked whether its target was `Occurrences::Occurrence::this`, `Base::things::that` or `StateActivity::isActive` by assembling the target's fully-qualified name and comparing it; the owner chain is now compared against the known name segment by segment, so a conformant body allocates nothing for the question, and every answer is as before.

- **Compiled calcs pass their Integers, Rationals, Reals and Booleans in three words.** The
compiled calc tier held each scalar in 56 bytes and, for every parameter, local and result,
boxed it back into the evaluator's constant to decide whether the declaration holds it, and
built the diagnostic a refusal would name whether or not one followed. A scalar is now the
constant packed into 24 bytes (`semantics.Packed`), unpacked without allocating; which kinds
a declaration holds whatever the value is decided once when the calc is compiled; the
diagnostic is built only on refusal; and the step charge inlines into every node.
`Fib(25)` interpreted runs in a fifth of the time with a quarter of the allocations; every
result, promotion to a big value, refusal, error message and step count is unchanged.

- **The gRPC service converts a cached model's diagnostics to protobuf once.** `ParseFile`, `ParseSources` and `GetDiagnostics` on a model parsed before answered the same diagnostics converted again on every call, two allocations per warning; each document's are now converted on first use and shared by every later response, and a model answered from a lineage shares those of every document the edit did not reach. The responses are unchanged.

- **A body statement's write no longer pays for its diagnostic description when it conforms.** The binding check of an assignment or declaration in an action, state or calculation body took the description of where it ran (`action node step`, `state behavior s`, `action A`) as a string built on every write; it now takes a function and formats the description only in the refusal it reports. An assignment step of an action loop allocates about a thousand fewer objects per thousand iterations, and every diagnostic reads as before.

- **Replacing a document no longer copies the re-export claims of every name it re-registers.** Before a name's registration changes, the index notes what a lookup of it reads so that one registered again exactly as it was does not count as a change; the note held a copy of every document's claims and routes on the name, taken once per name touched. It now shares the index's own claim tables, which a write replaces rather than changes, so an edit beside a large model allocates a fraction of what it did. What counts as a change is as before.

- Constraint checks and other runs no longer walk the succession graph for a cycle among held behaviors on every quiescent scan: the runtime remembers that no behavior is held until one enters held, by instantiation, image or snapshot restore, which cut `BenchmarkBatchConstraints` per-check time.

## 0.9.2 — 2026-10-05

### Added
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/action_control_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node,
func (e *performances) queueControlObject(frame *actionFrame, graph *lower.ActionGraph, flow lower.ObjectFlow, value Value) error {
for _, f := range graph.Features[flow.Target] {
if f.Name == flow.TargetPin && f.Direction == ast.DirIn {
if err := e.ctx.checkNamedWrite(graph.Scopes[flow.Target], nodeDescription(flow.Target), f.Name, &value); err != nil {
if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return nodeDescription(flow.Target) }, f.Name, &value); err != nil {
return fmt.Errorf("%s: %w", flowDescription(flow), err)
}
frame.hold(flow.Target, f.Name, nodeObject{value: value, flow: flow.Decl, carried: flow.Kind == lower.FlowSuccession})
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/action_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1537,7 +1537,7 @@ func (e *ActionExecutor) setFeature(name string, value Value) error {
if value, err = e.mirrorOccurrence(name, value); err != nil {
return err
}
} else if err := e.ctx.checkNamedWrite(e.graph.Scope, actionLabelPrefix+symbolText(e.action), name, &value); err != nil {
} else if err := e.ctx.checkNamedWriteAs(e.graph.Scope, func() string { return actionLabelPrefix + symbolText(e.action) }, name, &value); err != nil {
// No occurrence holds this feature, so its declaration is checked here
// rather than by the write to that occurrence.
return err
Expand Down
14 changes: 7 additions & 7 deletions internal/exec/runtime/action_frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ func (e *performances) seedPerformance(parent *actionFrame, flow *lower.ActionGr
continue
}
check := func(value *Value) error {
return e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, value)
return e.ctx.checkBodyDeclarationAs(feature.Scope, perf.describe, feature.Name, value)
}
context := func(scope *symbols.Scope) *EvalContext {
return e.evalBindingContext(perf, scope, feature.Name, activation)
Expand Down Expand Up @@ -497,7 +497,7 @@ func (e *performances) seedDeclaredValues(perf *actionFrame, features []lower.Fe
if feature.Binding {
cells := e.bodyCells(perf)
check := func(value *Value) error {
return e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, value)
return e.ctx.checkBodyDeclarationAs(feature.Scope, perf.describe, feature.Name, value)
}
name := perf.key(feature.Name)
cell := cells.existingCell(name)
Expand All @@ -514,7 +514,7 @@ func (e *performances) seedDeclaredValues(perf *actionFrame, features []lower.Fe
} else {
value, err = ec.Eval(feature.Value)
if err == nil {
err = e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, &value)
err = e.ctx.checkBodyDeclarationAs(feature.Scope, perf.describe, feature.Name, &value)
}
if err == nil {
e.ctx.writeBodyValue(perf.cells, perf.data, perf.key(feature.Name), value)
Expand Down Expand Up @@ -894,7 +894,7 @@ func (e *performances) deliver(f *actionFrame, flow *lower.ActionGraph, node ast
if !pins.declares(pin) {
return fmt.Errorf("%w: %s declares no %s", ErrNodePin, nodeDescription(node), pin)
}
if err := e.ctx.checkNamedWrite(flow.Scopes[node], nodeDescription(node), pin, &value); err != nil {
if err := e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return nodeDescription(node) }, pin, &value); err != nil {
return err
}
f.queue(node, canonical(pins.aliases, pin), value)
Expand Down Expand Up @@ -952,7 +952,7 @@ func (e *performances) checkNestedDelivery(flow *lower.ActionGraph, node ast.Nod
if !pins.declares(pin) {
return fmt.Errorf("%w: %s declares no %s", ErrNodePin, nodeDescription(node), pin)
}
return e.ctx.checkNamedWrite(flow.Scopes[node], nodeDescription(node), pin, value)
return e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return nodeDescription(node) }, pin, value)
}

// takeDeliveries moves the oldest delivery at each pin of node into perf, so that
Expand Down Expand Up @@ -1035,7 +1035,7 @@ func (e *performances) setFrameFeature(f *actionFrame, name string, value Value)
e.noteFrameWrite(f, name, value)
return nil
}
if err := e.ctx.checkNamedWrite(f.scope, f.describe(), name, &value); err != nil {
if err := e.ctx.checkNamedWriteAs(f.scope, f.describe, name, &value); err != nil {
return err
}
e.ctx.writeBodyValue(f.cells, f.data, f.key(name), value)
Expand Down Expand Up @@ -1101,7 +1101,7 @@ func (e *performances) streamFlow(
if !pins.declares(flow.TargetPin) {
return fmt.Errorf("%s: %w: %s declares no %s", flowDescription(flow), ErrNodePin, nodeDescription(flow.Target), flow.TargetPin)
}
if err := e.ctx.checkNamedWrite(graph.Scopes[flow.Target], nodeDescription(flow.Target), flow.TargetPin, &value); err != nil {
if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return nodeDescription(flow.Target) }, flow.TargetPin, &value); err != nil {
return fmt.Errorf("%s: %w", flowDescription(flow), err)
}
pin := canonical(pins.aliases, flow.TargetPin)
Expand Down
89 changes: 67 additions & 22 deletions internal/exec/runtime/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,46 @@ type scalarCheck struct {
holdsReal bool
// least is the smallest integer a Natural-holding declaration takes: 1 for Positive.
least int64
// plain has bit 1<<kind set for each constant kind the declaration holds
// whatever the value, decided once so accepts answers them with one test.
plain uint8
}

// settle decides plain from the lattice answers.
func (c *scalarCheck) settle() {
c.plain = 0
if !c.countOK {
return
}
if c.integerOK {
c.plain |= 1 << semantics.ValInt
}
if c.boolOK {
c.plain |= 1 << semantics.ValBool
}
if c.rationalOK {
c.plain |= 1 << semantics.ValRational
}
}

// accepts reports whether the declaration surely holds v, deciding on the
// scalar lattice alone; a value it declines is left to refuse.
func (c *scalarCheck) accepts(v scalar) bool {
return c.plain&(1<<v.Kind) != 0 || c.acceptsByValue(v)
}

// acceptsByValue is accepts for the kinds plain does not settle.
func (c *scalarCheck) acceptsByValue(v scalar) bool {
if !c.countOK {
return false
}
switch v.kind {
case scalarInt:
if v.big != nil {
return c.integerOK || (c.naturalOK && v.big.Sign() > 0)
switch v.Kind {
case semantics.ValInt:
if i, ok := v.smallInt(); ok {
return c.integerOK || (c.naturalOK && i >= c.least)
}
return c.integerOK || (c.naturalOK && v.int() >= c.least)
case scalarBool:
return c.integerOK || (c.naturalOK && v.Unpack().BigIntView().Sign() > 0)
case semantics.ValBool:
return c.boolOK
}
return c.acceptsReal(v)
Expand All @@ -138,15 +163,21 @@ func (c *scalarCheck) acceptsReal(v scalar) bool {
return false
}

// holds reports whether the declaration takes v as it is, so held has nothing to do.
func (c *scalarCheck) holds(v scalar) bool {
return !c.holdsReal || v.Kind != semantics.ValRational
}

// held is v as the declaration holds it: a Rational a Real declaration takes as its
// nearest binary64, refused when no finite Real is; what, if any, names the declaration.
func (c *scalarCheck) held(v scalar, what func() string) (scalar, error) {
if !c.holdsReal || v.kind != scalarRational {
if c.holds(v) {
return v, nil
}
real, err := semantics.RealOf(v.exact)
exact := v.Unpack()
real, err := semantics.RealOf(exact)
if err != nil {
err = fmt.Errorf("%s as a Real: %w", v.exact.FormatRational(), err)
err = fmt.Errorf("%s as a Real: %w", exact.FormatRational(), err)
if what != nil {
err = fmt.Errorf("%s: %w", what(), err)
}
Expand Down Expand Up @@ -388,11 +419,13 @@ func (c *calcCompiler) scalarCheckFor(decl *calcMemberDecl) (scalarCheck, bool)
check := scalarCheck{decl: decl, countOK: true,
boolOK: true, naturalOK: true, integerOK: true, rationalOK: true, realOK: true}
if decl.Target == nil {
check.settle()
return check, true
}
one := Value{Kind: ValConst}
check.countOK = c.ctx.writeCountRefusal(decl.Target, &one) == ""
if decl.Target.typ == nil {
check.settle()
return check, true
}
if decl.Target.typ.Kind == symbols.SymbolEnumerationDef {
Expand All @@ -412,6 +445,7 @@ func (c *calcCompiler) scalarCheckFor(decl *calcMemberDecl) (scalarCheck, bool)
if c.ctx.positiveScalar(decl.Target.typ) {
check.least = 1
}
check.settle()
return check, true
}

Expand Down Expand Up @@ -810,26 +844,19 @@ func (c *compiledCalc) invoke(ctx *Context, base int, bound paramSet) (scalar, e
for i := range c.params {
p := &c.params[i]
v := frame[i]
source := "argument"
if !bound.has(i) {
defaulted := !bound.has(i)
if defaulted {
var err error
if v, err = p.dflt(ctx, frame); err != nil {
ctx.leaveCalc()
return scalar{}, calcDefaultError(c.kind, c.name, p.name, err)
}
source = "default"
frame[i] = v
}
what := func() string {
return fmt.Sprintf("%s %s: %s for parameter %q", c.kind, c.name, source, p.name)
}
if !p.check.accepts(v) {
if err := p.check.refuse(ctx, v, what); err != nil {
ctx.leaveCalc()
return scalar{}, err
}
if p.check.accepts(v) && p.check.holds(v) {
continue
}
held, err := p.check.held(v, what)
held, err := c.holdParam(ctx, p, v, defaulted)
if err != nil {
ctx.leaveCalc()
return scalar{}, err
Expand All @@ -841,7 +868,7 @@ func (c *compiledCalc) invoke(ctx *Context, base int, bound paramSet) (scalar, e
if err != nil {
return scalar{}, calcFrame(c.kind, c.name, fmt.Errorf("%s%w", c.bodyErr, err))
}
if c.result != nil {
if c.result != nil && !(c.result.accepts(result) && c.result.holds(result)) {
what := func() string { return c.resultWhat }
if !c.result.accepts(result) {
if err := c.result.refuse(ctx, result, what); err != nil {
Expand All @@ -855,6 +882,24 @@ func (c *compiledCalc) invoke(ctx *Context, base int, bound paramSet) (scalar, e
return result, nil
}

// holdParam settles a parameter value its check did not take as it is: the
// reference's verdict on a value accepts declined, then the declaration's hold.
func (c *compiledCalc) holdParam(ctx *Context, p *compiledParam, v scalar, defaulted bool) (scalar, error) {
source := "argument"
if defaulted {
source = "default"
}
what := func() string {
return fmt.Sprintf("%s %s: %s for parameter %q", c.kind, c.name, source, p.name)
}
if !p.check.accepts(v) {
if err := p.check.refuse(ctx, v, what); err != nil {
return scalar{}, err
}
}
return p.check.held(v, what)
}

// frameAt extends the scalar stack to hold a frame of size slots at base; the
// local slots are written by their declarations before any read.
func (ctx *Context) frameAt(base, size int) []scalar {
Expand Down
Loading
Loading