Skip to content
Open
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
40 changes: 19 additions & 21 deletions benchmarks/results/interleaved.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
| bench | base_a52c7e1_ms | head_4026c43_ms | delta |
| bench | develop_ms | ctor_cache_ms | delta |
|---|---|---|---|
| bench_borrow_path.nx | 564.7 | 507.7 | -10.1% |
| bench_bst_owned.nx | 780.9 | 565.8 | -27.5% |
| bench_bst_ref.nx | 216.7 | 217.9 | 0.6% |
| bench_bubblesort.nx | 740 | 744.5 | 0.6% |
| bench_call_light.nx | 32.3 | 28.8 | -10.8% |
| bench_call_readonly.nx | 572.5 | 546.5 | -4.5% |
| bench_call_ref.nx | 1109.4 | 1091.6 | -1.6% |
| bench_conway.nx | 1264.3 | 1283.4 | 1.5% |
| bench_generic_vs_hand.nx | 431.6 | 432.9 | 0.3% |
| bench_map_churn.nx | 212.3 | 207 | -2.5% |
| bench_path_update.nx | 162 | 163.1 | 0.7% |
| bench_share_mutate.nx | 115.4 | 107.4 | -6.9% |
| bench_spawn_sum.nx | 400.7 | 394.4 | -1.6% |
| bench_struct_records.nx | 127.4 | 126 | -1.1% |
| bench_typed_call_map.nx | 28.2 | 27 | -4.3% |
| bench_value_call_mutate.nx | 28.4 | 27 | -4.9% |

Pulados (sem equivalencia entre os dois binarios):

- bench_hash31_bytes.nx — sem CHECKSUM no base_a52c7e1
| bench_borrow_path.nx | 499.4 | 507 | 1.5% |
| bench_bst_owned.nx | 529.1 | 520 | -1.7% |
| bench_bst_ref.nx | 214.2 | 208.7 | -2.6% |
| bench_bubblesort.nx | 673 | 678.3 | 0.8% |
| bench_call_light.nx | 26.2 | 24.5 | -6.5% |
| bench_call_readonly.nx | 468.1 | 493.8 | 5.5% |
| bench_call_ref.nx | 1010 | 1028.2 | 1.8% |
| bench_conway.nx | 1171.6 | 1171.4 | -0% |
| bench_dyn_write.nx | 112.3 | 124 | 10.4% |
| bench_generic_vs_hand.nx | 412.1 | 411.3 | -0.2% |
| bench_hash31_bytes.nx | 657.8 | 665 | 1.1% |
| bench_map_churn.nx | 179.5 | 181.5 | 1.1% |
| bench_path_update.nx | 144.5 | 142 | -1.7% |
| bench_share_mutate.nx | 93.8 | 93.3 | -0.5% |
| bench_spawn_sum.nx | 222.6 | 224.1 | 0.7% |
| bench_struct_records.nx | 107.9 | 51.7 | -52.1% |
| bench_typed_call_map.nx | 25.1 | 24.5 | -2.4% |
| bench_value_call_mutate.nx | 23 | 23.4 | 1.7% |
33 changes: 33 additions & 0 deletions internal/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,39 @@ type ObjStruct struct {
// (issue #86), que deixou de existir. Nil (literal montado a mao em
// teste) cai em varredura linear, correta e so mais lenta.
index map[string]int
// ctorCache: resultado de validStructConstructorType (vm) para este
// struct — funcao pura de ConstructorType, que e imutavel depois da
// compilacao. Sem o cache, CADA construcao alocava um map para detectar
// ciclo, reandava a arvore de tipos e formatava um TypeName por campo
// (~35 % do tempo de codigo com structs; issue #40 item 1 / #66 item 4).
// atomic.Pointer: structs sao lidos por tasks paralelas. So veredito
// VALIDO e cacheado; o leitor confere Schema == ConstructorType.
ctorCache atomic.Pointer[ValidatedCtor]
}

// ValidatedCtor e o schema aceito do construtor e os ParamInfo (IsRef,
// TypeName) ja prontos para validateParameterModes.
type ValidatedCtor struct {
Schema *RuntimeTypeInfo
Params []ParamInfo
}

// CtorCache devolve o cache do construtor se existir E ainda corresponder ao
// ConstructorType atual (nil-safe); senao nil.
func (os *ObjStruct) CtorCache() *ValidatedCtor {
if os == nil {
return nil
}
cached := os.ctorCache.Load()
if cached == nil || cached.Schema != os.ConstructorType {
return nil
}
return cached
}

// StoreCtorCache grava o veredito valido do construtor.
func (os *ObjStruct) StoreCtorCache(cached *ValidatedCtor) {
os.ctorCache.Store(cached)
}

// FieldIsRef informa se o campo foi declarado `ref T` (nil-safe).
Expand Down
35 changes: 28 additions & 7 deletions internal/vm/runtime_type_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,15 @@ func markReferenceTargetType(ref *value.ObjRef, targetType *value.RuntimeTypeInf
}

func (vm *VM) validateStructConstructorArguments(definition *value.ObjStruct, args []value.Value) error {
schema, valid := validStructConstructorType(definition)
cached, valid := validatedStructConstructor(definition)
if !valid {
return fmt.Errorf("struct constructor has incomplete runtime type metadata")
}
schema := cached.Schema
if len(schema.Params) != len(args) {
return fmt.Errorf("struct '%s' constructor has invalid runtime type metadata", definition.Name)
}
params := make([]value.ParamInfo, len(schema.Params))
for i, expected := range schema.Params {
params[i] = value.ParamInfo{IsRef: schema.ParamIsRef[i], TypeName: expected.String()}
}
if err := validateParameterModes(definition.Name, params, args); err != nil {
if err := validateParameterModes(definition.Name, cached.Params, args); err != nil {
return err
}
for i, expected := range schema.Params {
Expand All @@ -82,16 +79,40 @@ func (vm *VM) validateStructConstructorArguments(definition *value.ObjStruct, ar
}

func validStructConstructorType(definition *value.ObjStruct) (*value.RuntimeTypeInfo, bool) {
cached, valid := validatedStructConstructor(definition)
if !valid {
return nil, false
}
return cached.Schema, true
}

// validatedStructConstructor e validStructConstructorType com cache no proprio
// ObjStruct (issue #40 item 1): ConstructorType e imutavel depois da
// compilacao, entao o walk de runtimeTypeComplete (que aloca um map por
// chamada), as checagens estruturais e os ParamInfo sao calculados UMA vez por
// struct. So veredito valido e guardado — o invalido e caminho de erro e
// recalcula; o leitor (CtorCache) confere que o schema cacheado ainda e o
// ConstructorType atual.
func validatedStructConstructor(definition *value.ObjStruct) (*value.ValidatedCtor, bool) {
if definition == nil {
return nil, false
}
if cached := definition.CtorCache(); cached != nil {
return cached, true
}
schema := definition.ConstructorType
if !runtimeTypeComplete(schema, make(map[*value.RuntimeTypeInfo]bool)) || schema.Kind != value.TYPE_CALLABLE || schema.CallableBare ||
len(schema.Params) != len(definition.Fields) || len(schema.ParamIsRef) != len(schema.Params) || schema.Return == nil ||
schema.Return.Kind != value.TYPE_STRUCT || schema.Return.Name != definition.Name {
return nil, false
}
return schema, true
params := make([]value.ParamInfo, len(schema.Params))
for i, expected := range schema.Params {
params[i] = value.ParamInfo{IsRef: schema.ParamIsRef[i], TypeName: expected.String()}
}
cached := &value.ValidatedCtor{Schema: schema, Params: params}
definition.StoreCtorCache(cached)
return cached, true
}

// runtimeTagAccepted informa, em O(profundidade do tipo), se uma tag de
Expand Down
55 changes: 55 additions & 0 deletions internal/vm/struct_ctor_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package vm

import (
"strings"
"testing"
)

// ctorErrorBody devolve a mensagem sem o prefixo de posicao "[...:line N] ",
// para comparar erros emitidos em linhas diferentes.
func ctorErrorBody(t *testing.T, err error) string {
t.Helper()
if err == nil {
t.Fatalf("esperava erro de construtor, veio nil")
}
text := err.Error()
if i := strings.Index(text, "] "); i >= 0 {
return text[i+2:]
}
return text
}

// O cache do schema validado do construtor (issue #40 item 1 / #66 item 4)
// nao pode mudar o texto nem o momento do erro. O checker rejeita P(a) com
// a: any em compilacao; a fronteira dinamica e chamar o CONSTRUTOR atraves de
// any. Cache frio (1a construcao do programa) e cache quente (depois de uma
// construcao valida do mesmo struct) tem de dar a mesma mensagem.
func TestStructConstructorErrorsUnchangedWithCache(t *testing.T) {
cold := ctorErrorBody(t, interpretVMSource(t, New(),
"struct P\n x: int\nend\nlet ctor: any = P\nlet p: any = ctor(\"s\")\n"))
warm := ctorErrorBody(t, interpretVMSource(t, New(),
"struct P\n x: int\nend\nlet ok: P = P(1)\nlet ctor: any = P\nlet p: any = ctor(\"s\")\n"))
if !strings.HasPrefix(cold, "function 'P' argument 1: expected int, got ") {
t.Fatalf("cold = %q, want o erro de tipo do construtor", cold)
}
if cold != warm {
t.Fatalf("cache mudou a mensagem: frio %q, quente %q", cold, warm)
}
// Aridade errada tambem igual nos dois estados.
coldArity := ctorErrorBody(t, interpretVMSource(t, New(),
"struct P\n x: int\nend\nlet ctor: any = P\nlet p: any = ctor(1, 2)\n"))
warmArity := ctorErrorBody(t, interpretVMSource(t, New(),
"struct P\n x: int\nend\nlet ok: P = P(1)\nlet ctor: any = P\nlet p: any = ctor(1, 2)\n"))
if coldArity != warmArity || !strings.Contains(coldArity, "expected 1 arguments for struct P but got 2") {
t.Fatalf("aridade: frio %q, quente %q", coldArity, warmArity)
}
}

// Construcao valida repetida: o cache serve a partir da 2a e o resultado e o
// mesmo.
func TestStructConstructorCacheKeepsResults(t *testing.T) {
got := semArray(t, captureVMSource(t, "struct P\n x: int\n y: string\nend\nlet i: int = 0\nlet s: int = 0\nwhile i < 1000 do\n let p: P = P(i, \"a\")\n s = s + p.x\n i = i + 1\nend\ntest_report([s])\n"))
if got[0].Int() != 499500 {
t.Fatalf("got %s, want 499500", got[0].String())
}
}