diff --git a/benchmarks/results/interleaved.md b/benchmarks/results/interleaved.md index 2ca792df..4711b852 100644 --- a/benchmarks/results/interleaved.md +++ b/benchmarks/results/interleaved.md @@ -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% | diff --git a/internal/value/value.go b/internal/value/value.go index 5a85f84f..2d3f2119 100644 --- a/internal/value/value.go +++ b/internal/value/value.go @@ -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). diff --git a/internal/vm/runtime_type_validation.go b/internal/vm/runtime_type_validation.go index 50fdfbde..daff9b2f 100644 --- a/internal/vm/runtime_type_validation.go +++ b/internal/vm/runtime_type_validation.go @@ -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 { @@ -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 diff --git a/internal/vm/struct_ctor_cache_test.go b/internal/vm/struct_ctor_cache_test.go new file mode 100644 index 00000000..c76153a6 --- /dev/null +++ b/internal/vm/struct_ctor_cache_test.go @@ -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()) + } +}