unsigned integer constraint (uint)

This commit is contained in:
2026-07-18 00:12:59 +02:00
parent b09787029d
commit ef91f13e7b
15 changed files with 4618 additions and 2849 deletions
+5 -4
View File
@@ -24,7 +24,7 @@ roadmap and milestone history.
### scalar, aggregate, and pointer types ### scalar, aggregate, and pointer types
- exact-width integers, `isize`, `usize`, `f32`, `f64`, `bool`, `void`, `anyopaque`, and contextual `int`, `float`, and `range` constraints - exact-width integers, concrete pointer-sized `isize` / `usize`, `f32`, `f64`, `bool`, `void`, and `anyopaque`; contextual `int` accepts the whole integer family, while `uint` accepts only unsigned native and target-classified C integers
- target-dependent C scalar primitives from `c_char` through `c_longdouble`, kept semantically distinct from native scalars - target-dependent C scalar primitives from `c_char` through `c_longdouble`, kept semantically distinct from native scalars
- contextual integer/float/character literals, backward type-demand inference through names and arithmetic, and compile-time folding for numeric constant expressions - contextual integer/float/character literals, backward type-demand inference through names and arithmetic, and compile-time folding for numeric constant expressions
- strict numeric conversion by default, widening where valid, C scalar coercions at C boundaries, and explicit scalar keyword casts such as `i32(x)` / `c_float(x)` - strict numeric conversion by default, widening where valid, C scalar coercions at C boundaries, and explicit scalar keyword casts such as `i32(x)` / `c_float(x)`
@@ -44,16 +44,17 @@ roadmap and milestone history.
#### native record constraint fields #### native record constraint fields
A direct `int`, `float`, or `range` field in a named native struct or union is a A direct `int`, `uint`, `float`, or `range` field in a named native struct or union is a
program-wide constraint, not per-value polymorphism. Before record layout, all reachable keyed program-wide constraint, not per-value polymorphism. Before record layout, all reachable keyed
constructors, field assignments, and concrete uses of field reads contribute demands and the field constructors, field assignments, and concrete uses of field reads contribute demands and the field
resolves once to one concrete runtime type. Compatible scalar demands widen normally. Integer resolves once to one concrete runtime type. Compatible scalar demands widen normally. Integer
literals remain provisional until inference settles, so a later `usize` use can resolve an `int` literals remain provisional until inference settles, so a later `usize` use can resolve an `int`
field to `usize`; otherwise literal-only `int` fields use the widest smallest-signed type required, field to `usize`; otherwise literal-only `int` fields use the widest smallest-signed type required,
and literal-only `float` fields use `f64`. literal-only `uint` fields use the widest smallest-unsigned type required, and literal-only `float`
fields use `f64`.
An undemanded field or incompatible demands are errors. This inference applies only to direct An undemanded field or incompatible demands are errors. This inference applies only to direct
fields of named native records. `c_struct` fields, nested constraints such as `[]int`, and fields in fields of named native records. `c_struct` fields, nested constraints such as `[]int` / `[]uint`, and fields in
anonymous generated records still require concrete runtime types. anonymous generated records still require concrete runtime types.
#### keyword member names #### keyword member names
+6
View File
@@ -907,6 +907,12 @@
- filtering, skipping, fixtures, snapshots, parallelism, isolation, allocators, and more assertion - filtering, skipping, fixtures, snapshots, parallelism, isolation, allocators, and more assertion
families remain deferred families remain deferred
42. unsigned integer constraint (implemented)
- `uint` is the unsigned subset constraint while `int` remains the whole integer family
- literal-only values choose the smallest fitting `u8`, `u16`, `u32`, or `u64`
- native unsigned scalars and target-classified unsigned C scalars satisfy the constraint
- `isize` and `usize` remain concrete pointer-sized types
## A word on unchecked casts ## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions: For casts that bypass safety checks, Honey provides builtin functions:
+97 -33
View File
@@ -501,7 +501,7 @@ write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: ty
strings.write_string(builder, "enum") strings.write_string(builder, "enum")
case .Alias, .Distinct, .Named: case .Alias, .Distinct, .Named:
write_type_label(checker, builder, item.child) write_type_label(checker, builder, item.child)
case .Invalid, .Void, .Anyopaque, .Int_Constraint, .Float_Constraint, .Range_Constraint, .Scalar: case .Invalid, .Void, .Anyopaque, .Int_Constraint, .Uint_Constraint, .Float_Constraint, .Range_Constraint, .Scalar:
strings.write_string(builder, types.name(value)) strings.write_string(builder, types.name(value))
} }
} }
@@ -1052,6 +1052,26 @@ fits_i64 :: proc(value: i128) -> bool {
return fits_signed_type(value, types.I64) return fits_signed_type(value, types.I64)
} }
fits_u64 :: proc(value: i128) -> bool {
return value >= 0 && value <= i128(0xffff_ffff_ffff_ffff)
}
constraint_integer_literal_type :: proc(constraint: types.Type, value: i128) -> types.Type {
if constraint == types.UINT {
return types.smallest_unsigned_for_literal(u64(value)) if fits_u64(value) else types.INVALID
}
return types.smallest_signed_for_literal(i64(value)) if fits_i64(value) else types.INVALID
}
constraint_recovery_type :: proc(checker: ^Checker, constraint: types.Type) -> types.Type {
switch constraint {
case types.UINT: return types.U64
case types.FLOAT: return types.F64
case types.RANGE: return types.range(&checker.module.types, types.I64)
case: return types.I64
}
}
type_from_syntax :: proc( type_from_syntax :: proc(
checker: ^Checker, checker: ^Checker,
value: ast.Type_Syntax, value: ast.Type_Syntax,
@@ -2647,7 +2667,7 @@ call_arg_expected :: proc(checker: ^Checker, function: ast.Function, index: int)
declared := type_from_syntax(checker, function.params[index].type, function.pkg, function.file) declared := type_from_syntax(checker, function.params[index].type, function.pkg, function.file)
// A `float` param defaults to f64 so an integer-literal argument builds as a // A `float` param defaults to f64 so an integer-literal argument builds as a
// float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals. // float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals.
// `int`/`range` constraints have no single default and keep building naturally. // Integer/range constraints keep building naturally from their literals.
if declared == types.FLOAT { if declared == types.FLOAT {
return types.F64 return types.F64
} }
@@ -3624,14 +3644,22 @@ record_field_expr_candidate :: proc(
} }
expr := checker.ast_module.exprs[expr_id] expr := checker.ast_module.exprs[expr_id]
constraint := checker.record_field_constraints[slot] constraint := checker.record_field_constraints[slot]
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); if !numeric_operand_is_open(checker, expr_id, locals, pkg, file) {
constant.kind == .Value && fits_i64(constant.value) { concrete := types.constraint_target(constraint, inferred, &checker.module.types)
candidate := types.smallest_signed_for_literal(i64(constant.value)) if is_runtime_type(checker, concrete) {
if constraint == types.FLOAT { return merge_record_field_demand(checker, slot, concrete, expr.span)
candidate = types.F64
} }
}
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file);
constant.kind == .Value {
candidate := constraint_integer_literal_type(constraint, constant.value)
if constraint == types.FLOAT {
candidate = types.F64 if fits_i64(constant.value) else types.INVALID
}
if types.is_valid(candidate) {
return merge_record_field_default(checker, slot, candidate, expr.span) return merge_record_field_default(checker, slot, candidate, expr.span)
} }
}
if is_float_constant_expr(checker, expr_id) { if is_float_constant_expr(checker, expr_id) {
return merge_record_field_default(checker, slot, types.F64, expr.span) return merge_record_field_default(checker, slot, types.F64, expr.span)
} }
@@ -3693,14 +3721,7 @@ finalize_record_field_inference :: proc(checker: ^Checker) {
) )
} }
if types.is_constraint(current) { if types.is_constraint(current) {
switch constraint { checker.module.types.fields[slot].type = constraint_recovery_type(checker, constraint)
case types.INT:
checker.module.types.fields[slot].type = types.I64
case types.FLOAT:
checker.module.types.fields[slot].type = types.F64
case types.RANGE:
checker.module.types.fields[slot].type = types.range(&checker.module.types, types.I64)
}
} }
} }
} }
@@ -3999,7 +4020,7 @@ find_spec :: proc(
} }
// specialized_param_type maps a parameter's declared type to its monomorphized // specialized_param_type maps a parameter's declared type to its monomorphized
// type for a given actual argument. A constraint param (`int`/`float`/`range`) // type for a given actual argument. A constraint param (`int`/`uint`/`float`/`range`)
// resolves to the actual's family member (INVALID if out of family), so a call // resolves to the actual's family member (INVALID if out of family), so a call
// passing an out-of-family argument fails to specialize and is rejected. // passing an out-of-family argument fails to specialize and is rejected.
specialized_param_type :: proc( specialized_param_type :: proc(
@@ -4426,14 +4447,16 @@ infer_expr :: proc(
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name { if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr) constant = eval_constant(checker, frame.expr)
} }
if constant.kind == .Overflow || constant.kind == .Div_By_Zero || if constant.kind == .Overflow || constant.kind == .Div_By_Zero {
(constant.kind == .Value && !fits_i64(constant.value)) {
last = types.I64 last = types.I64
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
if constant.kind == .Value { if constant.kind == .Value {
last = types.smallest_signed_for_literal(i64(constant.value)) last = constraint_integer_literal_type(frame.expected, constant.value)
if !types.is_valid(last) {
last = types.I64
}
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
@@ -5066,7 +5089,7 @@ infer_statements :: proc(
declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr) declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr)
value_type := types.INVALID value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) { if !is_undefined_expr(checker, statement.expr) {
expected := declared_local if is_runtime_type(checker, declared_local) else types.INVALID expected := declared_local if is_runtime_type(checker, declared_local) || declared_local == types.UINT else types.INVALID
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected) value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected)
declared_local = resolve_inferred_array_from_type(checker, declared_local, value_type) declared_local = resolve_inferred_array_from_type(checker, declared_local, value_type)
} }
@@ -5082,7 +5105,8 @@ infer_statements :: proc(
const_val := i128(0) const_val := i128(0)
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file) constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file)
if constant.kind == .Value && fits_i64(constant.value) { if constant.kind == .Value &&
(fits_i64(constant.value) || declared_local == types.UINT && fits_u64(constant.value)) {
open = true open = true
const_val = constant.value const_val = constant.value
} else if is_float_constant_expr(checker, statement.expr) { } else if is_float_constant_expr(checker, statement.expr) {
@@ -5123,11 +5147,25 @@ infer_statements :: proc(
expected_assignment := types.INVALID expected_assignment := types.INVALID
if statement.target != ast.INVALID_EXPR { if statement.target != ast.INVALID_EXPR {
expected_assignment = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types) expected_assignment = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types)
target_expr := checker.ast_module.exprs[statement.target]
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
locals^[local_index].declared == types.UINT {
expected_assignment = types.UINT
} else if global := find_global(checker, target_expr.name, pkg, file); global != ast.INVALID_GLOBAL {
ast_global := checker.ast_module.globals[global]
if type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file) == types.UINT {
expected_assignment = types.UINT
}
}
}
} else if statement.name != checker.sink_symbol { } else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok { if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok {
expected_assignment = locals^[local_index].type expected_assignment = types.UINT if locals^[local_index].declared == types.UINT else locals^[local_index].type
} else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL { } else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL {
expected_assignment = checker.global_types[global] ast_global := checker.ast_module.globals[global]
declared_global := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
expected_assignment = types.UINT if declared_global == types.UINT else checker.global_types[global]
} }
} }
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected_assignment) value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected_assignment)
@@ -5430,7 +5468,7 @@ infer_spec_locals_and_result :: proc(
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared = types.I32 declared = types.I32
} }
result_hint := declared if is_runtime_type(checker, declared) else types.INVALID result_hint := declared if is_runtime_type(checker, declared) || declared == types.UINT else types.INVALID
locals: [dynamic]Infer_Local locals: [dynamic]Infer_Local
locals.allocator = checker.allocator locals.allocator = checker.allocator
@@ -5615,7 +5653,7 @@ open_const_default_type :: proc(
if index, ok := find_infer_local_index(locals, expr.name); ok { if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index] local := locals[index]
if local.open_const { if local.open_const {
return types.smallest_signed_for_literal(i64(local.const_value)) return constraint_integer_literal_type(local.declared, local.const_value)
} }
if local.open_float { if local.open_float {
return types.F64 return types.F64
@@ -5633,7 +5671,9 @@ open_const_default_type :: proc(
return types.INVALID return types.INVALID
} }
if checker.global_open_const[index] { if checker.global_open_const[index] {
return types.smallest_signed_for_literal(i64(checker.global_const_value[index])) ast_global := checker.ast_module.globals[global]
declared := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
return constraint_integer_literal_type(declared, checker.global_const_value[index])
} }
if checker.global_open_float[index] { if checker.global_open_float[index] {
return types.F64 return types.F64
@@ -5899,7 +5939,8 @@ infer_all :: proc(checker: ^Checker) {
} }
} }
constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file) constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file)
if constant.kind == .Value && fits_i64(constant.value) { if constant.kind == .Value &&
(fits_i64(constant.value) || declared == types.UINT && fits_u64(constant.value)) {
checker.global_open_const[index] = true checker.global_open_const[index] = true
checker.global_const_value[index] = constant.value checker.global_const_value[index] = constant.value
} else if is_float_constant_expr(checker, global.expr) { } else if is_float_constant_expr(checker, global.expr) {
@@ -5947,7 +5988,7 @@ infer_all :: proc(checker: ^Checker) {
type_from_syntax(checker, global.type, global.pkg, global.file), type_from_syntax(checker, global.type, global.pkg, global.file),
global.expr, global.expr,
) )
expected := declared if is_runtime_type(checker, declared) else types.INVALID expected := declared if is_runtime_type(checker, declared) || declared == types.UINT else types.INVALID
inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file, expected=expected) inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file, expected=expected)
if resolved := resolve_inferred_array_from_type(checker, declared, inferred); if resolved := resolve_inferred_array_from_type(checker, declared, inferred);
resolved != declared { resolved != declared {
@@ -6020,7 +6061,8 @@ infer_all :: proc(checker: ^Checker) {
continue continue
} }
if checker.global_open_const[index] { if checker.global_open_const[index] {
checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index])) declared := type_from_syntax(checker, global.type, global.pkg, global.file)
checker.global_types[index] = constraint_integer_literal_type(declared, checker.global_const_value[index])
defaulted = true defaulted = true
} else if checker.global_open_float[index] { } else if checker.global_open_float[index] {
checker.global_types[index] = types.F64 checker.global_types[index] = types.F64
@@ -6394,7 +6436,7 @@ build_constant_expr :: proc(
return invalid_hir_expr(checker, expr.span, id, recovery_type) return invalid_hir_expr(checker, expr.span, id, recovery_type)
} }
if constant.kind == .Overflow || if constant.kind == .Overflow ||
(!types.is_concrete_integer(expected) && !fits_i64(constant.value)) { (!types.is_concrete_integer(expected) && expected != types.UINT && !fits_i64(constant.value)) {
id := source.add( id := source.add(
checker.diagnostics, checker.diagnostics,
expr.span, expr.span,
@@ -6407,7 +6449,16 @@ build_constant_expr :: proc(
if constant.value >= 0 && constant.value <= i128(0xffff_ffff_ffff_ffff) { if constant.value >= 0 && constant.value <= i128(0xffff_ffff_ffff_ffff) {
value = transmute(i64)u64(constant.value) value = transmute(i64)u64(constant.value)
} }
result_type := types.smallest_signed_for_literal(value) result_type := constraint_integer_literal_type(expected, constant.value)
if expected == types.UINT && !types.is_valid(result_type) {
id := source.addf(
checker.diagnostics,
expr.span,
"integer constant %d does not satisfy uint",
constant.value,
)
return invalid_hir_expr(checker, expr.span, id, types.U64)
}
if types.is_concrete_integer(expected) { if types.is_concrete_integer(expected) {
if !fits_integer_type(constant.value, expected, checker.target) { if !fits_integer_type(constant.value, expected, checker.target) {
id := source.addf( id := source.addf(
@@ -12375,11 +12426,12 @@ build_globals :: proc(checker: ^Checker) {
if is_runtime_type(checker, declared) { if is_runtime_type(checker, declared) {
expected = declared expected = declared
} else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file); } else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file);
constant.kind == .Value && fits_i64(constant.value) && constant.kind == .Value &&
(fits_i64(constant.value) || declared == types.UINT && fits_u64(constant.value)) &&
is_runtime_type(checker, checker.global_types[global_index]) { is_runtime_type(checker, checker.global_types[global_index]) {
// Open integer constant: build against its demanded/defaulted type. Gated // Open integer constant: build against its demanded/defaulted type. Gated
// to the infer-side open-constant condition so out-of-range constants keep // to the infer-side open-constant condition so out-of-range constants keep
// their original "exceeds signed i64 range" diagnostic. // their original range diagnostic.
expected = checker.global_types[global_index] expected = checker.global_types[global_index]
} else if (is_float_constant_expr(checker, global.expr) || } else if (is_float_constant_expr(checker, global.expr) ||
is_numeric_arithmetic_expr(checker, global.expr)) && is_numeric_arithmetic_expr(checker, global.expr)) &&
@@ -12401,6 +12453,18 @@ build_globals :: proc(checker: ^Checker) {
global_type = types.I64 global_type = types.I64
} }
} }
if diagnostic == source.INVALID_DIAGNOSTIC && types.is_constraint(declared) &&
!types.constraint_accepts(declared, global_type, &checker.module.types) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"could not resolve the '%s' constraint for global '%s'",
types.name(declared),
symbol_text(checker, global.name),
)
global_type = constraint_recovery_type(checker, declared)
expr = invalid_hir_expr(checker, global.span, diagnostic, global_type)
}
if !global.immutable && is_undefined_expr(checker, global.expr) { if !global.immutable && is_undefined_expr(checker, global.expr) {
diagnostic = source.add( diagnostic = source.add(
checker.diagnostics, checker.diagnostics,
+1
View File
@@ -52,6 +52,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "anyopaque": return .Keyword_Anyopaque case "anyopaque": return .Keyword_Anyopaque
case "bool": return .Keyword_Bool case "bool": return .Keyword_Bool
case "int": return .Keyword_Int case "int": return .Keyword_Int
case "uint": return .Keyword_Uint
case "float": return .Keyword_Float case "float": return .Keyword_Float
case "range": return .Keyword_Range case "range": return .Keyword_Range
case "i8": return .Keyword_I8 case "i8": return .Keyword_I8
+5 -2
View File
@@ -146,7 +146,7 @@ invalid_expr :: proc(parser: ^Parser, span: source.Span, message: string) -> ast
is_type_token :: proc(kind: token.Kind) -> bool { is_type_token :: proc(kind: token.Kind) -> bool {
#partial switch kind { #partial switch kind {
case .Keyword_Int, .Keyword_Float, .Keyword_Range, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64, case .Keyword_Int, .Keyword_Uint, .Keyword_Float, .Keyword_Range, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64,
.Keyword_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64, .Keyword_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64,
.Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64, .Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64,
.Keyword_C_Char, .Keyword_C_Schar, .Keyword_C_Uchar, .Keyword_C_Char, .Keyword_C_Schar, .Keyword_C_Uchar,
@@ -280,6 +280,9 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
case .Keyword_Int: case .Keyword_Int:
advance(parser) advance(parser)
return types.INT return types.INT
case .Keyword_Uint:
advance(parser)
return types.UINT
case .Keyword_Float: case .Keyword_Float:
advance(parser) advance(parser)
return types.FLOAT return types.FLOAT
@@ -774,7 +777,7 @@ parse_integer_magnitude :: proc(text: string) -> (u64, bool) {
parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
tok := current(parser) tok := current(parser)
#partial switch tok.kind { #partial switch tok.kind {
case .Keyword_Int, .Keyword_Float, .Keyword_Range, .Keyword_Void, .Keyword_Anyopaque, .Keyword_Bool: case .Keyword_Int, .Keyword_Uint, .Keyword_Float, .Keyword_Range, .Keyword_Void, .Keyword_Anyopaque, .Keyword_Bool:
start := tok start := tok
target := parse_type_atom(parser) target := parse_type_atom(parser)
return add_expr(parser, ast.Expr{ return add_expr(parser, ast.Expr{
+1
View File
@@ -88,6 +88,7 @@ Kind :: enum u8 {
Keyword_Anyopaque, Keyword_Anyopaque,
Keyword_Bool, Keyword_Bool,
Keyword_Int, Keyword_Int,
Keyword_Uint,
Keyword_Float, Keyword_Float,
Keyword_Range, Keyword_Range,
Keyword_I8, Keyword_I8,
+23 -1
View File
@@ -44,6 +44,7 @@ BOOL :: Type(29)
FLOAT :: Type(30) FLOAT :: Type(30)
RANGE :: Type(31) RANGE :: Type(31)
ANYOPAQUE :: Type(32) ANYOPAQUE :: Type(32)
UINT :: Type(33)
DYNAMIC_START :: Type(64) DYNAMIC_START :: Type(64)
@@ -59,6 +60,7 @@ Kind :: enum u8 {
Void, Void,
Anyopaque, Anyopaque,
Int_Constraint, Int_Constraint,
Uint_Constraint,
Float_Constraint, Float_Constraint,
Range_Constraint, Range_Constraint,
Scalar, Scalar,
@@ -595,6 +597,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind {
return .Anyopaque return .Anyopaque
case INT: case INT:
return .Int_Constraint return .Int_Constraint
case UINT:
return .Uint_Constraint
case FLOAT: case FLOAT:
return .Float_Constraint return .Float_Constraint
case RANGE: case RANGE:
@@ -642,7 +646,7 @@ is_bool :: proc(value: Type) -> bool {
} }
is_constraint :: proc(value: Type) -> bool { is_constraint :: proc(value: Type) -> bool {
return value == INT || value == FLOAT || value == RANGE return value == INT || value == UINT || value == FLOAT || value == RANGE
} }
// constraint_target reports the concrete type a constraint binding (local, or a // constraint_target reports the concrete type a constraint binding (local, or a
@@ -656,6 +660,8 @@ constraint_target :: proc(constraint, inferred: Type, store: ^Store = nil) -> Ty
switch constraint { switch constraint {
case INT: case INT:
return inferred if is_concrete_integer(inferred) else INVALID return inferred if is_concrete_integer(inferred) else INVALID
case UINT:
return inferred if is_unsigned(inferred) else INVALID
case FLOAT: case FLOAT:
if is_float(inferred) { if is_float(inferred) {
return inferred return inferred
@@ -673,6 +679,8 @@ constraint_accepts :: proc(constraint, concrete: Type, store: ^Store = nil) -> b
switch constraint { switch constraint {
case INT: case INT:
return is_concrete_integer(concrete) return is_concrete_integer(concrete)
case UINT:
return is_unsigned(concrete)
case FLOAT: case FLOAT:
return is_float(concrete) return is_float(concrete)
case RANGE: case RANGE:
@@ -1686,6 +1694,19 @@ smallest_signed_for_literal :: proc(value: i64) -> Type {
return I64 return I64
} }
smallest_unsigned_for_literal :: proc(value: u64) -> Type {
if value <= 255 {
return U8
}
if value <= 65535 {
return U16
}
if value <= 4294967295 {
return U32
}
return U64
}
name :: proc(value: Type) -> string { name :: proc(value: Type) -> string {
switch value { switch value {
case INVALID: return "<invalid>" case INVALID: return "<invalid>"
@@ -1693,6 +1714,7 @@ name :: proc(value: Type) -> string {
case ANYOPAQUE: return "anyopaque" case ANYOPAQUE: return "anyopaque"
case BOOL: return "bool" case BOOL: return "bool"
case INT: return "int" case INT: return "int"
case UINT: return "uint"
case FLOAT: return "float" case FLOAT: return "float"
case RANGE: return "range" case RANGE: return "range"
case I8: return "i8" case I8: return "i8"
+188 -1
View File
@@ -1485,6 +1485,7 @@ main func() void {
_ = maxval!(u8, u16) _ = maxval!(u8, u16)
_ = minval!(1) _ = minval!(1)
_ = maxval!(int) _ = maxval!(int)
_ = maxval!(uint)
_ = maxval!(f32) _ = maxval!(f32)
_ = maxval!(bool) _ = maxval!(bool)
_ = maxval!(Named) _ = maxval!(Named)
@@ -1513,7 +1514,7 @@ main func() void {
} }
testing.expect_value(t, bad_arity, 2) testing.expect_value(t, bad_arity, 2)
testing.expect(t, bad_type) testing.expect(t, bad_type)
testing.expect_value(t, bad_target, 5) testing.expect_value(t, bad_target, 6)
} }
@(test) @(test)
@@ -12876,3 +12877,189 @@ dependency_passes test {
testing.expect(t, strings.contains(output, root_path)) testing.expect(t, strings.contains(output, root_path))
testing.expect(t, strings.contains(output, "3 passed, 1 failed")) testing.expect(t, strings.contains(output, "3 passed, 1 failed"))
} }
@(test)
uint_constraint_parses_as_type_and_comptime_value :: proc(t: ^testing.T) {
text := `value uint :: 1
Constraint :: uint
`
source_file := source.Source{path="uint.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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.globals), 2)
testing.expect_value(t, module.globals[0].type, types.UINT)
testing.expect_value(t, module.exprs[module.globals[1].expr].kind, ast.Expr_Kind.Type)
testing.expect_value(t, module.exprs[module.globals[1].expr].type, types.UINT)
}
@(test)
uint_constraint_uses_smallest_unsigned_types_and_widens :: proc(t: ^testing.T) {
text := `Zero uint :: 0
Byte uint :: 255
Word uint :: 256
Dword uint :: 65536
Maximum uint :: 18446744073709551615
Counter uint = 1
widen func() uint {
value uint = 1
value = 1000
return value
}
widen_global func() void { Counter = 1000 }
main func() void {
_ = Zero
_ = Byte
_ = Word
_ = Dword
_ = Maximum
_ = widen()
widen_global()
}
`
source_file := source.Source{path="uint_types.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)
expected := []types.Type{types.U8, types.U8, types.U16, types.U32, types.U64, types.U16}
for value_type, index in expected {
testing.expect_value(t, hir_module.globals[index].type, value_type)
}
widen_symbol := symbol.intern(&symbols, "widen")
found_widen := false
for function in hir_module.functions {
if function.name != widen_symbol {
continue
}
found_widen = true
testing.expect_value(t, function.result, types.U16)
testing.expect_value(t, function.locals[0].type, types.U16)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_widen)
}
@(test)
uint_constraint_infers_boundaries_calls_and_records :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-uint-constraint"
main_path := "/tmp/brolang-test-uint-constraint/main.bro"
output := "/tmp/brolang-test-uint-constraint-output"
text := `Box :: struct { value uint }
identity func(value uint) uint { return value }
whole func(value int) int { return value }
max_value func() uint { return 18446744073709551615 }
make_box func(value usize) Box { return Box{value = value} }
main func() i32 {
zero uint :: 0
byte uint :: 255
word uint :: 256
word_max uint :: 65535
dword uint :: 65536
maximum uint :: 18446744073709551615
platform usize = 7
c_value c_uint = c_uint(9)
box Box = make_box(7)
signed isize = -1
if (identity(zero) != 0) return 1
if (identity(byte) != 255) return 2
if (identity(word) != 256) return 3
if (identity(word_max) != 65535) return 4
if (identity(dword) != 65536) return 5
if (identity(maximum) != 18446744073709551615) return 6
if (box.value != 7) return 7
if (signed != -1) return 8
if (identity(platform) != 7) return 9
if (identity(c_value) != c_value) return 10
if (whole(word) != 256) return 11
if (max_value() != 18446744073709551615) return 12
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
uint_constraint_rejects_other_families_and_recovers_records_as_u64 :: proc(t: ^testing.T) {
text := `Unresolved :: struct { value uint }
Conflict :: struct { value uint }
take func(value uint) uint { return value }
bad_result func() uint { return -1 }
bad func(signed i32, decimal f64) void {
_ = take(signed)
_ = take(decimal)
negative uint :: -1
a Conflict = Conflict{value = signed}
_ = negative
_ = a
}
Overflow uint :: 18446744073709551616
NegativeGlobal uint :: -1
main func() void {
bad(1, 1.0)
_ = bad_result()
}
`
source_file := source.Source{path="bad_uint.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)
signed_error := false
float_error := false
negative_error := false
field_error := false
overflow_error := false
global_error := false
result_error := false
for diagnostic in diagnostics.items {
signed_error = signed_error || strings.contains(diagnostic.message, "cannot pass i32 to 'uint' parameter 'value'")
float_error = float_error || strings.contains(diagnostic.message, "cannot pass f64 to 'uint' parameter 'value'")
negative_error = negative_error || strings.contains(diagnostic.message, "could not resolve the 'uint' constraint for local 'negative'")
field_error = field_error || strings.contains(diagnostic.message, "type i32 does not satisfy the 'uint' constraint for field 'Conflict.value'")
overflow_error = overflow_error || strings.contains(diagnostic.message, "magnitude does not fit in u64")
global_error = global_error || strings.contains(diagnostic.message, "could not resolve the 'uint' constraint for global 'NegativeGlobal'")
result_error = result_error || strings.contains(diagnostic.message, "could not resolve a concrete result type for 'bad_result'")
}
unresolved, unresolved_ok := named_record_field_type(&hir_module, &symbols, "Unresolved", "value")
testing.expect(t, signed_error && float_error && negative_error && field_error && overflow_error && global_error && result_error)
testing.expect(t, unresolved_ok)
testing.expect_value(t, unresolved, types.U64)
}
+1 -1
View File
@@ -9,5 +9,5 @@ languages = ["languages/brolang"]
[grammars.brolang] [grammars.brolang]
repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang" repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang"
rev = "2e8234c6a9a12326f230ea16e901ac41a1b48c99" rev = "71357b2866d2a558f80b6d5cc65761fb853e74d3"
path = "tree-sitter-brolang" path = "tree-sitter-brolang"
+1 -1
View File
@@ -228,7 +228,7 @@ module.exports = grammar({
_type_constant: $ => seq(optional('-'), choice($.integer, $.character)), _type_constant: $ => seq(optional('-'), choice($.integer, $.character)),
builtin_type: _ => choice( builtin_type: _ => choice(
'void', 'anyopaque', 'bool', 'int', 'float', 'range', 'void', 'anyopaque', 'bool', 'int', 'uint', 'float', 'range',
'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64',
'isize', 'usize', 'f32', 'f64', 'isize', 'usize', 'f32', 'f64',
'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort', 'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort',
+4
View File
@@ -1476,6 +1476,10 @@
"type": "STRING", "type": "STRING",
"value": "int" "value": "int"
}, },
{
"type": "STRING",
"value": "uint"
},
{ {
"type": "STRING", "type": "STRING",
"value": "float" "value": "float"
+4
View File
@@ -2815,6 +2815,10 @@
"type": "u8", "type": "u8",
"named": false "named": false
}, },
{
"type": "uint",
"named": false
},
{ {
"type": "undefined", "type": "undefined",
"named": true "named": true
File diff suppressed because it is too large Load Diff
@@ -493,3 +493,19 @@ visit func(kind Kind, value Value) void {
(sink)) (sink))
(expression (expression
(identifier)))))))))))) (identifier))))))))))))
==================
Unsigned constraint
==================
value uint :: 255
---
(source_file
(global_constant_declaration
(identifier)
(type
(builtin_type))
(expression
(integer))))
@@ -1,6 +1,9 @@
hide helper func() void {} hide helper func() void {}
# <- keyword # <- keyword
value uint :: 1
# ^^^^ type.builtin
main func() void { main func() void {
_ = sizeof!(i32) _ = sizeof!(i32)
# ^^^^^^ function.builtin # ^^^^^^ function.builtin