diff --git a/.llm/BETA_8_CONCRETE_TYPE_ALIASES.md b/.llm/BETA_8_CONCRETE_TYPE_ALIASES.md new file mode 100644 index 0000000..6132d98 --- /dev/null +++ b/.llm/BETA_8_CONCRETE_TYPE_ALIASES.md @@ -0,0 +1,60 @@ +# 1.0.0-beta.8 Concrete Type Alias Foundation + +Status: release scope for `1.0.0-beta.8`. + +`1.0.0-beta.8` targets a deliberately small language-usability slice: +transparent top-level aliases for existing concrete Slovo types. + +## Source Surface + +The declaration form is: + +```slo +(type Alias TargetType) +``` + +Examples: + +```slo +(type JsonText string) +(type Scores (vec i32)) +(type MaybeName (option string)) +(type ReadResult (result string i32)) +``` + +Aliases are module-local names. A later declaration in the same module may use +the alias wherever the resolved concrete target type is already supported. + +## Resolution Contract + +- Resolve aliases before typed-core lowering, checked import signatures, + backend layout, ABI decisions, and runtime behavior. +- Treat aliases as transparent names, not new nominal types. +- Preserve the target type's existing value-flow, constructor, operator, + `match`, field-access, import, and diagnostic behavior. +- Normalize exported function or struct signatures that mention local aliases + to their concrete target types for cross-module use. +- Generated documentation may show source alias spellings; runtime, checked + lowering, and cross-module typing must use the resolved concrete target. +- Reject failed alias resolution; do not silently fall back to a primitive type. + +## Source Fixtures + +- `lib/std/json.slo` uses local `JsonText` and `JsonField` aliases for already + encoded JSON fragments. +- `examples/projects/std-import-json/` uses the same alias shape in the + explicit standard-import fixture without importing aliases. +- `examples/projects/std-layout-local-json/` mirrors the facade and example as + a local source fixture. + +These fixtures keep the existing JSON helper surface and test names. They do +not add compiler-known runtime calls or new public standard-library helper +names. + +## Deferrals + +This target does not add generic aliases, parameterized aliases, alias type +parameters, alias re-exports, cross-module alias imports, import aliases, glob +imports, maps/sets, alias-driven overloads, implicit casts, runtime helpers, +hosted runtime symbols, stable ABI/layout promises, or a standard-library API +freeze. diff --git a/.llm/ROADMAP_TO_STABLE.md b/.llm/ROADMAP_TO_STABLE.md index 7141ed5..3599e86 100644 --- a/.llm/ROADMAP_TO_STABLE.md +++ b/.llm/ROADMAP_TO_STABLE.md @@ -24,15 +24,18 @@ implementation scope. networking (released in `1.0.0-beta.2` with read-only text file handles plus narrow filesystem status and mutation calls). 3. Stabilize `lib/std` module boundaries and document beta-vs-stable APIs. -4. Improve language usability around entry points, `match`, aliases, and - concrete numeric completeness. +4. Improve language usability around entry points, `match`, concrete aliases, + and concrete numeric completeness. 5. Expand package/workspace discipline before remote registry work. 6. Add networking only after resource/error policy is coherent. 7. Add serialization/data-interchange helpers before richer network libraries (released in `1.0.0-beta.7` with compact JSON text construction and JSON string quoting). -8. Design generics and collection unification from real stdlib duplication +8. Promote concrete type aliases before generics so long concrete vector, + option/result, array, JSON, and resource-handle signatures can be named + without changing runtime representation (released in `1.0.0-beta.8`). +9. Design generics and collection unification from real stdlib duplication pressure. -9. Add editor-facing diagnostics, watch mode, and generated documentation +10. Add editor-facing diagnostics, watch mode, and generated documentation improvements. -10. Freeze migration/deprecation policy and cut stable only after beta feedback. +11. Freeze migration/deprecation policy and cut stable only after beta feedback. diff --git a/.llm/reviews/BETA_8_RELEASE_REVIEW.md b/.llm/reviews/BETA_8_RELEASE_REVIEW.md new file mode 100644 index 0000000..41b9440 --- /dev/null +++ b/.llm/reviews/BETA_8_RELEASE_REVIEW.md @@ -0,0 +1,52 @@ +# 1.0.0-beta.8 Release Review + +Date: 2026-05-22 + +Scope: concrete type alias foundation. + +## Verdict + +The Slovo documentation and source-fixture side is coherent for a beta.8 +concrete-alias slice. The contract is intentionally transparent: top-level +`(type Alias TargetType)` declarations name existing supported concrete target +types, normalize before backend/ABI decisions, and do not create runtime types, +hosted symbols, generic alias machinery, maps/sets, or cross-module alias +visibility. + +## Review Notes + +- The language spec defines syntax, resolution, formatter shape, diagnostics, + and explicit deferrals for concrete aliases. +- The public roadmap and stable-readiness notes place beta.8 between JSON data + interchange and generics/collection unification. +- `std/json.slo` uses `JsonText` and `JsonField` as local aliases only. The + helper export list remains unchanged. +- The explicit std-import and local JSON fixtures exercise local alias use + without importing, exporting, or re-exporting aliases. +- Glagol parser, checker, formatter, project import resolution, lowering + inspectors, diagnostics fixtures, and promotion inventory now carry the + matching implementation side. +- Reviewer findings were integrated: JSON fixtures use canonical alias spacing, + unsupported alias targets are rejected, alias/value name conflicts are + diagnosed, and alias export/import attempts produce explicit visibility + diagnostics. + +## Verification + +- `cargo fmt --check` +- `cargo test --test diagnostics_contract current_negative_cases_match_machine_diagnostic_snapshots` +- `cargo test --test concrete_type_aliases_beta` +- `cargo test --test project_mode type_aliases_are_local_across_project_visibility` +- `cargo test --test lowering_inspector` +- `cargo test --test promotion_gate promotion_gate_artifacts_are_aligned` +- `cargo test --test standard_json_source_facade_alpha` +- `cargo test` +- `git diff --check` + +## Remaining Deferred Work + +- Generated documentation may still display source alias spelling. Runtime + behavior, checked lowering, backend layout, and cross-module typing use the + resolved concrete target. +- Any future generic alias, cross-module alias import/export, or re-export + design. diff --git a/README.md b/README.md index cfff8b4..4c4c061 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository is the canonical public monorepo for the language design, standard library source, compiler, runtime, examples, benchmarks, and technical documents. -Current release: `1.0.0-beta.7`. +Current release: `1.0.0-beta.8`. ## Repository Layout @@ -24,20 +24,22 @@ scripts/ local release and document tooling ## Beta Scope -`1.0.0-beta.7` keeps the `1.0.0-beta` language baseline, includes the +`1.0.0-beta.8` keeps the `1.0.0-beta` language baseline, includes the `1.0.0-beta.1` tooling/install hardening slice, the `1.0.0-beta.2` runtime/resource foundation bundle, the `1.0.0-beta.3` standard-library stabilization bundle, the `1.0.0-beta.4` language-usability diagnostics bundle, the `1.0.0-beta.5` local package/workspace discipline bundle, and the `1.0.0-beta.6` loopback networking foundation, plus the `1.0.0-beta.7` -serialization/data-interchange foundation. The language baseline supports -practical local command-line, file, and loopback-network programs with: +serialization/data-interchange foundation and the `1.0.0-beta.8` concrete type +alias foundation. The language baseline supports practical local command-line, +file, and loopback-network programs with: - modules, explicit imports, packages, and local workspaces - `new`, `check`, `fmt`, `test`, `doc`, and `build` - `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, and internal `unit` - structs, enums, fixed arrays, concrete vectors, option/result families, and current `match` +- module-local transparent concrete type aliases - explicit `std/*.slo` imports from `lib/std`, installed `share/slovo/std`, or `SLOVO_STD_PATH` - beta-scoped loopback TCP handles through `std.net` @@ -48,6 +50,11 @@ Still deferred before stable: generics, maps/sets, broad package registry semantics, DNS/TLS/async networking, LSP/watch/debug-adapter guarantees, stable ABI and layout, and a stable standard-library compatibility freeze. +The next likely language slice is `1.0.0-beta.9`, focused on the first +generics and collection-unification design pressure. It should build on the +alias foundation without adding maps/sets or standard-library API freeze claims +until the contract and gates are explicit. + ## Build And Test ```bash @@ -194,6 +201,21 @@ This is not a complete JSON library. Full parsing, recursive JSON values, maps/sets, streaming encoders, schema validation, Unicode normalization, and a stable data-interchange API freeze remain deferred. +## 1.0.0-beta.8 Concrete Type Alias Foundation + +The `1.0.0-beta.8` release adds transparent concrete type aliases: + +```slo +(type JsonText string) +``` + +Aliases are module-local names for already supported concrete target types. +They may appear in local signatures and annotations, but they do not create new +runtime representations or stable ABI names. Project imports of functions that +use aliases see the resolved concrete target type. Alias exports, imports, +re-exports, generic aliases, parameterized aliases, maps/sets, and new +compiler-known runtime names remain out of scope. + ## Documentation - [Language Manifest](docs/language/MANIFEST.md) diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock index de8c2b7..65fea67 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "glagol" -version = "1.0.0-beta.7" +version = "1.0.0-beta.8" diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index 4d0ebd8..c274425 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glagol" -version = "1.0.0-beta.7" +version = "1.0.0-beta.8" edition = "2021" description = "Glagol, the first compiler for the Slovo language" license = "MIT OR Apache-2.0" diff --git a/compiler/src/ast.rs b/compiler/src/ast.rs index 85893fe..b63ea45 100644 --- a/compiler/src/ast.rs +++ b/compiler/src/ast.rs @@ -3,6 +3,7 @@ use crate::{token::Span, types::Type}; #[derive(Debug, Clone)] pub struct Program { pub module: String, + pub type_aliases: Vec, pub enums: Vec, pub structs: Vec, pub c_imports: Vec, @@ -10,6 +11,15 @@ pub struct Program { pub tests: Vec, } +#[derive(Debug, Clone)] +pub struct TypeAliasDecl { + pub name: String, + pub name_span: Span, + pub target: Type, + pub target_span: Span, + pub span: Span, +} + #[derive(Debug, Clone)] pub struct EnumDecl { pub name: String, diff --git a/compiler/src/check.rs b/compiler/src/check.rs index 3226a60..0eca950 100644 --- a/compiler/src/check.rs +++ b/compiler/src/check.rs @@ -1,9 +1,9 @@ -use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; use crate::{ ast::{ BinaryOp, EnumDecl, Expr, ExprKind, Function, MatchArm, MatchPatternKind, Program, - StructDecl, StructInitField, Test, + StructDecl, StructInitField, Test, TypeAliasDecl, }, diag::Diagnostic, lower, std_runtime, @@ -294,13 +294,23 @@ pub fn check_program_with_imports( fn check_program_inner( file: &str, - program: Program, + mut program: Program, external_functions: &[ExternalFunction], external_structs: &[ExternalStruct], external_enums: &[ExternalEnum], project_resolution: bool, ) -> Result> { let mut errors = Vec::new(); + errors.extend(resolve_type_aliases( + file, + &mut program, + external_structs, + external_enums, + )); + if !errors.is_empty() { + return Err(errors); + } + let mut functions = HashMap::new(); let mut structs = HashMap::new(); let mut enums = HashMap::new(); @@ -580,6 +590,641 @@ fn is_reserved_callable_name(name: &str) -> bool { std_runtime::is_reserved_name(name) || unsafe_ops::is_reserved_head(name) } +fn resolve_type_aliases( + file: &str, + program: &mut Program, + external_structs: &[ExternalStruct], + external_enums: &[ExternalEnum], +) -> Vec { + if program.type_aliases.is_empty() { + return Vec::new(); + } + + let mut errors = Vec::new(); + let mut struct_names = HashMap::new(); + for struct_decl in &program.structs { + struct_names + .entry(struct_decl.name.clone()) + .or_insert(struct_decl.name_span); + } + for struct_decl in external_structs { + struct_names + .entry(struct_decl.name.clone()) + .or_insert(struct_decl.span); + } + + let mut enum_names = HashMap::new(); + for enum_decl in &program.enums { + enum_names + .entry(enum_decl.name.clone()) + .or_insert(enum_decl.name_span); + } + for enum_decl in external_enums { + enum_names + .entry(enum_decl.name.clone()) + .or_insert(enum_decl.span); + } + + let mut function_names = HashMap::new(); + for function in &program.functions { + function_names + .entry(function.name.clone()) + .or_insert(function.span); + } + let mut c_import_names = HashMap::new(); + for import in &program.c_imports { + c_import_names + .entry(import.name.clone()) + .or_insert(import.name_span); + } + + let mut aliases = HashMap::::new(); + for alias in &program.type_aliases { + match aliases.entry(alias.name.clone()) { + Entry::Vacant(entry) => { + entry.insert(alias); + } + Entry::Occupied(entry) => errors.push( + Diagnostic::new( + file, + "DuplicateTypeAlias", + format!("duplicate type alias `{}`", alias.name), + ) + .with_span(alias.name_span) + .related("original type alias", entry.get().name_span), + ), + } + } + + for alias in &program.type_aliases { + if is_reserved_type_name(&alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!( + "type alias `{}` conflicts with a built-in type name", + alias.name + ), + ) + .with_span(alias.name_span) + .hint("choose an alias name distinct from built-in type names"), + ); + } + if let Some(span) = struct_names.get(&alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!("type alias `{}` conflicts with a struct", alias.name), + ) + .with_span(alias.name_span) + .related("struct declaration", *span), + ); + } + if let Some(span) = enum_names.get(&alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!("type alias `{}` conflicts with an enum", alias.name), + ) + .with_span(alias.name_span) + .related("enum declaration", *span), + ); + } + if let Some(span) = function_names.get(&alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!("type alias `{}` conflicts with a function", alias.name), + ) + .with_span(alias.name_span) + .related("function declaration", *span), + ); + } + if let Some(span) = c_import_names.get(&alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!("type alias `{}` conflicts with a C import", alias.name), + ) + .with_span(alias.name_span) + .related("C import declaration", *span), + ); + } + if matches!(&alias.target, Type::Named(name) if name == &alias.name) { + errors.push( + Diagnostic::new( + file, + "SelfTypeAlias", + format!("type alias `{}` directly aliases itself", alias.name), + ) + .with_span(alias.target_span) + .hint("point the alias at an existing concrete type"), + ); + } + } + + if !errors.is_empty() { + return errors; + } + + let known_structs = struct_names.keys().cloned().collect::>(); + let known_enums = enum_names.keys().cloned().collect::>(); + let mut resolved = HashMap::new(); + let mut failed = HashSet::new(); + let mut reported_cycles = BTreeSet::new(); + let mut names = aliases.keys().cloned().collect::>(); + names.sort(); + for name in names { + resolve_alias_target( + file, + &name, + &aliases, + &known_structs, + &known_enums, + &mut resolved, + &mut failed, + &mut Vec::new(), + &mut reported_cycles, + &mut errors, + ); + } + + if !errors.is_empty() { + return errors; + } + + rewrite_program_alias_types(program, &resolved); + Vec::new() +} + +fn resolve_alias_target( + file: &str, + name: &str, + aliases: &HashMap, + structs: &HashSet, + enums: &HashSet, + resolved: &mut HashMap, + failed: &mut HashSet, + stack: &mut Vec, + reported_cycles: &mut BTreeSet, + errors: &mut Vec, +) -> Option { + if let Some(ty) = resolved.get(name) { + return Some(ty.clone()); + } + if failed.contains(name) { + return None; + } + if let Some(cycle_start) = stack.iter().position(|candidate| candidate == name) { + let cycle = stack[cycle_start..].to_vec(); + let mut key = cycle.clone(); + key.sort(); + if reported_cycles.insert(key.join("|")) { + errors.push(type_alias_cycle(file, &cycle, aliases)); + } + failed.extend(cycle); + return None; + } + + let alias = aliases.get(name).copied()?; + stack.push(name.to_string()); + let target = resolve_alias_target_type( + file, + &alias.target, + alias.target_span, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + ); + stack.pop(); + + if let Some(target) = target { + resolved.insert(name.to_string(), target.clone()); + Some(target) + } else { + failed.insert(name.to_string()); + None + } +} + +fn resolve_alias_target_type( + file: &str, + ty: &Type, + span: Span, + aliases: &HashMap, + structs: &HashSet, + enums: &HashSet, + resolved: &mut HashMap, + failed: &mut HashSet, + stack: &mut Vec, + reported_cycles: &mut BTreeSet, + errors: &mut Vec, +) -> Option { + match ty { + Type::Named(name) if aliases.contains_key(name) => resolve_alias_target( + file, + name, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + ), + Type::Named(name) if structs.contains(name) || enums.contains(name) => { + Some(Type::Named(name.clone())) + } + Type::Named(name) => { + errors.push( + Diagnostic::new( + file, + "UnknownTypeAliasTarget", + format!("type alias target `{}` is not a known concrete type", name), + ) + .with_span(span) + .expected("built-in type, known struct, known enum, or known type alias") + .found(name.clone()) + .hint("declare the target type or alias before checking this alias set"), + ); + None + } + Type::Array(inner, len) => { + let resolved_inner = resolve_alias_target_type( + file, + inner, + span, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + )?; + let target = Type::Array(Box::new(resolved_inner), *len); + if alias_target_type_supported(&target, structs, enums) { + Some(target) + } else { + errors.push(unsupported_type_alias_target(file, span, &target)); + None + } + } + Type::Vec(inner) => { + let resolved_inner = resolve_alias_target_type( + file, + inner, + span, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + )?; + let target = Type::Vec(Box::new(resolved_inner)); + if alias_target_type_supported(&target, structs, enums) { + Some(target) + } else { + errors.push(unsupported_type_alias_target(file, span, &target)); + None + } + } + Type::Option(inner) => { + let resolved_inner = resolve_alias_target_type( + file, + inner, + span, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + )?; + let target = Type::Option(Box::new(resolved_inner)); + if alias_target_type_supported(&target, structs, enums) { + Some(target) + } else { + errors.push(unsupported_type_alias_target(file, span, &target)); + None + } + } + Type::Result(ok, err) => { + let resolved_ok = resolve_alias_target_type( + file, + ok, + span, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + )?; + let resolved_err = resolve_alias_target_type( + file, + err, + span, + aliases, + structs, + enums, + resolved, + failed, + stack, + reported_cycles, + errors, + )?; + let target = Type::Result(Box::new(resolved_ok), Box::new(resolved_err)); + if alias_target_type_supported(&target, structs, enums) { + Some(target) + } else { + errors.push(unsupported_type_alias_target(file, span, &target)); + None + } + } + _ => { + if alias_target_type_supported(ty, structs, enums) { + Some(ty.clone()) + } else { + errors.push(unsupported_type_alias_target(file, span, ty)); + None + } + } + } +} + +fn alias_target_type_supported( + ty: &Type, + structs: &HashSet, + enums: &HashSet, +) -> bool { + match ty { + Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String => { + true + } + Type::Named(name) => structs.contains(name) || enums.contains(name), + Type::Array(inner, len) => { + *len > 0 + && (matches!( + &**inner, + Type::I32 + | Type::I64 + | Type::U32 + | Type::U64 + | Type::F64 + | Type::Bool + | Type::String + ) || matches!(&**inner, Type::Named(name) if structs.contains(name) || enums.contains(name))) + } + Type::Vec(inner) => matches!( + &**inner, + Type::I32 | Type::I64 | Type::F64 | Type::Bool | Type::String + ), + Type::Option(inner) => option_payload_type_supported(inner), + Type::Result(ok, err) => result_payload_types_supported(ok, err), + Type::Unit | Type::Ptr(_) | Type::Slice(_) => false, + } +} + +fn unsupported_type_alias_target(file: &str, span: Span, ty: &Type) -> Diagnostic { + Diagnostic::new( + file, + "UnsupportedTypeAliasTarget", + "type alias target type is not supported in beta.8", + ) + .with_span(span) + .expected("i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec") + .found(ty.to_string()) + .hint("aliases are transparent and may only target concrete types already supported in the target use positions") +} + +fn type_alias_cycle( + file: &str, + cycle: &[String], + aliases: &HashMap, +) -> Diagnostic { + let first = &cycle[0]; + let first_alias = aliases[first]; + let mut diag = Diagnostic::new( + file, + "TypeAliasCycle", + format!("type alias cycle includes `{}`", first), + ) + .with_span(first_alias.name_span) + .hint("type aliases must resolve to an existing non-alias concrete type"); + + for name in &cycle[1..] { + diag = diag.related( + format!("cycle also includes `{}`", name), + aliases[name].name_span, + ); + } + + diag +} + +fn rewrite_program_alias_types(program: &mut Program, aliases: &HashMap) { + for enum_decl in &mut program.enums { + for variant in &mut enum_decl.variants { + if let Some(payload_ty) = &mut variant.payload_ty { + *payload_ty = resolve_use_type(payload_ty, aliases); + } + } + } + + for struct_decl in &mut program.structs { + for field in &mut struct_decl.fields { + field.ty = resolve_use_type(&field.ty, aliases); + } + } + + for import in &mut program.c_imports { + for param in &mut import.params { + param.ty = resolve_use_type(¶m.ty, aliases); + } + import.return_type = resolve_use_type(&import.return_type, aliases); + } + + for function in &mut program.functions { + for param in &mut function.params { + param.ty = resolve_use_type(¶m.ty, aliases); + } + function.return_type = resolve_use_type(&function.return_type, aliases); + for expr in &mut function.body { + rewrite_expr_alias_types(expr, aliases); + } + } + + for test in &mut program.tests { + for expr in &mut test.body { + rewrite_expr_alias_types(expr, aliases); + } + } +} + +fn rewrite_expr_alias_types(expr: &mut Expr, aliases: &HashMap) { + match &mut expr.kind { + ExprKind::StructInit { fields, .. } => { + for field in fields { + rewrite_expr_alias_types(&mut field.expr, aliases); + } + } + ExprKind::ArrayInit { + elem_ty, elements, .. + } => { + *elem_ty = resolve_use_type(elem_ty, aliases); + for element in elements { + rewrite_expr_alias_types(element, aliases); + } + } + ExprKind::OptionSome { + payload_ty, value, .. + } => { + *payload_ty = resolve_use_type(payload_ty, aliases); + rewrite_expr_alias_types(value, aliases); + } + ExprKind::OptionNone { payload_ty, .. } => { + *payload_ty = resolve_use_type(payload_ty, aliases); + } + ExprKind::ResultOk { + ok_ty, + err_ty, + value, + .. + } + | ExprKind::ResultErr { + ok_ty, + err_ty, + value, + .. + } => { + *ok_ty = resolve_use_type(ok_ty, aliases); + *err_ty = resolve_use_type(err_ty, aliases); + rewrite_expr_alias_types(value, aliases); + } + ExprKind::OptionIsSome { value } + | ExprKind::OptionIsNone { value } + | ExprKind::OptionUnwrapSome { value } + | ExprKind::ResultIsOk { value, .. } + | ExprKind::ResultIsErr { value, .. } + | ExprKind::ResultUnwrapOk { value, .. } + | ExprKind::ResultUnwrapErr { value, .. } => rewrite_expr_alias_types(value, aliases), + ExprKind::EnumVariant { args, .. } | ExprKind::Call { args, .. } => { + for arg in args { + rewrite_expr_alias_types(arg, aliases); + } + } + ExprKind::FieldAccess { value, .. } => rewrite_expr_alias_types(value, aliases), + ExprKind::Index { array, index } => { + rewrite_expr_alias_types(array, aliases); + rewrite_expr_alias_types(index, aliases); + } + ExprKind::Local { ty, expr, .. } => { + *ty = resolve_use_type(ty, aliases); + rewrite_expr_alias_types(expr, aliases); + } + ExprKind::Set { expr, .. } => rewrite_expr_alias_types(expr, aliases), + ExprKind::Binary { left, right, .. } => { + rewrite_expr_alias_types(left, aliases); + rewrite_expr_alias_types(right, aliases); + } + ExprKind::If { + condition, + then_expr, + else_expr, + } => { + rewrite_expr_alias_types(condition, aliases); + rewrite_expr_alias_types(then_expr, aliases); + rewrite_expr_alias_types(else_expr, aliases); + } + ExprKind::Match { subject, arms } => { + rewrite_expr_alias_types(subject, aliases); + for arm in arms { + for expr in &mut arm.body { + rewrite_expr_alias_types(expr, aliases); + } + } + } + ExprKind::While { condition, body } => { + rewrite_expr_alias_types(condition, aliases); + for expr in body { + rewrite_expr_alias_types(expr, aliases); + } + } + ExprKind::Unsafe { body } => { + for expr in body { + rewrite_expr_alias_types(expr, aliases); + } + } + ExprKind::Int(_) + | ExprKind::Int64(_) + | ExprKind::UInt32(_) + | ExprKind::UInt64(_) + | ExprKind::Float(_) + | ExprKind::Bool(_) + | ExprKind::String(_) + | ExprKind::Var(_) => {} + } +} + +fn resolve_use_type(ty: &Type, aliases: &HashMap) -> Type { + match ty { + Type::Named(name) => aliases + .get(name) + .cloned() + .unwrap_or_else(|| Type::Named(name.clone())), + Type::Ptr(inner) => Type::Ptr(Box::new(resolve_use_type(inner, aliases))), + Type::Array(inner, len) => Type::Array(Box::new(resolve_use_type(inner, aliases)), *len), + Type::Vec(inner) => Type::Vec(Box::new(resolve_use_type(inner, aliases))), + Type::Slice(inner) => Type::Slice(Box::new(resolve_use_type(inner, aliases))), + Type::Option(inner) => Type::Option(Box::new(resolve_use_type(inner, aliases))), + Type::Result(ok, err) => Type::Result( + Box::new(resolve_use_type(ok, aliases)), + Box::new(resolve_use_type(err, aliases)), + ), + _ => ty.clone(), + } +} + +fn is_reserved_type_name(name: &str) -> bool { + matches!( + name, + "i32" + | "i64" + | "u32" + | "u64" + | "f64" + | "bool" + | "unit" + | "string" + | "ptr" + | "slice" + | "option" + | "result" + | "array" + | "vec" + ) +} + fn check_struct_decl( file: &str, struct_decl: &StructDecl, diff --git a/compiler/src/formatter.rs b/compiler/src/formatter.rs index 800dfb9..712bf3e 100644 --- a/compiler/src/formatter.rs +++ b/compiler/src/formatter.rs @@ -19,6 +19,7 @@ pub fn format(file: &str, source: &str, forms: &[SExpr]) -> Result { function_names: HashSet, struct_names: HashSet, enum_names: HashSet, + type_alias_names: HashSet, errors: Vec, } @@ -64,6 +66,7 @@ impl Formatter<'_> { Some("module") => self.write_module(form), Some("import") => self.write_import(form), Some("import_c") => self.write_c_import(form), + Some("type") => self.write_type_alias(form), Some("enum") => self.write_enum(form), Some("struct") => self.write_struct(form), Some("fn") => self.write_function(form), @@ -251,6 +254,59 @@ impl Formatter<'_> { ); } + fn write_type_alias(&mut self, form: &SExpr) { + let Some(items) = expect_list(form) else { + self.errors.push( + Diagnostic::new( + self.file, + "MalformedTypeAlias", + "type alias form must be a list", + ) + .with_span(form.span), + ); + return; + }; + + if items.len() != 3 { + self.errors.push( + Diagnostic::new( + self.file, + "MalformedTypeAlias", + "type alias form must be `(type Alias TargetType)`", + ) + .with_span(form.span), + ); + return; + } + + let Some(name) = expect_ident(&items[1]) else { + self.errors.push( + Diagnostic::new( + self.file, + "InvalidTypeAliasName", + "type alias name must be an identifier", + ) + .with_span(items[1].span), + ); + return; + }; + + let Some(target) = self.render_alias_target_type(&items[2]) else { + return; + }; + + self.reject_comments_before( + form.span.end, + "formatter does not support comments inside type alias forms", + ); + + self.output.push_str("(type "); + self.output.push_str(name); + self.output.push(' '); + self.output.push_str(&target); + self.output.push(')'); + } + fn write_struct(&mut self, form: &SExpr) { let Some(items) = expect_list(form) else { self.errors.push( @@ -512,17 +568,17 @@ impl Formatter<'_> { } else if is_ident(&variant_items[1], "string") { "string".to_string() } else if let Some(name) = expect_ident(&variant_items[1]) { - if self.struct_names.contains(name) { + if self.struct_names.contains(name) || self.type_alias_names.contains(name) { name.to_string() } else { self.errors.push( Diagnostic::new( self.file, "UnsupportedFormatterForm", - "formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants", + "formatter supports only unary direct i32, i64, f64, bool, string, known type aliases, and known non-recursive struct enum payload variants", ) .with_span(variant_items[1].span) - .expected("i32, i64, f64, bool, string, or known non-recursive struct type"), + .expected("i32, i64, f64, bool, string, known type alias, or known non-recursive struct type"), ); continue; } @@ -531,10 +587,10 @@ impl Formatter<'_> { Diagnostic::new( self.file, "UnsupportedFormatterForm", - "formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants", + "formatter supports only unary direct i32, i64, f64, bool, string, known type aliases, and known non-recursive struct enum payload variants", ) .with_span(variant_items[1].span) - .expected("i32, i64, f64, bool, string, or known non-recursive struct type"), + .expected("i32, i64, f64, bool, string, known type alias, or known non-recursive struct type"), ); continue; }; @@ -847,7 +903,10 @@ impl Formatter<'_> { } else if is_ident(&pair[1], "string") { "string".to_string() } else if let Some(name) = expect_ident(&pair[1]) { - if self.struct_names.contains(name) || self.enum_names.contains(name) { + if self.struct_names.contains(name) + || self.enum_names.contains(name) + || self.type_alias_names.contains(name) + { name.to_string() } else { self.errors.push( @@ -861,14 +920,18 @@ impl Formatter<'_> { continue; } } else if let Some(items) = expect_list(&pair[1]) { - if let Some(text) = render_option_result_type(items) { + if let Some(text) = render_option_result_type(items, &self.type_alias_names) { text - } else if let Some(text) = - render_supported_array_type(items, &self.struct_names, &self.enum_names) + } else if let Some(text) = render_supported_array_type( + items, + &self.struct_names, + &self.enum_names, + &self.type_alias_names, + ) { + text + } else if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { text - } else if let Some(text) = render_supported_vec_type(items) { - text } else { self.errors.push( Diagnostic::new( @@ -946,7 +1009,10 @@ impl Formatter<'_> { } if let Some(name) = expect_ident(form) { - if self.struct_names.contains(name) || self.enum_names.contains(name) { + if self.struct_names.contains(name) + || self.enum_names.contains(name) + || self.type_alias_names.contains(name) + { return Some(name.to_string()); } } @@ -963,16 +1029,20 @@ impl Formatter<'_> { return None; }; - if let Some(text) = render_option_result_type(items) { + if let Some(text) = render_option_result_type(items, &self.type_alias_names) { return Some(text); } - if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names) - { + if let Some(text) = render_supported_array_type( + items, + &self.struct_names, + &self.enum_names, + &self.type_alias_names, + ) { return Some(text); } - if let Some(text) = render_supported_vec_type(items) { + if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(text); } @@ -987,6 +1057,58 @@ impl Formatter<'_> { None } + fn render_alias_target_type(&mut self, form: &SExpr) -> Option { + if let Some(text) = render_direct_type_atom( + form, + &self.struct_names, + &self.enum_names, + &self.type_alias_names, + ) { + return Some(text); + } + + let Some(items) = expect_list(form) else { + self.errors.push( + Diagnostic::new( + self.file, + "InvalidTypeAliasTarget", + "type alias target type is invalid", + ) + .with_span(form.span) + .expected("concrete type"), + ); + return None; + }; + + if let Some(text) = render_option_result_type(items, &self.type_alias_names) { + return Some(text); + } + + if let Some(text) = render_supported_array_type( + items, + &self.struct_names, + &self.enum_names, + &self.type_alias_names, + ) { + return Some(text); + } + + if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { + return Some(text); + } + + self.errors.push( + Diagnostic::new( + self.file, + "InvalidTypeAliasTarget", + "type alias target type is invalid", + ) + .with_span(form.span) + .expected("concrete type"), + ); + None + } + fn render_c_import_return_type(&mut self, form: &SExpr) -> Option { if is_ident(form, "i32") { return Some("i32".to_string()); @@ -1318,6 +1440,12 @@ impl Formatter<'_> { is_array: false, }); } + if self.type_alias_names.contains(name) { + return Some(RenderedType { + text: name.to_string(), + is_array: false, + }); + } } let Some(items) = expect_list(form) else { @@ -1332,14 +1460,14 @@ impl Formatter<'_> { return None; }; - if let Some(text) = render_option_result_type(items) { + if let Some(text) = render_option_result_type(items, &self.type_alias_names) { return Some(RenderedType { text, is_array: false, }); } - if let Some(text) = render_supported_vec_type(items) { + if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(RenderedType { text, is_array: false, @@ -1362,6 +1490,7 @@ impl Formatter<'_> { &items[1], &self.struct_names, &self.enum_names, + &self.type_alias_names, ) else { self.errors.push( Diagnostic::new( @@ -1422,7 +1551,10 @@ impl Formatter<'_> { } if let Some(name) = expect_ident(form) { - if self.struct_names.contains(name) || self.enum_names.contains(name) { + if self.struct_names.contains(name) + || self.enum_names.contains(name) + || self.type_alias_names.contains(name) + { return Some(name.to_string()); } } @@ -1439,16 +1571,20 @@ impl Formatter<'_> { return None; }; - if let Some(text) = render_option_result_type(items) { + if let Some(text) = render_option_result_type(items, &self.type_alias_names) { return Some(text); } - if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names) - { + if let Some(text) = render_supported_array_type( + items, + &self.struct_names, + &self.enum_names, + &self.type_alias_names, + ) { return Some(text); } - if let Some(text) = render_supported_vec_type(items) { + if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(text); } @@ -2051,6 +2187,7 @@ impl Formatter<'_> { &items[1], &self.struct_names, &self.enum_names, + &self.type_alias_names, ) else { self.errors.push( Diagnostic::new( @@ -2113,21 +2250,7 @@ impl Formatter<'_> { return None; } - let payload_type = if is_ident(&items[1], "i32") { - "i32" - } else if is_ident(&items[1], "i64") { - "i64" - } else if is_ident(&items[1], "u32") { - "u32" - } else if is_ident(&items[1], "u64") { - "u64" - } else if is_ident(&items[1], "f64") { - "f64" - } else if is_ident(&items[1], "bool") { - "bool" - } else if is_ident(&items[1], "string") { - "string" - } else { + let Some(payload_type) = render_payload_type_atom(&items[1], &self.type_alias_names) else { self.errors.push( Diagnostic::new( self.file, @@ -2169,21 +2292,18 @@ impl Formatter<'_> { return None; } - let result_type = if is_ident(&items[1], "i32") && is_ident(&items[2], "i32") { - "i32 i32" - } else if is_ident(&items[1], "i64") && is_ident(&items[2], "i32") { - "i64 i32" - } else if is_ident(&items[1], "u32") && is_ident(&items[2], "i32") { - "u32 i32" - } else if is_ident(&items[1], "u64") && is_ident(&items[2], "i32") { - "u64 i32" - } else if is_ident(&items[1], "f64") && is_ident(&items[2], "i32") { - "f64 i32" - } else if is_ident(&items[1], "bool") && is_ident(&items[2], "i32") { - "bool i32" - } else if is_ident(&items[1], "string") && is_ident(&items[2], "i32") { - "string i32" - } else { + let Some(ok_ty) = render_payload_type_atom(&items[1], &self.type_alias_names) else { + self.errors.push( + Diagnostic::new( + self.file, + "UnsupportedFormatterForm", + "formatter supports only `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, and `(result string i32)` constructors", + ) + .with_span(span), + ); + return None; + }; + let Some(err_ty) = render_result_err_type_atom(&items[2], &self.type_alias_names) else { self.errors.push( Diagnostic::new( self.file, @@ -2196,7 +2316,7 @@ impl Formatter<'_> { }; let value = self.render_expr(&items[3], env)?; - Some(format!("({} {} {})", name, result_type, value)) + Some(format!("({} {} {} {})", name, ok_ty, err_ty, value)) } fn render_call( @@ -2355,6 +2475,19 @@ fn collect_enum_names(forms: &[SExpr]) -> HashSet { .collect() } +fn collect_type_alias_names(forms: &[SExpr]) -> HashSet { + forms + .iter() + .filter_map(|form| { + let items = expect_list(form)?; + if !is_ident(items.first()?, "type") { + return None; + } + expect_ident(items.get(1)?).map(str::to_string) + }) + .collect() +} + fn qualified_enum_name(name: &str) -> Option<(&str, &str)> { let (enum_name, variant) = name.split_once('.')?; if enum_name.is_empty() || variant.is_empty() || variant.contains('.') { @@ -2467,89 +2600,19 @@ fn list_head(form: &SExpr) -> Option<&str> { expect_ident(first) } -fn render_option_result_type(items: &[SExpr]) -> Option { - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i32") { - return Some("(option i32)".to_string()); +fn render_option_result_type( + items: &[SExpr], + type_alias_names: &HashSet, +) -> Option { + if items.len() == 2 && is_ident(&items[0], "option") { + let payload = render_payload_type_atom(&items[1], type_alias_names)?; + return Some(format!("(option {})", payload)); } - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i64") { - return Some("(option i64)".to_string()); - } - - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u32") { - return Some("(option u32)".to_string()); - } - - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u64") { - return Some("(option u64)".to_string()); - } - - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "f64") { - return Some("(option f64)".to_string()); - } - - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "bool") { - return Some("(option bool)".to_string()); - } - - if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "string") { - return Some("(option string)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "i32") - && is_ident(&items[2], "i32") - { - return Some("(result i32 i32)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "i64") - && is_ident(&items[2], "i32") - { - return Some("(result i64 i32)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "u32") - && is_ident(&items[2], "i32") - { - return Some("(result u32 i32)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "u64") - && is_ident(&items[2], "i32") - { - return Some("(result u64 i32)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "f64") - && is_ident(&items[2], "i32") - { - return Some("(result f64 i32)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "bool") - && is_ident(&items[2], "i32") - { - return Some("(result bool i32)".to_string()); - } - - if items.len() == 3 - && is_ident(&items[0], "result") - && is_ident(&items[1], "string") - && is_ident(&items[2], "i32") - { - return Some("(result string i32)".to_string()); + if items.len() == 3 && is_ident(&items[0], "result") { + let ok = render_payload_type_atom(&items[1], type_alias_names)?; + let err = render_result_err_type_atom(&items[2], type_alias_names)?; + return Some(format!("(result {} {})", ok, err)); } None @@ -2559,42 +2622,27 @@ fn render_supported_array_constructor_type( form: &SExpr, struct_names: &HashSet, enum_names: &HashSet, + type_alias_names: &HashSet, ) -> Option { - if is_ident(form, "i32") { - Some("i32".to_string()) - } else if is_ident(form, "i64") { - Some("i64".to_string()) - } else if is_ident(form, "u32") { - Some("u32".to_string()) - } else if is_ident(form, "u64") { - Some("u64".to_string()) - } else if is_ident(form, "f64") { - Some("f64".to_string()) - } else if is_ident(form, "bool") { - Some("bool".to_string()) - } else if is_ident(form, "string") { - Some("string".to_string()) - } else if let Some(name) = expect_ident(form) { - if struct_names.contains(name) || enum_names.contains(name) { - Some(name.to_string()) - } else { - None - } - } else { - None - } + render_direct_type_atom(form, struct_names, enum_names, type_alias_names) } fn render_supported_array_type( items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, + type_alias_names: &HashSet, ) -> Option { if items.len() != 3 || !is_ident(&items[0], "array") { return None; } - let elem_ty = render_supported_array_constructor_type(&items[1], struct_names, enum_names)?; + let elem_ty = render_supported_array_constructor_type( + &items[1], + struct_names, + enum_names, + type_alias_names, + )?; let len = expect_int(&items[2])?; if len <= 0 { return None; @@ -2603,7 +2651,10 @@ fn render_supported_array_type( Some(format!("(array {} {})", elem_ty, len)) } -fn render_supported_vec_type(items: &[SExpr]) -> Option { +fn render_supported_vec_type( + items: &[SExpr], + type_alias_names: &HashSet, +) -> Option { if items.len() == 2 && is_ident(&items[0], "vec") { if is_ident(&items[1], "i32") { return Some("(vec i32)".to_string()); @@ -2620,11 +2671,66 @@ fn render_supported_vec_type(items: &[SExpr]) -> Option { if is_ident(&items[1], "string") { return Some("(vec string)".to_string()); } + if let Some(name) = expect_ident(&items[1]) { + if type_alias_names.contains(name) { + return Some(format!("(vec {})", name)); + } + } } None } +fn render_payload_type_atom(form: &SExpr, type_alias_names: &HashSet) -> Option { + if is_ident(form, "i32") { + Some("i32".to_string()) + } else if is_ident(form, "i64") { + Some("i64".to_string()) + } else if is_ident(form, "u32") { + Some("u32".to_string()) + } else if is_ident(form, "u64") { + Some("u64".to_string()) + } else if is_ident(form, "f64") { + Some("f64".to_string()) + } else if is_ident(form, "bool") { + Some("bool".to_string()) + } else if is_ident(form, "string") { + Some("string".to_string()) + } else if let Some(name) = expect_ident(form) { + type_alias_names.contains(name).then(|| name.to_string()) + } else { + None + } +} + +fn render_result_err_type_atom(form: &SExpr, type_alias_names: &HashSet) -> Option { + if is_ident(form, "i32") { + Some("i32".to_string()) + } else if let Some(name) = expect_ident(form) { + type_alias_names.contains(name).then(|| name.to_string()) + } else { + None + } +} + +fn render_direct_type_atom( + form: &SExpr, + struct_names: &HashSet, + enum_names: &HashSet, + type_alias_names: &HashSet, +) -> Option { + if let Some(text) = render_payload_type_atom(form, type_alias_names) { + return Some(text); + } + + let name = expect_ident(form)?; + if struct_names.contains(name) || enum_names.contains(name) { + Some(name.to_string()) + } else { + None + } +} + fn expect_list(form: &SExpr) -> Option<&[SExpr]> { match &form.kind { SExprKind::List(items) => Some(items), diff --git a/compiler/src/lower.rs b/compiler/src/lower.rs index 517c580..43cba05 100644 --- a/compiler/src/lower.rs +++ b/compiler/src/lower.rs @@ -5,7 +5,7 @@ use crate::{ ast::{ BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm, MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField, - Test, + Test, TypeAliasDecl, }, diag::Diagnostic, sexpr::{Atom, SExpr, SExprKind}, @@ -23,6 +23,8 @@ pub fn lower_program_with_imported_names( imported_names: &[String], ) -> Result> { let mut module = None; + let mut type_aliases = Vec::new(); + let mut alias_names = HashMap::new(); let mut enums = Vec::new(); let mut enum_names = imported_names.iter().cloned().collect::>(); let mut structs = Vec::new(); @@ -49,10 +51,67 @@ pub fn lower_program_with_imported_names( } Err(mut errs) => errors.append(&mut errs), }, + Some("type") => match lower_type_alias(file, form) { + Ok(alias) => match alias_names.entry(alias.name.clone()) { + Entry::Vacant(entry) => { + entry.insert(alias.name_span); + type_aliases.push(alias); + } + Entry::Occupied(entry) => errors.push( + Diagnostic::new( + file, + "DuplicateTypeAlias", + format!("duplicate type alias `{}`", alias.name), + ) + .with_span(alias.name_span) + .related("original type alias", *entry.get()), + ), + }, + Err(mut errs) => errors.append(&mut errs), + }, _ => {} } } + for alias in &type_aliases { + if is_reserved_type_name(&alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!( + "type alias `{}` conflicts with a built-in type name", + alias.name + ), + ) + .with_span(alias.name_span) + .hint("choose an alias name distinct from built-in type names"), + ); + } + if let Some(struct_decl) = structs.iter().find(|decl| decl.name == alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!("type alias `{}` conflicts with a struct", alias.name), + ) + .with_span(alias.name_span) + .related("struct declaration", struct_decl.name_span), + ); + } + if let Some(enum_decl) = enums.iter().find(|decl| decl.name == alias.name) { + errors.push( + Diagnostic::new( + file, + "TypeAliasNameConflict", + format!("type alias `{}` conflicts with an enum", alias.name), + ) + .with_span(alias.name_span) + .related("enum declaration", enum_decl.name_span), + ); + } + } + for form in forms { match list_head(form) { Some("module") => match lower_module(file, form) { @@ -61,6 +120,7 @@ pub fn lower_program_with_imported_names( }, Some("enum") => {} Some("struct") => {} + Some("type") => {} Some("import_c") => match lower_c_import(file, form) { Ok(import) => c_imports.push(import), Err(mut errs) => errors.append(&mut errs), @@ -106,6 +166,7 @@ pub fn lower_program_with_imported_names( if errors.is_empty() { Ok(Program { module: module.unwrap_or_else(|| "main".to_string()), + type_aliases, enums, structs, c_imports, @@ -124,6 +185,14 @@ pub fn print_program(program: &Program) -> String { output.push_str(&program.module); output.push('\n'); + for alias in &program.type_aliases { + output.push_str(" type "); + output.push_str(&alias.name); + output.push_str(" = "); + output.push_str(&alias.target.to_string()); + output.push('\n'); + } + for enum_decl in &program.enums { output.push_str(" enum "); output.push_str(&enum_decl.name); @@ -543,6 +612,71 @@ fn lower_module(file: &str, form: &SExpr) -> Result { }) } +fn lower_type_alias(file: &str, form: &SExpr) -> Result> { + let mut errors = Vec::new(); + let Some(items) = expect_list(form) else { + return Err(vec![Diagnostic::new( + file, + "MalformedTypeAlias", + "type alias form must be a list", + ) + .with_span(form.span)]); + }; + + if items.len() != 3 { + return Err(vec![Diagnostic::new( + file, + "MalformedTypeAlias", + "type alias form must be `(type Alias TargetType)`", + ) + .with_span(form.span) + .expected("(type Alias TargetType)")]); + } + + let name = match expect_ident(&items[1]) { + Some(name) => name.to_string(), + None => { + errors.push( + Diagnostic::new( + file, + "InvalidTypeAliasName", + "type alias name must be an identifier", + ) + .with_span(items[1].span), + ); + "".to_string() + } + }; + + let target = match lower_type(&items[2]) { + Some(ty) => ty, + None => { + errors.push( + Diagnostic::new( + file, + "InvalidTypeAliasTarget", + "type alias target type is invalid", + ) + .with_span(items[2].span) + .expected("concrete type"), + ); + Type::Unit + } + }; + + if errors.is_empty() { + Ok(TypeAliasDecl { + name, + name_span: items[1].span, + target, + target_span: items[2].span, + span: form.span, + }) + } else { + Err(errors) + } +} + fn lower_struct(file: &str, form: &SExpr) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { @@ -1249,6 +1383,26 @@ fn unsupported_unit_return_signature(file: &str, span: crate::token::Span) -> Di .hint("`unit` is reserved for compiler/runtime unit-producing forms") } +fn is_reserved_type_name(name: &str) -> bool { + matches!( + name, + "i32" + | "i64" + | "u32" + | "u64" + | "f64" + | "bool" + | "unit" + | "string" + | "ptr" + | "slice" + | "option" + | "result" + | "array" + | "vec" + ) +} + fn lower_type(form: &SExpr) -> Option { match &form.kind { SExprKind::Atom(Atom::Ident(name)) => match name.as_str() { diff --git a/compiler/src/project.rs b/compiler/src/project.rs index e758567..6fa1c84 100644 --- a/compiler/src/project.rs +++ b/compiler/src/project.rs @@ -177,6 +177,7 @@ struct ModuleUnit { local_functions: HashMap, local_structs: HashMap, local_enums: HashMap, + local_aliases: HashMap, } #[derive(Debug, Clone)] @@ -204,6 +205,7 @@ enum DeclKind { CImport, Struct, Enum, + TypeAlias, } #[derive(Debug, Clone)] @@ -2304,6 +2306,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result Result Result Option Option { + let source_enum = module + .program + .enums + .iter() + .find(|enum_decl| enum_decl.name == name)?; + let checked_enum = checked + .enums + .iter() + .find(|enum_decl| enum_decl.name == name)?; + + Some(ExternalEnum { + name: name.to_string(), + span: source_enum.name_span, + variants: checked_enum + .variants + .iter() + .map(|checked_variant| { + let source_variant = source_enum + .variants + .iter() + .find(|variant| variant.name == checked_variant.name); + ExternalEnumVariant { + name: checked_variant.name.clone(), + name_span: source_variant + .map_or(source_enum.name_span, |variant| variant.name_span), + payload_ty: checked_variant.payload_ty.clone(), + payload_ty_span: source_variant.and_then(|variant| variant.payload_ty_span), + } + }) + .collect(), + }) +} + fn local_declarations( file: &str, program: &Program, @@ -2628,14 +2670,44 @@ fn local_declarations( HashMap, HashMap, HashMap, + HashMap, Vec, ) { let mut all = HashMap::::new(); let mut functions = HashMap::new(); let mut structs = HashMap::new(); let mut enums = HashMap::new(); + let mut aliases = HashMap::new(); let mut errors = Vec::new(); + for alias in &program.type_aliases { + let decl = DeclInfo { + span: alias.name_span, + kind: DeclKind::TypeAlias, + }; + if let Some(original) = all.insert(alias.name.clone(), decl.clone()) { + if original.kind == DeclKind::CImport { + errors.push( + Diagnostic::new( + file, + "DuplicateTopLevelName", + format!("duplicate top-level name `{}`", alias.name), + ) + .with_span(alias.name_span) + .related("original declaration", original.span), + ); + } else { + errors.push(duplicate_name( + file, + &alias.name, + alias.name_span, + original.span, + )); + } + } + aliases.insert(alias.name.clone(), decl); + } + for function in &program.functions { let decl = DeclInfo { span: function.span, @@ -2767,7 +2839,7 @@ fn local_declarations( enums.insert(enum_decl.name.clone(), decl); } - (functions, structs, enums, errors) + (functions, structs, enums, aliases, errors) } fn validate_workspace_packages( @@ -3028,9 +3100,20 @@ fn resolve_and_check_workspace( let mut external_enums = Vec::new(); for (name, binding) in imports { let provider = &packages[binding.provider_package].modules[binding.provider_module]; + let checked_provider = + checked_by_module.get(&(binding.provider_package, provider.name.clone())); match binding.kind { DeclKind::Function => { - if let Some(function) = + if let Some(function) = checked_provider + .and_then(|program| program.functions.iter().find(|f| f.name == *name)) + { + external_functions.push(ExternalFunction { + name: name.clone(), + params: function.params.iter().map(|(_, ty)| ty.clone()).collect(), + return_type: function.return_type.clone(), + foreign: false, + }); + } else if let Some(function) = provider.program.functions.iter().find(|f| f.name == *name) { external_functions.push(ExternalFunction { @@ -3042,7 +3125,16 @@ fn resolve_and_check_workspace( } } DeclKind::CImport => { - if let Some(import) = + if let Some(import) = checked_provider + .and_then(|program| program.c_imports.iter().find(|f| f.name == *name)) + { + external_functions.push(ExternalFunction { + name: name.clone(), + params: import.params.iter().map(|(_, ty)| ty.clone()).collect(), + return_type: import.return_type.clone(), + foreign: true, + }); + } else if let Some(import) = provider.program.c_imports.iter().find(|f| f.name == *name) { external_functions.push(ExternalFunction { @@ -3054,7 +3146,15 @@ fn resolve_and_check_workspace( } } DeclKind::Struct => { - if let Some(struct_decl) = + if let Some(struct_decl) = checked_provider + .and_then(|program| program.structs.iter().find(|s| s.name == *name)) + { + external_structs.push(ExternalStruct { + name: name.clone(), + fields: struct_decl.fields.clone(), + span: struct_decl.span, + }); + } else if let Some(struct_decl) = provider.program.structs.iter().find(|s| s.name == *name) { external_structs.push(ExternalStruct { @@ -3069,10 +3169,16 @@ fn resolve_and_check_workspace( } } DeclKind::Enum => { - if let Some(enum_decl) = external_enum_from_module(provider, name) { + if let Some(enum_decl) = checked_provider + .and_then(|program| { + external_enum_from_checked_module(provider, program, name) + }) + .or_else(|| external_enum_from_module(provider, name)) + { external_enums.push(enum_decl); } } + DeclKind::TypeAlias => {} } } @@ -3243,9 +3349,19 @@ fn resolve_and_check( let mut external_enums = Vec::new(); for (name, binding) in imports { let provider = &modules[by_name[&binding.provider]]; + let checked_provider = checked_by_module.get(&binding.provider); match binding.kind { DeclKind::Function => { - if let Some(function) = + if let Some(function) = checked_provider + .and_then(|program| program.functions.iter().find(|f| f.name == *name)) + { + external_functions.push(ExternalFunction { + name: name.clone(), + params: function.params.iter().map(|(_, ty)| ty.clone()).collect(), + return_type: function.return_type.clone(), + foreign: false, + }); + } else if let Some(function) = provider.program.functions.iter().find(|f| f.name == *name) { external_functions.push(ExternalFunction { @@ -3257,7 +3373,16 @@ fn resolve_and_check( } } DeclKind::CImport => { - if let Some(import) = + if let Some(import) = checked_provider + .and_then(|program| program.c_imports.iter().find(|f| f.name == *name)) + { + external_functions.push(ExternalFunction { + name: name.clone(), + params: import.params.iter().map(|(_, ty)| ty.clone()).collect(), + return_type: import.return_type.clone(), + foreign: true, + }); + } else if let Some(import) = provider.program.c_imports.iter().find(|f| f.name == *name) { external_functions.push(ExternalFunction { @@ -3269,7 +3394,15 @@ fn resolve_and_check( } } DeclKind::Struct => { - if let Some(struct_decl) = + if let Some(struct_decl) = checked_provider + .and_then(|program| program.structs.iter().find(|s| s.name == *name)) + { + external_structs.push(ExternalStruct { + name: name.clone(), + fields: struct_decl.fields.clone(), + span: struct_decl.span, + }); + } else if let Some(struct_decl) = provider.program.structs.iter().find(|s| s.name == *name) { external_structs.push(ExternalStruct { @@ -3284,10 +3417,16 @@ fn resolve_and_check( } } DeclKind::Enum => { - if let Some(enum_decl) = external_enum_from_module(provider, name) { + if let Some(enum_decl) = checked_provider + .and_then(|program| { + external_enum_from_checked_module(provider, program, name) + }) + .or_else(|| external_enum_from_module(provider, name)) + { external_enums.push(enum_decl); } } + DeclKind::TypeAlias => {} } } @@ -3381,6 +3520,22 @@ fn build_export_maps( Some(kind) => { exports.insert(export.name.clone(), kind); } + None if module.local_aliases.contains_key(&export.name) => diagnostics.push( + Diagnostic::new( + &module.file, + "Visibility", + format!( + "type alias `{}` is module-local and cannot be exported", + export.name + ), + ) + .with_span(export.span) + .related( + "type alias declaration", + module.local_aliases[&export.name].span, + ) + .hint("export functions, structs, or enums; imported signatures see concrete target types"), + ), None => diagnostics.push( Diagnostic::new( &module.file, @@ -3428,6 +3583,7 @@ fn resolve_imports( .get(&name.name) .or_else(|| module.local_structs.get(&name.name)) .or_else(|| module.local_enums.get(&name.name)) + .or_else(|| module.local_aliases.get(&name.name)) { diagnostics.push(duplicate_name( &module.file, @@ -3470,7 +3626,8 @@ fn resolve_imports( .local_functions .get(&name.name) .or_else(|| provider.local_structs.get(&name.name)) - .or_else(|| provider.local_enums.get(&name.name)); + .or_else(|| provider.local_enums.get(&name.name)) + .or_else(|| provider.local_aliases.get(&name.name)); let exported = export_maps[provider_index].get(&name.name).copied(); match (provider_local, exported) { (_, Some(kind)) => { @@ -3483,6 +3640,23 @@ fn resolve_imports( }, ); } + (Some(decl), None) if decl.kind == DeclKind::TypeAlias => diagnostics.push( + Diagnostic::new( + &module.file, + "Visibility", + format!( + "type alias `{}` is module-local and cannot be imported from module `{}`", + name.name, provider.name + ), + ) + .with_span(name.span) + .related_in_file( + provider.file.clone(), + "type alias declaration", + decl.span, + ) + .hint("import functions, structs, or enums; function signatures expose the alias target type"), + ), (Some(decl), None) => diagnostics.push( Diagnostic::new( &module.file, @@ -3557,13 +3731,14 @@ fn resolve_workspace_imports( &packages[provider_package_index].modules[provider_module_index]; for name in &import.names { if let Some(local) = module - .local_functions - .get(&name.name) - .or_else(|| module.local_structs.get(&name.name)) - .or_else(|| module.local_enums.get(&name.name)) - { - diagnostics.push(duplicate_name( - &module.file, + .local_functions + .get(&name.name) + .or_else(|| module.local_structs.get(&name.name)) + .or_else(|| module.local_enums.get(&name.name)) + .or_else(|| module.local_aliases.get(&name.name)) + { + diagnostics.push(duplicate_name( + &module.file, &name.name, name.span, local.span, @@ -3608,7 +3783,8 @@ fn resolve_workspace_imports( .local_functions .get(&name.name) .or_else(|| provider.local_structs.get(&name.name)) - .or_else(|| provider.local_enums.get(&name.name)); + .or_else(|| provider.local_enums.get(&name.name)) + .or_else(|| provider.local_aliases.get(&name.name)); let exported = export_maps[provider_package_index] [provider_module_index] .get(&name.name) @@ -3625,6 +3801,25 @@ fn resolve_workspace_imports( }, ); } + (Some(decl), None) if decl.kind == DeclKind::TypeAlias => { + diagnostics.push( + Diagnostic::new( + &module.file, + "Visibility", + format!( + "type alias `{}` is module-local and cannot be imported from module `{}`", + name.name, import.module + ), + ) + .with_span(name.span) + .related_in_file( + provider.file.clone(), + "type alias declaration", + decl.span, + ) + .hint("import functions, structs, or enums; function signatures expose the alias target type"), + ); + } (Some(decl), None) => diagnostics.push( Diagnostic::new( &module.file, diff --git a/compiler/tests/concrete_type_aliases_beta.rs b/compiler/tests/concrete_type_aliases_beta.rs new file mode 100644 index 0000000..89de471 --- /dev/null +++ b/compiler/tests/concrete_type_aliases_beta.rs @@ -0,0 +1,116 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, + process::{Command, Output}, +}; + +#[test] +fn concrete_type_alias_fixture_erases_before_llvm_and_runs_tests() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.slo"); + + let compile = run_glagol([fixture.as_os_str()]); + let stdout = String::from_utf8_lossy(&compile.stdout); + let stderr = String::from_utf8_lossy(&compile.stderr); + assert!( + compile.status.success(), + "compiler rejected concrete type alias fixture\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr + ); + assert!( + stdout.contains("define i32 @bump(i32 %value)") + && stdout.contains("define [3 x i32] @make_counts(i32 %base)") + && stdout.contains("define i32 @pick_count([3 x i32] %values)") + && stdout.contains("define { i32, ptr } @make_measure(i32 %amount)") + && stdout.contains("define { i1, i32 } @maybe_amount(i32 %amount)") + && stdout.contains("define { i1, i32 } @ok_amount(i32 %amount)") + && stdout.contains("define ptr @empty_counts()") + && stdout.contains("define { i32, i32 } @reading_value(i32 %amount)") + && stdout.contains("define i32 @reading_score({ i32, i32 } %reading)") + && stdout.contains("call ptr @__glagol_vec_i32_empty()"), + "LLVM output did not show erased concrete alias shapes\nstdout:\n{}", + stdout + ); + assert!( + !stdout.contains("Count") + && !stdout.contains("Score") + && !stdout.contains("MaybeCount") + && !stdout.contains("ReadingAlias"), + "LLVM output leaked source alias names\nstdout:\n{}", + stdout + ); + assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); + + let run = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]); + assert_success_stdout( + run, + concat!( + "test \"aliases erase through arrays and structs\" ... ok\n", + "test \"aliases erase through option result vec and enum\" ... ok\n", + "2 test(s) passed\n", + ), + ); +} + +#[test] +fn concrete_type_alias_fixture_formats_and_lowers_stably() { + let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.slo"); + + let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]); + assert_success_stdout( + formatted, + &std::fs::read_to_string(&fixture).expect("read type alias fixture"), + ); + + let surface = run_glagol([ + OsStr::new("--inspect-lowering=surface"), + fixture.as_os_str(), + ]); + assert_success_stdout( + surface, + &std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.surface.lower"), + ) + .expect("read type alias surface snapshot"), + ); + + let checked = run_glagol([ + OsStr::new("--inspect-lowering=checked"), + fixture.as_os_str(), + ]); + assert_success_stdout( + checked, + &std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.checked.lower"), + ) + .expect("read type alias checked snapshot"), + ); +} + +fn run_glagol(args: I) -> Output +where + I: IntoIterator, + S: AsRef, +{ + Command::new(glagol_bin()) + .args(args) + .output() + .expect("run glagol") +} + +fn glagol_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_glagol")) +} + +fn assert_success_stdout(output: Output, expected: &str) { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "command failed\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr + ); + assert_eq!(stdout, expected); + assert!(stderr.is_empty(), "stderr was not empty:\n{}", stderr); +} diff --git a/compiler/tests/diagnostics_contract.rs b/compiler/tests/diagnostics_contract.rs index a14e7d2..19d2659 100644 --- a/compiler/tests/diagnostics_contract.rs +++ b/compiler/tests/diagnostics_contract.rs @@ -83,6 +83,98 @@ const CASES: &[DiagnosticCase] = &[ "#, snapshot: "../tests/unknown-top-level-form.diag", }, + DiagnosticCase { + name: "malformed-type-alias", + source: r#" +(module main) + +(type Count) +"#, + snapshot: "../tests/malformed-type-alias.diag", + }, + DiagnosticCase { + name: "duplicate-type-alias", + source: r#" +(module main) + +(type Count i32) +(type Count i64) +"#, + snapshot: "../tests/duplicate-type-alias.diag", + }, + DiagnosticCase { + name: "type-alias-name-conflict", + source: r#" +(module main) + +(struct Count + (value i32)) + +(type Count i32) +"#, + snapshot: "../tests/type-alias-name-conflict.diag", + }, + DiagnosticCase { + name: "type-alias-value-name-conflict", + source: r#" +(module main) + +(import_c c_add ((value i32)) -> i32) + +(type main i32) + +(type c_add i32) + +(fn main () -> i32 + 0) +"#, + snapshot: "../tests/type-alias-value-name-conflict.diag", + }, + DiagnosticCase { + name: "unknown-type-alias-target", + source: r#" +(module main) + +(type Count Missing) +"#, + snapshot: "../tests/unknown-type-alias-target.diag", + }, + DiagnosticCase { + name: "unsupported-type-alias-target", + source: r#" +(module main) + +(type BadUnit unit) + +(type BadPtr (ptr i32)) + +(type BadSlice (slice i32)) + +(type BadVec (vec u32)) + +(type BadResult (result i32 string)) +"#, + snapshot: "../tests/unsupported-type-alias-target.diag", + }, + DiagnosticCase { + name: "self-type-alias", + source: r#" +(module main) + +(type Count Count) +"#, + snapshot: "../tests/self-type-alias.diag", + }, + DiagnosticCase { + name: "cyclic-type-alias", + source: r#" +(module main) + +(type A B) +(type B A) +"#, + snapshot: "../tests/cyclic-type-alias.diag", + }, DiagnosticCase { name: "enum-empty", source: r#" diff --git a/compiler/tests/lowering_inspector.rs b/compiler/tests/lowering_inspector.rs index 663cd36..50dc9c3 100644 --- a/compiler/tests/lowering_inspector.rs +++ b/compiler/tests/lowering_inspector.rs @@ -13,6 +13,12 @@ const LOWERING_FIXTURES: &[LoweringFixture] = &[ surface_snapshot: "../tests/top-level-test.surface.lower", checked_snapshot: "../tests/top-level-test.checked.lower", }, + LoweringFixture { + name: "type-aliases", + source: "../tests/type-aliases.slo", + surface_snapshot: "../tests/type-aliases.surface.lower", + checked_snapshot: "../tests/type-aliases.checked.lower", + }, LoweringFixture { name: "comments", source: "../tests/comments.slo", diff --git a/compiler/tests/project_mode.rs b/compiler/tests/project_mode.rs index 1834302..6285822 100644 --- a/compiler/tests/project_mode.rs +++ b/compiler/tests/project_mode.rs @@ -1279,6 +1279,63 @@ fn enum_import_visibility_and_duplicate_cases_are_diagnostics() { ); } +#[test] +fn type_aliases_are_local_across_project_visibility() { + let erased_signature = write_project( + "alias-erased-signature", + &[( + "types", + "(module types (export make_count))\n\n(type Count i32)\n\n(fn make_count ((value Count)) -> Count\n value)\n", + )], + "(module main)\n\n(import types (make_count))\n\n(fn main () -> i32\n (make_count 42))\n", + ); + let erased_output = run_glagol(["check".as_ref(), erased_signature.as_os_str()]); + assert_success_stdout("alias erased project signature", erased_output, ""); + + let export_alias = write_project( + "alias-export", + &[( + "types", + "(module types (export Count))\n\n(type Count i32)\n\n(fn make_count ((value Count)) -> Count\n value)\n", + )], + "(module main)\n", + ); + let export_output = run_glagol(["check".as_ref(), export_alias.as_os_str()]); + assert_exit_code("alias export", &export_output, 1); + assert_stderr_contains("alias export", &export_output, "Visibility"); + assert_stderr_contains( + "alias export message", + &export_output, + "type alias `Count` is module-local and cannot be exported", + ); + + let import_alias = write_project( + "alias-import", + &[("types", "(module types)\n\n(type Count i32)\n")], + "(module main)\n\n(import types (Count))\n", + ); + let import_output = run_glagol(["check".as_ref(), import_alias.as_os_str()]); + assert_exit_code("alias import", &import_output, 1); + assert_stderr_contains("alias import", &import_output, "Visibility"); + assert_stderr_contains( + "alias import message", + &import_output, + "type alias `Count` is module-local and cannot be imported", + ); + + let duplicate_local = write_project( + "alias-import-duplicate", + &[( + "types", + "(module types (export value))\n\n(fn value () -> i32\n 1)\n", + )], + "(module main)\n\n(import types (Count))\n\n(type Count i32)\n", + ); + let duplicate_output = run_glagol(["check".as_ref(), duplicate_local.as_os_str()]); + assert_exit_code("alias import duplicate", &duplicate_output, 1); + assert_stderr_contains("alias import duplicate", &duplicate_output, "DuplicateName"); +} + #[test] fn project_diagnostic_families_have_json_golden_coverage() { let duplicate = write_project( diff --git a/compiler/tests/promotion_gate.rs b/compiler/tests/promotion_gate.rs index 55a25cb..08364c1 100644 --- a/compiler/tests/promotion_gate.rs +++ b/compiler/tests/promotion_gate.rs @@ -13,11 +13,13 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[ "array-return-type-mismatch.diag", "arity-mismatch.diag", "cyclic-struct-fields.diag", + "cyclic-type-alias.diag", "duplicate-local.diag", "duplicate-match-arm.diag", "duplicate-struct-constructor-field.diag", "duplicate-struct-field.diag", "duplicate-struct.diag", + "duplicate-type-alias.diag", "empty-array.diag", "enum-constructor-arity.diag", "enum-container-values.diag", @@ -65,6 +67,7 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[ "malformed-result-err-unwrap.diag", "malformed-result-ok-unwrap.diag", "malformed-result-constructor.diag", + "malformed-type-alias.diag", "malformed-unsafe-form.diag", "malformed-while.diag", "field-access-on-non-struct.diag", @@ -82,6 +85,7 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[ "set-immutable-local.diag", "set-parameter.diag", "single-file-main-i64-return.diag", + "self-type-alias.diag", "set-type-mismatch.diag", "set-unknown-local.diag", "std-abi-layout-unsupported.diag", @@ -280,7 +284,11 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[ "test-invalid-name.diag", "test-non-bool.diag", "type-mismatch.diag", + "type-alias-name-conflict.diag", + "type-alias-value-name-conflict.diag", "unclosed-list.diag", + "unknown-type-alias-target.diag", + "unsupported-type-alias-target.diag", "unknown-function.diag", "unknown-struct-local.diag", "unknown-struct-parameter.diag", @@ -465,6 +473,8 @@ const LOWERING_INSPECTOR_FIXTURES: &[&str] = &[ "time-sleep.surface.lower", "top-level-test.checked.lower", "top-level-test.surface.lower", + "type-aliases.checked.lower", + "type-aliases.surface.lower", "u32-numeric-primitive.checked.lower", "u32-numeric-primitive.surface.lower", "u64-numeric-primitive.checked.lower", @@ -490,6 +500,11 @@ const LOWERING_INSPECTOR_CASES: &[LoweringInspectorCase] = &[ surface_snapshot: "top-level-test.surface.lower", checked_snapshot: "top-level-test.checked.lower", }, + LoweringInspectorCase { + source: "tests/type-aliases.slo", + surface_snapshot: "type-aliases.surface.lower", + checked_snapshot: "type-aliases.checked.lower", + }, LoweringInspectorCase { source: "tests/comments.slo", surface_snapshot: "comments.surface.lower", @@ -853,6 +868,7 @@ const GLAGOL_TEST_FIXTURES: &[&str] = &[ "time-sleep.slo", "top-level-test.fmt", "top-level-test.slo", + "type-aliases.slo", "u32-numeric-primitive.slo", "u64-numeric-primitive.slo", "unsafe.slo", @@ -920,6 +936,7 @@ const SLOVO_FORMATTER_FIXTURES: &[&str] = &[ "struct.slo", "time-sleep.slo", "top-level-test.slo", + "type-aliases.slo", "u32-numeric-primitive.slo", "u64-numeric-primitive.slo", "unsafe.slo", diff --git a/compiler/tests/result_based_host_errors.rs b/compiler/tests/result_based_host_errors.rs index 2c3b640..29666ff 100644 --- a/compiler/tests/result_based_host_errors.rs +++ b/compiler/tests/result_based_host_errors.rs @@ -6,6 +6,7 @@ use std::{ path::{Path, PathBuf}, process::{Command, Output, Stdio}, sync::atomic::{AtomicUsize, Ordering}, + time::{SystemTime, UNIX_EPOCH}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); @@ -951,11 +952,16 @@ fn write_fixture(name: &str, source: &str) -> PathBuf { } fn temp_root(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock before Unix epoch") + .as_nanos(); env::temp_dir().join(format!( - "glagol-exp10-host-result-{}-{}-{}", + "glagol-exp10-host-result-{}-{}-{}-{}", name, std::process::id(), - NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) + NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed), + nanos )) } diff --git a/docs/POST_BETA_ROADMAP.md b/docs/POST_BETA_ROADMAP.md index 41a2c2e..a2caa27 100644 --- a/docs/POST_BETA_ROADMAP.md +++ b/docs/POST_BETA_ROADMAP.md @@ -105,7 +105,8 @@ Candidate features: - `glagol run`-friendly `main` conventions and clearer entry-point diagnostics - better `match` diagnostics and exhaustiveness checks where the current enum model allows it -- type aliases for long concrete types such as vectors, options, and results +- concrete type aliases for long concrete types such as vectors, options, and + results - destructuring for simple structs and enum payloads - additional numeric completeness such as `f32`, only if it can be tested across checker, formatter, runtime, and docs @@ -117,6 +118,8 @@ Released in `1.0.0-beta.4`: project/workspace build and run entry diagnostics now use entry-specific codes and explicitly show the required `(fn main () -> i32 ...)` contract. Non-exhaustive `match` diagnostics now use clearer missing-arm wording and deterministic found-arm output. +Concrete aliases were split into the follow-up `1.0.0-beta.8` language slice so +the syntax, formatter, diagnostics, and source fixtures could be gated directly. ### 5. Package And Workspace Discipline @@ -194,7 +197,37 @@ encoding policy beyond the current runtime string ABI remain deferred. Why seventh: networking and CLI tools need data interchange, but a complete JSON library depends on collection work. -### 8. Generics And Collection Unification +### 8. Concrete Type Alias Foundation + +Goal: reduce concrete type repetition without introducing generics or changing +runtime representation. + +Work: + +- add top-level `(type Alias TargetType)` declarations for aliases whose targets + are already supported concrete Slovo types +- resolve aliases before typed-core lowering, checked import signatures, backend + layout, ABI decisions, and runtime behavior +- keep aliases module-local: no alias exports, imports, re-exports, or + cross-module alias visibility +- update formatter and diagnostics for malformed, duplicate, unsupported, + cyclic, exported, or imported aliases +- exercise aliases sparingly in JSON source facades and explicit source + fixtures without adding compiler-known runtime names + +Released in `1.0.0-beta.8`: concrete aliases such as `(type JsonText string)` +are transparent names for existing concrete types. The compiler parses, +formats, checks, lowers, and erases aliases before backend behavior, while +project imports of functions that used local aliases see concrete target +types. Alias export/import attempts and unsupported targets are diagnostics. +Generic aliases, parameterized aliases, aliasing maps/sets, stable ABI/layout +names, and runtime changes remain deferred. + +Why eighth: concrete aliases remove real noise from current stdlib and fixture +code while deliberately postponing generic type parameters until the compiler +and standard library have stronger design pressure. + +### 9. Generics And Collection Unification Goal: stop duplicating every helper across concrete vector, option, and result families. @@ -207,11 +240,11 @@ Work: - add generic arrays/vectors first, then maps and sets - keep stable ABI/layout promises deferred until after real beta use -Why eighth: generics are important, but they should be introduced after the +Why ninth: generics are important, but they should be introduced after the compiler, docs, tests, and stdlib have enough real pressure to validate the design. -### 9. Developer Experience +### 10. Developer Experience Goal: make Slovo comfortable for repeated daily use. @@ -223,7 +256,7 @@ Work: - clearer benchmark harness output - machine-readable diagnostics stability policy -Why ninth: editor support matters, but it should follow a stable enough syntax, +Why tenth: editor support matters, but it should follow a stable enough syntax, project model, and diagnostic model. ## Stable `1.0.0` Gate diff --git a/docs/compiler/RELEASE_NOTES.md b/docs/compiler/RELEASE_NOTES.md index 114ba06..704161e 100644 --- a/docs/compiler/RELEASE_NOTES.md +++ b/docs/compiler/RELEASE_NOTES.md @@ -10,7 +10,45 @@ integration/readiness release, not the first real beta. ## Unreleased -No unreleased changes yet. +Next scoped Glagol work is expected to use the `1.0.0-beta.9` release line. +Generics and collection unification are the likely next design pressure, but +remain unreleased until their contracts and gates are explicit. + +No unreleased compiler scope is committed here yet. + +## 1.0.0-beta.8 + +Release label: `1.0.0-beta.8` + +Release date: 2026-05-22 + +Release state: released beta concrete type alias foundation update + +### Summary + +Glagol `1.0.0-beta.8` is scoped to transparent concrete type aliases. A source +module may declare `(type Alias TargetType)` for an existing supported concrete +type and then use `Alias` in supported type positions. The alias is not a new +runtime type and must be normalized before backend/ABI behavior. + +- Parse top-level `(type Alias TargetType)` declarations. +- Resolve aliases to existing supported concrete target types before typed-core + lowering, checked import signatures, LLVM/backend layout, ABI decisions, and + runtime behavior. +- Format aliases canonically as one-line top-level declarations. +- Diagnose malformed aliases, duplicate alias/type/value names, unknown or + unsupported target types, cycles, exported aliases, imported aliases, and + parameterized/generic alias attempts. +- Keep runtime behavior unchanged: no new compiler-known runtime names, hosted + symbols, maps/sets, generic aliases, parameterized aliases, or cross-module + alias visibility. + +### Explicit Deferrals + +This release does not add generics, parameterized aliases, alias +exports/imports, alias re-exports, cross-module alias visibility, maps/sets, +stable layout names, runtime helpers, hosted runtime symbols, or +standard-library API freeze claims. ## 1.0.0-beta.7 diff --git a/docs/compiler/ROADMAP.md b/docs/compiler/ROADMAP.md index 2089a5d..b1d26f0 100644 --- a/docs/compiler/ROADMAP.md +++ b/docs/compiler/ROADMAP.md @@ -22,8 +22,8 @@ general-purpose beta release. A Glagol feature is done only when it has parser/lowerer support, checker behavior, diagnostics for invalid forms, backend behavior or explicit unsupported diagnostics, and tests. -Current stage: `1.0.0-beta.7`, released on 2026-05-22 as the first post-beta -serialization/data-interchange foundation update. It keeps the `1.0.0-beta` language/compiler +Current stage: `1.0.0-beta.8`, released on 2026-05-22 as the first post-beta +concrete type alias foundation update. It keeps the `1.0.0-beta` language/compiler support baseline and includes the `1.0.0-beta.1` tooling hardening release, the `1.0.0-beta.2` runtime/resource foundation release, the `1.0.0-beta.3` standard-library stabilization release, the `1.0.0-beta.4` @@ -32,7 +32,14 @@ discipline release, and the `1.0.0-beta.6` compiler-known `std.net` loopback TCP runtime family with focused lowering, interpreter, diagnostics, source-facade, promotion, and hosted smoke coverage, plus the `1.0.0-beta.7` compiler-known `std.json.quote_string` runtime family with matching -source-facade, test-runner, hosted smoke, and benchmark-scaffold coverage. +source-facade, test-runner, hosted smoke, and benchmark-scaffold coverage, plus +top-level `(type Alias TargetType)` parsing, checking, formatting, lowering +inspection, project import normalization, and diagnostics coverage for concrete +aliases. + +Next stage target: `1.0.0-beta.9`, likely generics and collection +unification. The exact compiler-side contract must be frozen from the manifest +and roadmap before implementation. The final experimental precursor scope is `exp-125`. Its unsigned direct-value flow, parse/format runtime lanes, and matching staged stdlib helper breadth diff --git a/docs/language/RELEASE_NOTES.md b/docs/language/RELEASE_NOTES.md index c4aeaf2..6669dbe 100644 --- a/docs/language/RELEASE_NOTES.md +++ b/docs/language/RELEASE_NOTES.md @@ -8,7 +8,7 @@ Historical `exp-*` releases listed here are experimental maturity milestones. The pushed tag `v2.0.0-beta.1` is historical. It is now documented as an experimental integration/readiness release, not as a beta maturity claim. -The current release is `1.0.0-beta.7`, published on 2026-05-22. It keeps the +The current release is `1.0.0-beta.8`, published on 2026-05-22. It keeps the `1.0.0-beta` language surface, includes the first post-beta tooling/install hardening bundle from `1.0.0-beta.1`, and adds the first runtime/resource foundation bundle from `1.0.0-beta.2` plus the first standard-library @@ -16,11 +16,49 @@ stabilization bundle from `1.0.0-beta.3`, the first language-usability diagnostics bundle from `1.0.0-beta.4`, and the first local package/workspace discipline bundle from `1.0.0-beta.5`, plus the first loopback networking foundation bundle from `1.0.0-beta.6`, and the first -serialization/data-interchange foundation bundle from `1.0.0-beta.7`. +serialization/data-interchange foundation bundle from `1.0.0-beta.7`, and the +first concrete type alias foundation from `1.0.0-beta.8`. ## Unreleased -No unreleased changes yet. +Next scoped Slovo-side work is expected to use the `1.0.0-beta.9` release +line. Generics and collection unification are the likely next design pressure, +but remain unreleased until their contracts and gates are explicit. + +No unreleased language scope is committed here yet. + +## 1.0.0-beta.8 + +Release label: `1.0.0-beta.8` + +Release name: Concrete Type Alias Foundation + +Release date: 2026-05-22 + +Status: released beta concrete type alias foundation update on the +`1.0.0-beta` language baseline. + +`1.0.0-beta.8` promotes transparent type aliases for current concrete types: + +```slo +(type JsonText string) +``` + +An alias is a module-local source name. It may be used in supported type +positions, and the compiler resolves it to the existing concrete target type +before backend and ABI behavior. This release does not add generics, +parameterized aliases, cross-module alias imports, alias re-exports, maps/sets, +stable layout names, new compiler-known runtime names, or runtime changes. + +- Top-level `(type Alias TargetType)` declarations name existing concrete + supported target types without introducing new runtime representations. +- Aliases are transparent before typed-core lowering, checked import + signatures, backend layout, ABI decisions, and runtime behavior. +- The JSON source facade and explicit JSON example projects use local + `JsonText` / `JsonField` aliases as narrow source fixtures. +- Alias exports, imports, re-exports, cross-module alias visibility, generic + aliases, parameterized aliases, maps/sets, and runtime changes remain out of + scope. ## 1.0.0-beta.7 diff --git a/docs/language/ROADMAP.md b/docs/language/ROADMAP.md index 1ccaa12..84df965 100644 --- a/docs/language/ROADMAP.md +++ b/docs/language/ROADMAP.md @@ -10,19 +10,25 @@ Long-horizon planning lives in release train from the historical `v2.0.0-beta.1` tag toward and beyond the first real general-purpose beta Slovo contract. -Current stage: `1.0.0-beta.7`, released on 2026-05-22 as the first post-beta -serialization/data-interchange foundation update. It keeps the `1.0.0-beta` language contract and +Current stage: `1.0.0-beta.8`, released on 2026-05-22 as the first post-beta +concrete type alias foundation update. It keeps the `1.0.0-beta` language contract and includes the `1.0.0-beta.1` tooling hardening release, the `1.0.0-beta.2` runtime/resource foundation release, the `1.0.0-beta.3` standard-library stabilization release, the `1.0.0-beta.4` language-usability diagnostics release, the `1.0.0-beta.5` package/workspace discipline release, and a narrow `std.net` source facade for blocking loopback TCP client/server primitives over opaque `i32` handles and concrete `result` families, plus a narrow `std.json` -source facade for compact JSON text construction. JSON parsing, recursive JSON -values, maps/sets, DNS, TLS, UDP, async IO, non-loopback binding, HTTP -frameworks, rich host-error ADTs, stable ABI/layout, and a stable +source facade for compact JSON text construction, plus top-level module-local +transparent aliases for supported concrete target types. JSON parsing, +recursive JSON values, generic aliases, parameterized aliases, cross-module +alias visibility, maps/sets, DNS, TLS, UDP, async IO, non-loopback binding, +HTTP frameworks, rich host-error ADTs, stable ABI/layout, and a stable standard-library API freeze remain deferred. +Next stage target: `1.0.0-beta.9`, likely generics and collection +unification. The exact scope must be frozen from the manifest and roadmap +before implementation. + The final experimental precursor scope is `exp-125`, defined in `.llm/EXP_125_UNSIGNED_U32_U64_NUMERIC_AND_STDLIB_BREADTH_ALPHA.md`. Its unsigned direct-value flow, parse/format runtime lanes, and matching staged diff --git a/docs/language/SPEC-v1.md b/docs/language/SPEC-v1.md index cf4bc26..72b9fb2 100644 --- a/docs/language/SPEC-v1.md +++ b/docs/language/SPEC-v1.md @@ -107,6 +107,13 @@ Current v1 release surface and explicit experimental targets: v1.1-v1.7 surface is supported for small real flat local projects through `glagol new`, `check`, `fmt --check/--write`, `test`, `build`, `doc`, artifact manifests, JSON diagnostics, and the release-gate script +- `1.0.0-beta.8` concrete type alias target: top-level + `(type Alias TargetType)` declarations name existing supported concrete + target types and are resolved before typed-core lowering, backend layout, ABI + decisions, and runtime behavior; no generic aliases, parameterized + aliases, alias exports/imports/re-exports, cross-module alias visibility, + maps/sets, or runtime changes are included, once the matching Glagol gates + pass - `exp-1` owned runtime strings: compiler-known `std.string.concat` accepts two `string` values and returns an immutable runtime-owned `string`; existing string equality, length, printing, locals, parameters, returns, and calls work @@ -1108,6 +1115,71 @@ schema validation, Unicode normalization, stable text encoding policy beyond the current null-terminated runtime string ABI, stable runtime helper symbols, and stable standard-library API guarantees remain deferred. +### 4.4.6 Post-Beta Concrete Type Alias Foundation + +Status: released in `1.0.0-beta.8` with matching Glagol parser, checker, +formatter, diagnostics, fixture, documentation, and promotion gates. + +`1.0.0-beta.8` promotes transparent aliases for existing concrete type forms: + +```slo +(type JsonText string) +(type Scores (vec i32)) +(type MaybeName (option string)) +(type ReadResult (result string i32)) +``` + +The declaration form is exactly `(type Alias TargetType)` at top level. `Alias` +must be a single user type name. `TargetType` must resolve to an already +supported concrete Slovo type: direct scalar and string types, current fixed +array families, current concrete vec families, current concrete option/result +families, current known struct names, or current enum names where that type is +already legal in the use position. + +Aliases are transparent. Using `Alias` in a type position is equivalent to +using its resolved target type. The resolved target controls value flow, +operators, constructors, `match`, field access, imports of functions that use +the type, lowering, runtime behavior, and diagnostics. The alias does not create +a nominal type, wrapper, cast, runtime tag, layout name, hosted symbol, or C ABI +boundary. + +Alias declarations are module-local. They may be used by later declarations in +the same module and by exported functions or structs after normalization, but +the alias name itself is not exportable, importable, or re-exportable. A module +that imports a function whose signature used a local alias sees the normalized +concrete target type, not the alias name. + +Name resolution for type positions checks local concrete aliases alongside +local struct and enum type names before applying builtin/concrete type forms. +Duplicate alias, struct, enum, function, import-list, export-list, local, or +parameter names remain diagnostics under the existing duplicate-name policy. +Aliases must not shadow builtins, compiler-known standard-runtime names, or +reserved unsafe heads. + +Formatter output keeps aliases as one-line top-level declarations: + +```slo +(type JsonText string) +``` + +An alias target that is already a parenthesized concrete type form stays inline, +for example `(type JsonItems (vec string))`. Comments are allowed around the +top-level alias declaration under the existing full-line comment rules, not +inside the declaration form. + +Required diagnostics include malformed alias declarations, duplicate alias +names, unknown target types, unsupported target types, cyclic aliases, +parameterized alias attempts, exported aliases, imported aliases, and +cross-module alias references. An implementation may use precise diagnostic +codes, but it must not silently treat an alias as `string`, `i32`, or any other +fallback type after a failed resolution. + +This target explicitly does not add generic aliases, parameterized aliases, +alias type parameters, higher-kinded aliases, alias re-exports, cross-module +alias imports, import aliases, glob imports, maps/sets, alias-driven overloads, +implicit casts, new runtime helpers, standard-runtime names, stable ABI/layout +promises, or a stable standard-library API freeze. + ## 4.5 v2.0.0-beta.1 Experimental Integration Readiness Status: current experimental Slovo-side release contract, released 2026-05-17. @@ -5442,6 +5514,7 @@ comments inside: - `(module ...)` forms - `(struct ...)` forms +- `(type ...)` alias declaration forms - `fn` and `test` headers before their body begins - type forms such as `(array i32 N)`, `(option i32)`, `(result i32 i32)`, exp-2 `(vec i32)`, exp-94 `(vec i64)`, exp-103 `(vec f64)`, and exp-4 diff --git a/docs/language/STDLIB_API.md b/docs/language/STDLIB_API.md index d6e4bd4..ce2df53 100644 --- a/docs/language/STDLIB_API.md +++ b/docs/language/STDLIB_API.md @@ -6,7 +6,7 @@ Do not edit this file by hand. ## Stability Tiers - `beta-supported`: exported from `lib/std` and covered by source-search, promotion, or facade gates in the current beta line. -- `experimental`: not used for exported `lib/std` helpers in `1.0.0-beta.7`; future releases may mark new helpers this way before they graduate. +- `experimental`: not used for exported `lib/std` helpers in `1.0.0-beta.8`; future releases may mark new helpers this way before they graduate. - `internal`: helper names that are not exported from their module; they are intentionally omitted from this catalog. The catalog is a beta compatibility aid, not a stable `1.0.0` API freeze. diff --git a/docs/language/examples/README.md b/docs/language/examples/README.md index 49fd10f..2e585ce 100644 --- a/docs/language/examples/README.md +++ b/docs/language/examples/README.md @@ -12,6 +12,8 @@ runtime/resource foundation APIs. `1.0.0-beta.3` adds the generated stdlib API catalog and the checked `projects/stdlib-composition/` example. `1.0.0-beta.7` adds explicit `projects/std-import-json/` and `projects/std-layout-local-json/` examples for compact JSON text construction. +`1.0.0-beta.8` reuses those JSON projects for local `JsonText`/`JsonField` +concrete type alias fixtures. The language baseline absorbs the final exp-125 unsigned precursor scope alongside the already promoted project/package, stdlib-source, collection, composite-data, diff --git a/docs/language/examples/formatter/type-aliases.slo b/docs/language/examples/formatter/type-aliases.slo new file mode 100644 index 0000000..916634a --- /dev/null +++ b/docs/language/examples/formatter/type-aliases.slo @@ -0,0 +1,79 @@ +(module main) + +(type Count i32) + +(type Score Count) + +(type Text string) + +(type Counts (array Count 3)) + +(type CountVec (vec Count)) + +(type MaybeCount (option Count)) + +(type CountResult (result Count Count)) + +(struct Measurement + (amount Count) + (label Text)) + +(type Measure Measurement) + +(enum Reading + Empty + (Value Count)) + +(type ReadingAlias Reading) + +(fn bump ((value Count)) -> Score + (+ value 1)) + +(fn make_counts ((base Count)) -> Counts + (array Count base (+ base 1) (+ base 2))) + +(fn pick_count ((values Counts)) -> Count + (index values 1)) + +(fn make_measure ((amount Count)) -> Measure + (Measurement (amount amount) (label "ok"))) + +(fn measure_amount ((measure Measure)) -> Count + (. measure amount)) + +(fn maybe_amount ((amount Count)) -> MaybeCount + (some Count amount)) + +(fn maybe_empty () -> MaybeCount + (none Count)) + +(fn ok_amount ((amount Count)) -> CountResult + (ok Count Count amount)) + +(fn err_amount ((code Count)) -> CountResult + (err Count Count code)) + +(fn empty_counts () -> CountVec + (std.vec.i32.empty)) + +(fn reading_value ((amount Count)) -> ReadingAlias + (Reading.Value amount)) + +(fn reading_score ((reading ReadingAlias)) -> Count + (match reading + ((Reading.Empty) + 0) + ((Reading.Value amount) + amount))) + +(fn main () -> i32 + (let values Counts (make_counts 40)) + (+ (pick_count values) (measure_amount (make_measure 1)))) + +(test "aliases erase through arrays and structs" + (= (main) 42)) + +(test "aliases erase through option result vec and enum" + (let maybe MaybeCount (maybe_amount 7)) + (let result CountResult (ok_amount 8)) + (and (is_some maybe) (and (is_ok result) (and (= (std.vec.i32.len (empty_counts)) 0) (= (reading_score (reading_value 9)) 9))))) diff --git a/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf b/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf index ac00227..e279851 100644 Binary files a/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf and b/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf differ diff --git a/docs/papers/GLAGOL_WHITEPAPER.md b/docs/papers/GLAGOL_WHITEPAPER.md index 99a1d75..a61bfbf 100644 --- a/docs/papers/GLAGOL_WHITEPAPER.md +++ b/docs/papers/GLAGOL_WHITEPAPER.md @@ -5,7 +5,7 @@ Sanjin Gumbarevic
hermeticum_lab@protonmail.com -Publication release: `1.0.0-beta.7` +Publication release: `1.0.0-beta.8` Technical behavior baseline: compiler and language support through `1.0.0-beta`; tooling and install workflow through `1.0.0-beta.1`; @@ -13,12 +13,13 @@ runtime/resource foundation through `1.0.0-beta.2`; standard-library stabilization through `1.0.0-beta.3`; language-usability diagnostics through `1.0.0-beta.4`; package/workspace discipline through `1.0.0-beta.5`; loopback networking foundation through `1.0.0-beta.6`; -serialization/data-interchange foundation through `1.0.0-beta.7` +serialization/data-interchange foundation through `1.0.0-beta.7`; +concrete type alias foundation through `1.0.0-beta.8` Date: 2026-05-22 Evidence source: paired local Slovo/Glagol monorepo verification and benchmark -reruns from a local checkout; beta.7 release-gate verification from the public +reruns from a local checkout; beta.8 release-gate verification from the public monorepo Maturity: beta @@ -30,23 +31,26 @@ Slovo. It exists to make the language support boundary inspectable: tokens, S-expression tree, AST, typed AST, LLVM IR, hosted native executable, tests, diagnostics, and release documents should agree. -The current publication release, `1.0.0-beta.7`, keeps the first real +The current publication release, `1.0.0-beta.8`, keeps the first real general-purpose beta toolchain baseline from `1.0.0-beta` and records the first post-beta tooling/install hardening update plus the first runtime/resource foundation update plus the first standard-library stabilization update plus the first language-usability diagnostics update and the first local package/workspace discipline update plus the first loopback networking foundation update plus the first serialization/data-interchange -foundation update. The beta baseline includes the completed `u32` / `u64` -unsigned compiler and stdlib breadth scope, the narrow `std.net` loopback TCP -runtime family, the narrow `std.json.quote_string` runtime family, and the -current ten-scaffold benchmark suite. This paper records the current beta +foundation update plus the first concrete type alias foundation. The beta +baseline includes the completed `u32` / `u64` unsigned compiler and stdlib +breadth scope, the narrow `std.net` loopback TCP runtime family, the narrow +`std.json.quote_string` runtime family, transparent concrete alias parsing and +erasure, and the current ten-scaffold benchmark suite. This paper records the +current beta implementation surface, the benchmark method and results, the distinction between Glagol and Lisp-family implementations, the beta.1 tooling update, the beta.2 runtime/resource foundation, the beta.3 standard-library stabilization slice, the beta.4 diagnostics usability slice, the beta.5 package discipline slice, the beta.6 networking foundation slice, the beta.7 serialization -foundation slice, and the compiler path from beta to stable. +foundation slice, the beta.8 concrete alias foundation slice, and the compiler +path from beta to stable. ## 1. Compiler Thesis @@ -128,12 +132,14 @@ At the current technical behavior beta baseline, Glagol supports: staged source-authored `std/*.slo` gates - compact JSON string literal construction through `std.json.quote_string` and the hosted `__glagol_json_quote_string` runtime helper +- transparent concrete type aliases erased before typed lowering, import + signature use, backend layout, ABI, and runtime behavior - scalar C FFI imports - benchmark scaffolds for Slovo, C, Rust, Python, Clojure, and Common Lisp/SBCL, with `cold-process` and `hot-loop` timing modes -The current release, `1.0.0-beta.7`, is a beta serialization/data-interchange -foundation update on the first release line that may honestly use beta maturity +The current release, `1.0.0-beta.8`, is a beta concrete type alias foundation +update on the first release line that may honestly use beta maturity language for this toolchain. ## 4. Diagnostics And Support Discipline @@ -303,10 +309,11 @@ baseline. `1.0.0-beta.1` changes tooling and install workflow, and standard-library catalog and composition coverage, `1.0.0-beta.4` improves diagnostics, `1.0.0-beta.5` tightens package/workspace discipline, and `1.0.0-beta.6` adds a narrow loopback networking foundation, and -`1.0.0-beta.7` adds a narrow JSON construction foundation. None of these -post-beta slices claims changed benchmark performance. The beta.7 -`json-quote-loop` scaffold is present for local follow-up timing and is not -part of the exp-123 nine-row result table below. +`1.0.0-beta.7` adds a narrow JSON construction foundation, and +`1.0.0-beta.8` adds transparent concrete type aliases. None of these post-beta +slices claims changed benchmark performance. The beta.7 `json-quote-loop` +scaffold is present for local follow-up timing and is not part of the exp-123 +nine-row result table below. The exp-123 publication baseline widened the paired same-machine result set from seven rows to nine by adding two owned-vector kernels: @@ -394,15 +401,15 @@ coverage and compatibility: - package behavior becoming stable before dependency, manifest, and versioning rules are precise -## 9. Path Beyond `1.0.0-beta.7` +## 9. Path Beyond `1.0.0-beta.8` Glagol now implements the first real beta Slovo contract, the first post-beta tooling/install hardening release, the first runtime/resource foundation release, the first standard-library stabilization release, and the first diagnostics usability release, the first package/workspace discipline release, the first loopback networking foundation release, and the first -serialization/data-interchange foundation release. The remaining path is from -beta to stable. +serialization/data-interchange foundation release, and the first concrete type +alias foundation release. The remaining path is from beta to stable. Recommended compiler sequence: diff --git a/docs/papers/GLAGOL_WHITEPAPER.pdf b/docs/papers/GLAGOL_WHITEPAPER.pdf index a9a0638..a7bff57 100644 Binary files a/docs/papers/GLAGOL_WHITEPAPER.pdf and b/docs/papers/GLAGOL_WHITEPAPER.pdf differ diff --git a/docs/papers/SLOVO_MANIFEST.pdf b/docs/papers/SLOVO_MANIFEST.pdf index f148777..a8a2c62 100644 Binary files a/docs/papers/SLOVO_MANIFEST.pdf and b/docs/papers/SLOVO_MANIFEST.pdf differ diff --git a/docs/papers/SLOVO_WHITEPAPER.md b/docs/papers/SLOVO_WHITEPAPER.md index fb5db74..1650968 100644 --- a/docs/papers/SLOVO_WHITEPAPER.md +++ b/docs/papers/SLOVO_WHITEPAPER.md @@ -5,7 +5,7 @@ Sanjin Gumbarevic
hermeticum_lab@protonmail.com -Publication release: `1.0.0-beta.7` +Publication release: `1.0.0-beta.8` Technical behavior baseline: language surface through `1.0.0-beta`; tooling and install workflow through `1.0.0-beta.1`; runtime/resource foundation through @@ -13,12 +13,12 @@ and install workflow through `1.0.0-beta.1`; runtime/resource foundation through language-usability diagnostics through `1.0.0-beta.4`; package/workspace discipline through `1.0.0-beta.5`; loopback networking foundation through `1.0.0-beta.6`; serialization/data-interchange foundation through -`1.0.0-beta.7` +`1.0.0-beta.7`; concrete type alias foundation through `1.0.0-beta.8` Date: 2026-05-22 Evidence source: paired local Slovo/Glagol monorepo verification and benchmark -reruns from a local checkout; beta.7 release-gate verification from the public +reruns from a local checkout; beta.8 release-gate verification from the public monorepo Maturity: beta @@ -33,31 +33,34 @@ explicit types, explicit failure through `option` and `result`, lexical `unsafe`, and native compilation through the Glagol compiler to LLVM IR and hosted executables. -The current publication release, `1.0.0-beta.7`, keeps the first real +The current publication release, `1.0.0-beta.8`, keeps the first real general-purpose beta language baseline from `1.0.0-beta` and records the first post-beta tooling/install hardening update plus the first runtime/resource foundation update, the first standard-library stabilization update, and the first language-usability diagnostics update, plus the first local package/workspace discipline update, the first loopback networking foundation -update, and the first serialization/data-interchange foundation update. The -beta baseline includes the completed `u32` / `u64` +update, the first serialization/data-interchange foundation update, and the +first concrete type alias foundation. The beta baseline includes the completed +`u32` / `u64` unsigned scope, the staged stdlib breadth that makes ordinary command-line programs practical, the current narrow `std.net` loopback TCP surface, the -current narrow `std.json` text-construction surface, and the current -ten-scaffold benchmark suite. This paper records the current beta +current narrow `std.json` text-construction surface, module-local transparent +aliases for supported concrete types, and the current ten-scaffold benchmark +suite. This paper records the current beta technical state, the difference between Slovo and Lisp-family languages, the benchmark methodology, the beta.1 tooling update, the beta.2 runtime/resource foundation, the beta.3 standard-library stabilization slice, the beta.4 diagnostics usability slice, the beta.5 package discipline slice, the beta.6 networking foundation slice, the beta.7 serialization foundation slice, and -the remaining path from beta to stable. +the beta.8 concrete alias foundation slice, and the remaining path from beta +to stable. ## 1. Scope This document is a technical state paper for the current beta baseline. It summarizes the behavior represented by the paired local Slovo and Glagol workspaces, with `1.0.0-beta` as the current language-surface baseline and -`1.0.0-beta.7` as the current publication baseline. +`1.0.0-beta.8` as the current publication baseline. The support rule remains strict: @@ -69,7 +72,7 @@ The support rule remains strict: - partial parser recognition or speculative examples do not count as support Historical `exp-*` releases remain experimental alpha maturity. The current -publication accompanies `1.0.0-beta.7`. +publication accompanies `1.0.0-beta.8`. ## 2. Design Thesis @@ -380,10 +383,11 @@ baseline. `1.0.0-beta.1` changes tooling and install workflow, and standard-library catalog and composition coverage, `1.0.0-beta.4` improves diagnostics, `1.0.0-beta.5` tightens package/workspace discipline, and `1.0.0-beta.6` adds a narrow loopback networking foundation, and -`1.0.0-beta.7` adds a narrow JSON construction foundation. None of these -post-beta slices claims changed benchmark performance. The beta.7 -`json-quote-loop` scaffold is present for local follow-up timing and is not -part of the exp-123 nine-row result table below. +`1.0.0-beta.7` adds a narrow JSON construction foundation, and +`1.0.0-beta.8` adds transparent concrete type aliases. None of these post-beta +slices claims changed benchmark performance. The beta.7 `json-quote-loop` +scaffold is present for local follow-up timing and is not part of the exp-123 +nine-row result table below. The exp-123 publication baseline widened the paired same-machine result set from seven rows to nine by adding two owned-vector kernels: @@ -504,7 +508,7 @@ Major remaining gaps before `1.0.0`: - semantic versioning and deprecation policy - a clear separation between stable and experimental features -## 10. Path Beyond `1.0.0-beta.7` +## 10. Path Beyond `1.0.0-beta.8` The beta threshold is now real. The next work should treat `1.0.0-beta` as the language compatibility-governed baseline, `1.0.0-beta.1` as the first @@ -514,7 +518,8 @@ stabilization point, and `1.0.0-beta.4` as the first diagnostics usability point, and `1.0.0-beta.5` as the first package/workspace discipline point, and `1.0.0-beta.6` as the first loopback networking foundation point, and `1.0.0-beta.7` as the first serialization/data-interchange foundation point, -then move deliberately toward stable general-purpose status. +and `1.0.0-beta.8` as the first concrete type alias foundation point, then +move deliberately toward stable general-purpose status. Recommended sequence: diff --git a/docs/papers/SLOVO_WHITEPAPER.pdf b/docs/papers/SLOVO_WHITEPAPER.pdf index 9d287ce..092f7e8 100644 Binary files a/docs/papers/SLOVO_WHITEPAPER.pdf and b/docs/papers/SLOVO_WHITEPAPER.pdf differ diff --git a/examples/projects/std-import-json/src/main.slo b/examples/projects/std-import-json/src/main.slo index f2e0276..fa2c7b7 100644 --- a/examples/projects/std-import-json/src/main.slo +++ b/examples/projects/std-import-json/src/main.slo @@ -2,6 +2,10 @@ (import std.json (quote_string null_value bool_value i32_value u32_value i64_value u64_value f64_value field_string field_bool field_i32 field_u32 field_i64 field_u64 field_f64 field_null array0 array1 array2 array3 object0 object1 object2 object3)) +(type JsonText string) + +(type JsonField string) + (fn imported_json_quote_escapes () -> bool (if (= (quote_string "slovo") "\"slovo\"") (if (= (quote_string "slo\"vo") "\"slo\\\"vo\"") @@ -45,15 +49,21 @@ false) false)) +(fn imported_json_name_field () -> JsonField + (field_string "name" "slovo")) + +(fn imported_json_object_document () -> JsonText + (object3 (imported_json_name_field) (field_i32 "count" 3) (field_bool "ok" true))) + (fn imported_json_arrays_objects () -> bool (if (= (array0) "[]") (if (= (array1 (quote_string "a")) "[\"a\"]") (if (= (array2 (quote_string "a") (i32_value 7)) "[\"a\",7]") (if (= (array3 (quote_string "a") (i32_value 7) (bool_value true)) "[\"a\",7,true]") (if (= (object0) "{}") - (if (= (object1 (field_string "name" "slovo")) "{\"name\":\"slovo\"}") - (if (= (object2 (field_string "name" "slovo") (field_i32 "count" 3)) "{\"name\":\"slovo\",\"count\":3}") - (= (object3 (field_string "name" "slovo") (field_i32 "count" 3) (field_bool "ok" true)) "{\"name\":\"slovo\",\"count\":3,\"ok\":true}") + (if (= (object1 (imported_json_name_field)) "{\"name\":\"slovo\"}") + (if (= (object2 (imported_json_name_field) (field_i32 "count" 3)) "{\"name\":\"slovo\",\"count\":3}") + (= (imported_json_object_document) "{\"name\":\"slovo\",\"count\":3,\"ok\":true}") false) false) false) diff --git a/examples/projects/std-layout-local-json/src/json.slo b/examples/projects/std-layout-local-json/src/json.slo index 1933029..c1007ec 100644 --- a/examples/projects/std-layout-local-json/src/json.slo +++ b/examples/projects/std-layout-local-json/src/json.slo @@ -1,78 +1,82 @@ (module json (export quote_string null_value bool_value i32_value u32_value i64_value u64_value f64_value field_string field_bool field_i32 field_u32 field_i64 field_u64 field_f64 field_null array0 array1 array2 array3 object0 object1 object2 object3)) -(fn quote_string ((value string)) -> string +(type JsonText string) + +(type JsonField string) + +(fn quote_string ((value string)) -> JsonText (std.json.quote_string value)) -(fn null_value () -> string +(fn null_value () -> JsonText "null") -(fn bool_value ((value bool)) -> string +(fn bool_value ((value bool)) -> JsonText (if value "true" "false")) -(fn i32_value ((value i32)) -> string +(fn i32_value ((value i32)) -> JsonText (std.num.i32_to_string value)) -(fn u32_value ((value u32)) -> string +(fn u32_value ((value u32)) -> JsonText (std.num.u32_to_string value)) -(fn i64_value ((value i64)) -> string +(fn i64_value ((value i64)) -> JsonText (std.num.i64_to_string value)) -(fn u64_value ((value u64)) -> string +(fn u64_value ((value u64)) -> JsonText (std.num.u64_to_string value)) -(fn f64_value ((value f64)) -> string +(fn f64_value ((value f64)) -> JsonText (std.num.f64_to_string value)) -(fn field_fragment ((name string) (encoded_value string)) -> string +(fn field_fragment ((name string) (encoded_value JsonText)) -> JsonField (std.string.concat (std.string.concat (quote_string name) ":") encoded_value)) -(fn field_string ((name string) (value string)) -> string +(fn field_string ((name string) (value string)) -> JsonField (field_fragment name (quote_string value))) -(fn field_bool ((name string) (value bool)) -> string +(fn field_bool ((name string) (value bool)) -> JsonField (field_fragment name (bool_value value))) -(fn field_i32 ((name string) (value i32)) -> string +(fn field_i32 ((name string) (value i32)) -> JsonField (field_fragment name (i32_value value))) -(fn field_u32 ((name string) (value u32)) -> string +(fn field_u32 ((name string) (value u32)) -> JsonField (field_fragment name (u32_value value))) -(fn field_i64 ((name string) (value i64)) -> string +(fn field_i64 ((name string) (value i64)) -> JsonField (field_fragment name (i64_value value))) -(fn field_u64 ((name string) (value u64)) -> string +(fn field_u64 ((name string) (value u64)) -> JsonField (field_fragment name (u64_value value))) -(fn field_f64 ((name string) (value f64)) -> string +(fn field_f64 ((name string) (value f64)) -> JsonField (field_fragment name (f64_value value))) -(fn field_null ((name string)) -> string +(fn field_null ((name string)) -> JsonField (field_fragment name (null_value))) -(fn array0 () -> string +(fn array0 () -> JsonText "[]") -(fn array1 ((first string)) -> string +(fn array1 ((first JsonText)) -> JsonText (std.string.concat (std.string.concat "[" first) "]")) -(fn array2 ((first string) (second string)) -> string +(fn array2 ((first JsonText) (second JsonText)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat "[" first) ",") second) "]")) -(fn array3 ((first string) (second string) (third string)) -> string +(fn array3 ((first JsonText) (second JsonText) (third JsonText)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat "[" first) ",") second) ",") third) "]")) -(fn object0 () -> string +(fn object0 () -> JsonText "{}") -(fn object1 ((first string)) -> string +(fn object1 ((first JsonField)) -> JsonText (std.string.concat (std.string.concat "{" first) "}")) -(fn object2 ((first string) (second string)) -> string +(fn object2 ((first JsonField) (second JsonField)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat "{" first) ",") second) "}")) -(fn object3 ((first string) (second string) (third string)) -> string +(fn object3 ((first JsonField) (second JsonField) (third JsonField)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat "{" first) ",") second) ",") third) "}")) diff --git a/examples/projects/std-layout-local-json/src/main.slo b/examples/projects/std-layout-local-json/src/main.slo index 52601d7..7227a00 100644 --- a/examples/projects/std-layout-local-json/src/main.slo +++ b/examples/projects/std-layout-local-json/src/main.slo @@ -2,6 +2,10 @@ (import json (quote_string null_value bool_value i32_value u32_value i64_value u64_value f64_value field_string field_bool field_i32 field_u32 field_i64 field_u64 field_f64 field_null array0 array1 array2 array3 object0 object1 object2 object3)) +(type JsonText string) + +(type JsonField string) + (fn imported_json_quote_escapes () -> bool (if (= (quote_string "slovo") "\"slovo\"") (if (= (quote_string "slo\"vo") "\"slo\\\"vo\"") @@ -45,15 +49,21 @@ false) false)) +(fn imported_json_name_field () -> JsonField + (field_string "name" "slovo")) + +(fn imported_json_object_document () -> JsonText + (object3 (imported_json_name_field) (field_i32 "count" 3) (field_bool "ok" true))) + (fn imported_json_arrays_objects () -> bool (if (= (array0) "[]") (if (= (array1 (quote_string "a")) "[\"a\"]") (if (= (array2 (quote_string "a") (i32_value 7)) "[\"a\",7]") (if (= (array3 (quote_string "a") (i32_value 7) (bool_value true)) "[\"a\",7,true]") (if (= (object0) "{}") - (if (= (object1 (field_string "name" "slovo")) "{\"name\":\"slovo\"}") - (if (= (object2 (field_string "name" "slovo") (field_i32 "count" 3)) "{\"name\":\"slovo\",\"count\":3}") - (= (object3 (field_string "name" "slovo") (field_i32 "count" 3) (field_bool "ok" true)) "{\"name\":\"slovo\",\"count\":3,\"ok\":true}") + (if (= (object1 (imported_json_name_field)) "{\"name\":\"slovo\"}") + (if (= (object2 (imported_json_name_field) (field_i32 "count" 3)) "{\"name\":\"slovo\",\"count\":3}") + (= (imported_json_object_document) "{\"name\":\"slovo\",\"count\":3,\"ok\":true}") false) false) false) diff --git a/lib/std/README.md b/lib/std/README.md index cbce03f..e100801 100644 --- a/lib/std/README.md +++ b/lib/std/README.md @@ -35,7 +35,10 @@ prefix/suffix helper scopes; `1.0.0-beta.6` networking foundation work releases `std/net.slo` as an experimental loopback TCP facade over matching compiler-known runtime calls; `1.0.0-beta.7` serialization work releases `std/json.slo` as an experimental JSON text-construction facade over -`std.json.quote_string` and existing string/number helpers. +`std.json.quote_string` and existing string/number helpers; the +`1.0.0-beta.8` concrete type alias target keeps that same helper surface while +using local `JsonText` and `JsonField` aliases as transparent names for +`string` JSON fragments. This directory is the source home for staged standard library modules and examples. exp-44 lets project-mode source explicitly import `std/math.slo` as @@ -99,6 +102,11 @@ text-construction facade over `std.json.quote_string`, `std.string.concat`, and the current `std.num.*_to_string` helpers, while leaving JSON parsing, recursive JSON values, maps/sets, streaming encoders, schema validation, Unicode normalization, and stable text encoding policy deferred; +`1.0.0-beta.8` targets top-level concrete type aliases in source facades and +uses `JsonText` / `JsonField` only as local transparent aliases inside +`std/json.slo` and matching local JSON fixtures. These aliases are not exported +standard-library names and do not add runtime helpers, generic aliases, +parameterized aliases, maps/sets, or ABI/layout guarantees; exp-76 extends project-mode source search to `std/vec_i32.slo`, a concrete source-authored collection facade over the current promoted `std.vec.i32` runtime family; exp-77 extends that facade with concrete option-returning diff --git a/lib/std/json.slo b/lib/std/json.slo index 1933029..c1007ec 100644 --- a/lib/std/json.slo +++ b/lib/std/json.slo @@ -1,78 +1,82 @@ (module json (export quote_string null_value bool_value i32_value u32_value i64_value u64_value f64_value field_string field_bool field_i32 field_u32 field_i64 field_u64 field_f64 field_null array0 array1 array2 array3 object0 object1 object2 object3)) -(fn quote_string ((value string)) -> string +(type JsonText string) + +(type JsonField string) + +(fn quote_string ((value string)) -> JsonText (std.json.quote_string value)) -(fn null_value () -> string +(fn null_value () -> JsonText "null") -(fn bool_value ((value bool)) -> string +(fn bool_value ((value bool)) -> JsonText (if value "true" "false")) -(fn i32_value ((value i32)) -> string +(fn i32_value ((value i32)) -> JsonText (std.num.i32_to_string value)) -(fn u32_value ((value u32)) -> string +(fn u32_value ((value u32)) -> JsonText (std.num.u32_to_string value)) -(fn i64_value ((value i64)) -> string +(fn i64_value ((value i64)) -> JsonText (std.num.i64_to_string value)) -(fn u64_value ((value u64)) -> string +(fn u64_value ((value u64)) -> JsonText (std.num.u64_to_string value)) -(fn f64_value ((value f64)) -> string +(fn f64_value ((value f64)) -> JsonText (std.num.f64_to_string value)) -(fn field_fragment ((name string) (encoded_value string)) -> string +(fn field_fragment ((name string) (encoded_value JsonText)) -> JsonField (std.string.concat (std.string.concat (quote_string name) ":") encoded_value)) -(fn field_string ((name string) (value string)) -> string +(fn field_string ((name string) (value string)) -> JsonField (field_fragment name (quote_string value))) -(fn field_bool ((name string) (value bool)) -> string +(fn field_bool ((name string) (value bool)) -> JsonField (field_fragment name (bool_value value))) -(fn field_i32 ((name string) (value i32)) -> string +(fn field_i32 ((name string) (value i32)) -> JsonField (field_fragment name (i32_value value))) -(fn field_u32 ((name string) (value u32)) -> string +(fn field_u32 ((name string) (value u32)) -> JsonField (field_fragment name (u32_value value))) -(fn field_i64 ((name string) (value i64)) -> string +(fn field_i64 ((name string) (value i64)) -> JsonField (field_fragment name (i64_value value))) -(fn field_u64 ((name string) (value u64)) -> string +(fn field_u64 ((name string) (value u64)) -> JsonField (field_fragment name (u64_value value))) -(fn field_f64 ((name string) (value f64)) -> string +(fn field_f64 ((name string) (value f64)) -> JsonField (field_fragment name (f64_value value))) -(fn field_null ((name string)) -> string +(fn field_null ((name string)) -> JsonField (field_fragment name (null_value))) -(fn array0 () -> string +(fn array0 () -> JsonText "[]") -(fn array1 ((first string)) -> string +(fn array1 ((first JsonText)) -> JsonText (std.string.concat (std.string.concat "[" first) "]")) -(fn array2 ((first string) (second string)) -> string +(fn array2 ((first JsonText) (second JsonText)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat "[" first) ",") second) "]")) -(fn array3 ((first string) (second string) (third string)) -> string +(fn array3 ((first JsonText) (second JsonText) (third JsonText)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat "[" first) ",") second) ",") third) "]")) -(fn object0 () -> string +(fn object0 () -> JsonText "{}") -(fn object1 ((first string)) -> string +(fn object1 ((first JsonField)) -> JsonText (std.string.concat (std.string.concat "{" first) "}")) -(fn object2 ((first string) (second string)) -> string +(fn object2 ((first JsonField) (second JsonField)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat "{" first) ",") second) "}")) -(fn object3 ((first string) (second string) (third string)) -> string +(fn object3 ((first JsonField) (second JsonField) (third JsonField)) -> JsonText (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat (std.string.concat "{" first) ",") second) ",") third) "}")) diff --git a/tests/cyclic-type-alias.diag b/tests/cyclic-type-alias.diag new file mode 100644 index 0000000..67cdf56 --- /dev/null +++ b/tests/cyclic-type-alias.diag @@ -0,0 +1,21 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code TypeAliasCycle) + (message "type alias cycle includes `A`") + (file "") + (span + (bytes 22 23) + (range 4 7 4 8) + ) + (hint "type aliases must resolve to an existing non-alias concrete type") + (related + (span + (file "") + (bytes 33 34) + (range 5 7 5 8) + (message "cycle also includes `B`") + ) + ) +) diff --git a/tests/duplicate-type-alias.diag b/tests/duplicate-type-alias.diag new file mode 100644 index 0000000..fff67f0 --- /dev/null +++ b/tests/duplicate-type-alias.diag @@ -0,0 +1,20 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code DuplicateTypeAlias) + (message "duplicate type alias `Count`") + (file "") + (span + (bytes 39 44) + (range 5 7 5 12) + ) + (related + (span + (file "") + (bytes 22 27) + (range 4 7 4 12) + (message "original type alias") + ) + ) +) diff --git a/tests/malformed-type-alias.diag b/tests/malformed-type-alias.diag new file mode 100644 index 0000000..1457abb --- /dev/null +++ b/tests/malformed-type-alias.diag @@ -0,0 +1,13 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code MalformedTypeAlias) + (message "type alias form must be `(type Alias TargetType)`") + (file "") + (span + (bytes 16 28) + (range 4 1 4 13) + ) + (expected "(type Alias TargetType)") +) diff --git a/tests/self-type-alias.diag b/tests/self-type-alias.diag new file mode 100644 index 0000000..812fcc5 --- /dev/null +++ b/tests/self-type-alias.diag @@ -0,0 +1,13 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code SelfTypeAlias) + (message "type alias `Count` directly aliases itself") + (file "") + (span + (bytes 28 33) + (range 4 13 4 18) + ) + (hint "point the alias at an existing concrete type") +) diff --git a/tests/type-alias-name-conflict.diag b/tests/type-alias-name-conflict.diag new file mode 100644 index 0000000..f74539e --- /dev/null +++ b/tests/type-alias-name-conflict.diag @@ -0,0 +1,20 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code TypeAliasNameConflict) + (message "type alias `Count` conflicts with a struct") + (file "") + (span + (bytes 52 57) + (range 7 7 7 12) + ) + (related + (span + (file "") + (bytes 24 29) + (range 4 9 4 14) + (message "struct declaration") + ) + ) +) diff --git a/tests/type-alias-value-name-conflict.diag b/tests/type-alias-value-name-conflict.diag new file mode 100644 index 0000000..2abb3fe --- /dev/null +++ b/tests/type-alias-value-name-conflict.diag @@ -0,0 +1,41 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code TypeAliasNameConflict) + (message "type alias `main` conflicts with a function") + (file "") + (span + (bytes 61 65) + (range 6 7 6 11) + ) + (related + (span + (file "") + (bytes 90 113) + (range 10 1 11 5) + (message "function declaration") + ) + ) +) + +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code TypeAliasNameConflict) + (message "type alias `c_add` conflicts with a C import") + (file "") + (span + (bytes 78 83) + (range 8 7 8 12) + ) + (related + (span + (file "") + (bytes 26 31) + (range 4 11 4 16) + (message "C import declaration") + ) + ) +) diff --git a/tests/type-aliases.checked.lower b/tests/type-aliases.checked.lower new file mode 100644 index 0000000..17507fc --- /dev/null +++ b/tests/type-aliases.checked.lower @@ -0,0 +1,97 @@ +program main + enum Reading + variant Empty + variant Value i32 + struct Measurement + field amount: i32 + field label: string + fn bump(value: i32) -> i32 + binary + : i32 + var value : i32 + int 1 : i32 + fn make_counts(base: i32) -> (array i32 3) + array : (array i32 3) + var base : i32 + binary + : i32 + var base : i32 + int 1 : i32 + binary + : i32 + var base : i32 + int 2 : i32 + fn pick_count(values: (array i32 3)) -> i32 + index : i32 + var values : (array i32 3) + int 1 : i32 + fn make_measure(amount: i32) -> Measurement + construct Measurement : Measurement + field amount + var amount : i32 + field label + string "ok" : string + fn measure_amount(measure: Measurement) -> i32 + field-access amount : i32 + var measure : Measurement + fn maybe_amount(amount: i32) -> (option i32) + some : (option i32) + var amount : i32 + fn maybe_empty() -> (option i32) + none : (option i32) + fn ok_amount(amount: i32) -> (result i32 i32) + ok : (result i32 i32) + var amount : i32 + fn err_amount(code: i32) -> (result i32 i32) + err : (result i32 i32) + var code : i32 + fn empty_counts() -> (vec i32) + call std.vec.i32.empty : (vec i32) + fn reading_value(amount: i32) -> Reading + enum-variant Reading.Value #1 payload : Reading + var amount : i32 + fn reading_score(reading: Reading) -> i32 + match : i32 + subject + var reading : Reading + arm Reading.Empty + int 0 : i32 + arm Reading.Value amount + var amount : i32 + fn main() -> i32 + local let values : unit + call make_counts : (array i32 3) + int 40 : i32 + binary + : i32 + call pick_count : i32 + var values : (array i32 3) + call measure_amount : i32 + call make_measure : Measurement + int 1 : i32 + test "aliases erase through arrays and structs" + binary = : bool + call main : i32 + int 42 : i32 + test "aliases erase through option result vec and enum" + local let maybe : unit + call maybe_amount : (option i32) + int 7 : i32 + local let result : unit + call ok_amount : (result i32 i32) + int 8 : i32 + if : bool + is_some : bool + var maybe : (option i32) + if : bool + is_ok : bool + var result : (result i32 i32) + if : bool + binary = : bool + call std.vec.i32.len : i32 + call empty_counts : (vec i32) + int 0 : i32 + binary = : bool + call reading_score : i32 + call reading_value : Reading + int 9 : i32 + int 9 : i32 + bool false : bool + bool false : bool + bool false : bool diff --git a/tests/type-aliases.slo b/tests/type-aliases.slo new file mode 100644 index 0000000..916634a --- /dev/null +++ b/tests/type-aliases.slo @@ -0,0 +1,79 @@ +(module main) + +(type Count i32) + +(type Score Count) + +(type Text string) + +(type Counts (array Count 3)) + +(type CountVec (vec Count)) + +(type MaybeCount (option Count)) + +(type CountResult (result Count Count)) + +(struct Measurement + (amount Count) + (label Text)) + +(type Measure Measurement) + +(enum Reading + Empty + (Value Count)) + +(type ReadingAlias Reading) + +(fn bump ((value Count)) -> Score + (+ value 1)) + +(fn make_counts ((base Count)) -> Counts + (array Count base (+ base 1) (+ base 2))) + +(fn pick_count ((values Counts)) -> Count + (index values 1)) + +(fn make_measure ((amount Count)) -> Measure + (Measurement (amount amount) (label "ok"))) + +(fn measure_amount ((measure Measure)) -> Count + (. measure amount)) + +(fn maybe_amount ((amount Count)) -> MaybeCount + (some Count amount)) + +(fn maybe_empty () -> MaybeCount + (none Count)) + +(fn ok_amount ((amount Count)) -> CountResult + (ok Count Count amount)) + +(fn err_amount ((code Count)) -> CountResult + (err Count Count code)) + +(fn empty_counts () -> CountVec + (std.vec.i32.empty)) + +(fn reading_value ((amount Count)) -> ReadingAlias + (Reading.Value amount)) + +(fn reading_score ((reading ReadingAlias)) -> Count + (match reading + ((Reading.Empty) + 0) + ((Reading.Value amount) + amount))) + +(fn main () -> i32 + (let values Counts (make_counts 40)) + (+ (pick_count values) (measure_amount (make_measure 1)))) + +(test "aliases erase through arrays and structs" + (= (main) 42)) + +(test "aliases erase through option result vec and enum" + (let maybe MaybeCount (maybe_amount 7)) + (let result CountResult (ok_amount 8)) + (and (is_some maybe) (and (is_ok result) (and (= (std.vec.i32.len (empty_counts)) 0) (= (reading_score (reading_value 9)) 9))))) diff --git a/tests/type-aliases.surface.lower b/tests/type-aliases.surface.lower new file mode 100644 index 0000000..3f218a9 --- /dev/null +++ b/tests/type-aliases.surface.lower @@ -0,0 +1,106 @@ +program main + type Count = i32 + type Score = Count + type Text = string + type Counts = (array Count 3) + type CountVec = (vec Count) + type MaybeCount = (option Count) + type CountResult = (result Count Count) + type Measure = Measurement + type ReadingAlias = Reading + enum Reading + variant Empty + variant Value Count + struct Measurement + field amount: Count + field label: Text + fn bump(value: Count) -> Score + binary + + var value + int 1 + fn make_counts(base: Count) -> Counts + array Count + var base + binary + + var base + int 1 + binary + + var base + int 2 + fn pick_count(values: Counts) -> Count + index + var values + int 1 + fn make_measure(amount: Count) -> Measure + construct Measurement + field amount + var amount + field label + string "ok" + fn measure_amount(measure: Measure) -> Count + field-access amount + var measure + fn maybe_amount(amount: Count) -> MaybeCount + some Count + var amount + fn maybe_empty() -> MaybeCount + none Count + fn ok_amount(amount: Count) -> CountResult + ok Count Count + var amount + fn err_amount(code: Count) -> CountResult + err Count Count + var code + fn empty_counts() -> CountVec + call std.vec.i32.empty + fn reading_value(amount: Count) -> ReadingAlias + enum-variant Reading.Value + var amount + fn reading_score(reading: ReadingAlias) -> Count + match + subject + var reading + arm Reading.Empty + int 0 + arm Reading.Value amount + var amount + fn main() -> i32 + local let values: Counts + call make_counts + int 40 + binary + + call pick_count + var values + call measure_amount + call make_measure + int 1 + test "aliases erase through arrays and structs" + binary = + call main + int 42 + test "aliases erase through option result vec and enum" + local let maybe: MaybeCount + call maybe_amount + int 7 + local let result: CountResult + call ok_amount + int 8 + if + is_some + var maybe + if + is_ok + var result + if + binary = + call std.vec.i32.len + call empty_counts + int 0 + binary = + call reading_score + call reading_value + int 9 + int 9 + bool false + bool false + bool false diff --git a/tests/unknown-type-alias-target.diag b/tests/unknown-type-alias-target.diag new file mode 100644 index 0000000..bf561b1 --- /dev/null +++ b/tests/unknown-type-alias-target.diag @@ -0,0 +1,15 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code UnknownTypeAliasTarget) + (message "type alias target `Missing` is not a known concrete type") + (file "") + (span + (bytes 28 35) + (range 4 13 4 20) + ) + (expected "built-in type, known struct, known enum, or known type alias") + (found "Missing") + (hint "declare the target type or alias before checking this alias set") +) diff --git a/tests/unsupported-type-alias-target.diag b/tests/unsupported-type-alias-target.diag new file mode 100644 index 0000000..5169128 --- /dev/null +++ b/tests/unsupported-type-alias-target.diag @@ -0,0 +1,79 @@ +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code UnsupportedTypeAliasTarget) + (message "type alias target type is not supported in beta.8") + (file "") + (span + (bytes 50 59) + (range 6 14 6 23) + ) + (expected "i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec") + (found "(ptr i32)") + (hint "aliases are transparent and may only target concrete types already supported in the target use positions") +) + +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code UnsupportedTypeAliasTarget) + (message "type alias target type is not supported in beta.8") + (file "") + (span + (bytes 132 151) + (range 12 17 12 36) + ) + (expected "i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec") + (found "(result i32 string)") + (hint "aliases are transparent and may only target concrete types already supported in the target use positions") +) + +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code UnsupportedTypeAliasTarget) + (message "type alias target type is not supported in beta.8") + (file "") + (span + (bytes 77 88) + (range 8 16 8 27) + ) + (expected "i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec") + (found "(slice i32)") + (hint "aliases are transparent and may only target concrete types already supported in the target use positions") +) + +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code UnsupportedTypeAliasTarget) + (message "type alias target type is not supported in beta.8") + (file "") + (span + (bytes 30 34) + (range 4 15 4 19) + ) + (expected "i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec") + (found "unit") + (hint "aliases are transparent and may only target concrete types already supported in the target use positions") +) + +(diagnostic + (schema slovo.diagnostic) + (version 1) + (severity error) + (code UnsupportedTypeAliasTarget) + (message "type alias target type is not supported in beta.8") + (file "") + (span + (bytes 104 113) + (range 10 14 10 23) + ) + (expected "i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec") + (found "(vec u32)") + (hint "aliases are transparent and may only target concrete types already supported in the target use positions") +)