From c4fa8e930f2455b6b56c7d3586a161f0d9536354 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 15 Jul 2026 20:38:54 +0200 Subject: [PATCH] richer formatting --- LANGUAGE.md | 10 +- README.md | 2 +- TODO.md | 22 ++- compiler/checker/checker.odin | 209 ++++++++++++++++------ compiler/checker/comptime.odin | 222 +++++++++++++++++++++++- compiler_tests.odin | 133 +++++++++++++- examples/programs/milestone_39/main.bro | 110 ++++++++++++ std/io/io.bro | 192 +++++++++++++++++--- std/meta/meta.bro | 6 +- 9 files changed, 807 insertions(+), 99 deletions(-) create mode 100644 examples/programs/milestone_39/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index dc9157f..7ba21c7 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -150,12 +150,13 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - demand-monomorphized Brolang and C-ABI functions - integer comptime value parameters such as `make_array func($N usize) [N]u8`, specialized by value and omitted from the runtime ABI - explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI -- comptime type/integer/string parameters may appear anywhere, are erased from the runtime ABI, and may be omitted when uniquely recoverable from runtime arguments or the immediate expected result; `_` is an explicit inference hole +- comptime parameters may appear anywhere, are erased from the runtime ABI, and accept recursively stable booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, and tagged unions; equal structural values share specializations, while pointers, functions, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity +- comptime parameters may be omitted when uniquely recoverable from runtime arguments, the immediate expected result, or exact type-factory provenance; `_` is an explicit inference hole - forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }` - comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed - comptime type factories such as `Box func($T type) type { return struct { value T } }`; calls like `Box(i32)` are concrete nominal types and may appear anywhere a type is expected - tuple types are unnamed-field structs (`struct { i32, []u8 }`), tuple values use `{1, "bro"}` / `{1,}` / `{}`, and fields use canonical numeric names such as `.0` -- `typeinfo!`, `field!`, `compile_error!`, and semantic `inline for` provide compile-time record reflection and heterogeneous static expansion without runtime metadata; reflected aggregates remain persistent compile-time values, and inline-loop `break` / `continue` must be selected entirely at comptime +- `typeinfo!`, `field!`, `compile_error!`, and semantic `inline for` provide compile-time record and enum reflection and heterogeneous static expansion without runtime metadata; enum reflection exposes declaration-ordered fields, reflected aggregates remain persistent compile-time values, and inline-loop `break` / `continue` must be selected entirely at comptime - bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names - concrete-only C signatures, C variadic declarations/calls, and C default argument promotions - native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, and non-variadic native indirect calls @@ -174,7 +175,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - root `std` re-exports `ArrayList(T)` while its operations remain in `std/arraylist` - `std/mem` generic slice equality, allocator contract with raw byte operations, typed `empty` / `alloc` / `realloc` / `free`, overflow checks, zero-sized-type support, and failure-preserving reallocation - `std/arraylist` generic `ArrayList(T)` with direct `items` slice access, explicit capacity, allocator ownership, fallible reserve/append, clear, and deinit -- `std/io` explicit `Io` capabilities, `Reader`/`Writer` stream values, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting currently supports sequential `{s}` / `{d}` plus `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime +- `std/io` explicit `Io` capabilities, `Reader`/`Writer` stream values, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime - entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io` - `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init` @@ -190,12 +191,11 @@ as `math.divfloor(a, b)` resolve to ordinary functions. ## PLANNED / DEFERRED -- aggregate comptime parameters and stable aggregate specialization keys - native Brolang variadic functions - exporting Brolang functions to C and broader target-specific C ABI lowering - non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments - arenas, pools, build-mode heap policy, and escaping-allocation diagnostics -- recursive type factories, reflection payloads beyond records, and type-producing unions/enums +- recursive type factories, reflection payloads beyond records/enums, and type-producing unions/enums - broader Zig-style pointer/result casts beyond V1 `ptrcast!(T, ptr)` - sum-type ABI/layout polish, including dynamic tag-width shrinking, all-void channel collapse, and cross-module global-id determinism - backed/C enum composition and must-consume fallible linting diff --git a/README.md b/README.md index 20f2e3d..57a309a 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Current prototype features: - Plain imported C structs/unions, fixed arrays, and C function pointer typedefs, including keyed literals, field access, callbacks, and Apple Silicon by-value ABI lowering - Qualified imported globals and functions with package-aware symbol mangling - Demand-monomorphized Brolang and C-ABI functions -- Integer, type, and immutable byte-string comptime parameters may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit or uniquely inferred arguments +- Recursively stable comptime values—including booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, and tagged unions—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments or exact inference provenance - Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`) - Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`) - Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`/`errdefer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values diff --git a/TODO.md b/TODO.md index 2776663..94f36ee 100644 --- a/TODO.md +++ b/TODO.md @@ -686,8 +686,8 @@ evaluation; runtime-dependent values remain invalid in comptime contexts - runtime-only behavior is rejected in comptime: external/bodyless `c_func`, writable globals, and materializing comptime storage pointers/slices as runtime memory - - v1 keeps integer-only `$N` specialization keys; aggregate comptime parameters - and stable aggregate serialization are deferred + - milestone 39 extends specialization keys from integer/type/string values to + recursively stable values while keeping runtime ABI erasure unchanged 27.8 source-defined mutable runtime globals (implemented) - allow mutable global declarations in Brolang source for process-global runtime @@ -867,7 +867,23 @@ 38. place every intrinsic behind direct unqualified `name!(...)` syntax, freeing the bare names for user functions (implemented) -39. aggregate comptime parameters and richer formatting +39. stable comptime values and richer formatting (implemented) + - comptime parameters accept booleans, integers, floats, types, immutable bytes, + enums, fixed arrays, records/tuples, optionals, and tagged unions recursively + - canonical specialization keys include deterministic type identity, exact float bits, + byte contents, ordered aggregate children, optional state, and active union variants; + FNV-1a fingerprints accelerate lookup while exact key comparison handles collisions + - equal structural values reuse specializations and stable emitted names, distinct values + specialize separately, aggregate inference uses exact type-factory provenance, and all + comptime parameters remain erased from the runtime ABI + - undefined values, pointers, functions, general slices, fallibles, ranges, and untagged + unions diagnose that they have no stable comptime identity + - `@std/meta.TypeInfo.enum` carries declaration-ordered `EnumInfo.fields`, enabling enum + formatting through `field!` without runtime reflection metadata + - `io.print` and `debug.print` retain their APIs and expand `{}`, `{s}`, `{d}`, `{b}`, + `{o}`, `{x}`, `{X}`, `{c}`, and `{e}` at comptime; `{{` and `}}` remain escapes + - integer output uses one base-aware 65-byte stack buffer; float output uses fixed-buffer + libc `snprintf` with 32-bit and 64-bit general/scientific precision and propagates failure ## A word on unchecked casts diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 67f2825..fd00e7f 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -232,6 +232,8 @@ Checker :: struct { current_comptime_values: []Comptime_Value, static_state: Ct_State, static_bindings: [dynamic]Static_Binding, + comptime_keys: [dynamic]string, + comptime_static_values: [dynamic]Ct_Value_Id, inline_context: [dynamic]Inline_Expansion, type_factories: [dynamic]Type_Factory_Entry, generated_types: [dynamic]Generated_Type_Entry, @@ -528,7 +530,14 @@ static_field_value :: proc(checker: ^Checker, base, name: symbol.Id) -> (Ct_Valu if !ok || binding.value == INVALID_CT_VALUE || int(binding.value) >= len(checker.static_state.values) { return {}, false } - value := checker.static_state.values[binding.value] + return persistent_field_value(checker, binding.value, name) +} + +persistent_field_value :: proc(checker: ^Checker, root: Ct_Value_Id, name: symbol.Id) -> (Ct_Value, bool) { + if root == INVALID_CT_VALUE || int(root) >= len(checker.static_state.values) { + return {}, false + } + value := checker.static_state.values[root] index, _, found := find_struct_field(checker, value.type, name) children := ct_child_slice(&checker.static_state, value) if !found || index < 0 || index >= len(children) || children[index] == INVALID_CT_VALUE || @@ -1705,7 +1714,9 @@ explicit_comptime_argument_valid :: proc( if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { return eval_integer_constant_in_context(checker, arg, pkg, file).kind == .Value } - return false + declared := type_from_syntax(checker, param.type, function.pkg, function.file) + _, ok := eval_static_comptime_value(checker, param.name, arg, declared, pkg, file) + return ok } search_call_mappings :: proc(search: ^Call_Mapping_Search, param_index, source_index: int) { @@ -1966,13 +1977,15 @@ bind_inferred_comptime :: proc( if matches { if existing.kind == .Type { matches = types.equal(types.resolve_alias(existing.type, &checker.module.types), types.resolve_alias(value.type, &checker.module.types)) + } else if existing.kind == .Static { + matches = existing.fingerprint == value.fingerprint && existing.key == value.key && types.equal(existing.type, value.type) } else { matches = existing.value == value.value && types.equal(existing.type, value.type) } } if !matches && diagnose { - left := type_label(checker, existing.type) if existing.kind == .Type else fmt.aprintf("%d", existing.value, allocator=checker.allocator) - right := type_label(checker, value.type) if value.kind == .Type else fmt.aprintf("%d", value.value, allocator=checker.allocator) + left := type_label(checker, existing.type) if existing.kind == .Type else fmt.aprintf("", allocator=checker.allocator) if existing.kind == .Static else fmt.aprintf("%d", existing.value, allocator=checker.allocator) + right := type_label(checker, value.type) if value.kind == .Type else fmt.aprintf("", allocator=checker.allocator) if value.kind == .Static else fmt.aprintf("%d", value.value, allocator=checker.allocator) source.addf(checker.diagnostics, span, "conflicting inference for comptime parameter '%s': %s and %s", symbol_text(checker, name), left, right) if existing.kind != .Type { delete(left, checker.allocator) @@ -2221,9 +2234,12 @@ infer_call_comptime_values :: proc( } else if is_comptime_string_param(checker, param, function) { values[ordinal].kind = .String values[ordinal].type = type_from_syntax(checker, param.type, function.pkg, function.file) - } else { + } else if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { values[ordinal].kind = .Integer values[ordinal].type = type_from_syntax(checker, param.type, function.pkg, function.file) + } else { + values[ordinal].kind = .Static + values[ordinal].type = type_from_syntax(checker, param.type, function.pkg, function.file) } ordinal += 1 } @@ -2287,7 +2303,7 @@ infer_call_comptime_values :: proc( kind=.String, } bound[binding_index] = true - } else { + } else if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { declared := type_from_syntax(checker, param.type, function.pkg, function.file) constant := eval_integer_constant_in_context(checker, arg_id, pkg, file) if constant.kind != .Value { @@ -2321,6 +2337,32 @@ infer_call_comptime_values :: proc( } values[binding_index] = Comptime_Value{name=param.name, type=declared, value=constant.value, kind=.Integer} bound[binding_index] = true + } else { + declared := type_from_syntax(checker, param.type, function.pkg, function.file) + available: [dynamic]Comptime_Value + available.allocator = checker.allocator + append(&available, ..checker.current_comptime_values) + for prior, prior_index in values[:binding_index] { + if bound[prior_index] { + append(&available, prior) + } + } + value, value_ok := eval_static_comptime_value( + checker, param.name, arg_id, declared, pkg, file, available[:], diagnose, + ) + delete(available) + if !value_ok { + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf( + "argument %d for comptime parameter '%s' has no stable comptime identity", + source_index+1, symbol_text(checker, param.name), allocator=checker.allocator, + ) + } + matched = false + continue + } + values[binding_index] = value + bound[binding_index] = true } } all_bound := true @@ -2768,48 +2810,56 @@ collect_comptime_values :: proc( continue } declared := type_from_syntax(checker, param.type, function.pkg, function.file) - if !types.is_concrete_integer(declared) { - if diagnose { - source.addf( - checker.diagnostics, - param.span, - "comptime parameter '%s' requires a concrete integer type", - symbol_text(checker, param.name), - ) + if types.is_concrete_integer(declared) { + constant := Constant{kind = .Not_Constant} + if index < len(args) { + constant = eval_integer_constant_in_context(checker, args[index], pkg, file, values=extra_values) } + if constant.kind != .Value { + if diagnose { + source.addf( + checker.diagnostics, + span, + "argument for comptime parameter '%s' must be a compile-time integer expression", + symbol_text(checker, param.name), + ) + } + ok = false + continue + } + if !fits_integer_type(constant.value, declared, checker.target) { + if diagnose { + source.addf( + checker.diagnostics, + span, + "integer constant %d does not fit in %s", + constant.value, + types.name(declared), + ) + } + ok = false + continue + } + append(&values, Comptime_Value{name=param.name, type=declared, value=constant.value}) + continue + } + if index >= len(args) || args[index] == ast.INVALID_EXPR { ok = false continue } - constant := Constant{kind = .Not_Constant} - if index < len(args) { - constant = eval_integer_constant_in_context(checker, args[index], pkg, file, values=extra_values) - } - if constant.kind != .Value { - if diagnose { - source.addf( - checker.diagnostics, - span, - "argument for comptime parameter '%s' must be a compile-time integer expression", - symbol_text(checker, param.name), - ) - } + available: [dynamic]Comptime_Value + available.allocator = checker.allocator + append(&available, ..extra_values) + append(&available, ..values[:]) + value, value_ok := eval_static_comptime_value( + checker, param.name, args[index], declared, pkg, file, available[:], diagnose, + ) + delete(available) + if !value_ok { ok = false continue } - if !fits_integer_type(constant.value, declared, checker.target) { - if diagnose { - source.addf( - checker.diagnostics, - span, - "integer constant %d does not fit in %s", - constant.value, - types.name(declared), - ) - } - ok = false - continue - } - append(&values, Comptime_Value{name=param.name, type=declared, value=constant.value}) + append(&values, value) } if !ok { delete(values) @@ -3111,12 +3161,11 @@ validate_declarations :: proc(checker: ^Checker) { ) } if !is_type_metatype_syntax(checker, param.type) && - !types.is_concrete_integer(param_type) && - !is_comptime_string_param(checker, param, function) { + !is_runtime_type(checker, param_type) && param_type != types.RANGE { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, - "comptime parameter '%s' requires type, a concrete integer type, or immutable []u8", + "comptime parameter '%s' requires type or a concrete value type", symbol_text(checker, param.name), ) } @@ -3503,9 +3552,10 @@ validate_meta_schema :: proc(checker: ^Checker) { } field_info := find(checker, meta_package, "FieldInfo") record_info := find(checker, meta_package, "RecordInfo") + enum_info := find(checker, meta_package, "EnumInfo") type_info := find(checker, meta_package, "TypeInfo") layout := find(checker, meta_package, "Layout") - valid := types.is_valid(field_info) && types.is_valid(record_info) && + valid := types.is_valid(field_info) && types.is_valid(record_info) && types.is_valid(enum_info) && types.is_valid(type_info) && types.is_valid(layout) layout_item, layout_ok := types.node(&checker.module.types, layout) layout_members := types.enum_members_for(&checker.module.types, layout) @@ -3516,8 +3566,10 @@ validate_meta_schema :: proc(checker: ^Checker) { } field_item, field_ok := types.node(&checker.module.types, field_info) record_item, record_ok := types.node(&checker.module.types, record_info) + enum_item, enum_ok := types.node(&checker.module.types, enum_info) valid = valid && field_ok && field_item.kind == .Struct && !field_item.tuple && !field_item.c_layout && - record_ok && record_item.kind == .Struct && !record_item.tuple && !record_item.c_layout + record_ok && record_item.kind == .Struct && !record_item.tuple && !record_item.c_layout && + enum_ok && enum_item.kind == .Struct && !enum_item.tuple && !enum_item.c_layout field_fields := types.fields_for(&checker.module.types, field_info) valid = valid && len(field_fields) == 3 if valid { @@ -3528,6 +3580,13 @@ validate_meta_schema :: proc(checker: ^Checker) { symbol_text(checker, symbol.Id(field_fields[2].name)) == "index" && types.equal(field_fields[2].type, types.USIZE) } + enum_fields := types.fields_for(&checker.module.types, enum_info) + valid = valid && len(enum_fields) == 1 + if valid { + fields_item, fields_ok := types.node(&checker.module.types, enum_fields[0].type) + valid = symbol_text(checker, symbol.Id(enum_fields[0].name)) == "fields" && fields_ok && + fields_item.kind == .Slice && !fields_item.mutable && types.equal(fields_item.child, field_info) + } record_fields := types.fields_for(&checker.module.types, record_info) valid = valid && len(record_fields) == 4 if valid { @@ -3553,7 +3612,8 @@ validate_meta_schema :: proc(checker: ^Checker) { field := type_fields[index] if symbol_text(checker, symbol.Id(field.name)) != tag || (tag == "record" && !types.equal(field.type, record_info)) || - (tag != "record" && !types.is_void(field.type)) { + (tag == "enum" && !types.equal(field.type, enum_info)) || + (tag != "record" && tag != "enum" && !types.is_void(field.type)) { valid = false break } @@ -3623,7 +3683,7 @@ validate_type_nodes :: proc(checker: ^Checker) { } item_name := symbol_text(checker, symbol.Id(item.name)) comptime_meta := meta_package && - (item_name == "FieldInfo" || item_name == "RecordInfo" || item_name == "TypeInfo") + (item_name == "FieldInfo" || item_name == "RecordInfo" || item_name == "EnumInfo" || item_name == "TypeInfo") if item.c_layout && !item.opaque && item.field_count == 0 { source.add( checker.diagnostics, @@ -4263,6 +4323,12 @@ infer_expr :: proc( } if !types.is_valid(last) && symbol.is_valid(expr.qualifier) && find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { + if value, ok := current_comptime_value(checker, expr.qualifier); + ok && value.kind == .Static { + if _, field, found := find_struct_field(checker, value.type, expr.name); found { + last = field.type + } + } if value, ok := current_static_binding(checker, expr.qualifier); ok { if _, field, found := find_struct_field(checker, value.type, expr.name); found { last = field.type @@ -4298,7 +4364,7 @@ infer_expr :: proc( if !types.is_valid(last) { if !symbol.is_valid(expr.qualifier) { if value, ok := current_comptime_value(checker, expr.name); ok { - if value.kind == .Integer || value.kind == .String { + if value.kind == .Integer || value.kind == .String || value.kind == .Static { last = value.type } } @@ -4385,6 +4451,13 @@ infer_expr :: proc( continue } if field_expr, handled := field_intrinsic_expr(checker, expr, pkg, file); handled { + if enum_type, ok := resolve_type_argument(checker, field_expr.left, pkg, file); + ok && types.is_enum(enum_type, &checker.module.types) { + _, member_ok := find_enum_member(checker, enum_type, field_expr.name) + last = enum_type if member_ok else types.INVALID + _ = pop(&stack) + continue + } base_type := infer_nested_expr( checker, field_expr.left, locals, pkg, file, demanded, local_types, ) @@ -6360,6 +6433,15 @@ build_qualified_value_field :: proc( value, ok := build_field_from_value(checker, expr, base, local.type) return value, true, ok } + if value, ok := current_comptime_value(checker, expr.qualifier); + ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE && + int(value.static_value) < len(checker.static_state.values) { + field_value, field_ok := persistent_field_value(checker, value.static_value, expr.name) + if !field_ok { + return hir.INVALID_EXPR, true, false + } + return build_static_value(checker, field_value, expr.span, types.INVALID), true, true + } if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { base := build_global_reference(checker, global, expr.span, global_reads) value, ok := build_field_from_value(checker, expr, base, checker.global_types[global]) @@ -7482,6 +7564,15 @@ build_expr :: proc( }) } } + if last == hir.INVALID_EXPR && symbol.is_valid(expr.qualifier) { + if value, ok := current_comptime_value(checker, expr.qualifier); + ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE && + int(value.static_value) < len(checker.static_state.values) { + if field_value, found := persistent_field_value(checker, value.static_value, expr.name); found { + last = build_static_value(checker, field_value, expr.span, frame.expected) + } + } + } if last == hir.INVALID_EXPR && symbol.is_valid(expr.qualifier) { if value, ok := static_field_value(checker, expr.qualifier, expr.name); ok { last = build_static_value(checker, value, expr.span, frame.expected) @@ -7558,6 +7649,11 @@ build_expr :: proc( checker, constant_expr, locals, global_reads, calls, frame.expected, pkg, file, ) + } else if value.kind == .Static && value.static_value != INVALID_CT_VALUE && + int(value.static_value) < len(checker.static_state.values) { + last = build_static_value( + checker, checker.static_state.values[value.static_value], expr.span, frame.expected, + ) } else { id := source.addf( checker.diagnostics, @@ -7628,6 +7724,12 @@ build_expr :: proc( continue } if field_expr, handled := field_intrinsic_expr(checker, expr, pkg, file); handled { + if enum_type, ok := resolve_type_argument(checker, field_expr.left, pkg, file); + ok && types.is_enum(enum_type, &checker.module.types) { + last = enum_member_hir(checker, enum_type, field_expr.name, field_expr.span) + _ = pop(&stack) + continue + } base := build_nested_expr( checker, field_expr.left, locals, global_reads, calls, types.INVALID, pkg, file, @@ -8315,6 +8417,8 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string { strings.write_byte(&builder, hex[byte>>4]) strings.write_byte(&builder, hex[byte&0xf]) } + } else if value.kind == .Static { + fmt.sbprintf(&builder, "__ca%d_%016x", len(value.key), value.fingerprint) } else { strings.write_string(&builder, "__cv") if value.value < 0 { @@ -12070,6 +12174,8 @@ check :: proc( checker.type_factory_origins.allocator = allocator checker.call_resolutions.allocator = allocator checker.static_bindings.allocator = allocator + checker.comptime_keys.allocator = allocator + checker.comptime_static_values.allocator = allocator checker.inline_context.allocator = allocator checker.static_state = ct_state_make(&checker, 0, ast.INVALID_FILE) build_symbol_indexes(&checker) @@ -12146,6 +12252,11 @@ check :: proc( delete(checker.call_resolutions) ct_state_destroy(&checker.static_state) delete(checker.static_bindings) + for key in checker.comptime_keys { + delete(key, allocator) + } + delete(checker.comptime_keys) + delete(checker.comptime_static_values) delete(checker.inline_context) } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 926cf6c..118caad 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -8,6 +8,7 @@ import "../types" import "base:intrinsics" import "core:fmt" +import "core:hash" import "core:math" import "core:mem" import "core:strings" @@ -18,14 +19,18 @@ Comptime_Value_Kind :: enum u8 { Integer, Type, String, + Static, } Comptime_Value :: struct { - name: symbol.Id, - type: types.Type, - value: i128, - text: string, - kind: Comptime_Value_Kind, + name: symbol.Id, + type: types.Type, + value: i128, + text: string, + static_value: Ct_Value_Id, + key: string, + fingerprint: u64, + kind: Comptime_Value_Kind, } Constant_Kind :: enum { @@ -85,7 +90,8 @@ comptime_values_equal :: proc(left, right: []Comptime_Value) -> bool { other := right[index] if value.name != other.name || value.kind != other.kind || !types.equal(value.type, other.type) || (value.kind == .Integer && value.value != other.value) || - (value.kind == .String && value.text != other.text) { + (value.kind == .String && value.text != other.text) || + (value.kind == .Static && (value.fingerprint != other.fingerprint || value.key != other.key)) { return false } } @@ -362,6 +368,9 @@ ct_state_make :: proc( } id := ct_add_value(&state, Ct_Value{kind=.String, type=value.type, index=string_id}) ct_bind_value(&state, value.name, value.type, id, false) + } else if value.kind == .Static { + id := ct_clone_graph(&state, &checker.static_state, value.static_value) + ct_bind_value(&state, value.name, value.type, id, false) } } for binding in checker.static_bindings { @@ -2312,9 +2321,10 @@ ct_typeinfo_value :: proc(state: ^Ct_State, target: types.Type, span: source.Spa typeinfo_type := std_named_type(checker, "@std/meta", "TypeInfo") fieldinfo_type := std_named_type(checker, "@std/meta", "FieldInfo") recordinfo_type := std_named_type(checker, "@std/meta", "RecordInfo") + enuminfo_type := std_named_type(checker, "@std/meta", "EnumInfo") layout_type := std_named_type(checker, "@std/meta", "Layout") if !types.is_valid(typeinfo_type) || !types.is_valid(fieldinfo_type) || - !types.is_valid(recordinfo_type) || !types.is_valid(layout_type) { + !types.is_valid(recordinfo_type) || !types.is_valid(enuminfo_type) || !types.is_valid(layout_type) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( state, .Not_Comptime, span, "typeinfo! requires importing @std/meta", @@ -2352,6 +2362,31 @@ ct_typeinfo_value :: proc(state: ^Ct_State, target: types.Type, span: source.Spa if !variant_ok { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "@std/meta TypeInfo is malformed") } + if tag == "enum" { + members := types.enum_members_for(store, resolved) + field_values := make([]Ct_Value_Id, len(members), checker.allocator) + defer delete(field_values, checker.allocator) + for member, index in members { + name_value := ct_reflection_string(state, symbol_text(checker, symbol.Id(member.name))) + type_value := ct_add_value(state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(resolved)}) + index_value := ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(index)}) + field_values[index] = ct_struct_value( + state, fieldinfo_type, []Ct_Value_Id{name_value, type_value, index_value}, + ) + } + fields_start := u32(len(state.children)) + append(&state.children, ..field_values) + fields_type := types.array(store, fieldinfo_type, u64(len(field_values)), false) + fields_value := ct_add_value(state, Ct_Value{ + kind=.Array, type=fields_type, start=fields_start, count=u32(len(field_values)), + }) + enum_value := ct_struct_value(state, enuminfo_type, []Ct_Value_Id{fields_value}) + payload_start := u32(len(state.children)) + append(&state.children, enum_value) + return ct_add_value(state, Ct_Value{ + kind=.Struct, type=typeinfo_type, start=payload_start, count=1, active=i64(variant_index), + }), ct_flow(.Normal), true + } if tag != "record" { start := u32(len(state.children)) return ct_add_value(state, Ct_Value{ @@ -2437,6 +2472,22 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type if len(expr.args) != 2 { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "field! expects 2 arguments, got %d", len(expr.args)) } + if enum_type, ok := resolve_type_argument(checker, expr.args[0], state.pkg, state.file); + ok && types.is_enum(enum_type, &checker.module.types) { + name_value, name_flow, name_ok := ct_eval_expr(state, expr.args[1], types.INVALID, depth+1) + if !name_ok || name_flow.kind != .Normal { + return INVALID_CT_VALUE, name_flow, name_ok + } + name, text_ok := ct_value_bytes(state, name_value) + if !text_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "field! name must be a comptime immutable byte string") + } + member, member_ok := find_enum_member(checker, enum_type, symbol.intern(checker.symbols, name)) + if !member_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown enum member '%s'", name) + } + return ct_add_value(state, Ct_Value{kind=.Integer, type=enum_type, integer=member.value}), ct_flow(.Normal), true + } base, flow, ok := ct_eval_expr(state, expr.args[0], types.INVALID, depth+1) if !ok || flow.kind != .Normal { return INVALID_CT_VALUE, flow, ok @@ -2753,6 +2804,163 @@ store_static_binding :: proc(checker: ^Checker, source: ^Ct_State, id: Ct_Value_ return Static_Binding{name=name, type=value_type, value=value} } +ct_write_comptime_key :: proc(state: ^Ct_State, id: Ct_Value_Id, builder: ^strings.Builder, depth := 0) -> bool { + if depth > 256 || id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return false + } + value := state.values[id] + fmt.sbprintf(builder, "t%d:", value.type) + #partial switch value.kind { + case .Integer: + fmt.sbprintf(builder, "i%d;", value.integer) + return true + case .Float: + if types.bits(value.type, state.checker.target) == 32 { + fmt.sbprintf(builder, "f%08x;", transmute(u32)f32(value.float)) + } else { + fmt.sbprintf(builder, "f%016x;", transmute(u64)value.float) + } + return true + case .Bool: + fmt.sbprintf(builder, "b%d;", value.integer) + return true + case .Type: + fmt.sbprintf(builder, "y%d;", value.index) + return true + case .String: + if value.index >= u64(len(state.checker.ast_module.strings)) { + return false + } + text := state.checker.ast_module.strings[value.index] + fmt.sbprintf(builder, "s%d:", len(text)) + hex := "0123456789abcdef" + for byte in transmute([]byte)text { + strings.write_byte(builder, hex[byte>>4]) + strings.write_byte(builder, hex[byte&0xf]) + } + strings.write_byte(builder, ';') + return true + case .Slice: + item, item_ok := types.container(value.type, &state.checker.module.types) + if !item_ok || item.kind != .Slice || item.mutable || item.child != types.U8 { + return false + } + text, text_ok := ct_value_bytes(state, id) + if !text_ok { + return false + } + fmt.sbprintf(builder, "s%d:", len(text)) + hex := "0123456789abcdef" + for byte in transmute([]byte)text { + strings.write_byte(builder, hex[byte>>4]) + strings.write_byte(builder, hex[byte&0xf]) + } + strings.write_byte(builder, ';') + return true + case .Array: + children := ct_child_slice(state, value) + fmt.sbprintf(builder, "a%d[", len(children)) + for child in children { + if !ct_write_comptime_key(state, child, builder, depth+1) { + return false + } + } + strings.write_string(builder, "];") + return true + case .Struct: + if types.is_union(value.type, &state.checker.module.types) && + !types.is_tagged_union(value.type, &state.checker.module.types) { + return false + } + children := ct_child_slice(state, value) + fmt.sbprintf(builder, "r%d:%d[", value.active, len(children)) + for child in children { + if child != INVALID_CT_VALUE && !ct_write_comptime_key(state, child, builder, depth+1) { + return false + } + } + strings.write_string(builder, "];") + return true + case .None: + strings.write_string(builder, "n;") + return true + case .Optional_Some: + children := ct_child_slice(state, value) + if len(children) != 1 || !ct_write_comptime_key(state, children[0], builder, depth+1) { + return false + } + strings.write_string(builder, "o;") + return true + case .Invalid, .Void, .Undefined, .Range, .Pointer, .Function, .Fallible: + return false + } + return false +} + +eval_static_comptime_value :: proc( + checker: ^Checker, + name: symbol.Id, + expr: ast.Expr_Id, + declared: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, + values: []Comptime_Value = nil, + diagnose := false, +) -> (Comptime_Value, bool) { + state := ct_state_make(checker, pkg, file, values=values, diagnose=false) + defer ct_state_destroy(&state) + id, flow, ok := ct_eval_expr(&state, expr, declared, 0) + if ok && flow.kind == .Normal { + id, ok = ct_coerce_value(&state, id, declared, checker.ast_module.exprs[expr].span) + } + if !ok || flow.kind != .Normal || ct_value_contains_undefined(&state, id) { + if diagnose { + source.add( + checker.diagnostics, + checker.ast_module.exprs[expr].span, + "comptime argument has no stable comptime identity", + ) + } + return {}, false + } + builder := strings.builder_make(checker.allocator) + defer strings.builder_destroy(&builder) + if !ct_write_comptime_key(&state, id, &builder) { + if diagnose { + source.add( + checker.diagnostics, + checker.ast_module.exprs[expr].span, + "comptime argument has no stable comptime identity", + ) + } + return {}, false + } + key_view := strings.to_string(builder) + static_value := INVALID_CT_VALUE + key := "" + for existing, index in checker.comptime_keys { + if existing == key_view { + key = existing + static_value = checker.comptime_static_values[index] + break + } + } + if static_value == INVALID_CT_VALUE { + key = strings.clone(key_view, checker.allocator) + static_value = ct_clone_graph(&checker.static_state, &state, id) + append(&checker.comptime_keys, key) + append(&checker.comptime_static_values, static_value) + } + return Comptime_Value{ + name=name, + type=declared, + static_value=static_value, + key=key, + fingerprint=hash.fnv64a(transmute([]byte)key), + kind=.Static, + }, true +} + ct_eval_try_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { if state.defer_depth > 0 { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "cannot 'try' inside a 'defer'") diff --git a/compiler_tests.odin b/compiler_tests.odin index e3faea8..b0811f6 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2461,7 +2461,10 @@ main func(init process.Init) void { io.print(writer, "{d}", {"bro",}) catch |_| {} io.print(writer, "{d}{d}", {1,}) catch |_| {} io.print(writer, "{d}", {1, 2}) catch |_| {} - io.print(writer, "{x}", {}) catch |_| {} + io.print(writer, "{q}", {}) catch |_| {} + io.print(writer, "{b}", {1.5,}) catch |_| {} + io.print(writer, "{e}", {1,}) catch |_| {} + io.print(writer, "{c}", {i16(65),}) catch |_| {} io.print(writer, "}", {}) catch |_| {} } ` @@ -2481,16 +2484,19 @@ main func(init process.Init) void { hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) - found := [7]bool{} + found := [10]bool{} for diagnostic in diagnostics.items { message := diagnostic.message found[0] = found[0] || strings.contains(message, "arguments must be a tuple") found[1] = found[1] || strings.contains(message, "cannot implicitly convert i8 to []u8") - found[2] = found[2] || strings.contains(message, "'{d}' requires an integer") + found[2] = found[2] || strings.contains(message, "'{d}' requires an integer or float") found[3] = found[3] || strings.contains(message, "argument count does not match") found[4] = found[4] || strings.contains(message, "unknown specifier") found[5] = found[5] || strings.contains(message, "unmatched '}'") found[6] = found[6] || strings.contains(message, "compile-time-only metadata") + found[7] = found[7] || strings.contains(message, "integer format requires an integer argument") + found[8] = found[8] || strings.contains(message, "float format requires a float argument") + found[9] = found[9] || strings.contains(message, "'{c}' requires an unsigned integer that fits in u8") } testing.expect(t, loaded) for present in found { @@ -2498,6 +2504,121 @@ main func(init process.Init) void { } } +@(test) +milestone_39_stable_values_and_richer_formatting_compile_and_run :: proc(t: ^testing.T) { + stable_names: [dynamic]string + defer { + for name in stable_names { + delete(name) + } + delete(stable_names) + } + for pass := 0; pass < 2; pass += 1 { + sources := source.init_store() + diagnostics := source.init_store_diagnostics(&sources) + symbols := symbol.init_table() + ast_module, loaded := loader.load( + "examples/programs/milestone_39", &sources, &diagnostics, &symbols, project_root_path=".", + ) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + ir_module := lower.lower(&hir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + + score_count := 0 + for function in hir_module.functions { + if strings.contains(function.link_name, "bro__p0__score__ca") { + if pass == 0 { + append(&stable_names, strings.clone(function.link_name)) + } else { + testing.expect_value(t, function.link_name, stable_names[score_count]) + } + score_count += 1 + } + } + testing.expect(t, loaded) + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, score_count, 2) + testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__read_carrier__"), 1) + testing.expect(t, !strings.contains(llvm_text, "FormatToken")) + testing.expect(t, !strings.contains(llvm_text, "parse_format")) + testing.expect(t, !strings.contains(llvm_text, "FieldInfo")) + testing.expect(t, !strings.contains(llvm_text, "EnumInfo")) + + delete(llvm_text) + ir.destroy_module(&ir_module) + hir.destroy_module(&hir_module) + ast.destroy_module(&ast_module) + symbol.destroy_table(&symbols) + source.destroy_diagnostics(&diagnostics) + source.destroy_store(&sources) + } + + output := "/tmp/brolang-test-milestone-39" + defer _ = os.remove(output) + status := compiler_core.compile_package( + "examples/programs/milestone_39", output, nil, target.DEFAULT, cimport.Options{}, ".", + ) + testing.expect_value(t, status, 0) + state, stdout, stderr, _ := os2.process_exec( + os2.Process_Desc{command=[]string{output}}, context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + testing.expect_value(t, state.exit_code, 0) + testing.expect_value( + t, + string(stdout), + "true -42 1.5 .running bro 2.5 1010 12 ff FF A 1.5000000000000000e+00 {} -9223372036854775808 0 inf nan 1.50000000e+00\n", + ) + testing.expect_value(t, string(stderr), "debug=.idle 2a\n") +} + +@(test) +milestone_39_rejects_values_without_stable_identity :: proc(t: ^testing.T) { + text := `BadUnion :: union { number i32, flag bool } +Config :: struct { value i32 } + +identity func() i32 { return 1 } +reject_pointer func($value @i32) void {} +reject_function func($value @func() i32) void {} +reject_slice func($value []i32) void {} +reject_range func($value range) void {} +reject_union func($value BadUnion) void {} +reject_undefined func($value Config) void {} + +stored i32 :: 1 +items [2]i32 :: [1, 2] + +main func() void { + reject_pointer(&stored) + reject_function(identity) + reject_slice(items[..]) + reject_range(0..3) + reject_union(BadUnion {number = 1}) + reject_undefined(Config {value = undefined}) +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found := 0 + for diagnostic in diagnostics.items { + if strings.contains(diagnostic.message, "comptime argument has no stable comptime identity") { + found += 1 + } + } + testing.expect(t, found >= 6) +} + @(test) milestone_37_inline_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) { text := `main func() void { @@ -3327,12 +3448,13 @@ comptime_value_params_diagnose_invalid_uses :: proc(t: ^testing.T) { tiny func($N u8) i32 { return N } -bad_type func($T bool) void {} +good_bool func($T bool) void {} bad_use func($N usize) void { N = 1 _ = &N } main func() void { + good_bool(true) x usize = 4 _ = make(x) _ = make() @@ -3359,7 +3481,6 @@ main func() void { found_extra := false found_negative := false found_range := false - found_bad_type := false found_assignment := false found_address := false for diagnostic in diagnostics.items { @@ -3369,7 +3490,6 @@ main func() void { found_extra = found_extra || strings.contains(message, "has no unique complete argument mapping") found_negative = found_negative || strings.contains(message, "integer constant -1 does not fit in usize") found_range = found_range || strings.contains(message, "integer constant 300 does not fit in u8") - found_bad_type = found_bad_type || strings.contains(message, "requires type, a concrete integer type, or immutable []u8") found_assignment = found_assignment || strings.contains(message, "cannot assign comptime parameter 'N'") found_address = found_address || strings.contains(message, "'&' requires an addressable location") } @@ -3379,7 +3499,6 @@ main func() void { testing.expect(t, found_extra) testing.expect(t, found_negative) testing.expect(t, found_range) - testing.expect(t, found_bad_type) testing.expect(t, found_assignment) testing.expect(t, found_address) } diff --git a/examples/programs/milestone_39/main.bro b/examples/programs/milestone_39/main.bro new file mode 100644 index 0000000..a1de9cd --- /dev/null +++ b/examples/programs/milestone_39/main.bro @@ -0,0 +1,110 @@ +io :: import "@std/io" +process :: import "@std/process" +debug :: import "@std/debug" + +State :: enum { + idle + running +} + +Payload :: union(enum) { + count i32 + empty void +} + +Pair :: struct { i32, bool } + +Config :: struct { + enabled bool + ratio f64 + label []u8 + state State + values [2]i32 + pair Pair + maybe ?i32 + payload Payload +} + +first Config :: Config { + enabled = true, + ratio = 1.5, + label = "bro", + state = .running, + values = [20, 22], + pair = Pair {7, true}, + maybe = 9, + payload = Payload {count = 4}, +} + +same Config :: Config { + enabled = true, + ratio = 1.5, + label = "bro", + state = .running, + values = [20, 22], + pair = Pair {7, true}, + maybe = 9, + payload = Payload {count = 4}, +} + +different Config :: Config { + enabled = false, + ratio = 2.5, + label = "bro", + state = .idle, + values = [21, 21], + pair = Pair {7, false}, + maybe = none, + payload = .empty, +} + +score func($config Config) i32 { + return config.values[0] +} + +Carrier func($config Config) type { + return struct { value i32 } +} + +read_carrier func($config Config, carrier Carrier(config)) i32 { + return carrier.value + config.values[0] +} + +main func(init process.Init) i32 { + if score(first) != 20 or score(same) != 20 or score(different) != 21 { + return 1 + } + carrier Carrier(first) :: Carrier(first) {value = 22} + if read_carrier(carrier) != 42 { + return 2 + } + + writer io.Writer :: io.Writer {impl = init.io, stream = .stdout} + positive f64 = 1.0 + zero f64 = 0.0 + infinity f64 :: positive / zero + nan f64 :: zero / zero + io.print(writer, "{} {} {} {} {s} {d} {b} {o} {x} {X} {c} {e} {{}} {d} {b} {} {} {e}\n", { + true, + -42, + 1.5, + State.running, + "bro", + 2.5, + 10, + 10, + 255, + 255, + u8('A'), + 1.5, + minval!(i64), + u64(0), + infinity, + nan, + f32(1.5), + }) catch |_| { + return 3 + } + debug.print("debug={} {x}\n", {State.idle, 42}) + return 0 +} diff --git a/std/io/io.bro b/std/io/io.bro index b736643..6847aca 100644 --- a/std/io/io.bro +++ b/std/io/io.bro @@ -75,12 +75,12 @@ write_all func(writer Writer, bytes []u8) void ! WriteError { return } -hide write_decimal_signed func(writer Writer, value i64) void ! WriteError { - buffer [21]mut u8 = undefined +hide write_integer_signed func(writer Writer, value i64, base u64, uppercase bool) void ! WriteError { + buffer [65]mut u8 = undefined end usize = buffer.len current i64 = value while true { - digit_value i64 :: rem!(current, 10) + digit_value i64 :: rem!(current, i64(base)) digit u8 = 0 if digit_value < 0 { digit = u8(-digit_value) @@ -88,8 +88,14 @@ hide write_decimal_signed func(writer Writer, value i64) void ! WriteError { digit = u8(digit_value) } end -= 1 - buffer[end] = '0' + digit - current = divtrunc!(current, 10) + if digit < 10 { + buffer[end] = '0' + digit + } else if uppercase { + buffer[end] = 'A' + digit - 10 + } else { + buffer[end] = 'a' + digit - 10 + } + current = divtrunc!(current, i64(base)) if current == 0 { break } @@ -102,15 +108,21 @@ hide write_decimal_signed func(writer Writer, value i64) void ! WriteError { return } -hide write_decimal_unsigned func(writer Writer, value u64) void ! WriteError { - buffer [21]mut u8 = undefined +hide write_integer_unsigned func(writer Writer, value u64, base u64, uppercase bool) void ! WriteError { + buffer [65]mut u8 = undefined end usize = buffer.len current u64 = value while true { - digit u8 :: u8(rem!(current, 10)) + digit u8 :: u8(rem!(current, base)) end -= 1 - buffer[end] = '0' + digit - current = divtrunc!(current, 10) + if digit < 10 { + buffer[end] = '0' + digit + } else if uppercase { + buffer[end] = 'A' + digit - 10 + } else { + buffer[end] = 'a' + digit - 10 + } + current = divtrunc!(current, base) if current == 0 { break } @@ -122,8 +134,15 @@ hide write_decimal_unsigned func(writer Writer, value u64) void ! WriteError { hide FormatTokenKind :: enum { unused literal + default string decimal + binary + octal + hex_lower + hex_upper + character + scientific } hide FormatToken :: struct { @@ -171,16 +190,32 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { literal_start = cursor continue } - if cursor + 2 >= format.len or format[cursor + 2] != '}' { - compile_error!("io.print format expects '{s}' or '{d}'") - } - kind FormatTokenKind = .unused - if next == 's' { - kind = .string - } else if next == 'd' { - kind = .decimal - } else { - compile_error!("io.print format has an unknown specifier") + kind FormatTokenKind = .default + width usize = 2 + if next != '}' { + if cursor + 2 >= format.len or format[cursor + 2] != '}' { + compile_error!("io.print format expects a one-character specifier") + } + width = 3 + if next == 's' { + kind = .string + } else if next == 'd' { + kind = .decimal + } else if next == 'b' { + kind = .binary + } else if next == 'o' { + kind = .octal + } else if next == 'x' { + kind = .hex_lower + } else if next == 'X' { + kind = .hex_upper + } else if next == 'c' { + kind = .character + } else if next == 'e' { + kind = .scientific + } else { + compile_error!("io.print format has an unknown specifier") + } } if argument_count >= field_count { compile_error!("io.print format argument count does not match the tuple") @@ -193,7 +228,7 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { } token_count += 1 argument_count += 1 - cursor += 3 + cursor += width literal_start = cursor continue } @@ -229,14 +264,91 @@ hide format_field_name func($T type, index usize) []u8 { } } -hide write_decimal func(writer Writer, $T type, value T) void ! WriteError { +hide write_integer func(writer Writer, $T type, value T, base u64, uppercase bool) void ! WriteError { match typeinfo!(T) { .integer: if minval!(T) < 0 { - try write_decimal_signed(writer, i64(value)) + try write_integer_signed(writer, i64(value), base, uppercase) } else { - try write_decimal_unsigned(writer, u64(value)) + try write_integer_unsigned(writer, u64(value), base, uppercase) } - else: compile_error!("io.print '{d}' requires an integer argument") + else: compile_error!("io.print integer format requires an integer argument") + } + return +} + +# ponytail: libc keeps float formatting small; replace it with a native shortest-roundtrip writer if locale independence matters. +hide write_float func(writer Writer, $T type, value T, scientific bool) void ! WriteError { + match typeinfo!(T) { + .float: { + buffer [64]mut u8 = undefined + count c_int = 0 + if sizeof!(T) == 4 { + if scientific { + count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.8e", value) + } else { + count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.9g", value) + } + } else if scientific { + count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.16e", value) + } else { + count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.17g", value) + } + if count < 0 or usize(count) >= buffer.len { + return .write_failed + } + try write_all(writer, buffer[0..usize(count)]) + } + else: compile_error!("io.print float format requires a float argument") + } + return +} + +hide write_decimal func(writer Writer, $T type, value T) void ! WriteError { + match typeinfo!(T) { + .integer: try write_integer(writer, value, 10, false) + .float: try write_float(writer, value, false) + else: compile_error!("io.print '{d}' requires an integer or float argument") + } + return +} + +hide write_character func(writer Writer, $T type, value T) void ! WriteError { + match typeinfo!(T) { + .integer: { + if minval!(T) < 0 or maxval!(T) > 255 { + compile_error!("io.print '{c}' requires an unsigned integer that fits in u8") + } + buffer [1]u8 = [u8(value)] + try write_all(writer, buffer[..]) + } + else: compile_error!("io.print '{c}' requires an unsigned integer that fits in u8") + } + return +} + +hide write_default func(writer Writer, $T type, value T) void ! WriteError { + match typeinfo!(T) { + .bool: if value { + try write_all(writer, "true") + } else { + try write_all(writer, "false") + } + .integer: try write_integer(writer, value, 10, false) + .float: try write_float(writer, value, false) + .array: try write_all(writer, value) + .pointer: try write_all(writer, value) + .slice: try write_all(writer, value) + .enum |enum_info|: { + inline for enum_info.fields |field| { + if value == field!(T, field.name) { + try write_all(writer, ".") + try write_all(writer, field.name) + return + } + } + return .write_failed + } + else: compile_error!("io.print '{}' does not support this argument type") } return } @@ -259,7 +371,35 @@ print func( try write_all(writer, field!(args, token.field)) continue } - try write_decimal(writer, field!(args, token.field)) + if (token.kind == .default) { + try write_default(writer, field!(args, token.field)) + continue + } + if (token.kind == .decimal) { + try write_decimal(writer, field!(args, token.field)) + continue + } + if (token.kind == .binary) { + try write_integer(writer, field!(args, token.field), 2, false) + continue + } + if (token.kind == .octal) { + try write_integer(writer, field!(args, token.field), 8, false) + continue + } + if (token.kind == .hex_lower) { + try write_integer(writer, field!(args, token.field), 16, false) + continue + } + if (token.kind == .hex_upper) { + try write_integer(writer, field!(args, token.field), 16, true) + continue + } + if (token.kind == .character) { + try write_character(writer, field!(args, token.field)) + continue + } + try write_float(writer, field!(args, token.field), true) } return } diff --git a/std/meta/meta.bro b/std/meta/meta.bro index de21b76..709df7d 100644 --- a/std/meta/meta.bro +++ b/std/meta/meta.bro @@ -16,6 +16,10 @@ RecordInfo :: struct { layout Layout } +EnumInfo :: struct { + fields []FieldInfo +} + TypeInfo :: union(enum) { invalid void void void @@ -29,7 +33,7 @@ TypeInfo :: union(enum) { range void optional void function void - enum void + enum EnumInfo record RecordInfo union void fallible void