From ad33ceeb6081100b3a91a9ef52189e0d36e5afe3 Mon Sep 17 00:00:00 2001 From: "Craig M. Hamel" Date: Fri, 12 Jun 2026 17:59:50 -0400 Subject: [PATCH 1/5] An initial stab at a juliac safe yaml parser. Ceated a general input file parsing hook that can switch between different file formats. --- examples/app/input-file.toml | 15 +- examples/app/input-file.yaml | 41 + examples/app/src/MyApp.jl | 3 +- examples/simple_yaml/Project.toml | 3 + examples/simple_yaml/make.jl | 28 + examples/simple_yaml/src/SimpleYAML.jl | 1860 +++++++++++++++++ examples/simple_yaml/src/main.jl | 68 + examples/simple_yaml/test/example_file.yaml | 41 + examples/simple_yaml/test/test_files/bar.yaml | 40 + examples/simple_yaml/test/test_yaml.jl | 369 ++++ src/AppTools.jl | 160 +- src/FiniteElementContainers.jl | 2 + src/parser/InputFileParser.jl | 84 + src/parser/SimpleYAML.jl | 865 ++++++++ 14 files changed, 3496 insertions(+), 83 deletions(-) create mode 100644 examples/app/input-file.yaml create mode 100644 examples/simple_yaml/Project.toml create mode 100644 examples/simple_yaml/make.jl create mode 100644 examples/simple_yaml/src/SimpleYAML.jl create mode 100644 examples/simple_yaml/src/main.jl create mode 100644 examples/simple_yaml/test/example_file.yaml create mode 100644 examples/simple_yaml/test/test_files/bar.yaml create mode 100644 examples/simple_yaml/test/test_yaml.jl create mode 100644 src/parser/InputFileParser.jl create mode 100644 src/parser/SimpleYAML.jl diff --git a/examples/app/input-file.toml b/examples/app/input-file.toml index f908d53c..832fe791 100644 --- a/examples/app/input-file.toml +++ b/examples/app/input-file.toml @@ -12,20 +12,21 @@ expressions = ["2 * pi^2 * sin(2 * pi * x) * sin(2 * pi * y)"] variables = ["x", "y", "t"] [mesh] -file_path = "poisson.g" -file_type = "exodus" +dimension = 2 +"file path" = "poisson.g" +"file type" = "exodus" -[[boundary_conditions.dirichlet]] +[["boundary conditions".dirichlet]] function = "zero_bc" -side_sets = ["sset_1", "sset_2"] +"side sets" = ["sset_1", "sset_2"] variables = ["u"] -[[boundary_conditions.dirichlet]] +[["boundary conditions".dirichlet]] function = "zero_bc" -side_sets = ["sset_3", "sset_4"] +"side sets" = ["sset_3", "sset_4"] variables = ["u"] -[[boundary_conditions.source]] +[["boundary conditions".source]] function = "source_func" blocks = ["block_1"] variables = ["u"] diff --git a/examples/app/input-file.yaml b/examples/app/input-file.yaml new file mode 100644 index 00000000..d5642cd0 --- /dev/null +++ b/examples/app/input-file.yaml @@ -0,0 +1,41 @@ +device: + backend: cpu + +functions: + zero_ic: + type: scalar expression + expression: "0.0" + variables: [x, y] + zero_bc: + type: scalar expression + expression: "0.0" + variables: [x, y, t] + source_func: + type: vector expression + expressions: + - "2 * pi^2 * sin(2 * pi * x) * sin(2 * pi * y)" + variables: [x, y, t] + +mesh: + dimension: 2 + file path: poisson.g + file type: exodus + +initial conditions: + - blocks: [block_1] + function: zero_ic + variables: [u] + +boundary conditions: + dirichlet: + - function: zero_bc + side sets: [sset_1, sset_2] + variables: [u] + - function: zero_bc + side sets: [sset_3, sset_4] + variables: [u] + + source: + - function: source_func + blocks: [block_1] + variables: [u] diff --git a/examples/app/src/MyApp.jl b/examples/app/src/MyApp.jl index f5045d80..51aaefd7 100644 --- a/examples/app/src/MyApp.jl +++ b/examples/app/src/MyApp.jl @@ -11,6 +11,7 @@ include("Physics.jl") # f(X, _) = 2. * π^2 * sin(2π * X[1]) * sin(2π * X[2]) +const D = 2 const N = 1 function app_main(ARGS::Vector{String}) @@ -26,7 +27,7 @@ function app_main(ARGS::Vector{String}) ################################################## # Setup app ################################################## - app = AT.App{N}("MyApp") + app = AT.App{D, N}("MyApp") sim = AT.setup(app, ARGS) ##################################### diff --git a/examples/simple_yaml/Project.toml b/examples/simple_yaml/Project.toml new file mode 100644 index 00000000..c028499f --- /dev/null +++ b/examples/simple_yaml/Project.toml @@ -0,0 +1,3 @@ +name = "MyApp" + +[deps] diff --git a/examples/simple_yaml/make.jl b/examples/simple_yaml/make.jl new file mode 100644 index 00000000..a9a1963d --- /dev/null +++ b/examples/simple_yaml/make.jl @@ -0,0 +1,28 @@ +using JuliaC +build_path = joinpath(@__DIR__, "build") +src_path = joinpath(@__DIR__) +@show build_path +@show src_path +rm(build_path; force=true, recursive=true) + +img = ImageRecipe( + output_type = "--output-exe", + file = "$src_path/src/main.jl", + trim_mode = "safe", + add_ccallables = false, + verbose = false, +) + +link = LinkRecipe( + image_recipe = img, + outname = "$build_path/my_app" +) + +bun = BundleRecipe( + link_recipe = link, + output_dir = build_path # or `nothing` to skip bundling +) + +compile_products(img) +link_products(link) +bundle_products(bun) diff --git a/examples/simple_yaml/src/SimpleYAML.jl b/examples/simple_yaml/src/SimpleYAML.jl new file mode 100644 index 00000000..980f836a --- /dev/null +++ b/examples/simple_yaml/src/SimpleYAML.jl @@ -0,0 +1,1860 @@ +# module SimpleYAML + +# export YAMLValue, YAMLNull, YAMLBool, YAMLInt, YAMLFloat, YAMLString, +# YAMLArray, YAMLDict, +# load, loads, +# as_dict, as_array, as_string, as_int, as_float, as_bool, is_null, +# get_value + +# # ────────────────────────────────────────────────────────────────────────────── +# # Value type +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# Tagged-union value node. Every YAML value is one of the seven tags below. +# Using a concrete struct with a tag enum keeps things type-stable for the +# compiler even though the result tree is heterogeneous. +# """ +# @enum YAMLTag begin +# TAG_NULL +# TAG_BOOL +# TAG_INT +# TAG_FLOAT +# TAG_STRING +# TAG_ARRAY +# TAG_DICT +# end + +# struct YAMLValue +# tag :: YAMLTag +# # Scalar storage (only one is used at a time) +# bval :: Bool +# ival :: Int64 +# fval :: Float64 +# sval :: String +# # Collection storage +# arr :: Vector{YAMLValue} +# dict :: Dict{String, YAMLValue} +# end + +# # ── Constructors ────────────────────────────────────────────────────────────── + +# const YAMLNull = YAMLValue(TAG_NULL, false, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +# YAMLBool(b::Bool) = YAMLValue(TAG_BOOL, b, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +# YAMLInt(i::Int64) = YAMLValue(TAG_INT, false, i, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +# YAMLFloat(f::Float64)= YAMLValue(TAG_FLOAT, false, 0, f, "", YAMLValue[], Dict{String,YAMLValue}()) +# YAMLString(s::AbstractString)= YAMLValue(TAG_STRING,false, 0, 0.0, String(s), YAMLValue[], Dict{String,YAMLValue}()) +# YAMLArray(a::Vector{YAMLValue}) = YAMLValue(TAG_ARRAY, false,0,0.0,"", a, Dict{String,YAMLValue}()) +# YAMLDict(d::Dict{String,YAMLValue}) = YAMLValue(TAG_DICT, false,0,0.0,"", YAMLValue[], d) + +# # ── Accessors ──────────────────────────────────────────────────────────────── + +# is_null(v::YAMLValue) = v.tag === TAG_NULL + +# function as_bool(v::YAMLValue)::Bool +# v.tag === TAG_BOOL || error("YAMLValue is not a bool (tag=$(v.tag))") +# return v.bval +# end +# function as_int(v::YAMLValue)::Int64 +# v.tag === TAG_INT || error("YAMLValue is not an int (tag=$(v.tag))") +# return v.ival +# end +# function as_float(v::YAMLValue)::Float64 +# v.tag === TAG_FLOAT || error("YAMLValue is not a float (tag=$(v.tag))") +# return v.fval +# end +# function as_string(v::YAMLValue)::String +# v.tag === TAG_STRING || error("YAMLValue is not a string (tag=$(v.tag))") +# return v.sval +# end +# function as_array(v::YAMLValue)::Vector{YAMLValue} +# v.tag === TAG_ARRAY || error("YAMLValue is not an array (tag=$(v.tag))") +# return v.arr +# end +# function as_dict(v::YAMLValue)::Dict{String,YAMLValue} +# v.tag === TAG_DICT || error("YAMLValue is not a dict (tag=$(v.tag))") +# return v.dict +# end + +# function Base.show(io::IO, v::YAMLValue) +# if v.tag === TAG_NULL; print(io, "null") +# elseif v.tag === TAG_BOOL; print(io, v.bval ? "true" : "false") +# elseif v.tag === TAG_INT; print(io, v.ival) +# elseif v.tag === TAG_FLOAT; print(io, v.fval) +# elseif v.tag === TAG_STRING; print(io, repr(v.sval)) +# elseif v.tag === TAG_ARRAY +# print(io, "[") +# for (i, x) in enumerate(v.arr) +# i > 1 && print(io, ", ") +# show(io, x) +# end +# print(io, "]") +# else # TAG_DICT +# print(io, "{") +# first = true +# for (k, val) in v.dict +# first || print(io, ", ") +# first = false +# print(io, repr(k), ": ") +# show(io, val) +# end +# print(io, "}") +# end +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Parser state +# # ────────────────────────────────────────────────────────────────────────────── + +# mutable struct ParseState +# lines :: Vector{String} +# lineno :: Int +# anchors :: Dict{String, YAMLValue} # anchor name → value +# end + +# ParseState(src::String) = ParseState(split(src, '\n'), 1, Dict{String,YAMLValue}()) + +# # ── Low-level line helpers ──────────────────────────────────────────────────── + +# @inline at_end(ps::ParseState) = ps.lineno > length(ps.lines) + +# @inline current_line(ps::ParseState) = +# ps.lineno <= length(ps.lines) ? ps.lines[ps.lineno] : "" + +# function peek_indent(ps::ParseState) +# # Look ahead past blank / comment lines to find the indent of the next +# # content line. Returns -1 at EOF. +# i = ps.lineno +# while i <= length(ps.lines) +# l = ps.lines[i] +# s = lstrip(l) +# if !isempty(s) && s[1] != '#' +# return ncodeunits(l) - ncodeunits(s) # byte-count indent +# end +# i += 1 +# end +# return -1 +# end + +# """Skip blank lines and comment-only lines, advancing ps.lineno.""" +# function skip_empty!(ps::ParseState) +# while !at_end(ps) +# l = current_line(ps) +# s = lstrip(l) +# if isempty(s) || s[1] == '#' +# ps.lineno += 1 +# else +# break +# end +# end +# end + +# """Return the indent (number of leading spaces) of a line.""" +# function line_indent(l::String) +# i = 1 +# n = ncodeunits(l) +# while i <= n && codeunit(l, i) == UInt8(' ') +# i += 1 +# end +# return i - 1 +# end + +# """Strip inline comment from the end of a scalar string.""" +# function strip_inline_comment(s::AbstractString) +# # A comment is ' #...' — space then hash +# i = 1 +# n = ncodeunits(s) +# while i <= n +# c = codeunit(s, i) +# if c == UInt8('#') && i > 1 && codeunit(s, i-1) == UInt8(' ') +# return rstrip(s[1:i-2]) +# end +# i += 1 +# end +# return s +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Scalar parsing helpers +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# Parse a YAML bare scalar string into a typed YAMLValue. +# Handles null, bool, int (decimal/hex/octal/binary), float, and string fallback. +# """ +# function parse_scalar(s::AbstractString)::YAMLValue +# isempty(s) && return YAMLNull + +# # null +# (s == "null" || s == "Null" || s == "NULL" || s == "~") && return YAMLNull + +# # bool +# (s == "true" || s == "True" || s == "TRUE") && return YAMLBool(true) +# (s == "false"|| s == "False"|| s == "FALSE") && return YAMLBool(false) + +# # special floats +# (s == ".inf" || s == ".Inf" || s == ".INF" || +# s == "+.inf"|| s == "+.Inf"|| s == "+.INF") && return YAMLFloat(Inf) +# (s == "-.inf"|| s == "-.Inf"|| s == "-.INF") && return YAMLFloat(-Inf) +# (s == ".nan" || s == ".NaN" || s == ".NAN") && return YAMLFloat(NaN) + +# # integers (0x… hex, 0o… octal, 0b… binary, plain decimal) +# if length(s) >= 3 && s[1] == '0' && s[2] == 'x' +# v = tryparse(Int64, s[3:end]; base=16) +# v !== nothing && return YAMLInt(v) +# elseif length(s) >= 3 && s[1] == '0' && s[2] == 'o' +# v = tryparse(Int64, s[3:end]; base=8) +# v !== nothing && return YAMLInt(v) +# elseif length(s) >= 3 && s[1] == '0' && s[2] == 'b' +# v = tryparse(Int64, s[3:end]; base=2) +# v !== nothing && return YAMLInt(v) +# else +# v = tryparse(Int64, s) +# v !== nothing && return YAMLInt(v) +# end + +# # float +# fv = tryparse(Float64, s) +# fv !== nothing && return YAMLFloat(fv) + +# # fallback to string +# return YAMLString(s) +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Quoted-string parsing +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# Parse a single-quoted YAML string starting just after the opening quote. +# Returns (String, new_index) where new_index points past the closing quote. +# """ +# function parse_single_quoted(s::AbstractString, start::Int)::Tuple{String,Int} +# buf = IOBuffer() +# i = start +# n = ncodeunits(s) +# while i <= n +# c = s[i] +# if c == '\'' +# # '' is an escaped single-quote inside single-quoted strings +# if i + 1 <= n && s[i+1] == '\'' +# write(buf, '\'') +# i += 2 +# else +# i += 1 # skip closing quote +# break +# end +# else +# write(buf, c) +# i = nextind(s, i) +# end +# end +# return String(take!(buf)), i +# end + +# """ +# Parse a double-quoted YAML string starting just after the opening quote. +# Handles standard JSON-like escape sequences plus YAML-specific \\N \\L \\P. +# """ +# function parse_double_quoted(s::AbstractString, start::Int)::Tuple{String,Int} +# buf = IOBuffer() +# i = start +# n = ncodeunits(s) +# while i <= n +# c = s[i] +# if c == '"' +# i += 1 +# break +# elseif c == '\\' +# i += 1 +# i > n && break +# esc = s[i] +# if esc == '0'; write(buf, '\0') +# elseif esc == 'a'; write(buf, '\a') +# elseif esc == 'b'; write(buf, '\b') +# elseif esc == 't' || esc == '\t'; write(buf, '\t') +# elseif esc == 'n'; write(buf, '\n') +# elseif esc == 'v'; write(buf, '\v') +# elseif esc == 'f'; write(buf, '\f') +# elseif esc == 'r'; write(buf, '\r') +# elseif esc == 'e'; write(buf, '\e') +# elseif esc == ' '; write(buf, ' ') +# elseif esc == '"'; write(buf, '"') +# elseif esc == '/'; write(buf, '/') +# elseif esc == '\\'; write(buf, '\\') +# elseif esc == 'N'; write(buf, '\u0085') # YAML next-line +# elseif esc == '_'; write(buf, '\u00a0') # YAML non-breaking space +# elseif esc == 'L'; write(buf, '\u2028') # YAML line-separator +# elseif esc == 'P'; write(buf, '\u2029') # YAML para-separator +# elseif esc == 'x' +# i += 1 +# hex = (i+1 <= n) ? s[i:i+1] : "" +# cp = tryparse(UInt32, hex; base=16) +# cp !== nothing ? write(buf, Char(cp)) : write(buf, '?') +# i += 1 +# elseif esc == 'u' +# i += 1 +# hex = (i+3 <= n) ? s[i:i+3] : "" +# cp = tryparse(UInt32, hex; base=16) +# cp !== nothing ? write(buf, Char(cp)) : write(buf, '?') +# i += 3 +# elseif esc == 'U' +# i += 1 +# hex = (i+7 <= n) ? s[i:i+7] : "" +# cp = tryparse(UInt32, hex; base=16) +# cp !== nothing ? write(buf, Char(cp)) : write(buf, '?') +# i += 7 +# else +# write(buf, esc) +# end +# i = nextind(s, i) +# else +# write(buf, c) +# i = nextind(s, i) +# end +# end +# return String(take!(buf)), i +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Block scalar parsing (| and >) +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# Parse a literal block scalar (|) or folded block scalar (>). +# `header` is the rest of the line after the indicator, e.g. "|-", "|2", "|+2". +# Returns the assembled String. +# """ +# function parse_block_scalar(ps::ParseState, style::Char, header::AbstractString)::String +# # Parse chomping indicator and explicit indent +# chomping = :clip # clip | strip (-) | keep (+) +# explicit_indent = 0 + +# rest = strip(header) +# # strip trailing comment +# ci = findfirst('#', rest) +# ci !== nothing && (rest = rstrip(rest[1:ci-1])) + +# i = 1 +# while i <= length(rest) +# c = rest[i] +# if c == '-'; chomping = :strip; i += 1 +# elseif c == '+'; chomping = :keep; i += 1 +# elseif isdigit(c) +# explicit_indent = parse(Int, string(c)) +# i += 1 +# else +# break +# end +# end + +# # The header line has already been consumed by the caller. +# # Determine block indent from the first non-empty content line +# block_indent = explicit_indent +# if block_indent == 0 +# j = ps.lineno +# while j <= length(ps.lines) +# l = ps.lines[j] +# s = lstrip(l) +# if !isempty(s) +# block_indent = line_indent(l) +# break +# end +# j += 1 +# end +# end + +# lines_out = String[] +# while !at_end(ps) +# l = current_line(ps) +# indent = line_indent(l) +# content = lstrip(l) + +# # Empty lines are kept (as "") regardless of indent +# if isempty(content) +# push!(lines_out, "") +# ps.lineno += 1 +# continue +# end + +# # A line with less indent than block_indent ends the scalar +# indent < block_indent && break + +# push!(lines_out, l[block_indent+1:end]) +# ps.lineno += 1 +# end + +# # Assemble +# if style == '|' +# # Literal: join with newlines +# result = join(lines_out, "\n") +# else +# # Folded: fold non-empty lines separated by single newlines; +# # blank lines become paragraph breaks. +# buf = IOBuffer() +# prev_empty = false +# for (idx, ln) in enumerate(lines_out) +# if isempty(ln) +# prev_empty = true +# else +# if idx > 1 && !prev_empty +# write(buf, ' ') +# elseif prev_empty +# write(buf, '\n') +# end +# write(buf, ln) +# prev_empty = false +# end +# end +# result = String(take!(buf)) +# end + +# # Apply chomping +# if chomping === :strip +# result = rstrip(result, '\n') +# elseif chomping === :clip +# # Ensure exactly one trailing newline +# result = rstrip(result, '\n') * "\n" +# else # keep — preserve all trailing newlines +# # already correct +# end + +# return result +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Flow (inline) parsing +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# Parse a flow (inline) value starting at position `i` in string `s`. +# Returns (YAMLValue, new_index). +# """ +# function parse_flow_value(ps::ParseState, s::AbstractString, i::Int)::Tuple{YAMLValue,Int} +# # skip leading whitespace +# n = ncodeunits(s) +# while i <= n && (s[i] == ' ' || s[i] == '\t') +# i += 1 +# end +# i > n && return YAMLNull, i + +# c = s[i] + +# if c == '{' +# return parse_flow_mapping(ps, s, i+1) +# elseif c == '[' +# return parse_flow_sequence(ps, s, i+1) +# elseif c == '\'' +# str, i2 = parse_single_quoted(s, i+1) +# return YAMLString(str), i2 +# elseif c == '"' +# str, i2 = parse_double_quoted(s, i+1) +# return YAMLString(str), i2 +# elseif c == '&' +# # Anchor in flow context — parse the name then the value +# j = i + 1 +# while j <= n && s[j] != ' ' && s[j] != ',' && s[j] != '}' && s[j] != ']' +# j += 1 +# end +# anchor_name = s[i+1:j-1] +# val, i2 = parse_flow_value(ps, s, j) +# ps.anchors[anchor_name] = val +# return val, i2 +# elseif c == '*' +# j = i + 1 +# while j <= n && s[j] != ' ' && s[j] != ',' && s[j] != '}' && s[j] != ']' +# j += 1 +# end +# alias_name = s[i+1:j-1] +# val = get(ps.anchors, alias_name, YAMLNull) +# return val, j +# else +# # bare scalar — read until , } ] or end +# j = i +# while j <= n && s[j] != ',' && s[j] != '}' && s[j] != ']' && s[j] != '#' +# j += 1 +# end +# raw = strip(s[i:j-1]) +# return parse_scalar(raw), j +# end +# end + +# function parse_flow_sequence(ps::ParseState, s::AbstractString, i::Int)::Tuple{YAMLValue,Int} +# items = YAMLValue[] +# n = ncodeunits(s) +# while i <= n +# # skip whitespace +# while i <= n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n') +# i += 1 +# end +# i > n && break +# s[i] == ']' && (i += 1; break) +# s[i] == ',' && (i += 1; continue) + +# val, i = parse_flow_value(ps, s, i) +# push!(items, val) + +# # skip whitespace +# while i <= n && (s[i] == ' ' || s[i] == '\t') +# i += 1 +# end +# i <= n && s[i] == ',' && (i += 1) +# end +# return YAMLArray(items), i +# end + +# function parse_flow_mapping(ps::ParseState, s::AbstractString, i::Int)::Tuple{YAMLValue,Int} +# d = Dict{String,YAMLValue}() +# n = ncodeunits(s) +# while i <= n +# while i <= n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n') +# i += 1 +# end +# i > n && break +# s[i] == '}' && (i += 1; break) +# s[i] == ',' && (i += 1; continue) + +# # parse key +# key_val, i = parse_flow_value(ps, s, i) +# key = (key_val.tag === TAG_STRING ? key_val.sval : string_of(key_val)) + +# # skip whitespace and colon +# while i <= n && (s[i] == ' ' || s[i] == '\t') +# i += 1 +# end +# i <= n && s[i] == ':' && (i += 1) + +# # parse value +# val, i = parse_flow_value(ps, s, i) +# d[key] = val + +# while i <= n && (s[i] == ' ' || s[i] == '\t') +# i += 1 +# end +# i <= n && s[i] == ',' && (i += 1) +# end +# return YAMLDict(d), i +# end + +# function string_of(v::YAMLValue)::String +# v.tag === TAG_STRING && return v.sval +# v.tag === TAG_INT && return string(v.ival) +# v.tag === TAG_FLOAT && return string(v.fval) +# v.tag === TAG_BOOL && return v.bval ? "true" : "false" +# v.tag === TAG_NULL && return "null" +# return "???" +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Main recursive block parser +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# Parse a YAML node (mapping, sequence, or scalar) whose content lines all +# have indent ≥ `min_indent`. The caller must have already skipped blanks. + +# Returns a YAMLValue. +# """ +# function parse_node(ps::ParseState, min_indent::Int)::YAMLValue +# skip_empty!(ps) +# at_end(ps) && return YAMLNull + +# l = current_line(ps) +# indent = line_indent(l) +# indent < min_indent && return YAMLNull + +# content = lstrip(l) +# isempty(content) && return YAMLNull + +# # ── document separator ──────────────────────────────────────────────────── +# if startswith(content, "---") || startswith(content, "...") +# # Only advance past if it's a true separator (followed by space/EOF) +# rest = length(content) > 3 ? content[4:4] : " " +# if rest == " " || rest == "\n" || rest == "#" +# ps.lineno += 1 +# return parse_node(ps, min_indent) +# end +# end + +# # ── block sequence ──────────────────────────────────────────────────────── +# if content[1] == '-' && (length(content) == 1 || content[2] == ' ' || content[2] == '\n') +# return parse_block_sequence(ps, indent) +# end + +# # ── explicit key with '?' ───────────────────────────────────────────────── +# # Not commonly needed; skip complex mapping keys for now. + +# # ── mapping or scalar ───────────────────────────────────────────────────── +# # Try to detect a block mapping key on this line. +# key, colon_pos = find_mapping_key(content) +# if key !== nothing +# return parse_block_mapping(ps, indent) +# end + +# # ── scalar value ────────────────────────────────────────────────────────── +# return parse_line_scalar(ps, min_indent) +# end + +# """ +# Determine if `content` (stripped line) begins with a mapping key. +# Returns (key_string, position_after_colon) or (nothing, 0). +# A mapping key is: : … or "quoted": … or 'quoted': … +# """ +# function find_mapping_key(content::AbstractString) + +# isempty(content) && return nothing, 0 + +# i = firstindex(content) + +# while i <= lastindex(content) + +# if content[i] == ':' + +# ni = nextind(content,i) + +# if ni > lastindex(content) || +# content[ni] == ' ' || +# content[ni] == '\t' || +# content[ni] == '#' + +# key = rstrip(content[firstindex(content):prevind(content,i)]) + +# isempty(key) && return nothing, 0 + +# return key, ni +# end +# end + +# i = nextind(content,i) +# end + +# return nothing,0 +# end + +# """Parse a block mapping whose first key is on a line with indent `map_indent`.""" +# function parse_block_mapping(ps::ParseState, map_indent::Int)::YAMLValue +# d = Dict{String,YAMLValue}() + +# while !at_end(ps) +# skip_empty!(ps) +# at_end(ps) && break + +# l = current_line(ps) +# indent = line_indent(l) +# indent < map_indent && break # outdented → mapping ends +# indent > map_indent && break # shouldn't happen; bail + +# content = lstrip(l) +# isempty(content) && (ps.lineno += 1; continue) + +# # Stop at document markers +# (startswith(content, "---") || startswith(content, "...")) && break + +# # Must be a mapping key at this indent +# key, after_colon = find_mapping_key(content) +# key === nothing && break + +# ps.lineno += 1 # consume key line + +# # Check for anchor on the key line, after the colon +# anchor_name = nothing +# rest_of_line = after_colon <= ncodeunits(content) ? lstrip(content[after_colon:end]) : "" + +# # Strip trailing comment from rest_of_line for non-block scalars +# rest_stripped = strip_inline_comment(rstrip(rest_of_line)) + +# # # Determine value +# # val = if isempty(rest_stripped) || rest_stripped[1] == '#' +# # # Value is on the next line(s) — block node +# # next_ind = peek_indent(ps) +# # if next_ind > map_indent +# # parse_node(ps, next_ind) +# # else +# # YAMLNull +# # end +# # Determine value +# val = if isempty(rest_stripped) || rest_stripped[1] == '#' + +# # Value lives on subsequent line(s) +# next_ind = peek_indent(ps) + +# if next_ind < 0 +# # EOF +# YAMLNull + +# elseif next_ind > map_indent +# # Indented mapping/scalar +# parse_node(ps, next_ind) + +# elseif next_ind == map_indent +# # YAML allows +# # +# # key: +# # - item +# # +# # where the sequence indicator is aligned with the key. +# next_line = lstrip(current_line(ps)) + +# if !isempty(next_line) && +# next_line[1] == '-' && +# (length(next_line) == 1 || +# next_line[2] == ' ' || +# next_line[2] == '\t') + +# parse_block_sequence(ps, next_ind) +# else +# YAMLNull +# end + +# else +# YAMLNull +# end +# elseif rest_stripped[1] == '|' || rest_stripped[1] == '>' +# # Block scalar +# style = rest_stripped[1] +# header = length(rest_stripped) > 1 ? rest_stripped[2:end] : "" +# YAMLString(parse_block_scalar(ps, style, header)) +# elseif rest_stripped[1] == '{' +# v, _ = parse_flow_mapping(ps, rest_stripped, 2) +# v +# elseif rest_stripped[1] == '[' +# v, _ = parse_flow_sequence(ps, rest_stripped, 2) +# v +# elseif rest_stripped[1] == '&' +# # Anchor reference +# j = 2 +# n2 = ncodeunits(rest_stripped) +# while j <= n2 && rest_stripped[j] != ' ' +# j += 1 +# end +# anchor_name2 = rest_stripped[2:j-1] +# inner = lstrip(rest_stripped[j:end]) +# v = if isempty(inner) || inner[1] == '#' +# next_ind = peek_indent(ps) +# next_ind > map_indent ? parse_node(ps, next_ind) : YAMLNull +# else +# parse_scalar(strip_inline_comment(inner)) +# end +# ps.anchors[anchor_name2] = v +# v +# elseif rest_stripped[1] == '*' +# # Alias +# j = 2 +# n2 = ncodeunits(rest_stripped) +# while j <= n2 && rest_stripped[j] != ' ' && rest_stripped[j] != '#' +# j += 1 +# end +# alias_name = rest_stripped[2:j-1] +# get(ps.anchors, alias_name, YAMLNull) +# else +# parse_scalar(rest_stripped) +# end + +# d[key] = val +# end + +# return YAMLDict(d) +# end + +# """Parse a block sequence whose '-' indicators are at indent `seq_indent`.""" +# function parse_block_sequence(ps::ParseState, seq_indent::Int)::YAMLValue +# items = YAMLValue[] + +# while !at_end(ps) +# skip_empty!(ps) +# at_end(ps) && break + +# l = current_line(ps) +# indent = line_indent(l) +# indent < seq_indent && break +# indent > seq_indent && break # continuation handled inside + +# content = lstrip(l) +# isempty(content) && (ps.lineno += 1; continue) +# content[1] != '-' && break # not a sequence item + +# # Ensure it's '- ' or '-\n' or just '-' at end +# if length(content) >= 2 && content[2] != ' ' && content[2] != '\t' +# break +# end + +# ps.lineno += 1 # consume the '-' line + +# # The item content can be inline (after '-') or on subsequent lines +# inline = length(content) > 1 ? lstrip(content[2:end]) : "" +# inline_stripped = strip_inline_comment(rstrip(inline)) + +# val = if isempty(inline_stripped) || inline_stripped[1] == '#' +# # Item value is indented below +# next_ind = peek_indent(ps) +# if next_ind > seq_indent +# parse_node(ps, next_ind) +# else +# YAMLNull +# end +# elseif inline_stripped[1] == '|' || inline_stripped[1] == '>' +# style = inline_stripped[1] +# header = length(inline_stripped) > 1 ? inline_stripped[2:end] : "" +# YAMLString(parse_block_scalar(ps, style, header)) +# elseif inline_stripped[1] == '{' +# v, _ = parse_flow_mapping(ps, inline_stripped, 2) +# v +# elseif inline_stripped[1] == '[' +# v, _ = parse_flow_sequence(ps, inline_stripped, 2) +# v +# elseif inline_stripped[1] == '-' && (length(inline_stripped) == 1 || inline_stripped[2] == ' ') +# # Nested sequence inline with the '-' +# # Re-inject into lines at appropriate indent +# parse_block_sequence(ps, seq_indent + 2) +# else +# # check if it's a mapping key +# key, _ = find_mapping_key(inline_stripped) +# if key !== nothing +# # inline mapping: treat the current rest as a new line and parse +# # We push a virtual line back +# new_indent = seq_indent + 2 +# # fake_line = " "^new_indent * inline_stripped +# buf = IOBuffer() +# for i in 1:new_indent +# write(buf, ' ') +# end +# write(buf, inline_stripped) +# fake_line = String(take!(buf)) +# fake_line = fake_line * inline_stripped +# insert!(ps.lines, ps.lineno, fake_line) +# parse_node(ps, new_indent) +# else +# parse_scalar(inline_stripped) +# end +# end + +# push!(items, val) +# end + +# return YAMLArray(items) +# end + +# """ +# Parse a scalar that may span multiple lines (plain multi-line scalar). +# Continuation lines must be more indented than `min_indent`. +# """ +# function parse_line_scalar(ps::ParseState, min_indent::Int)::YAMLValue +# skip_empty!(ps) +# at_end(ps) && return YAMLNull + +# l = current_line(ps) +# indent = line_indent(l) +# indent < min_indent && return YAMLNull + +# content = lstrip(l) + +# # Anchor or alias on a standalone scalar line +# if !isempty(content) && content[1] == '&' +# ps.lineno += 1 +# j = 2 +# n = ncodeunits(content) +# while j <= n && content[j] != ' ' +# j += 1 +# end +# anchor_name = content[2:j-1] +# inner = j <= n ? lstrip(content[j:end]) : "" +# val = isempty(inner) ? parse_node(ps, min_indent) : parse_scalar(strip_inline_comment(rstrip(inner))) +# ps.anchors[anchor_name] = val +# return val +# elseif !isempty(content) && content[1] == '*' +# ps.lineno += 1 +# j = 2 +# n = ncodeunits(content) +# while j <= n && content[j] != ' ' && content[j] != '#' +# j += 1 +# end +# alias_name = content[2:j-1] +# return get(ps.anchors, alias_name, YAMLNull) +# end + +# # Quoted scalar +# if !isempty(content) +# if content[1] == '"' +# ps.lineno += 1 +# str, _ = parse_double_quoted(content, 2) +# return YAMLString(str) +# elseif content[1] == '\'' +# ps.lineno += 1 +# str, _ = parse_single_quoted(content, 2) +# return YAMLString(str) +# elseif content[1] == '|' || content[1] == '>' +# style = content[1] +# header = length(content) > 1 ? content[2:end] : "" +# ps.lineno += 1 # consume the | / > header line +# return YAMLString(parse_block_scalar(ps, style, header)) +# elseif content[1] == '{' +# ps.lineno += 1 +# v, _ = parse_flow_mapping(ps, content, 2) +# return v +# elseif content[1] == '[' +# ps.lineno += 1 +# v, _ = parse_flow_sequence(ps, content, 2) +# return v +# end +# end + +# # Plain multi-line scalar: first line +# ps.lineno += 1 +# parts = [strip_inline_comment(rstrip(content))] + +# # Continuation lines (more indented, not a key, not a sequence item) +# while !at_end(ps) +# skip_empty!(ps) +# at_end(ps) && break +# l2 = current_line(ps) +# ind2 = line_indent(l2) +# ind2 <= min_indent && break +# c2 = lstrip(l2) +# isempty(c2) && break +# c2[1] == '#' && break +# # Don't consume what looks like a new key or sequence +# k2, _ = find_mapping_key(c2) +# k2 !== nothing && break +# (c2[1] == '-' && (length(c2) == 1 || c2[2] == ' ')) && break + +# push!(parts, strip_inline_comment(rstrip(c2))) +# ps.lineno += 1 +# end + +# raw = join(parts, " ") +# return parse_scalar(raw) +# end + +# # ────────────────────────────────────────────────────────────────────────────── +# # Public API +# # ────────────────────────────────────────────────────────────────────────────── + +# """ +# loads(yaml::String) -> YAMLValue + +# Parse a YAML string and return a `YAMLValue` tree. +# """ +# function loads(yaml::String)::YAMLValue +# ps = ParseState(yaml) +# skip_empty!(ps) +# at_end(ps) && return YAMLNull +# return parse_node(ps, 0) +# end + +# """ +# load(path::String) -> YAMLValue + +# Read a YAML file from `path` and return a `YAMLValue` tree. +# """ +# function load(path::String)::YAMLValue +# src = read(path, String) +# return loads(src) +# end + +# struct YAMLInput +# data::Dict{String, Any} +# end + +# function get_value(d::Dict{String, YAMLValue}, key, ::Type{Array}) +# return as_array(d[key]) +# end + +# function get_value(d::Dict{String, YAMLValue}, key, ::Type{<:Dict}) +# return as_dict(d[key]) +# end + +# function get_value(d::Dict{String, YAMLValue}, key, ::Type{Float64}) +# return as_float(d[key]) +# end + +# function get_value(d::Dict{String, YAMLValue}, key, ::Type{Int}) +# return as_int(d[key]) +# end + +# function get_value(d::Dict{String, YAMLValue}, key, ::Type{String}) +# return as_string(d[key]) +# end + +# function to_dict(v::YAMLValue) +# if v.tag === TAG_NULL +# return nothing +# elseif v.tag === TAG_BOOL +# return v.bval +# elseif v.tag === TAG_INT +# return v.ival +# elseif v.tag === TAG_FLOAT +# return v.fval +# elseif v.tag === TAG_STRING +# return v.sval +# elseif v.tag === TAG_ARRAY +# vals = Any[] +# for v in v.arr +# push!(vals, to_dict(v)) +# end +# elseif v.tag === TAG_DICT +# d = Dict{String,Any}() +# for (k,val) in v.dict +# d[k] = to_dict(val) +# end +# return d +# else +# error("Unknown YAML tag") +# end +# end + +# function to_dict(d::Dict{String, YAMLValue}) +# new_d = Dict{String, Any}() +# for (k, v) in pairs(d) +# new_d[k] = to_dict(v) +# end +# return new_d +# end + +# end # module SimpleYAML +module SimpleYAML + +export YAMLValue, YAMLNull, YAMLBool, YAMLInt, YAMLFloat, YAMLString, + YAMLArray, YAMLDict, + load, loads, + as_dict, as_array, as_string, as_int, as_float, as_bool, is_null, + get_value, to_dict + +# ───────────────────────────────────────────────────────────────────────────── +# Value type (concrete tagged union — trim-safe, fully type-stable) +# ───────────────────────────────────────────────────────────────────────────── + +@enum YAMLTag begin + TAG_NULL; TAG_BOOL; TAG_INT; TAG_FLOAT; TAG_STRING; TAG_ARRAY; TAG_DICT +end + +struct YAMLValue + tag :: YAMLTag + bval :: Bool + ival :: Int64 + fval :: Float64 + sval :: String + arr :: Vector{YAMLValue} + dict :: Dict{String, YAMLValue} +end + +const YAMLNull = YAMLValue(TAG_NULL, false, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLBool(b::Bool) = YAMLValue(TAG_BOOL, b, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLInt(i::Int64) = YAMLValue(TAG_INT, false, i, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLFloat(f::Float64) = YAMLValue(TAG_FLOAT, false, 0, f, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLString(s::String) = YAMLValue(TAG_STRING,false, 0, 0.0, s, YAMLValue[], Dict{String,YAMLValue}()) +YAMLArray(a::Vector{YAMLValue}) = YAMLValue(TAG_ARRAY, false, 0, 0.0, "", a, Dict{String,YAMLValue}()) +YAMLDict(d::Dict{String,YAMLValue}) = YAMLValue(TAG_DICT, false, 0, 0.0, "", YAMLValue[], d) + +is_null(v::YAMLValue) = v.tag === TAG_NULL + +function as_bool(v::YAMLValue)::Bool + v.tag === TAG_BOOL || error("YAMLValue is not a bool (tag=$(v.tag))"); v.bval +end +function as_int(v::YAMLValue)::Int64 + v.tag === TAG_INT || error("YAMLValue is not an int (tag=$(v.tag))"); v.ival +end +function as_float(v::YAMLValue)::Float64 + v.tag === TAG_FLOAT || error("YAMLValue is not a float (tag=$(v.tag))"); v.fval +end +function as_string(v::YAMLValue)::String + v.tag === TAG_STRING || error("YAMLValue is not a string (tag=$(v.tag))"); v.sval +end +function as_array(v::YAMLValue)::Vector{YAMLValue} + v.tag === TAG_ARRAY || error("YAMLValue is not an array (tag=$(v.tag))"); v.arr +end +function as_dict(v::YAMLValue)::Dict{String,YAMLValue} + v.tag === TAG_DICT || error("YAMLValue is not a dict (tag=$(v.tag))"); v.dict +end + +function Base.show(io::IO, v::YAMLValue) + if v.tag === TAG_NULL; print(io, "null") + elseif v.tag === TAG_BOOL; print(io, v.bval ? "true" : "false") + elseif v.tag === TAG_INT; print(io, v.ival) + elseif v.tag === TAG_FLOAT; print(io, v.fval) + elseif v.tag === TAG_STRING; print(io, repr(v.sval)) + elseif v.tag === TAG_ARRAY + print(io, "[") + for (i, x) in enumerate(v.arr); i > 1 && print(io, ", "); show(io, x); end + print(io, "]") + else + print(io, "{") + first = true + for (k, val) in v.dict + first || print(io, ", "); first = false + print(io, repr(k), ": "); show(io, val) + end + print(io, "}") + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Parser state +# ───────────────────────────────────────────────────────────────────────────── + +mutable struct ParseState + lines :: Vector{String} + lineno :: Int + anchors :: Dict{String, YAMLValue} +end + +function ParseState(src::String)::ParseState + # split() returns SubStrings — convert immediately so all downstream + # functions only ever handle concrete String. + raw = split(src, '\n') + lines = Vector{String}(undef, length(raw)) + for i in eachindex(raw); lines[i] = String(raw[i]); end + return ParseState(lines, 1, Dict{String,YAMLValue}()) +end + +@inline at_end(ps::ParseState) = ps.lineno > length(ps.lines) +@inline current_line(ps::ParseState) = ps.lineno <= length(ps.lines) ? ps.lines[ps.lineno] : "" + +# ───────────────────────────────────────────────────────────────────────────── +# String helpers — ALL use only codeunit / nextind (never s[byte_index]). +# +# Design rule: the only place character indexing s[i] is allowed is when i +# comes from firstindex(s) or nextind(s, prev) — i.e. a proper character walk. +# Every other access uses codeunit(s, i) for byte inspection or byte-range +# slices s[a:b] where a/b are validated character boundaries. +# ───────────────────────────────────────────────────────────────────────────── + +"""Count leading ASCII space bytes.""" +function line_indent(l::String)::Int + n = ncodeunits(l); i = 1 + while i <= n && codeunit(l, i) == UInt8(' '); i += 1; end + return i - 1 +end + +""" +Return (indent::Int, content::String) for a raw line. +content is the line with leading spaces removed, as a proper String. +""" +function split_indent(l::String)::Tuple{Int,String} + n = ncodeunits(l) + ind = line_indent(l) + # ind is a count of ASCII space bytes, so ind+1 is always a valid char start. + return ind, ind < n ? l[ind+1:end] : "" +end + +"""Find the indent of the next non-blank non-comment line without advancing.""" +function peek_indent(ps::ParseState)::Int + i = ps.lineno + while i <= length(ps.lines) + ind, rest = split_indent(ps.lines[i]) + if !isempty(rest) && codeunit(rest, 1) != UInt8('#') + return ind + end + i += 1 + end + return -1 +end + +function skip_empty!(ps::ParseState) + while !at_end(ps) + _, rest = split_indent(current_line(ps)) + if isempty(rest) || codeunit(rest, 1) == UInt8('#') + ps.lineno += 1 + else + break + end + end +end + +""" +Strip trailing inline comment (' # ...') and trailing whitespace. +Always returns a plain String; uses only byte-level ops. +""" +function strip_inline_comment(s::String)::String + n = ncodeunits(s) + # Need at least 2 bytes: char before '#' + '#' itself + i = 2 + while i <= n + if codeunit(s, i) == UInt8('#') && codeunit(s, i-1) == UInt8(' ') + j = i - 1 # byte just before the space-hash + while j >= 1 && (codeunit(s, j) == UInt8(' ') || codeunit(s, j) == UInt8('\t')) + j -= 1 + end + return j >= 1 ? s[1:j] : "" + end + i += 1 + end + # no comment — rstrip trailing whitespace + j = n + while j >= 1 && (codeunit(s, j) == UInt8(' ') || codeunit(s, j) == UInt8('\t') || + codeunit(s, j) == UInt8('\r') || codeunit(s, j) == UInt8('\n')) + j -= 1 + end + return j >= 1 ? (j == n ? s : s[1:j]) : "" +end + +""" +lstrip ASCII whitespace, returning a plain String. +Only advances through single-byte ASCII space/tab characters, so the +first non-whitespace byte is always a valid character boundary. +""" +function lstrip_ascii(s::String)::String + n = ncodeunits(s); i = 1 + while i <= n && (codeunit(s, i) == UInt8(' ') || codeunit(s, i) == UInt8('\t')) + i += 1 + end + return i <= n ? s[i:end] : "" +end + +""" +Return the suffix of s starting one character after byte-position `byte_pos`. +`byte_pos` must point to the start of a character (i.e. be a valid index). +Uses nextind to step one character, then slices to end. +""" +function suffix_after_char(s::String, byte_pos::Int)::String + n = ncodeunits(s) + ni = nextind(s, byte_pos) + return ni <= n ? s[ni:end] : "" +end + +# ───────────────────────────────────────────────────────────────────────────── +# Scalar coercion +# ───────────────────────────────────────────────────────────────────────────── + +function parse_scalar(s::String)::YAMLValue + isempty(s) && return YAMLNull + (s == "null" || s == "Null" || s == "NULL" || s == "~") && return YAMLNull + (s == "true" || s == "True" || s == "TRUE") && return YAMLBool(true) + (s == "false" || s == "False" || s == "FALSE") && return YAMLBool(false) + (s == ".inf" || s == ".Inf" || s == ".INF" || + s == "+.inf" || s == "+.Inf" || s == "+.INF") && return YAMLFloat(Inf) + (s == "-.inf" || s == "-.Inf" || s == "-.INF") && return YAMLFloat(-Inf) + (s == ".nan" || s == ".NaN" || s == ".NAN") && return YAMLFloat(NaN) + + n = ncodeunits(s) + if n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('x') + v = tryparse(Int64, s[3:end]; base=16); v !== nothing && return YAMLInt(v) + elseif n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('o') + v = tryparse(Int64, s[3:end]; base=8); v !== nothing && return YAMLInt(v) + elseif n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('b') + v = tryparse(Int64, s[3:end]; base=2); v !== nothing && return YAMLInt(v) + else + v = tryparse(Int64, s); v !== nothing && return YAMLInt(v) + end + fv = tryparse(Float64, s); fv !== nothing && return YAMLFloat(fv) + return YAMLString(s) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Quoted-string parsers +# Take a String + byte start index (just after opening quote). +# Return (parsed String, byte index after closing quote). +# Use proper character walks (nextind), so Unicode-safe throughout. +# ───────────────────────────────────────────────────────────────────────────── + +function parse_single_quoted(s::String, start::Int)::Tuple{String,Int} + buf = IOBuffer(); i = start; n = ncodeunits(s) + while i <= n + # Walk one character at a time using nextind + ci = i + i = nextind(s, ci) # advance past current char + ch = s[ci] # safe: ci from nextind chain + if ch == '\'' + if i <= n && codeunit(s, i) == UInt8('\'') + write(buf, '\''); i = nextind(s, i) + else + break # closing quote; i already past it + end + else + write(buf, ch) + end + end + return String(take!(buf)), i +end + +function parse_double_quoted(s::String, start::Int)::Tuple{String,Int} + buf = IOBuffer(); i = start; n = ncodeunits(s) + while i <= n + ci = i; i = nextind(s, ci); ch = s[ci] + if ch == '"' + break + elseif ch == '\\' + i > n && break + ei = i; i = nextind(s, ei); esc = s[ei] + if esc == '0'; write(buf, '\0') + elseif esc == 'a'; write(buf, '\a') + elseif esc == 'b'; write(buf, '\b') + elseif esc == 't' || esc == '\t'; write(buf, '\t') + elseif esc == 'n'; write(buf, '\n') + elseif esc == 'v'; write(buf, '\v') + elseif esc == 'f'; write(buf, '\f') + elseif esc == 'r'; write(buf, '\r') + elseif esc == 'e'; write(buf, '\e') + elseif esc == ' '; write(buf, ' ') + elseif esc == '"'; write(buf, '"') + elseif esc == '/'; write(buf, '/') + elseif esc == '\\'; write(buf, '\\') + elseif esc == 'N'; write(buf, '\u0085') + elseif esc == '_'; write(buf, '\u00a0') + elseif esc == 'L'; write(buf, '\u2028') + elseif esc == 'P'; write(buf, '\u2029') + elseif esc == 'x' + hex = i+1 <= n ? s[i:i+1] : "00" + cp = tryparse(UInt32, hex; base=16) + write(buf, cp !== nothing ? Char(cp) : '?') + i <= n && (i = nextind(s, i)) + i <= n && (i = nextind(s, i)) + elseif esc == 'u' + hex = i+3 <= n ? s[i:i+3] : "0000" + cp = tryparse(UInt32, hex; base=16) + write(buf, cp !== nothing ? Char(cp) : '?') + for _ in 1:4; i <= n && (i = nextind(s, i)); end + elseif esc == 'U' + hex = i+7 <= n ? s[i:i+7] : "00000000" + cp = tryparse(UInt32, hex; base=16) + write(buf, cp !== nothing ? Char(cp) : '?') + for _ in 1:8; i <= n && (i = nextind(s, i)); end + else + write(buf, esc) + end + else + write(buf, ch) + end + end + return String(take!(buf)), i +end + +# ───────────────────────────────────────────────────────────────────────────── +# Block scalar (| and >) +# Caller has already consumed the header line; ps.lineno → first content line. +# ───────────────────────────────────────────────────────────────────────────── + +function parse_block_scalar(ps::ParseState, style::Char, header::String)::String + chomping = :clip; explicit_indent = 0 + h = header + # strip trailing comment + hi = firstindex(h) + while hi <= lastindex(h) + if codeunit(h, hi) == UInt8('#') && hi > 1 && codeunit(h, hi-1) == UInt8(' ') + h = String(rstrip(h[1:hi-2])); break + end + hi = nextind(h, hi) + end + h = String(strip(h)) + # parse chomping / explicit-indent chars (all ASCII, safe with nextind walk) + hi = firstindex(h) + while hi <= lastindex(h) + ch = h[hi] # safe: hi from nextind chain starting at firstindex + if ch == '-'; chomping = :strip; hi = nextind(h, hi) + elseif ch == '+'; chomping = :keep; hi = nextind(h, hi) + elseif isdigit(ch); explicit_indent = Int(ch - '0'); hi = nextind(h, hi) + else; break + end + end + + block_indent = explicit_indent + if block_indent == 0 + j = ps.lineno + while j <= length(ps.lines) + ind, rest = split_indent(ps.lines[j]) + if !isempty(rest); block_indent = ind; break; end + j += 1 + end + end + + lines_out = String[] + while !at_end(ps) + l = current_line(ps) + ind, rest = split_indent(l) + if isempty(rest) + push!(lines_out, ""); ps.lineno += 1; continue + end + ind < block_indent && break + push!(lines_out, l[block_indent+1:end]); ps.lineno += 1 + end + + result::String = if style == '|' + join(lines_out, "\n") + else + buf = IOBuffer(); prev_empty = false + for (idx, ln) in enumerate(lines_out) + if isempty(ln); prev_empty = true + else + if idx > 1 && !prev_empty; write(buf, ' ') + elseif prev_empty; write(buf, '\n') + end + write(buf, ln); prev_empty = false + end + end + String(take!(buf)) + end + + if chomping === :strip; result = String(rstrip(result, '\n')) + elseif chomping === :clip; result = String(rstrip(result, '\n')) * "\n" + end + return result +end + +# ───────────────────────────────────────────────────────────────────────────── +# Flow (inline) parsers +# All use codeunit for structural character checks and nextind for walking. +# ───────────────────────────────────────────────────────────────────────────── + +function parse_flow_value(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} + n = ncodeunits(s) + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')) + i += 1 + end + i > n && return YAMLNull, i + + # Peek at first byte — all YAML flow structural characters are single-byte ASCII. + b = codeunit(s, i) + if b == UInt8('{') + return parse_flow_mapping(ps, s, i+1) + elseif b == UInt8('[') + return parse_flow_sequence(ps, s, i+1) + elseif b == UInt8('\'') + str, i2 = parse_single_quoted(s, i+1) + return YAMLString(str), i2 + elseif b == UInt8('"') + str, i2 = parse_double_quoted(s, i+1) + return YAMLString(str), i2 + elseif b == UInt8('&') + j = i + 1 + while j <= n && codeunit(s,j) != UInt8(' ') && codeunit(s,j) != UInt8(',') && + codeunit(s,j) != UInt8('}') && codeunit(s,j) != UInt8(']') + j += 1 + end + aname = s[i+1:j-1] # anchor names are ASCII by convention + val, i2 = parse_flow_value(ps, s, j) + ps.anchors[aname] = val + return val, i2 + elseif b == UInt8('*') + j = i + 1 + while j <= n && codeunit(s,j) != UInt8(' ') && codeunit(s,j) != UInt8(',') && + codeunit(s,j) != UInt8('}') && codeunit(s,j) != UInt8(']') + j += 1 + end + return get(ps.anchors, s[i+1:j-1], YAMLNull), j + else + j = i + while j <= n + bj = codeunit(s, j) + (bj == UInt8(',') || bj == UInt8('}') || bj == UInt8(']') || bj == UInt8('#')) && break + j += 1 + end + raw = String(strip(s[i:j-1])) + return parse_scalar(raw), j + end +end + +function parse_flow_sequence(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} + items = YAMLValue[]; n = ncodeunits(s) + while i <= n + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t') || + codeunit(s,i) == UInt8('\n')) + i += 1 + end + i > n && break + codeunit(s,i) == UInt8(']') && (i += 1; break) + codeunit(s,i) == UInt8(',') && (i += 1; continue) + val, i = parse_flow_value(ps, s, i) + push!(items, val) + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end + i <= n && codeunit(s,i) == UInt8(',') && (i += 1) + end + return YAMLArray(items), i +end + +function parse_flow_mapping(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} + d = Dict{String,YAMLValue}(); n = ncodeunits(s) + while i <= n + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t') || + codeunit(s,i) == UInt8('\n')) + i += 1 + end + i > n && break + codeunit(s,i) == UInt8('}') && (i += 1; break) + codeunit(s,i) == UInt8(',') && (i += 1; continue) + key_val, i = parse_flow_value(ps, s, i) + key = yaml_value_to_key(key_val) + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end + i <= n && codeunit(s,i) == UInt8(':') && (i += 1) + val, i = parse_flow_value(ps, s, i) + d[key] = val + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end + i <= n && codeunit(s,i) == UInt8(',') && (i += 1) + end + return YAMLDict(d), i +end + +function yaml_value_to_key(v::YAMLValue)::String + v.tag === TAG_STRING && return v.sval + v.tag === TAG_INT && return string(v.ival) + v.tag === TAG_FLOAT && return string(v.fval) + v.tag === TAG_BOOL && return v.bval ? "true" : "false" + return "null" +end + +# ───────────────────────────────────────────────────────────────────────────── +# Key detection — fully Unicode-safe via character walks +# +# Returns (key::String, after_colon_byte_index::Int) or (nothing, 0). +# after_colon_byte_index points one past the ':' byte (i.e. the start of the +# value portion); it is always a valid character boundary because ':' is ASCII +# and the next byte starts a new character. +# ───────────────────────────────────────────────────────────────────────────── + +function find_mapping_key(content::String)::Tuple{Union{Nothing,String},Int} + isempty(content) && return nothing, 0 + n = ncodeunits(content) + b1 = codeunit(content, 1) + + if b1 == UInt8('"') + key, i = parse_double_quoted(content, 2) + # skip spaces (all ASCII, so += 1 safe) + while i <= n && codeunit(content, i) == UInt8(' '); i += 1; end + if i <= n && codeunit(content, i) == UInt8(':') + ni = i + 1 # safe: ':' is 1 byte, next byte starts a new char + if ni > n || codeunit(content, ni) == UInt8(' ') || + codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') + return key, ni + end + end + return nothing, 0 + + elseif b1 == UInt8('\'') + key, i = parse_single_quoted(content, 2) + while i <= n && codeunit(content, i) == UInt8(' '); i += 1; end + if i <= n && codeunit(content, i) == UInt8(':') + ni = i + 1 + if ni > n || codeunit(content, ni) == UInt8(' ') || + codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') + return key, ni + end + end + return nothing, 0 + + else + # Bare key: walk character by character using nextind. + # We are looking for a ':' byte followed by space/tab/EOF/comment. + # ':' is ASCII so codeunit comparison is correct; nextind handles + # multi-byte characters safely. + i = firstindex(content) # always 1, but semantically correct + while i <= lastindex(content) + if codeunit(content, i) == UInt8(':') + ni = i + 1 # ':' is ASCII (1 byte), so ni is a valid index + if ni > n || codeunit(content, ni) == UInt8(' ') || + codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') + # Collect key as content[1 .. prevind(content, i)] + # rstrip ASCII whitespace from the right + ke = i - 1 # byte before ':' + while ke >= 1 && (codeunit(content, ke) == UInt8(' ') || + codeunit(content, ke) == UInt8('\t')) + ke -= 1 + end + ke < 1 && return nothing, 0 + # content[1:ke] is a valid byte-range slice: ke is either + # the last byte of a multi-byte char (fine for range end) + # or a single-byte char. Julia range slices are byte-based + # and only require the START to be a valid char boundary; + # the end can be any byte position >= the last byte of a char. + return content[1:ke], ni + end + end + i = nextind(content, i) + end + return nothing, 0 + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Block mapping +# ───────────────────────────────────────────────────────────────────────────── + +function parse_block_mapping(ps::ParseState, map_indent::Int)::YAMLValue + d = Dict{String,YAMLValue}() + while !at_end(ps) + skip_empty!(ps); at_end(ps) && break + l = current_line(ps) + indent, content = split_indent(l) + indent != map_indent && break + isempty(content) && (ps.lineno += 1; continue) + (startswith(content, "---") || startswith(content, "...")) && break + + key, after_colon = find_mapping_key(content) + key === nothing && break + ps.lineno += 1 # consume key line + + # Value portion on the same line: content[after_colon:end], lstripped. + # after_colon is always a valid character boundary (one past ASCII ':'). + rest = after_colon <= n_cu(content) ? lstrip_ascii(content[after_colon:end]) : "" + rest = strip_inline_comment(rest) + + val::YAMLValue = _parse_value_after_colon(ps, rest, map_indent) + d[key] = val + end + return YAMLDict(d) +end + +# ncodeunits shorthand (avoids repeating the call name) +@inline n_cu(s::String) = ncodeunits(s) + +""" +Determine the value given the stripped inline remainder after a mapping colon +(or after a sequence dash), plus the current block's indent level. +""" +function _parse_value_after_colon(ps::ParseState, rest::String, block_indent::Int)::YAMLValue + if isempty(rest) || codeunit(rest, 1) == UInt8('#') + # value on subsequent line(s) + next_ind = peek_indent(ps) + if next_ind < 0 + return YAMLNull + elseif next_ind > block_indent + return parse_node(ps, next_ind) + elseif next_ind == block_indent + _, nc = split_indent(current_line(ps)) + if !isempty(nc) && codeunit(nc, 1) == UInt8('-') && + (n_cu(nc) == 1 || codeunit(nc, 2) == UInt8(' ') || codeunit(nc, 2) == UInt8('\t')) + return parse_block_sequence(ps, next_ind) + end + end + return YAMLNull + + elseif codeunit(rest, 1) == UInt8('|') || codeunit(rest, 1) == UInt8('>') + style = Char(codeunit(rest, 1)) + hdr = n_cu(rest) > 1 ? suffix_after_char(rest, 1) : "" + ps.lineno += 1 # consume block-scalar header (current line already consumed by caller) + return YAMLString(parse_block_scalar(ps, style, hdr)) + + elseif codeunit(rest, 1) == UInt8('{') + v, _ = parse_flow_mapping(ps, rest, 2); return v + + elseif codeunit(rest, 1) == UInt8('[') + v, _ = parse_flow_sequence(ps, rest, 2); return v + + elseif codeunit(rest, 1) == UInt8('&') + # &anchorname + j = 2 + while j <= n_cu(rest) && codeunit(rest,j) != UInt8(' ') && codeunit(rest,j) != UInt8('\t') + j += 1 + end + aname = rest[2:j-1] # anchor names are plain ASCII + inner = lstrip_ascii(j <= n_cu(rest) ? rest[j:end] : "") + v = if isempty(inner) || codeunit(inner, 1) == UInt8('#') + ni = peek_indent(ps) + ni > block_indent ? parse_node(ps, ni) : YAMLNull + else + parse_scalar(strip_inline_comment(inner)) + end + ps.anchors[aname] = v; return v + + elseif codeunit(rest, 1) == UInt8('*') + j = 2 + while j <= n_cu(rest) && codeunit(rest,j) != UInt8(' ') && codeunit(rest,j) != UInt8('#') + j += 1 + end + return get(ps.anchors, rest[2:j-1], YAMLNull) + + else + return parse_scalar(rest) + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Block sequence +# ───────────────────────────────────────────────────────────────────────────── + +function parse_block_sequence(ps::ParseState, seq_indent::Int)::YAMLValue + items = YAMLValue[] + while !at_end(ps) + skip_empty!(ps); at_end(ps) && break + l = current_line(ps) + indent, content = split_indent(l) + indent != seq_indent && break + isempty(content) && (ps.lineno += 1; continue) + codeunit(content, 1) != UInt8('-') && break + if n_cu(content) >= 2 + b2 = codeunit(content, 2) + b2 != UInt8(' ') && b2 != UInt8('\t') && break + end + + ps.lineno += 1 # consume '-' line + + # Content after '- ' (suffix_after_char handles multi-byte-safe skip of '-') + inline = lstrip_ascii(n_cu(content) > 1 ? suffix_after_char(content, 1) : "") + inline = strip_inline_comment(inline) + + val::YAMLValue = if isempty(inline) || codeunit(inline, 1) == UInt8('#') + next_ind = peek_indent(ps) + next_ind > seq_indent ? parse_node(ps, next_ind) : YAMLNull + + elseif codeunit(inline, 1) == UInt8('-') && + (n_cu(inline) == 1 || codeunit(inline, 2) == UInt8(' ')) + fake = " "^(seq_indent + 2) * inline + insert!(ps.lines, ps.lineno, fake) + parse_block_sequence(ps, seq_indent + 2) + + else + # Check for inline mapping key first, then delegate to _parse_value_after_colon + k, _ = find_mapping_key(inline) + if k !== nothing + new_ind = seq_indent + 2 + fake_line = " "^new_ind * inline + insert!(ps.lines, ps.lineno, fake_line) + parse_node(ps, new_ind) + else + _parse_value_after_colon(ps, inline, seq_indent) + end + end + + push!(items, val) + end + return YAMLArray(items) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Node dispatcher +# ───────────────────────────────────────────────────────────────────────────── + +function parse_node(ps::ParseState, min_indent::Int)::YAMLValue + skip_empty!(ps); at_end(ps) && return YAMLNull + l = current_line(ps) + indent, content = split_indent(l) + indent < min_indent && return YAMLNull + isempty(content) && return YAMLNull + + if startswith(content, "---") || startswith(content, "...") + nc = n_cu(content) + ok = nc == 3 || codeunit(content,4) == UInt8(' ') || + codeunit(content,4) == UInt8('\t') || codeunit(content,4) == UInt8('#') + if ok; ps.lineno += 1; return parse_node(ps, min_indent); end + end + + b1 = codeunit(content, 1) + if b1 == UInt8('-') && (n_cu(content) == 1 || codeunit(content,2) == UInt8(' ') || + codeunit(content,2) == UInt8('\t')) + return parse_block_sequence(ps, indent) + end + + key, _ = find_mapping_key(content) + key !== nothing && return parse_block_mapping(ps, indent) + + return parse_line_scalar(ps, min_indent) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Scalar line parser +# ───────────────────────────────────────────────────────────────────────────── + +function parse_line_scalar(ps::ParseState, min_indent::Int)::YAMLValue + skip_empty!(ps); at_end(ps) && return YAMLNull + l = current_line(ps) + indent, content = split_indent(l) + indent < min_indent && return YAMLNull + isempty(content) && return YAMLNull + + b1 = codeunit(content, 1) + + if b1 == UInt8('&') + ps.lineno += 1 + j = 2; nn = n_cu(content) + while j <= nn && codeunit(content,j) != UInt8(' ') && codeunit(content,j) != UInt8('\t') + j += 1 + end + aname = content[2:j-1] + inner = lstrip_ascii(j <= nn ? content[j:end] : "") + val = isempty(inner) || codeunit(inner,1) == UInt8('#') ? + parse_node(ps, min_indent) : + parse_scalar(strip_inline_comment(inner)) + ps.anchors[aname] = val; return val + end + + if b1 == UInt8('*') + ps.lineno += 1; j = 2; nn = n_cu(content) + while j <= nn && codeunit(content,j) != UInt8(' ') && codeunit(content,j) != UInt8('#') + j += 1 + end + return get(ps.anchors, content[2:j-1], YAMLNull) + end + + if b1 == UInt8('"') + ps.lineno += 1; str, _ = parse_double_quoted(content, 2); return YAMLString(str) + end + if b1 == UInt8('\'') + ps.lineno += 1; str, _ = parse_single_quoted(content, 2); return YAMLString(str) + end + if b1 == UInt8('|') || b1 == UInt8('>') + style = Char(b1) + hdr = n_cu(content) > 1 ? suffix_after_char(content, 1) : "" + ps.lineno += 1 + return YAMLString(parse_block_scalar(ps, style, hdr)) + end + if b1 == UInt8('{') + ps.lineno += 1; v, _ = parse_flow_mapping(ps, content, 2); return v + end + if b1 == UInt8('[') + ps.lineno += 1; v, _ = parse_flow_sequence(ps, content, 2); return v + end + + # Plain (possibly multi-line) scalar + ps.lineno += 1 + parts = String[strip_inline_comment(content)] + while !at_end(ps) + skip_empty!(ps); at_end(ps) && break + l2 = current_line(ps) + ind2, c2 = split_indent(l2) + ind2 <= min_indent && break + isempty(c2) && break + codeunit(c2,1) == UInt8('#') && break + k2, _ = find_mapping_key(c2); k2 !== nothing && break + if codeunit(c2,1) == UInt8('-') && (n_cu(c2) == 1 || codeunit(c2,2) == UInt8(' ')) + break + end + push!(parts, strip_inline_comment(c2)); ps.lineno += 1 + end + return parse_scalar(join(parts, " ")) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Public API +# ───────────────────────────────────────────────────────────────────────────── + +function loads(yaml::String)::YAMLValue + ps = ParseState(yaml); skip_empty!(ps) + at_end(ps) && return YAMLNull + return parse_node(ps, 0) +end + +function load(path::String)::YAMLValue + return loads(read(path, String)) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Convenience helpers +# ───────────────────────────────────────────────────────────────────────────── + +function get_value(d::Dict{String,YAMLValue}, key, ::Type{Array}); return as_array(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{<:Dict}); return as_dict(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{Float64}); return as_float(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{Int}); return as_int(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{String}); return as_string(d[key]);end + +function to_dict(v::YAMLValue) + if v.tag === TAG_NULL; return nothing + elseif v.tag === TAG_BOOL; return v.bval + elseif v.tag === TAG_INT; return v.ival + elseif v.tag === TAG_FLOAT; return v.fval + elseif v.tag === TAG_STRING; return v.sval + elseif v.tag === TAG_ARRAY + vals = Any[] + for item in v.arr; push!(vals, to_dict(item)); end + return vals + else + d = Dict{String,Any}() + for (k, val) in v.dict; d[k] = to_dict(val); end + return d + end +end + +function to_dict(d::Dict{String, YAMLValue}) + out = Dict{String, Any}() + for (k, v) in d; out[k] = to_dict(v); end + return out +end + +end # module SimpleYAML \ No newline at end of file diff --git a/examples/simple_yaml/src/main.jl b/examples/simple_yaml/src/main.jl new file mode 100644 index 00000000..dc28abea --- /dev/null +++ b/examples/simple_yaml/src/main.jl @@ -0,0 +1,68 @@ +# using SimpleYAML + +include("SimpleYAML.jl") + + +function print_dict(io::IO, d::Dict{String, T}) where T + isempty(d) && return + + # Compute max key length (type-stable) + maxlen = 0 + for k in Base.keys(d) + len = ncodeunits(k) # safer for strings than length in some contexts + if len > maxlen + maxlen = len + end + end + + # Optional: deterministic order without Pair allocations + ks = collect(Base.keys(d)) # Vector{String}, concrete + sort!(ks) + + for k in ks + v = d[k] + + print(io, '"') + print(io, k) + print(io, '"') + + # manual padding (avoids rpad allocation) + pad = maxlen - ncodeunits(k) + for _ in 1:pad + print(io, ' ') + end + + print(io, " => ") + + print(io, '"') + print(io, v) + println(io, '"') + end +end + +function app_main(ARGS) + input_file = ARGS[1] + println(Core.stdout, input_file) + + io = open(input_file, "r") + str = read(io, String) + close(io) + + println(Core.stdout, str) + d = SimpleYAML.loads(str) + d = SimpleYAML.as_dict(d) + + solver = SimpleYAML.get_value(d, "solver", Dict{String, Dict}) + print_dict(Core.stdout, d) + print_dict(Core.stdout, solver) + + d = SimpleYAML.to_dict(d) + # print_dict(Core.stdout, d) + + return d +end + +function @main(ARGS::Vector{String}) + app_main(ARGS) + return 0 +end \ No newline at end of file diff --git a/examples/simple_yaml/test/example_file.yaml b/examples/simple_yaml/test/example_file.yaml new file mode 100644 index 00000000..6d5d3db4 --- /dev/null +++ b/examples/simple_yaml/test/example_file.yaml @@ -0,0 +1,41 @@ +problem: + name: "Cantilever Beam" + type: static_structural + dimensions: 3 + +mesh: + file: mesh/beam.msh + order: 2 + refinement_level: 0 + +material: + name: Steel + density: 7850.0 # kg/m^3 + youngs modulus: 2.1e11 # Pa + poissons ratio: 0.3 + +boundary conditions: + - name: fixed_end + type: dirichlet + dof: [u, v, w] + value: 0.0 + nodesets: [1] + - name: tip_load + type: neumann + component: w + value: -5000.0 + facesets: [2] + +solver: + type: direct + library: MUMPS + tolerance: 1.0e-10 + max_iterations: 100 + +output: + format: vtk + fields: + - displacement + - stress + - strain + frequency: 1 diff --git a/examples/simple_yaml/test/test_files/bar.yaml b/examples/simple_yaml/test/test_files/bar.yaml new file mode 100644 index 00000000..80dd6c42 --- /dev/null +++ b/examples/simple_yaml/test/test_files/bar.yaml @@ -0,0 +1,40 @@ +type: single +input mesh file: bar-1.g +output mesh file: bar-1.e +model: + type: solid mechanics + material: + blocks: + block1: elastic + elastic: + model: linear elastic + elastic modulus: 1.0e+09 + Poisson's ratio: 0.25 + density: 1000.0 +time integrator: + type: Newmark + beta: 0.25 + gamma: 0.5 +initial conditions: + displacement: + - node set: nsall + component: x + function: "-1.0e-04" + velocity: + - node set: nsall + component: x + function: "100.0" +boundary conditions: + Schwarz contact: + - side set: ssx+ + source: bar-2 + source block: block2 + source side set: ssx- + friction type: frictionless +solver: + type: Hessian minimizer + step: full Newton + minimum iterations: 1 + maximum iterations: 16 + relative tolerance: 1.0e-10 + absolute tolerance: 1.0e-06 diff --git a/examples/simple_yaml/test/test_yaml.jl b/examples/simple_yaml/test/test_yaml.jl new file mode 100644 index 00000000..6ab5baf5 --- /dev/null +++ b/examples/simple_yaml/test/test_yaml.jl @@ -0,0 +1,369 @@ +include("SimpleYAML.jl") +using .SimpleYAML +using Test + +# ── helpers ──────────────────────────────────────────────────────────────────── +pass = 0 +fail = 0 + +# macro test(expr) +# quote +# try +# result = $(esc(expr)) +# if result === true +# global pass += 1 +# else +# global fail += 1 +# println("FAIL: ", $(string(expr)), " => ", result) +# end +# catch e +# global fail += 1 +# println("ERROR: ", $(string(expr)), " => ", e) +# end +# end +# end + +# ── scalars ─────────────────────────────────────────────────────────────────── +println("=== Scalars ===") +@test is_null(SimpleYAML.loads("null")) +@test is_null(SimpleYAML.loads("~")) +@test is_null(SimpleYAML.loads("Null")) +@test as_bool(SimpleYAML.loads("true")) == true +@test as_bool(SimpleYAML.loads("false")) == false +@test as_bool(SimpleYAML.loads("True")) == true +@test as_bool(SimpleYAML.loads("FALSE")) == false +@test as_int(SimpleYAML.loads("42")) == 42 +@test as_int(SimpleYAML.loads("-7")) == -7 +@test as_int(SimpleYAML.loads("0")) == 0 +@test as_int(SimpleYAML.loads("0xff")) == 255 +@test as_int(SimpleYAML.loads("0o17")) == 15 +@test as_int(SimpleYAML.loads("0b1010")) == 10 +@test as_float(SimpleYAML.loads("3.14")) ≈ 3.14 +@test as_float(SimpleYAML.loads("-1.5e2")) ≈ -150.0 +@test isinf(as_float(SimpleYAML.loads(".inf"))) +@test isinf(as_float(SimpleYAML.loads("-.inf"))) +@test isnan(as_float(SimpleYAML.loads(".nan"))) +@test as_string(SimpleYAML.loads("hello")) == "hello" +@test as_string(SimpleYAML.loads("hello world")) == "hello world" + +# ── quoted strings ───────────────────────────────────────────────────────────── +println("=== Quoted strings ===") +@test as_string(SimpleYAML.loads("\"hello\"")) == "hello" +@test as_string(SimpleYAML.loads("'hello'")) == "hello" +@test as_string(SimpleYAML.loads("\"tab\\there\"")) == "tab\there" +@test as_string(SimpleYAML.loads("\"new\\nline\"")) == "new\nline" +@test as_string(SimpleYAML.loads("\"quote\\\"here\"")) == "quote\"here" +@test as_string(SimpleYAML.loads("'it''s fine'")) == "it's fine" +@test as_string(SimpleYAML.loads("\"unicode \\u0041\"")) == "unicode A" +@test as_string(SimpleYAML.loads("\"null\"")) == "null" # quoted null is a string +@test as_string(SimpleYAML.loads("\"true\"")) == "true" # quoted bool is a string +@test as_string(SimpleYAML.loads("\"42\"")) == "42" # quoted int is a string + +# ── simple mapping ───────────────────────────────────────────────────────────── +println("=== Simple mapping ===") +v = SimpleYAML.loads(""" +name: Alice +age: 30 +active: true +score: 9.5 +""") +d = SimpleYAML.as_dict(v) +@test as_string(d["name"]) == "Alice" +@test as_int(d["age"]) == 30 +@test as_bool(d["active"]) == true +@test as_float(d["score"]) ≈ 9.5 + +# ── nested mapping ───────────────────────────────────────────────────────────── +println("=== Nested mapping ===") +v = SimpleYAML.loads(""" +server: + host: localhost + port: 8080 + tls: false +""") +d = SimpleYAML.as_dict(SimpleYAML.as_dict(v)["server"]) +@test as_string(d["host"]) == "localhost" +@test as_int(d["port"]) == 8080 +@test as_bool(d["tls"]) == false + +# ── block sequence ───────────────────────────────────────────────────────────── +println("=== Block sequence ===") +v = SimpleYAML.loads(""" +- 1 +- 2 +- 3 +""") +a = SimpleYAML.as_array(v) +@test length(a) == 3 +@test as_int(a[1]) == 1 +@test as_int(a[2]) == 2 +@test as_int(a[3]) == 3 + +# ── sequence of mappings ─────────────────────────────────────────────────────── +println("=== Sequence of mappings ===") +v = SimpleYAML.loads(""" +- name: Bob + age: 25 +- name: Carol + age: 28 +""") +a = SimpleYAML.as_array(v) +@test length(a) == 2 +@test as_string(as_dict(a[1])["name"]) == "Bob" +@test as_int(as_dict(a[2])["age"]) == 28 + +# ── mapping with sequence value ──────────────────────────────────────────────── +println("=== Mapping with sequence value ===") +v = SimpleYAML.loads(""" +colors: + - red + - green + - blue +""") +arr = SimpleYAML.as_array(SimpleYAML.as_dict(v)["colors"]) +@test length(arr) == 3 +@test as_string(arr[1]) == "red" +@test as_string(arr[3]) == "blue" + +# ── flow sequences ───────────────────────────────────────────────────────────── +println("=== Flow sequences ===") +v = SimpleYAML.loads("[1, 2, 3]") +a = as_array(v) +@test length(a) == 3 +@test as_int(a[2]) == 2 + +v = SimpleYAML.loads("[\"a\", 'b', c]") +a = as_array(v) +@test as_string(a[1]) == "a" +@test as_string(a[2]) == "b" +@test as_string(a[3]) == "c" + +# ── flow mappings ────────────────────────────────────────────────────────────── +# println("=== Flow mappings ===") +# v = SimpleYAML.loads("{x: 1, y: 2}") +# d = as_dict(v) +# @show d +# @test as_int(d["x"]) == 1 +# @test as_int(d["y"]) == 2 + +# ── inline flow in block context ─────────────────────────────────────────────── +# println("=== Inline flow in block ===") +# v = SimpleYAML.loads(""" +# point: {x: 10, y: 20} +# tags: [a, b, c] +# """) +# d = as_dict(v) +# @test as_int(as_dict(d["point"])["x"]) == 10 +# @test as_string(as_array(d["tags"])[2]) == "b" + +# ── comments ────────────────────────────────────────────────────────────────── +println("=== Comments ===") +v = SimpleYAML.loads(""" +# top comment +name: Dave # inline comment +# mid comment +age: 40 +""") +d = as_dict(v) +@test as_string(d["name"]) == "Dave" +@test as_int(d["age"]) == 40 + +# ── literal block scalar (|) ────────────────────────────────────────────────── +println("=== Literal block scalar ===") +v = SimpleYAML.loads(""" +text: | + Hello + World +""") +s = as_string(as_dict(v)["text"]) +@test s == "Hello\nWorld\n" + +v = SimpleYAML.loads(""" +text: |- + Hello + World +""") +@test as_string(as_dict(v)["text"]) == "Hello\nWorld" + +# ── folded block scalar (>) ─────────────────────────────────────────────────── +println("=== Folded block scalar ===") +v = SimpleYAML.loads(""" +text: > + Hello + World +""") +s = as_string(as_dict(v)["text"]) +@test s == "Hello World\n" + +# ── anchors and aliases ──────────────────────────────────────────────────────── +println("=== Anchors and aliases ===") +v = SimpleYAML.loads(""" +defaults: &defs + timeout: 30 + retries: 3 + +production: + <<: *defs + host: prod.example.com +""") +# Just check anchors were stored (merge key '<<' is not auto-applied, but alias is resolved) +d = as_dict(v) +@test as_int(as_dict(d["defaults"])["timeout"]) == 30 + +# Simple alias usage +v = SimpleYAML.loads(""" +base: &b 42 +copy: *b +""") +d = as_dict(v) +@test as_int(d["base"]) == 42 +@test as_int(d["copy"]) == 42 + +# ── null value ──────────────────────────────────────────────────────────────── +println("=== Null value ===") +v = SimpleYAML.loads(""" +key1: null +key2: ~ +key3: +""") +d = as_dict(v) +@test is_null(d["key1"]) +@test is_null(d["key2"]) +@test is_null(d["key3"]) + +# ── deeply nested ───────────────────────────────────────────────────────────── +println("=== Deeply nested ===") +v = SimpleYAML.loads(""" +simulation: + mesh: + elements: 1024 + order: 2 + material: + density: 7800.0 + moduli: + - 210e9 + - 0.3 + boundary_conditions: + - type: fixed + nodes: [1, 2, 3] + - type: load + value: -1000.0 +""") +sim = as_dict(as_dict(v)["simulation"]) +@test as_int(as_dict(sim["mesh"])["elements"]) == 1024 +@test as_float(as_dict(sim["material"])["density"]) ≈ 7800.0 +moduli = as_array(as_dict(sim["material"])["moduli"]) +@test as_float(moduli[1]) ≈ 210e9 +bcs = as_array(sim["boundary_conditions"]) +@test length(bcs) == 2 +@test as_string(as_dict(bcs[1])["type"]) == "fixed" +@test as_float(as_dict(bcs[2])["value"]) ≈ -1000.0 + +# ── quoted keys ─────────────────────────────────────────────────────────────── +println("=== Quoted keys ===") +v = SimpleYAML.loads(""" +"key with spaces": 1 +'another key': 2 +""") +d = as_dict(v) +@test as_int(d["key with spaces"]) == 1 +@test as_int(d["another key"]) == 2 + +# ── document separator ──────────────────────────────────────────────────────── +println("=== Document separator ===") +v = SimpleYAML.loads(""" +--- +name: test +age: 1 +""") +d = as_dict(v) +@test as_string(d["name"]) == "test" + +# ── FEM-style input file ─────────────────────────────────────────────────────── +println("=== FEM-style input ===") +fem_yaml = """ +# Finite Element Simulation Input +--- +problem: + name: "Cantilever Beam" + type: static_structural + dimensions: 3 + +mesh: + file: mesh/beam.msh + order: 2 + refinement_level: 0 + +material: + name: Steel + density: 7850.0 # kg/m^3 + youngs_modulus: 2.1e11 # Pa + poissons_ratio: 0.3 + +boundary_conditions: + - name: fixed_end + type: dirichlet + dof: [u, v, w] + value: 0.0 + nodesets: [1] + + - name: tip_load + type: neumann + component: w + value: -5000.0 + facesets: [2] + +solver: + type: direct + library: MUMPS + tolerance: 1.0e-10 + max_iterations: 100 + +output: + format: vtk + fields: + - displacement + - stress + - strain + frequency: 1 +""" + +v = SimpleYAML.loads(fem_yaml) +d = as_dict(v) +display(d) +prob = as_dict(d["problem"]) +@test as_string(prob["name"]) == "Cantilever Beam" +@test as_string(prob["type"]) == "static_structural" +@test as_int(prob["dimensions"]) == 3 + +mat = as_dict(d["material"]) +@test as_string(mat["name"]) == "Steel" +@test as_float(mat["density"]) ≈ 7850.0 +@test as_float(mat["youngs_modulus"]) ≈ 2.1e11 +@test as_float(mat["poissons_ratio"]) ≈ 0.3 + +bcs = as_array(d["boundary_conditions"]) +@test length(bcs) == 2 +bc1 = as_dict(bcs[1]) +@test as_string(bc1["name"]) == "fixed_end" +@test as_string(bc1["type"]) == "dirichlet" +dof = as_array(bc1["dof"]) +@test as_string(dof[1]) == "u" +@test as_string(dof[3]) == "w" + +bc2 = as_dict(bcs[2]) +@test as_float(bc2["value"]) ≈ -5000.0 + +solver = as_dict(d["solver"]) +@test as_string(solver["type"]) == "direct" +@test as_float(solver["tolerance"]) ≈ 1.0e-10 + +output = as_dict(d["output"]) +fields = as_array(output["fields"]) +@test length(fields) == 3 +@test as_string(fields[2]) == "stress" + +# ── summary ─────────────────────────────────────────────────────────────────── +println() +println("Results: $pass passed, $fail failed out of $(pass+fail) tests") +fail > 0 && exit(1) diff --git a/src/AppTools.jl b/src/AppTools.jl index ef6e35b2..7d726cf5 100644 --- a/src/AppTools.jl +++ b/src/AppTools.jl @@ -10,6 +10,7 @@ import ..FileMesh import ..FunctionSpace import ..H1Field import ..InitialCondition +import ..InputFileParser import ..NeumannBC import ..PeriodicBC import ..RobinBC @@ -21,6 +22,7 @@ import ..nodal_coordinates_and_ids import ..nodesets import ..sidesets using Exodus +using ..InputFileParser using ReferenceFiniteElements using TOML @@ -286,37 +288,38 @@ struct FunctionSettings{N, T <: Number} scalar_expr_funcs::Dict{String, ScalarExpressionFunction{T}} vector_expr_funcs::Dict{String, VectorExpressionFunction{N, T}} - function FunctionSettings{N, T}(log_file, data) where {N, T <: Number} + function FunctionSettings{N, T}(log_file, parser) where {N, T <: Number} print_banner(log_file, "Functions") scalar_functions = Dict{String, ScalarExpressionFunction{T}}() vector_functions = Dict{String, VectorExpressionFunction{N, T}}() - func_settings = data["functions"]::Dict{String, Any} - for (k, v) in pairs(func_settings) - name = k::String - temp = v::Dict{String, Any} - type = temp["type"]::String - # TODO constant and anaytic are really the same - # should we fuse these into "expression" or something like that? - if type == "scalar expression" - expr = temp["expression"]::String - vars = temp["variables"]::Vector{String} - println(log_file.io, "Parsing analytic function with expression = $expr") - scalar_functions[name] = ScalarExpressionFunction{T}(expr, vars) - elseif type == "vector expression" - exprs = temp["expressions"]::Vector{String} - vars = temp["variables"]::Vector{String} - println(log_file.io, "Parsing expression function expressions") - for expr in exprs - println(log_file.io, expr) + if haskey(parser, "functions") + func_settings = parser["functions"]::Dict{String, Any} + for (k, v) in pairs(func_settings) + name = k::String + temp = v::Dict{String, Any} + type = temp["type"]::String + vars = InputFileParser.get_string_array(temp, "variables", parser.input_style) + + # TODO constant and anaytic are really the same + # should we fuse these into "expression" or something like that? + if type == "scalar expression" + expr = temp["expression"]::String + expr = String(strip(expr, '"')) # for yaml + println(log_file.io, "Parsing analytic function with expression = $expr") + scalar_functions[name] = ScalarExpressionFunction{T}(expr, vars) + elseif type == "vector expression" + exprs = InputFileParser.get_string_array(temp, "expressions", parser.input_style) + for (n, expr) in enumerate(exprs) + exprs[n] = String(strip(expr, '"')) + end + println(log_file.io, "Parsing expression function expressions") + for expr in exprs + println(log_file.io, expr) + end + vector_functions[name] = VectorExpressionFunction{N, T}(exprs, vars) + else + @assert false "Unsupported function type $type" end - vector_functions[name] = VectorExpressionFunction{N, T}(exprs, vars) - # elseif type == "constant" - # expr = temp["expression"]::String - # vars = temp["variables"]::Vector{String} - # println(log_file.io, "Parsing constant function with expression = $expr") - # functions[name] = ScalarExpressionFunction{T}(expr, vars) - else - @assert false "Unsupported function type $type" end end return new{N, T}(scalar_functions, vector_functions) @@ -330,10 +333,10 @@ struct BCSettings{N, T <: Number} robin::Vector{RobinBC{VectorExpressionFunction{N, T}}} source::Vector{Source{VectorExpressionFunction{N, T}}} - function BCSettings{N, T}(log_file, data, functions::FunctionSettings{N, T}) where {N, T <: Number} + function BCSettings{N, T}(log_file, parser, functions::FunctionSettings{N, T}) where {N, T <: Number} print_banner(log_file, "Boundary conditions") - if haskey(data, "boundary_conditions") - bc_settings = data["boundary_conditions"]::Dict{String, Any} + if haskey(parser, "boundary conditions") + bc_settings = InputFileParser.get_nested_block(parser, "boundary conditions") else bc_settings = Dict{String, Any}() end @@ -350,23 +353,23 @@ struct BCSettings{N, T <: Number} temp = bc::Dict{String, Any} func = temp["function"]::String func = functions.scalar_expr_funcs[func] - vars = temp["variables"]::Vector{String} + vars = InputFileParser.get_string_array(temp, "variables", parser.input_style) if haskey(temp, "blocks") - blocks = temp["blocks"]::Vector{String} + blocks = InputFileParser.get_string_array(temp, "blocks", parser.input_style) for block in blocks for var in vars push!(dbcs, DirichletBC(var, func; block_name = block)) end end - elseif haskey(temp, "node_sets") - node_sets = temp["node_sets"]::Vector{String} + elseif haskey(temp, "node sets") + node_sets = InputFileParser.get_string_array(temp, "node sets", parser.input_style) for node_set in node_sets for var in vars push!(dbcs, DirichletBC(var, func; nodeset_name = node_set)) end end - elseif haskey(temp, "side_sets") - side_sets = temp["side_sets"]::Vector{String} + elseif haskey(temp, "side sets") + side_sets = InputFileParser.get_string_array(temp, "side sets", parser.input_style) for side_set in side_sets for var in vars push!(dbcs, DirichletBC(var, func; sideset_name = side_set)) @@ -384,7 +387,7 @@ struct BCSettings{N, T <: Number} temp = bc::Dict{String, Any} func = temp["function"]::String func = functions.vector_expr_funcs[func] - sidesets = temp["side_sets"]::Vector{String} + sidesets = temp["side sets"]::Vector{String} vars = temp["variables"]::Vector{String} for side_set in sidesets for var in vars @@ -395,6 +398,7 @@ struct BCSettings{N, T <: Number} end if haskey(bc_settings, "periodic") + # TODO @assert false end @@ -404,8 +408,8 @@ struct BCSettings{N, T <: Number} temp = bc::Dict{String, Any} func = temp["function"]::String func = functions.vector_expr_funcs[func] - sidesets = temp["side_sets"]::Vector{String} - vars = temp["variables"]::Vector{String} + sidesets = InputFileParser.get_string_array(temp, "side sets", parser.input_style) + vars = InputFileParser.get_string_array(temp, "variables", parser.input_style) for side_set in sidesets for var in vars push!(rbcs, RobinBC(var, func, side_set)) @@ -418,11 +422,11 @@ struct BCSettings{N, T <: Number} src_settings = bc_settings["source"]::Vector{Any} for src in src_settings temp = src::Dict{String, Any} - blocks = temp["blocks"]::Vector{String} + blocks = InputFileParser.get_string_array(temp, "blocks", parser.input_style) func = temp["function"]::String func = functions.vector_expr_funcs[func] - sidesets = temp["blocks"]::Vector{String} - vars = temp["variables"]::Vector{String} + sidesets = InputFileParser.get_string_array(temp, "blocks", parser.input_style) + vars = InputFileParser.get_string_array(temp, "variables", parser.input_style) for block in blocks for var in vars push!(srcs, Source(var, func, block)) @@ -445,32 +449,32 @@ end struct ICSettings{T <: Number} ics::Vector{InitialCondition{ScalarExpressionFunction{T}}} - function ICSettings{T}(log_file, data, functions::FunctionSettings{N, T}) where {N, T} + function ICSettings{T}(log_file, parser, functions::FunctionSettings{N, T}) where {N, T} print_banner(log_file, "Initial conditions") ics = InitialCondition{ScalarExpressionFunction{Float64}}[] - if haskey(data, "initial_conditions") - ic_settings = data["initial_conditions"]::Vector{Any} + if haskey(parser, "initial conditions") + ic_settings = parser["initial conditions"]::Vector{Any} for ic in ic_settings temp = ic::Dict{String, Any} func = temp["function"]::String func = functions.scalar_expr_funcs[func] - vars = temp["variables"]::Vector{String} + vars = InputFileParser.get_string_array(temp, "variables", parser.input_style) if haskey(temp, "blocks") - blocks = temp["blocks"]::Vector{String} + blocks = InputFileParser.get_string_array(temp, "blocks", parser.input_style) for block in blocks for var in vars push!(ics, InitialCondition(var, func; block_name = block)) end end - elseif haskey(temp, "nodesets") - nodesets = temp["nodesets"]::Vector{String} + elseif haskey(temp, "node sets") + nodesets = InputFileParser.get_string_array(temp, "node sets", parser.input_file) for nodeset in nodesets for var in vars push!(ics, InitialCondition(var, func; nodeset_name = nodeset)) end end - elseif haskey(temp, "sidesets") - sidesets = temp["sidesets"]::Vector{String} + elseif haskey(temp, "side sets") + sidesets = InputFileParser.get_string_array(temp, "side sets", parser.input_style) for sideset in sidesets for var in vars push!(ics, InitialCondition(var, func; sideset_name = sideset)) @@ -486,14 +490,16 @@ struct ICSettings{T <: Number} end struct MeshSettings + dimension::Int file_path::String file_type::String function MeshSettings(log_file, data) mesh_settings = data["mesh"]::Dict{String, Any} - file_path = mesh_settings["file_path"]::String - file_type = lowercase(mesh_settings["file_type"]::String) - new(file_path, file_type) + dimension = mesh_settings["dimension"]::Int + file_path = mesh_settings["file path"]::String + file_type = lowercase(mesh_settings["file type"]::String) + new(dimension, file_path, file_type) end end @@ -508,7 +514,7 @@ struct InputSettings{N, T <: Number} functions::FunctionSettings{N, T} ics::ICSettings{T} mesh::MeshSettings - raw_input::Dict{String, Any} + parser::InputFileParser.Parser end function InputSettings{N}(cli_args::CLIArgParser, log_file::LogFile, ::Type{T} = Float64) where {N, T <: Number} @@ -521,13 +527,13 @@ function InputSettings{N}(cli_args::CLIArgParser, log_file::LogFile, ::Type{T} = println(log_file.io, line) end close(io) - data = TOML.parsefile(input_file) + parser = InputFileParser.Parser(input_file) - functions = FunctionSettings{N, T}(log_file, data) - bcs = BCSettings{N, T}(log_file, data, functions) - ics = ICSettings{T}(log_file, data, functions) - mesh = MeshSettings(log_file, data) - return InputSettings{N, T}(bcs, functions, ics, mesh, data) + functions = FunctionSettings{N, T}(log_file, parser) + bcs = BCSettings{N, T}(log_file, parser, functions) + ics = ICSettings{T}(log_file, parser, functions) + mesh = MeshSettings(log_file, parser) + return InputSettings{N, T}(bcs, functions, ics, mesh, parser) end # function _parse_function_space_settings(log_file, data) @@ -559,7 +565,8 @@ end ####################################################### # MeshIO strongly typed helpers ####################################################### -function read_exodus_mesh(mesh_settings::MeshSettings) +# TODO this needs to have dimension as compile time constant... +function read_exodus_mesh(mesh_settings::MeshSettings, ::Val{D}) where D mesh_path = joinpath(pwd(), mesh_settings.file_path) exo = ExodusDatabase{Int32, Int32, Int32, Float64}(mesh_path, "r") fm = FileMesh{ @@ -567,7 +574,9 @@ function read_exodus_mesh(mesh_settings::MeshSettings) ExodusMesh }(mesh_path, exo) # read nodes - coords_type = H1Field{Float64, Vector{Float64}, 2} + # if mesh_settings.dimension == 1 + coords_type = H1Field{Float64, Vector{Float64}, D} + nodal_coords, n_id_map = nodal_coordinates_and_ids(coords_type, fm) # read element block types, conn, etc. el_id_map = element_ids(fm) @@ -584,7 +593,7 @@ function read_exodus_mesh(mesh_settings::MeshSettings) ExodusDatabase{Int32, Int32, Int32, Float64}, ExodusMesh }, - 2, Float64, Int, Nothing, Nothing + D, Float64, Int, Nothing, Nothing }( fm, nodal_coords, @@ -599,9 +608,10 @@ function read_exodus_mesh(mesh_settings::MeshSettings) return mesh end -function _setup_mesh(log_file::LogFile, settings::InputSettings) +function _setup_mesh(log_file::LogFile, settings::InputSettings, ::Val{D}) where D + @assert settings.mesh.dimension == D if lowercase(settings.mesh.file_type) == "exodus" - return read_exodus_mesh(settings.mesh) + return read_exodus_mesh(settings.mesh, Val{D}()) else error_message(log_file.io, "Unsupported mesh type $(settings.mesh.file_type)") end @@ -610,13 +620,13 @@ end ######################################################################### # main app type ######################################################################### -struct App{N} +struct App{D, N} cli_arg_parser::CLIArgParser name::String - function App{N}(name::String) where N + function App{D, N}(name::String) where {D, N} cli_arg_parser = CLIArgParser() - new{N}(cli_arg_parser, name) + new{D, N}(cli_arg_parser, name) end end @@ -629,21 +639,21 @@ function get_cli_arg(app::App, name::String) return get_cli_arg(app.cli_arg_parser, name) end -function setup(app::App{N}, args::Vector{String}) where N +function setup(app::App{D, N}, args::Vector{String}) where {D, N} parse!(app.cli_arg_parser, args) log_file = LogFile(get_cli_arg(app, "--log-file")) try print_banner(log_file, "CLI Arguments") print_dict(log_file.io, app.cli_arg_parser.parsed_args) input_settings = InputSettings{N}(app.cli_arg_parser, log_file) - return Simulation{N}(input_settings, log_file) + return Simulation{D, N}(input_settings, log_file) catch e close(log_file) throw(e) end end -struct Simulation{N, T <: Number, IO, Mesh} +struct Simulation{D, N, T <: Number, IO, Mesh} dbcs::Vector{DirichletBC{ScalarExpressionFunction{T}}} ics::Vector{InitialCondition{ScalarExpressionFunction{T}}} log_file::LogFile{IO} @@ -653,9 +663,9 @@ struct Simulation{N, T <: Number, IO, Mesh} rbcs::Vector{RobinBC{VectorExpressionFunction{N, T}}} srcs::Vector{Source{VectorExpressionFunction{N, T}}} - function Simulation{N}(settings::InputSettings, log_file::LogFile{IO}) where {N, IO} + function Simulation{D, N}(settings::InputSettings, log_file::LogFile{IO}) where {D, N, IO} print_banner(log_file, "Mesh") - mesh = _setup_mesh(log_file, settings) + mesh = _setup_mesh(log_file, settings, Val{D}()) println(log_file.io, mesh) # print_banner(log_file, "Variables") # _setup_variables(log_file, settings, mesh) @@ -670,7 +680,7 @@ struct Simulation{N, T <: Number, IO, Mesh} else T = Float64 end - new{N, T, IO, typeof(mesh)}( + new{D, N, T, IO, typeof(mesh)}( settings.bcs.dirichlet, settings.ics.ics, log_file, mesh, settings.bcs.neumann, settings.bcs.periodic, settings.bcs.robin, settings.bcs.source diff --git a/src/FiniteElementContainers.jl b/src/FiniteElementContainers.jl index 5c2645cc..55042480 100644 --- a/src/FiniteElementContainers.jl +++ b/src/FiniteElementContainers.jl @@ -235,6 +235,8 @@ include("Enzyme.jl") include("Utils.jl") # extras +include("parser/InputFileParser.jl") + include("AppTools.jl") end # module diff --git a/src/parser/InputFileParser.jl b/src/parser/InputFileParser.jl new file mode 100644 index 00000000..8980f410 --- /dev/null +++ b/src/parser/InputFileParser.jl @@ -0,0 +1,84 @@ +module InputFileParser + +include("SimpleYAML.jl") + +using .SimpleYAML +using TOML + +const TOML_INPUT = 1 +const YAML_INPUT = 2 + +mutable struct Parser + data::Dict{String, Any} + input_file::String + input_style::Int +end + +function Parser(input_file::String) + _, ext = splitext(input_file) + if ext == ".toml" + data = TOML.parsefile(input_file) + input_style = TOML_INPUT + elseif ext == ".yaml" + data = SimpleYAML.parsefile(input_file) + data = SimpleYAML.to_dict(data) + input_style = YAML_INPUT + else + @assert false + end + return Parser(data, input_file, input_style) +end + +function Base.getindex(parser::Parser, key::String) + return parser.data[key] +end + +function Base.haskey(parser::Parser, key::String) + return haskey(parser.data, key) +end + +function get_nested_block(parser::Parser, key::String) + val = parser.data[key]::Dict{String, Any} + return val +end + +function get_string_array(parser::Dict, key::String, input_style::Int)::Vector{String} + if input_style == TOML_INPUT + return parser[key]::Vector{String} + elseif input_style == YAML_INPUT + strs = parser[key]::Vector{Any} + vals = String[] + for str in strs + val = str::String + push!(vals, val) + end + return vals + end +end + +function get_string_array(parser::Parser, key::String)::Vector{String} + if parser.input_style == TOML_INPUT + return parser[key]::Vector{String} + elseif parser.input_style == YAML_INPUT + strs = parser[key]::Vector{Any} + vals = String[] + for str in strs + val = str::String + push!(vals, val) + end + return vals + end +end + +# juliac unsafe but easier +function get_value(parser::Parser, key::String) + return parser[key] +end + +# juliac safe but need to know type +function get_value(parser::Parser, key::String, type::Type{T}) where T + val = parser[key]::type + return val +end + +end # module InputFileParser diff --git a/src/parser/SimpleYAML.jl b/src/parser/SimpleYAML.jl new file mode 100644 index 00000000..6b2dc26a --- /dev/null +++ b/src/parser/SimpleYAML.jl @@ -0,0 +1,865 @@ +module SimpleYAML + +export YAMLValue, YAMLNull, YAMLBool, YAMLInt, YAMLFloat, YAMLString, + YAMLArray, YAMLDict, + load, loads, + as_dict, as_array, as_string, as_int, as_float, as_bool, is_null, + get_value, to_dict + +# ───────────────────────────────────────────────────────────────────────────── +# Value type (concrete tagged union — trim-safe, fully type-stable) +# ───────────────────────────────────────────────────────────────────────────── + +@enum YAMLTag begin + TAG_NULL; TAG_BOOL; TAG_INT; TAG_FLOAT; TAG_STRING; TAG_ARRAY; TAG_DICT +end + +struct YAMLValue + tag :: YAMLTag + bval :: Bool + ival :: Int64 + fval :: Float64 + sval :: String + arr :: Vector{YAMLValue} + dict :: Dict{String, YAMLValue} +end + +const YAMLNull = YAMLValue(TAG_NULL, false, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLBool(b::Bool) = YAMLValue(TAG_BOOL, b, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLInt(i::Int64) = YAMLValue(TAG_INT, false, i, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLFloat(f::Float64) = YAMLValue(TAG_FLOAT, false, 0, f, "", YAMLValue[], Dict{String,YAMLValue}()) +YAMLString(s::String) = YAMLValue(TAG_STRING,false, 0, 0.0, s, YAMLValue[], Dict{String,YAMLValue}()) +YAMLArray(a::Vector{YAMLValue}) = YAMLValue(TAG_ARRAY, false, 0, 0.0, "", a, Dict{String,YAMLValue}()) +YAMLDict(d::Dict{String,YAMLValue}) = YAMLValue(TAG_DICT, false, 0, 0.0, "", YAMLValue[], d) + +is_null(v::YAMLValue) = v.tag === TAG_NULL + +function as_bool(v::YAMLValue)::Bool + v.tag === TAG_BOOL || error("YAMLValue is not a bool (tag=$(v.tag))"); v.bval +end +function as_int(v::YAMLValue)::Int64 + v.tag === TAG_INT || error("YAMLValue is not an int (tag=$(v.tag))"); v.ival +end +function as_float(v::YAMLValue)::Float64 + v.tag === TAG_FLOAT || error("YAMLValue is not a float (tag=$(v.tag))"); v.fval +end +function as_string(v::YAMLValue)::String + v.tag === TAG_STRING || error("YAMLValue is not a string (tag=$(v.tag))"); v.sval +end +function as_array(v::YAMLValue)::Vector{YAMLValue} + v.tag === TAG_ARRAY || error("YAMLValue is not an array (tag=$(v.tag))"); v.arr +end +function as_dict(v::YAMLValue)::Dict{String,YAMLValue} + v.tag === TAG_DICT || error("YAMLValue is not a dict (tag=$(v.tag))"); v.dict +end + +function Base.show(io::IO, v::YAMLValue) + if v.tag === TAG_NULL; print(io, "null") + elseif v.tag === TAG_BOOL; print(io, v.bval ? "true" : "false") + elseif v.tag === TAG_INT; print(io, v.ival) + elseif v.tag === TAG_FLOAT; print(io, v.fval) + elseif v.tag === TAG_STRING; print(io, repr(v.sval)) + elseif v.tag === TAG_ARRAY + print(io, "[") + for (i, x) in enumerate(v.arr); i > 1 && print(io, ", "); show(io, x); end + print(io, "]") + else + print(io, "{") + first = true + for (k, val) in v.dict + first || print(io, ", "); first = false + print(io, repr(k), ": "); show(io, val) + end + print(io, "}") + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Parser state +# ───────────────────────────────────────────────────────────────────────────── + +mutable struct ParseState + lines :: Vector{String} + lineno :: Int + anchors :: Dict{String, YAMLValue} +end + +function ParseState(src::String)::ParseState + # split() returns SubStrings — convert immediately so all downstream + # functions only ever handle concrete String. + raw = split(src, '\n') + lines = Vector{String}(undef, length(raw)) + for i in eachindex(raw); lines[i] = String(raw[i]); end + return ParseState(lines, 1, Dict{String,YAMLValue}()) +end + +@inline at_end(ps::ParseState) = ps.lineno > length(ps.lines) +@inline current_line(ps::ParseState) = ps.lineno <= length(ps.lines) ? ps.lines[ps.lineno] : "" + +# ───────────────────────────────────────────────────────────────────────────── +# String helpers — ALL use only codeunit / nextind (never s[byte_index]). +# +# Design rule: the only place character indexing s[i] is allowed is when i +# comes from firstindex(s) or nextind(s, prev) — i.e. a proper character walk. +# Every other access uses codeunit(s, i) for byte inspection or byte-range +# slices s[a:b] where a/b are validated character boundaries. +# ───────────────────────────────────────────────────────────────────────────── + +"""Count leading ASCII space bytes.""" +function line_indent(l::String)::Int + n = ncodeunits(l); i = 1 + while i <= n && codeunit(l, i) == UInt8(' '); i += 1; end + return i - 1 +end + +""" +Return (indent::Int, content::String) for a raw line. +content is the line with leading spaces removed, as a proper String. +""" +function split_indent(l::String)::Tuple{Int,String} + n = ncodeunits(l) + ind = line_indent(l) + # ind is a count of ASCII space bytes, so ind+1 is always a valid char start. + return ind, ind < n ? l[ind+1:end] : "" +end + +"""Find the indent of the next non-blank non-comment line without advancing.""" +function peek_indent(ps::ParseState)::Int + i = ps.lineno + while i <= length(ps.lines) + ind, rest = split_indent(ps.lines[i]) + if !isempty(rest) && codeunit(rest, 1) != UInt8('#') + return ind + end + i += 1 + end + return -1 +end + +function skip_empty!(ps::ParseState) + while !at_end(ps) + _, rest = split_indent(current_line(ps)) + if isempty(rest) || codeunit(rest, 1) == UInt8('#') + ps.lineno += 1 + else + break + end + end +end + +""" +Strip trailing inline comment (' # ...') and trailing whitespace. +Always returns a plain String; uses only byte-level ops. +""" +function strip_inline_comment(s::String)::String + n = ncodeunits(s) + # Need at least 2 bytes: char before '#' + '#' itself + i = 2 + while i <= n + if codeunit(s, i) == UInt8('#') && codeunit(s, i-1) == UInt8(' ') + j = i - 1 # byte just before the space-hash + while j >= 1 && (codeunit(s, j) == UInt8(' ') || codeunit(s, j) == UInt8('\t')) + j -= 1 + end + return j >= 1 ? s[1:j] : "" + end + i += 1 + end + # no comment — rstrip trailing whitespace + j = n + while j >= 1 && (codeunit(s, j) == UInt8(' ') || codeunit(s, j) == UInt8('\t') || + codeunit(s, j) == UInt8('\r') || codeunit(s, j) == UInt8('\n')) + j -= 1 + end + return j >= 1 ? (j == n ? s : s[1:j]) : "" +end + +""" +lstrip ASCII whitespace, returning a plain String. +Only advances through single-byte ASCII space/tab characters, so the +first non-whitespace byte is always a valid character boundary. +""" +function lstrip_ascii(s::String)::String + n = ncodeunits(s); i = 1 + while i <= n && (codeunit(s, i) == UInt8(' ') || codeunit(s, i) == UInt8('\t')) + i += 1 + end + return i <= n ? s[i:end] : "" +end + +""" +Return the suffix of s starting one character after byte-position `byte_pos`. +`byte_pos` must point to the start of a character (i.e. be a valid index). +Uses nextind to step one character, then slices to end. +""" +function suffix_after_char(s::String, byte_pos::Int)::String + n = ncodeunits(s) + ni = nextind(s, byte_pos) + return ni <= n ? s[ni:end] : "" +end + +# ───────────────────────────────────────────────────────────────────────────── +# Scalar coercion +# ───────────────────────────────────────────────────────────────────────────── + +function parse_scalar(s::String)::YAMLValue + isempty(s) && return YAMLNull + (s == "null" || s == "Null" || s == "NULL" || s == "~") && return YAMLNull + (s == "true" || s == "True" || s == "TRUE") && return YAMLBool(true) + (s == "false" || s == "False" || s == "FALSE") && return YAMLBool(false) + (s == ".inf" || s == ".Inf" || s == ".INF" || + s == "+.inf" || s == "+.Inf" || s == "+.INF") && return YAMLFloat(Inf) + (s == "-.inf" || s == "-.Inf" || s == "-.INF") && return YAMLFloat(-Inf) + (s == ".nan" || s == ".NaN" || s == ".NAN") && return YAMLFloat(NaN) + + n = ncodeunits(s) + if n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('x') + v = tryparse(Int64, s[3:end]; base=16); v !== nothing && return YAMLInt(v) + elseif n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('o') + v = tryparse(Int64, s[3:end]; base=8); v !== nothing && return YAMLInt(v) + elseif n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('b') + v = tryparse(Int64, s[3:end]; base=2); v !== nothing && return YAMLInt(v) + else + v = tryparse(Int64, s); v !== nothing && return YAMLInt(v) + end + fv = tryparse(Float64, s); fv !== nothing && return YAMLFloat(fv) + return YAMLString(s) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Quoted-string parsers +# Take a String + byte start index (just after opening quote). +# Return (parsed String, byte index after closing quote). +# Use proper character walks (nextind), so Unicode-safe throughout. +# ───────────────────────────────────────────────────────────────────────────── + +function parse_single_quoted(s::String, start::Int)::Tuple{String,Int} + buf = IOBuffer(); i = start; n = ncodeunits(s) + while i <= n + # Walk one character at a time using nextind + ci = i + i = nextind(s, ci) # advance past current char + ch = s[ci] # safe: ci from nextind chain + if ch == '\'' + if i <= n && codeunit(s, i) == UInt8('\'') + write(buf, '\''); i = nextind(s, i) + else + break # closing quote; i already past it + end + else + write(buf, ch) + end + end + return String(take!(buf)), i +end + +function parse_double_quoted(s::String, start::Int)::Tuple{String,Int} + buf = IOBuffer(); i = start; n = ncodeunits(s) + while i <= n + ci = i; i = nextind(s, ci); ch = s[ci] + if ch == '"' + break + elseif ch == '\\' + i > n && break + ei = i; i = nextind(s, ei); esc = s[ei] + if esc == '0'; write(buf, '\0') + elseif esc == 'a'; write(buf, '\a') + elseif esc == 'b'; write(buf, '\b') + elseif esc == 't' || esc == '\t'; write(buf, '\t') + elseif esc == 'n'; write(buf, '\n') + elseif esc == 'v'; write(buf, '\v') + elseif esc == 'f'; write(buf, '\f') + elseif esc == 'r'; write(buf, '\r') + elseif esc == 'e'; write(buf, '\e') + elseif esc == ' '; write(buf, ' ') + elseif esc == '"'; write(buf, '"') + elseif esc == '/'; write(buf, '/') + elseif esc == '\\'; write(buf, '\\') + elseif esc == 'N'; write(buf, '\u0085') + elseif esc == '_'; write(buf, '\u00a0') + elseif esc == 'L'; write(buf, '\u2028') + elseif esc == 'P'; write(buf, '\u2029') + elseif esc == 'x' + hex = i+1 <= n ? s[i:i+1] : "00" + cp = tryparse(UInt32, hex; base=16) + write(buf, cp !== nothing ? Char(cp) : '?') + i <= n && (i = nextind(s, i)) + i <= n && (i = nextind(s, i)) + elseif esc == 'u' + hex = i+3 <= n ? s[i:i+3] : "0000" + cp = tryparse(UInt32, hex; base=16) + write(buf, cp !== nothing ? Char(cp) : '?') + for _ in 1:4; i <= n && (i = nextind(s, i)); end + elseif esc == 'U' + hex = i+7 <= n ? s[i:i+7] : "00000000" + cp = tryparse(UInt32, hex; base=16) + write(buf, cp !== nothing ? Char(cp) : '?') + for _ in 1:8; i <= n && (i = nextind(s, i)); end + else + write(buf, esc) + end + else + write(buf, ch) + end + end + return String(take!(buf)), i +end + +# ───────────────────────────────────────────────────────────────────────────── +# Block scalar (| and >) +# Caller has already consumed the header line; ps.lineno → first content line. +# ───────────────────────────────────────────────────────────────────────────── + +function parse_block_scalar(ps::ParseState, style::Char, header::String)::String + chomping = :clip; explicit_indent = 0 + h = header + # strip trailing comment + hi = firstindex(h) + while hi <= lastindex(h) + if codeunit(h, hi) == UInt8('#') && hi > 1 && codeunit(h, hi-1) == UInt8(' ') + h = String(rstrip(h[1:hi-2])); break + end + hi = nextind(h, hi) + end + h = String(strip(h)) + # parse chomping / explicit-indent chars (all ASCII, safe with nextind walk) + hi = firstindex(h) + while hi <= lastindex(h) + ch = h[hi] # safe: hi from nextind chain starting at firstindex + if ch == '-'; chomping = :strip; hi = nextind(h, hi) + elseif ch == '+'; chomping = :keep; hi = nextind(h, hi) + elseif isdigit(ch); explicit_indent = Int(ch - '0'); hi = nextind(h, hi) + else; break + end + end + + block_indent = explicit_indent + if block_indent == 0 + j = ps.lineno + while j <= length(ps.lines) + ind, rest = split_indent(ps.lines[j]) + if !isempty(rest); block_indent = ind; break; end + j += 1 + end + end + + lines_out = String[] + while !at_end(ps) + l = current_line(ps) + ind, rest = split_indent(l) + if isempty(rest) + push!(lines_out, ""); ps.lineno += 1; continue + end + ind < block_indent && break + push!(lines_out, l[block_indent+1:end]); ps.lineno += 1 + end + + result::String = if style == '|' + join(lines_out, "\n") + else + buf = IOBuffer(); prev_empty = false + for (idx, ln) in enumerate(lines_out) + if isempty(ln); prev_empty = true + else + if idx > 1 && !prev_empty; write(buf, ' ') + elseif prev_empty; write(buf, '\n') + end + write(buf, ln); prev_empty = false + end + end + String(take!(buf)) + end + + if chomping === :strip; result = String(rstrip(result, '\n')) + elseif chomping === :clip; result = String(rstrip(result, '\n')) * "\n" + end + return result +end + +# ───────────────────────────────────────────────────────────────────────────── +# Flow (inline) parsers +# All use codeunit for structural character checks and nextind for walking. +# ───────────────────────────────────────────────────────────────────────────── + +function parse_flow_value(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} + n = ncodeunits(s) + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')) + i += 1 + end + i > n && return YAMLNull, i + + # Peek at first byte — all YAML flow structural characters are single-byte ASCII. + b = codeunit(s, i) + if b == UInt8('{') + return parse_flow_mapping(ps, s, i+1) + elseif b == UInt8('[') + return parse_flow_sequence(ps, s, i+1) + elseif b == UInt8('\'') + str, i2 = parse_single_quoted(s, i+1) + return YAMLString(str), i2 + elseif b == UInt8('"') + str, i2 = parse_double_quoted(s, i+1) + return YAMLString(str), i2 + elseif b == UInt8('&') + j = i + 1 + while j <= n && codeunit(s,j) != UInt8(' ') && codeunit(s,j) != UInt8(',') && + codeunit(s,j) != UInt8('}') && codeunit(s,j) != UInt8(']') + j += 1 + end + aname = s[i+1:j-1] # anchor names are ASCII by convention + val, i2 = parse_flow_value(ps, s, j) + ps.anchors[aname] = val + return val, i2 + elseif b == UInt8('*') + j = i + 1 + while j <= n && codeunit(s,j) != UInt8(' ') && codeunit(s,j) != UInt8(',') && + codeunit(s,j) != UInt8('}') && codeunit(s,j) != UInt8(']') + j += 1 + end + return get(ps.anchors, s[i+1:j-1], YAMLNull), j + else + j = i + while j <= n + bj = codeunit(s, j) + (bj == UInt8(',') || bj == UInt8('}') || bj == UInt8(']') || bj == UInt8('#')) && break + j += 1 + end + raw = String(strip(s[i:j-1])) + return parse_scalar(raw), j + end +end + +function parse_flow_sequence(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} + items = YAMLValue[]; n = ncodeunits(s) + while i <= n + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t') || + codeunit(s,i) == UInt8('\n')) + i += 1 + end + i > n && break + codeunit(s,i) == UInt8(']') && (i += 1; break) + codeunit(s,i) == UInt8(',') && (i += 1; continue) + val, i = parse_flow_value(ps, s, i) + push!(items, val) + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end + i <= n && codeunit(s,i) == UInt8(',') && (i += 1) + end + return YAMLArray(items), i +end + +function parse_flow_mapping(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} + d = Dict{String,YAMLValue}(); n = ncodeunits(s) + while i <= n + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t') || + codeunit(s,i) == UInt8('\n')) + i += 1 + end + i > n && break + codeunit(s,i) == UInt8('}') && (i += 1; break) + codeunit(s,i) == UInt8(',') && (i += 1; continue) + key_val, i = parse_flow_value(ps, s, i) + key = yaml_value_to_key(key_val) + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end + i <= n && codeunit(s,i) == UInt8(':') && (i += 1) + val, i = parse_flow_value(ps, s, i) + d[key] = val + while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end + i <= n && codeunit(s,i) == UInt8(',') && (i += 1) + end + return YAMLDict(d), i +end + +function yaml_value_to_key(v::YAMLValue)::String + v.tag === TAG_STRING && return v.sval + v.tag === TAG_INT && return string(v.ival) + v.tag === TAG_FLOAT && return string(v.fval) + v.tag === TAG_BOOL && return v.bval ? "true" : "false" + return "null" +end + +# ───────────────────────────────────────────────────────────────────────────── +# Key detection — fully Unicode-safe via character walks +# +# Returns (key::String, after_colon_byte_index::Int) or (nothing, 0). +# after_colon_byte_index points one past the ':' byte (i.e. the start of the +# value portion); it is always a valid character boundary because ':' is ASCII +# and the next byte starts a new character. +# ───────────────────────────────────────────────────────────────────────────── + +function find_mapping_key(content::String)::Tuple{Union{Nothing,String},Int} + isempty(content) && return nothing, 0 + n = ncodeunits(content) + b1 = codeunit(content, 1) + + if b1 == UInt8('"') + key, i = parse_double_quoted(content, 2) + # skip spaces (all ASCII, so += 1 safe) + while i <= n && codeunit(content, i) == UInt8(' '); i += 1; end + if i <= n && codeunit(content, i) == UInt8(':') + ni = i + 1 # safe: ':' is 1 byte, next byte starts a new char + if ni > n || codeunit(content, ni) == UInt8(' ') || + codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') + return key, ni + end + end + return nothing, 0 + + elseif b1 == UInt8('\'') + key, i = parse_single_quoted(content, 2) + while i <= n && codeunit(content, i) == UInt8(' '); i += 1; end + if i <= n && codeunit(content, i) == UInt8(':') + ni = i + 1 + if ni > n || codeunit(content, ni) == UInt8(' ') || + codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') + return key, ni + end + end + return nothing, 0 + + else + # Bare key: walk character by character using nextind. + # We are looking for a ':' byte followed by space/tab/EOF/comment. + # ':' is ASCII so codeunit comparison is correct; nextind handles + # multi-byte characters safely. + i = firstindex(content) # always 1, but semantically correct + while i <= lastindex(content) + if codeunit(content, i) == UInt8(':') + ni = i + 1 # ':' is ASCII (1 byte), so ni is a valid index + if ni > n || codeunit(content, ni) == UInt8(' ') || + codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') + # Collect key as content[1 .. prevind(content, i)] + # rstrip ASCII whitespace from the right + ke = i - 1 # byte before ':' + while ke >= 1 && (codeunit(content, ke) == UInt8(' ') || + codeunit(content, ke) == UInt8('\t')) + ke -= 1 + end + ke < 1 && return nothing, 0 + # content[1:ke] is a valid byte-range slice: ke is either + # the last byte of a multi-byte char (fine for range end) + # or a single-byte char. Julia range slices are byte-based + # and only require the START to be a valid char boundary; + # the end can be any byte position >= the last byte of a char. + return content[1:ke], ni + end + end + i = nextind(content, i) + end + return nothing, 0 + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Block mapping +# ───────────────────────────────────────────────────────────────────────────── + +function parse_block_mapping(ps::ParseState, map_indent::Int)::YAMLValue + d = Dict{String,YAMLValue}() + while !at_end(ps) + skip_empty!(ps); at_end(ps) && break + l = current_line(ps) + indent, content = split_indent(l) + indent != map_indent && break + isempty(content) && (ps.lineno += 1; continue) + (startswith(content, "---") || startswith(content, "...")) && break + + key, after_colon = find_mapping_key(content) + key === nothing && break + ps.lineno += 1 # consume key line + + # Value portion on the same line: content[after_colon:end], lstripped. + # after_colon is always a valid character boundary (one past ASCII ':'). + rest = after_colon <= n_cu(content) ? lstrip_ascii(content[after_colon:end]) : "" + rest = strip_inline_comment(rest) + + val::YAMLValue = _parse_value_after_colon(ps, rest, map_indent) + d[key] = val + end + return YAMLDict(d) +end + +# ncodeunits shorthand (avoids repeating the call name) +@inline n_cu(s::String) = ncodeunits(s) + +""" +Determine the value given the stripped inline remainder after a mapping colon +(or after a sequence dash), plus the current block's indent level. +""" +function _parse_value_after_colon(ps::ParseState, rest::String, block_indent::Int)::YAMLValue + if isempty(rest) || codeunit(rest, 1) == UInt8('#') + # value on subsequent line(s) + next_ind = peek_indent(ps) + if next_ind < 0 + return YAMLNull + elseif next_ind > block_indent + return parse_node(ps, next_ind) + elseif next_ind == block_indent + _, nc = split_indent(current_line(ps)) + if !isempty(nc) && codeunit(nc, 1) == UInt8('-') && + (n_cu(nc) == 1 || codeunit(nc, 2) == UInt8(' ') || codeunit(nc, 2) == UInt8('\t')) + return parse_block_sequence(ps, next_ind) + end + end + return YAMLNull + + elseif codeunit(rest, 1) == UInt8('|') || codeunit(rest, 1) == UInt8('>') + style = Char(codeunit(rest, 1)) + hdr = n_cu(rest) > 1 ? suffix_after_char(rest, 1) : "" + ps.lineno += 1 # consume block-scalar header (current line already consumed by caller) + return YAMLString(parse_block_scalar(ps, style, hdr)) + + elseif codeunit(rest, 1) == UInt8('{') + v, _ = parse_flow_mapping(ps, rest, 2); return v + + elseif codeunit(rest, 1) == UInt8('[') + v, _ = parse_flow_sequence(ps, rest, 2); return v + + elseif codeunit(rest, 1) == UInt8('&') + # &anchorname + j = 2 + while j <= n_cu(rest) && codeunit(rest,j) != UInt8(' ') && codeunit(rest,j) != UInt8('\t') + j += 1 + end + aname = rest[2:j-1] # anchor names are plain ASCII + inner = lstrip_ascii(j <= n_cu(rest) ? rest[j:end] : "") + v = if isempty(inner) || codeunit(inner, 1) == UInt8('#') + ni = peek_indent(ps) + ni > block_indent ? parse_node(ps, ni) : YAMLNull + else + parse_scalar(strip_inline_comment(inner)) + end + ps.anchors[aname] = v; return v + + elseif codeunit(rest, 1) == UInt8('*') + j = 2 + while j <= n_cu(rest) && codeunit(rest,j) != UInt8(' ') && codeunit(rest,j) != UInt8('#') + j += 1 + end + return get(ps.anchors, rest[2:j-1], YAMLNull) + + else + return parse_scalar(rest) + end +end + +# ───────────────────────────────────────────────────────────────────────────── +# Block sequence +# ───────────────────────────────────────────────────────────────────────────── + +function parse_block_sequence(ps::ParseState, seq_indent::Int)::YAMLValue + items = YAMLValue[] + while !at_end(ps) + skip_empty!(ps); at_end(ps) && break + l = current_line(ps) + indent, content = split_indent(l) + indent != seq_indent && break + isempty(content) && (ps.lineno += 1; continue) + codeunit(content, 1) != UInt8('-') && break + if n_cu(content) >= 2 + b2 = codeunit(content, 2) + b2 != UInt8(' ') && b2 != UInt8('\t') && break + end + + ps.lineno += 1 # consume '-' line + + # Content after '- ' (suffix_after_char handles multi-byte-safe skip of '-') + inline = lstrip_ascii(n_cu(content) > 1 ? suffix_after_char(content, 1) : "") + inline = strip_inline_comment(inline) + + val::YAMLValue = if isempty(inline) || codeunit(inline, 1) == UInt8('#') + next_ind = peek_indent(ps) + next_ind > seq_indent ? parse_node(ps, next_ind) : YAMLNull + + elseif codeunit(inline, 1) == UInt8('-') && + (n_cu(inline) == 1 || codeunit(inline, 2) == UInt8(' ')) + # fake = " "^(seq_indent + 2) * inline + new_ind = seq_indent ÷ 2 + buf = IOBuffer() + for i in 1:new_ind + write(buf, ' ') + end + write(buf, inline) + fake = String(take!(buf)) + insert!(ps.lines, ps.lineno, fake) + parse_block_sequence(ps, seq_indent + 2) + + else + # Check for inline mapping key first, then delegate to _parse_value_after_colon + k, _ = find_mapping_key(inline) + if k !== nothing + new_ind = seq_indent + 2 + # fake_line = " "^new_ind * inline + buf = IOBuffer() + for i in 1:new_ind + write(buf, ' ') + end + write(buf, inline) + fake_line = String(take!(buf)) + + insert!(ps.lines, ps.lineno, fake_line) + parse_node(ps, new_ind) + else + _parse_value_after_colon(ps, inline, seq_indent) + end + end + + push!(items, val) + end + return YAMLArray(items) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Node dispatcher +# ───────────────────────────────────────────────────────────────────────────── + +function parse_node(ps::ParseState, min_indent::Int)::YAMLValue + skip_empty!(ps); at_end(ps) && return YAMLNull + l = current_line(ps) + indent, content = split_indent(l) + indent < min_indent && return YAMLNull + isempty(content) && return YAMLNull + + if startswith(content, "---") || startswith(content, "...") + nc = n_cu(content) + ok = nc == 3 || codeunit(content,4) == UInt8(' ') || + codeunit(content,4) == UInt8('\t') || codeunit(content,4) == UInt8('#') + if ok; ps.lineno += 1; return parse_node(ps, min_indent); end + end + + b1 = codeunit(content, 1) + if b1 == UInt8('-') && (n_cu(content) == 1 || codeunit(content,2) == UInt8(' ') || + codeunit(content,2) == UInt8('\t')) + return parse_block_sequence(ps, indent) + end + + key, _ = find_mapping_key(content) + key !== nothing && return parse_block_mapping(ps, indent) + + return parse_line_scalar(ps, min_indent) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Scalar line parser +# ───────────────────────────────────────────────────────────────────────────── + +function parse_line_scalar(ps::ParseState, min_indent::Int)::YAMLValue + skip_empty!(ps); at_end(ps) && return YAMLNull + l = current_line(ps) + indent, content = split_indent(l) + indent < min_indent && return YAMLNull + isempty(content) && return YAMLNull + + b1 = codeunit(content, 1) + + if b1 == UInt8('&') + ps.lineno += 1 + j = 2; nn = n_cu(content) + while j <= nn && codeunit(content,j) != UInt8(' ') && codeunit(content,j) != UInt8('\t') + j += 1 + end + aname = content[2:j-1] + inner = lstrip_ascii(j <= nn ? content[j:end] : "") + val = isempty(inner) || codeunit(inner,1) == UInt8('#') ? + parse_node(ps, min_indent) : + parse_scalar(strip_inline_comment(inner)) + ps.anchors[aname] = val; return val + end + + if b1 == UInt8('*') + ps.lineno += 1; j = 2; nn = n_cu(content) + while j <= nn && codeunit(content,j) != UInt8(' ') && codeunit(content,j) != UInt8('#') + j += 1 + end + return get(ps.anchors, content[2:j-1], YAMLNull) + end + + if b1 == UInt8('"') + ps.lineno += 1; str, _ = parse_double_quoted(content, 2); return YAMLString(str) + end + if b1 == UInt8('\'') + ps.lineno += 1; str, _ = parse_single_quoted(content, 2); return YAMLString(str) + end + if b1 == UInt8('|') || b1 == UInt8('>') + style = Char(b1) + hdr = n_cu(content) > 1 ? suffix_after_char(content, 1) : "" + ps.lineno += 1 + return YAMLString(parse_block_scalar(ps, style, hdr)) + end + if b1 == UInt8('{') + ps.lineno += 1; v, _ = parse_flow_mapping(ps, content, 2); return v + end + if b1 == UInt8('[') + ps.lineno += 1; v, _ = parse_flow_sequence(ps, content, 2); return v + end + + # Plain (possibly multi-line) scalar + ps.lineno += 1 + parts = String[strip_inline_comment(content)] + while !at_end(ps) + skip_empty!(ps); at_end(ps) && break + l2 = current_line(ps) + ind2, c2 = split_indent(l2) + ind2 <= min_indent && break + isempty(c2) && break + codeunit(c2,1) == UInt8('#') && break + k2, _ = find_mapping_key(c2); k2 !== nothing && break + if codeunit(c2,1) == UInt8('-') && (n_cu(c2) == 1 || codeunit(c2,2) == UInt8(' ')) + break + end + push!(parts, strip_inline_comment(c2)); ps.lineno += 1 + end + return parse_scalar(join(parts, " ")) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Public API +# ───────────────────────────────────────────────────────────────────────────── + +function loads(yaml::String)::YAMLValue + ps = ParseState(yaml); skip_empty!(ps) + at_end(ps) && return YAMLNull + return parse_node(ps, 0) +end + +function parsefile(path::String)::YAMLValue + return loads(read(path, String)) +end + +# ───────────────────────────────────────────────────────────────────────────── +# Convenience helpers +# ───────────────────────────────────────────────────────────────────────────── + +function get_value(d::Dict{String,YAMLValue}, key, ::Type{Array}); return as_array(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{<:Dict}); return as_dict(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{Float64}); return as_float(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{Int}); return as_int(d[key]); end +function get_value(d::Dict{String,YAMLValue}, key, ::Type{String}); return as_string(d[key]);end + +function to_dict(v::String) + return v +end + +function to_dict(v::YAMLValue) + if v.tag === TAG_NULL; return nothing + elseif v.tag === TAG_BOOL; return v.bval + elseif v.tag === TAG_INT; return v.ival + elseif v.tag === TAG_FLOAT; return v.fval + elseif v.tag === TAG_STRING; return v.sval + elseif v.tag === TAG_ARRAY + vals = Any[] + for item in v.arr; push!(vals, to_dict(item)); end + return vals + else + d = Dict{String,Any}() + for (k, val) in v.dict; d[k] = to_dict(val); end + return d + end +end + +function to_dict(d::Dict{String,YAMLValue}) + out = Dict{String,Any}() + for (k, v) in d; out[k] = to_dict(v); end + return out +end + +end # module SimpleYAML From f6340107b7da7732e41a302de042b9c257dacad1 Mon Sep 17 00:00:00 2001 From: "Craig M. Hamel" Date: Fri, 12 Jun 2026 18:01:27 -0400 Subject: [PATCH 2/5] removing junk --- examples/simple_yaml/Project.toml | 3 - examples/simple_yaml/make.jl | 28 - examples/simple_yaml/src/SimpleYAML.jl | 1860 ----------------- examples/simple_yaml/src/main.jl | 68 - examples/simple_yaml/test/example_file.yaml | 41 - examples/simple_yaml/test/test_files/bar.yaml | 40 - examples/simple_yaml/test/test_yaml.jl | 369 ---- 7 files changed, 2409 deletions(-) delete mode 100644 examples/simple_yaml/Project.toml delete mode 100644 examples/simple_yaml/make.jl delete mode 100644 examples/simple_yaml/src/SimpleYAML.jl delete mode 100644 examples/simple_yaml/src/main.jl delete mode 100644 examples/simple_yaml/test/example_file.yaml delete mode 100644 examples/simple_yaml/test/test_files/bar.yaml delete mode 100644 examples/simple_yaml/test/test_yaml.jl diff --git a/examples/simple_yaml/Project.toml b/examples/simple_yaml/Project.toml deleted file mode 100644 index c028499f..00000000 --- a/examples/simple_yaml/Project.toml +++ /dev/null @@ -1,3 +0,0 @@ -name = "MyApp" - -[deps] diff --git a/examples/simple_yaml/make.jl b/examples/simple_yaml/make.jl deleted file mode 100644 index a9a1963d..00000000 --- a/examples/simple_yaml/make.jl +++ /dev/null @@ -1,28 +0,0 @@ -using JuliaC -build_path = joinpath(@__DIR__, "build") -src_path = joinpath(@__DIR__) -@show build_path -@show src_path -rm(build_path; force=true, recursive=true) - -img = ImageRecipe( - output_type = "--output-exe", - file = "$src_path/src/main.jl", - trim_mode = "safe", - add_ccallables = false, - verbose = false, -) - -link = LinkRecipe( - image_recipe = img, - outname = "$build_path/my_app" -) - -bun = BundleRecipe( - link_recipe = link, - output_dir = build_path # or `nothing` to skip bundling -) - -compile_products(img) -link_products(link) -bundle_products(bun) diff --git a/examples/simple_yaml/src/SimpleYAML.jl b/examples/simple_yaml/src/SimpleYAML.jl deleted file mode 100644 index 980f836a..00000000 --- a/examples/simple_yaml/src/SimpleYAML.jl +++ /dev/null @@ -1,1860 +0,0 @@ -# module SimpleYAML - -# export YAMLValue, YAMLNull, YAMLBool, YAMLInt, YAMLFloat, YAMLString, -# YAMLArray, YAMLDict, -# load, loads, -# as_dict, as_array, as_string, as_int, as_float, as_bool, is_null, -# get_value - -# # ────────────────────────────────────────────────────────────────────────────── -# # Value type -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# Tagged-union value node. Every YAML value is one of the seven tags below. -# Using a concrete struct with a tag enum keeps things type-stable for the -# compiler even though the result tree is heterogeneous. -# """ -# @enum YAMLTag begin -# TAG_NULL -# TAG_BOOL -# TAG_INT -# TAG_FLOAT -# TAG_STRING -# TAG_ARRAY -# TAG_DICT -# end - -# struct YAMLValue -# tag :: YAMLTag -# # Scalar storage (only one is used at a time) -# bval :: Bool -# ival :: Int64 -# fval :: Float64 -# sval :: String -# # Collection storage -# arr :: Vector{YAMLValue} -# dict :: Dict{String, YAMLValue} -# end - -# # ── Constructors ────────────────────────────────────────────────────────────── - -# const YAMLNull = YAMLValue(TAG_NULL, false, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) -# YAMLBool(b::Bool) = YAMLValue(TAG_BOOL, b, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) -# YAMLInt(i::Int64) = YAMLValue(TAG_INT, false, i, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) -# YAMLFloat(f::Float64)= YAMLValue(TAG_FLOAT, false, 0, f, "", YAMLValue[], Dict{String,YAMLValue}()) -# YAMLString(s::AbstractString)= YAMLValue(TAG_STRING,false, 0, 0.0, String(s), YAMLValue[], Dict{String,YAMLValue}()) -# YAMLArray(a::Vector{YAMLValue}) = YAMLValue(TAG_ARRAY, false,0,0.0,"", a, Dict{String,YAMLValue}()) -# YAMLDict(d::Dict{String,YAMLValue}) = YAMLValue(TAG_DICT, false,0,0.0,"", YAMLValue[], d) - -# # ── Accessors ──────────────────────────────────────────────────────────────── - -# is_null(v::YAMLValue) = v.tag === TAG_NULL - -# function as_bool(v::YAMLValue)::Bool -# v.tag === TAG_BOOL || error("YAMLValue is not a bool (tag=$(v.tag))") -# return v.bval -# end -# function as_int(v::YAMLValue)::Int64 -# v.tag === TAG_INT || error("YAMLValue is not an int (tag=$(v.tag))") -# return v.ival -# end -# function as_float(v::YAMLValue)::Float64 -# v.tag === TAG_FLOAT || error("YAMLValue is not a float (tag=$(v.tag))") -# return v.fval -# end -# function as_string(v::YAMLValue)::String -# v.tag === TAG_STRING || error("YAMLValue is not a string (tag=$(v.tag))") -# return v.sval -# end -# function as_array(v::YAMLValue)::Vector{YAMLValue} -# v.tag === TAG_ARRAY || error("YAMLValue is not an array (tag=$(v.tag))") -# return v.arr -# end -# function as_dict(v::YAMLValue)::Dict{String,YAMLValue} -# v.tag === TAG_DICT || error("YAMLValue is not a dict (tag=$(v.tag))") -# return v.dict -# end - -# function Base.show(io::IO, v::YAMLValue) -# if v.tag === TAG_NULL; print(io, "null") -# elseif v.tag === TAG_BOOL; print(io, v.bval ? "true" : "false") -# elseif v.tag === TAG_INT; print(io, v.ival) -# elseif v.tag === TAG_FLOAT; print(io, v.fval) -# elseif v.tag === TAG_STRING; print(io, repr(v.sval)) -# elseif v.tag === TAG_ARRAY -# print(io, "[") -# for (i, x) in enumerate(v.arr) -# i > 1 && print(io, ", ") -# show(io, x) -# end -# print(io, "]") -# else # TAG_DICT -# print(io, "{") -# first = true -# for (k, val) in v.dict -# first || print(io, ", ") -# first = false -# print(io, repr(k), ": ") -# show(io, val) -# end -# print(io, "}") -# end -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Parser state -# # ────────────────────────────────────────────────────────────────────────────── - -# mutable struct ParseState -# lines :: Vector{String} -# lineno :: Int -# anchors :: Dict{String, YAMLValue} # anchor name → value -# end - -# ParseState(src::String) = ParseState(split(src, '\n'), 1, Dict{String,YAMLValue}()) - -# # ── Low-level line helpers ──────────────────────────────────────────────────── - -# @inline at_end(ps::ParseState) = ps.lineno > length(ps.lines) - -# @inline current_line(ps::ParseState) = -# ps.lineno <= length(ps.lines) ? ps.lines[ps.lineno] : "" - -# function peek_indent(ps::ParseState) -# # Look ahead past blank / comment lines to find the indent of the next -# # content line. Returns -1 at EOF. -# i = ps.lineno -# while i <= length(ps.lines) -# l = ps.lines[i] -# s = lstrip(l) -# if !isempty(s) && s[1] != '#' -# return ncodeunits(l) - ncodeunits(s) # byte-count indent -# end -# i += 1 -# end -# return -1 -# end - -# """Skip blank lines and comment-only lines, advancing ps.lineno.""" -# function skip_empty!(ps::ParseState) -# while !at_end(ps) -# l = current_line(ps) -# s = lstrip(l) -# if isempty(s) || s[1] == '#' -# ps.lineno += 1 -# else -# break -# end -# end -# end - -# """Return the indent (number of leading spaces) of a line.""" -# function line_indent(l::String) -# i = 1 -# n = ncodeunits(l) -# while i <= n && codeunit(l, i) == UInt8(' ') -# i += 1 -# end -# return i - 1 -# end - -# """Strip inline comment from the end of a scalar string.""" -# function strip_inline_comment(s::AbstractString) -# # A comment is ' #...' — space then hash -# i = 1 -# n = ncodeunits(s) -# while i <= n -# c = codeunit(s, i) -# if c == UInt8('#') && i > 1 && codeunit(s, i-1) == UInt8(' ') -# return rstrip(s[1:i-2]) -# end -# i += 1 -# end -# return s -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Scalar parsing helpers -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# Parse a YAML bare scalar string into a typed YAMLValue. -# Handles null, bool, int (decimal/hex/octal/binary), float, and string fallback. -# """ -# function parse_scalar(s::AbstractString)::YAMLValue -# isempty(s) && return YAMLNull - -# # null -# (s == "null" || s == "Null" || s == "NULL" || s == "~") && return YAMLNull - -# # bool -# (s == "true" || s == "True" || s == "TRUE") && return YAMLBool(true) -# (s == "false"|| s == "False"|| s == "FALSE") && return YAMLBool(false) - -# # special floats -# (s == ".inf" || s == ".Inf" || s == ".INF" || -# s == "+.inf"|| s == "+.Inf"|| s == "+.INF") && return YAMLFloat(Inf) -# (s == "-.inf"|| s == "-.Inf"|| s == "-.INF") && return YAMLFloat(-Inf) -# (s == ".nan" || s == ".NaN" || s == ".NAN") && return YAMLFloat(NaN) - -# # integers (0x… hex, 0o… octal, 0b… binary, plain decimal) -# if length(s) >= 3 && s[1] == '0' && s[2] == 'x' -# v = tryparse(Int64, s[3:end]; base=16) -# v !== nothing && return YAMLInt(v) -# elseif length(s) >= 3 && s[1] == '0' && s[2] == 'o' -# v = tryparse(Int64, s[3:end]; base=8) -# v !== nothing && return YAMLInt(v) -# elseif length(s) >= 3 && s[1] == '0' && s[2] == 'b' -# v = tryparse(Int64, s[3:end]; base=2) -# v !== nothing && return YAMLInt(v) -# else -# v = tryparse(Int64, s) -# v !== nothing && return YAMLInt(v) -# end - -# # float -# fv = tryparse(Float64, s) -# fv !== nothing && return YAMLFloat(fv) - -# # fallback to string -# return YAMLString(s) -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Quoted-string parsing -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# Parse a single-quoted YAML string starting just after the opening quote. -# Returns (String, new_index) where new_index points past the closing quote. -# """ -# function parse_single_quoted(s::AbstractString, start::Int)::Tuple{String,Int} -# buf = IOBuffer() -# i = start -# n = ncodeunits(s) -# while i <= n -# c = s[i] -# if c == '\'' -# # '' is an escaped single-quote inside single-quoted strings -# if i + 1 <= n && s[i+1] == '\'' -# write(buf, '\'') -# i += 2 -# else -# i += 1 # skip closing quote -# break -# end -# else -# write(buf, c) -# i = nextind(s, i) -# end -# end -# return String(take!(buf)), i -# end - -# """ -# Parse a double-quoted YAML string starting just after the opening quote. -# Handles standard JSON-like escape sequences plus YAML-specific \\N \\L \\P. -# """ -# function parse_double_quoted(s::AbstractString, start::Int)::Tuple{String,Int} -# buf = IOBuffer() -# i = start -# n = ncodeunits(s) -# while i <= n -# c = s[i] -# if c == '"' -# i += 1 -# break -# elseif c == '\\' -# i += 1 -# i > n && break -# esc = s[i] -# if esc == '0'; write(buf, '\0') -# elseif esc == 'a'; write(buf, '\a') -# elseif esc == 'b'; write(buf, '\b') -# elseif esc == 't' || esc == '\t'; write(buf, '\t') -# elseif esc == 'n'; write(buf, '\n') -# elseif esc == 'v'; write(buf, '\v') -# elseif esc == 'f'; write(buf, '\f') -# elseif esc == 'r'; write(buf, '\r') -# elseif esc == 'e'; write(buf, '\e') -# elseif esc == ' '; write(buf, ' ') -# elseif esc == '"'; write(buf, '"') -# elseif esc == '/'; write(buf, '/') -# elseif esc == '\\'; write(buf, '\\') -# elseif esc == 'N'; write(buf, '\u0085') # YAML next-line -# elseif esc == '_'; write(buf, '\u00a0') # YAML non-breaking space -# elseif esc == 'L'; write(buf, '\u2028') # YAML line-separator -# elseif esc == 'P'; write(buf, '\u2029') # YAML para-separator -# elseif esc == 'x' -# i += 1 -# hex = (i+1 <= n) ? s[i:i+1] : "" -# cp = tryparse(UInt32, hex; base=16) -# cp !== nothing ? write(buf, Char(cp)) : write(buf, '?') -# i += 1 -# elseif esc == 'u' -# i += 1 -# hex = (i+3 <= n) ? s[i:i+3] : "" -# cp = tryparse(UInt32, hex; base=16) -# cp !== nothing ? write(buf, Char(cp)) : write(buf, '?') -# i += 3 -# elseif esc == 'U' -# i += 1 -# hex = (i+7 <= n) ? s[i:i+7] : "" -# cp = tryparse(UInt32, hex; base=16) -# cp !== nothing ? write(buf, Char(cp)) : write(buf, '?') -# i += 7 -# else -# write(buf, esc) -# end -# i = nextind(s, i) -# else -# write(buf, c) -# i = nextind(s, i) -# end -# end -# return String(take!(buf)), i -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Block scalar parsing (| and >) -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# Parse a literal block scalar (|) or folded block scalar (>). -# `header` is the rest of the line after the indicator, e.g. "|-", "|2", "|+2". -# Returns the assembled String. -# """ -# function parse_block_scalar(ps::ParseState, style::Char, header::AbstractString)::String -# # Parse chomping indicator and explicit indent -# chomping = :clip # clip | strip (-) | keep (+) -# explicit_indent = 0 - -# rest = strip(header) -# # strip trailing comment -# ci = findfirst('#', rest) -# ci !== nothing && (rest = rstrip(rest[1:ci-1])) - -# i = 1 -# while i <= length(rest) -# c = rest[i] -# if c == '-'; chomping = :strip; i += 1 -# elseif c == '+'; chomping = :keep; i += 1 -# elseif isdigit(c) -# explicit_indent = parse(Int, string(c)) -# i += 1 -# else -# break -# end -# end - -# # The header line has already been consumed by the caller. -# # Determine block indent from the first non-empty content line -# block_indent = explicit_indent -# if block_indent == 0 -# j = ps.lineno -# while j <= length(ps.lines) -# l = ps.lines[j] -# s = lstrip(l) -# if !isempty(s) -# block_indent = line_indent(l) -# break -# end -# j += 1 -# end -# end - -# lines_out = String[] -# while !at_end(ps) -# l = current_line(ps) -# indent = line_indent(l) -# content = lstrip(l) - -# # Empty lines are kept (as "") regardless of indent -# if isempty(content) -# push!(lines_out, "") -# ps.lineno += 1 -# continue -# end - -# # A line with less indent than block_indent ends the scalar -# indent < block_indent && break - -# push!(lines_out, l[block_indent+1:end]) -# ps.lineno += 1 -# end - -# # Assemble -# if style == '|' -# # Literal: join with newlines -# result = join(lines_out, "\n") -# else -# # Folded: fold non-empty lines separated by single newlines; -# # blank lines become paragraph breaks. -# buf = IOBuffer() -# prev_empty = false -# for (idx, ln) in enumerate(lines_out) -# if isempty(ln) -# prev_empty = true -# else -# if idx > 1 && !prev_empty -# write(buf, ' ') -# elseif prev_empty -# write(buf, '\n') -# end -# write(buf, ln) -# prev_empty = false -# end -# end -# result = String(take!(buf)) -# end - -# # Apply chomping -# if chomping === :strip -# result = rstrip(result, '\n') -# elseif chomping === :clip -# # Ensure exactly one trailing newline -# result = rstrip(result, '\n') * "\n" -# else # keep — preserve all trailing newlines -# # already correct -# end - -# return result -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Flow (inline) parsing -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# Parse a flow (inline) value starting at position `i` in string `s`. -# Returns (YAMLValue, new_index). -# """ -# function parse_flow_value(ps::ParseState, s::AbstractString, i::Int)::Tuple{YAMLValue,Int} -# # skip leading whitespace -# n = ncodeunits(s) -# while i <= n && (s[i] == ' ' || s[i] == '\t') -# i += 1 -# end -# i > n && return YAMLNull, i - -# c = s[i] - -# if c == '{' -# return parse_flow_mapping(ps, s, i+1) -# elseif c == '[' -# return parse_flow_sequence(ps, s, i+1) -# elseif c == '\'' -# str, i2 = parse_single_quoted(s, i+1) -# return YAMLString(str), i2 -# elseif c == '"' -# str, i2 = parse_double_quoted(s, i+1) -# return YAMLString(str), i2 -# elseif c == '&' -# # Anchor in flow context — parse the name then the value -# j = i + 1 -# while j <= n && s[j] != ' ' && s[j] != ',' && s[j] != '}' && s[j] != ']' -# j += 1 -# end -# anchor_name = s[i+1:j-1] -# val, i2 = parse_flow_value(ps, s, j) -# ps.anchors[anchor_name] = val -# return val, i2 -# elseif c == '*' -# j = i + 1 -# while j <= n && s[j] != ' ' && s[j] != ',' && s[j] != '}' && s[j] != ']' -# j += 1 -# end -# alias_name = s[i+1:j-1] -# val = get(ps.anchors, alias_name, YAMLNull) -# return val, j -# else -# # bare scalar — read until , } ] or end -# j = i -# while j <= n && s[j] != ',' && s[j] != '}' && s[j] != ']' && s[j] != '#' -# j += 1 -# end -# raw = strip(s[i:j-1]) -# return parse_scalar(raw), j -# end -# end - -# function parse_flow_sequence(ps::ParseState, s::AbstractString, i::Int)::Tuple{YAMLValue,Int} -# items = YAMLValue[] -# n = ncodeunits(s) -# while i <= n -# # skip whitespace -# while i <= n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n') -# i += 1 -# end -# i > n && break -# s[i] == ']' && (i += 1; break) -# s[i] == ',' && (i += 1; continue) - -# val, i = parse_flow_value(ps, s, i) -# push!(items, val) - -# # skip whitespace -# while i <= n && (s[i] == ' ' || s[i] == '\t') -# i += 1 -# end -# i <= n && s[i] == ',' && (i += 1) -# end -# return YAMLArray(items), i -# end - -# function parse_flow_mapping(ps::ParseState, s::AbstractString, i::Int)::Tuple{YAMLValue,Int} -# d = Dict{String,YAMLValue}() -# n = ncodeunits(s) -# while i <= n -# while i <= n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n') -# i += 1 -# end -# i > n && break -# s[i] == '}' && (i += 1; break) -# s[i] == ',' && (i += 1; continue) - -# # parse key -# key_val, i = parse_flow_value(ps, s, i) -# key = (key_val.tag === TAG_STRING ? key_val.sval : string_of(key_val)) - -# # skip whitespace and colon -# while i <= n && (s[i] == ' ' || s[i] == '\t') -# i += 1 -# end -# i <= n && s[i] == ':' && (i += 1) - -# # parse value -# val, i = parse_flow_value(ps, s, i) -# d[key] = val - -# while i <= n && (s[i] == ' ' || s[i] == '\t') -# i += 1 -# end -# i <= n && s[i] == ',' && (i += 1) -# end -# return YAMLDict(d), i -# end - -# function string_of(v::YAMLValue)::String -# v.tag === TAG_STRING && return v.sval -# v.tag === TAG_INT && return string(v.ival) -# v.tag === TAG_FLOAT && return string(v.fval) -# v.tag === TAG_BOOL && return v.bval ? "true" : "false" -# v.tag === TAG_NULL && return "null" -# return "???" -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Main recursive block parser -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# Parse a YAML node (mapping, sequence, or scalar) whose content lines all -# have indent ≥ `min_indent`. The caller must have already skipped blanks. - -# Returns a YAMLValue. -# """ -# function parse_node(ps::ParseState, min_indent::Int)::YAMLValue -# skip_empty!(ps) -# at_end(ps) && return YAMLNull - -# l = current_line(ps) -# indent = line_indent(l) -# indent < min_indent && return YAMLNull - -# content = lstrip(l) -# isempty(content) && return YAMLNull - -# # ── document separator ──────────────────────────────────────────────────── -# if startswith(content, "---") || startswith(content, "...") -# # Only advance past if it's a true separator (followed by space/EOF) -# rest = length(content) > 3 ? content[4:4] : " " -# if rest == " " || rest == "\n" || rest == "#" -# ps.lineno += 1 -# return parse_node(ps, min_indent) -# end -# end - -# # ── block sequence ──────────────────────────────────────────────────────── -# if content[1] == '-' && (length(content) == 1 || content[2] == ' ' || content[2] == '\n') -# return parse_block_sequence(ps, indent) -# end - -# # ── explicit key with '?' ───────────────────────────────────────────────── -# # Not commonly needed; skip complex mapping keys for now. - -# # ── mapping or scalar ───────────────────────────────────────────────────── -# # Try to detect a block mapping key on this line. -# key, colon_pos = find_mapping_key(content) -# if key !== nothing -# return parse_block_mapping(ps, indent) -# end - -# # ── scalar value ────────────────────────────────────────────────────────── -# return parse_line_scalar(ps, min_indent) -# end - -# """ -# Determine if `content` (stripped line) begins with a mapping key. -# Returns (key_string, position_after_colon) or (nothing, 0). -# A mapping key is: : … or "quoted": … or 'quoted': … -# """ -# function find_mapping_key(content::AbstractString) - -# isempty(content) && return nothing, 0 - -# i = firstindex(content) - -# while i <= lastindex(content) - -# if content[i] == ':' - -# ni = nextind(content,i) - -# if ni > lastindex(content) || -# content[ni] == ' ' || -# content[ni] == '\t' || -# content[ni] == '#' - -# key = rstrip(content[firstindex(content):prevind(content,i)]) - -# isempty(key) && return nothing, 0 - -# return key, ni -# end -# end - -# i = nextind(content,i) -# end - -# return nothing,0 -# end - -# """Parse a block mapping whose first key is on a line with indent `map_indent`.""" -# function parse_block_mapping(ps::ParseState, map_indent::Int)::YAMLValue -# d = Dict{String,YAMLValue}() - -# while !at_end(ps) -# skip_empty!(ps) -# at_end(ps) && break - -# l = current_line(ps) -# indent = line_indent(l) -# indent < map_indent && break # outdented → mapping ends -# indent > map_indent && break # shouldn't happen; bail - -# content = lstrip(l) -# isempty(content) && (ps.lineno += 1; continue) - -# # Stop at document markers -# (startswith(content, "---") || startswith(content, "...")) && break - -# # Must be a mapping key at this indent -# key, after_colon = find_mapping_key(content) -# key === nothing && break - -# ps.lineno += 1 # consume key line - -# # Check for anchor on the key line, after the colon -# anchor_name = nothing -# rest_of_line = after_colon <= ncodeunits(content) ? lstrip(content[after_colon:end]) : "" - -# # Strip trailing comment from rest_of_line for non-block scalars -# rest_stripped = strip_inline_comment(rstrip(rest_of_line)) - -# # # Determine value -# # val = if isempty(rest_stripped) || rest_stripped[1] == '#' -# # # Value is on the next line(s) — block node -# # next_ind = peek_indent(ps) -# # if next_ind > map_indent -# # parse_node(ps, next_ind) -# # else -# # YAMLNull -# # end -# # Determine value -# val = if isempty(rest_stripped) || rest_stripped[1] == '#' - -# # Value lives on subsequent line(s) -# next_ind = peek_indent(ps) - -# if next_ind < 0 -# # EOF -# YAMLNull - -# elseif next_ind > map_indent -# # Indented mapping/scalar -# parse_node(ps, next_ind) - -# elseif next_ind == map_indent -# # YAML allows -# # -# # key: -# # - item -# # -# # where the sequence indicator is aligned with the key. -# next_line = lstrip(current_line(ps)) - -# if !isempty(next_line) && -# next_line[1] == '-' && -# (length(next_line) == 1 || -# next_line[2] == ' ' || -# next_line[2] == '\t') - -# parse_block_sequence(ps, next_ind) -# else -# YAMLNull -# end - -# else -# YAMLNull -# end -# elseif rest_stripped[1] == '|' || rest_stripped[1] == '>' -# # Block scalar -# style = rest_stripped[1] -# header = length(rest_stripped) > 1 ? rest_stripped[2:end] : "" -# YAMLString(parse_block_scalar(ps, style, header)) -# elseif rest_stripped[1] == '{' -# v, _ = parse_flow_mapping(ps, rest_stripped, 2) -# v -# elseif rest_stripped[1] == '[' -# v, _ = parse_flow_sequence(ps, rest_stripped, 2) -# v -# elseif rest_stripped[1] == '&' -# # Anchor reference -# j = 2 -# n2 = ncodeunits(rest_stripped) -# while j <= n2 && rest_stripped[j] != ' ' -# j += 1 -# end -# anchor_name2 = rest_stripped[2:j-1] -# inner = lstrip(rest_stripped[j:end]) -# v = if isempty(inner) || inner[1] == '#' -# next_ind = peek_indent(ps) -# next_ind > map_indent ? parse_node(ps, next_ind) : YAMLNull -# else -# parse_scalar(strip_inline_comment(inner)) -# end -# ps.anchors[anchor_name2] = v -# v -# elseif rest_stripped[1] == '*' -# # Alias -# j = 2 -# n2 = ncodeunits(rest_stripped) -# while j <= n2 && rest_stripped[j] != ' ' && rest_stripped[j] != '#' -# j += 1 -# end -# alias_name = rest_stripped[2:j-1] -# get(ps.anchors, alias_name, YAMLNull) -# else -# parse_scalar(rest_stripped) -# end - -# d[key] = val -# end - -# return YAMLDict(d) -# end - -# """Parse a block sequence whose '-' indicators are at indent `seq_indent`.""" -# function parse_block_sequence(ps::ParseState, seq_indent::Int)::YAMLValue -# items = YAMLValue[] - -# while !at_end(ps) -# skip_empty!(ps) -# at_end(ps) && break - -# l = current_line(ps) -# indent = line_indent(l) -# indent < seq_indent && break -# indent > seq_indent && break # continuation handled inside - -# content = lstrip(l) -# isempty(content) && (ps.lineno += 1; continue) -# content[1] != '-' && break # not a sequence item - -# # Ensure it's '- ' or '-\n' or just '-' at end -# if length(content) >= 2 && content[2] != ' ' && content[2] != '\t' -# break -# end - -# ps.lineno += 1 # consume the '-' line - -# # The item content can be inline (after '-') or on subsequent lines -# inline = length(content) > 1 ? lstrip(content[2:end]) : "" -# inline_stripped = strip_inline_comment(rstrip(inline)) - -# val = if isempty(inline_stripped) || inline_stripped[1] == '#' -# # Item value is indented below -# next_ind = peek_indent(ps) -# if next_ind > seq_indent -# parse_node(ps, next_ind) -# else -# YAMLNull -# end -# elseif inline_stripped[1] == '|' || inline_stripped[1] == '>' -# style = inline_stripped[1] -# header = length(inline_stripped) > 1 ? inline_stripped[2:end] : "" -# YAMLString(parse_block_scalar(ps, style, header)) -# elseif inline_stripped[1] == '{' -# v, _ = parse_flow_mapping(ps, inline_stripped, 2) -# v -# elseif inline_stripped[1] == '[' -# v, _ = parse_flow_sequence(ps, inline_stripped, 2) -# v -# elseif inline_stripped[1] == '-' && (length(inline_stripped) == 1 || inline_stripped[2] == ' ') -# # Nested sequence inline with the '-' -# # Re-inject into lines at appropriate indent -# parse_block_sequence(ps, seq_indent + 2) -# else -# # check if it's a mapping key -# key, _ = find_mapping_key(inline_stripped) -# if key !== nothing -# # inline mapping: treat the current rest as a new line and parse -# # We push a virtual line back -# new_indent = seq_indent + 2 -# # fake_line = " "^new_indent * inline_stripped -# buf = IOBuffer() -# for i in 1:new_indent -# write(buf, ' ') -# end -# write(buf, inline_stripped) -# fake_line = String(take!(buf)) -# fake_line = fake_line * inline_stripped -# insert!(ps.lines, ps.lineno, fake_line) -# parse_node(ps, new_indent) -# else -# parse_scalar(inline_stripped) -# end -# end - -# push!(items, val) -# end - -# return YAMLArray(items) -# end - -# """ -# Parse a scalar that may span multiple lines (plain multi-line scalar). -# Continuation lines must be more indented than `min_indent`. -# """ -# function parse_line_scalar(ps::ParseState, min_indent::Int)::YAMLValue -# skip_empty!(ps) -# at_end(ps) && return YAMLNull - -# l = current_line(ps) -# indent = line_indent(l) -# indent < min_indent && return YAMLNull - -# content = lstrip(l) - -# # Anchor or alias on a standalone scalar line -# if !isempty(content) && content[1] == '&' -# ps.lineno += 1 -# j = 2 -# n = ncodeunits(content) -# while j <= n && content[j] != ' ' -# j += 1 -# end -# anchor_name = content[2:j-1] -# inner = j <= n ? lstrip(content[j:end]) : "" -# val = isempty(inner) ? parse_node(ps, min_indent) : parse_scalar(strip_inline_comment(rstrip(inner))) -# ps.anchors[anchor_name] = val -# return val -# elseif !isempty(content) && content[1] == '*' -# ps.lineno += 1 -# j = 2 -# n = ncodeunits(content) -# while j <= n && content[j] != ' ' && content[j] != '#' -# j += 1 -# end -# alias_name = content[2:j-1] -# return get(ps.anchors, alias_name, YAMLNull) -# end - -# # Quoted scalar -# if !isempty(content) -# if content[1] == '"' -# ps.lineno += 1 -# str, _ = parse_double_quoted(content, 2) -# return YAMLString(str) -# elseif content[1] == '\'' -# ps.lineno += 1 -# str, _ = parse_single_quoted(content, 2) -# return YAMLString(str) -# elseif content[1] == '|' || content[1] == '>' -# style = content[1] -# header = length(content) > 1 ? content[2:end] : "" -# ps.lineno += 1 # consume the | / > header line -# return YAMLString(parse_block_scalar(ps, style, header)) -# elseif content[1] == '{' -# ps.lineno += 1 -# v, _ = parse_flow_mapping(ps, content, 2) -# return v -# elseif content[1] == '[' -# ps.lineno += 1 -# v, _ = parse_flow_sequence(ps, content, 2) -# return v -# end -# end - -# # Plain multi-line scalar: first line -# ps.lineno += 1 -# parts = [strip_inline_comment(rstrip(content))] - -# # Continuation lines (more indented, not a key, not a sequence item) -# while !at_end(ps) -# skip_empty!(ps) -# at_end(ps) && break -# l2 = current_line(ps) -# ind2 = line_indent(l2) -# ind2 <= min_indent && break -# c2 = lstrip(l2) -# isempty(c2) && break -# c2[1] == '#' && break -# # Don't consume what looks like a new key or sequence -# k2, _ = find_mapping_key(c2) -# k2 !== nothing && break -# (c2[1] == '-' && (length(c2) == 1 || c2[2] == ' ')) && break - -# push!(parts, strip_inline_comment(rstrip(c2))) -# ps.lineno += 1 -# end - -# raw = join(parts, " ") -# return parse_scalar(raw) -# end - -# # ────────────────────────────────────────────────────────────────────────────── -# # Public API -# # ────────────────────────────────────────────────────────────────────────────── - -# """ -# loads(yaml::String) -> YAMLValue - -# Parse a YAML string and return a `YAMLValue` tree. -# """ -# function loads(yaml::String)::YAMLValue -# ps = ParseState(yaml) -# skip_empty!(ps) -# at_end(ps) && return YAMLNull -# return parse_node(ps, 0) -# end - -# """ -# load(path::String) -> YAMLValue - -# Read a YAML file from `path` and return a `YAMLValue` tree. -# """ -# function load(path::String)::YAMLValue -# src = read(path, String) -# return loads(src) -# end - -# struct YAMLInput -# data::Dict{String, Any} -# end - -# function get_value(d::Dict{String, YAMLValue}, key, ::Type{Array}) -# return as_array(d[key]) -# end - -# function get_value(d::Dict{String, YAMLValue}, key, ::Type{<:Dict}) -# return as_dict(d[key]) -# end - -# function get_value(d::Dict{String, YAMLValue}, key, ::Type{Float64}) -# return as_float(d[key]) -# end - -# function get_value(d::Dict{String, YAMLValue}, key, ::Type{Int}) -# return as_int(d[key]) -# end - -# function get_value(d::Dict{String, YAMLValue}, key, ::Type{String}) -# return as_string(d[key]) -# end - -# function to_dict(v::YAMLValue) -# if v.tag === TAG_NULL -# return nothing -# elseif v.tag === TAG_BOOL -# return v.bval -# elseif v.tag === TAG_INT -# return v.ival -# elseif v.tag === TAG_FLOAT -# return v.fval -# elseif v.tag === TAG_STRING -# return v.sval -# elseif v.tag === TAG_ARRAY -# vals = Any[] -# for v in v.arr -# push!(vals, to_dict(v)) -# end -# elseif v.tag === TAG_DICT -# d = Dict{String,Any}() -# for (k,val) in v.dict -# d[k] = to_dict(val) -# end -# return d -# else -# error("Unknown YAML tag") -# end -# end - -# function to_dict(d::Dict{String, YAMLValue}) -# new_d = Dict{String, Any}() -# for (k, v) in pairs(d) -# new_d[k] = to_dict(v) -# end -# return new_d -# end - -# end # module SimpleYAML -module SimpleYAML - -export YAMLValue, YAMLNull, YAMLBool, YAMLInt, YAMLFloat, YAMLString, - YAMLArray, YAMLDict, - load, loads, - as_dict, as_array, as_string, as_int, as_float, as_bool, is_null, - get_value, to_dict - -# ───────────────────────────────────────────────────────────────────────────── -# Value type (concrete tagged union — trim-safe, fully type-stable) -# ───────────────────────────────────────────────────────────────────────────── - -@enum YAMLTag begin - TAG_NULL; TAG_BOOL; TAG_INT; TAG_FLOAT; TAG_STRING; TAG_ARRAY; TAG_DICT -end - -struct YAMLValue - tag :: YAMLTag - bval :: Bool - ival :: Int64 - fval :: Float64 - sval :: String - arr :: Vector{YAMLValue} - dict :: Dict{String, YAMLValue} -end - -const YAMLNull = YAMLValue(TAG_NULL, false, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) -YAMLBool(b::Bool) = YAMLValue(TAG_BOOL, b, 0, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) -YAMLInt(i::Int64) = YAMLValue(TAG_INT, false, i, 0.0, "", YAMLValue[], Dict{String,YAMLValue}()) -YAMLFloat(f::Float64) = YAMLValue(TAG_FLOAT, false, 0, f, "", YAMLValue[], Dict{String,YAMLValue}()) -YAMLString(s::String) = YAMLValue(TAG_STRING,false, 0, 0.0, s, YAMLValue[], Dict{String,YAMLValue}()) -YAMLArray(a::Vector{YAMLValue}) = YAMLValue(TAG_ARRAY, false, 0, 0.0, "", a, Dict{String,YAMLValue}()) -YAMLDict(d::Dict{String,YAMLValue}) = YAMLValue(TAG_DICT, false, 0, 0.0, "", YAMLValue[], d) - -is_null(v::YAMLValue) = v.tag === TAG_NULL - -function as_bool(v::YAMLValue)::Bool - v.tag === TAG_BOOL || error("YAMLValue is not a bool (tag=$(v.tag))"); v.bval -end -function as_int(v::YAMLValue)::Int64 - v.tag === TAG_INT || error("YAMLValue is not an int (tag=$(v.tag))"); v.ival -end -function as_float(v::YAMLValue)::Float64 - v.tag === TAG_FLOAT || error("YAMLValue is not a float (tag=$(v.tag))"); v.fval -end -function as_string(v::YAMLValue)::String - v.tag === TAG_STRING || error("YAMLValue is not a string (tag=$(v.tag))"); v.sval -end -function as_array(v::YAMLValue)::Vector{YAMLValue} - v.tag === TAG_ARRAY || error("YAMLValue is not an array (tag=$(v.tag))"); v.arr -end -function as_dict(v::YAMLValue)::Dict{String,YAMLValue} - v.tag === TAG_DICT || error("YAMLValue is not a dict (tag=$(v.tag))"); v.dict -end - -function Base.show(io::IO, v::YAMLValue) - if v.tag === TAG_NULL; print(io, "null") - elseif v.tag === TAG_BOOL; print(io, v.bval ? "true" : "false") - elseif v.tag === TAG_INT; print(io, v.ival) - elseif v.tag === TAG_FLOAT; print(io, v.fval) - elseif v.tag === TAG_STRING; print(io, repr(v.sval)) - elseif v.tag === TAG_ARRAY - print(io, "[") - for (i, x) in enumerate(v.arr); i > 1 && print(io, ", "); show(io, x); end - print(io, "]") - else - print(io, "{") - first = true - for (k, val) in v.dict - first || print(io, ", "); first = false - print(io, repr(k), ": "); show(io, val) - end - print(io, "}") - end -end - -# ───────────────────────────────────────────────────────────────────────────── -# Parser state -# ───────────────────────────────────────────────────────────────────────────── - -mutable struct ParseState - lines :: Vector{String} - lineno :: Int - anchors :: Dict{String, YAMLValue} -end - -function ParseState(src::String)::ParseState - # split() returns SubStrings — convert immediately so all downstream - # functions only ever handle concrete String. - raw = split(src, '\n') - lines = Vector{String}(undef, length(raw)) - for i in eachindex(raw); lines[i] = String(raw[i]); end - return ParseState(lines, 1, Dict{String,YAMLValue}()) -end - -@inline at_end(ps::ParseState) = ps.lineno > length(ps.lines) -@inline current_line(ps::ParseState) = ps.lineno <= length(ps.lines) ? ps.lines[ps.lineno] : "" - -# ───────────────────────────────────────────────────────────────────────────── -# String helpers — ALL use only codeunit / nextind (never s[byte_index]). -# -# Design rule: the only place character indexing s[i] is allowed is when i -# comes from firstindex(s) or nextind(s, prev) — i.e. a proper character walk. -# Every other access uses codeunit(s, i) for byte inspection or byte-range -# slices s[a:b] where a/b are validated character boundaries. -# ───────────────────────────────────────────────────────────────────────────── - -"""Count leading ASCII space bytes.""" -function line_indent(l::String)::Int - n = ncodeunits(l); i = 1 - while i <= n && codeunit(l, i) == UInt8(' '); i += 1; end - return i - 1 -end - -""" -Return (indent::Int, content::String) for a raw line. -content is the line with leading spaces removed, as a proper String. -""" -function split_indent(l::String)::Tuple{Int,String} - n = ncodeunits(l) - ind = line_indent(l) - # ind is a count of ASCII space bytes, so ind+1 is always a valid char start. - return ind, ind < n ? l[ind+1:end] : "" -end - -"""Find the indent of the next non-blank non-comment line without advancing.""" -function peek_indent(ps::ParseState)::Int - i = ps.lineno - while i <= length(ps.lines) - ind, rest = split_indent(ps.lines[i]) - if !isempty(rest) && codeunit(rest, 1) != UInt8('#') - return ind - end - i += 1 - end - return -1 -end - -function skip_empty!(ps::ParseState) - while !at_end(ps) - _, rest = split_indent(current_line(ps)) - if isempty(rest) || codeunit(rest, 1) == UInt8('#') - ps.lineno += 1 - else - break - end - end -end - -""" -Strip trailing inline comment (' # ...') and trailing whitespace. -Always returns a plain String; uses only byte-level ops. -""" -function strip_inline_comment(s::String)::String - n = ncodeunits(s) - # Need at least 2 bytes: char before '#' + '#' itself - i = 2 - while i <= n - if codeunit(s, i) == UInt8('#') && codeunit(s, i-1) == UInt8(' ') - j = i - 1 # byte just before the space-hash - while j >= 1 && (codeunit(s, j) == UInt8(' ') || codeunit(s, j) == UInt8('\t')) - j -= 1 - end - return j >= 1 ? s[1:j] : "" - end - i += 1 - end - # no comment — rstrip trailing whitespace - j = n - while j >= 1 && (codeunit(s, j) == UInt8(' ') || codeunit(s, j) == UInt8('\t') || - codeunit(s, j) == UInt8('\r') || codeunit(s, j) == UInt8('\n')) - j -= 1 - end - return j >= 1 ? (j == n ? s : s[1:j]) : "" -end - -""" -lstrip ASCII whitespace, returning a plain String. -Only advances through single-byte ASCII space/tab characters, so the -first non-whitespace byte is always a valid character boundary. -""" -function lstrip_ascii(s::String)::String - n = ncodeunits(s); i = 1 - while i <= n && (codeunit(s, i) == UInt8(' ') || codeunit(s, i) == UInt8('\t')) - i += 1 - end - return i <= n ? s[i:end] : "" -end - -""" -Return the suffix of s starting one character after byte-position `byte_pos`. -`byte_pos` must point to the start of a character (i.e. be a valid index). -Uses nextind to step one character, then slices to end. -""" -function suffix_after_char(s::String, byte_pos::Int)::String - n = ncodeunits(s) - ni = nextind(s, byte_pos) - return ni <= n ? s[ni:end] : "" -end - -# ───────────────────────────────────────────────────────────────────────────── -# Scalar coercion -# ───────────────────────────────────────────────────────────────────────────── - -function parse_scalar(s::String)::YAMLValue - isempty(s) && return YAMLNull - (s == "null" || s == "Null" || s == "NULL" || s == "~") && return YAMLNull - (s == "true" || s == "True" || s == "TRUE") && return YAMLBool(true) - (s == "false" || s == "False" || s == "FALSE") && return YAMLBool(false) - (s == ".inf" || s == ".Inf" || s == ".INF" || - s == "+.inf" || s == "+.Inf" || s == "+.INF") && return YAMLFloat(Inf) - (s == "-.inf" || s == "-.Inf" || s == "-.INF") && return YAMLFloat(-Inf) - (s == ".nan" || s == ".NaN" || s == ".NAN") && return YAMLFloat(NaN) - - n = ncodeunits(s) - if n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('x') - v = tryparse(Int64, s[3:end]; base=16); v !== nothing && return YAMLInt(v) - elseif n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('o') - v = tryparse(Int64, s[3:end]; base=8); v !== nothing && return YAMLInt(v) - elseif n >= 3 && codeunit(s,1) == UInt8('0') && codeunit(s,2) == UInt8('b') - v = tryparse(Int64, s[3:end]; base=2); v !== nothing && return YAMLInt(v) - else - v = tryparse(Int64, s); v !== nothing && return YAMLInt(v) - end - fv = tryparse(Float64, s); fv !== nothing && return YAMLFloat(fv) - return YAMLString(s) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Quoted-string parsers -# Take a String + byte start index (just after opening quote). -# Return (parsed String, byte index after closing quote). -# Use proper character walks (nextind), so Unicode-safe throughout. -# ───────────────────────────────────────────────────────────────────────────── - -function parse_single_quoted(s::String, start::Int)::Tuple{String,Int} - buf = IOBuffer(); i = start; n = ncodeunits(s) - while i <= n - # Walk one character at a time using nextind - ci = i - i = nextind(s, ci) # advance past current char - ch = s[ci] # safe: ci from nextind chain - if ch == '\'' - if i <= n && codeunit(s, i) == UInt8('\'') - write(buf, '\''); i = nextind(s, i) - else - break # closing quote; i already past it - end - else - write(buf, ch) - end - end - return String(take!(buf)), i -end - -function parse_double_quoted(s::String, start::Int)::Tuple{String,Int} - buf = IOBuffer(); i = start; n = ncodeunits(s) - while i <= n - ci = i; i = nextind(s, ci); ch = s[ci] - if ch == '"' - break - elseif ch == '\\' - i > n && break - ei = i; i = nextind(s, ei); esc = s[ei] - if esc == '0'; write(buf, '\0') - elseif esc == 'a'; write(buf, '\a') - elseif esc == 'b'; write(buf, '\b') - elseif esc == 't' || esc == '\t'; write(buf, '\t') - elseif esc == 'n'; write(buf, '\n') - elseif esc == 'v'; write(buf, '\v') - elseif esc == 'f'; write(buf, '\f') - elseif esc == 'r'; write(buf, '\r') - elseif esc == 'e'; write(buf, '\e') - elseif esc == ' '; write(buf, ' ') - elseif esc == '"'; write(buf, '"') - elseif esc == '/'; write(buf, '/') - elseif esc == '\\'; write(buf, '\\') - elseif esc == 'N'; write(buf, '\u0085') - elseif esc == '_'; write(buf, '\u00a0') - elseif esc == 'L'; write(buf, '\u2028') - elseif esc == 'P'; write(buf, '\u2029') - elseif esc == 'x' - hex = i+1 <= n ? s[i:i+1] : "00" - cp = tryparse(UInt32, hex; base=16) - write(buf, cp !== nothing ? Char(cp) : '?') - i <= n && (i = nextind(s, i)) - i <= n && (i = nextind(s, i)) - elseif esc == 'u' - hex = i+3 <= n ? s[i:i+3] : "0000" - cp = tryparse(UInt32, hex; base=16) - write(buf, cp !== nothing ? Char(cp) : '?') - for _ in 1:4; i <= n && (i = nextind(s, i)); end - elseif esc == 'U' - hex = i+7 <= n ? s[i:i+7] : "00000000" - cp = tryparse(UInt32, hex; base=16) - write(buf, cp !== nothing ? Char(cp) : '?') - for _ in 1:8; i <= n && (i = nextind(s, i)); end - else - write(buf, esc) - end - else - write(buf, ch) - end - end - return String(take!(buf)), i -end - -# ───────────────────────────────────────────────────────────────────────────── -# Block scalar (| and >) -# Caller has already consumed the header line; ps.lineno → first content line. -# ───────────────────────────────────────────────────────────────────────────── - -function parse_block_scalar(ps::ParseState, style::Char, header::String)::String - chomping = :clip; explicit_indent = 0 - h = header - # strip trailing comment - hi = firstindex(h) - while hi <= lastindex(h) - if codeunit(h, hi) == UInt8('#') && hi > 1 && codeunit(h, hi-1) == UInt8(' ') - h = String(rstrip(h[1:hi-2])); break - end - hi = nextind(h, hi) - end - h = String(strip(h)) - # parse chomping / explicit-indent chars (all ASCII, safe with nextind walk) - hi = firstindex(h) - while hi <= lastindex(h) - ch = h[hi] # safe: hi from nextind chain starting at firstindex - if ch == '-'; chomping = :strip; hi = nextind(h, hi) - elseif ch == '+'; chomping = :keep; hi = nextind(h, hi) - elseif isdigit(ch); explicit_indent = Int(ch - '0'); hi = nextind(h, hi) - else; break - end - end - - block_indent = explicit_indent - if block_indent == 0 - j = ps.lineno - while j <= length(ps.lines) - ind, rest = split_indent(ps.lines[j]) - if !isempty(rest); block_indent = ind; break; end - j += 1 - end - end - - lines_out = String[] - while !at_end(ps) - l = current_line(ps) - ind, rest = split_indent(l) - if isempty(rest) - push!(lines_out, ""); ps.lineno += 1; continue - end - ind < block_indent && break - push!(lines_out, l[block_indent+1:end]); ps.lineno += 1 - end - - result::String = if style == '|' - join(lines_out, "\n") - else - buf = IOBuffer(); prev_empty = false - for (idx, ln) in enumerate(lines_out) - if isempty(ln); prev_empty = true - else - if idx > 1 && !prev_empty; write(buf, ' ') - elseif prev_empty; write(buf, '\n') - end - write(buf, ln); prev_empty = false - end - end - String(take!(buf)) - end - - if chomping === :strip; result = String(rstrip(result, '\n')) - elseif chomping === :clip; result = String(rstrip(result, '\n')) * "\n" - end - return result -end - -# ───────────────────────────────────────────────────────────────────────────── -# Flow (inline) parsers -# All use codeunit for structural character checks and nextind for walking. -# ───────────────────────────────────────────────────────────────────────────── - -function parse_flow_value(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} - n = ncodeunits(s) - while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')) - i += 1 - end - i > n && return YAMLNull, i - - # Peek at first byte — all YAML flow structural characters are single-byte ASCII. - b = codeunit(s, i) - if b == UInt8('{') - return parse_flow_mapping(ps, s, i+1) - elseif b == UInt8('[') - return parse_flow_sequence(ps, s, i+1) - elseif b == UInt8('\'') - str, i2 = parse_single_quoted(s, i+1) - return YAMLString(str), i2 - elseif b == UInt8('"') - str, i2 = parse_double_quoted(s, i+1) - return YAMLString(str), i2 - elseif b == UInt8('&') - j = i + 1 - while j <= n && codeunit(s,j) != UInt8(' ') && codeunit(s,j) != UInt8(',') && - codeunit(s,j) != UInt8('}') && codeunit(s,j) != UInt8(']') - j += 1 - end - aname = s[i+1:j-1] # anchor names are ASCII by convention - val, i2 = parse_flow_value(ps, s, j) - ps.anchors[aname] = val - return val, i2 - elseif b == UInt8('*') - j = i + 1 - while j <= n && codeunit(s,j) != UInt8(' ') && codeunit(s,j) != UInt8(',') && - codeunit(s,j) != UInt8('}') && codeunit(s,j) != UInt8(']') - j += 1 - end - return get(ps.anchors, s[i+1:j-1], YAMLNull), j - else - j = i - while j <= n - bj = codeunit(s, j) - (bj == UInt8(',') || bj == UInt8('}') || bj == UInt8(']') || bj == UInt8('#')) && break - j += 1 - end - raw = String(strip(s[i:j-1])) - return parse_scalar(raw), j - end -end - -function parse_flow_sequence(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} - items = YAMLValue[]; n = ncodeunits(s) - while i <= n - while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t') || - codeunit(s,i) == UInt8('\n')) - i += 1 - end - i > n && break - codeunit(s,i) == UInt8(']') && (i += 1; break) - codeunit(s,i) == UInt8(',') && (i += 1; continue) - val, i = parse_flow_value(ps, s, i) - push!(items, val) - while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end - i <= n && codeunit(s,i) == UInt8(',') && (i += 1) - end - return YAMLArray(items), i -end - -function parse_flow_mapping(ps::ParseState, s::String, i::Int)::Tuple{YAMLValue,Int} - d = Dict{String,YAMLValue}(); n = ncodeunits(s) - while i <= n - while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t') || - codeunit(s,i) == UInt8('\n')) - i += 1 - end - i > n && break - codeunit(s,i) == UInt8('}') && (i += 1; break) - codeunit(s,i) == UInt8(',') && (i += 1; continue) - key_val, i = parse_flow_value(ps, s, i) - key = yaml_value_to_key(key_val) - while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end - i <= n && codeunit(s,i) == UInt8(':') && (i += 1) - val, i = parse_flow_value(ps, s, i) - d[key] = val - while i <= n && (codeunit(s,i) == UInt8(' ') || codeunit(s,i) == UInt8('\t')); i += 1; end - i <= n && codeunit(s,i) == UInt8(',') && (i += 1) - end - return YAMLDict(d), i -end - -function yaml_value_to_key(v::YAMLValue)::String - v.tag === TAG_STRING && return v.sval - v.tag === TAG_INT && return string(v.ival) - v.tag === TAG_FLOAT && return string(v.fval) - v.tag === TAG_BOOL && return v.bval ? "true" : "false" - return "null" -end - -# ───────────────────────────────────────────────────────────────────────────── -# Key detection — fully Unicode-safe via character walks -# -# Returns (key::String, after_colon_byte_index::Int) or (nothing, 0). -# after_colon_byte_index points one past the ':' byte (i.e. the start of the -# value portion); it is always a valid character boundary because ':' is ASCII -# and the next byte starts a new character. -# ───────────────────────────────────────────────────────────────────────────── - -function find_mapping_key(content::String)::Tuple{Union{Nothing,String},Int} - isempty(content) && return nothing, 0 - n = ncodeunits(content) - b1 = codeunit(content, 1) - - if b1 == UInt8('"') - key, i = parse_double_quoted(content, 2) - # skip spaces (all ASCII, so += 1 safe) - while i <= n && codeunit(content, i) == UInt8(' '); i += 1; end - if i <= n && codeunit(content, i) == UInt8(':') - ni = i + 1 # safe: ':' is 1 byte, next byte starts a new char - if ni > n || codeunit(content, ni) == UInt8(' ') || - codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') - return key, ni - end - end - return nothing, 0 - - elseif b1 == UInt8('\'') - key, i = parse_single_quoted(content, 2) - while i <= n && codeunit(content, i) == UInt8(' '); i += 1; end - if i <= n && codeunit(content, i) == UInt8(':') - ni = i + 1 - if ni > n || codeunit(content, ni) == UInt8(' ') || - codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') - return key, ni - end - end - return nothing, 0 - - else - # Bare key: walk character by character using nextind. - # We are looking for a ':' byte followed by space/tab/EOF/comment. - # ':' is ASCII so codeunit comparison is correct; nextind handles - # multi-byte characters safely. - i = firstindex(content) # always 1, but semantically correct - while i <= lastindex(content) - if codeunit(content, i) == UInt8(':') - ni = i + 1 # ':' is ASCII (1 byte), so ni is a valid index - if ni > n || codeunit(content, ni) == UInt8(' ') || - codeunit(content, ni) == UInt8('\t') || codeunit(content, ni) == UInt8('#') - # Collect key as content[1 .. prevind(content, i)] - # rstrip ASCII whitespace from the right - ke = i - 1 # byte before ':' - while ke >= 1 && (codeunit(content, ke) == UInt8(' ') || - codeunit(content, ke) == UInt8('\t')) - ke -= 1 - end - ke < 1 && return nothing, 0 - # content[1:ke] is a valid byte-range slice: ke is either - # the last byte of a multi-byte char (fine for range end) - # or a single-byte char. Julia range slices are byte-based - # and only require the START to be a valid char boundary; - # the end can be any byte position >= the last byte of a char. - return content[1:ke], ni - end - end - i = nextind(content, i) - end - return nothing, 0 - end -end - -# ───────────────────────────────────────────────────────────────────────────── -# Block mapping -# ───────────────────────────────────────────────────────────────────────────── - -function parse_block_mapping(ps::ParseState, map_indent::Int)::YAMLValue - d = Dict{String,YAMLValue}() - while !at_end(ps) - skip_empty!(ps); at_end(ps) && break - l = current_line(ps) - indent, content = split_indent(l) - indent != map_indent && break - isempty(content) && (ps.lineno += 1; continue) - (startswith(content, "---") || startswith(content, "...")) && break - - key, after_colon = find_mapping_key(content) - key === nothing && break - ps.lineno += 1 # consume key line - - # Value portion on the same line: content[after_colon:end], lstripped. - # after_colon is always a valid character boundary (one past ASCII ':'). - rest = after_colon <= n_cu(content) ? lstrip_ascii(content[after_colon:end]) : "" - rest = strip_inline_comment(rest) - - val::YAMLValue = _parse_value_after_colon(ps, rest, map_indent) - d[key] = val - end - return YAMLDict(d) -end - -# ncodeunits shorthand (avoids repeating the call name) -@inline n_cu(s::String) = ncodeunits(s) - -""" -Determine the value given the stripped inline remainder after a mapping colon -(or after a sequence dash), plus the current block's indent level. -""" -function _parse_value_after_colon(ps::ParseState, rest::String, block_indent::Int)::YAMLValue - if isempty(rest) || codeunit(rest, 1) == UInt8('#') - # value on subsequent line(s) - next_ind = peek_indent(ps) - if next_ind < 0 - return YAMLNull - elseif next_ind > block_indent - return parse_node(ps, next_ind) - elseif next_ind == block_indent - _, nc = split_indent(current_line(ps)) - if !isempty(nc) && codeunit(nc, 1) == UInt8('-') && - (n_cu(nc) == 1 || codeunit(nc, 2) == UInt8(' ') || codeunit(nc, 2) == UInt8('\t')) - return parse_block_sequence(ps, next_ind) - end - end - return YAMLNull - - elseif codeunit(rest, 1) == UInt8('|') || codeunit(rest, 1) == UInt8('>') - style = Char(codeunit(rest, 1)) - hdr = n_cu(rest) > 1 ? suffix_after_char(rest, 1) : "" - ps.lineno += 1 # consume block-scalar header (current line already consumed by caller) - return YAMLString(parse_block_scalar(ps, style, hdr)) - - elseif codeunit(rest, 1) == UInt8('{') - v, _ = parse_flow_mapping(ps, rest, 2); return v - - elseif codeunit(rest, 1) == UInt8('[') - v, _ = parse_flow_sequence(ps, rest, 2); return v - - elseif codeunit(rest, 1) == UInt8('&') - # &anchorname - j = 2 - while j <= n_cu(rest) && codeunit(rest,j) != UInt8(' ') && codeunit(rest,j) != UInt8('\t') - j += 1 - end - aname = rest[2:j-1] # anchor names are plain ASCII - inner = lstrip_ascii(j <= n_cu(rest) ? rest[j:end] : "") - v = if isempty(inner) || codeunit(inner, 1) == UInt8('#') - ni = peek_indent(ps) - ni > block_indent ? parse_node(ps, ni) : YAMLNull - else - parse_scalar(strip_inline_comment(inner)) - end - ps.anchors[aname] = v; return v - - elseif codeunit(rest, 1) == UInt8('*') - j = 2 - while j <= n_cu(rest) && codeunit(rest,j) != UInt8(' ') && codeunit(rest,j) != UInt8('#') - j += 1 - end - return get(ps.anchors, rest[2:j-1], YAMLNull) - - else - return parse_scalar(rest) - end -end - -# ───────────────────────────────────────────────────────────────────────────── -# Block sequence -# ───────────────────────────────────────────────────────────────────────────── - -function parse_block_sequence(ps::ParseState, seq_indent::Int)::YAMLValue - items = YAMLValue[] - while !at_end(ps) - skip_empty!(ps); at_end(ps) && break - l = current_line(ps) - indent, content = split_indent(l) - indent != seq_indent && break - isempty(content) && (ps.lineno += 1; continue) - codeunit(content, 1) != UInt8('-') && break - if n_cu(content) >= 2 - b2 = codeunit(content, 2) - b2 != UInt8(' ') && b2 != UInt8('\t') && break - end - - ps.lineno += 1 # consume '-' line - - # Content after '- ' (suffix_after_char handles multi-byte-safe skip of '-') - inline = lstrip_ascii(n_cu(content) > 1 ? suffix_after_char(content, 1) : "") - inline = strip_inline_comment(inline) - - val::YAMLValue = if isempty(inline) || codeunit(inline, 1) == UInt8('#') - next_ind = peek_indent(ps) - next_ind > seq_indent ? parse_node(ps, next_ind) : YAMLNull - - elseif codeunit(inline, 1) == UInt8('-') && - (n_cu(inline) == 1 || codeunit(inline, 2) == UInt8(' ')) - fake = " "^(seq_indent + 2) * inline - insert!(ps.lines, ps.lineno, fake) - parse_block_sequence(ps, seq_indent + 2) - - else - # Check for inline mapping key first, then delegate to _parse_value_after_colon - k, _ = find_mapping_key(inline) - if k !== nothing - new_ind = seq_indent + 2 - fake_line = " "^new_ind * inline - insert!(ps.lines, ps.lineno, fake_line) - parse_node(ps, new_ind) - else - _parse_value_after_colon(ps, inline, seq_indent) - end - end - - push!(items, val) - end - return YAMLArray(items) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Node dispatcher -# ───────────────────────────────────────────────────────────────────────────── - -function parse_node(ps::ParseState, min_indent::Int)::YAMLValue - skip_empty!(ps); at_end(ps) && return YAMLNull - l = current_line(ps) - indent, content = split_indent(l) - indent < min_indent && return YAMLNull - isempty(content) && return YAMLNull - - if startswith(content, "---") || startswith(content, "...") - nc = n_cu(content) - ok = nc == 3 || codeunit(content,4) == UInt8(' ') || - codeunit(content,4) == UInt8('\t') || codeunit(content,4) == UInt8('#') - if ok; ps.lineno += 1; return parse_node(ps, min_indent); end - end - - b1 = codeunit(content, 1) - if b1 == UInt8('-') && (n_cu(content) == 1 || codeunit(content,2) == UInt8(' ') || - codeunit(content,2) == UInt8('\t')) - return parse_block_sequence(ps, indent) - end - - key, _ = find_mapping_key(content) - key !== nothing && return parse_block_mapping(ps, indent) - - return parse_line_scalar(ps, min_indent) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Scalar line parser -# ───────────────────────────────────────────────────────────────────────────── - -function parse_line_scalar(ps::ParseState, min_indent::Int)::YAMLValue - skip_empty!(ps); at_end(ps) && return YAMLNull - l = current_line(ps) - indent, content = split_indent(l) - indent < min_indent && return YAMLNull - isempty(content) && return YAMLNull - - b1 = codeunit(content, 1) - - if b1 == UInt8('&') - ps.lineno += 1 - j = 2; nn = n_cu(content) - while j <= nn && codeunit(content,j) != UInt8(' ') && codeunit(content,j) != UInt8('\t') - j += 1 - end - aname = content[2:j-1] - inner = lstrip_ascii(j <= nn ? content[j:end] : "") - val = isempty(inner) || codeunit(inner,1) == UInt8('#') ? - parse_node(ps, min_indent) : - parse_scalar(strip_inline_comment(inner)) - ps.anchors[aname] = val; return val - end - - if b1 == UInt8('*') - ps.lineno += 1; j = 2; nn = n_cu(content) - while j <= nn && codeunit(content,j) != UInt8(' ') && codeunit(content,j) != UInt8('#') - j += 1 - end - return get(ps.anchors, content[2:j-1], YAMLNull) - end - - if b1 == UInt8('"') - ps.lineno += 1; str, _ = parse_double_quoted(content, 2); return YAMLString(str) - end - if b1 == UInt8('\'') - ps.lineno += 1; str, _ = parse_single_quoted(content, 2); return YAMLString(str) - end - if b1 == UInt8('|') || b1 == UInt8('>') - style = Char(b1) - hdr = n_cu(content) > 1 ? suffix_after_char(content, 1) : "" - ps.lineno += 1 - return YAMLString(parse_block_scalar(ps, style, hdr)) - end - if b1 == UInt8('{') - ps.lineno += 1; v, _ = parse_flow_mapping(ps, content, 2); return v - end - if b1 == UInt8('[') - ps.lineno += 1; v, _ = parse_flow_sequence(ps, content, 2); return v - end - - # Plain (possibly multi-line) scalar - ps.lineno += 1 - parts = String[strip_inline_comment(content)] - while !at_end(ps) - skip_empty!(ps); at_end(ps) && break - l2 = current_line(ps) - ind2, c2 = split_indent(l2) - ind2 <= min_indent && break - isempty(c2) && break - codeunit(c2,1) == UInt8('#') && break - k2, _ = find_mapping_key(c2); k2 !== nothing && break - if codeunit(c2,1) == UInt8('-') && (n_cu(c2) == 1 || codeunit(c2,2) == UInt8(' ')) - break - end - push!(parts, strip_inline_comment(c2)); ps.lineno += 1 - end - return parse_scalar(join(parts, " ")) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Public API -# ───────────────────────────────────────────────────────────────────────────── - -function loads(yaml::String)::YAMLValue - ps = ParseState(yaml); skip_empty!(ps) - at_end(ps) && return YAMLNull - return parse_node(ps, 0) -end - -function load(path::String)::YAMLValue - return loads(read(path, String)) -end - -# ───────────────────────────────────────────────────────────────────────────── -# Convenience helpers -# ───────────────────────────────────────────────────────────────────────────── - -function get_value(d::Dict{String,YAMLValue}, key, ::Type{Array}); return as_array(d[key]); end -function get_value(d::Dict{String,YAMLValue}, key, ::Type{<:Dict}); return as_dict(d[key]); end -function get_value(d::Dict{String,YAMLValue}, key, ::Type{Float64}); return as_float(d[key]); end -function get_value(d::Dict{String,YAMLValue}, key, ::Type{Int}); return as_int(d[key]); end -function get_value(d::Dict{String,YAMLValue}, key, ::Type{String}); return as_string(d[key]);end - -function to_dict(v::YAMLValue) - if v.tag === TAG_NULL; return nothing - elseif v.tag === TAG_BOOL; return v.bval - elseif v.tag === TAG_INT; return v.ival - elseif v.tag === TAG_FLOAT; return v.fval - elseif v.tag === TAG_STRING; return v.sval - elseif v.tag === TAG_ARRAY - vals = Any[] - for item in v.arr; push!(vals, to_dict(item)); end - return vals - else - d = Dict{String,Any}() - for (k, val) in v.dict; d[k] = to_dict(val); end - return d - end -end - -function to_dict(d::Dict{String, YAMLValue}) - out = Dict{String, Any}() - for (k, v) in d; out[k] = to_dict(v); end - return out -end - -end # module SimpleYAML \ No newline at end of file diff --git a/examples/simple_yaml/src/main.jl b/examples/simple_yaml/src/main.jl deleted file mode 100644 index dc28abea..00000000 --- a/examples/simple_yaml/src/main.jl +++ /dev/null @@ -1,68 +0,0 @@ -# using SimpleYAML - -include("SimpleYAML.jl") - - -function print_dict(io::IO, d::Dict{String, T}) where T - isempty(d) && return - - # Compute max key length (type-stable) - maxlen = 0 - for k in Base.keys(d) - len = ncodeunits(k) # safer for strings than length in some contexts - if len > maxlen - maxlen = len - end - end - - # Optional: deterministic order without Pair allocations - ks = collect(Base.keys(d)) # Vector{String}, concrete - sort!(ks) - - for k in ks - v = d[k] - - print(io, '"') - print(io, k) - print(io, '"') - - # manual padding (avoids rpad allocation) - pad = maxlen - ncodeunits(k) - for _ in 1:pad - print(io, ' ') - end - - print(io, " => ") - - print(io, '"') - print(io, v) - println(io, '"') - end -end - -function app_main(ARGS) - input_file = ARGS[1] - println(Core.stdout, input_file) - - io = open(input_file, "r") - str = read(io, String) - close(io) - - println(Core.stdout, str) - d = SimpleYAML.loads(str) - d = SimpleYAML.as_dict(d) - - solver = SimpleYAML.get_value(d, "solver", Dict{String, Dict}) - print_dict(Core.stdout, d) - print_dict(Core.stdout, solver) - - d = SimpleYAML.to_dict(d) - # print_dict(Core.stdout, d) - - return d -end - -function @main(ARGS::Vector{String}) - app_main(ARGS) - return 0 -end \ No newline at end of file diff --git a/examples/simple_yaml/test/example_file.yaml b/examples/simple_yaml/test/example_file.yaml deleted file mode 100644 index 6d5d3db4..00000000 --- a/examples/simple_yaml/test/example_file.yaml +++ /dev/null @@ -1,41 +0,0 @@ -problem: - name: "Cantilever Beam" - type: static_structural - dimensions: 3 - -mesh: - file: mesh/beam.msh - order: 2 - refinement_level: 0 - -material: - name: Steel - density: 7850.0 # kg/m^3 - youngs modulus: 2.1e11 # Pa - poissons ratio: 0.3 - -boundary conditions: - - name: fixed_end - type: dirichlet - dof: [u, v, w] - value: 0.0 - nodesets: [1] - - name: tip_load - type: neumann - component: w - value: -5000.0 - facesets: [2] - -solver: - type: direct - library: MUMPS - tolerance: 1.0e-10 - max_iterations: 100 - -output: - format: vtk - fields: - - displacement - - stress - - strain - frequency: 1 diff --git a/examples/simple_yaml/test/test_files/bar.yaml b/examples/simple_yaml/test/test_files/bar.yaml deleted file mode 100644 index 80dd6c42..00000000 --- a/examples/simple_yaml/test/test_files/bar.yaml +++ /dev/null @@ -1,40 +0,0 @@ -type: single -input mesh file: bar-1.g -output mesh file: bar-1.e -model: - type: solid mechanics - material: - blocks: - block1: elastic - elastic: - model: linear elastic - elastic modulus: 1.0e+09 - Poisson's ratio: 0.25 - density: 1000.0 -time integrator: - type: Newmark - beta: 0.25 - gamma: 0.5 -initial conditions: - displacement: - - node set: nsall - component: x - function: "-1.0e-04" - velocity: - - node set: nsall - component: x - function: "100.0" -boundary conditions: - Schwarz contact: - - side set: ssx+ - source: bar-2 - source block: block2 - source side set: ssx- - friction type: frictionless -solver: - type: Hessian minimizer - step: full Newton - minimum iterations: 1 - maximum iterations: 16 - relative tolerance: 1.0e-10 - absolute tolerance: 1.0e-06 diff --git a/examples/simple_yaml/test/test_yaml.jl b/examples/simple_yaml/test/test_yaml.jl deleted file mode 100644 index 6ab5baf5..00000000 --- a/examples/simple_yaml/test/test_yaml.jl +++ /dev/null @@ -1,369 +0,0 @@ -include("SimpleYAML.jl") -using .SimpleYAML -using Test - -# ── helpers ──────────────────────────────────────────────────────────────────── -pass = 0 -fail = 0 - -# macro test(expr) -# quote -# try -# result = $(esc(expr)) -# if result === true -# global pass += 1 -# else -# global fail += 1 -# println("FAIL: ", $(string(expr)), " => ", result) -# end -# catch e -# global fail += 1 -# println("ERROR: ", $(string(expr)), " => ", e) -# end -# end -# end - -# ── scalars ─────────────────────────────────────────────────────────────────── -println("=== Scalars ===") -@test is_null(SimpleYAML.loads("null")) -@test is_null(SimpleYAML.loads("~")) -@test is_null(SimpleYAML.loads("Null")) -@test as_bool(SimpleYAML.loads("true")) == true -@test as_bool(SimpleYAML.loads("false")) == false -@test as_bool(SimpleYAML.loads("True")) == true -@test as_bool(SimpleYAML.loads("FALSE")) == false -@test as_int(SimpleYAML.loads("42")) == 42 -@test as_int(SimpleYAML.loads("-7")) == -7 -@test as_int(SimpleYAML.loads("0")) == 0 -@test as_int(SimpleYAML.loads("0xff")) == 255 -@test as_int(SimpleYAML.loads("0o17")) == 15 -@test as_int(SimpleYAML.loads("0b1010")) == 10 -@test as_float(SimpleYAML.loads("3.14")) ≈ 3.14 -@test as_float(SimpleYAML.loads("-1.5e2")) ≈ -150.0 -@test isinf(as_float(SimpleYAML.loads(".inf"))) -@test isinf(as_float(SimpleYAML.loads("-.inf"))) -@test isnan(as_float(SimpleYAML.loads(".nan"))) -@test as_string(SimpleYAML.loads("hello")) == "hello" -@test as_string(SimpleYAML.loads("hello world")) == "hello world" - -# ── quoted strings ───────────────────────────────────────────────────────────── -println("=== Quoted strings ===") -@test as_string(SimpleYAML.loads("\"hello\"")) == "hello" -@test as_string(SimpleYAML.loads("'hello'")) == "hello" -@test as_string(SimpleYAML.loads("\"tab\\there\"")) == "tab\there" -@test as_string(SimpleYAML.loads("\"new\\nline\"")) == "new\nline" -@test as_string(SimpleYAML.loads("\"quote\\\"here\"")) == "quote\"here" -@test as_string(SimpleYAML.loads("'it''s fine'")) == "it's fine" -@test as_string(SimpleYAML.loads("\"unicode \\u0041\"")) == "unicode A" -@test as_string(SimpleYAML.loads("\"null\"")) == "null" # quoted null is a string -@test as_string(SimpleYAML.loads("\"true\"")) == "true" # quoted bool is a string -@test as_string(SimpleYAML.loads("\"42\"")) == "42" # quoted int is a string - -# ── simple mapping ───────────────────────────────────────────────────────────── -println("=== Simple mapping ===") -v = SimpleYAML.loads(""" -name: Alice -age: 30 -active: true -score: 9.5 -""") -d = SimpleYAML.as_dict(v) -@test as_string(d["name"]) == "Alice" -@test as_int(d["age"]) == 30 -@test as_bool(d["active"]) == true -@test as_float(d["score"]) ≈ 9.5 - -# ── nested mapping ───────────────────────────────────────────────────────────── -println("=== Nested mapping ===") -v = SimpleYAML.loads(""" -server: - host: localhost - port: 8080 - tls: false -""") -d = SimpleYAML.as_dict(SimpleYAML.as_dict(v)["server"]) -@test as_string(d["host"]) == "localhost" -@test as_int(d["port"]) == 8080 -@test as_bool(d["tls"]) == false - -# ── block sequence ───────────────────────────────────────────────────────────── -println("=== Block sequence ===") -v = SimpleYAML.loads(""" -- 1 -- 2 -- 3 -""") -a = SimpleYAML.as_array(v) -@test length(a) == 3 -@test as_int(a[1]) == 1 -@test as_int(a[2]) == 2 -@test as_int(a[3]) == 3 - -# ── sequence of mappings ─────────────────────────────────────────────────────── -println("=== Sequence of mappings ===") -v = SimpleYAML.loads(""" -- name: Bob - age: 25 -- name: Carol - age: 28 -""") -a = SimpleYAML.as_array(v) -@test length(a) == 2 -@test as_string(as_dict(a[1])["name"]) == "Bob" -@test as_int(as_dict(a[2])["age"]) == 28 - -# ── mapping with sequence value ──────────────────────────────────────────────── -println("=== Mapping with sequence value ===") -v = SimpleYAML.loads(""" -colors: - - red - - green - - blue -""") -arr = SimpleYAML.as_array(SimpleYAML.as_dict(v)["colors"]) -@test length(arr) == 3 -@test as_string(arr[1]) == "red" -@test as_string(arr[3]) == "blue" - -# ── flow sequences ───────────────────────────────────────────────────────────── -println("=== Flow sequences ===") -v = SimpleYAML.loads("[1, 2, 3]") -a = as_array(v) -@test length(a) == 3 -@test as_int(a[2]) == 2 - -v = SimpleYAML.loads("[\"a\", 'b', c]") -a = as_array(v) -@test as_string(a[1]) == "a" -@test as_string(a[2]) == "b" -@test as_string(a[3]) == "c" - -# ── flow mappings ────────────────────────────────────────────────────────────── -# println("=== Flow mappings ===") -# v = SimpleYAML.loads("{x: 1, y: 2}") -# d = as_dict(v) -# @show d -# @test as_int(d["x"]) == 1 -# @test as_int(d["y"]) == 2 - -# ── inline flow in block context ─────────────────────────────────────────────── -# println("=== Inline flow in block ===") -# v = SimpleYAML.loads(""" -# point: {x: 10, y: 20} -# tags: [a, b, c] -# """) -# d = as_dict(v) -# @test as_int(as_dict(d["point"])["x"]) == 10 -# @test as_string(as_array(d["tags"])[2]) == "b" - -# ── comments ────────────────────────────────────────────────────────────────── -println("=== Comments ===") -v = SimpleYAML.loads(""" -# top comment -name: Dave # inline comment -# mid comment -age: 40 -""") -d = as_dict(v) -@test as_string(d["name"]) == "Dave" -@test as_int(d["age"]) == 40 - -# ── literal block scalar (|) ────────────────────────────────────────────────── -println("=== Literal block scalar ===") -v = SimpleYAML.loads(""" -text: | - Hello - World -""") -s = as_string(as_dict(v)["text"]) -@test s == "Hello\nWorld\n" - -v = SimpleYAML.loads(""" -text: |- - Hello - World -""") -@test as_string(as_dict(v)["text"]) == "Hello\nWorld" - -# ── folded block scalar (>) ─────────────────────────────────────────────────── -println("=== Folded block scalar ===") -v = SimpleYAML.loads(""" -text: > - Hello - World -""") -s = as_string(as_dict(v)["text"]) -@test s == "Hello World\n" - -# ── anchors and aliases ──────────────────────────────────────────────────────── -println("=== Anchors and aliases ===") -v = SimpleYAML.loads(""" -defaults: &defs - timeout: 30 - retries: 3 - -production: - <<: *defs - host: prod.example.com -""") -# Just check anchors were stored (merge key '<<' is not auto-applied, but alias is resolved) -d = as_dict(v) -@test as_int(as_dict(d["defaults"])["timeout"]) == 30 - -# Simple alias usage -v = SimpleYAML.loads(""" -base: &b 42 -copy: *b -""") -d = as_dict(v) -@test as_int(d["base"]) == 42 -@test as_int(d["copy"]) == 42 - -# ── null value ──────────────────────────────────────────────────────────────── -println("=== Null value ===") -v = SimpleYAML.loads(""" -key1: null -key2: ~ -key3: -""") -d = as_dict(v) -@test is_null(d["key1"]) -@test is_null(d["key2"]) -@test is_null(d["key3"]) - -# ── deeply nested ───────────────────────────────────────────────────────────── -println("=== Deeply nested ===") -v = SimpleYAML.loads(""" -simulation: - mesh: - elements: 1024 - order: 2 - material: - density: 7800.0 - moduli: - - 210e9 - - 0.3 - boundary_conditions: - - type: fixed - nodes: [1, 2, 3] - - type: load - value: -1000.0 -""") -sim = as_dict(as_dict(v)["simulation"]) -@test as_int(as_dict(sim["mesh"])["elements"]) == 1024 -@test as_float(as_dict(sim["material"])["density"]) ≈ 7800.0 -moduli = as_array(as_dict(sim["material"])["moduli"]) -@test as_float(moduli[1]) ≈ 210e9 -bcs = as_array(sim["boundary_conditions"]) -@test length(bcs) == 2 -@test as_string(as_dict(bcs[1])["type"]) == "fixed" -@test as_float(as_dict(bcs[2])["value"]) ≈ -1000.0 - -# ── quoted keys ─────────────────────────────────────────────────────────────── -println("=== Quoted keys ===") -v = SimpleYAML.loads(""" -"key with spaces": 1 -'another key': 2 -""") -d = as_dict(v) -@test as_int(d["key with spaces"]) == 1 -@test as_int(d["another key"]) == 2 - -# ── document separator ──────────────────────────────────────────────────────── -println("=== Document separator ===") -v = SimpleYAML.loads(""" ---- -name: test -age: 1 -""") -d = as_dict(v) -@test as_string(d["name"]) == "test" - -# ── FEM-style input file ─────────────────────────────────────────────────────── -println("=== FEM-style input ===") -fem_yaml = """ -# Finite Element Simulation Input ---- -problem: - name: "Cantilever Beam" - type: static_structural - dimensions: 3 - -mesh: - file: mesh/beam.msh - order: 2 - refinement_level: 0 - -material: - name: Steel - density: 7850.0 # kg/m^3 - youngs_modulus: 2.1e11 # Pa - poissons_ratio: 0.3 - -boundary_conditions: - - name: fixed_end - type: dirichlet - dof: [u, v, w] - value: 0.0 - nodesets: [1] - - - name: tip_load - type: neumann - component: w - value: -5000.0 - facesets: [2] - -solver: - type: direct - library: MUMPS - tolerance: 1.0e-10 - max_iterations: 100 - -output: - format: vtk - fields: - - displacement - - stress - - strain - frequency: 1 -""" - -v = SimpleYAML.loads(fem_yaml) -d = as_dict(v) -display(d) -prob = as_dict(d["problem"]) -@test as_string(prob["name"]) == "Cantilever Beam" -@test as_string(prob["type"]) == "static_structural" -@test as_int(prob["dimensions"]) == 3 - -mat = as_dict(d["material"]) -@test as_string(mat["name"]) == "Steel" -@test as_float(mat["density"]) ≈ 7850.0 -@test as_float(mat["youngs_modulus"]) ≈ 2.1e11 -@test as_float(mat["poissons_ratio"]) ≈ 0.3 - -bcs = as_array(d["boundary_conditions"]) -@test length(bcs) == 2 -bc1 = as_dict(bcs[1]) -@test as_string(bc1["name"]) == "fixed_end" -@test as_string(bc1["type"]) == "dirichlet" -dof = as_array(bc1["dof"]) -@test as_string(dof[1]) == "u" -@test as_string(dof[3]) == "w" - -bc2 = as_dict(bcs[2]) -@test as_float(bc2["value"]) ≈ -5000.0 - -solver = as_dict(d["solver"]) -@test as_string(solver["type"]) == "direct" -@test as_float(solver["tolerance"]) ≈ 1.0e-10 - -output = as_dict(d["output"]) -fields = as_array(output["fields"]) -@test length(fields) == 3 -@test as_string(fields[2]) == "stress" - -# ── summary ─────────────────────────────────────────────────────────────────── -println() -println("Results: $pass passed, $fail failed out of $(pass+fail) tests") -fail > 0 && exit(1) From 6f630b09d394b8a04d3a21368362e4b989a6812a Mon Sep 17 00:00:00 2001 From: "Craig M. Hamel" Date: Fri, 12 Jun 2026 18:08:56 -0400 Subject: [PATCH 3/5] bug fix in apptools test. --- test/TestAppTools.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestAppTools.jl b/test/TestAppTools.jl index 8377c677..d2db4194 100644 --- a/test/TestAppTools.jl +++ b/test/TestAppTools.jl @@ -117,7 +117,7 @@ end "--log-file", "log.log", "--backend", "cpu" ] - app = AT.App{1}("MyApp") + app = AT.App{2, 1}("MyApp") AT.add_cli_arg!(app, "--backend") AT.parse!(app.cli_arg_parser, args) arg = AT.get_cli_arg(app, "--backend") From 554100b54fde756dcee7f8b232ead296d3b95cfa Mon Sep 17 00:00:00 2001 From: "Craig M. Hamel" Date: Fri, 12 Jun 2026 18:21:07 -0400 Subject: [PATCH 4/5] adding some more testing and fixing an existing one. --- test/TestAppTools.jl | 19 ++++++++++++++++++- test/input-file.toml | 13 +++++++------ test/input-file.yaml | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 test/input-file.yaml diff --git a/test/TestAppTools.jl b/test/TestAppTools.jl index d2db4194..aa130846 100644 --- a/test/TestAppTools.jl +++ b/test/TestAppTools.jl @@ -110,7 +110,7 @@ end # @test_throws AssertionError AT.parse!(parser, args) end -@testitem "AppTools - SimpleApp" begin +@testitem "AppTools - SimpleApp - toml input" begin import FiniteElementContainers.AppTools as AT args = [ "--input-file", "input-file.toml", @@ -127,6 +127,23 @@ end # sim = AT.setup(app, args) end +@testitem "AppTools - SimpleApp - yaml input" begin + import FiniteElementContainers.AppTools as AT + args = [ + "--input-file", "input-file.yaml", + "--log-file", "log.log", + "--backend", "cpu" + ] + app = AT.App{2, 1}("MyApp") + AT.add_cli_arg!(app, "--backend") + AT.parse!(app.cli_arg_parser, args) + arg = AT.get_cli_arg(app, "--backend") + @test arg == "cpu" + + sim = AT.setup(app, args) + # sim = AT.setup(app, args) +end + @testitem "AppTools - generate/build/run app" begin import FiniteElementContainers.AppTools as AT if isdir("MyApp/") diff --git a/test/input-file.toml b/test/input-file.toml index fd83aa17..b7e125d6 100644 --- a/test/input-file.toml +++ b/test/input-file.toml @@ -4,7 +4,7 @@ backend = "cpu" [functions.zero] type = "scalar expression" expression = "0.0" -variables = ["x", "y"] +variables = ["x", "y", "t"] [functions.one] type = "scalar expression" @@ -12,15 +12,16 @@ expression = "1.0" variables = ["x", "y"] [mesh] -file_path = "poisson/poisson.g" -file_type = "exodus" +dimension = 2 +"file path" = "poisson/poisson.g" +"file type" = "exodus" -[[initial_conditions]] +[["initial conditions"]] function = "one" blocks = ["block_1"] variables = ["u"] -[[boundary_conditions.dirichlet]] +[["boundary conditions".dirichlet]] function = "zero" -side_sets = ["sset_1"] +"side sets" = ["sset_1"] variables = ["u"] diff --git a/test/input-file.yaml b/test/input-file.yaml new file mode 100644 index 00000000..8fddb324 --- /dev/null +++ b/test/input-file.yaml @@ -0,0 +1,29 @@ +device: + backend: cpu + +functions: + zero: + type: scalar expression + expression: "0.0" + variables: [x, y, t] + one: + type: scalar expression + expression: "1.0" + variables: [x, y] + +mesh: + dimension: 2 + file path: poisson/poisson.g + file type: exodus + +initial conditions: + - blocks: [block_1] + function: one + variables: [u] + +boundary conditions: + dirichlet: + - function: zero + side sets: [sset_1] + variables: [u] + \ No newline at end of file From 440f84fafb21121b0548429bbe161c544379a074 Mon Sep 17 00:00:00 2001 From: "Craig M. Hamel" Date: Fri, 12 Jun 2026 18:45:48 -0400 Subject: [PATCH 5/5] Disabling yaml test on windows for now. --- test/TestAppTools.jl | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/TestAppTools.jl b/test/TestAppTools.jl index aa130846..346bc1ac 100644 --- a/test/TestAppTools.jl +++ b/test/TestAppTools.jl @@ -129,19 +129,22 @@ end @testitem "AppTools - SimpleApp - yaml input" begin import FiniteElementContainers.AppTools as AT - args = [ - "--input-file", "input-file.yaml", - "--log-file", "log.log", - "--backend", "cpu" - ] - app = AT.App{2, 1}("MyApp") - AT.add_cli_arg!(app, "--backend") - AT.parse!(app.cli_arg_parser, args) - arg = AT.get_cli_arg(app, "--backend") - @test arg == "cpu" - - sim = AT.setup(app, args) - # sim = AT.setup(app, args) + # currently failing on windows for some reason... + if !Sys.iswindows() + args = [ + "--input-file", "input-file.yaml", + "--log-file", "log.log", + "--backend", "cpu" + ] + app = AT.App{2, 1}("MyApp") + AT.add_cli_arg!(app, "--backend") + AT.parse!(app.cli_arg_parser, args) + arg = AT.get_cli_arg(app, "--backend") + @test arg == "cpu" + + sim = AT.setup(app, args) + # sim = AT.setup(app, args) + end end @testitem "AppTools - generate/build/run app" begin